Skip to content

Commit

Permalink
feat: add support for FlutterAndroidDriver (#2203)
Browse files Browse the repository at this point in the history
  • Loading branch information
sudharsan-selvaraj committed Jul 23, 2024
1 parent bb4ee2d commit 7f28bfb
Show file tree
Hide file tree
Showing 14 changed files with 544 additions and 21 deletions.
48 changes: 34 additions & 14 deletions .github/workflows/gradle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,32 +26,36 @@ env:
XCODE_VERSION: "15.4"
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"

jobs:
build:

strategy:
matrix:
include:
- java: 11
# 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
platform: ubuntu-latest
e2e-tests: android
- java: 21
platform: ubuntu-latest
- java: 11
# 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
platform: ubuntu-latest
e2e-tests: android
- java: 17
platform: ubuntu-latest
e2e-tests: flutter-android
- java: 21
platform: ubuntu-latest
fail-fast: false

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

name: JDK ${{ matrix.java }} - ${{ matrix.platform }}
name: JDK ${{ matrix.java }} - ${{ matrix.platform }} ${{ matrix.e2e-tests }}
steps:
- uses: actions/checkout@v4

- name: Enable KVM group perms
if: matrix.e2e-tests == 'android'
if: matrix.e2e-tests == 'android' || matrix.e2e-tests == 'flutter-android'
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
Expand All @@ -73,18 +77,23 @@ jobs:
./gradlew clean build -PisCI -Pselenium.version=$latest_snapshot
- name: Install Node.js
if: matrix.e2e-tests == 'android' || matrix.e2e-tests == 'ios'
if: matrix.e2e-tests == 'android' || matrix.e2e-tests == 'ios' || matrix.e2e-tests == 'flutter-android'
uses: actions/setup-node@v4
with:
node-version: 'lts/*'

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

- name: Install UIA2 driver
if: matrix.e2e-tests == 'android'
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'
run: appium driver install appium-flutter-integration-driver --source npm

- name: Run Android E2E tests
if: matrix.e2e-tests == 'android'
uses: reactivecircus/android-emulator-runner@v2
Expand All @@ -96,6 +105,17 @@ jobs:
disable-animations: true
target: ${{ env.ANDROID_EMU_TARGET }}

- name: Run Flutter Android E2E tests
if: matrix.e2e-tests == 'flutter-android'
uses: reactivecircus/android-emulator-runner@v2
with:
script: ./gradlew e2eFlutterTest -Pplatform="android" -Pselenium.version=$latest_snapshot -PisCI -PflutterApp=${{ env.FLUTTER_ANDROID_APP }}
api-level: ${{ env.ANDROID_SDK_VERSION }}
avd-name: ${{ env.ANDROID_EMU_NAME }}
disable-spellchecker: true
disable-animations: true
target: ${{ env.ANDROID_EMU_TARGET }}

- name: Select Xcode
if: matrix.e2e-tests == 'ios'
uses: maxim-lobanov/setup-xcode@v1
Expand Down
23 changes: 21 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ dependencies {
}

dependencyCheck {
failBuildOnCVSS=22
failBuildOnCVSS = 22
}

jacoco {
Expand Down Expand Up @@ -185,7 +185,7 @@ wrapper {

processResources {
filter ReplaceTokens, tokens: [
'selenium.version': seleniumVersion,
'selenium.version' : seleniumVersion,
'appiumClient.version': appiumClientVersion
]
}
Expand Down Expand Up @@ -290,5 +290,24 @@ testing {
}
}
}

e2eFlutterTest(JvmTestSuite) {
sources {
java {
srcDirs = ['src/e2eFlutterTest/java']
}
}
dependencies {
implementation project()
implementation(sourceSets.test.output)
}

targets.configureEach {
testTask.configure {
shouldRunAfter(test)
systemProperties project.properties.subMap(["platform", "flutterApp"])
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package io.appium.java_client.android;

import io.appium.java_client.AppiumBy;
import io.appium.java_client.android.options.UiAutomator2Options;
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.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.util.Optional;

class BaseFlutterTest {

private static final boolean IS_ANDROID = Optional
.ofNullable(System.getProperty("platform"))
.orElse("android")
.equalsIgnoreCase("android");
private static final String APP_ID = IS_ANDROID
? "com.example.appium_testing_app" : "com.example.appiumTestingApp";
protected static final int PORT = 4723;

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

/**
* initialization.
*/
@BeforeAll
public static void beforeClass() {
service = new AppiumServiceBuilder()
.withIPAddress("127.0.0.1")
.usingPort(PORT)
.build();
service.start();
}

@BeforeEach
public void startSession() throws MalformedURLException {
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);
} else {
throw new InvalidArgumentException(
"Currently flutter driver implementation only supports android platform");
}
}

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

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

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

import io.appium.java_client.AppiumBy;
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.WebElement;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

class CommandTest extends BaseFlutterTest {

private static final AppiumBy.FlutterBy MESSAGE_FIELD = AppiumBy.flutterKey("message_field");
private static final AppiumBy.FlutterBy TOGGLE_BUTTON = AppiumBy.flutterKey("toggle_button");

@Test
public void testWaitCommand() {
WebElement loginButton = driver.findElement(BaseFlutterTest.LOGIN_BUTTON);
loginButton.click();
openScreen("Lazy Loading");

WebElement messageField = driver.findElement(MESSAGE_FIELD);
WebElement toggleButton = driver.findElement(TOGGLE_BUTTON);

assertEquals(messageField.getText(), "Hello world");
toggleButton.click();
assertEquals(messageField.getText(), "Hello world");

WaitParameter waitParameter = new WaitParameter().setLocator(MESSAGE_FIELD);

driver.waitForInVisible(waitParameter);
assertEquals(0, driver.findElements(MESSAGE_FIELD).size());
toggleButton.click();
driver.waitForVisible(waitParameter);
assertEquals(1, driver.findElements(MESSAGE_FIELD).size());
assertEquals(messageField.getText(), "Hello world");
}

@Test
public void testScrollTillVisibleCommand() {
WebElement loginButton = driver.findElement(BaseFlutterTest.LOGIN_BUTTON);
loginButton.click();
openScreen("Vertical Swiping");

WebElement firstElement = driver.scrollTillVisible(new ScrollParameter(AppiumBy.flutterText("Java")));
assertTrue(Boolean.parseBoolean(firstElement.getAttribute("displayed")));

WebElement lastElement = driver.scrollTillVisible(new ScrollParameter(AppiumBy.flutterText("Protractor")));
assertTrue(Boolean.parseBoolean(lastElement.getAttribute("displayed")));
assertFalse(Boolean.parseBoolean(firstElement.getAttribute("displayed")));

firstElement = driver.scrollTillVisible(
new ScrollParameter(AppiumBy.flutterText("Java"),
ScrollParameter.ScrollDirection.UP)
);
assertTrue(Boolean.parseBoolean(firstElement.getAttribute("displayed")));
assertFalse(Boolean.parseBoolean(lastElement.getAttribute("displayed")));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package io.appium.java_client.android;

import io.appium.java_client.AppiumBy;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.WebElement;

import static org.junit.jupiter.api.Assertions.assertEquals;


class FinderTests extends BaseFlutterTest {

@Test
public void testFlutterByKey() {
WebElement userNameField = driver.findElement(AppiumBy.flutterKey("username_text_field"));
assertEquals("admin", userNameField.getText());
userNameField.clear();
driver.findElement(AppiumBy.flutterKey("username_text_field")).sendKeys("admin123");
assertEquals("admin123", userNameField.getText());
}

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

@Test
public void testFlutterText() {
WebElement loginButton = driver.findElement(AppiumBy.flutterText("Login"));
assertEquals(loginButton.getText(), "Login");
loginButton.click();

assertEquals(1, driver.findElements(AppiumBy.flutterText("Slider")).size());
}

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

@Test
public void testFlutterSemanticsLabel() {
WebElement loginButton = driver.findElement(BaseFlutterTest.LOGIN_BUTTON);
loginButton.click();
openScreen("Lazy Loading");

WebElement messageField = driver.findElement(AppiumBy.flutterSemanticsLabel("message_field"));
assertEquals(messageField.getText(),
"Hello world");
}
}
10 changes: 5 additions & 5 deletions src/main/java/io/appium/java_client/AppiumBy.java
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ public static By iOSNsPredicateString(final String iOSNsPredicateString) {
* @param selector is the value defined to the key attribute of the flutter element
* @return an instance of {@link AppiumBy.ByFlutterKey}
*/
public static By flutterKey(final String selector) {
public static FlutterBy flutterKey(final String selector) {
return new ByFlutterKey(selector);
}

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

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

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

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

Expand Down
Loading

0 comments on commit 7f28bfb

Please sign in to comment.