-
-
Notifications
You must be signed in to change notification settings - Fork 8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introduce atoms and java bindings for friendly locators
This is not an exhaustive set, but enough to give idea of what needs to be done. Provided here are new locators (called "friendly", but we can change that) that hook into the normal `bot.locators` mechanism. The locators are: * "above" * "below" * "left" * "right" * "near" All of these are based on location in the DOM. That is "above" means that an element matches the locator iff its bottom edge is above the top edge of an element we're comparing against. "Near" is currently set to mean "within 50 pixels of any edge of an element". This may not be the best measure.
- Loading branch information
Showing
12 changed files
with
878 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
<html> | ||
<head> | ||
<title>Friendly Locators</title> | ||
<style> | ||
table { | ||
text-align: center; | ||
border: solid; | ||
} | ||
td { | ||
border: solid; | ||
} | ||
</style> | ||
</head> | ||
<body> | ||
<h1>Friendly Locator Tests</h1> | ||
<p id="above">This text is above. | ||
<p id="mid">This is a paragraph of text in the middle. | ||
<p id="below">This text is below. | ||
|
||
|
||
<table> | ||
<tr> | ||
<td id="first">1</td> | ||
<td id="second" style="width: 100px">2</td> | ||
<td id="third">3</td> | ||
</tr> | ||
<tr> | ||
<td id="fourth">4</td> | ||
<td id="center">5</td> | ||
<td id="sixth">6</td> | ||
</tr> | ||
<tr> | ||
<td id="seventh">7</td> | ||
<td id="eighth">8</td> | ||
<td id="ninth">9</td> | ||
</tr> | ||
</table> | ||
|
||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
23 changes: 23 additions & 0 deletions
23
java/client/src/org/openqa/selenium/support/friendly/BUILD.bazel
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
java_library( | ||
name = "friendly", | ||
srcs = glob(["*.java"]), | ||
resources = [ | ||
":find-elements", | ||
], | ||
deps = [ | ||
"//java/client/src/org/openqa/selenium:core", | ||
"//java/client/src/org/openqa/selenium/json", | ||
"//third_party/java/guava", | ||
], | ||
visibility = [ | ||
"//java/client/src/org/openqa/selenium/support:__pkg__", | ||
"//java/client/test/org/openqa/selenium/support/friendly:__pkg__", | ||
] | ||
) | ||
|
||
genrule( | ||
name = "find-elements", | ||
srcs = ["//javascript/atoms/fragments:find-elements.js"], | ||
outs = ["findElements.js"], | ||
cmd = "cp $< $@", | ||
) |
250 changes: 250 additions & 0 deletions
250
java/client/src/org/openqa/selenium/support/friendly/FriendlyLocators.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,250 @@ | ||
package org.openqa.selenium.support.friendly; | ||
|
||
import com.google.common.collect.ImmutableList; | ||
import com.google.common.collect.ImmutableMap; | ||
import com.google.common.io.Resources; | ||
import org.openqa.selenium.By; | ||
import org.openqa.selenium.JavascriptExecutor; | ||
import org.openqa.selenium.SearchContext; | ||
import org.openqa.selenium.WebDriver; | ||
import org.openqa.selenium.WebElement; | ||
import org.openqa.selenium.WrapsDriver; | ||
import org.openqa.selenium.json.Json; | ||
import org.openqa.selenium.json.JsonException; | ||
|
||
import java.io.IOException; | ||
import java.io.UncheckedIOException; | ||
import java.net.URL; | ||
import java.nio.charset.StandardCharsets; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Objects; | ||
|
||
import static com.google.common.base.Preconditions.checkArgument; | ||
import static org.openqa.selenium.json.Json.MAP_TYPE; | ||
|
||
public class FriendlyLocators { | ||
|
||
private static final Json JSON = new Json(); | ||
private static final String FIND_ELEMENTS; | ||
static { | ||
try { | ||
String location = String.format( | ||
"/%s/%s", | ||
FriendlyLocators.class.getPackage().getName().replace(".", "/"), | ||
"findElements.js"); | ||
|
||
URL url = FriendlyLocators.class.getResource(location); | ||
|
||
String rawFunction = Resources.toString(url, StandardCharsets.UTF_8); | ||
FIND_ELEMENTS = String.format("return (%s).apply(null, arguments);", rawFunction); | ||
} catch (IOException e) { | ||
throw new UncheckedIOException(e); | ||
} | ||
} | ||
public static FriendlyBy withTagName(String tagName) { | ||
Objects.requireNonNull(tagName, "Tag name to look for must be set"); | ||
|
||
return new FriendlyBy(By.tagName(tagName)); | ||
} | ||
|
||
public static class FriendlyBy extends By { | ||
private final Object root; | ||
private final List<Map<String, Object>> filters; | ||
|
||
private FriendlyBy(Object rootLocator) { | ||
this(rootLocator, ImmutableList.of()); | ||
} | ||
|
||
private FriendlyBy(Object rootLocator, List<Map<String, Object>> filters) { | ||
if (rootLocator instanceof By) { | ||
assertLocatorCanBeSerialized(rootLocator); | ||
rootLocator = asAtomLocatorParameter((By) rootLocator); | ||
} else if (rootLocator instanceof Map) { | ||
if (((Map<?, ?>) rootLocator).keySet().size() != 1) { | ||
throw new IllegalArgumentException( | ||
"Root locators as find element payload must only have a single key: " + rootLocator); | ||
} | ||
} else if (!(rootLocator instanceof WebElement)) { | ||
throw new IllegalArgumentException("Root locator must be an element or a locator: " + rootLocator); | ||
} | ||
|
||
this.root = Objects.requireNonNull(rootLocator); | ||
this.filters = ImmutableList.copyOf(Objects.requireNonNull(filters)); | ||
} | ||
|
||
public FriendlyBy above(WebElement element) { | ||
Objects.requireNonNull(element, "Element to search for must be set."); | ||
return simpleDirection("above", element); | ||
} | ||
|
||
public FriendlyBy above(By locator) { | ||
Objects.requireNonNull(locator, "Locator to use must be set."); | ||
assertLocatorCanBeSerialized(locator); | ||
return simpleDirection("above", locator); | ||
} | ||
|
||
public FriendlyBy below(WebElement element) { | ||
Objects.requireNonNull(element, "Element to search for must be set."); | ||
return simpleDirection("below", element); | ||
} | ||
|
||
public FriendlyBy below(By locator) { | ||
Objects.requireNonNull(locator, "Locator to use must be set."); | ||
assertLocatorCanBeSerialized(locator); | ||
return simpleDirection("below", locator); | ||
} | ||
|
||
public FriendlyBy toLeftOf(WebElement element) { | ||
Objects.requireNonNull(element, "Element to search for must be set."); | ||
return simpleDirection("left", element); | ||
} | ||
|
||
public FriendlyBy toLeftOf(By locator) { | ||
Objects.requireNonNull(locator, "Locator to use must be set."); | ||
assertLocatorCanBeSerialized(locator); | ||
return simpleDirection("left", locator); | ||
} | ||
|
||
public FriendlyBy toRightOf(WebElement element) { | ||
Objects.requireNonNull(element, "Element to search for must be set."); | ||
return simpleDirection("right", element); | ||
} | ||
|
||
public FriendlyBy toRightOf(By locator) { | ||
Objects.requireNonNull(locator, "Locator to use must be set."); | ||
assertLocatorCanBeSerialized(locator); | ||
return simpleDirection("right", locator); | ||
} | ||
|
||
public FriendlyBy near(WebElement element) { | ||
Objects.requireNonNull(element, "Element to search for must be set."); | ||
return near(element, 50); | ||
} | ||
|
||
public FriendlyBy near(WebElement element, int atMostDistanceInPixels) { | ||
Objects.requireNonNull(element, "Element to search for must be set."); | ||
checkArgument(atMostDistanceInPixels > 0, "Distance must be greater than 0."); | ||
|
||
return near((Object) element, atMostDistanceInPixels); | ||
} | ||
|
||
public FriendlyBy near(By locator) { | ||
Objects.requireNonNull(locator, "Locator to use for must be set."); | ||
return near((Object) locator, 50); | ||
} | ||
|
||
public FriendlyBy near(By locator, int atMostDistanceInPixels) { | ||
Objects.requireNonNull(locator, "Locator to use for must be set."); | ||
checkArgument(atMostDistanceInPixels > 0, "Distance must be greater than 0."); | ||
|
||
return near((Object) locator, atMostDistanceInPixels); | ||
} | ||
|
||
private FriendlyBy near(Object locator, int atMostDistanceInPixels) { | ||
Objects.requireNonNull(locator, "Locator to use must be set."); | ||
checkArgument(atMostDistanceInPixels > 0, "Distance must be greater than 0."); | ||
|
||
return new FriendlyBy( | ||
root, | ||
amend(ImmutableMap.of( | ||
"kind", "near", | ||
"args", ImmutableList.of(asAtomLocatorParameter(locator), "distance", atMostDistanceInPixels)))); | ||
} | ||
|
||
@Override | ||
public List<WebElement> findElements(SearchContext context) { | ||
JavascriptExecutor js = extractJsExecutor(context); | ||
|
||
@SuppressWarnings("unchecked") | ||
List<WebElement> elements = (List<WebElement>) js.executeScript(FIND_ELEMENTS, this.toJson()); | ||
System.out.println(elements); | ||
return elements; | ||
} | ||
|
||
private FriendlyBy simpleDirection(String direction, Object locator) { | ||
Objects.requireNonNull(direction, "Direction to search in must be set."); | ||
Objects.requireNonNull(locator, "Locator to use must be set."); | ||
|
||
return new FriendlyBy( | ||
root, | ||
amend(ImmutableMap.of( | ||
"kind", direction, | ||
"args", ImmutableList.of(asAtomLocatorParameter(locator))))); | ||
|
||
} | ||
|
||
private List<Map<String, Object>> amend(Map<String, Object> toAdd) { | ||
return ImmutableList.<Map<String, Object>>builder() | ||
.addAll(filters) | ||
.add(toAdd) | ||
.build(); | ||
} | ||
|
||
private JavascriptExecutor extractJsExecutor(SearchContext context) { | ||
if (context instanceof JavascriptExecutor) { | ||
return (JavascriptExecutor) context; | ||
} | ||
|
||
Object current = context; | ||
while (current instanceof WrapsDriver) { | ||
WebDriver driver = ((WrapsDriver) context).getWrappedDriver(); | ||
if (driver instanceof JavascriptExecutor) { | ||
return (JavascriptExecutor) driver; | ||
} | ||
current = driver; | ||
} | ||
|
||
throw new IllegalArgumentException("Cannot find elements, since the context cannot execute JS: " + context); | ||
} | ||
|
||
private Map<String, Object> toJson() { | ||
return ImmutableMap.of( | ||
"friendly", ImmutableMap.of( | ||
"root", root, | ||
"filters", filters)); | ||
} | ||
} | ||
|
||
private static Object asAtomLocatorParameter(Object object) { | ||
if (object instanceof WebElement) { | ||
return object; | ||
} | ||
|
||
if (!(object instanceof By)) { | ||
throw new IllegalArgumentException("Expected locator to be either an element or a By: " + object); | ||
} | ||
|
||
assertLocatorCanBeSerialized((By) object); | ||
|
||
Map<String, Object> raw = JSON.toType(JSON.toJson(object), MAP_TYPE); | ||
|
||
if (!(raw.get("using") instanceof String)) { | ||
throw new JsonException("Expected JSON encoded form of locator to have a 'using' field. " + raw); | ||
} | ||
if (!raw.containsKey("value")) { | ||
throw new JsonException("Expected JSON encoded form of locator to have a 'value' field: " + raw); | ||
} | ||
|
||
return ImmutableMap.of((String) raw.get("using"), raw.get("value")); | ||
} | ||
|
||
private static void assertLocatorCanBeSerialized(Object locator) { | ||
Objects.requireNonNull(locator, "Locator must be set."); | ||
|
||
Class<?> clazz = locator.getClass(); | ||
|
||
while (!clazz.equals(Object.class)) { | ||
try { | ||
clazz.getDeclaredMethod("toJson"); | ||
return; | ||
} catch (NoSuchMethodException e) { | ||
// Do nothing. Continue with the loop | ||
} | ||
clazz = clazz.getSuperclass(); | ||
} | ||
|
||
throw new IllegalArgumentException( | ||
"Locator must be serializable to JSON using a `toJson` method. " + locator); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
17 changes: 17 additions & 0 deletions
17
java/client/test/org/openqa/selenium/support/friendly/BUILD.bazel
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
load("//java:test.bzl", "java_selenium_test_suite", "java_test_suite") | ||
|
||
java_selenium_test_suite( | ||
name = "MediumTests", | ||
size = "medium", | ||
browsers = ["chrome"], | ||
srcs = glob(["*Test.java"]), | ||
deps = [ | ||
"//java/client/src/org/openqa/selenium:core", | ||
"//java/client/src/org/openqa/selenium/remote", | ||
"//java/client/src/org/openqa/selenium/support/friendly", | ||
"//java/client/test/org/openqa/selenium/testing:test-base", | ||
"//third_party/java/assertj", | ||
"//third_party/java/guava", | ||
"//third_party/java/junit", | ||
] | ||
) |
39 changes: 39 additions & 0 deletions
39
java/client/test/org/openqa/selenium/support/friendly/FriendlyLocatorsTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
package org.openqa.selenium.support.friendly; | ||
|
||
import com.google.common.collect.ImmutableList; | ||
import org.junit.Test; | ||
import org.openqa.selenium.By; | ||
import org.openqa.selenium.WebElement; | ||
import org.openqa.selenium.testing.JUnit4TestBase; | ||
|
||
import java.util.List; | ||
import java.util.stream.Collectors; | ||
|
||
import static org.assertj.core.api.Assertions.assertThat; | ||
import static org.openqa.selenium.support.friendly.FriendlyLocators.withTagName; | ||
|
||
public class FriendlyLocatorsTest extends JUnit4TestBase { | ||
|
||
@Test | ||
public void shouldBeAbleToFindElementsAboveAnother() { | ||
driver.get(appServer.whereIs("friendly_locators.html")); | ||
|
||
WebElement lowest = driver.findElement(By.id("below")); | ||
|
||
List<WebElement> elements = driver.findElements(withTagName("p").above(lowest)); | ||
List<String> ids = elements.stream().map(e -> e.getAttribute("id")).collect(Collectors.toList()); | ||
|
||
assertThat(ids).isEqualTo(ImmutableList.of("above", "mid")); | ||
} | ||
|
||
@Test | ||
public void shouldBeAbleToCombineFilters() { | ||
driver.get(appServer.whereIs("friendly_locators.html")); | ||
|
||
List<WebElement> seen = driver.findElements(withTagName("td").above(By.id("center")).toRightOf(By.id("second"))); | ||
|
||
List<String> ids = seen.stream().map(e -> e.getAttribute("id")).collect(Collectors.toList()); | ||
assertThat(ids).isEqualTo(ImmutableList.of("third")); | ||
} | ||
|
||
} |
Oops, something went wrong.