Skip to content

Commit 6bd38a1

Browse files
feat: Add support for FlutterIOSDriver (appium#2206)
1 parent 62a3a6f commit 6bd38a1

17 files changed

+506
-39
lines changed

.github/workflows/gradle.yml

+17-8
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ env:
2727
IOS_DEVICE_NAME: iPhone 15
2828
IOS_PLATFORM_VERSION: "17.5"
2929
FLUTTER_ANDROID_APP: "https://github.com/AppiumTestDistribution/appium-flutter-server/releases/latest/download/app-debug.apk"
30+
FLUTTER_IOS_APP: "https://github.com/AppiumTestDistribution/appium-flutter-server/releases/latest/download/ios.zip"
3031

3132
jobs:
3233
build:
@@ -38,6 +39,10 @@ jobs:
3839
# Need to use specific (not `-latest`) version of macOS to be sure the required version of Xcode/simulator is available
3940
platform: macos-14
4041
e2e-tests: ios
42+
- java: 17
43+
# Need to use specific (not `-latest`) version of macOS to be sure the required version of Xcode/simulator is available
44+
platform: macos-14
45+
e2e-tests: flutter-ios
4146
- java: 17
4247
platform: ubuntu-latest
4348
e2e-tests: android
@@ -71,27 +76,27 @@ jobs:
7176
- name: Build with Gradle
7277
run: |
7378
latest_snapshot=$(curl -sf https://oss.sonatype.org/content/repositories/snapshots/org/seleniumhq/selenium/selenium-api/ | \
74-
python -c "import sys,re; print(re.findall(r'\d+\.\d+\.\d+-SNAPSHOT', sys.stdin.read())[-1])")
79+
python -c "import sys,re; print(re.findall(r'\d+\.\d+\.\d+-SNAPSHOT', sys.stdin.read())[-1])")
7580
echo ">>> $latest_snapshot"
7681
echo "latest_snapshot=$latest_snapshot" >> "$GITHUB_ENV"
7782
./gradlew clean build -PisCI -Pselenium.version=$latest_snapshot
7883
7984
- name: Install Node.js
80-
if: matrix.e2e-tests == 'android' || matrix.e2e-tests == 'ios' || matrix.e2e-tests == 'flutter-android'
85+
if: ${{ matrix.e2e-tests }}
8186
uses: actions/setup-node@v4
8287
with:
8388
node-version: 'lts/*'
8489

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

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

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

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

119124
- name: Select Xcode
120-
if: matrix.e2e-tests == 'ios'
125+
if: matrix.e2e-tests == 'ios' || matrix.e2e-tests == 'flutter-ios'
121126
uses: maxim-lobanov/setup-xcode@v1
122127
with:
123128
xcode-version: "${{ env.XCODE_VERSION }}"
124129
- name: Prepare iOS simulator
125-
if: matrix.e2e-tests == 'ios'
130+
if: matrix.e2e-tests == 'ios' || matrix.e2e-tests == 'flutter-ios'
126131
uses: futureware-tech/simulator-action@v3
127132
with:
128133
model: "${{ env.IOS_DEVICE_NAME }}"
129134
os_version: "${{ env.IOS_PLATFORM_VERSION }}"
130135
- name: Install XCUITest driver
131-
if: matrix.e2e-tests == 'ios'
136+
if: matrix.e2e-tests == 'ios' || matrix.e2e-tests == 'flutter-ios'
132137
run: appium driver install xcuitest
133138
- name: Prebuild XCUITest driver
134-
if: matrix.e2e-tests == 'ios'
139+
if: matrix.e2e-tests == 'ios' || matrix.e2e-tests == 'flutter-ios'
135140
run: appium driver run xcuitest build-wda
136141
- name: Run iOS E2E tests
137142
if: matrix.e2e-tests == 'ios'
138143
run: ./gradlew e2eIosTest -PisCI -Pselenium.version=$latest_snapshot
144+
145+
- name: Run Flutter iOS E2E tests
146+
if: matrix.e2e-tests == 'flutter-ios'
147+
run: ./gradlew e2eFlutterTest -Pplatform="ios" -Pselenium.version=$latest_snapshot -PisCI -PflutterApp=${{ env.FLUTTER_IOS_APP }}

src/e2eFlutterTest/java/io/appium/java_client/android/BaseFlutterTest.java

+35-15
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,23 @@
22

33
import io.appium.java_client.AppiumBy;
44
import io.appium.java_client.android.options.UiAutomator2Options;
5+
import io.appium.java_client.flutter.FlutterDriverOptions;
6+
import io.appium.java_client.flutter.FlutterIntegrationTestDriver;
57
import io.appium.java_client.flutter.android.FlutterAndroidDriver;
68
import io.appium.java_client.flutter.commands.ScrollParameter;
7-
import io.appium.java_client.remote.AutomationName;
9+
import io.appium.java_client.flutter.ios.FlutterIOSDriver;
10+
import io.appium.java_client.ios.options.XCUITestOptions;
811
import io.appium.java_client.service.local.AppiumDriverLocalService;
912
import io.appium.java_client.service.local.AppiumServiceBuilder;
1013
import org.junit.jupiter.api.AfterAll;
1114
import org.junit.jupiter.api.AfterEach;
1215
import org.junit.jupiter.api.BeforeAll;
1316
import org.junit.jupiter.api.BeforeEach;
1417
import org.openqa.selenium.By;
15-
import org.openqa.selenium.InvalidArgumentException;
1618
import org.openqa.selenium.WebElement;
1719

1820
import java.net.MalformedURLException;
21+
import java.time.Duration;
1922
import java.util.Optional;
2023

2124
class BaseFlutterTest {
@@ -29,7 +32,7 @@ class BaseFlutterTest {
2932
protected static final int PORT = 4723;
3033

3134
private static AppiumDriverLocalService service;
32-
protected static FlutterAndroidDriver driver;
35+
protected static FlutterIntegrationTestDriver driver;
3336
protected static final By LOGIN_BUTTON = AppiumBy.flutterText("Login");
3437

3538
/**
@@ -45,35 +48,52 @@ public static void beforeClass() {
4548
}
4649

4750
@BeforeEach
48-
public void startSession() throws MalformedURLException {
51+
void startSession() throws MalformedURLException {
52+
FlutterDriverOptions flutterOptions = new FlutterDriverOptions()
53+
.setFlutterServerLaunchTimeout(Duration.ofMinutes(2))
54+
.setFlutterSystemPort(9999)
55+
.setFlutterElementWaitTimeout(Duration.ofSeconds(10));
4956
if (IS_ANDROID) {
50-
// TODO: update it with FlutterDriverOptions once implemented
51-
UiAutomator2Options options = new UiAutomator2Options()
52-
.setAutomationName(AutomationName.FLUTTER_INTEGRATION)
53-
.setApp(System.getProperty("flutterApp"))
54-
.eventTimings();
55-
driver = new FlutterAndroidDriver(service.getUrl(), options);
57+
driver = new FlutterAndroidDriver(service.getUrl(), flutterOptions
58+
.setUiAutomator2Options(new UiAutomator2Options()
59+
.setApp(System.getProperty("flutterApp"))
60+
.eventTimings())
61+
);
5662
} else {
57-
throw new InvalidArgumentException(
58-
"Currently flutter driver implementation only supports android platform");
63+
String deviceName = System.getenv("IOS_DEVICE_NAME") != null
64+
? System.getenv("IOS_DEVICE_NAME")
65+
: "iPhone 12";
66+
String platformVersion = System.getenv("IOS_PLATFORM_VERSION") != null
67+
? System.getenv("IOS_PLATFORM_VERSION")
68+
: "14.5";
69+
driver = new FlutterIOSDriver(service.getUrl(), flutterOptions
70+
.setXCUITestOptions(new XCUITestOptions()
71+
.setApp(System.getProperty("flutterApp"))
72+
.setDeviceName(deviceName)
73+
.setPlatformVersion(platformVersion)
74+
.setWdaLaunchTimeout(Duration.ofMinutes(4))
75+
.setSimulatorStartupTimeout(Duration.ofMinutes(5))
76+
.eventTimings()
77+
)
78+
);
5979
}
6080
}
6181

6282
@AfterEach
63-
public void stopSession() {
83+
void stopSession() {
6484
if (driver != null) {
6585
driver.quit();
6686
}
6787
}
6888

6989
@AfterAll
70-
public static void afterClass() {
90+
static void afterClass() {
7191
if (service.isRunning()) {
7292
service.stop();
7393
}
7494
}
7595

76-
public void openScreen(String screenTitle) {
96+
void openScreen(String screenTitle) {
7797
ScrollParameter scrollOptions = new ScrollParameter(
7898
AppiumBy.flutterText(screenTitle), ScrollParameter.ScrollDirection.DOWN);
7999
WebElement element = driver.scrollTillVisible(scrollOptions);

src/e2eFlutterTest/java/io/appium/java_client/android/CommandTest.java

+58-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
package io.appium.java_client.android;
22

33
import io.appium.java_client.AppiumBy;
4+
import io.appium.java_client.flutter.commands.DoubleClickParameter;
5+
import io.appium.java_client.flutter.commands.DragAndDropParameter;
6+
import io.appium.java_client.flutter.commands.LongPressParameter;
47
import io.appium.java_client.flutter.commands.ScrollParameter;
58
import io.appium.java_client.flutter.commands.WaitParameter;
69
import org.junit.jupiter.api.Test;
10+
import org.openqa.selenium.Point;
711
import org.openqa.selenium.WebElement;
812

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

1822
@Test
19-
public void testWaitCommand() {
23+
void testWaitCommand() {
2024
WebElement loginButton = driver.findElement(BaseFlutterTest.LOGIN_BUTTON);
2125
loginButton.click();
2226
openScreen("Lazy Loading");
@@ -39,7 +43,7 @@ public void testWaitCommand() {
3943
}
4044

4145
@Test
42-
public void testScrollTillVisibleCommand() {
46+
void testScrollTillVisibleCommand() {
4347
WebElement loginButton = driver.findElement(BaseFlutterTest.LOGIN_BUTTON);
4448
loginButton.click();
4549
openScreen("Vertical Swiping");
@@ -59,4 +63,56 @@ public void testScrollTillVisibleCommand() {
5963
assertFalse(Boolean.parseBoolean(lastElement.getAttribute("displayed")));
6064
}
6165

66+
@Test
67+
void testDoubleClickCommand() {
68+
driver.findElement(BaseFlutterTest.LOGIN_BUTTON).click();
69+
openScreen("Double Tap");
70+
71+
WebElement doubleTapButton = driver
72+
.findElement(AppiumBy.flutterKey("double_tap_button"))
73+
.findElement(AppiumBy.flutterText("Double Tap"));
74+
assertEquals("Double Tap", doubleTapButton.getText());
75+
76+
AppiumBy.FlutterBy okButton = AppiumBy.flutterText("Ok");
77+
AppiumBy.FlutterBy successPopup = AppiumBy.flutterTextContaining("Successful");
78+
79+
driver.performDoubleClick(new DoubleClickParameter().setElement(doubleTapButton));
80+
assertEquals(driver.findElement(successPopup).getText(), "Double Tap Successful");
81+
driver.findElement(okButton).click();
82+
83+
driver.performDoubleClick(new DoubleClickParameter()
84+
.setElement(doubleTapButton)
85+
.setOffset(new Point(10, 2))
86+
);
87+
assertEquals(driver.findElement(successPopup).getText(), "Double Tap Successful");
88+
driver.findElement(okButton).click();
89+
}
90+
91+
@Test
92+
void testLongPressCommand() {
93+
driver.findElement(BaseFlutterTest.LOGIN_BUTTON).click();
94+
openScreen("Long Press");
95+
96+
AppiumBy.FlutterBy successPopup = AppiumBy.flutterText("It was a long press");
97+
WebElement longPressButton = driver
98+
.findElement(AppiumBy.flutterKey("long_press_button"));
99+
100+
driver.performLongPress(new LongPressParameter().setElement(longPressButton));
101+
assertEquals(driver.findElement(successPopup).getText(), "It was a long press");
102+
assertTrue(driver.findElement(successPopup).isDisplayed());
103+
}
104+
105+
@Test
106+
void testDragAndDropCommand() {
107+
driver.findElement(BaseFlutterTest.LOGIN_BUTTON).click();
108+
openScreen("Drag & Drop");
109+
110+
driver.performDragAndDrop(new DragAndDropParameter(
111+
driver.findElement(AppiumBy.flutterKey("drag_me")),
112+
driver.findElement(AppiumBy.flutterKey("drop_zone"))
113+
));
114+
assertTrue(driver.findElement(AppiumBy.flutterText("The box is dropped")).isDisplayed());
115+
assertEquals(driver.findElement(AppiumBy.flutterText("The box is dropped")).getText(), "The box is dropped");
116+
117+
}
62118
}

src/e2eFlutterTest/java/io/appium/java_client/android/FinderTests.java

+5-5
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
class FinderTests extends BaseFlutterTest {
1111

1212
@Test
13-
public void testFlutterByKey() {
13+
void testFlutterByKey() {
1414
WebElement userNameField = driver.findElement(AppiumBy.flutterKey("username_text_field"));
1515
assertEquals("admin", userNameField.getText());
1616
userNameField.clear();
@@ -19,13 +19,13 @@ public void testFlutterByKey() {
1919
}
2020

2121
@Test
22-
public void testFlutterByType() {
22+
void testFlutterByType() {
2323
WebElement loginButton = driver.findElement(AppiumBy.flutterType("ElevatedButton"));
2424
assertEquals(loginButton.findElement(AppiumBy.flutterType("Text")).getText(), "Login");
2525
}
2626

2727
@Test
28-
public void testFlutterText() {
28+
void testFlutterText() {
2929
WebElement loginButton = driver.findElement(AppiumBy.flutterText("Login"));
3030
assertEquals(loginButton.getText(), "Login");
3131
loginButton.click();
@@ -34,15 +34,15 @@ public void testFlutterText() {
3434
}
3535

3636
@Test
37-
public void testFlutterTextContaining() {
37+
void testFlutterTextContaining() {
3838
WebElement loginButton = driver.findElement(BaseFlutterTest.LOGIN_BUTTON);
3939
loginButton.click();
4040
assertEquals(driver.findElement(AppiumBy.flutterTextContaining("Vertical")).getText(),
4141
"Vertical Swiping");
4242
}
4343

4444
@Test
45-
public void testFlutterSemanticsLabel() {
45+
void testFlutterSemanticsLabel() {
4646
WebElement loginButton = driver.findElement(BaseFlutterTest.LOGIN_BUTTON);
4747
loginButton.click();
4848
openScreen("Lazy Loading");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package io.appium.java_client.flutter;
2+
3+
import io.appium.java_client.android.options.UiAutomator2Options;
4+
import io.appium.java_client.flutter.options.SupportsFlutterElementWaitTimeoutOption;
5+
import io.appium.java_client.flutter.options.SupportsFlutterServerLaunchTimeoutOption;
6+
import io.appium.java_client.flutter.options.SupportsFlutterSystemPortOption;
7+
import io.appium.java_client.ios.options.XCUITestOptions;
8+
import io.appium.java_client.remote.AutomationName;
9+
import io.appium.java_client.remote.options.BaseOptions;
10+
import org.openqa.selenium.Capabilities;
11+
12+
import java.util.Map;
13+
14+
/**
15+
* https://github.com/AppiumTestDistribution/appium-flutter-integration-driver#capabilities-for-appium-flutter-integration-driver
16+
*/
17+
public class FlutterDriverOptions extends BaseOptions<FlutterDriverOptions> implements
18+
SupportsFlutterSystemPortOption<FlutterDriverOptions>,
19+
SupportsFlutterServerLaunchTimeoutOption<FlutterDriverOptions>,
20+
SupportsFlutterElementWaitTimeoutOption<FlutterDriverOptions> {
21+
22+
public FlutterDriverOptions() {
23+
setDefaultOptions();
24+
}
25+
26+
public FlutterDriverOptions(Capabilities source) {
27+
super(source);
28+
setDefaultOptions();
29+
}
30+
31+
public FlutterDriverOptions(Map<String, ?> source) {
32+
super(source);
33+
setDefaultOptions();
34+
}
35+
36+
public FlutterDriverOptions setUiAutomator2Options(UiAutomator2Options uiAutomator2Options) {
37+
return setDefaultOptions(merge(uiAutomator2Options));
38+
}
39+
40+
public FlutterDriverOptions setXCUITestOptions(XCUITestOptions xcuiTestOptions) {
41+
return setDefaultOptions(merge(xcuiTestOptions));
42+
}
43+
44+
private void setDefaultOptions() {
45+
setDefaultOptions(this);
46+
}
47+
48+
private FlutterDriverOptions setDefaultOptions(FlutterDriverOptions flutterDriverOptions) {
49+
return flutterDriverOptions.setAutomationName(AutomationName.FLUTTER_INTEGRATION);
50+
}
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package io.appium.java_client.flutter;
2+
3+
import org.openqa.selenium.WebDriver;
4+
5+
/**
6+
* The {@code FlutterDriver} interface represents a driver that controls interactions with
7+
* Flutter applications, extending WebDriver and providing additional capabilities for
8+
* interacting with Flutter-specific elements and behaviors.
9+
*
10+
* <p> This interface serves as a common entity for drivers that support Flutter applications
11+
* on different platforms, such as Android and iOS. </p>
12+
*
13+
* @see WebDriver
14+
* @see SupportsGestureOnFlutterElements
15+
* @see SupportsScrollingOfFlutterElements
16+
* @see SupportsWaitingForFlutterElements
17+
*/
18+
public interface FlutterIntegrationTestDriver extends
19+
WebDriver,
20+
SupportsGestureOnFlutterElements,
21+
SupportsScrollingOfFlutterElements,
22+
SupportsWaitingForFlutterElements {
23+
}

0 commit comments

Comments
 (0)