Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new locator type for Shadow Root elements #7905

Closed
wants to merge 7 commits into from
Closed
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
23 changes: 23 additions & 0 deletions common/src/web/shadowElements.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<html>
<body>
<div id="outside_shadow">
<p id="inner_shadow_text">outside shadow</p>
</div>
<div id="no_shadow">
<p id="inner_no_shadow">no shadow</p>
</div>
<script>
var nestedElement = document.createElement('nested-element');
nestedElement.setAttribute('nestedAttribute', 'nestedAttributeValue');

var nestedElementLevel2 = document.createElement('nested-element-level-2');
nestedElementLevel2.setAttribute('nestedAttribute2', 'nestedAttributeValue2');

nestedElement.attachShadow({mode: 'open'});
nestedElement.shadowRoot.appendChild(nestedElementLevel2);

document.getElementById("outside_shadow").attachShadow({mode: 'open'});
document.getElementById("outside_shadow").shadowRoot.appendChild(nestedElement);
</script>
</body>
</html>
6 changes: 5 additions & 1 deletion java/client/src/org/openqa/selenium/By.java
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,7 @@ public ByCssSelector(String cssSelector) {
if (cssSelector == null) {
throw new IllegalArgumentException("Cannot find elements when the selector is null");
}

this.cssSelector = cssSelector;
}

Expand Down Expand Up @@ -513,6 +513,10 @@ public String toString() {
return "By.cssSelector: " + cssSelector;
}

public String getCssSelector() {
return cssSelector;
}

private Map<String, Object> toJson() {
Map<String, Object> asJson = new HashMap<>();
asJson.put("using", "css selector");
Expand Down
110 changes: 110 additions & 0 deletions java/client/src/org/openqa/selenium/ShadowElementFinder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package org.openqa.selenium;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

/**
* Helper to to verify if some element has a Shadow Root attached to it and allow further querying to locate nested elements attached to it.
*/
public class ShadowElementFinder {

private JavascriptExecutor jsExecutor;

public ShadowElementFinder(SearchContext context) {
jsExecutor = (JavascriptExecutor) context;
}

/**
* Verifies if the given element has a Shadow Root attached to it.
*
* @param element The element to be checked for Shadow Root
* @return boolean True if it has a Shadow Root, false otherwise
*/
public boolean hasShadowElement(WebElement element) {
try {
final String SHADOW_ROOT_SCRIPT = "return arguments[0].shadowRoot.nodeName";
Object result = jsExecutor.executeScript(SHADOW_ROOT_SCRIPT, element);
return result != null;
} catch (Exception ignored) {
return false;
}
}

/**
* Runs the script to extract the shadow root of an element using the given {@link By}
*
* @param element The element verify and extract the Shadow Root from
* @param by A {@link org.openqa.selenium.By.ByCssSelector} to run on the element
* @return A WebElement if there is a Shadow Root attached to the element, null otherwise
*/
@SuppressWarnings("unchecked")
public Optional<List<WebElement>> extractShadowElementsOf(WebElement element, By by) {
try {
String cssSelector = getCssSelectorOfBy(by);
final String SHADOW_ROOT_SCRIPT = String.format("return arguments[0].shadowRoot.querySelectorAll('%s')", cssSelector);
List<WebElement> webElements = (List<WebElement>) jsExecutor.executeScript(SHADOW_ROOT_SCRIPT, element);
return Optional.ofNullable(webElements);
} catch (Exception e) {
throw new NoSuchElementException("It was not possible to locate the elements inside the Shadow Root. Locator " + by.toString());
}
}

/**
* Safely locates elements from the element using the {@link By}
*
* @param element An element with a shadow root
* @param by A {@link org.openqa.selenium.By.ByCssSelector}
* @return A list of the found elements, or an empty list if there is an error
*/
public List<WebElement> safeLocateElementsFromShadow(WebElement element, By by) {
try {
Optional<List<WebElement>> optional = extractShadowElementsOf(element, by);
return optional.orElseGet(ArrayList::new);
} catch (Exception e) {
return new ArrayList<>();
}
}

/**
* Safely locates elements from the element using the {@link By}, and returns the first found element
*
* @param element An element with a shadow root
* @param by A {@link org.openqa.selenium.By.ByCssSelector}
* @return The first element of the list, or the element provided if nothing is found
*/
public Optional<WebElement> safeLocateElementFromShadow(WebElement element, By by) {
Optional<List<WebElement>> optional = extractShadowElementsOf(element, by);
return optional.map(webElements -> webElements.get(0));
}

/**
* Gets the CssSelector of the given {@link By} if it's a {@link org.openqa.selenium.By.ByCssSelector},
* throws an exception otherwise.
*
* @param by A {@link org.openqa.selenium.By.ByCssSelector}
* @return The css
*/
protected String getCssSelectorOfBy(By by) {
if (by instanceof By.ByCssSelector) {
By.ByCssSelector byCssSelector = (By.ByCssSelector) by;
return byCssSelector.getCssSelector();
} else {
throw new InvalidSelectorException("Only css selectors are allowing for elements with shadow root");
}
}

/**
* Extracts the Shadow Root of a list of elements.
*
* @param elements A list of {@link WebElement}s
* @return The Shadow Root of the element, or the same element if it doesn't have a Shadow Root.
*/
public List<WebElement> extractShadowElementsWithBy(List<WebElement> elements, By by) {
List<WebElement> extractedElements = new ArrayList<>();
for (WebElement element : elements) {
extractedElements.addAll(safeLocateElementsFromShadow(element, by));
}
return extractedElements;
}
}
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 @@ -42,6 +42,7 @@ java_library(
"FindAll.java",
"FindBy.java",
"FindBys.java",
"FindShadowBy.java",
"How.java",
"PageFactory.java",
"PageFactoryFinder.java",
Expand Down
36 changes: 36 additions & 0 deletions java/client/src/org/openqa/selenium/support/FindShadowBy.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package org.openqa.selenium.support;

import org.openqa.selenium.By;
import org.openqa.selenium.support.pagefactory.ByChainedShadow;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE})
@PageFactoryFinder(FindShadowBy.FindShadowByBuilder.class)
public @interface FindShadowBy {

FindBy[] value();

public static class FindShadowByBuilder extends AbstractFindByBuilder {
@Override
public By buildIt(Object annotation, Field field) {
FindShadowBy findBys = (FindShadowBy) annotation;
for (FindBy findBy : findBys.value()) {
assertValidFindBy(findBy);
}

FindBy[] findByArray = findBys.value();
By[] byArray = new By[findByArray.length];
for (int i = 0; i < findByArray.length; i++) {
byArray[i] = buildByFromFindBy(findByArray[i]);
}

return new ByChainedShadow(byArray);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package org.openqa.selenium.support.pagefactory;

import org.openqa.selenium.By;
import org.openqa.selenium.SearchContext;
import org.openqa.selenium.ShadowElementFinder;
import org.openqa.selenium.WebElement;

import java.util.ArrayList;
import java.util.List;

/**
* Similar to {@link ByChained}, but for each {@link By} provided it will verify if it has a Shadow Root
* attached to it, and return it.
* <br> if you need to find an element that is inside a Shadow Root, use:
* <pre>
* driver.findElements(new ByChainedShadow(shadowRootBy, targetElementBy))
* </pre>
*
* This will locate the shadow element first using <var>shadowRootBy</var>, and from there use
* <var>targetElementBy</var> to locate the element inside the shadow element.
* <br>
* <br>Note that using {@link org.openqa.selenium.By.ByXPath} will throw an exception if used inside
* any Shadow Root. Using {@link org.openqa.selenium.By.ByXPath} on <var>shadowRootBy</var> it's ok
* (as it is the first find), using it on <var>targetElementBy</var> won't work and will throw an error.
*/
public class ByChainedShadow extends ByChained {

private By[] bys;

public ByChainedShadow(By... bys) {
super(bys);
this.bys = bys;
}

@Override
public List<WebElement> findElements(SearchContext context) {
if (bys.length == 0) {
return new ArrayList<>();
}

List<WebElement> elements = null;
ShadowElementFinder shadowElementFinder = new ShadowElementFinder(context);
for (By by : bys) {
List<WebElement> newElements = new ArrayList<>();
if (elements == null) {
newElements = by.findElements(context);
} else {
List<WebElement> webElements = shadowElementFinder.extractShadowElementsWithBy(elements, by);
newElements.addAll(webElements);
}
elements = newElements;
}

return elements;
}
}
Loading