Skip to content

Commit 407247b

Browse files
feat: add support for descendant and ancestor flutter locators (#2357)
1 parent 958f4b1 commit 407247b

File tree

3 files changed

+162
-0
lines changed

3 files changed

+162
-0
lines changed

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,4 +160,37 @@ void testCameraMocking() throws IOException {
160160
driver.findElement(AppiumBy.flutterText("PICK")).click();
161161
assertTrue(driver.findElement(AppiumBy.flutterText("Success!")).isDisplayed());
162162
}
163+
164+
@Test
165+
void testScrollTillVisibleForAncestor() {
166+
WebElement loginButton = driver.findElement(BaseFlutterTest.LOGIN_BUTTON);
167+
loginButton.click();
168+
openScreen("Nested Scroll");
169+
170+
AppiumBy.FlutterBy ancestorBy = AppiumBy.flutterAncestor(
171+
AppiumBy.flutterText("Child 2"),
172+
AppiumBy.flutterKey("parent_card_4")
173+
);
174+
175+
assertEquals(0, driver.findElements(ancestorBy).size());
176+
driver.scrollTillVisible(new ScrollParameter(ancestorBy));
177+
assertEquals(1, driver.findElements(ancestorBy).size());
178+
}
179+
180+
@Test
181+
void testScrollTillVisibleForDescendant() {
182+
WebElement loginButton = driver.findElement(BaseFlutterTest.LOGIN_BUTTON);
183+
loginButton.click();
184+
openScreen("Nested Scroll");
185+
186+
AppiumBy.FlutterBy descendantBy = AppiumBy.flutterDescendant(
187+
AppiumBy.flutterKey("parent_card_4"),
188+
AppiumBy.flutterText("Child 2")
189+
);
190+
191+
assertEquals(0, driver.findElements(descendantBy).size());
192+
driver.scrollTillVisible(new ScrollParameter(descendantBy));
193+
// Make sure the card is visible after scrolling
194+
assertEquals(1, driver.findElements(descendantBy).size());
195+
}
163196
}

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: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,12 @@
2323
import org.openqa.selenium.By.Remotable;
2424
import org.openqa.selenium.SearchContext;
2525
import org.openqa.selenium.WebElement;
26+
import org.openqa.selenium.json.Json;
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

@@ -250,6 +253,57 @@ 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 since version 1.4.0.
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(
266+
final FlutterBy of,
267+
final FlutterBy matching,
268+
boolean matchRoot,
269+
boolean skipOffstage) {
270+
return new ByFlutterDescendant(of, matching, matchRoot, skipOffstage);
271+
}
272+
273+
/**
274+
* This locator strategy is available in FlutterIntegration Driver mode since version 1.4.0.
275+
*
276+
* @param of represents the parent widget locator
277+
* @param matching represents the descendant widget locator to match
278+
* @return an instance of {@link AppiumBy.ByFlutterDescendant}
279+
*/
280+
public static FlutterBy flutterDescendant(final FlutterBy of, final FlutterBy matching) {
281+
return flutterDescendant(of, matching, false, true);
282+
}
283+
284+
/**
285+
* This locator strategy is available in FlutterIntegration Driver mode since version 1.4.0.
286+
*
287+
* @param of represents the child widget locator
288+
* @param matching represents the ancestor widget locator to match
289+
* @param matchRoot determines whether to include the root widget in the search
290+
* @return an instance of {@link AppiumBy.ByFlutterAncestor}
291+
*/
292+
public static FlutterBy flutterAncestor(final FlutterBy of, final FlutterBy matching, boolean matchRoot) {
293+
return new ByFlutterAncestor(of, matching, matchRoot);
294+
}
295+
296+
/**
297+
* This locator strategy is available in FlutterIntegration Driver mode since version 1.4.0.
298+
*
299+
* @param of represents the child widget locator
300+
* @param matching represents the ancestor widget locator to match
301+
* @return an instance of {@link AppiumBy.ByFlutterAncestor}
302+
*/
303+
public static FlutterBy flutterAncestor(final FlutterBy of, final FlutterBy matching) {
304+
return flutterAncestor(of, matching, false);
305+
}
306+
253307
public static class ByAccessibilityId extends AppiumBy implements Serializable {
254308
public ByAccessibilityId(String accessibilityId) {
255309
super("accessibility id", accessibilityId, "accessibilityId");
@@ -328,6 +382,32 @@ protected FlutterBy(String selector, String locatorString, String locatorName) {
328382
}
329383
}
330384

385+
public abstract static class FlutterByHierarchy extends FlutterBy {
386+
private static final Json JSON = new Json();
387+
388+
protected FlutterByHierarchy(
389+
String selector,
390+
FlutterBy of,
391+
FlutterBy matching,
392+
Map<String, Object> properties,
393+
String locatorName) {
394+
super(selector, formatLocator(of, matching, properties), locatorName);
395+
}
396+
397+
static Map<String, Object> parseFlutterLocator(FlutterBy by) {
398+
Parameters params = by.getRemoteParameters();
399+
return Map.of("using", params.using(), "value", params.value());
400+
}
401+
402+
static String formatLocator(FlutterBy of, FlutterBy matching, Map<String, Object> properties) {
403+
Map<String, Object> locator = new HashMap<>();
404+
locator.put("of", parseFlutterLocator(of));
405+
locator.put("matching", parseFlutterLocator(matching));
406+
locator.put("parameters", properties);
407+
return JSON.toJson(locator);
408+
}
409+
}
410+
331411
public static class ByFlutterType extends FlutterBy implements Serializable {
332412
protected ByFlutterType(String locatorString) {
333413
super("-flutter type", locatorString, "flutterType");
@@ -358,4 +438,23 @@ protected ByFlutterTextContaining(String locatorString) {
358438
}
359439
}
360440

441+
public static class ByFlutterDescendant extends FlutterByHierarchy implements Serializable {
442+
protected ByFlutterDescendant(FlutterBy of, FlutterBy matching, boolean matchRoot, boolean skipOffstage) {
443+
super(
444+
"-flutter descendant",
445+
of,
446+
matching,
447+
Map.of("matchRoot", matchRoot, "skipOffstage", skipOffstage), "flutterDescendant");
448+
}
449+
}
450+
451+
public static class ByFlutterAncestor extends FlutterByHierarchy implements Serializable {
452+
protected ByFlutterAncestor(FlutterBy of, FlutterBy matching, boolean matchRoot) {
453+
super(
454+
"-flutter ancestor",
455+
of,
456+
matching,
457+
Map.of("matchRoot", matchRoot), "flutterAncestor");
458+
}
459+
}
361460
}

0 commit comments

Comments
 (0)