diff --git a/CSF.Screenplay.Selenium/Builders/FindElementBuilder.cs b/CSF.Screenplay.Selenium/Builders/FindElementBuilder.cs index d39756e2..4c042039 100644 --- a/CSF.Screenplay.Selenium/Builders/FindElementBuilder.cs +++ b/CSF.Screenplay.Selenium/Builders/FindElementBuilder.cs @@ -19,6 +19,7 @@ namespace CSF.Screenplay.Selenium.Builders public class FindElementBuilder : IGetsPerformableWithResult { readonly ITarget target; + readonly IHasSearchContext searchContext; Locator locator; string name; @@ -46,7 +47,9 @@ public FindElementBuilder AndNameIt(string name) IPerformableWithResult IGetsPerformableWithResult.GetPerformable() { - return SingleElementPerformableWithResultAdapter.From(new FindElement(name, locator), target); + return target != null + ? new FindElement(target, name, locator) + : new FindElement(searchContext, name, locator); } /// @@ -59,13 +62,12 @@ public FindElementBuilder(ITarget target) } /// - /// Converts a to a . + /// Initializes a new instance of the class with the specified target. /// - /// The instance to convert. - /// A instance. - public static implicit operator SingleElementPerformableWithResultAdapter(FindElementBuilder builder) + /// The target within which elements will be found. + public FindElementBuilder(IHasSearchContext searchContext) { - return SingleElementPerformableWithResultAdapter.From(new FindElement(builder.name, builder.locator), builder.target); + this.searchContext = searchContext ?? throw new System.ArgumentNullException(nameof(searchContext)); } } } \ No newline at end of file diff --git a/CSF.Screenplay.Selenium/Builders/FindElementsBuilder.cs b/CSF.Screenplay.Selenium/Builders/FindElementsBuilder.cs index 0968bc69..4983441f 100644 --- a/CSF.Screenplay.Selenium/Builders/FindElementsBuilder.cs +++ b/CSF.Screenplay.Selenium/Builders/FindElementsBuilder.cs @@ -19,6 +19,7 @@ namespace CSF.Screenplay.Selenium.Builders public class FindElementsBuilder : IGetsPerformableWithResult { readonly ITarget target; + readonly IHasSearchContext searchContext; Locator locator; string name; @@ -46,7 +47,9 @@ public FindElementsBuilder AndNameThem(string name) IPerformableWithResult IGetsPerformableWithResult.GetPerformable() { - return SingleElementPerformableWithResultAdapter.From(new FindElements(name, locator), target); + return target != null + ? new FindElements(target, name, locator) + : new FindElements(searchContext, name, locator); } /// @@ -59,13 +62,12 @@ public FindElementsBuilder(ITarget target) } /// - /// Converts a to a . + /// Initializes a new instance of the class with the specified target. /// - /// The instance to convert. - /// A instance. - public static implicit operator SingleElementPerformableWithResultAdapter(FindElementsBuilder builder) + /// The target within which elements will be found. + public FindElementsBuilder(IHasSearchContext searchContext) { - return SingleElementPerformableWithResultAdapter.From(new FindElements(builder.name, builder.locator), builder.target); + this.searchContext = searchContext ?? throw new System.ArgumentNullException(nameof(searchContext)); } } } \ No newline at end of file diff --git a/CSF.Screenplay.Selenium/Elements/SeleniumElement.cs b/CSF.Screenplay.Selenium/Elements/SeleniumElement.cs index b0c3c4a7..0703e40f 100644 --- a/CSF.Screenplay.Selenium/Elements/SeleniumElement.cs +++ b/CSF.Screenplay.Selenium/Elements/SeleniumElement.cs @@ -12,18 +12,18 @@ namespace CSF.Screenplay.Selenium.Elements /// a . This optional, but recommended, technique facilitates human-readable reporting. /// /// - public class SeleniumElement : ITarget + public class SeleniumElement : ITarget, IHasWebElement, IHasSearchContext { const string unknownNameFormat = "an HTML {0} element"; /// public string Name { get; } - /// - /// Gets the native Selenium web element. - /// + /// public IWebElement WebElement { get; } + ISearchContext IHasSearchContext.SearchContext => WebElement; + SeleniumElementCollection ITarget.GetElements(IWebDriver driver) => new SeleniumElementCollection(new[] { this }, Name); SeleniumElement ITarget.GetElement(IWebDriver driver) => this; @@ -42,4 +42,26 @@ public SeleniumElement(IWebElement webElement, string name = null) Name = name ?? string.Format(unknownNameFormat, webElement.TagName); } } + + /// + /// An object which exposes a Selenium web element. + /// + public interface IHasWebElement : IHasName + { + /// + /// Gets the native Selenium web element. + /// + IWebElement WebElement { get; } + } + + /// + /// An object which exposes a Selenium search context. + /// + public interface IHasSearchContext : IHasName + { + /// + /// Gets the native Selenium search context. + /// + ISearchContext SearchContext { get; } + } } \ No newline at end of file diff --git a/CSF.Screenplay.Selenium/Elements/ShadowRoot.cs b/CSF.Screenplay.Selenium/Elements/ShadowRoot.cs new file mode 100644 index 00000000..16039c99 --- /dev/null +++ b/CSF.Screenplay.Selenium/Elements/ShadowRoot.cs @@ -0,0 +1,34 @@ +using System; +using OpenQA.Selenium; + +namespace CSF.Screenplay.Selenium.Elements +{ + /// + /// Implementation of which represents an HTML shadow-root node. + /// + /// + /// + /// + public class ShadowRoot : IHasSearchContext + { + readonly ISearchContext shadowRoot; + + /// + public ISearchContext SearchContext => shadowRoot; + + /// + public string Name { get; } + + /// + /// Initializes a new instance of . + /// + /// The wrapped shadow root element + /// The name of this Shadow Root object + /// If is + public ShadowRoot(ISearchContext shadowRoot, string name = null) + { + this.shadowRoot = shadowRoot ?? throw new ArgumentNullException(nameof(shadowRoot)); + Name = name ?? "Shadow root"; + } + } +} \ No newline at end of file diff --git a/CSF.Screenplay.Selenium/Elements/ShadowRootAdapter.cs b/CSF.Screenplay.Selenium/Elements/ShadowRootAdapter.cs deleted file mode 100644 index eef3102e..00000000 --- a/CSF.Screenplay.Selenium/Elements/ShadowRootAdapter.cs +++ /dev/null @@ -1,123 +0,0 @@ - -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 c2d9a9c0..a3fd2467 100644 --- a/CSF.Screenplay.Selenium/PerformableBuilder.elementQuestions.cs +++ b/CSF.Screenplay.Selenium/PerformableBuilder.elementQuestions.cs @@ -21,17 +21,30 @@ public static partial class PerformableBuilder /// /// The target within which to find HTML elements /// A builder, which may be used to configure/get a question that finds elements - public static FindElementsBuilder FindElementsWithin(ITarget target) => new FindElementsBuilder(target); + public static FindElementsBuilder FindElementsWithin(Locator target) => new FindElementsBuilder(target); /// - /// Gets a builder which may be used to create a performable action which finds a collection of elements within the body of the page. + /// Gets a builder which may be used to create a performable action which finds a collection of elements within a specified target. /// /// /// - /// If you want to find elements which are descendents of a specified target, consider using + /// If you only want to find elements within the <body> element of the page, consider using /// instead. /// /// + /// The target within which to find HTML elements + /// A builder, which may be used to configure/get a question that finds elements + public static FindElementsBuilder FindElementsWithin(IHasSearchContext target) => new FindElementsBuilder(target); + + /// + /// Gets a builder which may be used to create a performable action which finds a collection of elements within the body of the page. + /// + /// + /// + /// If you want to find elements which are descendents of a specified target, consider using + /// or instead. + /// + /// /// A builder, which may be used to configure/get a question that finds elements public static FindElementsBuilder FindElementsOnThePage() => new FindElementsBuilder(CssSelector.BodyElement); @@ -46,17 +59,30 @@ public static partial class PerformableBuilder /// /// The target within which to find HTML elements /// A builder, which may be used to configure/get a question that finds an element - public static FindElementBuilder FindAnElementWithin(ITarget target) => new FindElementBuilder(target); + public static FindElementBuilder FindAnElementWithin(Locator target) => new FindElementBuilder(target); /// - /// Gets a builder which may be used to create a performable action which finds a single element within the body of the page. + /// Gets a builder which may be used to create a performable action which finds a single element within a specified target. /// /// /// - /// If you want to find an element which is a descendent of a specified target, consider using + /// If you only want to find an element within the <body> element of the page, consider using /// instead. /// /// + /// The target within which to find HTML elements + /// A builder, which may be used to configure/get a question that finds an element + public static FindElementBuilder FindAnElementWithin(IHasSearchContext target) => new FindElementBuilder(target); + + /// + /// Gets a builder which may be used to create a performable action which finds a single element within the body of the page. + /// + /// + /// + /// If you want to find an element which is a descendent of a specified target, consider using + /// or instead. + /// + /// /// A builder, which may be used to configure/get a question that finds an element public static FindElementBuilder FindAnElementOnThePage() => new FindElementBuilder(CssSelector.BodyElement); @@ -121,11 +147,6 @@ public static FilterElementsBuilder Filter(SeleniumElementCollection elements) /// 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. /// @@ -136,7 +157,7 @@ public static FilterElementsBuilder Filter(SeleniumElementCollection elements) /// /// The Shadow Host element, or a locator which identifies it /// A performable which gets the Shadow Root. - public static IPerformableWithResult GetTheShadowRootNativelyFrom(ITarget shadowHost) + public static IPerformableWithResult GetTheShadowRootNativelyFrom(ITarget shadowHost) => SingleElementPerformableWithResultAdapter.From(new GetShadowRootNatively(), shadowHost); /// @@ -150,11 +171,6 @@ public static IPerformableWithResult GetTheShadowRootNativelyFr /// 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. /// @@ -165,7 +181,7 @@ public static IPerformableWithResult GetTheShadowRootNativelyFr /// /// The Shadow Host element, or a locator which identifies it /// A performable which gets the Shadow Root. - public static IPerformableWithResult GetTheShadowRootWithJavaScriptFrom(ITarget shadowHost) + public static IPerformableWithResult GetTheShadowRootWithJavaScriptFrom(ITarget shadowHost) => SingleElementPerformableWithResultAdapter.From(new GetShadowRootWithJavaScript(), shadowHost); /// @@ -179,11 +195,6 @@ public static IPerformableWithResult GetTheShadowRootWithJavaSc /// 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. /// @@ -195,7 +206,7 @@ public static IPerformableWithResult GetTheShadowRootWithJavaSc /// /// The Shadow Host element, or a locator which identifies it /// A performable which gets the Shadow Root. - public static IPerformableWithResult GetTheShadowRootFrom(ITarget shadowHost) + public static IPerformableWithResult GetTheShadowRootFrom(ITarget shadowHost) => new GetShadowRoot(shadowHost); } } \ No newline at end of file diff --git a/CSF.Screenplay.Selenium/Questions/FindElement.cs b/CSF.Screenplay.Selenium/Questions/FindElement.cs index 4ad27f39..1498d1ac 100644 --- a/CSF.Screenplay.Selenium/Questions/FindElement.cs +++ b/CSF.Screenplay.Selenium/Questions/FindElement.cs @@ -12,9 +12,10 @@ namespace CSF.Screenplay.Selenium.Questions /// /// /// - /// Use this question via either of the builder methods - /// or . The first searches within a specified target, - /// the second searches within the whole page <body>. This question will only ever return a single + /// Use this question via either of the builder methods , + /// or . + /// The first two search within a specified target, + /// the third searches within the whole page <body>. This question will only ever return a single /// , or it will raise an exception if the search does not find any matching elements. /// If multiple elements are found which match the criteria then this question will return only the first. /// If you are expecting to find multiple elements, then consider using the question @@ -51,32 +52,61 @@ namespace CSF.Screenplay.Selenium.Questions /// /// /// - /// - public class FindElement : ISingleElementPerformableWithResult + /// + /// + public class FindElement : IPerformableWithResult, ICanReport { + readonly ITarget target; + readonly IHasSearchContext searchContext; readonly string elementsName; readonly Locator locatorBasedMatcher; + Lazy lazyElement; /// - public ReportFragment GetReportFragment(Actor actor, Lazy element, IFormatsReportFragment formatter) - => formatter.Format("{Actor} finds {ElementsName} from {Element}", actor, GetElementsName(element.Value) ?? "HTML elements", element.Value); + public ReportFragment GetReportFragment(Actor actor, IFormatsReportFragment formatter) + { + lazyElement = lazyElement ?? GetLazyElement(actor); + return formatter.Format("{Actor} finds {ElementsName} from {Element}", actor, GetElementsName(lazyElement.Value) ?? "HTML elements", lazyElement.Value); + } /// - public ValueTask PerformAsAsync(ICanPerform actor, IWebDriver webDriver, Lazy element, CancellationToken cancellationToken = default) + public ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + { + lazyElement = lazyElement ?? GetLazyElement(actor); + var element = lazyElement.Value.SearchContext.FindElement(locatorBasedMatcher ?? CssSelector.AnyElement); + return new ValueTask(new SeleniumElement(element, GetElementsName(lazyElement.Value))); + } + + string GetElementsName(IHasName element) => elementsName ?? $"{locatorBasedMatcher?.Name} within {element.Name}"; + + Lazy GetLazyElement(ICanPerform actor) { - var webElement = element.Value.WebElement.FindElement(locatorBasedMatcher ?? CssSelector.AnyElement); - return new ValueTask(new SeleniumElement(webElement, GetElementsName(element.Value))); + if(target != null) return new Lazy(() => actor.GetLazyElement(target).Value); + return new Lazy(() => searchContext); } - string GetElementsName(SeleniumElement element) => elementsName ?? $"{locatorBasedMatcher?.Name} within {element.Name}"; + /// + /// Initializes a new instance of the class. + /// + /// A target which describes an element + /// An optional short, descriptive, human-readable name to give to the collection of elements which are found. + /// An optional which should be used to filter the elements which are returned. + public FindElement(ITarget target, string elementsName = null, Locator locatorBasedMatcher = null) + { + this.target = target ?? throw new ArgumentNullException(nameof(target)); + this.elementsName = elementsName; + this.locatorBasedMatcher = locatorBasedMatcher; + } /// /// Initializes a new instance of the class. /// + /// An object which provides a search context, within which we can find elements /// An optional short, descriptive, human-readable name to give to the collection of elements which are found. /// An optional which should be used to filter the elements which are returned. - public FindElement(string elementsName = null, Locator locatorBasedMatcher = null) + public FindElement(IHasSearchContext searchContext, string elementsName = null, Locator locatorBasedMatcher = null) { + this.searchContext = searchContext ?? throw new ArgumentNullException(nameof(searchContext)); this.elementsName = elementsName; this.locatorBasedMatcher = locatorBasedMatcher; } diff --git a/CSF.Screenplay.Selenium/Questions/FindElements.cs b/CSF.Screenplay.Selenium/Questions/FindElements.cs index 304840c9..960d75f2 100644 --- a/CSF.Screenplay.Selenium/Questions/FindElements.cs +++ b/CSF.Screenplay.Selenium/Questions/FindElements.cs @@ -12,9 +12,10 @@ namespace CSF.Screenplay.Selenium.Questions /// /// /// - /// Use this question via either of the builder methods - /// or . The first searches within a specified target, - /// the second searches within the whole page <body>. This question returns a collection of elements + /// Use this question via either of the builder methods , + /// or . + /// The first two search within a specified target, the third searches within the whole page <body>. + /// This question returns a collection of elements /// but that collection could be empty if the search does not find any matching elements. /// If you are looking for a single element and a 'nothing found' result should raise an exception then /// consider using the question instead. @@ -50,32 +51,61 @@ namespace CSF.Screenplay.Selenium.Questions /// /// /// - /// - public class FindElements : ISingleElementPerformableWithResult + /// + /// + public class FindElements : IPerformableWithResult, ICanReport { + readonly ITarget target; + readonly IHasSearchContext searchContext; readonly string elementsName; readonly Locator locatorBasedMatcher; + Lazy lazyElement; /// - public ReportFragment GetReportFragment(Actor actor, Lazy element, IFormatsReportFragment formatter) - => formatter.Format("{Actor} finds {ElementsName} from {Element}", actor, GetElementsName(element.Value) ?? "HTML elements", element.Value); + public ReportFragment GetReportFragment(Actor actor, IFormatsReportFragment formatter) + { + lazyElement = lazyElement ?? GetLazyElement(actor); + return formatter.Format("{Actor} finds {ElementsName} from {Element}", actor, GetElementsName(lazyElement.Value) ?? "HTML elements", lazyElement.Value); + } /// - public ValueTask PerformAsAsync(ICanPerform actor, IWebDriver webDriver, Lazy element, CancellationToken cancellationToken = default) + public ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + { + lazyElement = lazyElement ?? GetLazyElement(actor); + var elements = lazyElement.Value.SearchContext.FindElements(locatorBasedMatcher ?? CssSelector.AnyElement); + return new ValueTask(new SeleniumElementCollection(elements, GetElementsName(lazyElement.Value))); + } + + string GetElementsName(IHasName element) => elementsName ?? $"{locatorBasedMatcher?.Name} within {element.Name}"; + + Lazy GetLazyElement(ICanPerform actor) { - var elements = element.Value.WebElement.FindElements(locatorBasedMatcher ?? CssSelector.AnyElement); - return new ValueTask(new SeleniumElementCollection(elements, GetElementsName(element.Value))); + if(target != null) return new Lazy(() => actor.GetLazyElement(target).Value); + return new Lazy(() => searchContext); } - string GetElementsName(SeleniumElement element) => elementsName ?? $"{locatorBasedMatcher?.Name} within {element.Name}"; + /// + /// Initializes a new instance of the class. + /// + /// A target which describes an element + /// An optional short, descriptive, human-readable name to give to the collection of elements which are found. + /// An optional which should be used to filter the elements which are returned. + public FindElements(ITarget target, string elementsName = null, Locator locatorBasedMatcher = null) + { + this.target = target ?? throw new ArgumentNullException(nameof(target)); + this.elementsName = elementsName; + this.locatorBasedMatcher = locatorBasedMatcher; + } /// /// Initializes a new instance of the class. /// + /// An object which provides a search context, within which we can find elements /// An optional short, descriptive, human-readable name to give to the collection of elements which are found. /// An optional which should be used to filter the elements which are returned. - public FindElements(string elementsName = null, Locator locatorBasedMatcher = null) + public FindElements(IHasSearchContext searchContext, string elementsName = null, Locator locatorBasedMatcher = null) { + this.searchContext = searchContext ?? throw new ArgumentNullException(nameof(searchContext)); this.elementsName = elementsName; this.locatorBasedMatcher = locatorBasedMatcher; } diff --git a/CSF.Screenplay.Selenium/Questions/GetShadowRootNatively.cs b/CSF.Screenplay.Selenium/Questions/GetShadowRootNatively.cs index 54239f9f..37d89bc4 100644 --- a/CSF.Screenplay.Selenium/Questions/GetShadowRootNatively.cs +++ b/CSF.Screenplay.Selenium/Questions/GetShadowRootNatively.cs @@ -28,17 +28,17 @@ namespace CSF.Screenplay.Selenium.Questions /// This technique is known to work on Chromium-based browsers from 96 onward and Firefox 113 onward. /// /// - public class GetShadowRootNatively : ISingleElementPerformableWithResult + 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) + 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))); + return new ValueTask(new Elements.ShadowRoot(shadowRoot)); } } } \ No newline at end of file diff --git a/CSF.Screenplay.Selenium/Questions/GetShadowRootWithJavaScript.cs b/CSF.Screenplay.Selenium/Questions/GetShadowRootWithJavaScript.cs index ba9d831b..a23d87d4 100644 --- a/CSF.Screenplay.Selenium/Questions/GetShadowRootWithJavaScript.cs +++ b/CSF.Screenplay.Selenium/Questions/GetShadowRootWithJavaScript.cs @@ -29,17 +29,17 @@ namespace CSF.Screenplay.Selenium.Questions /// This technique is known to work on older Chromium versions (before 96) and Safari. /// /// - public class GetShadowRootWithJavaScript : ISingleElementPerformableWithResult + 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) + 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)); + return new Elements.ShadowRoot(shadowRoot); } } } \ No newline at end of file diff --git a/CSF.Screenplay.Selenium/Tasks/GetShadowRoot.cs b/CSF.Screenplay.Selenium/Tasks/GetShadowRoot.cs index c795aa87..e392ee99 100644 --- a/CSF.Screenplay.Selenium/Tasks/GetShadowRoot.cs +++ b/CSF.Screenplay.Selenium/Tasks/GetShadowRoot.cs @@ -32,7 +32,7 @@ namespace CSF.Screenplay.Selenium.Tasks /// 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 + public class GetShadowRoot : IPerformableWithResult, ICanReport { readonly ITarget element; @@ -41,7 +41,7 @@ public ReportFragment GetReportFragment(Actor actor, IFormatsReportFragment form => formatter.Format("{Actor} gets the Shadow Root from {Element}", actor, element); /// - public ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + public ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) { var browseTheWeb = actor.GetAbility(); diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Actions/SelectByIndexTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Actions/SelectByIndexTests.cs index 4970c944..48bb5306 100644 --- a/Tests/CSF.Screenplay.Selenium.Tests/Actions/SelectByIndexTests.cs +++ b/Tests/CSF.Screenplay.Selenium.Tests/Actions/SelectByIndexTests.cs @@ -9,7 +9,7 @@ namespace CSF.Screenplay.Selenium.Actions; [TestFixture, Parallelizable] public class SelectByIndexTests { - static readonly ITarget + static readonly Locator selectElement = new ElementId("selectElement", "the select element"), displayText = new ElementId("display", "the displayable text"); diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Actions/SelectByTextTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Actions/SelectByTextTests.cs index cbe6405e..0542768c 100644 --- a/Tests/CSF.Screenplay.Selenium.Tests/Actions/SelectByTextTests.cs +++ b/Tests/CSF.Screenplay.Selenium.Tests/Actions/SelectByTextTests.cs @@ -8,7 +8,7 @@ namespace CSF.Screenplay.Selenium.Actions; [TestFixture, Parallelizable] public class SelectByTextTests { - static readonly ITarget + static readonly Locator selectElement = new ElementId("selectElement", "the select element"), displayText = new ElementId("display", "the displayable text"); diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Actions/SelectByValueTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Actions/SelectByValueTests.cs index eb2dfd09..05217d4c 100644 --- a/Tests/CSF.Screenplay.Selenium.Tests/Actions/SelectByValueTests.cs +++ b/Tests/CSF.Screenplay.Selenium.Tests/Actions/SelectByValueTests.cs @@ -9,7 +9,7 @@ namespace CSF.Screenplay.Selenium.Actions; [TestFixture, Parallelizable] public class SelectByValueTests { - static readonly ITarget + static readonly Locator selectElement = new ElementId("selectElement", "the select element"), displayText = new ElementId("display", "the displayable text"); diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Elements/ShadowRootAdapterTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Elements/ShadowRootAdapterTests.cs deleted file mode 100644 index 921a29f2..00000000 --- a/Tests/CSF.Screenplay.Selenium.Tests/Elements/ShadowRootAdapterTests.cs +++ /dev/null @@ -1,130 +0,0 @@ -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/Tasks/GetShadowRootTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Tasks/GetShadowRootTests.cs index d5a216d9..68a7dcf5 100644 --- a/Tests/CSF.Screenplay.Selenium.Tests/Tasks/GetShadowRootTests.cs +++ b/Tests/CSF.Screenplay.Selenium.Tests/Tasks/GetShadowRootTests.cs @@ -1,3 +1,4 @@ +using System.Linq; using CSF.Screenplay.Selenium.Elements; using OpenQA.Selenium; using static CSF.Screenplay.PerformanceStarter; @@ -30,4 +31,23 @@ public async Task GetShadowRootShouldResultInBeingAbleToReadTheShadowDomContent( Assert.That(text, Is.EqualTo("I am an element inside the Shadow DOM")); } + + [Test, Screenplay] + public async Task GetShadowRootShouldResultInBeingAbleToReadACollectionOfShadowDomContent(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(FindElementsWithin(shadowRoot).WhichMatch(content)); + var text = await Then(webster).Should(ReadFromTheCollectionOfElements(shadowContent).Text()); + + using var scope = Assert.EnterMultipleScope(); + Assert.That(text, Has.Count.EqualTo(1), "Count of elements found"); + Assert.That(text.First(), Is.EqualTo("I am an element inside the Shadow DOM"), "Correct text in found element"); + } } diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Tasks/ReadTheListItems.cs b/Tests/CSF.Screenplay.Selenium.Tests/Tasks/ReadTheListItems.cs index 1b757964..b885faf6 100644 --- a/Tests/CSF.Screenplay.Selenium.Tests/Tasks/ReadTheListItems.cs +++ b/Tests/CSF.Screenplay.Selenium.Tests/Tasks/ReadTheListItems.cs @@ -8,7 +8,7 @@ public class ReadTheListItems : IPerformableWithResult>, I { static readonly Locator listItems = new CssSelector("li", "the list items"); - readonly ITarget target; + readonly Locator target; public ReportFragment GetReportFragment(Actor actor, IFormatsReportFragment formatter) => formatter.Format("{Actor} reads the text from list items within {Target}", actor, target); @@ -19,7 +19,7 @@ public async ValueTask> PerformAsAsync(ICanPerform actor, return await actor.PerformAsync(ReadFromTheCollectionOfElements(items).Text(), cancellationToken); } - public ReadTheListItems(ITarget target) + public ReadTheListItems(Locator target) { this.target = target ?? throw new System.ArgumentNullException(nameof(target)); } @@ -27,6 +27,6 @@ public ReadTheListItems(ITarget target) public static class ReadTheListItemsBuilder { - public static IPerformableWithResult> ReadTheListItemsIn(ITarget target) + public static IPerformableWithResult> ReadTheListItemsIn(Locator target) => new ReadTheListItems(target); } \ No newline at end of file