Skip to content

Commit 469997f

Browse files
feat: Add support for FlutterIOSDriver
1 parent 7f28bfb commit 469997f

File tree

13 files changed

+409
-18
lines changed

13 files changed

+409
-18
lines changed

.github/workflows/gradle.yml

Lines changed: 15 additions & 6 deletions
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
@@ -77,13 +82,13 @@ jobs:
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 == 'android' || matrix.e2e-tests == 'ios' || matrix.e2e-tests == 'flutter-android' || matrix.e2e-tests == 'flutter-ios'
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 == 'android' || matrix.e2e-tests == 'ios' || matrix.e2e-tests == 'flutter-android' || matrix.e2e-tests == 'flutter-ios'
8792
run: npm install --location=global appium
8893

8994
- name: Install UIA2 driver
@@ -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

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@
22

33
import io.appium.java_client.AppiumBy;
44
import io.appium.java_client.android.options.UiAutomator2Options;
5+
import io.appium.java_client.flutter.FlutterDriver;
6+
import io.appium.java_client.flutter.FlutterDriverOptions;
57
import io.appium.java_client.flutter.android.FlutterAndroidDriver;
68
import io.appium.java_client.flutter.commands.ScrollParameter;
9+
import io.appium.java_client.flutter.ios.FlutterIOSDriver;
10+
import io.appium.java_client.ios.options.XCUITestOptions;
711
import io.appium.java_client.remote.AutomationName;
812
import io.appium.java_client.service.local.AppiumDriverLocalService;
913
import io.appium.java_client.service.local.AppiumServiceBuilder;
@@ -12,10 +16,10 @@
1216
import org.junit.jupiter.api.BeforeAll;
1317
import org.junit.jupiter.api.BeforeEach;
1418
import org.openqa.selenium.By;
15-
import org.openqa.selenium.InvalidArgumentException;
1619
import org.openqa.selenium.WebElement;
1720

1821
import java.net.MalformedURLException;
22+
import java.time.Duration;
1923
import java.util.Optional;
2024

2125
class BaseFlutterTest {
@@ -29,7 +33,7 @@ class BaseFlutterTest {
2933
protected static final int PORT = 4723;
3034

3135
private static AppiumDriverLocalService service;
32-
protected static FlutterAndroidDriver driver;
36+
protected static FlutterDriver driver;
3337
protected static final By LOGIN_BUTTON = AppiumBy.flutterText("Login");
3438

3539
/**
@@ -46,16 +50,21 @@ public static void beforeClass() {
4650

4751
@BeforeEach
4852
public void startSession() throws MalformedURLException {
53+
FlutterDriverOptions flutterOptions = new FlutterDriverOptions()
54+
.setFlutterSystemPort(9999)
55+
.setFlutterServerLaunchTimeout(Duration.ofSeconds(10));
56+
4957
if (IS_ANDROID) {
50-
// TODO: update it with FlutterDriverOptions once implemented
5158
UiAutomator2Options options = new UiAutomator2Options()
52-
.setAutomationName(AutomationName.FLUTTER_INTEGRATION)
5359
.setApp(System.getProperty("flutterApp"))
5460
.eventTimings();
55-
driver = new FlutterAndroidDriver(service.getUrl(), options);
61+
driver = new FlutterAndroidDriver(service.getUrl(), options.merge(flutterOptions));
5662
} else {
57-
throw new InvalidArgumentException(
58-
"Currently flutter driver implementation only supports android platform");
63+
XCUITestOptions options = new XCUITestOptions()
64+
.setAutomationName(AutomationName.FLUTTER_INTEGRATION)
65+
.setApp(System.getProperty("flutterApp"))
66+
.eventTimings();
67+
driver = new FlutterIOSDriver(service.getUrl(), options.merge(flutterOptions));
5968
}
6069
}
6170

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

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33
import io.appium.java_client.AppiumBy;
44
import io.appium.java_client.flutter.commands.ScrollParameter;
55
import io.appium.java_client.flutter.commands.WaitParameter;
6+
import io.appium.java_client.flutter.commands.DoubleClickParameter;
7+
import io.appium.java_client.flutter.commands.LongPressParameter;
8+
import io.appium.java_client.flutter.commands.DragAndDropParameter;
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;
@@ -59,4 +63,56 @@ public void testScrollTillVisibleCommand() {
5963
assertFalse(Boolean.parseBoolean(lastElement.getAttribute("displayed")));
6064
}
6165

66+
@Test
67+
public 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+
public 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+
public 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
}
Lines changed: 23 additions & 0 deletions
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 FlutterDriver extends
19+
WebDriver,
20+
SupportsGestureOnFlutterElements,
21+
SupportsScrollingOfFlutterElements,
22+
SupportsWaitingForFlutterElements {
23+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package io.appium.java_client.flutter;
2+
3+
import io.appium.java_client.flutter.options.SupportsFlutterServerLaunchTimeoutOption;
4+
import io.appium.java_client.flutter.options.SupportsFlutterSystemPortOption;
5+
import io.appium.java_client.remote.AutomationName;
6+
import io.appium.java_client.remote.options.BaseOptions;
7+
import org.openqa.selenium.Capabilities;
8+
9+
import java.util.Map;
10+
11+
/**
12+
* https://github.com/AppiumTestDistribution/appium-flutter-integration-driver#capabilities-for-appium-flutter-integration-driver
13+
*/
14+
public class FlutterDriverOptions extends BaseOptions<FlutterDriverOptions> implements
15+
SupportsFlutterSystemPortOption<FlutterDriverOptions>,
16+
SupportsFlutterServerLaunchTimeoutOption<FlutterDriverOptions> {
17+
18+
public FlutterDriverOptions() {
19+
setCommonOptions();
20+
}
21+
22+
public FlutterDriverOptions(Capabilities source) {
23+
super(source);
24+
setCommonOptions();
25+
}
26+
27+
public FlutterDriverOptions(Map<String, ?> source) {
28+
super(source);
29+
setCommonOptions();
30+
}
31+
32+
private void setCommonOptions() {
33+
setAutomationName(AutomationName.FLUTTER_INTEGRATION);
34+
}
35+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package io.appium.java_client.flutter;
2+
3+
import io.appium.java_client.flutter.commands.DoubleClickParameter;
4+
import io.appium.java_client.flutter.commands.DragAndDropParameter;
5+
import io.appium.java_client.flutter.commands.LongPressParameter;
6+
7+
public interface SupportsGestureOnFlutterElements extends CanExecuteFlutterScripts {
8+
9+
/**
10+
* Performs a double click action on an element.
11+
*
12+
* @param parameter The parameters for double-clicking, specifying element details.
13+
*/
14+
default void performDoubleClick(DoubleClickParameter parameter) {
15+
executeFlutterCommand("doubleClick", parameter);
16+
}
17+
18+
/**
19+
* Performs a long press action on an element.
20+
*
21+
* @param parameter The parameters for long pressing, specifying element details.
22+
*/
23+
default void performLongPress(LongPressParameter parameter) {
24+
executeFlutterCommand("longPress", parameter);
25+
}
26+
27+
/**
28+
* Performs a drag-and-drop action between two elements.
29+
*
30+
* @param parameter The parameters for drag-and-drop, specifying source and target elements.
31+
*/
32+
default void performDragAndDrop(DragAndDropParameter parameter) {
33+
executeFlutterCommand("dragAndDrop", parameter);
34+
}
35+
}

src/main/java/io/appium/java_client/flutter/android/FlutterAndroidDriver.java

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22

33
import io.appium.java_client.AppiumClientConfig;
44
import io.appium.java_client.android.AndroidDriver;
5-
import io.appium.java_client.flutter.SupportsScrollingOfFlutterElements;
6-
import io.appium.java_client.flutter.SupportsWaitingForFlutterElements;
5+
import io.appium.java_client.flutter.FlutterDriver;
76
import io.appium.java_client.service.local.AppiumDriverLocalService;
87
import io.appium.java_client.service.local.AppiumServiceBuilder;
98
import org.openqa.selenium.Capabilities;
@@ -16,9 +15,7 @@
1615
/**
1716
* Custom AndroidDriver implementation with additional Flutter-specific capabilities.
1817
*/
19-
public class FlutterAndroidDriver extends AndroidDriver implements
20-
SupportsWaitingForFlutterElements,
21-
SupportsScrollingOfFlutterElements {
18+
public class FlutterAndroidDriver extends AndroidDriver implements FlutterDriver {
2219

2320
public FlutterAndroidDriver(HttpCommandExecutor executor, Capabilities capabilities) {
2421
super(executor, capabilities);
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package io.appium.java_client.flutter.commands;
2+
3+
import com.google.common.base.Preconditions;
4+
import lombok.Setter;
5+
import lombok.experimental.Accessors;
6+
import org.openqa.selenium.Point;
7+
import org.openqa.selenium.WebElement;
8+
9+
import java.util.Collections;
10+
import java.util.HashMap;
11+
import java.util.Map;
12+
import java.util.Optional;
13+
14+
@Accessors(chain = true)
15+
@Setter
16+
public class DoubleClickParameter extends FlutterCommandParameter {
17+
private WebElement element;
18+
private Point offset;
19+
20+
@Override
21+
public Map<String, Object> toJson() {
22+
Preconditions.checkArgument(element != null || offset != null,
23+
"Must supply a valid element or offset to perform flutter gesture event");
24+
25+
Map<String, Object> params = new HashMap<>();
26+
Optional.ofNullable(element).ifPresent(element -> params.put("origin", element));
27+
Optional.ofNullable(offset).ifPresent(offset ->
28+
params.put("offset", Map.of("x", offset.getX(), "y", offset.getY())));
29+
return Collections.unmodifiableMap(params);
30+
}
31+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package io.appium.java_client.flutter.commands;
2+
3+
import com.google.common.base.Preconditions;
4+
import lombok.Getter;
5+
import lombok.experimental.Accessors;
6+
import org.openqa.selenium.WebElement;
7+
8+
import java.util.Map;
9+
10+
@Accessors(chain = true)
11+
@Getter
12+
public class DragAndDropParameter extends FlutterCommandParameter {
13+
private final WebElement source;
14+
private final WebElement target;
15+
16+
public DragAndDropParameter(WebElement source, WebElement target) {
17+
Preconditions.checkArgument(source != null && target != null,
18+
"Must supply valid source and target element to perform drag and drop event");
19+
this.source = source;
20+
this.target = target;
21+
}
22+
23+
@Override
24+
public Map<String, Object> toJson() {
25+
return Map.of("source", source, "target", target);
26+
}
27+
}

0 commit comments

Comments
 (0)