Skip to content

Commit 7f28bfb

Browse files
feat: add support for FlutterAndroidDriver (#2203)
1 parent bb4ee2d commit 7f28bfb

File tree

14 files changed

+544
-21
lines changed

14 files changed

+544
-21
lines changed

.github/workflows/gradle.yml

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,32 +26,36 @@ env:
2626
XCODE_VERSION: "15.4"
2727
IOS_DEVICE_NAME: iPhone 15
2828
IOS_PLATFORM_VERSION: "17.5"
29+
FLUTTER_ANDROID_APP: "https://github.com/AppiumTestDistribution/appium-flutter-server/releases/latest/download/app-debug.apk"
2930

3031
jobs:
3132
build:
3233

3334
strategy:
3435
matrix:
3536
include:
36-
- java: 11
37-
# Need to use specific (not `-latest`) version of macOS to be sure the required version of Xcode/simulator is available
38-
platform: macos-14
39-
e2e-tests: ios
40-
- java: 17
41-
platform: ubuntu-latest
42-
e2e-tests: android
43-
- java: 21
44-
platform: ubuntu-latest
37+
- java: 11
38+
# Need to use specific (not `-latest`) version of macOS to be sure the required version of Xcode/simulator is available
39+
platform: macos-14
40+
e2e-tests: ios
41+
- java: 17
42+
platform: ubuntu-latest
43+
e2e-tests: android
44+
- java: 17
45+
platform: ubuntu-latest
46+
e2e-tests: flutter-android
47+
- java: 21
48+
platform: ubuntu-latest
4549
fail-fast: false
4650

4751
runs-on: ${{ matrix.platform }}
4852

49-
name: JDK ${{ matrix.java }} - ${{ matrix.platform }}
53+
name: JDK ${{ matrix.java }} - ${{ matrix.platform }} ${{ matrix.e2e-tests }}
5054
steps:
5155
- uses: actions/checkout@v4
5256

5357
- name: Enable KVM group perms
54-
if: matrix.e2e-tests == 'android'
58+
if: matrix.e2e-tests == 'android' || matrix.e2e-tests == 'flutter-android'
5559
run: |
5660
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
5761
sudo udevadm control --reload-rules
@@ -73,18 +77,23 @@ jobs:
7377
./gradlew clean build -PisCI -Pselenium.version=$latest_snapshot
7478
7579
- name: Install Node.js
76-
if: matrix.e2e-tests == 'android' || matrix.e2e-tests == 'ios'
80+
if: matrix.e2e-tests == 'android' || matrix.e2e-tests == 'ios' || matrix.e2e-tests == 'flutter-android'
7781
uses: actions/setup-node@v4
7882
with:
7983
node-version: 'lts/*'
8084

8185
- name: Install Appium
82-
if: matrix.e2e-tests == 'android' || matrix.e2e-tests == 'ios'
86+
if: matrix.e2e-tests == 'android' || matrix.e2e-tests == 'ios' || matrix.e2e-tests == 'flutter-android'
8387
run: npm install --location=global appium
8488

8589
- name: Install UIA2 driver
86-
if: matrix.e2e-tests == 'android'
90+
if: matrix.e2e-tests == 'android' || matrix.e2e-tests == 'flutter-android'
8791
run: appium driver install uiautomator2
92+
93+
- name: Install Flutter Integration driver
94+
if: matrix.e2e-tests == 'flutter-android'
95+
run: appium driver install appium-flutter-integration-driver --source npm
96+
8897
- name: Run Android E2E tests
8998
if: matrix.e2e-tests == 'android'
9099
uses: reactivecircus/android-emulator-runner@v2
@@ -96,6 +105,17 @@ jobs:
96105
disable-animations: true
97106
target: ${{ env.ANDROID_EMU_TARGET }}
98107

108+
- name: Run Flutter Android E2E tests
109+
if: matrix.e2e-tests == 'flutter-android'
110+
uses: reactivecircus/android-emulator-runner@v2
111+
with:
112+
script: ./gradlew e2eFlutterTest -Pplatform="android" -Pselenium.version=$latest_snapshot -PisCI -PflutterApp=${{ env.FLUTTER_ANDROID_APP }}
113+
api-level: ${{ env.ANDROID_SDK_VERSION }}
114+
avd-name: ${{ env.ANDROID_EMU_NAME }}
115+
disable-spellchecker: true
116+
disable-animations: true
117+
target: ${{ env.ANDROID_EMU_TARGET }}
118+
99119
- name: Select Xcode
100120
if: matrix.e2e-tests == 'ios'
101121
uses: maxim-lobanov/setup-xcode@v1

build.gradle

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ dependencies {
7070
}
7171

7272
dependencyCheck {
73-
failBuildOnCVSS=22
73+
failBuildOnCVSS = 22
7474
}
7575

7676
jacoco {
@@ -185,7 +185,7 @@ wrapper {
185185

186186
processResources {
187187
filter ReplaceTokens, tokens: [
188-
'selenium.version': seleniumVersion,
188+
'selenium.version' : seleniumVersion,
189189
'appiumClient.version': appiumClientVersion
190190
]
191191
}
@@ -290,5 +290,24 @@ testing {
290290
}
291291
}
292292
}
293+
294+
e2eFlutterTest(JvmTestSuite) {
295+
sources {
296+
java {
297+
srcDirs = ['src/e2eFlutterTest/java']
298+
}
299+
}
300+
dependencies {
301+
implementation project()
302+
implementation(sourceSets.test.output)
303+
}
304+
305+
targets.configureEach {
306+
testTask.configure {
307+
shouldRunAfter(test)
308+
systemProperties project.properties.subMap(["platform", "flutterApp"])
309+
}
310+
}
311+
}
293312
}
294313
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package io.appium.java_client.android;
2+
3+
import io.appium.java_client.AppiumBy;
4+
import io.appium.java_client.android.options.UiAutomator2Options;
5+
import io.appium.java_client.flutter.android.FlutterAndroidDriver;
6+
import io.appium.java_client.flutter.commands.ScrollParameter;
7+
import io.appium.java_client.remote.AutomationName;
8+
import io.appium.java_client.service.local.AppiumDriverLocalService;
9+
import io.appium.java_client.service.local.AppiumServiceBuilder;
10+
import org.junit.jupiter.api.AfterAll;
11+
import org.junit.jupiter.api.AfterEach;
12+
import org.junit.jupiter.api.BeforeAll;
13+
import org.junit.jupiter.api.BeforeEach;
14+
import org.openqa.selenium.By;
15+
import org.openqa.selenium.InvalidArgumentException;
16+
import org.openqa.selenium.WebElement;
17+
18+
import java.net.MalformedURLException;
19+
import java.util.Optional;
20+
21+
class BaseFlutterTest {
22+
23+
private static final boolean IS_ANDROID = Optional
24+
.ofNullable(System.getProperty("platform"))
25+
.orElse("android")
26+
.equalsIgnoreCase("android");
27+
private static final String APP_ID = IS_ANDROID
28+
? "com.example.appium_testing_app" : "com.example.appiumTestingApp";
29+
protected static final int PORT = 4723;
30+
31+
private static AppiumDriverLocalService service;
32+
protected static FlutterAndroidDriver driver;
33+
protected static final By LOGIN_BUTTON = AppiumBy.flutterText("Login");
34+
35+
/**
36+
* initialization.
37+
*/
38+
@BeforeAll
39+
public static void beforeClass() {
40+
service = new AppiumServiceBuilder()
41+
.withIPAddress("127.0.0.1")
42+
.usingPort(PORT)
43+
.build();
44+
service.start();
45+
}
46+
47+
@BeforeEach
48+
public void startSession() throws MalformedURLException {
49+
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);
56+
} else {
57+
throw new InvalidArgumentException(
58+
"Currently flutter driver implementation only supports android platform");
59+
}
60+
}
61+
62+
@AfterEach
63+
public void stopSession() {
64+
if (driver != null) {
65+
driver.quit();
66+
}
67+
}
68+
69+
@AfterAll
70+
public static void afterClass() {
71+
if (service.isRunning()) {
72+
service.stop();
73+
}
74+
}
75+
76+
public void openScreen(String screenTitle) {
77+
ScrollParameter scrollOptions = new ScrollParameter(
78+
AppiumBy.flutterText(screenTitle), ScrollParameter.ScrollDirection.DOWN);
79+
WebElement element = driver.scrollTillVisible(scrollOptions);
80+
element.click();
81+
}
82+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package io.appium.java_client.android;
2+
3+
import io.appium.java_client.AppiumBy;
4+
import io.appium.java_client.flutter.commands.ScrollParameter;
5+
import io.appium.java_client.flutter.commands.WaitParameter;
6+
import org.junit.jupiter.api.Test;
7+
import org.openqa.selenium.WebElement;
8+
9+
import static org.junit.jupiter.api.Assertions.assertEquals;
10+
import static org.junit.jupiter.api.Assertions.assertFalse;
11+
import static org.junit.jupiter.api.Assertions.assertTrue;
12+
13+
class CommandTest extends BaseFlutterTest {
14+
15+
private static final AppiumBy.FlutterBy MESSAGE_FIELD = AppiumBy.flutterKey("message_field");
16+
private static final AppiumBy.FlutterBy TOGGLE_BUTTON = AppiumBy.flutterKey("toggle_button");
17+
18+
@Test
19+
public void testWaitCommand() {
20+
WebElement loginButton = driver.findElement(BaseFlutterTest.LOGIN_BUTTON);
21+
loginButton.click();
22+
openScreen("Lazy Loading");
23+
24+
WebElement messageField = driver.findElement(MESSAGE_FIELD);
25+
WebElement toggleButton = driver.findElement(TOGGLE_BUTTON);
26+
27+
assertEquals(messageField.getText(), "Hello world");
28+
toggleButton.click();
29+
assertEquals(messageField.getText(), "Hello world");
30+
31+
WaitParameter waitParameter = new WaitParameter().setLocator(MESSAGE_FIELD);
32+
33+
driver.waitForInVisible(waitParameter);
34+
assertEquals(0, driver.findElements(MESSAGE_FIELD).size());
35+
toggleButton.click();
36+
driver.waitForVisible(waitParameter);
37+
assertEquals(1, driver.findElements(MESSAGE_FIELD).size());
38+
assertEquals(messageField.getText(), "Hello world");
39+
}
40+
41+
@Test
42+
public void testScrollTillVisibleCommand() {
43+
WebElement loginButton = driver.findElement(BaseFlutterTest.LOGIN_BUTTON);
44+
loginButton.click();
45+
openScreen("Vertical Swiping");
46+
47+
WebElement firstElement = driver.scrollTillVisible(new ScrollParameter(AppiumBy.flutterText("Java")));
48+
assertTrue(Boolean.parseBoolean(firstElement.getAttribute("displayed")));
49+
50+
WebElement lastElement = driver.scrollTillVisible(new ScrollParameter(AppiumBy.flutterText("Protractor")));
51+
assertTrue(Boolean.parseBoolean(lastElement.getAttribute("displayed")));
52+
assertFalse(Boolean.parseBoolean(firstElement.getAttribute("displayed")));
53+
54+
firstElement = driver.scrollTillVisible(
55+
new ScrollParameter(AppiumBy.flutterText("Java"),
56+
ScrollParameter.ScrollDirection.UP)
57+
);
58+
assertTrue(Boolean.parseBoolean(firstElement.getAttribute("displayed")));
59+
assertFalse(Boolean.parseBoolean(lastElement.getAttribute("displayed")));
60+
}
61+
62+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package io.appium.java_client.android;
2+
3+
import io.appium.java_client.AppiumBy;
4+
import org.junit.jupiter.api.Test;
5+
import org.openqa.selenium.WebElement;
6+
7+
import static org.junit.jupiter.api.Assertions.assertEquals;
8+
9+
10+
class FinderTests extends BaseFlutterTest {
11+
12+
@Test
13+
public void testFlutterByKey() {
14+
WebElement userNameField = driver.findElement(AppiumBy.flutterKey("username_text_field"));
15+
assertEquals("admin", userNameField.getText());
16+
userNameField.clear();
17+
driver.findElement(AppiumBy.flutterKey("username_text_field")).sendKeys("admin123");
18+
assertEquals("admin123", userNameField.getText());
19+
}
20+
21+
@Test
22+
public void testFlutterByType() {
23+
WebElement loginButton = driver.findElement(AppiumBy.flutterType("ElevatedButton"));
24+
assertEquals(loginButton.findElement(AppiumBy.flutterType("Text")).getText(), "Login");
25+
}
26+
27+
@Test
28+
public void testFlutterText() {
29+
WebElement loginButton = driver.findElement(AppiumBy.flutterText("Login"));
30+
assertEquals(loginButton.getText(), "Login");
31+
loginButton.click();
32+
33+
assertEquals(1, driver.findElements(AppiumBy.flutterText("Slider")).size());
34+
}
35+
36+
@Test
37+
public void testFlutterTextContaining() {
38+
WebElement loginButton = driver.findElement(BaseFlutterTest.LOGIN_BUTTON);
39+
loginButton.click();
40+
assertEquals(driver.findElement(AppiumBy.flutterTextContaining("Vertical")).getText(),
41+
"Vertical Swiping");
42+
}
43+
44+
@Test
45+
public void testFlutterSemanticsLabel() {
46+
WebElement loginButton = driver.findElement(BaseFlutterTest.LOGIN_BUTTON);
47+
loginButton.click();
48+
openScreen("Lazy Loading");
49+
50+
WebElement messageField = driver.findElement(AppiumBy.flutterSemanticsLabel("message_field"));
51+
assertEquals(messageField.getText(),
52+
"Hello world");
53+
}
54+
}

src/main/java/io/appium/java_client/AppiumBy.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ public static By iOSNsPredicateString(final String iOSNsPredicateString) {
206206
* @param selector is the value defined to the key attribute of the flutter element
207207
* @return an instance of {@link AppiumBy.ByFlutterKey}
208208
*/
209-
public static By flutterKey(final String selector) {
209+
public static FlutterBy flutterKey(final String selector) {
210210
return new ByFlutterKey(selector);
211211
}
212212

@@ -216,7 +216,7 @@ public static By flutterKey(final String selector) {
216216
* @param selector is the Type of widget mounted in the app tree
217217
* @return an instance of {@link AppiumBy.ByFlutterType}
218218
*/
219-
public static By flutterType(final String selector) {
219+
public static FlutterBy flutterType(final String selector) {
220220
return new ByFlutterType(selector);
221221
}
222222

@@ -226,7 +226,7 @@ public static By flutterType(final String selector) {
226226
* @param selector is the text that is present on the widget
227227
* @return an instance of {@link AppiumBy.ByFlutterText}
228228
*/
229-
public static By flutterText(final String selector) {
229+
public static FlutterBy flutterText(final String selector) {
230230
return new ByFlutterText(selector);
231231
}
232232

@@ -236,7 +236,7 @@ public static By flutterText(final String selector) {
236236
* @param selector is the text that is partially present on the widget
237237
* @return an instance of {@link AppiumBy.ByFlutterTextContaining}
238238
*/
239-
public static By flutterTextContaining(final String selector) {
239+
public static FlutterBy flutterTextContaining(final String selector) {
240240
return new ByFlutterTextContaining(selector);
241241
}
242242

@@ -246,7 +246,7 @@ public static By flutterTextContaining(final String selector) {
246246
* @param semanticsLabel represents the value assigned to the label attribute of semantics element
247247
* @return an instance of {@link AppiumBy.ByFlutterSemanticsLabel}
248248
*/
249-
public static By flutterSemanticsLabel(final String semanticsLabel) {
249+
public static FlutterBy flutterSemanticsLabel(final String semanticsLabel) {
250250
return new ByFlutterSemanticsLabel(semanticsLabel);
251251
}
252252

0 commit comments

Comments
 (0)