Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for FlutterIOSDriver #2206

Merged
merged 14 commits into from
Jul 24, 2024
25 changes: 17 additions & 8 deletions .github/workflows/gradle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ env:
IOS_DEVICE_NAME: iPhone 15
IOS_PLATFORM_VERSION: "17.5"
FLUTTER_ANDROID_APP: "https://github.com/AppiumTestDistribution/appium-flutter-server/releases/latest/download/app-debug.apk"
FLUTTER_IOS_APP: "https://github.com/AppiumTestDistribution/appium-flutter-server/releases/latest/download/ios.zip"

jobs:
build:
Expand All @@ -38,6 +39,10 @@ jobs:
# Need to use specific (not `-latest`) version of macOS to be sure the required version of Xcode/simulator is available
platform: macos-14
e2e-tests: ios
- java: 17
# Need to use specific (not `-latest`) version of macOS to be sure the required version of Xcode/simulator is available
platform: macos-14
e2e-tests: flutter-ios
- java: 17
platform: ubuntu-latest
e2e-tests: android
Expand Down Expand Up @@ -71,27 +76,27 @@ jobs:
- name: Build with Gradle
run: |
latest_snapshot=$(curl -sf https://oss.sonatype.org/content/repositories/snapshots/org/seleniumhq/selenium/selenium-api/ | \
python -c "import sys,re; print(re.findall(r'\d+\.\d+\.\d+-SNAPSHOT', sys.stdin.read())[-1])")
python -c "import sys,re; print(re.findall(r'\d+\.\d+\.\d+-SNAPSHOT', sys.stdin.read())[-1])")
echo ">>> $latest_snapshot"
echo "latest_snapshot=$latest_snapshot" >> "$GITHUB_ENV"
./gradlew clean build -PisCI -Pselenium.version=$latest_snapshot

- name: Install Node.js
if: matrix.e2e-tests == 'android' || matrix.e2e-tests == 'ios' || matrix.e2e-tests == 'flutter-android'
if: ${{ matrix.e2e-tests }}
uses: actions/setup-node@v4
with:
node-version: 'lts/*'

- name: Install Appium
if: matrix.e2e-tests == 'android' || matrix.e2e-tests == 'ios' || matrix.e2e-tests == 'flutter-android'
if: ${{ matrix.e2e-tests }}
run: npm install --location=global appium

- name: Install UIA2 driver
if: matrix.e2e-tests == 'android' || matrix.e2e-tests == 'flutter-android'
run: appium driver install uiautomator2

- name: Install Flutter Integration driver
if: matrix.e2e-tests == 'flutter-android'
if: matrix.e2e-tests == 'flutter-android' || matrix.e2e-tests == 'flutter-ios'
run: appium driver install appium-flutter-integration-driver --source npm

- name: Run Android E2E tests
Expand All @@ -117,22 +122,26 @@ jobs:
target: ${{ env.ANDROID_EMU_TARGET }}

- name: Select Xcode
if: matrix.e2e-tests == 'ios'
if: matrix.e2e-tests == 'ios' || matrix.e2e-tests == 'flutter-ios'
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: "${{ env.XCODE_VERSION }}"
- name: Prepare iOS simulator
if: matrix.e2e-tests == 'ios'
if: matrix.e2e-tests == 'ios' || matrix.e2e-tests == 'flutter-ios'
uses: futureware-tech/simulator-action@v3
with:
model: "${{ env.IOS_DEVICE_NAME }}"
os_version: "${{ env.IOS_PLATFORM_VERSION }}"
- name: Install XCUITest driver
if: matrix.e2e-tests == 'ios'
if: matrix.e2e-tests == 'ios' || matrix.e2e-tests == 'flutter-ios'
run: appium driver install xcuitest
- name: Prebuild XCUITest driver
if: matrix.e2e-tests == 'ios'
if: matrix.e2e-tests == 'ios' || matrix.e2e-tests == 'flutter-ios'
run: appium driver run xcuitest build-wda
- name: Run iOS E2E tests
if: matrix.e2e-tests == 'ios'
run: ./gradlew e2eIosTest -PisCI -Pselenium.version=$latest_snapshot

- name: Run Flutter iOS E2E tests
if: matrix.e2e-tests == 'flutter-ios'
run: ./gradlew e2eFlutterTest -Pplatform="ios" -Pselenium.version=$latest_snapshot -PisCI -PflutterApp=${{ env.FLUTTER_IOS_APP }}
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,23 @@

import io.appium.java_client.AppiumBy;
import io.appium.java_client.android.options.UiAutomator2Options;
import io.appium.java_client.flutter.FlutterDriverOptions;
import io.appium.java_client.flutter.FlutterIntegrationTestDriver;
import io.appium.java_client.flutter.android.FlutterAndroidDriver;
import io.appium.java_client.flutter.commands.ScrollParameter;
import io.appium.java_client.remote.AutomationName;
import io.appium.java_client.flutter.ios.FlutterIOSDriver;
import io.appium.java_client.ios.options.XCUITestOptions;
import io.appium.java_client.service.local.AppiumDriverLocalService;
import io.appium.java_client.service.local.AppiumServiceBuilder;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.openqa.selenium.By;
import org.openqa.selenium.InvalidArgumentException;
import org.openqa.selenium.WebElement;

import java.net.MalformedURLException;
import java.time.Duration;
import java.util.Optional;

class BaseFlutterTest {
Expand All @@ -29,7 +32,7 @@ class BaseFlutterTest {
protected static final int PORT = 4723;

private static AppiumDriverLocalService service;
protected static FlutterAndroidDriver driver;
protected static FlutterIntegrationTestDriver driver;
protected static final By LOGIN_BUTTON = AppiumBy.flutterText("Login");

/**
Expand All @@ -45,35 +48,52 @@ public static void beforeClass() {
}

@BeforeEach
public void startSession() throws MalformedURLException {
void startSession() throws MalformedURLException {
FlutterDriverOptions flutterOptions = new FlutterDriverOptions()
.setFlutterServerLaunchTimeout(Duration.ofMinutes(2))
.setFlutterSystemPort(9999)
.setFlutterElementWaitTimeout(Duration.ofSeconds(10));
if (IS_ANDROID) {
// TODO: update it with FlutterDriverOptions once implemented
UiAutomator2Options options = new UiAutomator2Options()
.setAutomationName(AutomationName.FLUTTER_INTEGRATION)
.setApp(System.getProperty("flutterApp"))
.eventTimings();
driver = new FlutterAndroidDriver(service.getUrl(), options);
driver = new FlutterAndroidDriver(service.getUrl(), flutterOptions
.setUiAutomator2Options(new UiAutomator2Options()
.setApp(System.getProperty("flutterApp"))
.eventTimings())
);
} else {
throw new InvalidArgumentException(
"Currently flutter driver implementation only supports android platform");
String deviceName = System.getenv("IOS_DEVICE_NAME") != null
? System.getenv("IOS_DEVICE_NAME")
: "iPhone 12";
String platformVersion = System.getenv("IOS_PLATFORM_VERSION") != null
? System.getenv("IOS_PLATFORM_VERSION")
: "14.5";
driver = new FlutterIOSDriver(service.getUrl(), flutterOptions
.setXCUITestOptions(new XCUITestOptions()
.setApp(System.getProperty("flutterApp"))
.setDeviceName(deviceName)
.setPlatformVersion(platformVersion)
.setWdaLaunchTimeout(Duration.ofMinutes(4))
.setSimulatorStartupTimeout(Duration.ofMinutes(5))
.eventTimings()
)
);
}
}

@AfterEach
public void stopSession() {
void stopSession() {
if (driver != null) {
driver.quit();
}
}

@AfterAll
public static void afterClass() {
static void afterClass() {
if (service.isRunning()) {
service.stop();
}
}

public void openScreen(String screenTitle) {
void openScreen(String screenTitle) {
ScrollParameter scrollOptions = new ScrollParameter(
AppiumBy.flutterText(screenTitle), ScrollParameter.ScrollDirection.DOWN);
WebElement element = driver.scrollTillVisible(scrollOptions);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package io.appium.java_client.android;

import io.appium.java_client.AppiumBy;
import io.appium.java_client.flutter.commands.DoubleClickParameter;
import io.appium.java_client.flutter.commands.DragAndDropParameter;
import io.appium.java_client.flutter.commands.LongPressParameter;
import io.appium.java_client.flutter.commands.ScrollParameter;
import io.appium.java_client.flutter.commands.WaitParameter;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.Point;
import org.openqa.selenium.WebElement;

import static org.junit.jupiter.api.Assertions.assertEquals;
Expand All @@ -16,7 +20,7 @@ class CommandTest extends BaseFlutterTest {
private static final AppiumBy.FlutterBy TOGGLE_BUTTON = AppiumBy.flutterKey("toggle_button");

@Test
public void testWaitCommand() {
void testWaitCommand() {
WebElement loginButton = driver.findElement(BaseFlutterTest.LOGIN_BUTTON);
loginButton.click();
openScreen("Lazy Loading");
Expand All @@ -39,7 +43,7 @@ public void testWaitCommand() {
}

@Test
public void testScrollTillVisibleCommand() {
void testScrollTillVisibleCommand() {
WebElement loginButton = driver.findElement(BaseFlutterTest.LOGIN_BUTTON);
loginButton.click();
openScreen("Vertical Swiping");
Expand All @@ -59,4 +63,56 @@ public void testScrollTillVisibleCommand() {
assertFalse(Boolean.parseBoolean(lastElement.getAttribute("displayed")));
}

@Test
void testDoubleClickCommand() {
driver.findElement(BaseFlutterTest.LOGIN_BUTTON).click();
openScreen("Double Tap");

WebElement doubleTapButton = driver
.findElement(AppiumBy.flutterKey("double_tap_button"))
.findElement(AppiumBy.flutterText("Double Tap"));
assertEquals("Double Tap", doubleTapButton.getText());

AppiumBy.FlutterBy okButton = AppiumBy.flutterText("Ok");
AppiumBy.FlutterBy successPopup = AppiumBy.flutterTextContaining("Successful");

driver.performDoubleClick(new DoubleClickParameter().setElement(doubleTapButton));
assertEquals(driver.findElement(successPopup).getText(), "Double Tap Successful");
driver.findElement(okButton).click();

driver.performDoubleClick(new DoubleClickParameter()
.setElement(doubleTapButton)
.setOffset(new Point(10, 2))
);
assertEquals(driver.findElement(successPopup).getText(), "Double Tap Successful");
driver.findElement(okButton).click();
}

@Test
void testLongPressCommand() {
driver.findElement(BaseFlutterTest.LOGIN_BUTTON).click();
openScreen("Long Press");

AppiumBy.FlutterBy successPopup = AppiumBy.flutterText("It was a long press");
WebElement longPressButton = driver
.findElement(AppiumBy.flutterKey("long_press_button"));

driver.performLongPress(new LongPressParameter().setElement(longPressButton));
assertEquals(driver.findElement(successPopup).getText(), "It was a long press");
assertTrue(driver.findElement(successPopup).isDisplayed());
}

@Test
void testDragAndDropCommand() {
driver.findElement(BaseFlutterTest.LOGIN_BUTTON).click();
openScreen("Drag & Drop");

driver.performDragAndDrop(new DragAndDropParameter(
driver.findElement(AppiumBy.flutterKey("drag_me")),
driver.findElement(AppiumBy.flutterKey("drop_zone"))
));
assertTrue(driver.findElement(AppiumBy.flutterText("The box is dropped")).isDisplayed());
assertEquals(driver.findElement(AppiumBy.flutterText("The box is dropped")).getText(), "The box is dropped");

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
class FinderTests extends BaseFlutterTest {

@Test
public void testFlutterByKey() {
void testFlutterByKey() {
WebElement userNameField = driver.findElement(AppiumBy.flutterKey("username_text_field"));
assertEquals("admin", userNameField.getText());
userNameField.clear();
Expand All @@ -19,13 +19,13 @@ public void testFlutterByKey() {
}

@Test
public void testFlutterByType() {
void testFlutterByType() {
WebElement loginButton = driver.findElement(AppiumBy.flutterType("ElevatedButton"));
assertEquals(loginButton.findElement(AppiumBy.flutterType("Text")).getText(), "Login");
}

@Test
public void testFlutterText() {
void testFlutterText() {
WebElement loginButton = driver.findElement(AppiumBy.flutterText("Login"));
assertEquals(loginButton.getText(), "Login");
loginButton.click();
Expand All @@ -34,15 +34,15 @@ public void testFlutterText() {
}

@Test
public void testFlutterTextContaining() {
void testFlutterTextContaining() {
WebElement loginButton = driver.findElement(BaseFlutterTest.LOGIN_BUTTON);
loginButton.click();
assertEquals(driver.findElement(AppiumBy.flutterTextContaining("Vertical")).getText(),
"Vertical Swiping");
}

@Test
public void testFlutterSemanticsLabel() {
void testFlutterSemanticsLabel() {
WebElement loginButton = driver.findElement(BaseFlutterTest.LOGIN_BUTTON);
loginButton.click();
openScreen("Lazy Loading");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package io.appium.java_client.flutter;

import io.appium.java_client.android.options.UiAutomator2Options;
import io.appium.java_client.flutter.options.SupportsFlutterElementWaitTimeoutOption;
import io.appium.java_client.flutter.options.SupportsFlutterServerLaunchTimeoutOption;
import io.appium.java_client.flutter.options.SupportsFlutterSystemPortOption;
import io.appium.java_client.ios.options.XCUITestOptions;
import io.appium.java_client.remote.AutomationName;
import io.appium.java_client.remote.options.BaseOptions;
import org.openqa.selenium.Capabilities;

import java.util.Map;

/**
* https://github.com/AppiumTestDistribution/appium-flutter-integration-driver#capabilities-for-appium-flutter-integration-driver
*/
public class FlutterDriverOptions extends BaseOptions<FlutterDriverOptions> implements
SupportsFlutterSystemPortOption<FlutterDriverOptions>,
SupportsFlutterServerLaunchTimeoutOption<FlutterDriverOptions>,
SupportsFlutterElementWaitTimeoutOption<FlutterDriverOptions> {

public FlutterDriverOptions() {
setDefaultOptions();
}

public FlutterDriverOptions(Capabilities source) {
super(source);
setDefaultOptions();
}

public FlutterDriverOptions(Map<String, ?> source) {
super(source);
setDefaultOptions();
}

public FlutterDriverOptions setUiAutomator2Options(UiAutomator2Options uiAutomator2Options) {
return setDefaultOptions(merge(uiAutomator2Options));
}

public FlutterDriverOptions setXCUITestOptions(XCUITestOptions xcuiTestOptions) {
return setDefaultOptions(merge(xcuiTestOptions));
}

private void setDefaultOptions() {
setDefaultOptions(this);
}

private FlutterDriverOptions setDefaultOptions(FlutterDriverOptions flutterDriverOptions) {
return flutterDriverOptions.setAutomationName(AutomationName.FLUTTER_INTEGRATION);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.appium.java_client.flutter;

import org.openqa.selenium.WebDriver;

/**
* The {@code FlutterDriver} interface represents a driver that controls interactions with
* Flutter applications, extending WebDriver and providing additional capabilities for
* interacting with Flutter-specific elements and behaviors.
*
* <p> This interface serves as a common entity for drivers that support Flutter applications
* on different platforms, such as Android and iOS. </p>
*
* @see WebDriver
* @see SupportsGestureOnFlutterElements
* @see SupportsScrollingOfFlutterElements
* @see SupportsWaitingForFlutterElements
*/
public interface FlutterIntegrationTestDriver extends
WebDriver,
SupportsGestureOnFlutterElements,
SupportsScrollingOfFlutterElements,
SupportsWaitingForFlutterElements {
}
Loading