Skip to content

Commit

Permalink
Rework how By works
Browse files Browse the repository at this point in the history
With this change, each WebDriver implementation needs to take
responsibility for handling locators. In the case of the classes that
extend `RemoteWebDriver` (which is most of them), we first check to
see if a locator directly supports remote location. If it does, we try
to find the element that way first. If that fails in a way that
indicates that the remote end didn't understand the search mechanism,
we fall back to passing the search context to the locator.

In order to be as efficient as possible, drivers store a map of which
search strategy should work.
  • Loading branch information
shs96c committed Feb 1, 2021
1 parent e43cce3 commit 0aaa401
Show file tree
Hide file tree
Showing 9 changed files with 590 additions and 362 deletions.
352 changes: 139 additions & 213 deletions java/client/src/org/openqa/selenium/By.java

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions java/client/src/org/openqa/selenium/remote/DriverCommand.java
Expand Up @@ -73,11 +73,11 @@ static CommandPayload DELETE_COOKIE(String name) {
String DELETE_ALL_COOKIES = "deleteAllCookies";

String FIND_ELEMENT = "findElement";
static CommandPayload FIND_ELEMENT(String strategy, String value) {
static CommandPayload FIND_ELEMENT(String strategy, Object value) {
return new CommandPayload(FIND_ELEMENT, ImmutableMap.of("using", strategy, "value", value));
}
String FIND_ELEMENTS = "findElements";
static CommandPayload FIND_ELEMENTS(String strategy, String value) {
static CommandPayload FIND_ELEMENTS(String strategy, Object value) {
return new CommandPayload(FIND_ELEMENTS, ImmutableMap.of("using", strategy, "value", value));
}
String FIND_CHILD_ELEMENT = "findChildElement";
Expand Down
242 changes: 193 additions & 49 deletions java/client/src/org/openqa/selenium/remote/RemoteWebDriver.java
Expand Up @@ -27,14 +27,17 @@
import org.openqa.selenium.Dimension;
import org.openqa.selenium.HasCapabilities;
import org.openqa.selenium.ImmutableCapabilities;
import org.openqa.selenium.InvalidArgumentException;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.MutableCapabilities;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.NoSuchFrameException;
import org.openqa.selenium.NoSuchWindowException;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.Pdf;
import org.openqa.selenium.Platform;
import org.openqa.selenium.Point;
import org.openqa.selenium.PrintsPage;
import org.openqa.selenium.SearchContext;
import org.openqa.selenium.SessionNotCreatedException;
import org.openqa.selenium.TakesScreenshot;
Expand All @@ -55,8 +58,6 @@
import org.openqa.selenium.logging.Logs;
import org.openqa.selenium.logging.NeedsLocalLogs;
import org.openqa.selenium.print.PrintOptions;
import org.openqa.selenium.Pdf;
import org.openqa.selenium.PrintsPage;
import org.openqa.selenium.remote.internal.WebElementToJsonConverter;
import org.openqa.selenium.virtualauthenticator.Credential;
import org.openqa.selenium.virtualauthenticator.HasVirtualAuthenticator;
Expand All @@ -69,6 +70,7 @@
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
Expand All @@ -92,10 +94,22 @@ public class RemoteWebDriver implements WebDriver, JavascriptExecutor, HasInputD
HasCapabilities, Interactive, TakesScreenshot,
HasVirtualAuthenticator, PrintsPage {

// TODO(dawagner): This static logger should be unified with the per-instance localLogs
// TODO: This static logger should be unified with the per-instance localLogs
private static final Logger logger = Logger.getLogger(RemoteWebDriver.class.getName());
private Level level = Level.FINE;

private Map<Class<? extends By>, Mechanism> remotableBys = new HashMap<>();
// We would do this in either `init` or a constructor, but there are
// multiple constructors and we occasionally add a new one, and `init`
// may be overridden by the user
{
remotableBys.put(By.cssSelector("").getClass(), Mechanism.REMOTE);
remotableBys.put(By.linkText("").getClass(), Mechanism.REMOTE);
remotableBys.put(By.partialLinkText("").getClass(), Mechanism.REMOTE);
remotableBys.put(By.tagName("").getClass(), Mechanism.REMOTE);
remotableBys.put(By.xpath("").getClass(), Mechanism.REMOTE);
}

private ErrorHandler errorHandler = new ErrorHandler();
private CommandExecutor executor;
private Capabilities capabilities;
Expand All @@ -112,7 +126,7 @@ public class RemoteWebDriver implements WebDriver, JavascriptExecutor, HasInputD

// For cglib
protected RemoteWebDriver() {
this.capabilities=init(new ImmutableCapabilities());
this.capabilities = init(new ImmutableCapabilities());
}

public RemoteWebDriver(Capabilities capabilities) {
Expand All @@ -129,7 +143,7 @@ public RemoteWebDriver(CommandExecutor executor, Capabilities capabilities) {
}
this.executor = executor;

capabilities=init(capabilities);
capabilities = init(capabilities);

if (executor instanceof NeedsLocalLogs) {
((NeedsLocalLogs) executor).setLocalLogs(localLogs);
Expand Down Expand Up @@ -342,69 +356,121 @@ public Pdf print(PrintOptions printOptions) throws WebDriverException {

@Override
public WebElement findElement(By locator) {
if (locator instanceof By.StandardLocator) {
return ((By.StandardLocator) locator).findElement(this, this::findElement);
} else {
return locator.findElement(this);
Require.nonNull("Locator", locator);

return findElement(this, this, locator);
}

WebElement findElement(RemoteWebDriver parent, SearchContext context, By locator) {
Mechanism mechanism = remotableBys.get(locator.getClass());
if (mechanism != null) {
WebElement element = mechanism.findElement(parent, context, locator);
return massage(parent, context, element, locator);
}

// Attempt to find the element remotely
if (locator instanceof By.Remotable) {
try {
WebElement element = Mechanism.REMOTE.findElement(parent, context, locator);
remotableBys.put(locator.getClass(), Mechanism.REMOTE);
return massage(parent, context, element, locator);
} catch (NoSuchElementException e) {
remotableBys.put(locator.getClass(), Mechanism.REMOTE);
throw e;
} catch (InvalidArgumentException e) {
// Fall through
}
}

try {
WebElement element = Mechanism.CONTEXT.findElement(parent, context, locator);
remotableBys.put(locator.getClass(), Mechanism.CONTEXT);
return massage(parent, context, element, locator);
} catch (NoSuchElementException e) {
remotableBys.put(locator.getClass(), Mechanism.CONTEXT);
throw e;
}
}

@Override
public List<WebElement> findElements(By locator) {
if (locator instanceof By.StandardLocator) {
return ((By.StandardLocator) locator).findElements(this, this::findElements);
} else {
return locator.findElements(this);
}
Require.nonNull("Locator", locator);

return findElements(this, this, locator);
}

protected WebElement findElement(String by, String using) {
if (using == null) {
throw new IllegalArgumentException("Cannot find elements when the selector is null.");
public List<WebElement> findElements(RemoteWebDriver parent, SearchContext context, By locator) {
Mechanism mechanism = remotableBys.get(locator.getClass());
if (mechanism != null) {
List<WebElement> elements = mechanism.findElements(parent, context, locator);
elements.forEach(e -> massage(parent, context, e, locator));
return elements;
}

Response response = execute(DriverCommand.FIND_ELEMENT(by, using));
Object value = response.getValue();
if (value == null) { // see https://github.com/SeleniumHQ/selenium/issues/5809
throw new NoSuchElementException(String.format("Cannot locate an element using %s=%s", by, using));
// Attempt to find the element remotely
if (locator instanceof By.Remotable) {
try {
List<WebElement> elements = Mechanism.REMOTE.findElements(parent, context, locator);
remotableBys.put(locator.getClass(), Mechanism.REMOTE);
elements.forEach(e -> massage(parent, context, e, locator));
return elements;
} catch (NoSuchElementException e) {
remotableBys.put(locator.getClass(), Mechanism.REMOTE);
throw e;
} catch (InvalidArgumentException e) {
// Fall through
}
}
if (!(value instanceof WebElement)) {
throw new WebDriverException("Returned value cannot be converted to WebElement: " + value);

try {
List<WebElement> elements = Mechanism.CONTEXT.findElements(parent, context, locator);
remotableBys.put(locator.getClass(), Mechanism.CONTEXT);
elements.forEach(e -> massage(parent, context, e, locator));
return elements;
} catch (NoSuchElementException e) {
remotableBys.put(locator.getClass(), Mechanism.CONTEXT);
throw e;
}
WebElement element = (WebElement) value;
setFoundBy(this, element, by, using);
return element;
}

protected void setFoundBy(SearchContext context, WebElement element, String by, String using) {
if (element instanceof RemoteWebElement) {
RemoteWebElement remoteElement = (RemoteWebElement) element;
remoteElement.setFoundBy(context, by, using);
remoteElement.setFileDetector(getFileDetector());
private WebElement massage(RemoteWebDriver parent, SearchContext context, WebElement element, By locator) {
if (!(element instanceof RemoteWebElement)) {
return element;
}

RemoteWebElement remoteElement = (RemoteWebElement) element;
if (locator instanceof By.Remotable) {
By.Remotable.Parameters params = ((By.Remotable) locator).getRemoteParameters();
remoteElement.setFoundBy(context, params.using(), String.valueOf(params.value()));
}
remoteElement.setFileDetector(parent.getFileDetector());
remoteElement.setParent(parent);

return remoteElement;
}

@SuppressWarnings("unchecked")
/**
* @deprecated Rely on using {@link By.Remotable} instead
*/
@Deprecated
protected WebElement findElement(String by, String using) {
throw new UnsupportedOperationException("`findElement` has been replaced by usages of " + By.Remotable.class);
}

/**
* @deprecated Rely on using {@link By.Remotable} instead
*/
@Deprecated
protected List<WebElement> findElements(String by, String using) {
if (using == null) {
throw new IllegalArgumentException("Cannot find elements when the selector is null.");
}
throw new UnsupportedOperationException("`findElement` has been replaced by usages of " + By.Remotable.class);
}

Response response = execute(DriverCommand.FIND_ELEMENTS(by, using));
Object value = response.getValue();
if (value == null) { // see https://github.com/SeleniumHQ/selenium/issues/4555
return Collections.emptyList();
}
List<WebElement> allElements;
try {
allElements = (List<WebElement>) value;
} catch (ClassCastException ex) {
throw new WebDriverException("Returned value cannot be converted to List<WebElement>: " + value, ex);
}
for (WebElement element : allElements) {
setFoundBy(this, element, by, using);
protected void setFoundBy(SearchContext context, WebElement element, String by, String using) {
if (element instanceof RemoteWebElement) {
RemoteWebElement remoteElement = (RemoteWebElement) element;
remoteElement.setFoundBy(context, by, using);
remoteElement.setFileDetector(getFileDetector());
}
return allElements;
}

// Misc
Expand Down Expand Up @@ -1111,4 +1177,82 @@ public String toString() {
platform,
getSessionId());
}

private enum Mechanism {
CONTEXT {
@Override
WebElement findElement(RemoteWebDriver parent, SearchContext context, By locator) {
return locator.findElement(context);
}

@Override
List<WebElement> findElements(RemoteWebDriver parent, SearchContext context, By locator) {
return locator.findElements(context);
}
},
REMOTE {
@Override
WebElement findElement(RemoteWebDriver parent, SearchContext context, By locator) {
String commandName;
Map<String, Object> params = new HashMap<>();

By.Remotable.Parameters fromLocator = ((By.Remotable) locator).getRemoteParameters();
params.put("using", fromLocator.using());
params.put("value", fromLocator.value());

if (context instanceof RemoteWebElement) {
commandName = DriverCommand.FIND_CHILD_ELEMENT;
params.put("id", ((RemoteWebElement) context).getId());
} else {
commandName = DriverCommand.FIND_ELEMENT;
}

Response response = parent.execute(new CommandPayload(commandName, params));
Object value = response.getValue();
if (value == null) {
throw new NoSuchElementException("Unable to find element with locator " + locator);
}
try {
return (WebElement) value;
} catch (ClassCastException ex) {
throw new WebDriverException(
"Returned value cannot be converted to WebElement: " + value, ex);
}
}

@Override
List<WebElement> findElements(RemoteWebDriver parent, SearchContext context, By locator) {
String commandName;
Map<String, Object> params = new HashMap<>();

By.Remotable.Parameters fromLocator = ((By.Remotable) locator).getRemoteParameters();
params.put("using", fromLocator.using());
params.put("value", fromLocator.value());

if (context instanceof RemoteWebElement) {
commandName = DriverCommand.FIND_CHILD_ELEMENTS;
params.put("id", ((RemoteWebElement) context).getId());
} else {
commandName = DriverCommand.FIND_ELEMENTS;
}

Response response = parent.execute(new CommandPayload(commandName, params));
Object value = response.getValue();
if (value == null) { // see https://github.com/SeleniumHQ/selenium/issues/4555
return Collections.emptyList();
}
try {
@SuppressWarnings("unchecked") List<WebElement> toReturn = (List<WebElement>) value;
return toReturn;
} catch (ClassCastException ex) {
throw new WebDriverException(
"Returned value cannot be converted to WebElement: " + value, ex);
}
}
}
;

abstract WebElement findElement(RemoteWebDriver parent, SearchContext context, By locator);
abstract List<WebElement> findElements(RemoteWebDriver parent, SearchContext context, By locator);
}
}

0 comments on commit 0aaa401

Please sign in to comment.