Skip to content
Merged
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
16 changes: 10 additions & 6 deletions CSF.Screenplay.JsonToHtmlReport.Template/src/css/scenarioList.css
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,7 @@
text-align: right;
padding-right: 0.4em;
}
.reportableList .result,
.reportableList .exception {
.reportableList .result {
margin-left: 5em;
}
.reportableList .type+.report {
Expand All @@ -143,14 +142,19 @@
font-family: 'Lucida Console', 'Courier New', Courier, monospace;
white-space: pre;
display: block;
text-indent: -3em;
padding-left: 3em;
background: #FF000022;
background: #FF000011;
padding: 0.25em;
margin-left: 6.25em;
font-size: 80%;
line-height: 120%;
overflow: auto;
max-height: 15em;
}
.reportableList .exception::before {
font-family: 'Trebuchet MS', 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', Arial, sans-serif;
content: "Error: ";
content: "Error";
color: #888;
display: block;
}
.reportableList aside {
position: absolute;
Expand Down
47 changes: 47 additions & 0 deletions CSF.Screenplay.Selenium/BrowserQuirks.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using CSF.Extensions.WebDriver.Quirks;
using CSF.Screenplay.Selenium.Questions;

namespace CSF.Screenplay.Selenium
{
Expand Down Expand Up @@ -67,6 +68,28 @@ public static class BrowserQuirks
/// <seealso cref="Tasks.ClickAndWaitForDocumentReady"/>
public static readonly string NeedsToWaitAfterPageLoad = "NeedsToWaitAfterPageLoad";

/// <summary>
/// Gets the name of a browser quirk, for browser which cannot get a Shadow Root node using the native Selenium technique.
/// </summary>
/// <remarks>
/// <para>
/// Browsers with this quirk cannot use <see cref="GetShadowRootNatively"/> and must fall back to <see cref="GetShadowRootWithJavaScript"/>.
/// This makes use of a JavaScript fallback to get the Shadow Root node from the Shadow Host.
/// </para>
/// </remarks>
public static readonly string NeedsJavaScriptToGetShadowRoot = "NeedsJavaScriptToGetShadowRoot";

/// <summary>
/// Gets the name of a browser quirk, for browser which cannot get a Shadow Root node at all.
/// </summary>
/// <remarks>
/// <para>
/// Browsers with this quirk cannot use any technique to get a Shadow Root node from a Shadow Host.
/// They will fail with an exception stating that the technique is unsupported if such an operation is attempted.
/// </para>
/// </remarks>
public static readonly string CannotGetShadowRoot = "CannotGetShadowRoot";

/// <summary>
/// Gets hard-coded information about known browser quirks.
/// </summary>
Expand Down Expand Up @@ -104,6 +127,30 @@ public static QuirksData GetQuirksData()
new BrowserInfo { Name = "safari" },
}
}
},
{
NeedsJavaScriptToGetShadowRoot,
new BrowserInfoCollection
{
AffectedBrowsers = new HashSet<BrowserInfo>
{
new BrowserInfo { Name = "safari" },
// There is no Chrome 95.1.0.0 but this covers any 95.0.x
// The additional trailing zeroes are to work around https://github.com/csf-dev/CSF.Extensions.WebDriver/issues/56
new BrowserInfo { Name = "chrome", MaxVersion = "95.1.0.0" },
}
}
},
{
CannotGetShadowRoot,
new BrowserInfoCollection
{
AffectedBrowsers = new HashSet<BrowserInfo>
{
// There is no Firefox 112.1 but this covers any 112.0.x
new BrowserInfo { Name = "firefox", MaxVersion = "112.1" }
}
}
}
}
};
Expand Down
123 changes: 123 additions & 0 deletions CSF.Screenplay.Selenium/Elements/ShadowRootAdapter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@

using System;
using System.Collections.ObjectModel;
using System.Drawing;
using OpenQA.Selenium;

namespace CSF.Screenplay.Selenium.Elements
{
/// <summary>
/// An adapter for Shadow Root objects, to use them as if they were <see cref="IWebElement"/>.
/// </summary>
/// <remarks>
/// <para>
/// All functionality of this type throws exceptions, except for <see cref="FindElement(By)"/> and <see cref="FindElements(By)"/>.
/// </para>
/// </remarks>
public class ShadowRootAdapter : IWebElement
{
readonly ISearchContext shadowRoot;


/// <inheritdoc/>
public IWebElement FindElement(By by) => shadowRoot.FindElement(by);

/// <inheritdoc/>
public ReadOnlyCollection<IWebElement> FindElements(By by) => shadowRoot.FindElements(by);

/// <summary>
/// Returns a false name indicating that it is a shadow root.
/// </summary>
public string TagName => "#shadow-root";

/// <summary>
/// Unsupported functionality, always throws.
/// </summary>
public string Text => throw new NotSupportedException();

/// <summary>
/// Unsupported functionality, always throws.
/// </summary>
public bool Enabled => throw new NotSupportedException();

/// <summary>
/// Unsupported functionality, always throws.
/// </summary>
public bool Selected => throw new NotSupportedException();

/// <summary>
/// Unsupported functionality, always throws.
/// </summary>
public Point Location => throw new NotSupportedException();

/// <summary>
/// Unsupported functionality, always throws.
/// </summary>
public Size Size => throw new NotSupportedException();

/// <summary>
/// Unsupported functionality, always throws.
/// </summary>
public bool Displayed => throw new NotSupportedException();

/// <summary>
/// Unsupported functionality, always throws.
/// </summary>
public void Clear() => throw new NotSupportedException();

/// <summary>
/// Unsupported functionality, always throws.
/// </summary>
public void Click() => throw new NotSupportedException();

/// <summary>
/// Unsupported functionality, always throws.
/// </summary>
public string GetAttribute(string attributeName) => throw new NotSupportedException();

/// <summary>
/// Unsupported functionality, always throws.
/// </summary>
public string GetCssValue(string propertyName) => throw new NotSupportedException();

/// <summary>
/// Unsupported functionality, always throws.
/// </summary>
public string GetDomAttribute(string attributeName) => throw new NotSupportedException();

/// <summary>
/// Unsupported functionality, always throws.
/// </summary>
public string GetDomProperty(string propertyName) => throw new NotSupportedException();

/// <summary>
/// Unsupported functionality, always throws.
/// </summary>
public string GetProperty(string propertyName) => throw new NotSupportedException();

/// <summary>
/// Unsupported functionality, always throws.
/// </summary>
public ISearchContext GetShadowRoot() => throw new NotSupportedException();

/// <summary>
/// Unsupported functionality, always throws.
/// </summary>
public void SendKeys(string text) => throw new NotSupportedException();

/// <summary>
/// Unsupported functionality, always throws.
/// </summary>
public void Submit() => throw new NotSupportedException();

/// <summary>
/// Initializes a new instance of <see cref="ShadowRootAdapter"/>.
/// </summary>
/// <param name="shadowRoot">The wrapped shadow root element</param>
/// <exception cref="ArgumentNullException">If <paramref name="shadowRoot"/> is <see langword="null"/></exception>
public ShadowRootAdapter(ISearchContext shadowRoot)
{
this.shadowRoot = shadowRoot ?? throw new ArgumentNullException(nameof(shadowRoot));
}
}
}
91 changes: 91 additions & 0 deletions CSF.Screenplay.Selenium/PerformableBuilder.elementQuestions.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
using System;
using System.Threading;
using CSF.Screenplay.Selenium.Builders;
using CSF.Screenplay.Selenium.Elements;
using CSF.Screenplay.Selenium.Queries;
using CSF.Screenplay.Selenium.Questions;
using CSF.Screenplay.Selenium.Tasks;

namespace CSF.Screenplay.Selenium
{
Expand Down Expand Up @@ -106,5 +109,93 @@ public static FilterElementsBuilder Filter(SeleniumElementCollection elements)
/// <param name="element">The elements to interrogate for values.</param>
/// <returns>A builder which chooses the query</returns>
public static QuestionMultiQueryBuilder ReadFromTheCollectionOfElements(ITarget element) => new QuestionMultiQueryBuilder(element);

/// <summary>
/// Gets a performable task/question which gets a Shadow Root from the specified Shadow Host target.
/// </summary>
/// <remarks>
/// <para>
/// This is used when working with web pages which use
/// <see href="https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM">The Shadow DOM technique</see>.
/// This question allows Screenplay to 'pierce' the Shadow DOM and get the Shadow Root element, so that the Performance
/// may continue and interact with elements which are inside the Shadow DOM.
/// </para>
/// <para>
/// Note that the <see cref="SeleniumElement"/> which is returned from this question is not a fully-fledged Selenium Element.
/// It may be used only to get/find elements from inside the Shadow DOM. Use with any other performables will raise
/// <see cref="NotSupportedException"/>.
/// </para>
/// <para>
/// The <see cref="ITarget"/> passed to this performable as a parameter must be the Shadow Host element, or else this question will
/// throw.
/// </para>
/// <para>
/// This technique is supported only by recent Chromium and Firefox versions, and not by Safari.
/// Use <see cref="GetTheShadowRootFrom(ITarget)"/> in order to automatically select the best technique for the current web browser.
/// </para>
/// </remarks>
/// <param name="shadowHost">The Shadow Host element, or a locator which identifies it</param>
/// <returns>A performable which gets the Shadow Root.</returns>
public static IPerformableWithResult<SeleniumElement> GetTheShadowRootNativelyFrom(ITarget shadowHost)
=> SingleElementPerformableWithResultAdapter.From(new GetShadowRootNatively(), shadowHost);

/// <summary>
/// Gets a performable task/question which gets a Shadow Root from the specified Shadow Host target.
/// </summary>
/// <remarks>
/// <para>
/// This is used when working with web pages which use
/// <see href="https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM">The Shadow DOM technique</see>.
/// This question allows Screenplay to 'pierce' the Shadow DOM and get the Shadow Root element, so that the Performance
/// may continue and interact with elements which are inside the Shadow DOM.
/// </para>
/// <para>
/// Note that the <see cref="SeleniumElement"/> which is returned from this question is not a fully-fledged Selenium Element.
/// It may be used only to get/find elements from inside the Shadow DOM. Use with any other performables will raise
/// <see cref="NotSupportedException"/>.
/// </para>
/// <para>
/// The <see cref="ITarget"/> passed to this performable as a parameter must be the Shadow Host element, or else this question will
/// throw.
/// </para>
/// <para>
/// This technique is supported only by older Chromium versions and Safari.
/// Use <see cref="GetTheShadowRootFrom(ITarget)"/> in order to automatically select the best technique for the current web browser.
/// </para>
/// </remarks>
/// <param name="shadowHost">The Shadow Host element, or a locator which identifies it</param>
/// <returns>A performable which gets the Shadow Root.</returns>
public static IPerformableWithResult<SeleniumElement> GetTheShadowRootWithJavaScriptFrom(ITarget shadowHost)
=> SingleElementPerformableWithResultAdapter.From(new GetShadowRootWithJavaScript(), shadowHost);

/// <summary>
/// Gets a performable task/question which gets a Shadow Root from the specified Shadow Host target.
/// </summary>
/// <remarks>
/// <para>
/// This is used when working with web pages which use
/// <see href="https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM">The Shadow DOM technique</see>.
/// This question allows Screenplay to 'pierce' the Shadow DOM and get the Shadow Root element, so that the Performance
/// may continue and interact with elements which are inside the Shadow DOM.
/// </para>
/// <para>
/// Note that the <see cref="SeleniumElement"/> which is returned from this question is not a fully-fledged Selenium Element.
/// It may be used only to get/find elements from inside the Shadow DOM. Use with any other performables will raise
/// <see cref="NotSupportedException"/>.
/// </para>
/// <para>
/// The <see cref="ITarget"/> passed to this performable as a parameter must be the Shadow Host element, or else this question will
/// throw.
/// </para>
/// <para>
/// Use this method to automatically select the best technique to use for the current web browser.
/// This functionality is unavailable for Firefox versions 112 and below, which do not support piercing the Shadow DOM from
/// Selenium.
/// </para>
/// </remarks>
/// <param name="shadowHost">The Shadow Host element, or a locator which identifies it</param>
/// <returns>A performable which gets the Shadow Root.</returns>
public static IPerformableWithResult<SeleniumElement> GetTheShadowRootFrom(ITarget shadowHost)
=> new GetShadowRoot(shadowHost);
}
}
44 changes: 44 additions & 0 deletions CSF.Screenplay.Selenium/Questions/GetShadowRootNatively.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using CSF.Screenplay.Selenium.Elements;
using OpenQA.Selenium;

namespace CSF.Screenplay.Selenium.Questions
{
/// <summary>
/// A Screenplay Question which gets the Shadow Root element from the specified Selenium Element, using the native Selenium technique.
/// </summary>
/// <remarks>
/// <para>
/// This is used when working with web pages which use
/// <see href="https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM">The Shadow DOM technique</see>.
/// This question allows Screenplay to 'pierce' the Shadow DOM and get the Shadow Root element, so that the Performance
/// may continue and interact with elements which are inside the Shadow DOM.
/// </para>
/// <para>
/// Note that the <see cref="SeleniumElement"/> which is returned from this question is not a fully-fledged Selenium Element.
/// It may be used only to get/find elements from inside the Shadow DOM. Use with any other performables will raise
/// <see cref="NotSupportedException"/>.
/// </para>
/// <para>
/// The <see cref="SeleniumElement"/> passed to this performable as a parameter must be the Shadow Host element.
/// </para>
/// <para>
/// This technique is known to work on Chromium-based browsers from 96 onward and Firefox 113 onward.
/// </para>
/// </remarks>
public class GetShadowRootNatively : ISingleElementPerformableWithResult<SeleniumElement>
{
/// <inheritdoc/>
public ReportFragment GetReportFragment(Actor actor, Lazy<SeleniumElement> element, IFormatsReportFragment formatter)
=> formatter.Format("{Actor} gets the Shadow Root node from {Element} using the native Selenium technique", actor, element.Value);

/// <inheritdoc/>
public ValueTask<SeleniumElement> PerformAsAsync(ICanPerform actor, IWebDriver webDriver, Lazy<SeleniumElement> element, CancellationToken cancellationToken = default)
{
var shadowRoot = element.Value.WebElement.GetShadowRoot();
return new ValueTask<SeleniumElement>(new SeleniumElement(new ShadowRootAdapter(shadowRoot)));
}
}
}
Loading
Loading