diff --git a/CSF.Screenplay.JsonToHtmlReport.Template/src/css/scenarioList.css b/CSF.Screenplay.JsonToHtmlReport.Template/src/css/scenarioList.css index 8c543a89..7b46c32f 100644 --- a/CSF.Screenplay.JsonToHtmlReport.Template/src/css/scenarioList.css +++ b/CSF.Screenplay.JsonToHtmlReport.Template/src/css/scenarioList.css @@ -122,8 +122,7 @@ text-align: right; padding-right: 0.4em; } - .reportableList .result, - .reportableList .exception { + .reportableList .result { margin-left: 5em; } .reportableList .type+.report { @@ -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; diff --git a/CSF.Screenplay.Selenium/BrowserQuirks.cs b/CSF.Screenplay.Selenium/BrowserQuirks.cs index 46b8354e..48c50116 100644 --- a/CSF.Screenplay.Selenium/BrowserQuirks.cs +++ b/CSF.Screenplay.Selenium/BrowserQuirks.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using CSF.Extensions.WebDriver.Quirks; +using CSF.Screenplay.Selenium.Questions; namespace CSF.Screenplay.Selenium { @@ -67,6 +68,28 @@ public static class BrowserQuirks /// public static readonly string NeedsToWaitAfterPageLoad = "NeedsToWaitAfterPageLoad"; + /// + /// Gets the name of a browser quirk, for browser which cannot get a Shadow Root node using the native Selenium technique. + /// + /// + /// + /// Browsers with this quirk cannot use and must fall back to . + /// This makes use of a JavaScript fallback to get the Shadow Root node from the Shadow Host. + /// + /// + public static readonly string NeedsJavaScriptToGetShadowRoot = "NeedsJavaScriptToGetShadowRoot"; + + /// + /// Gets the name of a browser quirk, for browser which cannot get a Shadow Root node at all. + /// + /// + /// + /// 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. + /// + /// + public static readonly string CannotGetShadowRoot = "CannotGetShadowRoot"; + /// /// Gets hard-coded information about known browser quirks. /// @@ -104,6 +127,30 @@ public static QuirksData GetQuirksData() new BrowserInfo { Name = "safari" }, } } + }, + { + NeedsJavaScriptToGetShadowRoot, + new BrowserInfoCollection + { + AffectedBrowsers = new HashSet + { + 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 + { + // There is no Firefox 112.1 but this covers any 112.0.x + new BrowserInfo { Name = "firefox", MaxVersion = "112.1" } + } + } } } }; diff --git a/CSF.Screenplay.Selenium/Elements/ShadowRootAdapter.cs b/CSF.Screenplay.Selenium/Elements/ShadowRootAdapter.cs new file mode 100644 index 00000000..eef3102e --- /dev/null +++ b/CSF.Screenplay.Selenium/Elements/ShadowRootAdapter.cs @@ -0,0 +1,123 @@ + +using System; +using System.Collections.ObjectModel; +using System.Drawing; +using OpenQA.Selenium; + +namespace CSF.Screenplay.Selenium.Elements +{ + /// + /// An adapter for Shadow Root objects, to use them as if they were . + /// + /// + /// + /// All functionality of this type throws exceptions, except for and . + /// + /// + public class ShadowRootAdapter : IWebElement + { + readonly ISearchContext shadowRoot; + + + /// + public IWebElement FindElement(By by) => shadowRoot.FindElement(by); + + /// + public ReadOnlyCollection FindElements(By by) => shadowRoot.FindElements(by); + + /// + /// Returns a false name indicating that it is a shadow root. + /// + public string TagName => "#shadow-root"; + + /// + /// Unsupported functionality, always throws. + /// + public string Text => throw new NotSupportedException(); + + /// + /// Unsupported functionality, always throws. + /// + public bool Enabled => throw new NotSupportedException(); + + /// + /// Unsupported functionality, always throws. + /// + public bool Selected => throw new NotSupportedException(); + + /// + /// Unsupported functionality, always throws. + /// + public Point Location => throw new NotSupportedException(); + + /// + /// Unsupported functionality, always throws. + /// + public Size Size => throw new NotSupportedException(); + + /// + /// Unsupported functionality, always throws. + /// + public bool Displayed => throw new NotSupportedException(); + + /// + /// Unsupported functionality, always throws. + /// + public void Clear() => throw new NotSupportedException(); + + /// + /// Unsupported functionality, always throws. + /// + public void Click() => throw new NotSupportedException(); + + /// + /// Unsupported functionality, always throws. + /// + public string GetAttribute(string attributeName) => throw new NotSupportedException(); + + /// + /// Unsupported functionality, always throws. + /// + public string GetCssValue(string propertyName) => throw new NotSupportedException(); + + /// + /// Unsupported functionality, always throws. + /// + public string GetDomAttribute(string attributeName) => throw new NotSupportedException(); + + /// + /// Unsupported functionality, always throws. + /// + public string GetDomProperty(string propertyName) => throw new NotSupportedException(); + + /// + /// Unsupported functionality, always throws. + /// + public string GetProperty(string propertyName) => throw new NotSupportedException(); + + /// + /// Unsupported functionality, always throws. + /// + public ISearchContext GetShadowRoot() => throw new NotSupportedException(); + + /// + /// Unsupported functionality, always throws. + /// + public void SendKeys(string text) => throw new NotSupportedException(); + + /// + /// Unsupported functionality, always throws. + /// + public void Submit() => throw new NotSupportedException(); + + /// + /// Initializes a new instance of . + /// + /// The wrapped shadow root element + /// If is + public ShadowRootAdapter(ISearchContext shadowRoot) + { + this.shadowRoot = shadowRoot ?? throw new ArgumentNullException(nameof(shadowRoot)); + } + } +} \ No newline at end of file diff --git a/CSF.Screenplay.Selenium/PerformableBuilder.elementQuestions.cs b/CSF.Screenplay.Selenium/PerformableBuilder.elementQuestions.cs index af3198e1..c2d9a9c0 100644 --- a/CSF.Screenplay.Selenium/PerformableBuilder.elementQuestions.cs +++ b/CSF.Screenplay.Selenium/PerformableBuilder.elementQuestions.cs @@ -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 { @@ -106,5 +109,93 @@ public static FilterElementsBuilder Filter(SeleniumElementCollection elements) /// The elements to interrogate for values. /// A builder which chooses the query public static QuestionMultiQueryBuilder ReadFromTheCollectionOfElements(ITarget element) => new QuestionMultiQueryBuilder(element); + + /// + /// Gets a performable task/question which gets a Shadow Root from the specified Shadow Host target. + /// + /// + /// + /// This is used when working with web pages which use + /// The Shadow DOM technique. + /// 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. + /// + /// + /// Note that the 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 + /// . + /// + /// + /// The passed to this performable as a parameter must be the Shadow Host element, or else this question will + /// throw. + /// + /// + /// This technique is supported only by recent Chromium and Firefox versions, and not by Safari. + /// Use in order to automatically select the best technique for the current web browser. + /// + /// + /// The Shadow Host element, or a locator which identifies it + /// A performable which gets the Shadow Root. + public static IPerformableWithResult GetTheShadowRootNativelyFrom(ITarget shadowHost) + => SingleElementPerformableWithResultAdapter.From(new GetShadowRootNatively(), shadowHost); + + /// + /// Gets a performable task/question which gets a Shadow Root from the specified Shadow Host target. + /// + /// + /// + /// This is used when working with web pages which use + /// The Shadow DOM technique. + /// 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. + /// + /// + /// Note that the 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 + /// . + /// + /// + /// The passed to this performable as a parameter must be the Shadow Host element, or else this question will + /// throw. + /// + /// + /// This technique is supported only by older Chromium versions and Safari. + /// Use in order to automatically select the best technique for the current web browser. + /// + /// + /// The Shadow Host element, or a locator which identifies it + /// A performable which gets the Shadow Root. + public static IPerformableWithResult GetTheShadowRootWithJavaScriptFrom(ITarget shadowHost) + => SingleElementPerformableWithResultAdapter.From(new GetShadowRootWithJavaScript(), shadowHost); + + /// + /// Gets a performable task/question which gets a Shadow Root from the specified Shadow Host target. + /// + /// + /// + /// This is used when working with web pages which use + /// The Shadow DOM technique. + /// 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. + /// + /// + /// Note that the 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 + /// . + /// + /// + /// The passed to this performable as a parameter must be the Shadow Host element, or else this question will + /// throw. + /// + /// + /// 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. + /// + /// + /// The Shadow Host element, or a locator which identifies it + /// A performable which gets the Shadow Root. + public static IPerformableWithResult GetTheShadowRootFrom(ITarget shadowHost) + => new GetShadowRoot(shadowHost); } } \ No newline at end of file diff --git a/CSF.Screenplay.Selenium/Questions/GetShadowRootNatively.cs b/CSF.Screenplay.Selenium/Questions/GetShadowRootNatively.cs new file mode 100644 index 00000000..54239f9f --- /dev/null +++ b/CSF.Screenplay.Selenium/Questions/GetShadowRootNatively.cs @@ -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 +{ + /// + /// A Screenplay Question which gets the Shadow Root element from the specified Selenium Element, using the native Selenium technique. + /// + /// + /// + /// This is used when working with web pages which use + /// The Shadow DOM technique. + /// 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. + /// + /// + /// Note that the 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 + /// . + /// + /// + /// The passed to this performable as a parameter must be the Shadow Host element. + /// + /// + /// This technique is known to work on Chromium-based browsers from 96 onward and Firefox 113 onward. + /// + /// + public class GetShadowRootNatively : ISingleElementPerformableWithResult + { + /// + public ReportFragment GetReportFragment(Actor actor, Lazy element, IFormatsReportFragment formatter) + => formatter.Format("{Actor} gets the Shadow Root node from {Element} using the native Selenium technique", actor, element.Value); + + /// + public ValueTask PerformAsAsync(ICanPerform actor, IWebDriver webDriver, Lazy element, CancellationToken cancellationToken = default) + { + var shadowRoot = element.Value.WebElement.GetShadowRoot(); + return new ValueTask(new SeleniumElement(new ShadowRootAdapter(shadowRoot))); + } + } +} \ No newline at end of file diff --git a/CSF.Screenplay.Selenium/Questions/GetShadowRootWithJavaScript.cs b/CSF.Screenplay.Selenium/Questions/GetShadowRootWithJavaScript.cs new file mode 100644 index 00000000..ba9d831b --- /dev/null +++ b/CSF.Screenplay.Selenium/Questions/GetShadowRootWithJavaScript.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using CSF.Screenplay.Selenium.Elements; +using OpenQA.Selenium; +using static CSF.Screenplay.Selenium.PerformableBuilder; + +namespace CSF.Screenplay.Selenium.Questions +{ + /// + /// A Screenplay Question which gets the Shadow Root element from the specified Selenium Element, using the simple JavaScript technique. + /// + /// + /// + /// This is used when working with web pages which use + /// The Shadow DOM technique. + /// 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. + /// + /// + /// Note that the 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 + /// . + /// + /// + /// The passed to this performable as a parameter must be the Shadow Host element. + /// + /// + /// This technique is known to work on older Chromium versions (before 96) and Safari. + /// + /// + public class GetShadowRootWithJavaScript : ISingleElementPerformableWithResult + { + /// + public ReportFragment GetReportFragment(Actor actor, Lazy element, IFormatsReportFragment formatter) + => formatter.Format("{Actor} gets the Shadow Root node from {Element} using JavaScript", actor, element.Value); + + /// + public async ValueTask PerformAsAsync(ICanPerform actor, IWebDriver webDriver, Lazy element, CancellationToken cancellationToken = default) + { + var shadowRoot = await actor.PerformAsync(ExecuteAScript(Scripts.GetShadowRoot, element.Value.WebElement), cancellationToken).ConfigureAwait(false); + return new SeleniumElement(new ShadowRootAdapter(shadowRoot)); + } + } +} \ No newline at end of file diff --git a/CSF.Screenplay.Selenium/Resources/ScriptResources.cs b/CSF.Screenplay.Selenium/Resources/ScriptResources.cs index 8e621b1f..e85923a1 100644 --- a/CSF.Screenplay.Selenium/Resources/ScriptResources.cs +++ b/CSF.Screenplay.Selenium/Resources/ScriptResources.cs @@ -20,5 +20,8 @@ static class ScriptResources /// Gets a short JavaScript which sets the value of an HTML element in a way that simulates updating the element interactively. internal static string SetElementValueSimulatedInteractively => resourceManager.GetString("SetElementValueSimulatedInteractively"); + + /// Gets a short JavaScript which gets a Shadow Root node from a Shadow Host element. + internal static string GetShadowRoot => resourceManager.GetString("GetShadowRoot"); } } \ No newline at end of file diff --git a/CSF.Screenplay.Selenium/Resources/ScriptResources.restext b/CSF.Screenplay.Selenium/Resources/ScriptResources.restext index c1dc8bca..ea7ccfee 100644 --- a/CSF.Screenplay.Selenium/Resources/ScriptResources.restext +++ b/CSF.Screenplay.Selenium/Resources/ScriptResources.restext @@ -1,4 +1,5 @@ ClearLocalStorage = localStorage.clear() GetDocReadyState = return document.readyState SetElementValue = arguments[0].value = arguments[1] -SetElementValueSimulatedInteractively = ((el, v) => {const d = (e, n) => e.dispatchEvent(new Event(n, {bubbles: true}));d(el, 'focus');el.value = v;d(el, 'input');d(el, 'change');d(el, 'blur');})(arguments[0], arguments[1]) \ No newline at end of file +SetElementValueSimulatedInteractively = ((el, v) => {const d = (e, n) => e.dispatchEvent(new Event(n, {bubbles: true}));d(el, 'focus');el.value = v;d(el, 'input');d(el, 'change');d(el, 'blur');})(arguments[0], arguments[1]) +GetShadowRoot = return arguments[0].shadowRoot \ No newline at end of file diff --git a/CSF.Screenplay.Selenium/Scripts.cs b/CSF.Screenplay.Selenium/Scripts.cs index 0c14be1b..486720a7 100644 --- a/CSF.Screenplay.Selenium/Scripts.cs +++ b/CSF.Screenplay.Selenium/Scripts.cs @@ -41,5 +41,11 @@ public static NamedScriptWithResult GetTheDocumentReadyState /// public static NamedScript SetElementValueSimulatedInteractively => new NamedScript(Resources.ScriptResources.SetElementValueSimulatedInteractively, "simulate setting the element's value interactively"); + + /// + /// Gets a which gets the shadow root contained within the specified shadow host. + /// + public static NamedScriptWithResult GetShadowRoot + => new NamedScriptWithResult(Resources.ScriptResources.GetShadowRoot, "get a shadow root"); } } \ No newline at end of file diff --git a/CSF.Screenplay.Selenium/Tasks/GetShadowRoot.cs b/CSF.Screenplay.Selenium/Tasks/GetShadowRoot.cs new file mode 100644 index 00000000..c795aa87 --- /dev/null +++ b/CSF.Screenplay.Selenium/Tasks/GetShadowRoot.cs @@ -0,0 +1,67 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using CSF.Screenplay.Selenium.Elements; +using OpenQA.Selenium; +using static CSF.Screenplay.Selenium.PerformableBuilder; + +namespace CSF.Screenplay.Selenium.Tasks +{ + /// + /// A Screenplay Task which gets the Shadow Root element from the specified Selenium Element, using the best available technique for the current web browser. + /// + /// + /// + /// This is used when working with web pages which use + /// The Shadow DOM technique. + /// 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. + /// + /// + /// Note that the 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 + /// . + /// + /// + /// The passed to this performable as a parameter must be the Shadow Host element. + /// + /// + /// This task will automatically select the best technique by which to get a Shadow Root. For modern Chromium or Firefox-based + /// browsers, it will use the native technique: . For older Chromium-based + /// browsers, or any version of Safari it will use a JavaScript approach: . + /// For very old versions of Firefox, this performable will throw an exception, as there is no supported way to get a Shadow Root. + /// + /// + public class GetShadowRoot : IPerformableWithResult, ICanReport + { + readonly ITarget element; + + /// + public ReportFragment GetReportFragment(Actor actor, IFormatsReportFragment formatter) + => formatter.Format("{Actor} gets the Shadow Root from {Element}", actor, element); + + /// + public ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + { + var browseTheWeb = actor.GetAbility(); + + if(browseTheWeb.WebDriver.HasQuirk(BrowserQuirks.CannotGetShadowRoot)) + throw new NotSupportedException("The current web browser is not capable of getting Shadow Roots via any known technique"); + + if(browseTheWeb.WebDriver.HasQuirk(BrowserQuirks.NeedsJavaScriptToGetShadowRoot)) + return actor.PerformAsync(GetTheShadowRootWithJavaScriptFrom(element), cancellationToken); + + return actor.PerformAsync(GetTheShadowRootNativelyFrom(element), cancellationToken); + } + + /// + /// Initializes a new instance of . + /// + /// The Shadow Host element + /// If is + public GetShadowRoot(ITarget element) + { + this.element = element ?? throw new ArgumentNullException(nameof(element)); + } + } +} \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.TestWebapp/Controllers/DelayedOpeningController.cs b/Tests/CSF.Screenplay.Selenium.TestWebapp/Controllers/DelayedOpeningController.cs index f877f04c..f62e0a09 100644 --- a/Tests/CSF.Screenplay.Selenium.TestWebapp/Controllers/DelayedOpeningController.cs +++ b/Tests/CSF.Screenplay.Selenium.TestWebapp/Controllers/DelayedOpeningController.cs @@ -2,6 +2,8 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; +namespace CSF.Screenplay.Selenium.TestWebApp; + public class DelayedOpeningController : Controller { [HttpGet, Route("DelayedOpening")] diff --git a/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/GetShadowRoot.html b/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/GetShadowRoot.html new file mode 100644 index 00000000..adfe6594 --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.TestWebapp/wwwroot/GetShadowRoot.html @@ -0,0 +1,23 @@ + + + Selenium Shadow Root tests + + +

Selenium Shadow Root tests

+

+ This page includes a Shadow DOM, + which means that its elements cannot be selected by Selenium natively. +

+
+ + + \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Elements/ShadowRootAdapterTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Elements/ShadowRootAdapterTests.cs new file mode 100644 index 00000000..921a29f2 --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.Tests/Elements/ShadowRootAdapterTests.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.ObjectModel; +using Moq; +using OpenQA.Selenium; + +namespace CSF.Screenplay.Selenium.Elements; + +[TestFixture, Parallelizable] +public class ShadowRootAdapterTests +{ + [Test, AutoMoqData] + public void FindElementShouldExerciseWrappedImpl([Frozen] ISearchContext wrapped, ShadowRootAdapter sut) + { + var by = By.Id("foo"); + sut.FindElement(by); + Mock.Get(wrapped).Verify(x => x.FindElement(by)); + } + + [Test, AutoMoqData] + public void FindElementsShouldExerciseWrappedImpl([Frozen] ISearchContext wrapped, ShadowRootAdapter sut) + { + var by = By.Id("foo"); + Mock.Get(wrapped).Setup(x => x.FindElements(by)).Returns(new ReadOnlyCollection([])); + sut.FindElements(by); + Mock.Get(wrapped).Verify(x => x.FindElements(by)); + } + + [Test, AutoMoqData] + public void TagNameShouldReturnHardcodedResult(ShadowRootAdapter sut) + { + Assert.That(sut.TagName, Is.EqualTo("#shadow-root")); + } + + [Test, AutoMoqData] + public void TextShouldThrow(ShadowRootAdapter sut) + { + Assert.That(() => sut.Text, Throws.InstanceOf()); + } + + + [Test, AutoMoqData] + public void EnabledShouldThrow(ShadowRootAdapter sut) + { + Assert.That(() => sut.Enabled, Throws.InstanceOf()); + } + + [Test, AutoMoqData] + public void SelectedShouldThrow(ShadowRootAdapter sut) + { + Assert.That(() => sut.Selected, Throws.InstanceOf()); + } + + [Test, AutoMoqData] + public void LocationShouldThrow(ShadowRootAdapter sut) + { + Assert.That(() => sut.Location, Throws.InstanceOf()); + } + + [Test, AutoMoqData] + public void SizeShouldThrow(ShadowRootAdapter sut) + { + Assert.That(() => sut.Size, Throws.InstanceOf()); + } + + [Test, AutoMoqData] + public void DisplayedShouldThrow(ShadowRootAdapter sut) + { + Assert.That(() => sut.Displayed, Throws.InstanceOf()); + } + + [Test, AutoMoqData] + public void ClearShouldThrow(ShadowRootAdapter sut) + { + Assert.That(sut.Clear, Throws.InstanceOf()); + } + + [Test, AutoMoqData] + public void ClickShouldThrow(ShadowRootAdapter sut) + { + Assert.That(sut.Click, Throws.InstanceOf()); + } + + [Test, AutoMoqData] + public void GetAttributeShouldThrow(ShadowRootAdapter sut, string attributeName) + { + Assert.That(() => sut.GetAttribute(attributeName), Throws.InstanceOf()); + } + + [Test, AutoMoqData] + public void GetCssValueShouldThrow(ShadowRootAdapter sut, string propertyName) + { + Assert.That(() => sut.GetCssValue(propertyName), Throws.InstanceOf()); + } + + [Test, AutoMoqData] + public void GetDomAttributeShouldThrow(ShadowRootAdapter sut, string attributeName) + { + Assert.That(() => sut.GetDomAttribute(attributeName), Throws.InstanceOf()); + } + + [Test, AutoMoqData] + public void GetDomPropertyShouldThrow(ShadowRootAdapter sut, string propertyName) + { + Assert.That(() => sut.GetDomProperty(propertyName), Throws.InstanceOf()); + } + + [Test, AutoMoqData] + public void GetPropertyShouldThrow(ShadowRootAdapter sut, string propertyName) + { + Assert.That(() => sut.GetProperty(propertyName), Throws.InstanceOf()); + } + + [Test, AutoMoqData] + public void GetShadowRootShouldThrow(ShadowRootAdapter sut) + { + Assert.That(sut.GetShadowRoot, Throws.InstanceOf()); + } + + [Test, AutoMoqData] + public void SendKeysShouldThrow(ShadowRootAdapter sut, string text) + { + Assert.That(() => sut.SendKeys(text), Throws.InstanceOf()); + } + + [Test, AutoMoqData] + public void SubmitShouldThrow(ShadowRootAdapter sut) + { + Assert.That(sut.Submit, Throws.InstanceOf()); + } +} \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Questions/GetShadowRootNativelyTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Questions/GetShadowRootNativelyTests.cs new file mode 100644 index 00000000..a140cd1f --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.Tests/Questions/GetShadowRootNativelyTests.cs @@ -0,0 +1,34 @@ +using CSF.Screenplay.Selenium.Elements; +using OpenQA.Selenium; +using static CSF.Screenplay.PerformanceStarter; +using static CSF.Screenplay.Selenium.PerformableBuilder; + +namespace CSF.Screenplay.Selenium.Questions; + +[TestFixture, Parallelizable] +public class GetShadowRootNativelyTests +{ + static readonly Locator + host = new ElementId("shadowHost", "The shadow host"), + content = new CssSelector("p.content", "The content inside the Shadow DOM"); + + static readonly NamedUri testPage = new NamedUri("GetShadowRoot.html", "the test page"); + + [Test, Screenplay] + public async Task GetShadowRootNativelyShouldResultInBeingAbleToReadTheShadowDomContent(IStage stage) + { + var webster = stage.Spotlight(); + var browseTheWeb = webster.GetAbility(); + + if (browseTheWeb.WebDriver.HasQuirk(BrowserQuirks.CannotGetShadowRoot) + || browseTheWeb.WebDriver.HasQuirk(BrowserQuirks.NeedsJavaScriptToGetShadowRoot)) + Assert.Pass("This test cannot be run on the current web browser"); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + var shadowRoot = await When(webster).AttemptsTo(GetTheShadowRootNativelyFrom(host)); + var shadowContent = await Then(webster).Should(FindAnElementWithin(shadowRoot).WhichMatches(content)); + var text = await Then(webster).Should(ReadFromTheElement(shadowContent).TheText()); + + Assert.That(text, Is.EqualTo("I am an element inside the Shadow DOM")); + } +} diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Questions/GetShadowRootWithJavaScriptTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Questions/GetShadowRootWithJavaScriptTests.cs new file mode 100644 index 00000000..14b10026 --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.Tests/Questions/GetShadowRootWithJavaScriptTests.cs @@ -0,0 +1,33 @@ +using CSF.Screenplay.Selenium.Elements; +using OpenQA.Selenium; +using static CSF.Screenplay.PerformanceStarter; +using static CSF.Screenplay.Selenium.PerformableBuilder; + +namespace CSF.Screenplay.Selenium.Questions; + +[TestFixture, Parallelizable] +public class GetShadowRootWithJavaScriptTests +{ + static readonly Locator + host = new ElementId("shadowHost", "The shadow host"), + content = new CssSelector("p.content", "The content inside the Shadow DOM"); + + static readonly NamedUri testPage = new NamedUri("GetShadowRoot.html", "the test page"); + + [Test, Screenplay] + public async Task GetShadowRootWithJavaScriptShouldResultInBeingAbleToReadTheShadowDomContent(IStage stage) + { + var webster = stage.Spotlight(); + var browseTheWeb = webster.GetAbility(); + + if (browseTheWeb.WebDriver.HasQuirk(BrowserQuirks.CannotGetShadowRoot)) + Assert.Pass("This test cannot be run on the current web browser"); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + var shadowRoot = await When(webster).AttemptsTo(GetTheShadowRootWithJavaScriptFrom(host)); + var shadowContent = await Then(webster).Should(FindAnElementWithin(shadowRoot).WhichMatches(content)); + var text = await Then(webster).Should(ReadFromTheElement(shadowContent).TheText()); + + Assert.That(text, Is.EqualTo("I am an element inside the Shadow DOM")); + } +} \ No newline at end of file diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Tasks/GetShadowRootNativelyTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Tasks/GetShadowRootNativelyTests.cs new file mode 100644 index 00000000..d5a216d9 --- /dev/null +++ b/Tests/CSF.Screenplay.Selenium.Tests/Tasks/GetShadowRootNativelyTests.cs @@ -0,0 +1,33 @@ +using CSF.Screenplay.Selenium.Elements; +using OpenQA.Selenium; +using static CSF.Screenplay.PerformanceStarter; +using static CSF.Screenplay.Selenium.PerformableBuilder; + +namespace CSF.Screenplay.Selenium.Tasks; + +[TestFixture, Parallelizable] +public class GetShadowRootTests +{ + static readonly Locator + host = new ElementId("shadowHost", "The shadow host"), + content = new CssSelector("p.content", "The content inside the Shadow DOM"); + + static readonly NamedUri testPage = new NamedUri("GetShadowRoot.html", "the test page"); + + [Test, Screenplay] + public async Task GetShadowRootShouldResultInBeingAbleToReadTheShadowDomContent(IStage stage) + { + var webster = stage.Spotlight(); + var browseTheWeb = webster.GetAbility(); + + if (browseTheWeb.WebDriver.HasQuirk(BrowserQuirks.CannotGetShadowRoot)) + Assert.Pass("This test cannot be run on the current web browser"); + + await Given(webster).WasAbleTo(OpenTheUrl(testPage)); + var shadowRoot = await When(webster).AttemptsTo(GetTheShadowRootFrom(host)); + var shadowContent = await Then(webster).Should(FindAnElementWithin(shadowRoot).WhichMatches(content)); + var text = await Then(webster).Should(ReadFromTheElement(shadowContent).TheText()); + + Assert.That(text, Is.EqualTo("I am an element inside the Shadow DOM")); + } +}