Skip to content

Commit

Permalink
Implement ShadowDOM APIs in the Java bindings
Browse files Browse the repository at this point in the history
  • Loading branch information
shs96c committed Jun 22, 2021
1 parent 548f4b8 commit e1d15c4
Show file tree
Hide file tree
Showing 14 changed files with 676 additions and 190 deletions.
33 changes: 33 additions & 0 deletions java/client/src/org/openqa/selenium/NoSuchShadowRootException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

package org.openqa.selenium;

public class NoSuchShadowRootException extends NotFoundException {

public NoSuchShadowRootException(String message) {
super(message);
}

public NoSuchShadowRootException(Throwable cause) {
super(cause);
}

public NoSuchShadowRootException(String message, Throwable cause) {
super(message, cause);
}
}
16 changes: 12 additions & 4 deletions java/client/src/org/openqa/selenium/WebElement.java
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ public interface WebElement extends SearchContext, TakesScreenshot {
* @return The property's current value or null if the value is not set.
*/
default String getDomProperty(String name) {
throw new UnsupportedOperationException();
throw new UnsupportedOperationException("getDomProperty");
}

/**
Expand All @@ -128,7 +128,7 @@ default String getDomProperty(String name) {
* @return The attribute's value or null if the value is not set.
*/
default String getDomAttribute(String name) {
throw new UnsupportedOperationException();
throw new UnsupportedOperationException("getDomAttribute");
}

/**
Expand Down Expand Up @@ -177,7 +177,7 @@ default String getDomAttribute(String name) {
* @return the WAI-ARIA role of the element.
*/
default String getAriaRole() {
throw new UnsupportedOperationException();
throw new UnsupportedOperationException("getAriaRole");
}

/**
Expand All @@ -189,7 +189,7 @@ default String getAriaRole() {
* @return the accessible name of the element.
*/
default String getAccessibleName() {
throw new UnsupportedOperationException();
throw new UnsupportedOperationException("getAccessibleName");
}

/**
Expand Down Expand Up @@ -268,6 +268,14 @@ default String getAccessibleName() {
@Override
WebElement findElement(By by);

/**
* @return A representation of an element's shadow root for accessing the shadow DOM of a web component.
* @throws NoSuchShadowRootException If no shadow root is found
*/
default SearchContext getShadowRoot() {
throw new UnsupportedOperationException("getShadowRoot");
}

/**
* Is this element displayed or not? This method avoids the problem of having to parse an
* element's "style" attribute.
Expand Down
11 changes: 11 additions & 0 deletions java/client/src/org/openqa/selenium/remote/Dialect.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ public ResponseCodec<HttpResponse> getResponseCodec() {
public String getEncodedElementKey() {
return "ELEMENT";
}

@Override
public String getShadowRootElementKey() {
return "shadow-6066-11e4-a52e-4f735466cecf";
}
},
W3C {
@Override
Expand All @@ -56,9 +61,15 @@ public ResponseCodec<HttpResponse> getResponseCodec() {
public String getEncodedElementKey() {
return "element-6066-11e4-a52e-4f735466cecf";
}

@Override
public String getShadowRootElementKey() {
return "shadow-6066-11e4-a52e-4f735466cecf";
}
};

public abstract CommandCodec<HttpRequest> getCommandCodec();
public abstract ResponseCodec<HttpResponse> getResponseCodec();
public abstract String getEncodedElementKey();
public abstract String getShadowRootElementKey();
}
30 changes: 28 additions & 2 deletions java/client/src/org/openqa/selenium/remote/DriverCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@

import java.time.Duration;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import static java.util.Collections.singletonMap;

/**
* An empty interface defining constants for the standard commands defined in the WebDriver JSON
* wire protocol.
Expand Down Expand Up @@ -108,6 +109,31 @@ static CommandPayload FIND_CHILD_ELEMENTS(String id, String strategy, String val
return new CommandPayload(FIND_CHILD_ELEMENTS,
ImmutableMap.of("id", id, "using", strategy, "value", value));
}
String GET_ELEMENT_SHADOW_ROOT = "getElementShadowRoot";
static CommandPayload GET_ELEMENT_SHADOW_ROOT(String id) {
Require.nonNull("Element ID", id);
return new CommandPayload(GET_ELEMENT_SHADOW_ROOT, singletonMap("id", id));
}

String FIND_ELEMENT_FROM_SHADOW_ROOT = "findElementFromShadowRoot";
static CommandPayload FIND_ELEMENT_FROM_SHADOW_ROOT(String shadowId, String strategy, String value) {
Require.nonNull("Shadow root ID", shadowId);
Require.nonNull("Element finding strategy", strategy);
Require.nonNull("Value for finding strategy", value);
return new CommandPayload(
FIND_ELEMENT_FROM_SHADOW_ROOT,
ImmutableMap.of("shadowId", shadowId, "using", strategy, "value", value));
}

String FIND_ELEMENTS_FROM_SHADOW_ROOT = "findElementsFromShadowRoot";
static CommandPayload FIND_ELEMENTS_FROM_SHADOW_ROOT(String shadowId, String strategy, String value) {
Require.nonNull("Shadow root ID", shadowId);
Require.nonNull("Element finding strategy", strategy);
Require.nonNull("Value for finding strategy", value);
return new CommandPayload(
FIND_ELEMENTS_FROM_SHADOW_ROOT,
ImmutableMap.of("shadowId", shadowId, "using", strategy, "value", value));
}

String CLEAR_ELEMENT = "clearElement";
static CommandPayload CLEAR_ELEMENT(String id) {
Expand Down Expand Up @@ -149,7 +175,7 @@ static CommandPayload SWITCH_TO_NEW_WINDOW(WindowType typeHint) {
String SWITCH_TO_CONTEXT = "switchToContext";
String SWITCH_TO_FRAME = "switchToFrame";
static CommandPayload SWITCH_TO_FRAME(Object frame) {
return new CommandPayload(SWITCH_TO_FRAME, Collections.singletonMap("id", frame));
return new CommandPayload(SWITCH_TO_FRAME, singletonMap("id", frame));
}
String SWITCH_TO_PARENT_FRAME = "switchToParentFrame";
String GET_ACTIVE_ELEMENT = "getActiveElement";
Expand Down
221 changes: 221 additions & 0 deletions java/client/src/org/openqa/selenium/remote/ElementLocation.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

package org.openqa.selenium.remote;

import org.openqa.selenium.By;
import org.openqa.selenium.InvalidArgumentException;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.SearchContext;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.internal.Require;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.stream.Collectors;

// Very deliberately kept package private.
class ElementLocation {

private final Map<Class<? extends By>, ElementFinder> finders = new HashMap<>();

public ElementLocation() {
finders.put(By.cssSelector("a").getClass(), ElementFinder.REMOTE);
finders.put(By.linkText("a").getClass(), ElementFinder.REMOTE);
finders.put(By.partialLinkText("a").getClass(), ElementFinder.REMOTE);
finders.put(By.tagName("a").getClass(), ElementFinder.REMOTE);
finders.put(By.xpath("//a").getClass(), ElementFinder.REMOTE);
}

public WebElement findElement(
RemoteWebDriver driver,
SearchContext context,
BiFunction<String, Object, CommandPayload> createPayload,
By locator) {

Require.nonNull("WebDriver", driver);
Require.nonNull("Context for finding elements", context);
Require.nonNull("Method for creating remote requests", createPayload);
Require.nonNull("Locator", locator);

ElementFinder mechanism = finders.get(locator.getClass());
if (mechanism != null) {
return mechanism.findElement(driver, context, createPayload, locator);
}

// We prefer to use the remote version if possible
if (locator instanceof By.Remotable) {
try {
WebElement element = ElementFinder.REMOTE.findElement(driver, context, createPayload, locator);
finders.put(locator.getClass(), ElementFinder.REMOTE);
return element;
} catch (NoSuchElementException e) {
finders.put(locator.getClass(), ElementFinder.REMOTE);
throw e;
} catch (InvalidArgumentException e) {
// Fall through
}
}

// But if that's not an option, then default to using the locator
// itself for finding things.
try {
WebElement element = ElementFinder.CONTEXT.findElement(driver, context, createPayload, locator);
finders.put(locator.getClass(), ElementFinder.CONTEXT);
return element;
} catch (NoSuchElementException e) {
finders.put(locator.getClass(), ElementFinder.CONTEXT);
throw e;
}
}

public List<WebElement> findElements(
RemoteWebDriver driver,
SearchContext context,
BiFunction<String, Object, CommandPayload> createPayload,
By locator) {

Require.nonNull("WebDriver", driver);
Require.nonNull("Context for finding elements", context);
Require.nonNull("Method for creating remote requests", createPayload);
Require.nonNull("Locator", locator);

ElementFinder finder = finders.get(locator.getClass());
if (finder != null) {
return finder.findElements(driver, context, createPayload, locator);
}

// We prefer to use the remote version if possible
if (locator instanceof By.Remotable) {
try {
List<WebElement> element = ElementFinder.REMOTE.findElements(driver, context, createPayload, locator);
finders.put(locator.getClass(), ElementFinder.REMOTE);
return element;
} catch (NoSuchElementException e) {
finders.put(locator.getClass(), ElementFinder.REMOTE);
throw e;
} catch (InvalidArgumentException e) {
// Fall through
}
}

// But if that's not an option, then default to using the locator
// itself for finding things.
List<WebElement> elements = ElementFinder.CONTEXT.findElements(driver, context, createPayload, locator);

// Only store the finder if we actually completed successfully.
finders.put(locator.getClass(), ElementFinder.CONTEXT);
return elements;
}

private enum ElementFinder {
CONTEXT {
@Override
WebElement findElement(
RemoteWebDriver driver,
SearchContext context,
BiFunction<String, Object, CommandPayload> createPayload,
By locator) {
WebElement element = locator.findElement(context);
return massage(driver, context, element, locator);
}

@Override
List<WebElement> findElements(
RemoteWebDriver driver,
SearchContext context,
BiFunction<String, Object, CommandPayload> createPayload,
By locator) {
List<WebElement> elements = locator.findElements(context);
return elements.stream()
.map(e -> massage(driver, context, e, locator))
.collect(Collectors.toList());
}
},
REMOTE {
@Override
WebElement findElement(
RemoteWebDriver driver,
SearchContext context,
BiFunction<String, Object, CommandPayload> createPayload,
By locator) {
By.Remotable.Parameters params = ((By.Remotable) locator).getRemoteParameters();
CommandPayload commandPayload = createPayload.apply(params.using(), params.value());

Response response = driver.execute(commandPayload);
WebElement element = (WebElement) response.getValue();
if (element == null) {
throw new NoSuchElementException("Unable to find element with locator " + locator);
}
return massage(driver, context, element, locator);
}

@Override
List<WebElement> findElements(
RemoteWebDriver driver,
SearchContext context,
BiFunction<String, Object, CommandPayload> createPayload,
By locator) {
By.Remotable.Parameters params = ((By.Remotable) locator).getRemoteParameters();
CommandPayload commandPayload = createPayload.apply(params.using(), params.value());

Response response = driver.execute(commandPayload);
@SuppressWarnings("unchecked") List<WebElement> elements = (List<WebElement>) response.getValue();

if (elements == null) { // see https://github.com/SeleniumHQ/selenium/issues/4555
return Collections.emptyList();
}

return elements.stream()
.map(e -> massage(driver, context, e, locator))
.collect(Collectors.toList());
}
}
;

abstract WebElement findElement(
RemoteWebDriver driver,
SearchContext context,
BiFunction<String, Object, CommandPayload> createPayload,
By locator);

abstract List<WebElement> findElements(
RemoteWebDriver driver,
SearchContext context,
BiFunction<String, Object, CommandPayload> createPayload,
By locator);

protected WebElement massage(RemoteWebDriver driver, 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(driver.getFileDetector());
remoteElement.setParent(driver);

return remoteElement;
}
}
}
Loading

2 comments on commit e1d15c4

@StanislavKharchenko
Copy link

Choose a reason for hiding this comment

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

Do you have plans to implement the similar API to work with Shadow DOM for JS binding?

@AutomatedTester
Copy link
Member

Choose a reason for hiding this comment

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

@StanislavKharchenko yes! We need browser vendors to help implement the work within the browser first

Please # to comment.