Skip to content

Commit b2cf065

Browse files
committed
[dotnet] Add support for finding elements in shadow DOM
This commit adds support for the WebDriver protocol commands for finding elements located in a Shadow DOM. Please note that until a driver implementation (chromedriver, geckodriver, etc.) supports the end points documented in the specification, none of these methods will function at all.
1 parent 7a932e7 commit b2cf065

File tree

9 files changed

+249
-1
lines changed

9 files changed

+249
-1
lines changed

dotnet/src/webdriver/DriverCommand.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,16 @@ public static class DriverCommand
123123
/// </summary>
124124
public static readonly string FindChildElements = "findChildElements";
125125

126+
/// <summary>
127+
/// Represents FindShadowChildElement command
128+
/// </summary>
129+
public static readonly string FindShadowChildElement = "findShadowChildElement";
130+
131+
/// <summary>
132+
/// Represents FindShadosChildElements command
133+
/// </summary>
134+
public static readonly string FindShadowChildElements = "findShadowChildElements";
135+
126136
/// <summary>
127137
/// Describes an element
128138
/// </summary>
@@ -283,6 +293,11 @@ public static class DriverCommand
283293
/// </summary>
284294
public static readonly string GetComputedAccessibleRole = "getComputedAccessibleRole";
285295

296+
/// <summary>
297+
/// Represents the GetElementShadowRoot command.
298+
/// </summary>
299+
public static readonly string GetElementShadowRoot = "getElementShadowRoot";
300+
286301
/// <summary>
287302
/// Represents ElementEquals command
288303
/// </summary>

dotnet/src/webdriver/IWebElement.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,5 +219,12 @@ public interface IWebElement : ISearchContext
219219
/// return "#008000" for its value.</remarks>
220220
/// <exception cref="StaleElementReferenceException">Thrown when the target element is no longer valid in the document DOM.</exception>
221221
string GetCssValue(string propertyName);
222+
223+
/// <summary>
224+
/// Gets the representation of an element's shadow root for accessing the shadow DOM of a web component.
225+
/// </summary>
226+
/// <exception cref="NoSuchShadowRootException">Thrown when this element does not have a shadow root.</exception>
227+
/// <returns>A shadow root representation.</returns>
228+
ISearchContext GetShadowRoot();
222229
}
223230
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// <copyright file="NoSuchShadowRootException.cs" company="WebDriver Committers">
2+
// Licensed to the Software Freedom Conservancy (SFC) under one
3+
// or more contributor license agreements. See the NOTICE file
4+
// distributed with this work for additional information
5+
// regarding copyright ownership. The SFC licenses this file
6+
// to you under the Apache License, Version 2.0 (the "License");
7+
// you may not use this file except in compliance with the License.
8+
// You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
// </copyright>
18+
19+
using System;
20+
using System.Runtime.Serialization;
21+
22+
namespace OpenQA.Selenium
23+
{
24+
/// <summary>
25+
/// The exception that is thrown when a frame is not found.
26+
/// </summary>
27+
[Serializable]
28+
public class NoSuchShadowRootException : NotFoundException
29+
{
30+
/// <summary>
31+
/// Initializes a new instance of the <see cref="NoSuchShadowRootException"/> class.
32+
/// </summary>
33+
public NoSuchShadowRootException()
34+
: base()
35+
{
36+
}
37+
38+
/// <summary>
39+
/// Initializes a new instance of the <see cref="NoSuchShadowRootException"/> class with
40+
/// a specified error message.
41+
/// </summary>
42+
/// <param name="message">The message that describes the error.</param>
43+
public NoSuchShadowRootException(string message)
44+
: base(message)
45+
{
46+
}
47+
48+
/// <summary>
49+
/// Initializes a new instance of the <see cref="NoSuchShadowRootException"/> class with
50+
/// a specified error message and a reference to the inner exception that is the
51+
/// cause of this exception.
52+
/// </summary>
53+
/// <param name="message">The error message that explains the reason for the exception.</param>
54+
/// <param name="innerException">The exception that is the cause of the current exception,
55+
/// or <see langword="null"/> if no inner exception is specified.</param>
56+
public NoSuchShadowRootException(string message, Exception innerException)
57+
: base(message, innerException)
58+
{
59+
}
60+
61+
/// <summary>
62+
/// Initializes a new instance of the <see cref="NoSuchShadowRootException"/> class with serialized data.
63+
/// </summary>
64+
/// <param name="info">The <see cref="SerializationInfo"/> that holds the serialized
65+
/// object data about the exception being thrown.</param>
66+
/// <param name="context">The <see cref="StreamingContext"/> that contains contextual
67+
/// information about the source or destination.</param>
68+
protected NoSuchShadowRootException(SerializationInfo info, StreamingContext context)
69+
: base(info, context)
70+
{
71+
}
72+
}
73+
}

dotnet/src/webdriver/Remote/W3CWireProtocolCommandInfoRepository.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,13 @@ protected override void InitializeCommandDictionary()
8383
this.TryAddCommand(DriverCommand.MinimizeWindow, new HttpCommandInfo(HttpCommandInfo.PostCommand, "/session/{sessionId}/window/minimize"));
8484
this.TryAddCommand(DriverCommand.FullScreenWindow, new HttpCommandInfo(HttpCommandInfo.PostCommand, "/session/{sessionId}/window/fullscreen"));
8585
this.TryAddCommand(DriverCommand.GetActiveElement, new HttpCommandInfo(HttpCommandInfo.GetCommand, "/session/{sessionId}/element/active"));
86+
this.TryAddCommand(DriverCommand.GetElementShadowRoot, new HttpCommandInfo(HttpCommandInfo.GetCommand, "/session/{sessionId}/element/{id}/shadow"));
8687
this.TryAddCommand(DriverCommand.FindElement, new HttpCommandInfo(HttpCommandInfo.PostCommand, "/session/{sessionId}/element"));
8788
this.TryAddCommand(DriverCommand.FindElements, new HttpCommandInfo(HttpCommandInfo.PostCommand, "/session/{sessionId}/elements"));
8889
this.TryAddCommand(DriverCommand.FindChildElement, new HttpCommandInfo(HttpCommandInfo.PostCommand, "/session/{sessionId}/element/{id}/element"));
8990
this.TryAddCommand(DriverCommand.FindChildElements, new HttpCommandInfo(HttpCommandInfo.PostCommand, "/session/{sessionId}/element/{id}/elements"));
91+
this.TryAddCommand(DriverCommand.FindShadowChildElement, new HttpCommandInfo(HttpCommandInfo.PostCommand, "/session/{sessionId}/shadow/{id}/element"));
92+
this.TryAddCommand(DriverCommand.FindShadowChildElements, new HttpCommandInfo(HttpCommandInfo.PostCommand, "/session/{sessionId}/shadow/{id}/elements"));
9093
this.TryAddCommand(DriverCommand.IsElementSelected, new HttpCommandInfo(HttpCommandInfo.GetCommand, "/session/{sessionId}/element/{id}/selected"));
9194
this.TryAddCommand(DriverCommand.ClickElement, new HttpCommandInfo(HttpCommandInfo.PostCommand, "/session/{sessionId}/element/{id}/click"));
9295
this.TryAddCommand(DriverCommand.ClearElement, new HttpCommandInfo(HttpCommandInfo.PostCommand, "/session/{sessionId}/element/{id}/clear"));

dotnet/src/webdriver/ShadowRoot.cs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// <copyright file="ShadowRoot.cs" company="WebDriver Committers">
2+
// Licensed to the Software Freedom Conservancy (SFC) under one
3+
// or more contributor license agreements. See the NOTICE file
4+
// distributed with this work for additional information
5+
// regarding copyright ownership. The SFC licenses this file
6+
// to you under the Apache License, Version 2.0 (the "License");
7+
// you may not use this file except in compliance with the License.
8+
// You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
// </copyright>
18+
19+
using System;
20+
using System.Collections.Generic;
21+
using System.Collections.ObjectModel;
22+
23+
namespace OpenQA.Selenium
24+
{
25+
/// <summary>
26+
/// Provides a representation of an element's shadow root.
27+
/// </summary>
28+
public class ShadowRoot : ISearchContext, IWrapsDriver
29+
{
30+
/// <summary>
31+
/// The property name that represents an element shadow root in the wire protocol.
32+
/// </summary>
33+
public const string ShadowRootReferencePropertyName = "shadow-6066-11e4-a52e-4f735466cecf";
34+
35+
private WebDriver driver;
36+
private string shadowRootId;
37+
38+
/// <summary>
39+
/// Initializes a new instance of the <see cref="ShadowRoot"/> class.
40+
/// </summary>
41+
/// <param name="parentDriver">The <see cref="WebDriver"/> instance that is driving this shadow root.</param>
42+
/// <param name="id">The ID value provided to identify the shadow root.</param>
43+
public ShadowRoot(WebDriver parentDriver, string id)
44+
{
45+
this.driver = parentDriver;
46+
this.shadowRootId = id;
47+
}
48+
49+
/// <summary>
50+
/// Gets the <see cref="IWebDriver"/> driving this shadow root.
51+
/// </summary>
52+
public IWebDriver WrappedDriver
53+
{
54+
get { return this.driver; }
55+
}
56+
57+
/// <summary>
58+
/// Finds the first <see cref="IWebElement"/> using the given method.
59+
/// </summary>
60+
/// <param name="by">The locating mechanism to use.</param>
61+
/// <returns>The first matching <see cref="IWebElement"/> on the current context.</returns>
62+
/// <exception cref="NoSuchElementException">If no element matches the criteria.</exception>
63+
public IWebElement FindElement(By by)
64+
{
65+
if (by == null)
66+
{
67+
throw new ArgumentNullException("by", "by cannot be null");
68+
}
69+
70+
Dictionary<string, object> parameters = new Dictionary<string, object>();
71+
parameters.Add("id", this.shadowRootId);
72+
parameters.Add("using", by.Mechanism);
73+
parameters.Add("value", by.Criteria);
74+
Response commandResponse = this.driver.InternalExecute(DriverCommand.FindShadowChildElement, parameters);
75+
return this.driver.GetElementFromResponse(commandResponse);
76+
}
77+
78+
/// <summary>
79+
/// Finds all <see cref="IWebElement">IWebElements</see> within the current context
80+
/// using the given mechanism.
81+
/// </summary>
82+
/// <param name="by">The locating mechanism to use.</param>
83+
/// <returns>A <see cref="ReadOnlyCollection{T}"/> of all <see cref="IWebElement">WebElements</see>
84+
/// matching the current criteria, or an empty list if nothing matches.</returns>
85+
public ReadOnlyCollection<IWebElement> FindElements(By by)
86+
{
87+
if (by == null)
88+
{
89+
throw new ArgumentNullException("by", "by cannot be null");
90+
}
91+
92+
Dictionary<string, object> parameters = new Dictionary<string, object>();
93+
parameters.Add("id", this.shadowRootId);
94+
parameters.Add("using", by.Mechanism);
95+
parameters.Add("value", by.Criteria);
96+
Response commandResponse = this.driver.InternalExecute(DriverCommand.FindShadowChildElements, parameters);
97+
return this.driver.GetElementsFromResponse(commandResponse);
98+
}
99+
}
100+
}

dotnet/src/webdriver/WebDriver.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -803,6 +803,9 @@ private static void UnpackAndThrowOnError(Response errorResponse)
803803
case WebDriverResult.MoveTargetOutOfBounds:
804804
throw new MoveTargetOutOfBoundsException(errorMessage);
805805

806+
case WebDriverResult.NoSuchShadowRoot:
807+
throw new NoSuchShadowRootException(errorMessage);
808+
806809
default:
807810
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "{0} ({1})", errorMessage, errorResponse.Status));
808811
}

dotnet/src/webdriver/WebDriverError.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,11 @@ internal static class WebDriverError
123123
/// </summary>
124124
public const string NoSuchWindow = "no such window";
125125

126+
/// <summary>
127+
/// Represents the no such shadow root error.
128+
/// </summary>
129+
public const string NoSuchShadowRoot = "no such shadow root";
130+
126131
/// <summary>
127132
/// Represents the script timeout error.
128133
/// </summary>
@@ -226,6 +231,7 @@ private static void InitializeResultMap()
226231
resultMap[NoSuchElement] = WebDriverResult.NoSuchElement;
227232
resultMap[NoSuchFrame] = WebDriverResult.NoSuchFrame;
228233
resultMap[NoSuchWindow] = WebDriverResult.NoSuchWindow;
234+
resultMap[NoSuchShadowRoot] = WebDriverResult.NoSuchShadowRoot;
229235
resultMap[ScriptTimeout] = WebDriverResult.AsyncScriptTimeout;
230236
resultMap[SessionNotCreated] = WebDriverResult.SessionNotCreated;
231237
resultMap[StaleElementReference] = WebDriverResult.ObsoleteElement;

dotnet/src/webdriver/WebDriverResult.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,11 @@ public enum WebDriverResult
221221
/// <summary>
222222
/// The click on the element was intercepted by a different element.
223223
/// </summary>
224-
ElementClickIntercepted = 64
224+
ElementClickIntercepted = 64,
225+
226+
/// <summary>
227+
/// The element does not have a shadow root.
228+
/// </summary>
229+
NoSuchShadowRoot = 65
225230
}
226231
}

dotnet/src/webdriver/WebElement.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,20 @@ public class WebElement : IWebElement, IFindsElement, IWrapsDriver, ILocatable,
3939
private WebDriver driver;
4040
private string elementId;
4141

42+
/// <summary>
43+
/// Initializes a new instance of the <see cref="WebElement"/> class.
44+
/// </summary>
45+
/// <param name="parentDriver">The <see cref="WebDriver"/> instance that is driving this element.</param>
46+
/// <param name="id">The ID value provided to identify the element.</param>
4247
public WebElement(WebDriver parentDriver, string id)
4348
{
4449
this.driver = parentDriver;
4550
this.elementId = id;
4651
}
52+
53+
/// <summary>
54+
/// Gets the <see cref="IWebDriver"/> driving this element.
55+
/// </summary>
4756
public IWebDriver WrappedDriver
4857
{
4958
get { return this.driver; }
@@ -499,6 +508,33 @@ public virtual string GetDomProperty(string propertyName)
499508
return propertyValue;
500509
}
501510

511+
/// <summary>
512+
/// Gets the representation of an element's shadow root for accessing the shadow DOM of a web component.
513+
/// </summary>
514+
/// <returns>A shadow root representation.</returns>
515+
/// <exception cref="StaleElementReferenceException">Thrown when the target element is no longer valid in the document DOM.</exception>
516+
/// <exception cref="NoSuchShadowRootException">Thrown when this element does not have a shadow root.</exception>
517+
public virtual ISearchContext GetShadowRoot()
518+
{
519+
Dictionary<string, object> parameters = new Dictionary<string, object>();
520+
parameters.Add("id", this.Id);
521+
522+
Response commandResponse = this.Execute(DriverCommand.GetElementShadowRoot, parameters);
523+
Dictionary<string, object> shadowRootDictionary = commandResponse.Value as Dictionary<string, object>;
524+
if (shadowRootDictionary == null)
525+
{
526+
throw new WebDriverException("Get shadow root command succeeded, but response value does not represent a shadow root.");
527+
}
528+
529+
if (!shadowRootDictionary.ContainsKey(ShadowRoot.ShadowRootReferencePropertyName))
530+
{
531+
throw new WebDriverException("Get shadow root command succeeded, but response value does not have a shadow root key value.");
532+
}
533+
534+
string shadowRootId = shadowRootDictionary[ShadowRoot.ShadowRootReferencePropertyName].ToString();
535+
return new ShadowRoot(this.driver, shadowRootId);
536+
}
537+
502538
/// <summary>
503539
/// Gets the value of a CSS property of this element.
504540
/// </summary>

0 commit comments

Comments
 (0)