Skip to content

Commit e1d15c4

Browse files
committed
Implement ShadowDOM APIs in the Java bindings
1 parent 548f4b8 commit e1d15c4

14 files changed

+676
-190
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Licensed to the Software Freedom Conservancy (SFC) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The SFC licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package org.openqa.selenium;
19+
20+
public class NoSuchShadowRootException extends NotFoundException {
21+
22+
public NoSuchShadowRootException(String message) {
23+
super(message);
24+
}
25+
26+
public NoSuchShadowRootException(Throwable cause) {
27+
super(cause);
28+
}
29+
30+
public NoSuchShadowRootException(String message, Throwable cause) {
31+
super(message, cause);
32+
}
33+
}

java/client/src/org/openqa/selenium/WebElement.java

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ public interface WebElement extends SearchContext, TakesScreenshot {
104104
* @return The property's current value or null if the value is not set.
105105
*/
106106
default String getDomProperty(String name) {
107-
throw new UnsupportedOperationException();
107+
throw new UnsupportedOperationException("getDomProperty");
108108
}
109109

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

134134
/**
@@ -177,7 +177,7 @@ default String getDomAttribute(String name) {
177177
* @return the WAI-ARIA role of the element.
178178
*/
179179
default String getAriaRole() {
180-
throw new UnsupportedOperationException();
180+
throw new UnsupportedOperationException("getAriaRole");
181181
}
182182

183183
/**
@@ -189,7 +189,7 @@ default String getAriaRole() {
189189
* @return the accessible name of the element.
190190
*/
191191
default String getAccessibleName() {
192-
throw new UnsupportedOperationException();
192+
throw new UnsupportedOperationException("getAccessibleName");
193193
}
194194

195195
/**
@@ -268,6 +268,14 @@ default String getAccessibleName() {
268268
@Override
269269
WebElement findElement(By by);
270270

271+
/**
272+
* @return A representation of an element's shadow root for accessing the shadow DOM of a web component.
273+
* @throws NoSuchShadowRootException If no shadow root is found
274+
*/
275+
default SearchContext getShadowRoot() {
276+
throw new UnsupportedOperationException("getShadowRoot");
277+
}
278+
271279
/**
272280
* Is this element displayed or not? This method avoids the problem of having to parse an
273281
* element's "style" attribute.

java/client/src/org/openqa/selenium/remote/Dialect.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ public ResponseCodec<HttpResponse> getResponseCodec() {
4040
public String getEncodedElementKey() {
4141
return "ELEMENT";
4242
}
43+
44+
@Override
45+
public String getShadowRootElementKey() {
46+
return "shadow-6066-11e4-a52e-4f735466cecf";
47+
}
4348
},
4449
W3C {
4550
@Override
@@ -56,9 +61,15 @@ public ResponseCodec<HttpResponse> getResponseCodec() {
5661
public String getEncodedElementKey() {
5762
return "element-6066-11e4-a52e-4f735466cecf";
5863
}
64+
65+
@Override
66+
public String getShadowRootElementKey() {
67+
return "shadow-6066-11e4-a52e-4f735466cecf";
68+
}
5969
};
6070

6171
public abstract CommandCodec<HttpRequest> getCommandCodec();
6272
public abstract ResponseCodec<HttpResponse> getResponseCodec();
6373
public abstract String getEncodedElementKey();
74+
public abstract String getShadowRootElementKey();
6475
}

java/client/src/org/openqa/selenium/remote/DriverCommand.java

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,12 @@
2929

3030
import java.time.Duration;
3131
import java.util.Collection;
32-
import java.util.Collections;
3332
import java.util.List;
3433
import java.util.concurrent.TimeUnit;
3534
import java.util.stream.Collectors;
3635

36+
import static java.util.Collections.singletonMap;
37+
3738
/**
3839
* An empty interface defining constants for the standard commands defined in the WebDriver JSON
3940
* wire protocol.
@@ -108,6 +109,31 @@ static CommandPayload FIND_CHILD_ELEMENTS(String id, String strategy, String val
108109
return new CommandPayload(FIND_CHILD_ELEMENTS,
109110
ImmutableMap.of("id", id, "using", strategy, "value", value));
110111
}
112+
String GET_ELEMENT_SHADOW_ROOT = "getElementShadowRoot";
113+
static CommandPayload GET_ELEMENT_SHADOW_ROOT(String id) {
114+
Require.nonNull("Element ID", id);
115+
return new CommandPayload(GET_ELEMENT_SHADOW_ROOT, singletonMap("id", id));
116+
}
117+
118+
String FIND_ELEMENT_FROM_SHADOW_ROOT = "findElementFromShadowRoot";
119+
static CommandPayload FIND_ELEMENT_FROM_SHADOW_ROOT(String shadowId, String strategy, String value) {
120+
Require.nonNull("Shadow root ID", shadowId);
121+
Require.nonNull("Element finding strategy", strategy);
122+
Require.nonNull("Value for finding strategy", value);
123+
return new CommandPayload(
124+
FIND_ELEMENT_FROM_SHADOW_ROOT,
125+
ImmutableMap.of("shadowId", shadowId, "using", strategy, "value", value));
126+
}
127+
128+
String FIND_ELEMENTS_FROM_SHADOW_ROOT = "findElementsFromShadowRoot";
129+
static CommandPayload FIND_ELEMENTS_FROM_SHADOW_ROOT(String shadowId, String strategy, String value) {
130+
Require.nonNull("Shadow root ID", shadowId);
131+
Require.nonNull("Element finding strategy", strategy);
132+
Require.nonNull("Value for finding strategy", value);
133+
return new CommandPayload(
134+
FIND_ELEMENTS_FROM_SHADOW_ROOT,
135+
ImmutableMap.of("shadowId", shadowId, "using", strategy, "value", value));
136+
}
111137

112138
String CLEAR_ELEMENT = "clearElement";
113139
static CommandPayload CLEAR_ELEMENT(String id) {
@@ -149,7 +175,7 @@ static CommandPayload SWITCH_TO_NEW_WINDOW(WindowType typeHint) {
149175
String SWITCH_TO_CONTEXT = "switchToContext";
150176
String SWITCH_TO_FRAME = "switchToFrame";
151177
static CommandPayload SWITCH_TO_FRAME(Object frame) {
152-
return new CommandPayload(SWITCH_TO_FRAME, Collections.singletonMap("id", frame));
178+
return new CommandPayload(SWITCH_TO_FRAME, singletonMap("id", frame));
153179
}
154180
String SWITCH_TO_PARENT_FRAME = "switchToParentFrame";
155181
String GET_ACTIVE_ELEMENT = "getActiveElement";
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
// Licensed to the Software Freedom Conservancy (SFC) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The SFC licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package org.openqa.selenium.remote;
19+
20+
import org.openqa.selenium.By;
21+
import org.openqa.selenium.InvalidArgumentException;
22+
import org.openqa.selenium.NoSuchElementException;
23+
import org.openqa.selenium.SearchContext;
24+
import org.openqa.selenium.WebElement;
25+
import org.openqa.selenium.internal.Require;
26+
27+
import java.util.Collections;
28+
import java.util.HashMap;
29+
import java.util.List;
30+
import java.util.Map;
31+
import java.util.function.BiFunction;
32+
import java.util.stream.Collectors;
33+
34+
// Very deliberately kept package private.
35+
class ElementLocation {
36+
37+
private final Map<Class<? extends By>, ElementFinder> finders = new HashMap<>();
38+
39+
public ElementLocation() {
40+
finders.put(By.cssSelector("a").getClass(), ElementFinder.REMOTE);
41+
finders.put(By.linkText("a").getClass(), ElementFinder.REMOTE);
42+
finders.put(By.partialLinkText("a").getClass(), ElementFinder.REMOTE);
43+
finders.put(By.tagName("a").getClass(), ElementFinder.REMOTE);
44+
finders.put(By.xpath("//a").getClass(), ElementFinder.REMOTE);
45+
}
46+
47+
public WebElement findElement(
48+
RemoteWebDriver driver,
49+
SearchContext context,
50+
BiFunction<String, Object, CommandPayload> createPayload,
51+
By locator) {
52+
53+
Require.nonNull("WebDriver", driver);
54+
Require.nonNull("Context for finding elements", context);
55+
Require.nonNull("Method for creating remote requests", createPayload);
56+
Require.nonNull("Locator", locator);
57+
58+
ElementFinder mechanism = finders.get(locator.getClass());
59+
if (mechanism != null) {
60+
return mechanism.findElement(driver, context, createPayload, locator);
61+
}
62+
63+
// We prefer to use the remote version if possible
64+
if (locator instanceof By.Remotable) {
65+
try {
66+
WebElement element = ElementFinder.REMOTE.findElement(driver, context, createPayload, locator);
67+
finders.put(locator.getClass(), ElementFinder.REMOTE);
68+
return element;
69+
} catch (NoSuchElementException e) {
70+
finders.put(locator.getClass(), ElementFinder.REMOTE);
71+
throw e;
72+
} catch (InvalidArgumentException e) {
73+
// Fall through
74+
}
75+
}
76+
77+
// But if that's not an option, then default to using the locator
78+
// itself for finding things.
79+
try {
80+
WebElement element = ElementFinder.CONTEXT.findElement(driver, context, createPayload, locator);
81+
finders.put(locator.getClass(), ElementFinder.CONTEXT);
82+
return element;
83+
} catch (NoSuchElementException e) {
84+
finders.put(locator.getClass(), ElementFinder.CONTEXT);
85+
throw e;
86+
}
87+
}
88+
89+
public List<WebElement> findElements(
90+
RemoteWebDriver driver,
91+
SearchContext context,
92+
BiFunction<String, Object, CommandPayload> createPayload,
93+
By locator) {
94+
95+
Require.nonNull("WebDriver", driver);
96+
Require.nonNull("Context for finding elements", context);
97+
Require.nonNull("Method for creating remote requests", createPayload);
98+
Require.nonNull("Locator", locator);
99+
100+
ElementFinder finder = finders.get(locator.getClass());
101+
if (finder != null) {
102+
return finder.findElements(driver, context, createPayload, locator);
103+
}
104+
105+
// We prefer to use the remote version if possible
106+
if (locator instanceof By.Remotable) {
107+
try {
108+
List<WebElement> element = ElementFinder.REMOTE.findElements(driver, context, createPayload, locator);
109+
finders.put(locator.getClass(), ElementFinder.REMOTE);
110+
return element;
111+
} catch (NoSuchElementException e) {
112+
finders.put(locator.getClass(), ElementFinder.REMOTE);
113+
throw e;
114+
} catch (InvalidArgumentException e) {
115+
// Fall through
116+
}
117+
}
118+
119+
// But if that's not an option, then default to using the locator
120+
// itself for finding things.
121+
List<WebElement> elements = ElementFinder.CONTEXT.findElements(driver, context, createPayload, locator);
122+
123+
// Only store the finder if we actually completed successfully.
124+
finders.put(locator.getClass(), ElementFinder.CONTEXT);
125+
return elements;
126+
}
127+
128+
private enum ElementFinder {
129+
CONTEXT {
130+
@Override
131+
WebElement findElement(
132+
RemoteWebDriver driver,
133+
SearchContext context,
134+
BiFunction<String, Object, CommandPayload> createPayload,
135+
By locator) {
136+
WebElement element = locator.findElement(context);
137+
return massage(driver, context, element, locator);
138+
}
139+
140+
@Override
141+
List<WebElement> findElements(
142+
RemoteWebDriver driver,
143+
SearchContext context,
144+
BiFunction<String, Object, CommandPayload> createPayload,
145+
By locator) {
146+
List<WebElement> elements = locator.findElements(context);
147+
return elements.stream()
148+
.map(e -> massage(driver, context, e, locator))
149+
.collect(Collectors.toList());
150+
}
151+
},
152+
REMOTE {
153+
@Override
154+
WebElement findElement(
155+
RemoteWebDriver driver,
156+
SearchContext context,
157+
BiFunction<String, Object, CommandPayload> createPayload,
158+
By locator) {
159+
By.Remotable.Parameters params = ((By.Remotable) locator).getRemoteParameters();
160+
CommandPayload commandPayload = createPayload.apply(params.using(), params.value());
161+
162+
Response response = driver.execute(commandPayload);
163+
WebElement element = (WebElement) response.getValue();
164+
if (element == null) {
165+
throw new NoSuchElementException("Unable to find element with locator " + locator);
166+
}
167+
return massage(driver, context, element, locator);
168+
}
169+
170+
@Override
171+
List<WebElement> findElements(
172+
RemoteWebDriver driver,
173+
SearchContext context,
174+
BiFunction<String, Object, CommandPayload> createPayload,
175+
By locator) {
176+
By.Remotable.Parameters params = ((By.Remotable) locator).getRemoteParameters();
177+
CommandPayload commandPayload = createPayload.apply(params.using(), params.value());
178+
179+
Response response = driver.execute(commandPayload);
180+
@SuppressWarnings("unchecked") List<WebElement> elements = (List<WebElement>) response.getValue();
181+
182+
if (elements == null) { // see https://github.com/SeleniumHQ/selenium/issues/4555
183+
return Collections.emptyList();
184+
}
185+
186+
return elements.stream()
187+
.map(e -> massage(driver, context, e, locator))
188+
.collect(Collectors.toList());
189+
}
190+
}
191+
;
192+
193+
abstract WebElement findElement(
194+
RemoteWebDriver driver,
195+
SearchContext context,
196+
BiFunction<String, Object, CommandPayload> createPayload,
197+
By locator);
198+
199+
abstract List<WebElement> findElements(
200+
RemoteWebDriver driver,
201+
SearchContext context,
202+
BiFunction<String, Object, CommandPayload> createPayload,
203+
By locator);
204+
205+
protected WebElement massage(RemoteWebDriver driver, SearchContext context, WebElement element, By locator) {
206+
if (!(element instanceof RemoteWebElement)) {
207+
return element;
208+
}
209+
210+
RemoteWebElement remoteElement = (RemoteWebElement) element;
211+
if (locator instanceof By.Remotable) {
212+
By.Remotable.Parameters params = ((By.Remotable) locator).getRemoteParameters();
213+
remoteElement.setFoundBy(context, params.using(), String.valueOf(params.value()));
214+
}
215+
remoteElement.setFileDetector(driver.getFileDetector());
216+
remoteElement.setParent(driver);
217+
218+
return remoteElement;
219+
}
220+
}
221+
}

0 commit comments

Comments
 (0)