diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 93527e84a..7a694ccc8 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -35,22 +35,9 @@ 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 - # 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 - java: 17 platform: ubuntu-latest e2e-tests: flutter-android - - java: 21 - platform: ubuntu-latest fail-fast: false runs-on: ${{ matrix.platform }} @@ -110,17 +97,29 @@ 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 }} + script: | + pwd + mkdir ${{ github.workspace }}/logs + ls + ./gradlew e2eFlutterTest -Pplatform="android" -Pselenium.version=$latest_snapshot -PisCI -PflutterApp=${{ env.FLUTTER_ANDROID_APP }} --tests "io.appium.java_client.android.CommandTest.testCameraMocking" api-level: ${{ env.ANDROID_SDK_VERSION }} avd-name: ${{ env.ANDROID_EMU_NAME }} disable-spellchecker: true disable-animations: true target: ${{ env.ANDROID_EMU_TARGET }} + - name: upload appium logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: appium-logs + path: ${{ github.workspace }}/logs + - name: Select Xcode if: matrix.e2e-tests == 'ios' || matrix.e2e-tests == 'flutter-ios' uses: maxim-lobanov/setup-xcode@v1 diff --git a/src/e2eFlutterTest/java/io/appium/java_client/android/BaseFlutterTest.java b/src/e2eFlutterTest/java/io/appium/java_client/android/BaseFlutterTest.java index b2dc6f1eb..a0dd5ccaa 100644 --- a/src/e2eFlutterTest/java/io/appium/java_client/android/BaseFlutterTest.java +++ b/src/e2eFlutterTest/java/io/appium/java_client/android/BaseFlutterTest.java @@ -10,6 +10,7 @@ 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 io.appium.java_client.service.local.flags.GeneralServerFlag; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; @@ -43,6 +44,10 @@ public static void beforeClass() { service = new AppiumServiceBuilder() .withIPAddress("127.0.0.1") .usingPort(PORT) + // Flutter driver mocking command requires adb_shell permission to set certain permissions + // to the AUT. This can be removed once the server logic is updated to use a different approach + // for setting the permission + .withArgument(GeneralServerFlag.ALLOW_INSECURE, "adb_shell") .build(); service.start(); } @@ -52,11 +57,14 @@ void startSession() throws MalformedURLException { FlutterDriverOptions flutterOptions = new FlutterDriverOptions() .setFlutterServerLaunchTimeout(Duration.ofMinutes(2)) .setFlutterSystemPort(9999) - .setFlutterElementWaitTimeout(Duration.ofSeconds(10)); + .setFlutterElementWaitTimeout(Duration.ofSeconds(10)) + .setFlutterEnableMockCamera(true); + if (IS_ANDROID) { driver = new FlutterAndroidDriver(service.getUrl(), flutterOptions .setUiAutomator2Options(new UiAutomator2Options() .setApp(System.getProperty("flutterApp")) + .setAutoGrantPermissions(true) .eventTimings()) ); } else { diff --git a/src/e2eFlutterTest/java/io/appium/java_client/android/CommandTest.java b/src/e2eFlutterTest/java/io/appium/java_client/android/CommandTest.java index efee1c74b..8b970993f 100644 --- a/src/e2eFlutterTest/java/io/appium/java_client/android/CommandTest.java +++ b/src/e2eFlutterTest/java/io/appium/java_client/android/CommandTest.java @@ -1,15 +1,23 @@ package io.appium.java_client.android; import io.appium.java_client.AppiumBy; +import io.appium.java_client.TestUtils; 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.OutputType; import org.openqa.selenium.Point; +import org.openqa.selenium.TakesScreenshot; import org.openqa.selenium.WebElement; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; + import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -115,4 +123,34 @@ void testDragAndDropCommand() { assertEquals(driver.findElement(AppiumBy.flutterText("The box is dropped")).getText(), "The box is dropped"); } + + @Test + void testCameraMocking() throws IOException { + try { + System.out.printf("Directory Path: " + System.getProperty("user.dir")); + driver.findElement(BaseFlutterTest.LOGIN_BUTTON).click(); + openScreen("Image Picker"); + + final String successQr = driver.injectMockImage( + new File(String.valueOf(TestUtils.resourcePathToAbsolutePath("success_qr.png")))); + driver.injectMockImage(new File(String.valueOf(TestUtils.resourcePathToAbsolutePath("second_qr.png")))); + + driver.findElement(AppiumBy.flutterKey("capture_image")).click(); + driver.findElement(AppiumBy.flutterText("PICK")).click(); + assertEquals(driver.findElement(AppiumBy.flutterText("SecondInjectedImage")).getText(), + "SecondInjectedImage"); + assertTrue(driver.findElement(AppiumBy.flutterText("SecondInjectedImage")).isDisplayed()); + + driver.activateInjectedImage(successQr); + + driver.findElement(AppiumBy.flutterKey("capture_image")).click(); + driver.findElement(AppiumBy.flutterText("PICK")).click(); + assertEquals(driver.findElement(AppiumBy.flutterText("Success!")).getText(), "Success!"); + assertTrue(driver.findElement(AppiumBy.flutterText("Success!")).isDisplayed()); + } catch (Exception e) { + File image = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE); + Files.copy(image.toPath(), + new FileOutputStream(System.getProperty("user.dir") + "/logs/screenshot.png")); + } + } } diff --git a/src/e2eFlutterTest/resources/second_qr.png b/src/e2eFlutterTest/resources/second_qr.png new file mode 100644 index 000000000..355548c30 Binary files /dev/null and b/src/e2eFlutterTest/resources/second_qr.png differ diff --git a/src/e2eFlutterTest/resources/success_qr.png b/src/e2eFlutterTest/resources/success_qr.png new file mode 100644 index 000000000..8896d86f6 Binary files /dev/null and b/src/e2eFlutterTest/resources/success_qr.png differ diff --git a/src/main/java/io/appium/java_client/flutter/CanExecuteFlutterScripts.java b/src/main/java/io/appium/java_client/flutter/CanExecuteFlutterScripts.java index f8eaf3af4..e844388be 100644 --- a/src/main/java/io/appium/java_client/flutter/CanExecuteFlutterScripts.java +++ b/src/main/java/io/appium/java_client/flutter/CanExecuteFlutterScripts.java @@ -3,6 +3,8 @@ import io.appium.java_client.flutter.commands.FlutterCommandParameter; import org.openqa.selenium.JavascriptExecutor; +import java.util.Map; + public interface CanExecuteFlutterScripts extends JavascriptExecutor { /** @@ -13,8 +15,19 @@ public interface CanExecuteFlutterScripts extends JavascriptExecutor { * @return The result of executing the script. */ default Object executeFlutterCommand(String scriptName, FlutterCommandParameter parameter) { + return executeFlutterCommand(scriptName, parameter.toJson()); + } + + /** + * Executes a Flutter-specific script using JavascriptExecutor. + * + * @param scriptName The name of the Flutter script to execute. + * @param args The args for the Flutter command in Map format. + * @return The result of executing the script. + */ + default Object executeFlutterCommand(String scriptName, Map args) { String commandName = String.format("flutter: %s", scriptName); - return executeScript(commandName, parameter.toJson()); + return executeScript(commandName, args); } } diff --git a/src/main/java/io/appium/java_client/flutter/FlutterDriverOptions.java b/src/main/java/io/appium/java_client/flutter/FlutterDriverOptions.java index e50b5d134..6a00c0510 100644 --- a/src/main/java/io/appium/java_client/flutter/FlutterDriverOptions.java +++ b/src/main/java/io/appium/java_client/flutter/FlutterDriverOptions.java @@ -2,6 +2,7 @@ import io.appium.java_client.android.options.UiAutomator2Options; import io.appium.java_client.flutter.options.SupportsFlutterElementWaitTimeoutOption; +import io.appium.java_client.flutter.options.SupportsFlutterEnableMockCamera; import io.appium.java_client.flutter.options.SupportsFlutterServerLaunchTimeoutOption; import io.appium.java_client.flutter.options.SupportsFlutterSystemPortOption; import io.appium.java_client.ios.options.XCUITestOptions; @@ -17,7 +18,8 @@ public class FlutterDriverOptions extends BaseOptions implements SupportsFlutterSystemPortOption, SupportsFlutterServerLaunchTimeoutOption, - SupportsFlutterElementWaitTimeoutOption { + SupportsFlutterElementWaitTimeoutOption, + SupportsFlutterEnableMockCamera { public FlutterDriverOptions() { setDefaultOptions(); diff --git a/src/main/java/io/appium/java_client/flutter/FlutterIntegrationTestDriver.java b/src/main/java/io/appium/java_client/flutter/FlutterIntegrationTestDriver.java index dce74507c..4eb74e82a 100644 --- a/src/main/java/io/appium/java_client/flutter/FlutterIntegrationTestDriver.java +++ b/src/main/java/io/appium/java_client/flutter/FlutterIntegrationTestDriver.java @@ -19,5 +19,6 @@ public interface FlutterIntegrationTestDriver extends WebDriver, SupportsGestureOnFlutterElements, SupportsScrollingOfFlutterElements, - SupportsWaitingForFlutterElements { + SupportsWaitingForFlutterElements, + SupportsFlutterCameraMocking { } diff --git a/src/main/java/io/appium/java_client/flutter/SupportsFlutterCameraMocking.java b/src/main/java/io/appium/java_client/flutter/SupportsFlutterCameraMocking.java new file mode 100644 index 000000000..364c3dae4 --- /dev/null +++ b/src/main/java/io/appium/java_client/flutter/SupportsFlutterCameraMocking.java @@ -0,0 +1,47 @@ +package io.appium.java_client.flutter; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Base64; +import java.util.Map; + +/** + * This interface extends {@link CanExecuteFlutterScripts} and provides methods + * to support mocking of camera inputs in Flutter applications. + */ +public interface SupportsFlutterCameraMocking extends CanExecuteFlutterScripts { + + /** + * Injects a mock image into the Flutter application using the provided file. + * + * @param image the image file to be mocked + * @return an {@code Integer} representing the result of the injection operation + * @throws IOException if an I/O error occurs while reading the image file + */ + default String injectMockImage(File image) throws IOException { + String base64EncodedImage = Base64.getEncoder().encodeToString(Files.readAllBytes(image.toPath())); + return injectMockImage(base64EncodedImage); + } + + /** + * Injects a mock image into the Flutter application using the provided Base64-encoded image string. + * + * @param base64Image the Base64-encoded string representation of the image + * @return an {@code Integer} representing the result of the injection operation + */ + default String injectMockImage(String base64Image) { + return (String) executeFlutterCommand("injectImage", Map.of( + "base64Image", base64Image + )); + } + + /** + * Activates the injected image identified by the specified image ID in the Flutter application. + * + * @param imageId the ID of the injected image to activate + */ + default void activateInjectedImage(String imageId) { + executeFlutterCommand("activateInjectedImage", Map.of("imageId", imageId)); + } +} diff --git a/src/main/java/io/appium/java_client/flutter/options/SupportsFlutterEnableMockCamera.java b/src/main/java/io/appium/java_client/flutter/options/SupportsFlutterEnableMockCamera.java new file mode 100644 index 000000000..baffaf96d --- /dev/null +++ b/src/main/java/io/appium/java_client/flutter/options/SupportsFlutterEnableMockCamera.java @@ -0,0 +1,33 @@ +package io.appium.java_client.flutter.options; + +import io.appium.java_client.remote.options.BaseOptions; +import io.appium.java_client.remote.options.CanSetCapability; +import org.openqa.selenium.Capabilities; + +import java.util.Optional; + +import static io.appium.java_client.internal.CapabilityHelpers.toSafeBoolean; + +public interface SupportsFlutterEnableMockCamera> extends + Capabilities, CanSetCapability { + String FLUTTER_ENABLE_MOCK_CAMERA_OPTION = "flutterEnableMockCamera"; + + /** + * Sets the 'flutterEnableMockCamera' capability to the specified value. + * + * @param value the value to set for the 'flutterEnableMockCamera' capability + * @return an instance of type {@code T} with the updated capability set + */ + default T setFlutterEnableMockCamera(boolean value) { + return amend(FLUTTER_ENABLE_MOCK_CAMERA_OPTION, value); + } + + /** + * Retrieves the current value of the 'flutterEnableMockCamera' capability, if available. + * + * @return an {@code Optional} containing the current value of the capability, + */ + default Optional doesFlutterEnableMockCamera() { + return Optional.ofNullable(toSafeBoolean(getCapability(FLUTTER_ENABLE_MOCK_CAMERA_OPTION))); + } +}