Skip to content

feat: add support for FlutterAndroidDriver #2203

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

Merged
merged 16 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are these changes needed?

Copy link
Contributor Author

@sudharsan-selvaraj sudharsan-selvaraj Jul 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currenlty all flutter script can only work with locators supported by flutter driver. This change is specifically introduced the simplify the API consumption and to prevent the manual type casting when calling the commands.

driver.waitForInVisible(new WaitParameter().setLocator(AppiumBy.flutterKey("message_field")));

Instead of

driver.waitForInVisible(new WaitParameter().setLocator((AppiumBy.FlutterBy)AppiumBy.flutterKey("message_field")));

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