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 0f056e51a..772b316fb 100644 --- a/src/e2eFlutterTest/java/io/appium/java_client/android/CommandTest.java +++ b/src/e2eFlutterTest/java/io/appium/java_client/android/CommandTest.java @@ -160,4 +160,37 @@ void testCameraMocking() throws IOException { driver.findElement(AppiumBy.flutterText("PICK")).click(); assertTrue(driver.findElement(AppiumBy.flutterText("Success!")).isDisplayed()); } + + @Test + void testScrollTillVisibleForAncestor() { + WebElement loginButton = driver.findElement(BaseFlutterTest.LOGIN_BUTTON); + loginButton.click(); + openScreen("Nested Scroll"); + + AppiumBy.FlutterBy ancestorBy = AppiumBy.flutterAncestor( + AppiumBy.flutterText("Child 2"), + AppiumBy.flutterKey("parent_card_4") + ); + + assertEquals(0, driver.findElements(ancestorBy).size()); + driver.scrollTillVisible(new ScrollParameter(ancestorBy)); + assertEquals(1, driver.findElements(ancestorBy).size()); + } + + @Test + void testScrollTillVisibleForDescendant() { + WebElement loginButton = driver.findElement(BaseFlutterTest.LOGIN_BUTTON); + loginButton.click(); + openScreen("Nested Scroll"); + + AppiumBy.FlutterBy descendantBy = AppiumBy.flutterDescendant( + AppiumBy.flutterKey("parent_card_4"), + AppiumBy.flutterText("Child 2") + ); + + assertEquals(0, driver.findElements(descendantBy).size()); + driver.scrollTillVisible(new ScrollParameter(descendantBy)); + // Make sure the card is visible after scrolling + assertEquals(1, driver.findElements(descendantBy).size()); + } } diff --git a/src/e2eFlutterTest/java/io/appium/java_client/android/FinderTests.java b/src/e2eFlutterTest/java/io/appium/java_client/android/FinderTests.java index e8f78e414..f00301885 100644 --- a/src/e2eFlutterTest/java/io/appium/java_client/android/FinderTests.java +++ b/src/e2eFlutterTest/java/io/appium/java_client/android/FinderTests.java @@ -5,6 +5,7 @@ import org.openqa.selenium.WebElement; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; class FinderTests extends BaseFlutterTest { @@ -51,4 +52,33 @@ void testFlutterSemanticsLabel() { assertEquals(messageField.getText(), "Hello world"); } + + @Test + void testFlutterDescendant() { + WebElement loginButton = driver.findElement(BaseFlutterTest.LOGIN_BUTTON); + loginButton.click(); + openScreen("Nested Scroll"); + + AppiumBy descendantBy = AppiumBy.flutterDescendant( + AppiumBy.flutterKey("parent_card_1"), + AppiumBy.flutterText("Child 2") + ); + WebElement childElement = driver.findElement(descendantBy); + assertEquals("Child 2", + childElement.getText()); + } + + @Test + void testFlutterAncestor() { + WebElement loginButton = driver.findElement(BaseFlutterTest.LOGIN_BUTTON); + loginButton.click(); + openScreen("Nested Scroll"); + + AppiumBy ancestorBy = AppiumBy.flutterAncestor( + AppiumBy.flutterText("Child 2"), + AppiumBy.flutterKey("parent_card_1") + ); + WebElement parentElement = driver.findElement(ancestorBy); + assertTrue(parentElement.isDisplayed()); + } } diff --git a/src/main/java/io/appium/java_client/AppiumBy.java b/src/main/java/io/appium/java_client/AppiumBy.java index 4314dc891..1c24b29c1 100644 --- a/src/main/java/io/appium/java_client/AppiumBy.java +++ b/src/main/java/io/appium/java_client/AppiumBy.java @@ -23,9 +23,12 @@ import org.openqa.selenium.By.Remotable; import org.openqa.selenium.SearchContext; import org.openqa.selenium.WebElement; +import org.openqa.selenium.json.Json; import java.io.Serializable; +import java.util.HashMap; import java.util.List; +import java.util.Map; import static com.google.common.base.Strings.isNullOrEmpty; @@ -250,6 +253,57 @@ public static FlutterBy flutterSemanticsLabel(final String semanticsLabel) { return new ByFlutterSemanticsLabel(semanticsLabel); } + /** + * This locator strategy is available in FlutterIntegration Driver mode since version 1.4.0. + * + * @param of represents the parent widget locator + * @param matching represents the descendant widget locator to match + * @param matchRoot determines whether to include the root widget in the search + * @param skipOffstage determines whether to skip offstage widgets + * @return an instance of {@link AppiumBy.ByFlutterDescendant} + */ + public static FlutterBy flutterDescendant( + final FlutterBy of, + final FlutterBy matching, + boolean matchRoot, + boolean skipOffstage) { + return new ByFlutterDescendant(of, matching, matchRoot, skipOffstage); + } + + /** + * This locator strategy is available in FlutterIntegration Driver mode since version 1.4.0. + * + * @param of represents the parent widget locator + * @param matching represents the descendant widget locator to match + * @return an instance of {@link AppiumBy.ByFlutterDescendant} + */ + public static FlutterBy flutterDescendant(final FlutterBy of, final FlutterBy matching) { + return flutterDescendant(of, matching, false, true); + } + + /** + * This locator strategy is available in FlutterIntegration Driver mode since version 1.4.0. + * + * @param of represents the child widget locator + * @param matching represents the ancestor widget locator to match + * @param matchRoot determines whether to include the root widget in the search + * @return an instance of {@link AppiumBy.ByFlutterAncestor} + */ + public static FlutterBy flutterAncestor(final FlutterBy of, final FlutterBy matching, boolean matchRoot) { + return new ByFlutterAncestor(of, matching, matchRoot); + } + + /** + * This locator strategy is available in FlutterIntegration Driver mode since version 1.4.0. + * + * @param of represents the child widget locator + * @param matching represents the ancestor widget locator to match + * @return an instance of {@link AppiumBy.ByFlutterAncestor} + */ + public static FlutterBy flutterAncestor(final FlutterBy of, final FlutterBy matching) { + return flutterAncestor(of, matching, false); + } + public static class ByAccessibilityId extends AppiumBy implements Serializable { public ByAccessibilityId(String accessibilityId) { super("accessibility id", accessibilityId, "accessibilityId"); @@ -328,6 +382,32 @@ protected FlutterBy(String selector, String locatorString, String locatorName) { } } + public abstract static class FlutterByHierarchy extends FlutterBy { + private static final Json JSON = new Json(); + + protected FlutterByHierarchy( + String selector, + FlutterBy of, + FlutterBy matching, + Map properties, + String locatorName) { + super(selector, formatLocator(of, matching, properties), locatorName); + } + + static Map parseFlutterLocator(FlutterBy by) { + Parameters params = by.getRemoteParameters(); + return Map.of("using", params.using(), "value", params.value()); + } + + static String formatLocator(FlutterBy of, FlutterBy matching, Map properties) { + Map locator = new HashMap<>(); + locator.put("of", parseFlutterLocator(of)); + locator.put("matching", parseFlutterLocator(matching)); + locator.put("parameters", properties); + return JSON.toJson(locator); + } + } + public static class ByFlutterType extends FlutterBy implements Serializable { protected ByFlutterType(String locatorString) { super("-flutter type", locatorString, "flutterType"); @@ -358,4 +438,23 @@ protected ByFlutterTextContaining(String locatorString) { } } + public static class ByFlutterDescendant extends FlutterByHierarchy implements Serializable { + protected ByFlutterDescendant(FlutterBy of, FlutterBy matching, boolean matchRoot, boolean skipOffstage) { + super( + "-flutter descendant", + of, + matching, + Map.of("matchRoot", matchRoot, "skipOffstage", skipOffstage), "flutterDescendant"); + } + } + + public static class ByFlutterAncestor extends FlutterByHierarchy implements Serializable { + protected ByFlutterAncestor(FlutterBy of, FlutterBy matching, boolean matchRoot) { + super( + "-flutter ancestor", + of, + matching, + Map.of("matchRoot", matchRoot), "flutterAncestor"); + } + } }