Skip to content

Commit dd9130e

Browse files
authored
Merge pull request #324 from csf-dev/craigfowler/issue307
Resolve #307 - Add capability to get shadow root elements
2 parents 36579ca + 671474b commit dd9130e

16 files changed

Lines changed: 693 additions & 7 deletions

File tree

CSF.Screenplay.JsonToHtmlReport.Template/src/css/scenarioList.css

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,7 @@
122122
text-align: right;
123123
padding-right: 0.4em;
124124
}
125-
.reportableList .result,
126-
.reportableList .exception {
125+
.reportableList .result {
127126
margin-left: 5em;
128127
}
129128
.reportableList .type+.report {
@@ -143,14 +142,19 @@
143142
font-family: 'Lucida Console', 'Courier New', Courier, monospace;
144143
white-space: pre;
145144
display: block;
146-
text-indent: -3em;
147-
padding-left: 3em;
148-
background: #FF000022;
145+
background: #FF000011;
146+
padding: 0.25em;
147+
margin-left: 6.25em;
148+
font-size: 80%;
149+
line-height: 120%;
150+
overflow: auto;
151+
max-height: 15em;
149152
}
150153
.reportableList .exception::before {
151154
font-family: 'Trebuchet MS', 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', Arial, sans-serif;
152-
content: "Error: ";
155+
content: "Error";
153156
color: #888;
157+
display: block;
154158
}
155159
.reportableList aside {
156160
position: absolute;

CSF.Screenplay.Selenium/BrowserQuirks.cs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Collections.Generic;
22
using CSF.Extensions.WebDriver.Quirks;
3+
using CSF.Screenplay.Selenium.Questions;
34

45
namespace CSF.Screenplay.Selenium
56
{
@@ -67,6 +68,28 @@ public static class BrowserQuirks
6768
/// <seealso cref="Tasks.ClickAndWaitForDocumentReady"/>
6869
public static readonly string NeedsToWaitAfterPageLoad = "NeedsToWaitAfterPageLoad";
6970

71+
/// <summary>
72+
/// Gets the name of a browser quirk, for browser which cannot get a Shadow Root node using the native Selenium technique.
73+
/// </summary>
74+
/// <remarks>
75+
/// <para>
76+
/// Browsers with this quirk cannot use <see cref="GetShadowRootNatively"/> and must fall back to <see cref="GetShadowRootWithJavaScript"/>.
77+
/// This makes use of a JavaScript fallback to get the Shadow Root node from the Shadow Host.
78+
/// </para>
79+
/// </remarks>
80+
public static readonly string NeedsJavaScriptToGetShadowRoot = "NeedsJavaScriptToGetShadowRoot";
81+
82+
/// <summary>
83+
/// Gets the name of a browser quirk, for browser which cannot get a Shadow Root node at all.
84+
/// </summary>
85+
/// <remarks>
86+
/// <para>
87+
/// Browsers with this quirk cannot use any technique to get a Shadow Root node from a Shadow Host.
88+
/// They will fail with an exception stating that the technique is unsupported if such an operation is attempted.
89+
/// </para>
90+
/// </remarks>
91+
public static readonly string CannotGetShadowRoot = "CannotGetShadowRoot";
92+
7093
/// <summary>
7194
/// Gets hard-coded information about known browser quirks.
7295
/// </summary>
@@ -104,6 +127,30 @@ public static QuirksData GetQuirksData()
104127
new BrowserInfo { Name = "safari" },
105128
}
106129
}
130+
},
131+
{
132+
NeedsJavaScriptToGetShadowRoot,
133+
new BrowserInfoCollection
134+
{
135+
AffectedBrowsers = new HashSet<BrowserInfo>
136+
{
137+
new BrowserInfo { Name = "safari" },
138+
// There is no Chrome 95.1.0.0 but this covers any 95.0.x
139+
// The additional trailing zeroes are to work around https://github.com/csf-dev/CSF.Extensions.WebDriver/issues/56
140+
new BrowserInfo { Name = "chrome", MaxVersion = "95.1.0.0" },
141+
}
142+
}
143+
},
144+
{
145+
CannotGetShadowRoot,
146+
new BrowserInfoCollection
147+
{
148+
AffectedBrowsers = new HashSet<BrowserInfo>
149+
{
150+
// There is no Firefox 112.1 but this covers any 112.0.x
151+
new BrowserInfo { Name = "firefox", MaxVersion = "112.1" }
152+
}
153+
}
107154
}
108155
}
109156
};
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
2+
using System;
3+
using System.Collections.ObjectModel;
4+
using System.Drawing;
5+
using OpenQA.Selenium;
6+
7+
namespace CSF.Screenplay.Selenium.Elements
8+
{
9+
/// <summary>
10+
/// An adapter for Shadow Root objects, to use them as if they were <see cref="IWebElement"/>.
11+
/// </summary>
12+
/// <remarks>
13+
/// <para>
14+
/// All functionality of this type throws exceptions, except for <see cref="FindElement(By)"/> and <see cref="FindElements(By)"/>.
15+
/// </para>
16+
/// </remarks>
17+
public class ShadowRootAdapter : IWebElement
18+
{
19+
readonly ISearchContext shadowRoot;
20+
21+
22+
/// <inheritdoc/>
23+
public IWebElement FindElement(By by) => shadowRoot.FindElement(by);
24+
25+
/// <inheritdoc/>
26+
public ReadOnlyCollection<IWebElement> FindElements(By by) => shadowRoot.FindElements(by);
27+
28+
/// <summary>
29+
/// Returns a false name indicating that it is a shadow root.
30+
/// </summary>
31+
public string TagName => "#shadow-root";
32+
33+
/// <summary>
34+
/// Unsupported functionality, always throws.
35+
/// </summary>
36+
public string Text => throw new NotSupportedException();
37+
38+
/// <summary>
39+
/// Unsupported functionality, always throws.
40+
/// </summary>
41+
public bool Enabled => throw new NotSupportedException();
42+
43+
/// <summary>
44+
/// Unsupported functionality, always throws.
45+
/// </summary>
46+
public bool Selected => throw new NotSupportedException();
47+
48+
/// <summary>
49+
/// Unsupported functionality, always throws.
50+
/// </summary>
51+
public Point Location => throw new NotSupportedException();
52+
53+
/// <summary>
54+
/// Unsupported functionality, always throws.
55+
/// </summary>
56+
public Size Size => throw new NotSupportedException();
57+
58+
/// <summary>
59+
/// Unsupported functionality, always throws.
60+
/// </summary>
61+
public bool Displayed => throw new NotSupportedException();
62+
63+
/// <summary>
64+
/// Unsupported functionality, always throws.
65+
/// </summary>
66+
public void Clear() => throw new NotSupportedException();
67+
68+
/// <summary>
69+
/// Unsupported functionality, always throws.
70+
/// </summary>
71+
public void Click() => throw new NotSupportedException();
72+
73+
/// <summary>
74+
/// Unsupported functionality, always throws.
75+
/// </summary>
76+
public string GetAttribute(string attributeName) => throw new NotSupportedException();
77+
78+
/// <summary>
79+
/// Unsupported functionality, always throws.
80+
/// </summary>
81+
public string GetCssValue(string propertyName) => throw new NotSupportedException();
82+
83+
/// <summary>
84+
/// Unsupported functionality, always throws.
85+
/// </summary>
86+
public string GetDomAttribute(string attributeName) => throw new NotSupportedException();
87+
88+
/// <summary>
89+
/// Unsupported functionality, always throws.
90+
/// </summary>
91+
public string GetDomProperty(string propertyName) => throw new NotSupportedException();
92+
93+
/// <summary>
94+
/// Unsupported functionality, always throws.
95+
/// </summary>
96+
public string GetProperty(string propertyName) => throw new NotSupportedException();
97+
98+
/// <summary>
99+
/// Unsupported functionality, always throws.
100+
/// </summary>
101+
public ISearchContext GetShadowRoot() => throw new NotSupportedException();
102+
103+
/// <summary>
104+
/// Unsupported functionality, always throws.
105+
/// </summary>
106+
public void SendKeys(string text) => throw new NotSupportedException();
107+
108+
/// <summary>
109+
/// Unsupported functionality, always throws.
110+
/// </summary>
111+
public void Submit() => throw new NotSupportedException();
112+
113+
/// <summary>
114+
/// Initializes a new instance of <see cref="ShadowRootAdapter"/>.
115+
/// </summary>
116+
/// <param name="shadowRoot">The wrapped shadow root element</param>
117+
/// <exception cref="ArgumentNullException">If <paramref name="shadowRoot"/> is <see langword="null"/></exception>
118+
public ShadowRootAdapter(ISearchContext shadowRoot)
119+
{
120+
this.shadowRoot = shadowRoot ?? throw new ArgumentNullException(nameof(shadowRoot));
121+
}
122+
}
123+
}

CSF.Screenplay.Selenium/PerformableBuilder.elementQuestions.cs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
using System;
12
using System.Threading;
23
using CSF.Screenplay.Selenium.Builders;
34
using CSF.Screenplay.Selenium.Elements;
45
using CSF.Screenplay.Selenium.Queries;
6+
using CSF.Screenplay.Selenium.Questions;
7+
using CSF.Screenplay.Selenium.Tasks;
58

69
namespace CSF.Screenplay.Selenium
710
{
@@ -106,5 +109,93 @@ public static FilterElementsBuilder Filter(SeleniumElementCollection elements)
106109
/// <param name="element">The elements to interrogate for values.</param>
107110
/// <returns>A builder which chooses the query</returns>
108111
public static QuestionMultiQueryBuilder ReadFromTheCollectionOfElements(ITarget element) => new QuestionMultiQueryBuilder(element);
112+
113+
/// <summary>
114+
/// Gets a performable task/question which gets a Shadow Root from the specified Shadow Host target.
115+
/// </summary>
116+
/// <remarks>
117+
/// <para>
118+
/// This is used when working with web pages which use
119+
/// <see href="https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM">The Shadow DOM technique</see>.
120+
/// This question allows Screenplay to 'pierce' the Shadow DOM and get the Shadow Root element, so that the Performance
121+
/// may continue and interact with elements which are inside the Shadow DOM.
122+
/// </para>
123+
/// <para>
124+
/// Note that the <see cref="SeleniumElement"/> which is returned from this question is not a fully-fledged Selenium Element.
125+
/// It may be used only to get/find elements from inside the Shadow DOM. Use with any other performables will raise
126+
/// <see cref="NotSupportedException"/>.
127+
/// </para>
128+
/// <para>
129+
/// The <see cref="ITarget"/> passed to this performable as a parameter must be the Shadow Host element, or else this question will
130+
/// throw.
131+
/// </para>
132+
/// <para>
133+
/// This technique is supported only by recent Chromium and Firefox versions, and not by Safari.
134+
/// Use <see cref="GetTheShadowRootFrom(ITarget)"/> in order to automatically select the best technique for the current web browser.
135+
/// </para>
136+
/// </remarks>
137+
/// <param name="shadowHost">The Shadow Host element, or a locator which identifies it</param>
138+
/// <returns>A performable which gets the Shadow Root.</returns>
139+
public static IPerformableWithResult<SeleniumElement> GetTheShadowRootNativelyFrom(ITarget shadowHost)
140+
=> SingleElementPerformableWithResultAdapter.From(new GetShadowRootNatively(), shadowHost);
141+
142+
/// <summary>
143+
/// Gets a performable task/question which gets a Shadow Root from the specified Shadow Host target.
144+
/// </summary>
145+
/// <remarks>
146+
/// <para>
147+
/// This is used when working with web pages which use
148+
/// <see href="https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM">The Shadow DOM technique</see>.
149+
/// This question allows Screenplay to 'pierce' the Shadow DOM and get the Shadow Root element, so that the Performance
150+
/// may continue and interact with elements which are inside the Shadow DOM.
151+
/// </para>
152+
/// <para>
153+
/// Note that the <see cref="SeleniumElement"/> which is returned from this question is not a fully-fledged Selenium Element.
154+
/// It may be used only to get/find elements from inside the Shadow DOM. Use with any other performables will raise
155+
/// <see cref="NotSupportedException"/>.
156+
/// </para>
157+
/// <para>
158+
/// The <see cref="ITarget"/> passed to this performable as a parameter must be the Shadow Host element, or else this question will
159+
/// throw.
160+
/// </para>
161+
/// <para>
162+
/// This technique is supported only by older Chromium versions and Safari.
163+
/// Use <see cref="GetTheShadowRootFrom(ITarget)"/> in order to automatically select the best technique for the current web browser.
164+
/// </para>
165+
/// </remarks>
166+
/// <param name="shadowHost">The Shadow Host element, or a locator which identifies it</param>
167+
/// <returns>A performable which gets the Shadow Root.</returns>
168+
public static IPerformableWithResult<SeleniumElement> GetTheShadowRootWithJavaScriptFrom(ITarget shadowHost)
169+
=> SingleElementPerformableWithResultAdapter.From(new GetShadowRootWithJavaScript(), shadowHost);
170+
171+
/// <summary>
172+
/// Gets a performable task/question which gets a Shadow Root from the specified Shadow Host target.
173+
/// </summary>
174+
/// <remarks>
175+
/// <para>
176+
/// This is used when working with web pages which use
177+
/// <see href="https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM">The Shadow DOM technique</see>.
178+
/// This question allows Screenplay to 'pierce' the Shadow DOM and get the Shadow Root element, so that the Performance
179+
/// may continue and interact with elements which are inside the Shadow DOM.
180+
/// </para>
181+
/// <para>
182+
/// Note that the <see cref="SeleniumElement"/> which is returned from this question is not a fully-fledged Selenium Element.
183+
/// It may be used only to get/find elements from inside the Shadow DOM. Use with any other performables will raise
184+
/// <see cref="NotSupportedException"/>.
185+
/// </para>
186+
/// <para>
187+
/// The <see cref="ITarget"/> passed to this performable as a parameter must be the Shadow Host element, or else this question will
188+
/// throw.
189+
/// </para>
190+
/// <para>
191+
/// Use this method to automatically select the best technique to use for the current web browser.
192+
/// This functionality is unavailable for Firefox versions 112 and below, which do not support piercing the Shadow DOM from
193+
/// Selenium.
194+
/// </para>
195+
/// </remarks>
196+
/// <param name="shadowHost">The Shadow Host element, or a locator which identifies it</param>
197+
/// <returns>A performable which gets the Shadow Root.</returns>
198+
public static IPerformableWithResult<SeleniumElement> GetTheShadowRootFrom(ITarget shadowHost)
199+
=> new GetShadowRoot(shadowHost);
109200
}
110201
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using System;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
using CSF.Screenplay.Selenium.Elements;
5+
using OpenQA.Selenium;
6+
7+
namespace CSF.Screenplay.Selenium.Questions
8+
{
9+
/// <summary>
10+
/// A Screenplay Question which gets the Shadow Root element from the specified Selenium Element, using the native Selenium technique.
11+
/// </summary>
12+
/// <remarks>
13+
/// <para>
14+
/// This is used when working with web pages which use
15+
/// <see href="https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM">The Shadow DOM technique</see>.
16+
/// This question allows Screenplay to 'pierce' the Shadow DOM and get the Shadow Root element, so that the Performance
17+
/// may continue and interact with elements which are inside the Shadow DOM.
18+
/// </para>
19+
/// <para>
20+
/// Note that the <see cref="SeleniumElement"/> which is returned from this question is not a fully-fledged Selenium Element.
21+
/// It may be used only to get/find elements from inside the Shadow DOM. Use with any other performables will raise
22+
/// <see cref="NotSupportedException"/>.
23+
/// </para>
24+
/// <para>
25+
/// The <see cref="SeleniumElement"/> passed to this performable as a parameter must be the Shadow Host element.
26+
/// </para>
27+
/// <para>
28+
/// This technique is known to work on Chromium-based browsers from 96 onward and Firefox 113 onward.
29+
/// </para>
30+
/// </remarks>
31+
public class GetShadowRootNatively : ISingleElementPerformableWithResult<SeleniumElement>
32+
{
33+
/// <inheritdoc/>
34+
public ReportFragment GetReportFragment(Actor actor, Lazy<SeleniumElement> element, IFormatsReportFragment formatter)
35+
=> formatter.Format("{Actor} gets the Shadow Root node from {Element} using the native Selenium technique", actor, element.Value);
36+
37+
/// <inheritdoc/>
38+
public ValueTask<SeleniumElement> PerformAsAsync(ICanPerform actor, IWebDriver webDriver, Lazy<SeleniumElement> element, CancellationToken cancellationToken = default)
39+
{
40+
var shadowRoot = element.Value.WebElement.GetShadowRoot();
41+
return new ValueTask<SeleniumElement>(new SeleniumElement(new ShadowRootAdapter(shadowRoot)));
42+
}
43+
}
44+
}

0 commit comments

Comments
 (0)