From cb64b056e3e0de93085b201e514734a6e37c2b91 Mon Sep 17 00:00:00 2001 From: sudharsan selvaraj Date: Thu, 9 Oct 2025 10:26:02 +0530 Subject: [PATCH 1/7] feat: add support for ancestor and descendant flutter locators --- .../java_client/android/FinderTests.java | 30 +++++++ .../java/io/appium/java_client/AppiumBy.java | 88 ++++++++++++++++++- 2 files changed, 115 insertions(+), 3 deletions(-) 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..998bd4197 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..fbb4f1bdb 100644 --- a/src/main/java/io/appium/java_client/AppiumBy.java +++ b/src/main/java/io/appium/java_client/AppiumBy.java @@ -17,6 +17,7 @@ package io.appium.java_client; import com.google.common.base.Preconditions; +import com.google.gson.Gson; import lombok.EqualsAndHashCode; import lombok.Getter; import org.openqa.selenium.By; @@ -25,7 +26,9 @@ import org.openqa.selenium.WebElement; import java.io.Serializable; +import java.util.HashMap; import java.util.List; +import java.util.Map; import static com.google.common.base.Strings.isNullOrEmpty; @@ -169,9 +172,9 @@ public static By custom(final String selector) { * as for OpenCV library. * @return an instance of {@link ByImage} * @see - * The documentation on Image Comparison Features + * The documentation on Image Comparison Features * @see - * The settings available for lookup fine-tuning + * The settings available for lookup fine-tuning * @since Appium 1.8.2 */ public static By image(final String b64Template) { @@ -250,6 +253,53 @@ public static FlutterBy flutterSemanticsLabel(final String semanticsLabel) { return new ByFlutterSemanticsLabel(semanticsLabel); } + /** + * This locator strategy is available in FlutterIntegration Driver mode. + * + * @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. + * + * @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. + * + * @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. + * + * @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 +378,27 @@ protected FlutterBy(String selector, String locatorString, String locatorName) { } } + public abstract static class FlutterByHierarchy extends FlutterBy { + private static final Gson GSON = new Gson(); + + 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 GSON.toJson(locator); + } + } + public static class ByFlutterType extends FlutterBy implements Serializable { protected ByFlutterType(String locatorString) { super("-flutter type", locatorString, "flutterType"); @@ -358,4 +429,15 @@ 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"); + } + } +} \ No newline at end of file From efd5beab7307f26ceeb2c091f074710fc1f73d75 Mon Sep 17 00:00:00 2001 From: sudharsan selvaraj Date: Thu, 9 Oct 2025 10:29:45 +0530 Subject: [PATCH 2/7] add new line --- src/main/java/io/appium/java_client/AppiumBy.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/io/appium/java_client/AppiumBy.java b/src/main/java/io/appium/java_client/AppiumBy.java index fbb4f1bdb..4b728a365 100644 --- a/src/main/java/io/appium/java_client/AppiumBy.java +++ b/src/main/java/io/appium/java_client/AppiumBy.java @@ -440,4 +440,4 @@ protected ByFlutterAncestor(FlutterBy of, FlutterBy matching, boolean matchRoot) super("-flutter ancestor", of, matching, Map.of("matchRoot", matchRoot), "flutterAncestor"); } } -} \ No newline at end of file +} From d1711b98f661591dd4d074399783d6c4515b479b Mon Sep 17 00:00:00 2001 From: sudharsan selvaraj Date: Thu, 9 Oct 2025 10:54:06 +0530 Subject: [PATCH 3/7] Fix styling errors --- .../java_client/android/FinderTests.java | 8 ++--- .../java/io/appium/java_client/AppiumBy.java | 29 +++++++++++++++---- 2 files changed, 27 insertions(+), 10 deletions(-) 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 998bd4197..f00301885 100644 --- a/src/e2eFlutterTest/java/io/appium/java_client/android/FinderTests.java +++ b/src/e2eFlutterTest/java/io/appium/java_client/android/FinderTests.java @@ -54,7 +54,7 @@ void testFlutterSemanticsLabel() { } @Test - void testFlutterDescendant() { + void testFlutterDescendant() { WebElement loginButton = driver.findElement(BaseFlutterTest.LOGIN_BUTTON); loginButton.click(); openScreen("Nested Scroll"); @@ -63,13 +63,13 @@ void testFlutterDescendant() { AppiumBy.flutterKey("parent_card_1"), AppiumBy.flutterText("Child 2") ); - WebElement childElement = driver.findElement(descendantBy); + WebElement childElement = driver.findElement(descendantBy); assertEquals("Child 2", childElement.getText()); } @Test - void testFlutterAncestor() { + void testFlutterAncestor() { WebElement loginButton = driver.findElement(BaseFlutterTest.LOGIN_BUTTON); loginButton.click(); openScreen("Nested Scroll"); @@ -78,7 +78,7 @@ void testFlutterAncestor() { AppiumBy.flutterText("Child 2"), AppiumBy.flutterKey("parent_card_1") ); - WebElement parentElement = driver.findElement(ancestorBy); + 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 4b728a365..d65ef09f6 100644 --- a/src/main/java/io/appium/java_client/AppiumBy.java +++ b/src/main/java/io/appium/java_client/AppiumBy.java @@ -172,9 +172,9 @@ public static By custom(final String selector) { * as for OpenCV library. * @return an instance of {@link ByImage} * @see - * The documentation on Image Comparison Features + * The documentation on Image Comparison Features * @see - * The settings available for lookup fine-tuning + * The settings available for lookup fine-tuning * @since Appium 1.8.2 */ public static By image(final String b64Template) { @@ -262,7 +262,11 @@ public static FlutterBy flutterSemanticsLabel(final String semanticsLabel) { * @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) { + public static FlutterBy flutterDescendant( + final FlutterBy of, + final FlutterBy matching, + boolean matchRoot, + boolean skipOffstage) { return new ByFlutterDescendant(of, matching, matchRoot, skipOffstage); } @@ -381,7 +385,12 @@ protected FlutterBy(String selector, String locatorString, String locatorName) { public abstract static class FlutterByHierarchy extends FlutterBy { private static final Gson GSON = new Gson(); - protected FlutterByHierarchy(String selector, FlutterBy of, FlutterBy matching, Map properties, String locatorName) { + protected FlutterByHierarchy( + String selector, + FlutterBy of, + FlutterBy matching, + Map properties, + String locatorName) { super(selector, formatLocator(of, matching, properties), locatorName); } @@ -431,13 +440,21 @@ 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"); + 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"); + super( + "-flutter ancestor", + of, + matching, + Map.of("matchRoot", matchRoot), "flutterAncestor"); } } } From 521ef10dc58d447fedf800a0dc89dc1dea42babb Mon Sep 17 00:00:00 2001 From: sudharsan selvaraj Date: Thu, 9 Oct 2025 11:40:59 +0530 Subject: [PATCH 4/7] add supported version to javadoc comments --- src/main/java/io/appium/java_client/AppiumBy.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/io/appium/java_client/AppiumBy.java b/src/main/java/io/appium/java_client/AppiumBy.java index d65ef09f6..5a8f63a45 100644 --- a/src/main/java/io/appium/java_client/AppiumBy.java +++ b/src/main/java/io/appium/java_client/AppiumBy.java @@ -254,7 +254,7 @@ public static FlutterBy flutterSemanticsLabel(final String semanticsLabel) { } /** - * This locator strategy is available in FlutterIntegration Driver mode. + * 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 @@ -271,7 +271,7 @@ public static FlutterBy flutterDescendant( } /** - * This locator strategy is available in FlutterIntegration Driver mode. + * 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 @@ -282,7 +282,7 @@ public static FlutterBy flutterDescendant(final FlutterBy of, final FlutterBy ma } /** - * This locator strategy is available in FlutterIntegration Driver mode. + * 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 @@ -294,7 +294,7 @@ public static FlutterBy flutterAncestor(final FlutterBy of, final FlutterBy matc } /** - * This locator strategy is available in FlutterIntegration Driver mode. + * 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 From 7915c155f72b67b3d718c7da3768d955afbc3052 Mon Sep 17 00:00:00 2001 From: sudharsan selvaraj Date: Thu, 9 Oct 2025 16:09:19 +0530 Subject: [PATCH 5/7] Replace GSON with native JSON serializer --- .../java_client/android/CommandTest.java | 32 +++++++++++++++++++ .../java/io/appium/java_client/AppiumBy.java | 5 +-- 2 files changed, 35 insertions(+), 2 deletions(-) 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..5918539fc 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,36 @@ 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)); + assertEquals(1, driver.findElements(descendantBy).size()); + } } diff --git a/src/main/java/io/appium/java_client/AppiumBy.java b/src/main/java/io/appium/java_client/AppiumBy.java index 5a8f63a45..3428ff677 100644 --- a/src/main/java/io/appium/java_client/AppiumBy.java +++ b/src/main/java/io/appium/java_client/AppiumBy.java @@ -24,6 +24,7 @@ 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; @@ -383,7 +384,7 @@ protected FlutterBy(String selector, String locatorString, String locatorName) { } public abstract static class FlutterByHierarchy extends FlutterBy { - private static final Gson GSON = new Gson(); + private static final Json json = new Json(); protected FlutterByHierarchy( String selector, @@ -404,7 +405,7 @@ static String formatLocator(FlutterBy of, FlutterBy matching, Map Date: Thu, 9 Oct 2025 16:17:46 +0530 Subject: [PATCH 6/7] fix checkstyle errors --- src/main/java/io/appium/java_client/AppiumBy.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/io/appium/java_client/AppiumBy.java b/src/main/java/io/appium/java_client/AppiumBy.java index 3428ff677..1c24b29c1 100644 --- a/src/main/java/io/appium/java_client/AppiumBy.java +++ b/src/main/java/io/appium/java_client/AppiumBy.java @@ -17,7 +17,6 @@ package io.appium.java_client; import com.google.common.base.Preconditions; -import com.google.gson.Gson; import lombok.EqualsAndHashCode; import lombok.Getter; import org.openqa.selenium.By; @@ -384,7 +383,7 @@ protected FlutterBy(String selector, String locatorString, String locatorName) { } public abstract static class FlutterByHierarchy extends FlutterBy { - private static final Json json = new Json(); + private static final Json JSON = new Json(); protected FlutterByHierarchy( String selector, @@ -405,7 +404,7 @@ static String formatLocator(FlutterBy of, FlutterBy matching, Map Date: Thu, 9 Oct 2025 16:39:20 +0530 Subject: [PATCH 7/7] add comment to the test --- .../java/io/appium/java_client/android/CommandTest.java | 1 + 1 file changed, 1 insertion(+) 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 5918539fc..772b316fb 100644 --- a/src/e2eFlutterTest/java/io/appium/java_client/android/CommandTest.java +++ b/src/e2eFlutterTest/java/io/appium/java_client/android/CommandTest.java @@ -190,6 +190,7 @@ void testScrollTillVisibleForDescendant() { 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()); } }