From 7f28bfbcf051ec0b3d53533280b1236ca2a2838f Mon Sep 17 00:00:00 2001 From: Sudharsan Selvaraj Date: Tue, 23 Jul 2024 06:20:12 +0530 Subject: [PATCH] feat: add support for FlutterAndroidDriver (#2203) --- .github/workflows/gradle.yml | 48 +++++++--- build.gradle | 23 ++++- .../java_client/android/BaseFlutterTest.java | 82 +++++++++++++++++ .../java_client/android/CommandTest.java | 62 +++++++++++++ .../java_client/android/FinderTests.java | 54 ++++++++++++ .../java/io/appium/java_client/AppiumBy.java | 10 +-- .../flutter/CanExecuteFlutterScripts.java | 20 +++++ .../SupportsScrollingOfFlutterElements.java | 17 ++++ .../SupportsWaitingForFlutterElements.java | 25 ++++++ .../flutter/android/FlutterAndroidDriver.java | 72 +++++++++++++++ .../commands/FlutterCommandParameter.java | 25 ++++++ .../flutter/commands/ScrollParameter.java | 87 +++++++++++++++++++ .../flutter/commands/WaitParameter.java | 38 ++++++++ .../java_client/remote/AutomationName.java | 2 + 14 files changed, 544 insertions(+), 21 deletions(-) create mode 100644 src/e2eFlutterTest/java/io/appium/java_client/android/BaseFlutterTest.java create mode 100644 src/e2eFlutterTest/java/io/appium/java_client/android/CommandTest.java create mode 100644 src/e2eFlutterTest/java/io/appium/java_client/android/FinderTests.java create mode 100644 src/main/java/io/appium/java_client/flutter/CanExecuteFlutterScripts.java create mode 100644 src/main/java/io/appium/java_client/flutter/SupportsScrollingOfFlutterElements.java create mode 100644 src/main/java/io/appium/java_client/flutter/SupportsWaitingForFlutterElements.java create mode 100644 src/main/java/io/appium/java_client/flutter/android/FlutterAndroidDriver.java create mode 100644 src/main/java/io/appium/java_client/flutter/commands/FlutterCommandParameter.java create mode 100644 src/main/java/io/appium/java_client/flutter/commands/ScrollParameter.java create mode 100644 src/main/java/io/appium/java_client/flutter/commands/WaitParameter.java diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index e8006e8df..8cdab0ef7 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -26,6 +26,7 @@ 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: @@ -33,25 +34,28 @@ jobs: 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 @@ -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 @@ -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 diff --git a/build.gradle b/build.gradle index 8de5f95be..3488703d7 100644 --- a/build.gradle +++ b/build.gradle @@ -70,7 +70,7 @@ dependencies { } dependencyCheck { - failBuildOnCVSS=22 + failBuildOnCVSS = 22 } jacoco { @@ -185,7 +185,7 @@ wrapper { processResources { filter ReplaceTokens, tokens: [ - 'selenium.version': seleniumVersion, + 'selenium.version' : seleniumVersion, 'appiumClient.version': appiumClientVersion ] } @@ -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"]) + } + } + } } } diff --git a/src/e2eFlutterTest/java/io/appium/java_client/android/BaseFlutterTest.java b/src/e2eFlutterTest/java/io/appium/java_client/android/BaseFlutterTest.java new file mode 100644 index 000000000..e18112d4a --- /dev/null +++ b/src/e2eFlutterTest/java/io/appium/java_client/android/BaseFlutterTest.java @@ -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(); + } +} diff --git a/src/e2eFlutterTest/java/io/appium/java_client/android/CommandTest.java b/src/e2eFlutterTest/java/io/appium/java_client/android/CommandTest.java new file mode 100644 index 000000000..80f32002d --- /dev/null +++ b/src/e2eFlutterTest/java/io/appium/java_client/android/CommandTest.java @@ -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"))); + } + +} diff --git a/src/e2eFlutterTest/java/io/appium/java_client/android/FinderTests.java b/src/e2eFlutterTest/java/io/appium/java_client/android/FinderTests.java new file mode 100644 index 000000000..dc2361869 --- /dev/null +++ b/src/e2eFlutterTest/java/io/appium/java_client/android/FinderTests.java @@ -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"); + } +} diff --git a/src/main/java/io/appium/java_client/AppiumBy.java b/src/main/java/io/appium/java_client/AppiumBy.java index 164c7d3e7..aca01fea7 100644 --- a/src/main/java/io/appium/java_client/AppiumBy.java +++ b/src/main/java/io/appium/java_client/AppiumBy.java @@ -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); } @@ -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); } @@ -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); } @@ -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); } @@ -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); } diff --git a/src/main/java/io/appium/java_client/flutter/CanExecuteFlutterScripts.java b/src/main/java/io/appium/java_client/flutter/CanExecuteFlutterScripts.java new file mode 100644 index 000000000..f8eaf3af4 --- /dev/null +++ b/src/main/java/io/appium/java_client/flutter/CanExecuteFlutterScripts.java @@ -0,0 +1,20 @@ +package io.appium.java_client.flutter; + +import io.appium.java_client.flutter.commands.FlutterCommandParameter; +import org.openqa.selenium.JavascriptExecutor; + +public interface CanExecuteFlutterScripts extends JavascriptExecutor { + + /** + * Executes a Flutter-specific script using JavascriptExecutor. + * + * @param scriptName The name of the Flutter script to execute. + * @param parameter The parameters for the Flutter command. + * @return The result of executing the script. + */ + default Object executeFlutterCommand(String scriptName, FlutterCommandParameter parameter) { + String commandName = String.format("flutter: %s", scriptName); + return executeScript(commandName, parameter.toJson()); + } + +} diff --git a/src/main/java/io/appium/java_client/flutter/SupportsScrollingOfFlutterElements.java b/src/main/java/io/appium/java_client/flutter/SupportsScrollingOfFlutterElements.java new file mode 100644 index 000000000..25a734cf7 --- /dev/null +++ b/src/main/java/io/appium/java_client/flutter/SupportsScrollingOfFlutterElements.java @@ -0,0 +1,17 @@ +package io.appium.java_client.flutter; + +import io.appium.java_client.flutter.commands.ScrollParameter; +import org.openqa.selenium.WebElement; + +public interface SupportsScrollingOfFlutterElements extends CanExecuteFlutterScripts { + + /** + * Scrolls to make an element visible on the screen. + * + * @param parameter The parameters for scrolling, specifying element details. + * @return The WebElement that was scrolled to. + */ + default WebElement scrollTillVisible(ScrollParameter parameter) { + return (WebElement) executeFlutterCommand("scrollTillVisible", parameter); + } +} diff --git a/src/main/java/io/appium/java_client/flutter/SupportsWaitingForFlutterElements.java b/src/main/java/io/appium/java_client/flutter/SupportsWaitingForFlutterElements.java new file mode 100644 index 000000000..521f75cc8 --- /dev/null +++ b/src/main/java/io/appium/java_client/flutter/SupportsWaitingForFlutterElements.java @@ -0,0 +1,25 @@ +package io.appium.java_client.flutter; + +import io.appium.java_client.flutter.commands.WaitParameter; + +public interface SupportsWaitingForFlutterElements extends CanExecuteFlutterScripts { + + /** + * Waits for an element to become visible on the screen. + * + * @param parameter The parameters for waiting, specifying timeout and element details. + */ + default void waitForVisible(WaitParameter parameter) { + executeFlutterCommand("waitForVisible", parameter); + } + + /** + * Waits for an element to become absent on the screen. + * + * @param parameter The parameters for waiting, specifying timeout and element details. + */ + default void waitForInVisible(WaitParameter parameter) { + executeFlutterCommand("waitForAbsent", parameter); + } + +} diff --git a/src/main/java/io/appium/java_client/flutter/android/FlutterAndroidDriver.java b/src/main/java/io/appium/java_client/flutter/android/FlutterAndroidDriver.java new file mode 100644 index 000000000..f7be154ff --- /dev/null +++ b/src/main/java/io/appium/java_client/flutter/android/FlutterAndroidDriver.java @@ -0,0 +1,72 @@ +package io.appium.java_client.flutter.android; + +import io.appium.java_client.AppiumClientConfig; +import io.appium.java_client.android.AndroidDriver; +import io.appium.java_client.flutter.SupportsScrollingOfFlutterElements; +import io.appium.java_client.flutter.SupportsWaitingForFlutterElements; +import io.appium.java_client.service.local.AppiumDriverLocalService; +import io.appium.java_client.service.local.AppiumServiceBuilder; +import org.openqa.selenium.Capabilities; +import org.openqa.selenium.remote.HttpCommandExecutor; +import org.openqa.selenium.remote.http.ClientConfig; +import org.openqa.selenium.remote.http.HttpClient; + +import java.net.URL; + +/** + * Custom AndroidDriver implementation with additional Flutter-specific capabilities. + */ +public class FlutterAndroidDriver extends AndroidDriver implements + SupportsWaitingForFlutterElements, + SupportsScrollingOfFlutterElements { + + public FlutterAndroidDriver(HttpCommandExecutor executor, Capabilities capabilities) { + super(executor, capabilities); + } + + public FlutterAndroidDriver(URL remoteAddress, Capabilities capabilities) { + super(remoteAddress, capabilities); + } + + public FlutterAndroidDriver(URL remoteAddress, HttpClient.Factory httpClientFactory, Capabilities capabilities) { + super(remoteAddress, httpClientFactory, capabilities); + } + + public FlutterAndroidDriver(AppiumDriverLocalService service, Capabilities capabilities) { + super(service, capabilities); + } + + public FlutterAndroidDriver( + AppiumDriverLocalService service, HttpClient.Factory httpClientFactory, Capabilities capabilities) { + super(service, httpClientFactory, capabilities); + } + + public FlutterAndroidDriver(AppiumServiceBuilder builder, Capabilities capabilities) { + super(builder, capabilities); + } + + public FlutterAndroidDriver( + AppiumServiceBuilder builder, HttpClient.Factory httpClientFactory, Capabilities capabilities) { + super(builder, httpClientFactory, capabilities); + } + + public FlutterAndroidDriver(HttpClient.Factory httpClientFactory, Capabilities capabilities) { + super(httpClientFactory, capabilities); + } + + public FlutterAndroidDriver(ClientConfig clientConfig, Capabilities capabilities) { + super(clientConfig, capabilities); + } + + public FlutterAndroidDriver(AppiumClientConfig appiumClientConfig, Capabilities capabilities) { + super(appiumClientConfig, capabilities); + } + + public FlutterAndroidDriver(Capabilities capabilities) { + super(capabilities); + } + + public FlutterAndroidDriver(URL remoteSessionAddress, String automationName) { + super(remoteSessionAddress, automationName); + } +} diff --git a/src/main/java/io/appium/java_client/flutter/commands/FlutterCommandParameter.java b/src/main/java/io/appium/java_client/flutter/commands/FlutterCommandParameter.java new file mode 100644 index 000000000..ddd2d74f6 --- /dev/null +++ b/src/main/java/io/appium/java_client/flutter/commands/FlutterCommandParameter.java @@ -0,0 +1,25 @@ +package io.appium.java_client.flutter.commands; + +import io.appium.java_client.AppiumBy; +import org.openqa.selenium.By; + +import java.util.Map; + +public abstract class FlutterCommandParameter { + + /** + * Parses an Appium Flutter locator into a Map representation suitable for Flutter Integration Driver. + * + * @param by The FlutterBy instance representing the locator to parse. + * @return A Map containing the parsed locator information with keys using and value. + */ + protected static Map parseFlutterLocator(AppiumBy.FlutterBy by) { + By.Remotable.Parameters parameters = by.getRemoteParameters(); + return Map.of( + "using", parameters.using(), + "value", parameters.value() + ); + } + + public abstract Map toJson(); +} diff --git a/src/main/java/io/appium/java_client/flutter/commands/ScrollParameter.java b/src/main/java/io/appium/java_client/flutter/commands/ScrollParameter.java new file mode 100644 index 000000000..695cb060d --- /dev/null +++ b/src/main/java/io/appium/java_client/flutter/commands/ScrollParameter.java @@ -0,0 +1,87 @@ +package io.appium.java_client.flutter.commands; + +import com.google.common.base.Preconditions; +import io.appium.java_client.AppiumBy; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.openqa.selenium.WebElement; + +import java.time.Duration; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +@Accessors(chain = true) +@Getter +@Setter +public class ScrollParameter extends FlutterCommandParameter { + private AppiumBy.FlutterBy scrollTo; + private WebElement scrollView; + private ScrollDirection scrollDirection; + private Integer delta; + private Integer maxScrolls; + private Integer settleBetweenScrollsTimeout; + private Duration dragDuration; + + private ScrollParameter() { + } + + /** + * Constructs a new ScrollOptions object with the given parameters. + * + * @param scrollTo the locator used for scrolling to a specific element + */ + public ScrollParameter(AppiumBy.FlutterBy scrollTo) { + this(scrollTo, ScrollDirection.DOWN); + } + + /** + * Constructs a new ScrollOptions object with the given parameters. + * + * @param scrollTo the locator used for scrolling to a specific element + * @param scrollDirection the direction in which to scroll (e.g., ScrollDirection.DOWN) + * @throws IllegalArgumentException if scrollTo is null + */ + public ScrollParameter(AppiumBy.FlutterBy scrollTo, ScrollDirection scrollDirection) { + Preconditions.checkArgument(scrollTo != null, "Must supply a valid locator for scrollTo"); + this.scrollTo = scrollTo; + this.scrollDirection = scrollDirection; + } + + @Override + public Map toJson() { + Map params = new HashMap<>(); + + params.put("finder", parseFlutterLocator(scrollTo)); + Optional.ofNullable(scrollView) + .ifPresent(scrollView -> params.put("scrollView", scrollView)); + Optional.ofNullable(delta) + .ifPresent(delta -> params.put("delta", delta)); + Optional.ofNullable(maxScrolls) + .ifPresent(maxScrolls -> params.put("delta", maxScrolls)); + Optional.ofNullable(settleBetweenScrollsTimeout) + .ifPresent(timeout -> params.put("delta", settleBetweenScrollsTimeout)); + Optional.ofNullable(scrollDirection) + .ifPresent(direction -> params.put("scrollDirection", direction.getDirection())); + Optional.ofNullable(dragDuration) + .ifPresent(direction -> params.put("dragDuration", dragDuration.getSeconds())); + + return Collections.unmodifiableMap(params); + } + + @Getter + public static enum ScrollDirection { + UP("up"), + RIGHT("right"), + DOWN("down"), + LEFT("left"); + + private final String direction; + + ScrollDirection(String direction) { + this.direction = direction; + } + } +} diff --git a/src/main/java/io/appium/java_client/flutter/commands/WaitParameter.java b/src/main/java/io/appium/java_client/flutter/commands/WaitParameter.java new file mode 100644 index 000000000..89e0a19cf --- /dev/null +++ b/src/main/java/io/appium/java_client/flutter/commands/WaitParameter.java @@ -0,0 +1,38 @@ +package io.appium.java_client.flutter.commands; + +import com.google.common.base.Preconditions; +import io.appium.java_client.AppiumBy; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.openqa.selenium.WebElement; + +import java.time.Duration; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +@Accessors(chain = true) +@Getter +@Setter +public class WaitParameter extends FlutterCommandParameter { + private WebElement element; + private AppiumBy.FlutterBy locator; + private Duration timeout; + + @Override + public Map toJson() { + Preconditions.checkArgument(element != null || locator != null, + "Must supply a valid element or locator to wait for"); + Map params = new HashMap<>(); + Optional.ofNullable(element) + .ifPresent(element -> params.put("element", element)); + Optional.ofNullable(locator) + .ifPresent(locator -> params.put("locator", parseFlutterLocator(locator))); + Optional.ofNullable(timeout) + .ifPresent(timeout -> params.put("timeout", timeout)); + + return Collections.unmodifiableMap(params); + } +} diff --git a/src/main/java/io/appium/java_client/remote/AutomationName.java b/src/main/java/io/appium/java_client/remote/AutomationName.java index 7df904c4d..e941d516b 100644 --- a/src/main/java/io/appium/java_client/remote/AutomationName.java +++ b/src/main/java/io/appium/java_client/remote/AutomationName.java @@ -38,4 +38,6 @@ public interface AutomationName { // Third-party drivers // https://github.com/YOU-i-Labs/appium-youiengine-driver String YOUI_ENGINE = "youiengine"; + //https://github.com/AppiumTestDistribution/appium-flutter-integration-driver + String FLUTTER_INTEGRATION = "FlutterIntegration"; }