Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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());
}
}
99 changes: 99 additions & 0 deletions src/main/java/io/appium/java_client/AppiumBy.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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<String, Object> properties,
String locatorName) {
super(selector, formatLocator(of, matching, properties), locatorName);
}

static Map<String, Object> parseFlutterLocator(FlutterBy by) {
Parameters params = by.getRemoteParameters();
return Map.of("using", params.using(), "value", params.value());
}

static String formatLocator(FlutterBy of, FlutterBy matching, Map<String, Object> properties) {
Map<String, Object> locator = new HashMap<>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we also use Map.of like above?

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");
Expand Down Expand Up @@ -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");
}
}
}