Skip to content
Open
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
27 changes: 13 additions & 14 deletions .github/workflows/gradle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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"));
}
}
}
Binary file added src/e2eFlutterTest/resources/second_qr.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/e2eFlutterTest/resources/success_qr.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

/**
Expand All @@ -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<String, Object> args) {
String commandName = String.format("flutter: %s", scriptName);
return executeScript(commandName, parameter.toJson());
return executeScript(commandName, args);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -17,7 +18,8 @@
public class FlutterDriverOptions extends BaseOptions<FlutterDriverOptions> implements
SupportsFlutterSystemPortOption<FlutterDriverOptions>,
SupportsFlutterServerLaunchTimeoutOption<FlutterDriverOptions>,
SupportsFlutterElementWaitTimeoutOption<FlutterDriverOptions> {
SupportsFlutterElementWaitTimeoutOption<FlutterDriverOptions>,
SupportsFlutterEnableMockCamera<FlutterDriverOptions> {

public FlutterDriverOptions() {
setDefaultOptions();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@ public interface FlutterIntegrationTestDriver extends
WebDriver,
SupportsGestureOnFlutterElements,
SupportsScrollingOfFlutterElements,
SupportsWaitingForFlutterElements {
SupportsWaitingForFlutterElements,
SupportsFlutterCameraMocking {
}
Original file line number Diff line number Diff line change
@@ -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));
}
}
Original file line number Diff line number Diff line change
@@ -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<T extends BaseOptions<T>> extends
Capabilities, CanSetCapability<T> {
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<Boolean>} containing the current value of the capability,
*/
default Optional<Boolean> doesFlutterEnableMockCamera() {
return Optional.ofNullable(toSafeBoolean(getCapability(FLUTTER_ENABLE_MOCK_CAMERA_OPTION)));
}
}