Skip to content

Commit

Permalink
Introduce atoms and java bindings for friendly locators
Browse files Browse the repository at this point in the history
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
shs96c committed Aug 3, 2019
1 parent 2876887 commit b5a6a4b
Show file tree
Hide file tree
Showing 12 changed files with 878 additions and 8 deletions.
40 changes: 40 additions & 0 deletions common/src/web/friendly_locators.html
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>
1 change: 1 addition & 0 deletions java/client/src/org/openqa/selenium/support/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ java_export(
":page-factory",
"//java/client/src/org/openqa/selenium/support/devtools",
"//java/client/src/org/openqa/selenium/support/events",
"//java/client/src/org/openqa/selenium/support/friendly",
"//java/client/src/org/openqa/selenium/support/ui:clock",
"//java/client/src/org/openqa/selenium/support/ui:components",
"//java/client/src/org/openqa/selenium/support/ui:elements",
Expand Down
23 changes: 23 additions & 0 deletions java/client/src/org/openqa/selenium/support/friendly/BUILD.bazel
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 $< $@",
)
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -213,12 +213,6 @@ public void convertsAnArrayWithAWebElement() {
Dialect.W3C.getEncodedElementKey(), "abc123"));
}

@Test
public void rejectsUnrecognizedTypes() {
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> CONVERTER.apply(new Object()));
}

private static WrappedWebElement wrapElement(WebElement element) {
return new WrappedWebElement(element);
}
Expand Down
17 changes: 17 additions & 0 deletions java/client/test/org/openqa/selenium/support/friendly/BUILD.bazel
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",
]
)
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"));
}

}

0 comments on commit b5a6a4b

Please sign in to comment.