Skip to content

Commit cb64b05

Browse files
feat: add support for ancestor and descendant flutter locators
1 parent 958f4b1 commit cb64b05

File tree

2 files changed

+115
-3
lines changed

2 files changed

+115
-3
lines changed

src/e2eFlutterTest/java/io/appium/java_client/android/FinderTests.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import org.openqa.selenium.WebElement;
66

77
import static org.junit.jupiter.api.Assertions.assertEquals;
8+
import static org.junit.jupiter.api.Assertions.assertTrue;
89

910

1011
class FinderTests extends BaseFlutterTest {
@@ -51,4 +52,33 @@ void testFlutterSemanticsLabel() {
5152
assertEquals(messageField.getText(),
5253
"Hello world");
5354
}
55+
56+
@Test
57+
void testFlutterDescendant() {
58+
WebElement loginButton = driver.findElement(BaseFlutterTest.LOGIN_BUTTON);
59+
loginButton.click();
60+
openScreen("Nested Scroll");
61+
62+
AppiumBy descendantBy = AppiumBy.flutterDescendant(
63+
AppiumBy.flutterKey("parent_card_1"),
64+
AppiumBy.flutterText("Child 2")
65+
);
66+
WebElement childElement = driver.findElement(descendantBy);
67+
assertEquals("Child 2",
68+
childElement.getText());
69+
}
70+
71+
@Test
72+
void testFlutterAncestor() {
73+
WebElement loginButton = driver.findElement(BaseFlutterTest.LOGIN_BUTTON);
74+
loginButton.click();
75+
openScreen("Nested Scroll");
76+
77+
AppiumBy ancestorBy = AppiumBy.flutterAncestor(
78+
AppiumBy.flutterText("Child 2"),
79+
AppiumBy.flutterKey("parent_card_1")
80+
);
81+
WebElement parentElement = driver.findElement(ancestorBy);
82+
assertTrue(parentElement.isDisplayed());
83+
}
5484
}

src/main/java/io/appium/java_client/AppiumBy.java

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package io.appium.java_client;
1818

1919
import com.google.common.base.Preconditions;
20+
import com.google.gson.Gson;
2021
import lombok.EqualsAndHashCode;
2122
import lombok.Getter;
2223
import org.openqa.selenium.By;
@@ -25,7 +26,9 @@
2526
import org.openqa.selenium.WebElement;
2627

2728
import java.io.Serializable;
29+
import java.util.HashMap;
2830
import java.util.List;
31+
import java.util.Map;
2932

3033
import static com.google.common.base.Strings.isNullOrEmpty;
3134

@@ -169,9 +172,9 @@ public static By custom(final String selector) {
169172
* as for OpenCV library.
170173
* @return an instance of {@link ByImage}
171174
* @see <a href="https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/image-comparison.md">
172-
* The documentation on Image Comparison Features</a>
175+
* The documentation on Image Comparison Features</a>
173176
* @see <a href="https://github.com/appium/appium-base-driver/blob/master/lib/basedriver/device-settings.js">
174-
* The settings available for lookup fine-tuning</a>
177+
* The settings available for lookup fine-tuning</a>
175178
* @since Appium 1.8.2
176179
*/
177180
public static By image(final String b64Template) {
@@ -250,6 +253,53 @@ public static FlutterBy flutterSemanticsLabel(final String semanticsLabel) {
250253
return new ByFlutterSemanticsLabel(semanticsLabel);
251254
}
252255

256+
/**
257+
* This locator strategy is available in FlutterIntegration Driver mode.
258+
*
259+
* @param of represents the parent widget locator
260+
* @param matching represents the descendant widget locator to match
261+
* @param matchRoot determines whether to include the root widget in the search
262+
* @param skipOffstage determines whether to skip offstage widgets
263+
* @return an instance of {@link AppiumBy.ByFlutterDescendant}
264+
*/
265+
public static FlutterBy flutterDescendant(final FlutterBy of, final FlutterBy matching, boolean matchRoot, boolean skipOffstage) {
266+
return new ByFlutterDescendant(of, matching, matchRoot, skipOffstage);
267+
}
268+
269+
/**
270+
* This locator strategy is available in FlutterIntegration Driver mode.
271+
*
272+
* @param of represents the parent widget locator
273+
* @param matching represents the descendant widget locator to match
274+
* @return an instance of {@link AppiumBy.ByFlutterDescendant}
275+
*/
276+
public static FlutterBy flutterDescendant(final FlutterBy of, final FlutterBy matching) {
277+
return flutterDescendant(of, matching, false, true);
278+
}
279+
280+
/**
281+
* This locator strategy is available in FlutterIntegration Driver mode.
282+
*
283+
* @param of represents the child widget locator
284+
* @param matching represents the ancestor widget locator to match
285+
* @param matchRoot determines whether to include the root widget in the search
286+
* @return an instance of {@link AppiumBy.ByFlutterAncestor}
287+
*/
288+
public static FlutterBy flutterAncestor(final FlutterBy of, final FlutterBy matching, boolean matchRoot) {
289+
return new ByFlutterAncestor(of, matching, matchRoot);
290+
}
291+
292+
/**
293+
* This locator strategy is available in FlutterIntegration Driver mode.
294+
*
295+
* @param of represents the child widget locator
296+
* @param matching represents the ancestor widget locator to match
297+
* @return an instance of {@link AppiumBy.ByFlutterAncestor}
298+
*/
299+
public static FlutterBy flutterAncestor(final FlutterBy of, final FlutterBy matching) {
300+
return flutterAncestor(of, matching, false);
301+
}
302+
253303
public static class ByAccessibilityId extends AppiumBy implements Serializable {
254304
public ByAccessibilityId(String accessibilityId) {
255305
super("accessibility id", accessibilityId, "accessibilityId");
@@ -328,6 +378,27 @@ protected FlutterBy(String selector, String locatorString, String locatorName) {
328378
}
329379
}
330380

381+
public abstract static class FlutterByHierarchy extends FlutterBy {
382+
private static final Gson GSON = new Gson();
383+
384+
protected FlutterByHierarchy(String selector, FlutterBy of, FlutterBy matching, Map<String, Object> properties, String locatorName) {
385+
super(selector, formatLocator(of, matching, properties), locatorName);
386+
}
387+
388+
static Map<String, Object> parseFlutterLocator(FlutterBy by) {
389+
Parameters params = by.getRemoteParameters();
390+
return Map.of("using", params.using(), "value", params.value());
391+
}
392+
393+
static String formatLocator(FlutterBy of, FlutterBy matching, Map<String, Object> properties) {
394+
Map<String, Object> locator = new HashMap<>();
395+
locator.put("of", parseFlutterLocator(of));
396+
locator.put("matching", parseFlutterLocator(matching));
397+
locator.put("parameters", properties);
398+
return GSON.toJson(locator);
399+
}
400+
}
401+
331402
public static class ByFlutterType extends FlutterBy implements Serializable {
332403
protected ByFlutterType(String locatorString) {
333404
super("-flutter type", locatorString, "flutterType");
@@ -358,4 +429,15 @@ protected ByFlutterTextContaining(String locatorString) {
358429
}
359430
}
360431

361-
}
432+
public static class ByFlutterDescendant extends FlutterByHierarchy implements Serializable {
433+
protected ByFlutterDescendant(FlutterBy of, FlutterBy matching, boolean matchRoot, boolean skipOffstage) {
434+
super("-flutter descendant", of, matching, Map.of("matchRoot", matchRoot, "skipOffstage", skipOffstage), "flutterDescendant");
435+
}
436+
}
437+
438+
public static class ByFlutterAncestor extends FlutterByHierarchy implements Serializable {
439+
protected ByFlutterAncestor(FlutterBy of, FlutterBy matching, boolean matchRoot) {
440+
super("-flutter ancestor", of, matching, Map.of("matchRoot", matchRoot), "flutterAncestor");
441+
}
442+
}
443+
}

0 commit comments

Comments
 (0)