Skip to content
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

feat: Add support for FlutterIOSDriver #2206

Merged
merged 14 commits into from
Jul 24, 2024
23 changes: 16 additions & 7 deletions .github/workflows/gradle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ env:
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"
FLUTTER_IOS_APP: "https://github.com/AppiumTestDistribution/appium-flutter-server/releases/latest/download/ios.zip"

jobs:
build:
Expand All @@ -38,6 +39,10 @@ jobs:
# 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
# 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: flutter-ios
- java: 17
platform: ubuntu-latest
e2e-tests: android
Expand Down Expand Up @@ -77,21 +82,21 @@ jobs:
./gradlew clean build -PisCI -Pselenium.version=$latest_snapshot

- name: Install Node.js
if: matrix.e2e-tests == 'android' || matrix.e2e-tests == 'ios' || matrix.e2e-tests == 'flutter-android'
if: matrix.e2e-tests == 'android' || matrix.e2e-tests == 'ios' || matrix.e2e-tests == 'flutter-android' || matrix.e2e-tests == 'flutter-ios'
sudharsan-selvaraj marked this conversation as resolved.
Show resolved Hide resolved
uses: actions/setup-node@v4
with:
node-version: 'lts/*'

- name: Install Appium
if: matrix.e2e-tests == 'android' || matrix.e2e-tests == 'ios' || matrix.e2e-tests == 'flutter-android'
if: matrix.e2e-tests == 'android' || matrix.e2e-tests == 'ios' || matrix.e2e-tests == 'flutter-android' || matrix.e2e-tests == 'flutter-ios'
sudharsan-selvaraj marked this conversation as resolved.
Show resolved Hide resolved
run: npm install --location=global appium

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

- name: Run Android E2E tests
Expand All @@ -117,22 +122,26 @@ jobs:
target: ${{ env.ANDROID_EMU_TARGET }}

- name: Select Xcode
if: matrix.e2e-tests == 'ios'
if: matrix.e2e-tests == 'ios' || matrix.e2e-tests == 'flutter-ios'
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: "${{ env.XCODE_VERSION }}"
- name: Prepare iOS simulator
if: matrix.e2e-tests == 'ios'
if: matrix.e2e-tests == 'ios' || matrix.e2e-tests == 'flutter-ios'
uses: futureware-tech/simulator-action@v3
with:
model: "${{ env.IOS_DEVICE_NAME }}"
os_version: "${{ env.IOS_PLATFORM_VERSION }}"
- name: Install XCUITest driver
if: matrix.e2e-tests == 'ios'
if: matrix.e2e-tests == 'ios' || matrix.e2e-tests == 'flutter-ios'
run: appium driver install xcuitest
- name: Prebuild XCUITest driver
if: matrix.e2e-tests == 'ios'
if: matrix.e2e-tests == 'ios' || matrix.e2e-tests == 'flutter-ios'
run: appium driver run xcuitest build-wda
- name: Run iOS E2E tests
if: matrix.e2e-tests == 'ios'
run: ./gradlew e2eIosTest -PisCI -Pselenium.version=$latest_snapshot

- name: Run Flutter iOS E2E tests
if: matrix.e2e-tests == 'flutter-ios'
run: ./gradlew e2eFlutterTest -Pplatform="ios" -Pselenium.version=$latest_snapshot -PisCI -PflutterApp=${{ env.FLUTTER_IOS_APP }}
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,23 @@

import io.appium.java_client.AppiumBy;
import io.appium.java_client.android.options.UiAutomator2Options;
import io.appium.java_client.flutter.FlutterDriver;
import io.appium.java_client.flutter.FlutterDriverOptions;
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.flutter.ios.FlutterIOSDriver;
import io.appium.java_client.ios.options.XCUITestOptions;
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.time.Duration;
import java.util.Optional;

class BaseFlutterTest {
Expand All @@ -29,7 +32,7 @@ class BaseFlutterTest {
protected static final int PORT = 4723;

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

/**
Expand All @@ -46,16 +49,24 @@ public static void beforeClass() {

@BeforeEach
public void startSession() throws MalformedURLException {
FlutterDriverOptions flutterOptions = new FlutterDriverOptions()
.setFlutterSystemPort(9999)
.setFlutterServerLaunchTimeout(Duration.ofSeconds(30))
.setFlutterElementWaitTimeout(Duration.ofSeconds(3));
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);
driver = new FlutterAndroidDriver(service.getUrl(), flutterOptions
.setUiAutomator2Options(new UiAutomator2Options()
.setApp(System.getProperty("flutterApp"))
.eventTimings())
);
} else {
throw new InvalidArgumentException(
"Currently flutter driver implementation only supports android platform");
driver = new FlutterIOSDriver(service.getUrl(), flutterOptions
.setXCUITestOptions(new XCUITestOptions()
.setApp(System.getProperty("flutterApp"))
.setWdaLaunchTimeout(Duration.ofMinutes(2))
.eventTimings()
)
);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package io.appium.java_client.android;

import io.appium.java_client.AppiumBy;
import io.appium.java_client.flutter.commands.DoubleClickParameter;
import io.appium.java_client.flutter.commands.DragAndDropParameter;
import io.appium.java_client.flutter.commands.LongPressParameter;
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.Point;
import org.openqa.selenium.WebElement;

import static org.junit.jupiter.api.Assertions.assertEquals;
Expand Down Expand Up @@ -59,4 +63,56 @@ public void testScrollTillVisibleCommand() {
assertFalse(Boolean.parseBoolean(lastElement.getAttribute("displayed")));
}

@Test
public void testDoubleClickCommand() {
sudharsan-selvaraj marked this conversation as resolved.
Show resolved Hide resolved
driver.findElement(BaseFlutterTest.LOGIN_BUTTON).click();
openScreen("Double Tap");

WebElement doubleTapButton = driver
.findElement(AppiumBy.flutterKey("double_tap_button"))
.findElement(AppiumBy.flutterText("Double Tap"));
assertEquals("Double Tap", doubleTapButton.getText());

AppiumBy.FlutterBy okButton = AppiumBy.flutterText("Ok");
AppiumBy.FlutterBy successPopup = AppiumBy.flutterTextContaining("Successful");

driver.performDoubleClick(new DoubleClickParameter().setElement(doubleTapButton));
assertEquals(driver.findElement(successPopup).getText(), "Double Tap Successful");
driver.findElement(okButton).click();

driver.performDoubleClick(new DoubleClickParameter()
.setElement(doubleTapButton)
.setOffset(new Point(10, 2))
);
assertEquals(driver.findElement(successPopup).getText(), "Double Tap Successful");
driver.findElement(okButton).click();
}

@Test
public void testLongPressCommand() {
driver.findElement(BaseFlutterTest.LOGIN_BUTTON).click();
openScreen("Long Press");

AppiumBy.FlutterBy successPopup = AppiumBy.flutterText("It was a long press");
WebElement longPressButton = driver
.findElement(AppiumBy.flutterKey("long_press_button"));

driver.performLongPress(new LongPressParameter().setElement(longPressButton));
assertEquals(driver.findElement(successPopup).getText(), "It was a long press");
assertTrue(driver.findElement(successPopup).isDisplayed());
}

@Test
public void testDragAndDropCommand() {
driver.findElement(BaseFlutterTest.LOGIN_BUTTON).click();
openScreen("Drag & Drop");

driver.performDragAndDrop(new DragAndDropParameter(
driver.findElement(AppiumBy.flutterKey("drag_me")),
driver.findElement(AppiumBy.flutterKey("drop_zone"))
));
assertTrue(driver.findElement(AppiumBy.flutterText("The box is dropped")).isDisplayed());
assertEquals(driver.findElement(AppiumBy.flutterText("The box is dropped")).getText(), "The box is dropped");

}
}
23 changes: 23 additions & 0 deletions src/main/java/io/appium/java_client/flutter/FlutterDriver.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.appium.java_client.flutter;

import org.openqa.selenium.WebDriver;

/**
* The {@code FlutterDriver} interface represents a driver that controls interactions with
* Flutter applications, extending WebDriver and providing additional capabilities for
* interacting with Flutter-specific elements and behaviors.
*
* <p> This interface serves as a common entity for drivers that support Flutter applications
* on different platforms, such as Android and iOS. </p>
*
* @see WebDriver
* @see SupportsGestureOnFlutterElements
* @see SupportsScrollingOfFlutterElements
* @see SupportsWaitingForFlutterElements
*/
public interface FlutterDriver extends
sudharsan-selvaraj marked this conversation as resolved.
Show resolved Hide resolved
WebDriver,
mykola-mokhnach marked this conversation as resolved.
Show resolved Hide resolved
SupportsGestureOnFlutterElements,
SupportsScrollingOfFlutterElements,
SupportsWaitingForFlutterElements {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package io.appium.java_client.flutter;

import io.appium.java_client.android.options.UiAutomator2Options;
import io.appium.java_client.flutter.options.SupportsFlutterElementWaitTimeoutOption;
import io.appium.java_client.flutter.options.SupportsFlutterServerLaunchTimeoutOption;
import io.appium.java_client.flutter.options.SupportsFlutterSystemPortOption;
import io.appium.java_client.ios.options.XCUITestOptions;
import io.appium.java_client.remote.AutomationName;
import io.appium.java_client.remote.options.BaseOptions;
import org.openqa.selenium.Capabilities;

import java.util.Map;

/**
* https://github.com/AppiumTestDistribution/appium-flutter-integration-driver#capabilities-for-appium-flutter-integration-driver
*/
public class FlutterDriverOptions extends BaseOptions<FlutterDriverOptions> implements
SupportsFlutterSystemPortOption<FlutterDriverOptions>,
SupportsFlutterServerLaunchTimeoutOption<FlutterDriverOptions>,
SupportsFlutterElementWaitTimeoutOption<FlutterDriverOptions> {

public FlutterDriverOptions() {
setCommonOptions();
}

public FlutterDriverOptions(Capabilities source) {
super(source);
setCommonOptions();
}

public FlutterDriverOptions(Map<String, ?> source) {
super(source);
setCommonOptions();
}

public FlutterDriverOptions setUiAutomator2Options(UiAutomator2Options uiAutomator2Options) {
return merge(uiAutomator2Options);
sudharsan-selvaraj marked this conversation as resolved.
Show resolved Hide resolved
}

public FlutterDriverOptions setXCUITestOptions(XCUITestOptions xcuiTestOptions) {
return merge(xcuiTestOptions);
}

private void setCommonOptions() {
setAutomationName(AutomationName.FLUTTER_INTEGRATION);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package io.appium.java_client.flutter;

import io.appium.java_client.flutter.commands.DoubleClickParameter;
import io.appium.java_client.flutter.commands.DragAndDropParameter;
import io.appium.java_client.flutter.commands.LongPressParameter;

public interface SupportsGestureOnFlutterElements extends CanExecuteFlutterScripts {

/**
* Performs a double click action on an element.
*
* @param parameter The parameters for double-clicking, specifying element details.
*/
default void performDoubleClick(DoubleClickParameter parameter) {
executeFlutterCommand("doubleClick", parameter);
}

/**
* Performs a long press action on an element.
*
* @param parameter The parameters for long pressing, specifying element details.
*/
default void performLongPress(LongPressParameter parameter) {
executeFlutterCommand("longPress", parameter);
}

/**
* Performs a drag-and-drop action between two elements.
*
* @param parameter The parameters for drag-and-drop, specifying source and target elements.
*/
default void performDragAndDrop(DragAndDropParameter parameter) {
executeFlutterCommand("dragAndDrop", parameter);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@

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.flutter.FlutterDriver;
import io.appium.java_client.service.local.AppiumDriverLocalService;
import io.appium.java_client.service.local.AppiumServiceBuilder;
import org.openqa.selenium.Capabilities;
Expand All @@ -16,9 +15,7 @@
/**
* Custom AndroidDriver implementation with additional Flutter-specific capabilities.
*/
public class FlutterAndroidDriver extends AndroidDriver implements
SupportsWaitingForFlutterElements,
SupportsScrollingOfFlutterElements {
public class FlutterAndroidDriver extends AndroidDriver implements FlutterDriver {

public FlutterAndroidDriver(HttpCommandExecutor executor, Capabilities capabilities) {
super(executor, capabilities);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package io.appium.java_client.flutter.commands;

import com.google.common.base.Preconditions;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import org.openqa.selenium.Point;
import org.openqa.selenium.WebElement;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

@Accessors(chain = true)
@Setter
@Getter
public class DoubleClickParameter extends FlutterCommandParameter {
private WebElement element;
private Point offset;


@Override
public Map<String, Object> toJson() {
Preconditions.checkArgument(element != null || offset != null,
"Must supply a valid element or offset to perform flutter gesture event");
sudharsan-selvaraj marked this conversation as resolved.
Show resolved Hide resolved

Map<String, Object> params = new HashMap<>();
Optional.ofNullable(element).ifPresent(element -> params.put("origin", element));
Optional.ofNullable(offset).ifPresent(offset ->
params.put("offset", Map.of("x", offset.getX(), "y", offset.getY())));
return Collections.unmodifiableMap(params);
}
}
Loading
Loading