diff --git a/CSF.Screenplay.Docs/docfx.json b/CSF.Screenplay.Docs/docfx.json index 0c6b88cf..cb912c98 100644 --- a/CSF.Screenplay.Docs/docfx.json +++ b/CSF.Screenplay.Docs/docfx.json @@ -48,6 +48,10 @@ "_appTitle": "Screenplay", "_enableSearch": true, "pdf": false - } + }, + "xref": [ + "https://www.selenium.dev/selenium/docs/api/dotnet/xrefmap.yml", + "https://csf-dev.github.io/CSF.Extensions.WebDriver/xrefmap.yml" + ] } } \ No newline at end of file diff --git a/CSF.Screenplay.Docs/docs/Assets.md b/CSF.Screenplay.Docs/docs/Assets.md new file mode 100644 index 00000000..2ecf6f03 --- /dev/null +++ b/CSF.Screenplay.Docs/docs/Assets.md @@ -0,0 +1,7 @@ +--- +uid: AssetsArticle +--- + +# Report assets + +TODO: Write this docco page; #301 \ No newline at end of file diff --git a/CSF.Screenplay.Docs/docs/GettingReports.md b/CSF.Screenplay.Docs/docs/GettingReports.md index d01c425d..8e16a004 100644 --- a/CSF.Screenplay.Docs/docs/GettingReports.md +++ b/CSF.Screenplay.Docs/docs/GettingReports.md @@ -17,7 +17,7 @@ The default filename includes a representation of the current timestamp, so each This path may be altered by specifying a different set of options to the [`AddScreenplay`] service collection extension method. [`ScreenplayOptions.ReportPath`]: xref:CSF.Screenplay.ScreenplayOptions.ReportPath -[`AddScreenplay`]: xref:CSF.Screenplay.ScreenplayServiceCollectionExtensions.AddScreenplay(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.Action{CSF.Screenplay.ScreenplayOptions}) +[`AddScreenplay`]: xref:CSF.Screenplay.ScreenplayServiceCollectionExtensions.AddScreenplay(Microsoft.Extensions.DependencyInjection.IServiceCollection) ## Converting JSON to a human-readable report diff --git a/CSF.Screenplay.Docs/docs/concepts/HowScreenplayAndPerformanceRelate.md b/CSF.Screenplay.Docs/docs/concepts/HowScreenplayAndPerformanceRelate.md index 35efb41c..ebbdbf52 100644 --- a/CSF.Screenplay.Docs/docs/concepts/HowScreenplayAndPerformanceRelate.md +++ b/CSF.Screenplay.Docs/docs/concepts/HowScreenplayAndPerformanceRelate.md @@ -25,7 +25,7 @@ If Screenplay is not being used for testing, then the consuming logic has the fo [Performance]: xref:CSF.Screenplay.IPerformance [`Screenplay`]: xref:CSF.Screenplay.Screenplay -[`Create`]: xref:CSF.Screenplay.Screenplay.Create(System.Action{Microsoft.Extensions.DependencyInjection.IServiceCollection},System.Action{CSF.Screenplay.ScreenplayOptions}) +[`Create`]: xref:CSF.Screenplay.Screenplay.Create(System.Action{Microsoft.Extensions.DependencyInjection.IServiceCollection}) [`BeginScreenplay`]: xref:CSF.Screenplay.Screenplay.BeginScreenplay [`ExecuteAsPerformanceAsync`]: xref:CSF.Screenplay.ScreenplayExtensions.ExecuteAsPerformanceAsync``1(CSF.Screenplay.Screenplay,System.Collections.Generic.IList{CSF.Screenplay.Performances.IdentifierAndName},System.Threading.CancellationToken) [`ExecuteAsPerformance`]: xref:CSF.Screenplay.ScreenplayExtensions.ExecuteAsPerformance(CSF.Screenplay.Screenplay,System.Func{System.IServiceProvider,System.Nullable{System.Boolean}},System.Collections.Generic.IList{CSF.Screenplay.Performances.IdentifierAndName},System.Threading.CancellationToken) diff --git a/CSF.Screenplay.Docs/docs/dependencyInjection/AddingServices.md b/CSF.Screenplay.Docs/docs/dependencyInjection/AddingServices.md index f7a3a025..acc94a5a 100644 --- a/CSF.Screenplay.Docs/docs/dependencyInjection/AddingServices.md +++ b/CSF.Screenplay.Docs/docs/dependencyInjection/AddingServices.md @@ -3,7 +3,7 @@ There are two techniques to add dependency services to Screenplay. You may either integrate Screenplay into an existing container for your application or tests or you may add additional services via the static [`Create`] factory method. -[`Create`]: xref:CSF.Screenplay.Screenplay.Create(System.Action{Microsoft.Extensions.DependencyInjection.IServiceCollection},System.Action{CSF.Screenplay.ScreenplayOptions}) +[`Create`]: xref:CSF.Screenplay.Screenplay.Create(System.Action{Microsoft.Extensions.DependencyInjection.IServiceCollection}) ## Integrating with an existing container @@ -29,7 +29,7 @@ var screenplay = Screenplay.Create(services => { }); ``` -[`Screenplay.Create`]: xref:CSF.Screenplay.Screenplay.Create(System.Action{Microsoft.Extensions.DependencyInjection.IServiceCollection},System.Action{CSF.Screenplay.ScreenplayOptions}) +[`Screenplay.Create`]: xref:CSF.Screenplay.Screenplay.Create(System.Action{Microsoft.Extensions.DependencyInjection.IServiceCollection}) ## A reminder on lifetime scopes diff --git a/CSF.Screenplay.Docs/docs/dependencyInjection/index.md b/CSF.Screenplay.Docs/docs/dependencyInjection/index.md index 81760bae..9d7be416 100644 --- a/CSF.Screenplay.Docs/docs/dependencyInjection/index.md +++ b/CSF.Screenplay.Docs/docs/dependencyInjection/index.md @@ -10,9 +10,9 @@ Alternatively you may create an instance of [`Screenplay`] which uses its own se [dependency injection]: https://en.wikipedia.org/wiki/Dependency_injection [container]: xref:Microsoft.Extensions.DependencyInjection.IServiceCollection -[`AddScreenplay`]: xref:CSF.Screenplay.ScreenplayServiceCollectionExtensions.AddScreenplay(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.Action{CSF.Screenplay.ScreenplayOptions}) +[`AddScreenplay`]: xref:CSF.Screenplay.ScreenplayServiceCollectionExtensions.AddScreenplay(Microsoft.Extensions.DependencyInjection.IServiceCollection) [`Screenplay`]: xref:CSF.Screenplay.Screenplay -[`Screenplay.Create`]: xref:CSF.Screenplay.Screenplay.Create(System.Action{Microsoft.Extensions.DependencyInjection.IServiceCollection},System.Action{CSF.Screenplay.ScreenplayOptions}) +[`Screenplay.Create`]: xref:CSF.Screenplay.Screenplay.Create(System.Action{Microsoft.Extensions.DependencyInjection.IServiceCollection}) ## Scopes diff --git a/CSF.Screenplay.Docs/docs/extendingScreenplay/TestIntegrations.md b/CSF.Screenplay.Docs/docs/extendingScreenplay/TestIntegrations.md index c04f84d0..47d98d44 100644 --- a/CSF.Screenplay.Docs/docs/extendingScreenplay/TestIntegrations.md +++ b/CSF.Screenplay.Docs/docs/extendingScreenplay/TestIntegrations.md @@ -37,8 +37,8 @@ Terminologies differ between testing frameworks; the word [Scenario] is used to [`Screenplay`]: xref:CSF.Screenplay.Screenplay [Scenarios]: ../../glossary/Scenario.md -[`AddScreenplay`]: xref:CSF.Screenplay.ScreenplayServiceCollectionExtensions.AddScreenplay(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.Action{CSF.Screenplay.ScreenplayOptions}) -[`Create`]: xref:CSF.Screenplay.Screenplay.Create(System.Action{Microsoft.Extensions.DependencyInjection.IServiceCollection},System.Action{CSF.Screenplay.ScreenplayOptions}) +[`AddScreenplay`]: xref:CSF.Screenplay.ScreenplayServiceCollectionExtensions.AddScreenplay(Microsoft.Extensions.DependencyInjection.IServiceCollection) +[`Create`]: xref:CSF.Screenplay.Screenplay.Create(System.Action{Microsoft.Extensions.DependencyInjection.IServiceCollection}) [`IGetsScreenplay`]: xref:CSF.Screenplay.IGetsScreenplay [`BeginScreenplay()`]: xref:CSF.Screenplay.Screenplay.BeginScreenplay [`CompleteScreenplay()`]: xref:CSF.Screenplay.Screenplay.CompleteScreenplay diff --git a/CSF.Screenplay.Docs/docs/extensions/index.md b/CSF.Screenplay.Docs/docs/extensions/index.md index 08e4278c..8b9f69b1 100644 --- a/CSF.Screenplay.Docs/docs/extensions/index.md +++ b/CSF.Screenplay.Docs/docs/extensions/index.md @@ -5,4 +5,4 @@ This page lists the officially-supported **[Screenplay Extensions]**. * **[Selenium](selenium/index.md)**: _remote control Web Browsers using Selenium Web Driver_ * **[Web APIs](webApis/index.md)**: _communicate with Web APIs with an HTTP Client_ -[Screenplay Extensions]: ../../glossary/Extension.md \ No newline at end of file +[Screenplay Extensions]: ../../glossary/Extension.md diff --git a/CSF.Screenplay.Docs/docs/extensions/selenium/Actions.md b/CSF.Screenplay.Docs/docs/extensions/selenium/Actions.md new file mode 100644 index 00000000..36fa7a20 --- /dev/null +++ b/CSF.Screenplay.Docs/docs/extensions/selenium/Actions.md @@ -0,0 +1,72 @@ +# Selenium actions + +The Selenium extension for Screenplay provides numerous [Actions], each of which represents one of the smallest possible interactions with a web browser/page. +Broadly, these actions fall into the following categories: + +* Actions which work with HTML elements (targets), such as _"click on a specified element"_ or _"type some keys into an input box"_. These actions will always require [a target] as a parameter. +* Actions which interact directly with the web browser, such as _"take a screenshot"_ or _"clear the cookies for the current domain"_. +* Actions which represent waiting for something, such as _"wait until the progress bar reaches 100%"_ or _"wait until the shopping cart items are visible"_. + +Recall that Screenplay Actions _do something_ without getting any result. +To interrogate a web page (to observe it in some manner), use [one of the Selenium extension's Questions]. +Below is a summary of each of these actions' usage, with a link to their detailed documentation page. + +[Actions]: ../../../glossary/Action.md +[a target]: Targets.md +[one of the Selenium extension's Questions]: Questions.md + +## Actions that work with targets + +| Action | Usage | +| ------ | ----- | +| [`ClearTheContents`] | Remove/clear all text from the specified element, such as an `` | +| [`Click`] | Click on the specified element | +| [`DeselectByIndex`] | Deselect an option from the specified `` element by its displayed text | +| [`DeselectByValue`] | Deselect an option from the specified `` element | +| [`SelectByIndex`] | Select an option from the specified `` element by its displayed text | +| [`SelectByValue`] | Select an option from the specified `` in a cross-browser manner | +| [`OpenUrlRespectingBase`] | Navigates to a URL, using a base URL provided by the [`UseABaseUri`] ability | +| [`SetTheElementValue`] | Sets the `value` of an element with JavaScript, emulating updating it interactively | +| [`TakeAndSaveScreenshot`] | Convenience task to combine the taking & saving of a browser screenshot | + +[`ClickAndWaitForDocumentReady`]: xref:CSF.Screenplay.Selenium.Tasks.ClickAndWaitForDocumentReady +[`EnterTheDate`]: xref:CSF.Screenplay.Selenium.Tasks.EnterTheDate +[`OpenUrlRespectingBase`]: xref:CSF.Screenplay.Selenium.Tasks.OpenUrlRespectingBase +[`UseABaseUri`]: xref:CSF.Screenplay.Selenium.UseABaseUri +[`SetTheElementValue`]: xref:CSF.Screenplay.Selenium.Tasks.SetTheElementValue +[`TakeAndSaveScreenshot`]: xref:CSF.Screenplay.Selenium.Tasks.TakeAndSaveScreenshot diff --git a/CSF.Screenplay.Docs/docs/extensions/selenium/index.md b/CSF.Screenplay.Docs/docs/extensions/selenium/index.md index ef1a52e2..dbde6967 100644 --- a/CSF.Screenplay.Docs/docs/extensions/selenium/index.md +++ b/CSF.Screenplay.Docs/docs/extensions/selenium/index.md @@ -1 +1,54 @@ -TODO: Write this docs page +--- +uid: SeleniumArticle +--- + +# CSF.Screenplay.Selenium Extension + +The Selenium extension allows [Actors] to control web browsers using [Selenium WebDriver] technology. +The control of web browsers in this manner is the origin of [the Screenplay pattern], in which it was an improvement and thorough refactoring of the older [Page Object pattern]. + +[Actors]: xref:CSF.Screenplay.Actor +[Selenium WebDriver]: https://www.selenium.dev/ +[the Screenplay pattern]: https://www.browserstack.com/guide/screenplay-pattern-approach-in-selenium +[Page Object pattern]: https://martinfowler.com/bliki/PageObject.html + +## Core contents of this extension + +Like many extensions, the Selenium extension provides: + +* The [Ability] [`BrowseTheWeb`], which provides access to the WebDriver API +* Several [Actions, which allow an actor to interact with the web page] +* Several [Questions, which allow an actor to read or observe the web page] +* [Tasks, for commonly-performed combinations of actions & questions] +* Models representing the UI, URLs and areas of the page + * The [`NamedUri`] class is used to navigate to specific pages + * Types which provide [references to HTML elements on the page], deriving from [`ITarget`] + +Many of the [Performables] provided by this extension operate in the format _"Actor performs X vaction upon Y element"_. +A core benefit of Screenplay (over Page Object pattern) is the separation of _the functionality_ (the logic of the interaction) and _the UI elements_ (the target of the interaction). +This improves reusability and composability. + +[Ability]: ../../../glossary/Ability.md +[`BrowseTheWeb`]: xref:CSF.Screenplay.Selenium.BrowseTheWeb +[Actions, which allow an actor to interact with the web page]: Actions.md +[Questions, which allow an actor to read or observe the web page]: Questions.md +[Tasks, for commonly-performed combinations of actions & questions]: Tasks.md +[`NamedUri`]: xref:CSF.Screenplay.Selenium.NamedUri +[`ITarget`]: xref:CSF.Screenplay.Selenium.Elements.ITarget +[references to HTML elements on the page]: Targets.md +[Performables]: ../../../glossary/Performable.md + +## Additional contents + +The types listed above are the core of the Selenium plugin, but there's more available. + +* This extension integrates with [the Browser Quirks feature of CSF.Extensions.WebDriver]. That allows it to provide integrated workarounds for the quirks of some browsers. These workarounds are amongst the Tasks noted above. +* This extension provides a [`Color`] struct which provides a cross-browser way of describing sRGB color and testing for equality. +* For occasions on which JavaScript must be sent to the browser, this extension provides [Named Script] classes which offer a type-safe mechanism to refer to those scripts from .NET code. +* If [the default wait timeout] of 5 seconds is unsuitable, the ability [`UseADefaultWaitTime`] may be used to specify a default timeout suitable for your use-case. + +[the Browser Quirks feature of CSF.Extensions.WebDriver]: https://csf-dev.github.io/CSF.Extensions.WebDriver/docs/Quirks.html +[`Color`]: xref:CSF.Screenplay.Selenium.Color +[Named Script]: NamedScripts.md +[the default wait timeout]: xref:CSF.Screenplay.Selenium.Actions.Wait.DefaultTimeout +[`UseADefaultWaitTime`]: xref:CSF.Screenplay.Selenium.UseADefaultWaitTime diff --git a/CSF.Screenplay.Docs/docs/extensions/selenium/toc.yml b/CSF.Screenplay.Docs/docs/extensions/selenium/toc.yml new file mode 100644 index 00000000..7d5dad53 --- /dev/null +++ b/CSF.Screenplay.Docs/docs/extensions/selenium/toc.yml @@ -0,0 +1,15 @@ +- name: The Selenium extension + href: index.md +- name: Actions + href: Actions.md +- name: Questions + href: Questions.md + items: + - name: Queries + href: Queries.md +- name: Tasks + href: Tasks.md +- name: Targets + href: Targets.md +- name: Named scripts + href: NamedScripts.md diff --git a/CSF.Screenplay.Docs/docs/extensions/webApis/toc.yml b/CSF.Screenplay.Docs/docs/extensions/webApis/toc.yml index 08d0ea3a..7847a9de 100644 --- a/CSF.Screenplay.Docs/docs/extensions/webApis/toc.yml +++ b/CSF.Screenplay.Docs/docs/extensions/webApis/toc.yml @@ -1,4 +1,4 @@ -- name: Web APIs +- name: The Web APIs extension href: index.md - name: Endpoints href: Endpoints.md diff --git a/CSF.Screenplay.Docs/docs/toc.yml b/CSF.Screenplay.Docs/docs/toc.yml index ecc58f7a..db0b88ef 100644 --- a/CSF.Screenplay.Docs/docs/toc.yml +++ b/CSF.Screenplay.Docs/docs/toc.yml @@ -43,6 +43,9 @@ href: builderPattern/index.md - name: Reports href: GettingReports.md + items: + - name: Assets + href: Assets.md - name: Performables href: performables/toc.yml - name: Best practices diff --git a/CSF.Screenplay.Docs/glossary/Asset.md b/CSF.Screenplay.Docs/glossary/Asset.md index e312d020..6a30e4fe 100644 --- a/CSF.Screenplay.Docs/glossary/Asset.md +++ b/CSF.Screenplay.Docs/glossary/Asset.md @@ -19,6 +19,8 @@ Indeed, _asset files must not be created or saved_ if [reporting is disabled]. [Screenplay's]: xref:CSF.Screenplay.Screenplay [reporting is disabled]: TODO -## How to create assets +## Learn more about Assets -TODO: Write this docco +Further information about working with Assets [is available in the documentation]. + +[is available in the documentation]: xref:AssetsArticle diff --git a/CSF.Screenplay.Selenium/Actions/ClearCookies.cs b/CSF.Screenplay.Selenium/Actions/ClearCookies.cs index d3f42c60..46b078a6 100644 --- a/CSF.Screenplay.Selenium/Actions/ClearCookies.cs +++ b/CSF.Screenplay.Selenium/Actions/ClearCookies.cs @@ -4,8 +4,30 @@ namespace CSF.Screenplay.Selenium.Actions { /// - /// Represents an action to clear all browser cookies for the current site. + /// An action which clears all browser cookies for the current site/domain. /// + /// + /// + /// Use this action via the builder method . + /// Performing this action, as an actor which has the ability, instructs the web browser + /// to clear all cookies for the current domain. This is equivalent to a human user entering the browser's + /// settings and doing the same. + /// + /// + /// + /// + /// using static CSF.Screenplay.Selenium.PerformableBuilder; + /// + /// // Within the logic of a custom task, deriving from IPerformable + /// public async ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + /// { + /// // ... other performance logic + /// await actor.PerformAsync(ClearAllDomainCookies(), cancellationToken); + /// // ... other performance logic + /// } + /// + /// + /// public class ClearCookies : IPerformable, ICanReport { /// diff --git a/CSF.Screenplay.Selenium/Actions/ClearLocalStorage.cs b/CSF.Screenplay.Selenium/Actions/ClearLocalStorage.cs index 6f6933f7..342e0b59 100644 --- a/CSF.Screenplay.Selenium/Actions/ClearLocalStorage.cs +++ b/CSF.Screenplay.Selenium/Actions/ClearLocalStorage.cs @@ -1,18 +1,43 @@ -using System; using System.Threading; using System.Threading.Tasks; -using CSF.Screenplay.Performables; using static CSF.Screenplay.Selenium.PerformableBuilder; namespace CSF.Screenplay.Selenium.Actions { /// - /// Represents an action to clear the browser's local storage for the current site. + /// An action which clears the browser's local storage for the current site. /// + /// + /// + /// Use this action via the builder method . + /// Performing this action, as an actor which has the ability, instructs the web browser + /// to clear the local storage + /// store for the current domain. + /// + /// + /// Note that this operation is not supported first-class by Selenium WebDriver (it is + /// not part of the WebDriver specification). Clearing local storage is implemented by sending the following + /// JavaScript to the web browser via : localStorage.clear(). + /// Bear this in mind when using this action, as it is not neccesarily identical to a user clearing the storage + /// interactively via the browser's settings. + /// + /// + /// + /// + /// using static CSF.Screenplay.Selenium.PerformableBuilder; + /// + /// // Within the logic of a custom task, deriving from IPerformable + /// public async ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + /// { + /// // ... other performance logic + /// await actor.PerformAsync(ClearLocalStorage(), cancellationToken); + /// // ... other performance logic + /// } + /// + /// + /// public class ClearLocalStorage : IPerformable, ICanReport { - readonly bool throwIfUnsupported; - /// public ReportFragment GetReportFragment(Actor actor, IFormatsReportFragment formatter) => formatter.Format("{Actor} clears their browser local storage for the current site", actor.Name); @@ -20,24 +45,7 @@ public ReportFragment GetReportFragment(Actor actor, IFormatsReportFragment form /// public async ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) { - try - { - await actor.PerformAsync(ExecuteAScript(Scripts.ClearLocalStorage)); - } - catch(PerformableException e) - { - if(e.InnerException is NotSupportedException && !throwIfUnsupported) return; - throw; - } - } - - /// - /// Initializes a new instance of the class. - /// - /// If set to true, an exception will be thrown if the WebDriver does not support the execution of JavaScript. - public ClearLocalStorage(bool throwIfUnsupported = true) - { - this.throwIfUnsupported = throwIfUnsupported; + await actor.PerformAsync(ExecuteAScript(Scripts.ClearLocalStorage)); } } } \ No newline at end of file diff --git a/CSF.Screenplay.Selenium/Actions/ClearTheContents.cs b/CSF.Screenplay.Selenium/Actions/ClearTheContents.cs index c4404dce..c87d7be4 100644 --- a/CSF.Screenplay.Selenium/Actions/ClearTheContents.cs +++ b/CSF.Screenplay.Selenium/Actions/ClearTheContents.cs @@ -7,15 +7,41 @@ namespace CSF.Screenplay.Selenium.Actions { /// - /// Represents an action to clear the contents of a specified HTML element. + /// A partial Screenplay Action which clears the contents of an HTML element. /// /// /// + /// Use this action via the builder method . + /// Performing this action, as an actor which has the ability, instructs the web browser + /// to clear the contents of the HTML element indicated by . + /// + /// /// The element to be cleared is typically an HTML <input> or <textarea> element. /// It could be any element which supports Webdriver's Clear() method. /// For input or textarea elements, this action is equivalent to the user selecting all text and pressing the delete key. /// + /// /// + /// + /// + /// This example clears the contents of the element with ID username. + /// + /// + /// using CSF.Screenplay.Selenium.Elements; + /// using static CSF.Screenplay.Selenium.PerformableBuilder; + /// + /// readonly ITarget username = new ElementId("username", "the username field"); + /// + /// // Within the logic of a custom task, deriving from IPerformable + /// public async ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + /// { + /// // ... other performance logic + /// await actor.PerformAsync(ClearTheContentsOf(username), cancellationToken); + /// // ... other performance logic + /// } + /// + /// + /// public class ClearTheContents : ISingleElementPerformable { /// diff --git a/CSF.Screenplay.Selenium/Actions/Click.cs b/CSF.Screenplay.Selenium/Actions/Click.cs index 9e8e17a4..1d982f6c 100644 --- a/CSF.Screenplay.Selenium/Actions/Click.cs +++ b/CSF.Screenplay.Selenium/Actions/Click.cs @@ -7,8 +7,36 @@ namespace CSF.Screenplay.Selenium.Actions { /// - /// A which represents an actor clicking upon a single HTML element. + /// A partial Screenplay Action which clicks on an HTML element. /// + /// + /// + /// Use this action via the builder method . + /// Performing this action, as an actor which has the ability, instructs the web browser + /// to click on the HTML element indicated by . + /// + /// + /// + /// + /// + /// This example clicks on the element with ID buy_now. + /// + /// + /// using CSF.Screenplay.Selenium.Elements; + /// using static CSF.Screenplay.Selenium.PerformableBuilder; + /// + /// readonly ITarget buyNow = new ElementId("buy_now", "the 'buy now' button"); + /// + /// // Within the logic of a custom task, deriving from IPerformable + /// public async ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + /// { + /// // ... other performance logic + /// await actor.PerformAsync(ClickOn(buyNow), cancellationToken); + /// // ... other performance logic + /// } + /// + /// + /// public class Click : ISingleElementPerformable { /// diff --git a/CSF.Screenplay.Selenium/Actions/DeleteTheCookie.cs b/CSF.Screenplay.Selenium/Actions/DeleteTheCookie.cs index 64b26c3f..b634d1df 100644 --- a/CSF.Screenplay.Selenium/Actions/DeleteTheCookie.cs +++ b/CSF.Screenplay.Selenium/Actions/DeleteTheCookie.cs @@ -4,8 +4,33 @@ namespace CSF.Screenplay.Selenium.Actions { /// - /// Represents an action to delete a specific browser cookie by name. + /// An action which deletes a specific browser cookie by name. /// + /// + /// + /// Use this action via the builder method . + /// Performing this action, as an actor which has the ability, instructs the web browser + /// to delete the cookie (for the current domain) which has the specified name. + /// This is equivalent to a human user entering the browser's developer tools and doing the same. + /// + /// + /// + /// + /// In this example, the action will delete a cookie named "MyCookieName". + /// + /// + /// using static CSF.Screenplay.Selenium.PerformableBuilder; + /// + /// // Within the logic of a custom task, deriving from IPerformable + /// public async ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + /// { + /// // ... other performance logic + /// await actor.PerformAsync(DeleteTheCookieNamed("MyCookieName"), cancellationToken); + /// // ... other performance logic + /// } + /// + /// + /// public class DeleteTheCookie : IPerformable, ICanReport { readonly string cookieName; diff --git a/CSF.Screenplay.Selenium/Actions/DeselectAll.cs b/CSF.Screenplay.Selenium/Actions/DeselectAll.cs index 897c228f..05848a3e 100644 --- a/CSF.Screenplay.Selenium/Actions/DeselectAll.cs +++ b/CSF.Screenplay.Selenium/Actions/DeselectAll.cs @@ -7,8 +7,42 @@ namespace CSF.Screenplay.Selenium.Actions { /// - /// A which represents an actor deselecting all options from a <select> element. + /// A partial Screenplay Action which de-selects every option from an HTML <select> element. /// + /// + /// + /// Use this action via the builder method . + /// Performing this action, as an actor which has the ability, instructs the web browser + /// to deselect every option from the HTML element indicated by . + /// + /// + /// Use of this action makes sense only for <select> elements which have the multiple attribute. + /// Select elements which do not permit multiple-selection must have precisely one option always selected, + /// which means that this action will not be effective. + /// + /// + /// + /// + /// + /// This example deselects every option from the element with class optional_extras, which is a descendent of an + /// element with the ID confirm_purchase. + /// + /// + /// using CSF.Screenplay.Selenium.Elements; + /// using static CSF.Screenplay.Selenium.PerformableBuilder; + /// + /// readonly ITarget optionalExtras = new CssSelector("#confirm_purchase .optional_extras", "the list of optional extras"); + /// + /// // Within the logic of a custom task, deriving from IPerformable + /// public async ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + /// { + /// // ... other performance logic + /// await actor.PerformAsync(DeselectEverythingFrom(optionalExtras), cancellationToken); + /// // ... other performance logic + /// } + /// + /// + /// public class DeselectAll : ISingleElementPerformable { /// diff --git a/CSF.Screenplay.Selenium/Actions/DeselectByIndex.cs b/CSF.Screenplay.Selenium/Actions/DeselectByIndex.cs index c0cf326d..98d8deb5 100644 --- a/CSF.Screenplay.Selenium/Actions/DeselectByIndex.cs +++ b/CSF.Screenplay.Selenium/Actions/DeselectByIndex.cs @@ -7,9 +7,43 @@ namespace CSF.Screenplay.Selenium.Actions { /// - /// A which represents an actor deselecting an option from a <select> element, - /// where the option is specified by its zero-based index within the available options. + /// A partial Screenplay Action which de-selects an option from an HTML <select> element based + /// upon that option's zero-based index (position). /// + /// + /// + /// Use this action via the builder method . + /// Performing this action, as an actor which has the ability, instructs the web browser + /// to deselect an option from the HTML element indicated by . + /// + /// + /// Use of this action makes sense only for <select> elements which have the multiple attribute. + /// Select elements which do not permit multiple-selection must have precisely one option always selected, + /// which means that this action will not be effective. + /// + /// + /// + /// + /// + /// This example deselects the second option (index 1) from the element with class optional_extras, which is a + /// descendent of an element with the ID confirm_purchase. + /// + /// + /// using CSF.Screenplay.Selenium.Elements; + /// using static CSF.Screenplay.Selenium.PerformableBuilder; + /// + /// readonly ITarget optionalExtras = new CssSelector("#confirm_purchase .optional_extras", "the list of optional extras"); + /// + /// // Within the logic of a custom task, deriving from IPerformable + /// public async ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + /// { + /// // ... other performance logic + /// await actor.PerformAsync(DeselectTheOption(1).From(optionalExtras), cancellationToken); + /// // ... other performance logic + /// } + /// + /// + /// public class DeselectByIndex : ISingleElementPerformable { readonly int index; diff --git a/CSF.Screenplay.Selenium/Actions/DeselectByText.cs b/CSF.Screenplay.Selenium/Actions/DeselectByText.cs index 20fc3cef..1120f875 100644 --- a/CSF.Screenplay.Selenium/Actions/DeselectByText.cs +++ b/CSF.Screenplay.Selenium/Actions/DeselectByText.cs @@ -7,9 +7,43 @@ namespace CSF.Screenplay.Selenium.Actions { /// - /// A which represents an actor deselecting an option from a <select> element, - /// where the option is specified by its human-readable display text. + /// A partial Screenplay Action which de-selects an option from an HTML <select> element based + /// upon that option's displayed text. /// + /// + /// + /// Use this action via the builder method . + /// Performing this action, as an actor which has the ability, instructs the web browser + /// to deselect an option from the HTML element indicated by . + /// + /// + /// Use of this action makes sense only for <select> elements which have the multiple attribute. + /// Select elements which do not permit multiple-selection must have precisely one option always selected, + /// which means that this action will not be effective. + /// + /// + /// + /// + /// + /// This example deselects the option "French Fries" from the element with class optional_extras, which is a + /// descendent of an element with the ID confirm_purchase. + /// + /// + /// using CSF.Screenplay.Selenium.Elements; + /// using static CSF.Screenplay.Selenium.PerformableBuilder; + /// + /// readonly ITarget optionalExtras = new CssSelector("#confirm_purchase .optional_extras", "the list of optional extras"); + /// + /// // Within the logic of a custom task, deriving from IPerformable + /// public async ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + /// { + /// // ... other performance logic + /// await actor.PerformAsync(DeselectTheOption("French Fries").From(optionalExtras), cancellationToken); + /// // ... other performance logic + /// } + /// + /// + /// public class DeselectByText : ISingleElementPerformable { readonly string text; diff --git a/CSF.Screenplay.Selenium/Actions/DeselectByValue.cs b/CSF.Screenplay.Selenium/Actions/DeselectByValue.cs index 496bd25b..85b70252 100644 --- a/CSF.Screenplay.Selenium/Actions/DeselectByValue.cs +++ b/CSF.Screenplay.Selenium/Actions/DeselectByValue.cs @@ -7,9 +7,43 @@ namespace CSF.Screenplay.Selenium.Actions { /// - /// A which represents an actor deselecting an option from a <select> element, - /// where the option is specified by its underlying value. + /// A partial Screenplay Action which de-selects an option from an HTML <select> element based + /// upon that option's value; the value attribute. /// + /// + /// + /// Use this action via the builder method . + /// Performing this action, as an actor which has the ability, instructs the web browser + /// to deselect an option from the HTML element indicated by . + /// + /// + /// Use of this action makes sense only for <select> elements which have the multiple attribute. + /// Select elements which do not permit multiple-selection must have precisely one option always selected, + /// which means that this action will not be effective. + /// + /// + /// + /// + /// + /// This example deselects the option with value "f_fries" from the element with class optional_extras, + /// which is a descendent of an element with the ID confirm_purchase. + /// + /// + /// using CSF.Screenplay.Selenium.Elements; + /// using static CSF.Screenplay.Selenium.PerformableBuilder; + /// + /// readonly ITarget optionalExtras = new CssSelector("#confirm_purchase .optional_extras", "the list of optional extras"); + /// + /// // Within the logic of a custom task, deriving from IPerformable + /// public async ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + /// { + /// // ... other performance logic + /// await actor.PerformAsync(DeselectTheOptionWithValue("f_fries").From(optionalExtras), cancellationToken); + /// // ... other performance logic + /// } + /// + /// + /// public class DeselectByValue : ISingleElementPerformable { readonly string value; diff --git a/CSF.Screenplay.Selenium/Actions/ExecuteJavaScript.cs b/CSF.Screenplay.Selenium/Actions/ExecuteJavaScript.cs index 6ea3cff3..3efeac4d 100644 --- a/CSF.Screenplay.Selenium/Actions/ExecuteJavaScript.cs +++ b/CSF.Screenplay.Selenium/Actions/ExecuteJavaScript.cs @@ -1,18 +1,62 @@ using System.Threading; using System.Threading.Tasks; -using CSF.Screenplay.Selenium.Questions; namespace CSF.Screenplay.Selenium.Actions { /// - /// An action which executes some arbitrary JavaScript in the web browser. + /// An action which sends a JavaScript string to a web browser and executes it. + /// This action ignores the result (if any) from that script. /// /// /// - /// This performable does not expect or return any result from the script. - /// If the script returns a result then use instead. + /// The best way to use this action is via the builder method + /// or one of its same-named overloads. + /// The class, and its counterparts with generic type parameters, provide a manner + /// in which scripts to be executed by this action may be stored within the application or test logic, providing + /// type safety for their parameters. + /// + /// + /// Performing this action, as an actor which has the ability, sends the body of the + /// script and its parameter values to the web browser and executes it. This is roughly equivalent to a user + /// opening the developer tools Console, typing the script and its parameters there, and pressing enter. + /// This action does not return any result from the script, so it is useful only when the script to execute + /// does not return a result, or when the intent is to ignore the result. If the result is important then use + /// instead. + /// + /// + /// Within the script body, any parameters are accessible via the arguments object. That object exposes the + /// parameter values by their zero-based index (position) in the parameters list. So, a script which takes three + /// parameters will expose these to the script as arguments[0], arguments[1] and + /// arguments[2], respectively for the first, second and third parameter values. /// /// + /// + /// + /// In this example, the script will write "I can count to 5" into the developer console. + /// + /// + /// using static CSF.Screenplay.Selenium.PerformableBuilder; + /// + /// var iCanCount = new NamedScript<int>("console.log('I can count to ' + arguments[0])", "my counting script"); + /// + /// // Within the logic of a custom task, deriving from IPerformable + /// public async ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + /// { + /// // ... other performance logic + /// await actor.PerformAsync(ExecuteAScript(iCanCount, 5), cancellationToken); + /// // ... other performance logic + /// } + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// public class ExecuteJavaScript : IPerformable, ICanReport { readonly string script; diff --git a/CSF.Screenplay.Selenium/Actions/ISingleElementPerformable.cs b/CSF.Screenplay.Selenium/Actions/ISingleElementPerformable.cs index dfd64ac3..9c0002ca 100644 --- a/CSF.Screenplay.Selenium/Actions/ISingleElementPerformable.cs +++ b/CSF.Screenplay.Selenium/Actions/ISingleElementPerformable.cs @@ -7,7 +7,7 @@ namespace CSF.Screenplay.Selenium.Actions { /// - /// Similar to but provides access to a Selenium WebDriver and a Selenium element. + /// An object which is similar to but provides access to a Selenium WebDriver and a Selenium element. /// /// /// diff --git a/CSF.Screenplay.Selenium/Actions/OpenUrl.cs b/CSF.Screenplay.Selenium/Actions/OpenUrl.cs index 58254350..ee74b16d 100644 --- a/CSF.Screenplay.Selenium/Actions/OpenUrl.cs +++ b/CSF.Screenplay.Selenium/Actions/OpenUrl.cs @@ -5,8 +5,43 @@ namespace CSF.Screenplay.Selenium.Actions { /// - /// Screenplay action which opens (navigates) directly to a specified URL using the actor's WebDriver. + /// An action which navigates to a specified URL, as if the user had entered it into the browser address bar. /// + /// + /// + /// Use this action via the builder method . + /// The OpenTheUrl builder method does not have a one-to-one relationship with this action, though. + /// The builder method actually returns a named + /// . The purpose of that task is to prepend a base URL to + /// URLs which are relative. This action is capable only of navigating to absolute URLs, and it will + /// raise an exception if the URL is not absolute. + /// + /// + /// Performing this action, as an actor which has the ability, instructs the web browser + /// to navigate to the specified URL. This is performed in the same way as if the user had entered + /// the URL into their web browser address bar and pressed enter. + /// + /// + /// + /// + /// In this example, the action will navigate the web browser to https://example.com/myPage. + /// + /// + /// using static CSF.Screenplay.Selenium.PerformableBuilder; + /// + /// var examplePage = new NamedUri("https://example.com/myPage", "the example web page"); + /// + /// // Within the logic of a custom task, deriving from IPerformable + /// public async ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + /// { + /// // ... other performance logic + /// await actor.PerformAsync(OpenTheUrl(examplePage), cancellationToken); + /// // ... other performance logic + /// } + /// + /// + /// + /// public class OpenUrl : IPerformable, ICanReport { readonly NamedUri uri; diff --git a/CSF.Screenplay.Selenium/Actions/SaveScreenshot.cs b/CSF.Screenplay.Selenium/Actions/SaveScreenshot.cs index 5b9eb301..83642e20 100644 --- a/CSF.Screenplay.Selenium/Actions/SaveScreenshot.cs +++ b/CSF.Screenplay.Selenium/Actions/SaveScreenshot.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using System.Threading; using System.Threading.Tasks; using CSF.Screenplay.Abilities; @@ -8,13 +7,57 @@ namespace CSF.Screenplay.Selenium.Actions { /// - /// Screenplay action which saves a Selenium Screenshot instance as an asset file. + /// An action which saves a Selenium Screenshot object to disk as an asset file. /// /// /// - /// Use of this performable requires the actor to have the ability . + /// Use this action via the builder method . + /// Instead of this action, consider using the task , + /// via either of the builder methods + /// or . Use of this action (standalone) + /// is only required if you wish to examine or interact with the Screenshot object. If all you want to + /// achieve is to take the screenshot and save it, the task is likely to be more convenient. + /// + /// + /// Performing this action with a specified Screenshot object, such as one retrieved via the + /// question, saves that Screenshot object to disk as + /// + /// + /// + /// To perform this action, the actor must have the ability . + /// If the actor does not have the ability then this action will throw an exception. /// /// + /// + /// + /// In this example, the action will save the screenshot with the name Shopping cart items, using the JPEG + /// file format. + /// Note that because of the internal workings of the mechanism, the precise file + /// name by which the asset is saved is indeterminate. The precise details of file naming are determined by + /// . + /// + /// + /// using OpenQA.Selenium; + /// using static CSF.Screenplay.Selenium.PerformableBuilder; + /// + /// // Retrieved via (for example) the TakeScreenshot question + /// readonly Screenshot screenshot; + /// + /// // Within the logic of a custom task, deriving from IPerformable + /// public async ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + /// { + /// // ... other performance logic + /// await actor.PerformAsync(SaveTheScreenshot(screenshot) + /// .WithTheName("Shopping cart items") + /// .WithTheFormat(ScreenshotImageFormat.Jpeg), + /// cancellationToken); + /// // ... other performance logic + /// } + /// + /// + /// + /// + /// public class SaveScreenshot : IPerformable, ICanReport { readonly Screenshot screenshot; diff --git a/CSF.Screenplay.Selenium/Actions/SelectByIndex.cs b/CSF.Screenplay.Selenium/Actions/SelectByIndex.cs index 8fab33a0..c3225716 100644 --- a/CSF.Screenplay.Selenium/Actions/SelectByIndex.cs +++ b/CSF.Screenplay.Selenium/Actions/SelectByIndex.cs @@ -7,9 +7,52 @@ namespace CSF.Screenplay.Selenium.Actions { /// - /// A which represents an actor selecting an option from a <select> element, - /// where the option is specified by its zero-based index within the available options. + /// A partial Screenplay Action which selects an option from an HTML <select> element based + /// upon that option's zero-based index (position). /// + /// + /// + /// Use this action via the builder method . + /// Performing this action, as an actor which has the ability, instructs the web browser + /// to select an option from the HTML element indicated by . + /// + /// + /// This action has differing behaviour depending whether the <select> element has the + /// multiple attribute or not. + /// + /// + /// + /// For an element which permits multiple-selection, this action adds the chosen option to the option(s) which + /// are already selected. + /// + /// + /// For an element which does not permit multiple-selection, this action replaces the option which is currently + /// selected with the chosen option. + /// + /// + /// + /// + /// + /// + /// This example selects the second option (index 1) from the element with class optional_extras, + /// which is a descendent of an element with the ID confirm_purchase. + /// + /// + /// using CSF.Screenplay.Selenium.Elements; + /// using static CSF.Screenplay.Selenium.PerformableBuilder; + /// + /// readonly ITarget optionalExtras = new CssSelector("#confirm_purchase .optional_extras", "the list of optional extras"); + /// + /// // Within the logic of a custom task, deriving from IPerformable + /// public async ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + /// { + /// // ... other performance logic + /// await actor.PerformAsync(SelectTheOption(1).From(optionalExtras), cancellationToken); + /// // ... other performance logic + /// } + /// + /// + /// public class SelectByIndex : ISingleElementPerformable { readonly int index; diff --git a/CSF.Screenplay.Selenium/Actions/SelectByText.cs b/CSF.Screenplay.Selenium/Actions/SelectByText.cs index 2f83cac9..e371fe40 100644 --- a/CSF.Screenplay.Selenium/Actions/SelectByText.cs +++ b/CSF.Screenplay.Selenium/Actions/SelectByText.cs @@ -7,9 +7,52 @@ namespace CSF.Screenplay.Selenium.Actions { /// - /// A which represents an actor selecting an option from a <select> element, - /// where the option is specified by its human-readable display text. + /// A partial Screenplay Action which selects an option from an HTML <select> element based + /// upon that option's displayed text. /// + /// + /// + /// Use this action via the builder method . + /// Performing this action, as an actor which has the ability, instructs the web browser + /// to select an option from the HTML element indicated by . + /// + /// + /// This action has differing behaviour depending whether the <select> element has the + /// multiple attribute or not. + /// + /// + /// + /// For an element which permits multiple-selection, this action adds the chosen option to the option(s) which + /// are already selected. + /// + /// + /// For an element which does not permit multiple-selection, this action replaces the option which is currently + /// selected with the chosen option. + /// + /// + /// + /// + /// + /// + /// This example selects the option "French Fries" from the element with class optional_extras, + /// which is a descendent of an element with the ID confirm_purchase. + /// + /// + /// using CSF.Screenplay.Selenium.Elements; + /// using static CSF.Screenplay.Selenium.PerformableBuilder; + /// + /// readonly ITarget optionalExtras = new CssSelector("#confirm_purchase .optional_extras", "the list of optional extras"); + /// + /// // Within the logic of a custom task, deriving from IPerformable + /// public async ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + /// { + /// // ... other performance logic + /// await actor.PerformAsync(SelectTheOption("French Fries").From(optionalExtras), cancellationToken); + /// // ... other performance logic + /// } + /// + /// + /// public class SelectByText : ISingleElementPerformable { readonly string text; diff --git a/CSF.Screenplay.Selenium/Actions/SelectByValue.cs b/CSF.Screenplay.Selenium/Actions/SelectByValue.cs index 650798e1..4b7fa0f3 100644 --- a/CSF.Screenplay.Selenium/Actions/SelectByValue.cs +++ b/CSF.Screenplay.Selenium/Actions/SelectByValue.cs @@ -7,9 +7,52 @@ namespace CSF.Screenplay.Selenium.Actions { /// - /// A which represents an actor selecting an option from a <select> element, - /// where the option is specified by its underlying value. + /// A partial Screenplay Action which selects an option from an HTML <select> element based + /// upon that option's value; the value attribute. /// + /// + /// + /// Use this action via the builder method . + /// Performing this action, as an actor which has the ability, instructs the web browser + /// to select an option from the HTML element indicated by . + /// + /// + /// This action has differing behaviour depending whether the <select> element has the + /// multiple attribute or not. + /// + /// + /// + /// For an element which permits multiple-selection, this action adds the chosen option to the option(s) which + /// are already selected. + /// + /// + /// For an element which does not permit multiple-selection, this action replaces the option which is currently + /// selected with the chosen option. + /// + /// + /// + /// + /// + /// + /// This example selects the option with value "f_fries" from the element with class optional_extras, + /// which is a descendent of an element with the ID confirm_purchase. + /// + /// + /// using CSF.Screenplay.Selenium.Elements; + /// using static CSF.Screenplay.Selenium.PerformableBuilder; + /// + /// readonly ITarget optionalExtras = new CssSelector("#confirm_purchase .optional_extras", "the list of optional extras"); + /// + /// // Within the logic of a custom task, deriving from IPerformable + /// public async ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + /// { + /// // ... other performance logic + /// await actor.PerformAsync(SelectTheOptionWithValue("f_fries").From(optionalExtras), cancellationToken); + /// // ... other performance logic + /// } + /// + /// + /// public class SelectByValue : ISingleElementPerformable { readonly string value; diff --git a/CSF.Screenplay.Selenium/Actions/SendKeys.cs b/CSF.Screenplay.Selenium/Actions/SendKeys.cs index 63465be6..b41c2dd6 100644 --- a/CSF.Screenplay.Selenium/Actions/SendKeys.cs +++ b/CSF.Screenplay.Selenium/Actions/SendKeys.cs @@ -7,14 +7,46 @@ namespace CSF.Screenplay.Selenium.Actions { /// - /// A which represents an actor pressing some keys whilst focussed upon a single HTML element. + /// A partial Screenplay Action which sends keys (enters text) to an HTML element. /// /// /// - /// This performable may be used to enter text into an element which supports it, such as an <input> or <textarea> element. - /// It may also be used, with Selenium's Keys class, to send special keys such as the Enter key, directional Arrow keys and so on. + /// Use this action via the builder method . + /// Performing this action, as an actor which has the ability, instructs the web browser + /// to press the specified keys, essentially entering text into the HTML element indicated by + /// . /// + /// + /// Typically, this action is performed upon an element which supports user-input such as an + /// <input> or <textarea> element. However, it is not limited to these elements; any element + /// may receive key-presses. + /// + /// + /// You may send non-printable or special keys such as Enter or directional arrow key-presses by + /// using Selenium's Keys class. + /// + /// /// + /// + /// + /// This example types "Jane Doe" into the element with ID name. + /// + /// + /// using CSF.Screenplay.Selenium.Elements; + /// using static CSF.Screenplay.Selenium.PerformableBuilder; + /// + /// readonly ITarget name = new ElementId("name", "the name field"); + /// + /// // Within the logic of a custom task, deriving from IPerformable + /// public async ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + /// { + /// // ... other performance logic + /// await actor.PerformAsync(EnterTheText("Jane Doe").Into(name), cancellationToken); + /// // ... other performance logic + /// } + /// + /// + /// public class SendKeys : ISingleElementPerformable { readonly string text; diff --git a/CSF.Screenplay.Selenium/Actions/SetTheElementValue.cs b/CSF.Screenplay.Selenium/Actions/SetTheElementValue.cs deleted file mode 100644 index 9169abbe..00000000 --- a/CSF.Screenplay.Selenium/Actions/SetTheElementValue.cs +++ /dev/null @@ -1,45 +0,0 @@ -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.Actions -{ - /// - /// An action that sets the value of a web element using JavaScript. - /// - public class SetTheElementValue : ISingleElementPerformable - { - readonly object value; - readonly bool simulateInteractiveSet; - - /// - public ReportFragment GetReportFragment(Actor actor, Lazy element, IFormatsReportFragment formatter) - { - return simulateInteractiveSet - ? formatter.Format("{Actor} uses JavaScript to simulate setting the value of {Element} to {Value} interactively", actor, element.Value, value) - : formatter.Format("{Actor} uses JavaScript to set the value of {Element} to {Value}", actor, element.Value, value); - } - - /// - public ValueTask PerformAsAsync(ICanPerform actor, IWebDriver webDriver, Lazy element, CancellationToken cancellationToken = default) - { - return simulateInteractiveSet - ? actor.PerformAsync(ExecuteAScript(Scripts.SetElementValueSimulatedInteractively, element.Value.WebElement, value), cancellationToken) - : actor.PerformAsync(ExecuteAScript(Scripts.SetElementValue, element.Value.WebElement, value), cancellationToken); - } - - /// - /// Initializes a new instance of the class. - /// - /// The value to set on the element. - /// If then the JavaScript will fire UI events to simulate having set the element interactively - public SetTheElementValue(object value, bool simulateInteractiveSet) - { - this.value = value; - this.simulateInteractiveSet = simulateInteractiveSet; - } - } -} \ No newline at end of file diff --git a/CSF.Screenplay.Selenium/Actions/SingleElementPerformableDocs.xml b/CSF.Screenplay.Selenium/Actions/SingleElementPerformableDocs.xml new file mode 100644 index 00000000..5131efb1 --- /dev/null +++ b/CSF.Screenplay.Selenium/Actions/SingleElementPerformableDocs.xml @@ -0,0 +1,23 @@ + + + + + + + This class is not a complete , as it relies + upon shared logic to retrieve the upon which it shall act. + It has this in common with many Screenplay + actions & tasks in the Selenium extension which operate upon a single element. These performables derive from + . In order for this class to be used as a full-fledged + , an instance of this type must be wrapped + within an instance of . + The adapter class provides the shared boilerplate logic which provides access to the Selenium Element. + Note that the builder method(s) which create instances of this type include the 'wrap within an adapter' + logic. Normal usage of this performable, when creating it from a builder, does not need to be + concerned with this factor. + + + + \ No newline at end of file diff --git a/CSF.Screenplay.Selenium/Actions/Wait.cs b/CSF.Screenplay.Selenium/Actions/Wait.cs index a37d7e53..fd6c8d9d 100644 --- a/CSF.Screenplay.Selenium/Actions/Wait.cs +++ b/CSF.Screenplay.Selenium/Actions/Wait.cs @@ -9,26 +9,99 @@ namespace CSF.Screenplay.Selenium.Actions { /// - /// A performable action that waits until a specified condition is met, or a timeout expires (whichever is sooner). + /// A Screenplay action which pauses the Performance until either a specified condition is met or a timeout expires, + /// whichever occurs sooner. /// /// /// - /// This action makes use of Selenium's class to implement the waiting logic. - /// The maximum time (the timeout) for which to wait is determined by the following rules of precedence: + /// Use this action via either the builder method + /// or . The former is preferred over the latter, + /// as it permits the use of a fluent builder syntax to specify the behaviour of the wait. + /// See the documentation of those builder methods to learn more about their use. + /// + /// + /// This action forces the progress of the to be delayed + /// whilst the WebDriver waits for a condition to be true. Internally, this action makes use of Selenium's + /// WebDriverWait functionality to implement the waiting logic. Documentation for the WebDriver Wait + /// functionality from the web may be useful if you would like a deeper understanding of this action. + /// This action accepts a number of parameters. + /// + /// + /// The Predicate is the condition for which we are waiting. In other words, we are waiting until the Predicate + /// is . This is expressed using a , of . + /// See for extension methods which act as builders for a predicate: + /// builds a predicate for a single element whereas + /// builds a predicate for a collection of + /// elements. + /// + /// + /// The Timeout is the maximum time which this Action will wait. If the Predicate (above) is not true. + /// If the timeout elapses before the Predicate is true then this action will throw + /// . In fact, it is the underlying Selenium WebDriver which will throw + /// this exception; this action does not change that. + /// It is impossible to specify an indefinite timeout. The following rules of precedence are used to ensure that + /// the timeout is always set: /// /// /// If a timeout was specified when constructing this instance, that value is used. - /// If the performing this question has the ability , then the - /// time specified by is used. + /// If the performing this question has the ability , + /// then the time specified by is used. /// If neither of the above apply, a default timeout of 5 seconds is used. /// /// - /// If the wait fails to complete within the timeout period, a is thrown. + /// The Polling Interval controls how often the WebDriver queries the web browser to determine if the + /// Predicate (above) has been fulfilled. Shorter polling intervals can lead to more responsive waits, the Performance + /// is not left waiting longer than it needs. The 'cost' of a shorter polling interval is increased traffic between + /// the Screenplay and the WebDriver. This is particularly relevant when using a Remote WebDriver, as this translates + /// to actual network/Internet traffic and latency. + /// If unspecified, Selenium's default polling interval is used. At the time of writing that is 500 milliseconds. + /// + /// + /// Lastly, the Ignored Exception Types is a collection of . Each entry should be a type which + /// derives from which should be silently ignored whilst testing the Predicate (above). + /// This can be useful to ignore errors which are raised because the Predicate isn't true yet, without needing + /// expensive (and performance-impacting) defensive programming. If this collection is specified then these exception + /// types will be caught and silently ignored whilst evaluating the Predicate, treating an exception as "not true". /// /// + /// + /// + /// In this example, Screenplay instructs the WebDriver to wait for up to 2 seconds, or until no element + /// with the class loading_spinner is visible on the web page. + /// + /// + /// using CSF.Screenplay.Selenium.Elements; + /// using static CSF.Screenplay.Selenium.PerformableBuilder; + /// + /// readonly ITarget spinner = new ClassName("loading_spinner", "the loading spinner"); + /// + /// // Within the logic of a custom task, deriving from IPerformable + /// public async ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + /// { + /// // ... other performance logic + /// await actor.PerformAsync(WaitUntil(spinner.Is().NotVisible().ForAtMost(TimeSpan.FromSeconds(2)), cancellationToken); + /// // ... other performance logic + /// } + /// + /// + /// + /// + /// public class Wait : IPerformable, ICanReport { - static readonly TimeSpan defaultTimeout = TimeSpan.FromSeconds(5); + /// + /// This default timeout of 5 seconds used when no timeout is specified and the actor has no ability. + /// + /// + /// + /// This, along with a few other performables in the Selenium extension for Screenplay involve waiting, with a timeout + /// to prevent waiting indefinitely. + /// If no timeout has been specified then this 5-second timeout is used as a fall-back default. + /// This may be overridden by granting the the ability , with a different + /// timeout specified. + /// + /// + public static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(5); readonly TimeSpan? timeout; readonly TimeSpan? pollingInterval; @@ -62,7 +135,7 @@ TimeSpan GetTimeout(ICanPerform actor) if(actor.TryGetAbility(out var ability)) return ability.WaitTime; - return defaultTimeout; + return DefaultTimeout; } /// diff --git a/CSF.Screenplay.Selenium/Actions/WaitForSomeTime.cs b/CSF.Screenplay.Selenium/Actions/WaitForSomeTime.cs index e79fbe38..fc8c853d 100644 --- a/CSF.Screenplay.Selenium/Actions/WaitForSomeTime.cs +++ b/CSF.Screenplay.Selenium/Actions/WaitForSomeTime.cs @@ -5,8 +5,34 @@ namespace CSF.Screenplay.Selenium.Actions { /// - /// A performable action that waits for a specified amount of time. + /// A Screenplay action which pauses the performance for a specified amount of time. /// + /// + /// + /// Create instances of this action via the builder method . + /// This wait action inserts an arbitrary pause into the progress of the . + /// For most kinds of waiting scenario, consider using instead of this action. + /// This kind of wait will always wait for the full time, wheras a is configured with a + /// condition/Predicate. Once that condition is true it stops waiting and the performance is resumed. + /// + /// + /// + /// + /// In this example, Screenplay waits for 3 seconds. + /// + /// + /// using static CSF.Screenplay.Selenium.PerformableBuilder; + /// + /// // Within the logic of a custom task, deriving from IPerformable + /// public async ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + /// { + /// // ... other performance logic + /// await actor.PerformAsync(WaitFor(TimeSpan.FromSeconds(3), cancellationToken); + /// // ... other performance logic + /// } + /// + /// + /// public class WaitForSomeTime : IPerformable, ICanReport { readonly TimeSpan duration; diff --git a/CSF.Screenplay.Selenium/ActorExtensions.cs b/CSF.Screenplay.Selenium/ActorExtensions.cs index b267ac60..70b3de45 100644 --- a/CSF.Screenplay.Selenium/ActorExtensions.cs +++ b/CSF.Screenplay.Selenium/ActorExtensions.cs @@ -9,7 +9,7 @@ namespace CSF.Screenplay.Selenium public static class ActorExtensions { /// - /// Gets a lazy-loaded instance for the given actor and target. + /// Gets a lazy-loaded instance matching the specified . /// /// An actor, who must have the ability. /// A target which describes an HTML element @@ -22,7 +22,7 @@ public static Lazy GetLazyElement(this ICanPerform actor, ITarg => new Lazy(() => target.GetElement(actor.GetAbility().WebDriver)); /// - /// Gets a lazy-loaded instance for the given actor and target. + /// Gets a lazy-loaded instance matching the specified . /// /// An actor, who must have the ability. /// A target which describes a collection of HTML elements diff --git a/CSF.Screenplay.Selenium/BrowseTheWeb.cs b/CSF.Screenplay.Selenium/BrowseTheWeb.cs index ca229ade..e93d785d 100644 --- a/CSF.Screenplay.Selenium/BrowseTheWeb.cs +++ b/CSF.Screenplay.Selenium/BrowseTheWeb.cs @@ -8,24 +8,88 @@ namespace CSF.Screenplay.Selenium /// /// Screenplay ability which allows an to browse the web using a Selenium WebDriver. /// + /// + /// + /// The 'Browse the Web' ability wraps a Selenium WebDriver and provides access to it via + /// the property. It also provide access to the options which were used to create the web driver, + /// via the property. + /// Following Selenium's architecture, both of these properties are abstract/interfaces and offer only the base/lowest common + /// denominator types. Often, this is sufficient. Developers may check for other Selenium-related interfaces if they are required, + /// using patterns such as the following: + /// + /// + /// if(browseTheWeb.WebDriver is OpenQA.Selenium.ISupportsPrint printingDriver) + /// /* ... exercise printingDriver ... */ + /// + /// + /// This ability makes use of from the + /// CSF.Extensions.WebDriver package. This provides a + /// configurable manner, using the .NET Options + /// pattern, to specify the WebDriver without hard-coding the choice. + /// + /// + /// The Selenium Extension to Screenplay makes the web driver factory available via dependency injection. + /// The recommended way in which to grant an Actor this ability is via an . The web driver factory may be safely + /// constructor-injected into the persona class. + /// + /// + /// + /// + /// Imagine you have configuration like the following in your appsettings.json, following the + /// Microsoft Options Pattern, with + /// a configuration file. + /// + /// + /// { + /// "WebDriverFactory": { + /// "DriverConfigurations": { + /// "LocalChrome": { "DriverType": "ChromeDriver" }, + /// "LocalFirefox": { "DriverType": "FirefoxDriver" }, + /// }, + /// } + /// } + /// + /// + /// You could now use the following technique when you configure your with this ability. + /// The following example shows how to do this using an . + /// + /// + /// In addition to the configuration above and the persona shown below, run this code whilst an environment variable + /// named WebDriverFactory__SelectedConfiguration is defined and set to LocalFirefox. + /// This environment variable selects which of the two configured WebDrivers is used (in this case, Firefox). + /// Developers may use the configuration to store a library of available WebDriver configurations, and use a single + /// environment variable to switch between them at execution time. + /// + /// + /// using CSF.Extensions.WebDriver; + /// using CSF.Screenplay.Selenium; + /// + /// public class Webster(IGetsWebDriver webDriverFactory) : IPersona + /// { + /// public string Name => "Webster"; + /// + /// public Actor GetActor(Guid performanceIdentity) + /// { + /// var webster = new Actor(Name, performanceIdentity); + /// var browseTheWeb = new BrowseTheWeb(webDriverFactory); + /// webster.IsAbleTo(browseTheWeb); + /// return webster; + /// } + /// } + /// + /// + /// + /// + /// public class BrowseTheWeb : ICanReport, IDisposable { - readonly IGetsWebDriver webDriverFactory; - WebDriverAndOptions webDriverAndOptions; + readonly Lazy webDriverAndOptions; bool disposedValue; /// /// Gets the Selenium WebDriver associated with the current ability instance. /// - public IWebDriver WebDriver - { - get { - if(webDriverAndOptions is null) - webDriverAndOptions = webDriverFactory.GetDefaultWebDriver(); - - return webDriverAndOptions.WebDriver; - } - } + public IWebDriver WebDriver => webDriverAndOptions.Value.WebDriver; /// /// Gets the WebDriver options which were used to create . @@ -36,26 +100,18 @@ public IWebDriver WebDriver /// The purpose of this property is to allow developers to inspect the options which were used to create the WebDriver. /// /// - public DriverOptions DriverOptions - { - get { - if(webDriverAndOptions is null) - webDriverAndOptions = webDriverFactory.GetDefaultWebDriver(); + public DriverOptions DriverOptions => webDriverAndOptions.Value.DriverOptions; - return webDriverAndOptions.DriverOptions; - } - } - - /// - /// Gets a JavaScript executor object based upon the current . - /// - /// A JavaScript executor object - /// If the current web driver does not support JavaScript execution. - public IJavaScriptExecutor GetJavaScriptExecutor() + static Lazy GetLazyWebDriverAndOptions(IGetsWebDriver webDriverFactory, string webDriverName) { - return WebDriver is IJavaScriptExecutor executor - ? executor - : throw new NotSupportedException($"The web driver must implement {nameof(IJavaScriptExecutor)}"); + if (webDriverFactory is null) throw new ArgumentNullException(nameof(webDriverFactory)); + + return new Lazy(() => + { + return webDriverName is null + ? webDriverFactory.GetDefaultWebDriver() + : webDriverFactory.GetWebDriver(webDriverName); + }); } /// @@ -67,10 +123,26 @@ public ReportFragment GetReportFragment(Actor actor, IFormatsReportFragment form /// /// Initializes a new instance of the class. /// - /// - public BrowseTheWeb(IGetsWebDriver webDriverFactory) + /// + /// + /// It is quite normal to omit the parameter, leaving it with its default value. + /// If the WebDriver name is omitted or null then the method will + /// be used to get the WebDriver. This requires that the WebDriver factory is configured with a default driver. This could be done via + /// the SelectedConfiguration property of the JSON configuration, or via an environment variable (see the example code in the remarks + /// to this class) or any other way which the Microsoft + /// Configuration Pattern supports. + /// Alternatively, to activate a specific named configuration, you may specify the WebDriver name here. + /// + /// + /// It is normal to retrieve the parameter via Dependency Injection. The Selenium Extension to + /// Screenplay makes the factory available in that manner. + /// + /// + /// A universal WebDriver factory instance + /// An optional name, specifying the WebDriver configuration (within those available in the factory) to use. + public BrowseTheWeb(IGetsWebDriver webDriverFactory, string webDriverName = null) { - this.webDriverFactory = webDriverFactory ?? throw new ArgumentNullException(nameof(webDriverFactory)); + webDriverAndOptions = GetLazyWebDriverAndOptions(webDriverFactory, webDriverName); } /// @@ -81,10 +153,10 @@ protected virtual void Dispose(bool disposing) { if (!disposedValue) { - if (disposing) + if (disposing && webDriverAndOptions.IsValueCreated) { - webDriverAndOptions?.WebDriver.Quit(); - webDriverAndOptions?.Dispose(); + webDriverAndOptions.Value.WebDriver.Quit(); + webDriverAndOptions.Value.Dispose(); } disposedValue = true; } diff --git a/CSF.Screenplay.Selenium/BrowseTheWebExtensions.cs b/CSF.Screenplay.Selenium/BrowseTheWebExtensions.cs new file mode 100644 index 00000000..529a5f6a --- /dev/null +++ b/CSF.Screenplay.Selenium/BrowseTheWebExtensions.cs @@ -0,0 +1,24 @@ +using System; +using OpenQA.Selenium; + +namespace CSF.Screenplay.Selenium +{ + /// + /// Extension methods for the ability. + /// + public static class BrowseTheWebExtensions + { + /// + /// Gets a JavaScript executor object based upon the current . + /// + /// A JavaScript executor object + /// If the current web driver does not support JavaScript execution. + public static IJavaScriptExecutor GetJavaScriptExecutor(this BrowseTheWeb ability) + { + return ability.WebDriver is IJavaScriptExecutor executor + ? executor + : throw new NotSupportedException($"The web driver must implement {nameof(IJavaScriptExecutor)}"); + } + + } +} \ No newline at end of file diff --git a/CSF.Screenplay.Selenium/BrowserQuirks.cs b/CSF.Screenplay.Selenium/BrowserQuirks.cs index 5408b388..46b8354e 100644 --- a/CSF.Screenplay.Selenium/BrowserQuirks.cs +++ b/CSF.Screenplay.Selenium/BrowserQuirks.cs @@ -6,37 +6,65 @@ namespace CSF.Screenplay.Selenium /// /// Static class which holds known browser quirks information. /// + /// + /// + /// Quirks are a feature of the universal WebDriver factory. + /// See for more information. + /// + /// + /// + /// public static class BrowserQuirks { /// - /// Gets the name of a browser quirk which means that a Web Driver cannot set the value of an <input type="date"> using - /// "Send Keys". + /// Gets the name of a browser quirk, for browsers which cannot set the value of an <input type="date"> using the + /// "Send Keys" technique. /// /// /// - /// Date input fields have patchy support amongst browsers and web drivers. The UI which appears in order to set a date often responds - /// poorly to typing a new date manually. Additionally, the calendar UI which most browsers will render is not built from HTML elements - /// and thus cannot be interacted with in the traditional way, by Web Drivers. Thus, if we cannot 'type' a new date and cannot interact + /// Some WebDriver implementations permit typing a locale-formatted date string into the HTML element, as if it were any other + /// <input> element. Other WebDriver implementations cannot interact with Date inputs in that way and + /// can only enter a new value via a JavaScript workaround. This quirk marks browsers which require such a workaround. + /// + /// + /// The calendar UI which most browsers will render for a Date input field is not built from HTML elements + /// and thus cannot be interacted with by a WebDriver. Thus, if we cannot 'type' a new date and cannot interact /// by clicking on the calendar, all that remains is to use a JavaScript workaround to set the date. /// /// + /// public static readonly string CannotSetInputTypeDateWithSendKeys = "CannotSetInputTypeDateWithSendKeys"; /// - /// Gets the name of a browser quirk which means that - after navigation to a different page - the browser must be instructed to wait - /// until the following page document is ready. + /// Gets the name of a browser quirk, for browsers which must be instructed to wait after navigating to a new web page, until that new + /// page has finished loading. /// /// /// - /// Most Web Driver implementations, without this quirk, automatically wait for the 'incoming' page (in traditional web document - /// navigation) to be ready before they attempt to interact with it. That is - if the "document ready" event has not yet occurred, - /// they will continue waiting. Some browsers/web drivers (those with this quirk) need to be instructed to wait for that ready-state, - /// or else they are liable to attempt to interact with a page which is not yet fully loaded. + /// In traditional (non-SPA) web page navigation, there is a brief delay after clicking a hyperlink which navigates to a new web page. + /// That delay is the time for the 'incoming' web page to load and render. During that delay, the 'outgoing' web page is still visible + /// on-screen. + /// + /// + /// Most WebDriver implementations, those without this quirk, automatically wait during that loading delay. They do not execute further + /// WebDriver commands until the 'incoming' web page has finished loading and is displayed on the screen. This prevents unexpected + /// results and being thrown, and similar. + /// Browsers/WebDrivers which are affected by this quirk (at the time of writing, all versions of Apple Safari) do not wait during this + /// loading delay. This means that unless they are explicitly instructed to wait, they can attempt to execute further WebDriver commands + /// prematurely. + /// + /// + /// The task analyses the WebDriver for the presence of this quirk. + /// If the quirk is present then the task inserts wait logic, to work around this behaviour of the affected Browser/WebDriver. + /// This allows Selenium/Screenplay logic to be written in a manner that behaves consistently cross-browser. /// /// - /// Note that this only applies to traditional web navigation. It does not apply to navigating SPA-type apps. + /// It is important to remember that this behaviour and quirk applies only to traditional web browser navigation. That is a sequence + /// in which the outgoing page is fully unloaded and the incoming page is retrieved and loaded from the webserver. It does not apply + /// to navigating through Single Page Applications. /// /// + /// public static readonly string NeedsToWaitAfterPageLoad = "NeedsToWaitAfterPageLoad"; /// @@ -45,8 +73,8 @@ public static class BrowserQuirks /// /// /// This information ships with CSF.Screenplay.Selenium. It may be overridden by user-supplied configuration, should - /// things change in the future. See the - /// browser quirks reference material for more information. + /// things change in the future. See + /// for more information. /// /// /// Quirks data, about the peculiarities of specific browsers. diff --git a/CSF.Screenplay.Selenium/Builders/ClickBuilder.cs b/CSF.Screenplay.Selenium/Builders/ClickBuilder.cs index 42211761..58c510bf 100644 --- a/CSF.Screenplay.Selenium/Builders/ClickBuilder.cs +++ b/CSF.Screenplay.Selenium/Builders/ClickBuilder.cs @@ -24,8 +24,7 @@ public IPerformable GetPerformable() /// /// Use this method when the click is expected to cause a new web page to load into the browser. /// In that case, the performable returned by this method will not only click on the target element. - /// It will also wait for the DOMContentLoaded event from the web page which is loaded following that click. - /// This ensures that subsequent interactions with the Web Browser are not performed upon a page which is not yet loaded. + /// See the task for more information. /// /// /// Note that the meaning of "a new page loading" is a full Web Browser page load (an entirely new HTML document). @@ -33,9 +32,9 @@ public IPerformable GetPerformable() /// /// /// - /// + /// A performable public IPerformable AndWaitForANewPageToLoad(TimeSpan? forAtMost = null) - => SingleElementPerformableAdapter.From(new ClickAndWaitForDocumentReady(forAtMost ?? TimeSpan.FromSeconds(5)), target); + => SingleElementPerformableAdapter.From(new ClickAndWaitForDocumentReady(forAtMost), target); /// /// Initializes a new instance of the class with the specified target. diff --git a/CSF.Screenplay.Selenium/Builders/EnterTheDateBuilder.cs b/CSF.Screenplay.Selenium/Builders/EnterTheDateBuilder.cs index 95c63371..abbb4f7b 100644 --- a/CSF.Screenplay.Selenium/Builders/EnterTheDateBuilder.cs +++ b/CSF.Screenplay.Selenium/Builders/EnterTheDateBuilder.cs @@ -1,8 +1,8 @@ using System; using System.Globalization; using CSF.Screenplay.Performables; -using CSF.Screenplay.Selenium.Actions; using CSF.Screenplay.Selenium.Elements; +using CSF.Screenplay.Selenium.Tasks; namespace CSF.Screenplay.Selenium.Builders { diff --git a/CSF.Screenplay.Selenium/Builders/QueryPredicatePrototypeBuilder.cs b/CSF.Screenplay.Selenium/Builders/QueryPredicatePrototypeBuilder.cs index 168ae25c..e84563a6 100644 --- a/CSF.Screenplay.Selenium/Builders/QueryPredicatePrototypeBuilder.cs +++ b/CSF.Screenplay.Selenium/Builders/QueryPredicatePrototypeBuilder.cs @@ -15,9 +15,68 @@ namespace CSF.Screenplay.Selenium.Builders /// /// /// - /// These are generally used for WebDriver waits, to wait until a target element meets the specified conditions. + /// Predicates, built by the members of this class, are primarily used for two purposes: + /// + /// + /// The action, to specify the condition under which the wait should end. + /// The question, to specify the criteria by which to match elements. + /// + /// + /// For more information about interrogating the state of targets, see ; + /// queries are the foundation upon which this predicate functionality is based. /// /// + /// + /// + /// This first example shows how a action may be written, to wait until the text of a status display reads "finished". + /// Note the section .Has.Text("finished"), which demonstrates use of the functionality of this class. + /// + /// + /// using CSF.Screenplay.Selenium.Elements; + /// using static CSF.Screenplay.Selenium.PerformableBuilder; + /// + /// readonly ITarget status = new CssSelector("#footer .status", "the status display"); + /// + /// // Within the logic of a custom task, deriving from IPerformable + /// public async ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + /// { + /// // ... other performance logic + /// await actor.PerformAsync(WaitUntil(status.Has().Text("finished"), cancellationToken); + /// // ... other performance logic + /// } + /// + /// + /// The second example, below, shows the usage of , matching only elements + /// which are larger than 50×50 pixels in both dimensions. Imagine a bubble chart, in which we are looking for bubbles + /// larger than a certain size. + /// Note the section .ForThoseWhich(q => q.Size(s => s.Height > 50 && s.Width > 50)), which demonstrates + /// use of the functionality of this class. + /// + /// + /// using CSF.Screenplay.Selenium.Elements; + /// using static CSF.Screenplay.Selenium.PerformableBuilder; + /// + /// readonly ITarget bubbles = new CssSelector(".bubble_chart .bubble", "the items in the bubble chart"); + /// + /// // Within the logic of a custom task, deriving from IPerformableWithResult<SeleniumElementCollection> + /// public async ValueTask<SeleniumElementCollection> PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + /// { + /// // ... other performance logic + /// var allBubbles = await actor.PerformAsync(FindElementsOnThePage().WhichMatch(bubbles), cancellationToken); + /// var largeBubbles = await actor.PerformAsync(Filter(allBubbles) + /// .ForThoseWhich(q => q.Size(s => s.Height > 50 && s.Width > 50)), + /// cancellationToken); + /// // ... other performance logic + /// } + /// + /// + /// + /// + /// + /// + /// + /// + /// public class QueryPredicatePrototypeBuilder { readonly ITarget target; @@ -28,10 +87,14 @@ public class QueryPredicatePrototypeBuilder /// /// /// - /// Note that this specification makes use of Selenium's GetAttribute method. - /// The behaviour of that method in fact queries the DOM property of the element first, before querying the HTML attribute. - /// It also has special handling for certain attributes such as class & readonly attributes (substituting them with className - /// and readOnly respectively when querying DOM properties). + /// The underlying implementation of this method makes use of the GetAttribute method of Selenium's + /// type to query the current attribute value. The get-attribute behaviour + /// queries the corresponding DOM property of the element first, before querying the HTML attribute. + /// Thus, if the DOM has been updated since the element was rendered onto the page, the current live value + /// of the element will be used. + /// Selenium's get-attribute functionality also has special handling for certain attributes such as class + /// & readonly. When inspecting the DOM properties, these names are transparently normalised to + /// className and readOnly respectively. /// Finally, 'boolean' attribute values are returned with either the string true if present or if not. /// /// @@ -39,6 +102,8 @@ public class QueryPredicatePrototypeBuilder /// by using a predicate which checks for . /// /// + /// See the remarks for the class for two examples of + /// usage. The methods of this type all follow the same broad pattern to create predicates. /// The name of the attribute to query. /// The predicate to apply to the attribute value. /// A , which may be converted to a full predicate. @@ -50,13 +115,23 @@ public QueryPredicatePrototype AttributeValue(string attributeName, Func /// /// /// - /// Note that this specification makes use of Selenium's GetAttribute method. - /// The behaviour of that method in fact queries the DOM property of the element first, before querying the HTML attribute. - /// It also has special handling for certain attributes such as class & readonly attributes (substituting them with className - /// and readOnly respectively when querying DOM properties). + /// The underlying implementation of this method makes use of the GetAttribute method of Selenium's + /// type to query the current attribute value. The get-attribute behaviour + /// queries the corresponding DOM property of the element first, before querying the HTML attribute. + /// Thus, if the DOM has been updated since the element was rendered onto the page, the current live value + /// of the element will be used. + /// Selenium's get-attribute functionality also has special handling for certain attributes such as class + /// & readonly. When inspecting the DOM properties, these names are transparently normalised to + /// className and readOnly respectively. /// Finally, 'boolean' attribute values are returned with either the string true if present or if not. /// + /// + /// Note that the last behaviour, described above, means that this method may be used to match elements which do not have the specified attribute, + /// by using as the desired value. + /// /// + /// See the remarks for the class for two examples of + /// usage. The methods of this type all follow the same broad pattern to create predicates. /// The name of the attribute to query. /// The value of the attribute to query. /// A , which may be converted to a full predicate. @@ -68,13 +143,23 @@ public QueryPredicatePrototype AttributeValue(string attributeName, stri /// /// /// - /// Note that this specification makes use of Selenium's GetAttribute method. - /// The behaviour of that method in fact queries the DOM property of the element first, before querying the HTML attribute. - /// It also has special handling for certain attributes such as class & readonly attributes (substituting them with className - /// and readOnly respectively when querying DOM properties). + /// The underlying implementation of this method makes use of the GetAttribute method of Selenium's + /// type to query the current attribute value. The get-attribute behaviour + /// queries the corresponding DOM property of the element first, before querying the HTML attribute. + /// Thus, if the DOM has been updated since the element was rendered onto the page, the current live value + /// of the element will be used. + /// Selenium's get-attribute functionality also has special handling for certain attributes such as class + /// & readonly. When inspecting the DOM properties, these names are transparently normalised to + /// className and readOnly respectively. /// Finally, 'boolean' attribute values are returned with either the string true if present or if not. /// + /// + /// This method specifically targets that last behaviour, described above. It verifies that the response for attribute's + /// value is not . + /// /// + /// See the remarks for the class for two examples of + /// usage. The methods of this type all follow the same broad pattern to create predicates. /// The name of the attribute to query. /// A , which may be converted to a full predicate. public QueryPredicatePrototype Attribute(string attributeName) @@ -85,13 +170,23 @@ public QueryPredicatePrototype Attribute(string attributeName) /// /// /// - /// Note that this specification makes use of Selenium's GetAttribute method. - /// The behaviour of that method in fact queries the DOM property of the element first, before querying the HTML attribute. - /// It also has special handling for certain attributes such as class & readonly attributes (substituting them with className - /// and readOnly respectively when querying DOM properties). + /// The underlying implementation of this method makes use of the GetAttribute method of Selenium's + /// type to query the current attribute value. The get-attribute behaviour + /// queries the corresponding DOM property of the element first, before querying the HTML attribute. + /// Thus, if the DOM has been updated since the element was rendered onto the page, the current live value + /// of the element will be used. + /// Selenium's get-attribute functionality also has special handling for certain attributes such as class + /// & readonly. When inspecting the DOM properties, these names are transparently normalised to + /// className and readOnly respectively. /// Finally, 'boolean' attribute values are returned with either the string true if present or if not. /// + /// + /// This method inspects the class attribute and determines whether the specified class is present amongst the element's + /// class list. + /// /// + /// See the remarks for the class for two examples of + /// usage. The methods of this type all follow the same broad pattern to create predicates. /// The name of the class to query. /// A , which may be converted to a full predicate. public QueryPredicatePrototype Class(string @class) @@ -102,13 +197,23 @@ public QueryPredicatePrototype Class(string @class) /// /// /// - /// Note that this specification makes use of Selenium's GetAttribute method. - /// The behaviour of that method in fact queries the DOM property of the element first, before querying the HTML attribute. - /// It also has special handling for certain attributes such as class & readonly attributes (substituting them with className - /// and readOnly respectively when querying DOM properties). + /// The underlying implementation of this method makes use of the GetAttribute method of Selenium's + /// type to query the current attribute value. The get-attribute behaviour + /// queries the corresponding DOM property of the element first, before querying the HTML attribute. + /// Thus, if the DOM has been updated since the element was rendered onto the page, the current live value + /// of the element will be used. + /// Selenium's get-attribute functionality also has special handling for certain attributes such as class + /// & readonly. When inspecting the DOM properties, these names are transparently normalised to + /// className and readOnly respectively. /// Finally, 'boolean' attribute values are returned with either the string true if present or if not. /// + /// + /// This method inspects the class attribute and determines whether the specified class is not present amongst the element's + /// class list. + /// /// + /// See the remarks for the class for two examples of + /// usage. The methods of this type all follow the same broad pattern to create predicates. /// The name of the class to query. /// A , which may be converted to a full predicate. public QueryPredicatePrototype NotClass(string @class) @@ -119,13 +224,23 @@ public QueryPredicatePrototype NotClass(string @class) /// /// /// - /// Note that this specification makes use of Selenium's GetAttribute method. - /// The behaviour of that method in fact queries the DOM property of the element first, before querying the HTML attribute. - /// It also has special handling for certain attributes such as class & readonly attributes (substituting them with className - /// and readOnly respectively when querying DOM properties). + /// The underlying implementation of this method makes use of the GetAttribute method of Selenium's + /// type to query the current attribute value. The get-attribute behaviour + /// queries the corresponding DOM property of the element first, before querying the HTML attribute. + /// Thus, if the DOM has been updated since the element was rendered onto the page, the current live value + /// of the element will be used. + /// Selenium's get-attribute functionality also has special handling for certain attributes such as class + /// & readonly. When inspecting the DOM properties, these names are transparently normalised to + /// className and readOnly respectively. /// Finally, 'boolean' attribute values are returned with either the string true if present or if not. /// + /// + /// This method inspects the class attribute and determines whether every one of the specified classes are present + /// in the element's class list. + /// /// + /// See the remarks for the class for two examples of + /// usage. The methods of this type all follow the same broad pattern to create predicates. /// The names of the classes to query. /// A , which may be converted to a full predicate. public QueryPredicatePrototype AllClasses(params string[] classes) @@ -150,36 +265,74 @@ static bool HasClass(string attributeValue, string @class) } /// - /// Creates a query predicate based on whether or not the element is clickable. + /// Creates a query predicate based on the element's clickability. /// + /// + /// + /// An element is considered 'clickable' if it is both visible in the web browser and it is not disabled. + /// + /// + /// See the remarks for the class for two examples of + /// usage. The methods of this type all follow the same broad pattern to create predicates. /// The predicate to apply to the element's "clickability". /// A , which may be converted to a full predicate. public QueryPredicatePrototype Clickability(Func predicate) => CreatePrototype(new ClickableQuery(), Spec.Func(x => predicate(x)), t => $"{t.Name} has clickability matching a predicate"); /// - /// Creates a query predicate based on whether or not the element is clickable. + /// Creates a query predicate based on whether the element is clickable. /// + /// + /// + /// An element is considered 'clickable' if it is both visible in the web browser and it is not disabled. + /// + /// + /// See the remarks for the class for two examples of + /// usage. The methods of this type all follow the same broad pattern to create predicates. /// The value to compare against the element's "clickability". /// A , which may be converted to a full predicate. public QueryPredicatePrototype Clickability(bool value) => CreatePrototype(new ClickableQuery(), Spec.Func(x => x == value), t => $"{t.Name} {(value ? "is clickable" : "is not clickable")}"); /// - /// Creates a query predicate based on whether the element is clickable. + /// Creates a query predicate for only clickable elements. /// + /// + /// + /// An element is considered 'clickable' if it is both visible in the web browser and it is not disabled. + /// + /// + /// See the remarks for the class for two examples of + /// usage. The methods of this type all follow the same broad pattern to create predicates. /// A , which may be converted to a full predicate. public QueryPredicatePrototype Clickable() => Clickability(true); /// - /// Creates a query predicate based on whether the element is clickable. + /// Creates a query predicate for only elements which are not clickable. /// + /// + /// + /// An element is considered 'clickable' if it is both visible in the web browser and it is not disabled. + /// + /// + /// See the remarks for the class for two examples of + /// usage. The methods of this type all follow the same broad pattern to create predicates. /// A , which may be converted to a full predicate. public QueryPredicatePrototype NotClickable() => Clickability(false); /// /// Creates a query predicate based on a specified CSS property and a predicate for the property's value. /// + /// + /// + /// Note that this is not limited to reading the styling of an element/elements as it was defined in + /// a stylesheet. It reads the live styling of the element as it is when the question executes. + /// Thus, if JavaScript or a class change has altered the styling of the element since it was first rendered + /// on-screen, its up-to-date styling information will be inspected. + /// + /// + /// See the remarks for the class for two examples of + /// usage. The methods of this type all follow the same broad pattern to create predicates. /// The name of the property to query. /// The predicate to apply to the property value. /// A , which may be converted to a full predicate. @@ -189,6 +342,16 @@ public QueryPredicatePrototype CssProperty(string propertyName, Func /// Creates a query predicate based on a specified CSS property and a desired value for that property. /// + /// + /// + /// Note that this is not limited to reading the styling of an element/elements as it was defined in + /// a stylesheet. It reads the live styling of the element as it is when the question executes. + /// Thus, if JavaScript or a class change has altered the styling of the element since it was first rendered + /// on-screen, its up-to-date styling information will be inspected. + /// + /// + /// See the remarks for the class for two examples of + /// usage. The methods of this type all follow the same broad pattern to create predicates. /// The name of the property to query. /// The value to compare against the property value. /// A , which may be converted to a full predicate. @@ -198,6 +361,8 @@ public QueryPredicatePrototype CssProperty(string propertyName, string v /// /// Creates a query predicate based on the element's location (its top-left corner). /// + /// See the remarks for the class for two examples of + /// usage. The methods of this type all follow the same broad pattern to create predicates. /// The predicate to apply to the element's location. /// A , which may be converted to a full predicate. public QueryPredicatePrototype Location(Func predicate) @@ -206,6 +371,8 @@ public QueryPredicatePrototype Location(Func predicate) /// /// Creates a query predicate based on the element's location (its top-left corner). /// + /// See the remarks for the class for two examples of + /// usage. The methods of this type all follow the same broad pattern to create predicates. /// The value to compare against the element's location. /// A , which may be converted to a full predicate. public QueryPredicatePrototype Location(Point value) @@ -214,6 +381,8 @@ public QueryPredicatePrototype Location(Point value) /// /// Creates a query predicate based on the element's size (width & height in pixels). /// + /// See the remarks for the class for two examples of + /// usage. The methods of this type all follow the same broad pattern to create predicates. /// The predicate to apply to the element's size. /// A , which may be converted to a full predicate. public QueryPredicatePrototype Size(Func predicate) @@ -222,6 +391,8 @@ public QueryPredicatePrototype Size(Func predicate) /// /// Creates a query predicate based on the element's size (width & height in pixels). /// + /// See the remarks for the class for two examples of + /// usage. The methods of this type all follow the same broad pattern to create predicates. /// The value to compare against the element's size. /// A , which may be converted to a full predicate. public QueryPredicatePrototype Size(Size value) @@ -238,12 +409,14 @@ public QueryPredicatePrototype Size(Size value) /// space which is inherent in the markup, but which browsers ignore when actually displaying content. /// /// - /// Trimming it by default ensures that Screenplay reproduces functionality reliably cross-browser. + /// By trimming whitespace, the Selenium extension for Screenplay ensures that behaviour is consistent + /// cross-browser. /// If this causes an issue and you would like the leading/trailing whitespace included the use /// instead. - /// Note that you may see different results in browsers which include leading/trailing whitespace anyway. /// /// + /// See the remarks for the class for two examples of + /// usage. The methods of this type all follow the same broad pattern to create predicates. /// The predicate to apply to the element's text. /// A , which may be converted to a full predicate. public QueryPredicatePrototype Text(Func predicate) @@ -260,12 +433,14 @@ public QueryPredicatePrototype Text(Func predicate) /// space which is inherent in the markup, but which browsers ignore when actually displaying content. /// /// - /// Trimming it by default ensures that Screenplay reproduces functionality reliably cross-browser. + /// By trimming whitespace, the Selenium extension for Screenplay ensures that behaviour is consistent + /// cross-browser. /// If this causes an issue and you would like the leading/trailing whitespace included the use /// instead. - /// Note that you may see different results in browsers which include leading/trailing whitespace anyway. /// /// + /// See the remarks for the class for two examples of + /// usage. The methods of this type all follow the same broad pattern to create predicates. /// The value to compare against the element's text. /// A , which may be converted to a full predicate. public QueryPredicatePrototype Text(string value) @@ -276,11 +451,15 @@ public QueryPredicatePrototype Text(string value) /// /// /// - /// When reading text from the web browser, this predicate will leave any leading/trailing whitespace in the text - /// without trimming it. - /// Note that you may see different results in browsers which include leading/trailing whitespace anyway. + /// When reading text from the web browser, this predicate will preserve any leading/trailing whitespace in the text + /// without trimming it. Be aware that some browsers (Safari) may include leading/trailing whitespace when reading text, + /// which other WebDriver implementations do not. This can result in inconsistent results when + /// operating cross-browser. If cross-browser consistency is important then consider using + /// instead. /// /// + /// See the remarks for the class for two examples of + /// usage. The methods of this type all follow the same broad pattern to create predicates. /// The predicate to apply to the element's text. /// A , which may be converted to a full predicate. public QueryPredicatePrototype TextWithoutTrimmingWhitespace(Func predicate) @@ -291,11 +470,15 @@ public QueryPredicatePrototype TextWithoutTrimmingWhitespace(Func /// /// - /// When reading text from the web browser, this predicate will leave any leading/trailing whitespace in the text - /// without trimming it. - /// Note that you may see different results in browsers which include leading/trailing whitespace anyway. + /// When reading text from the web browser, this predicate will preserve any leading/trailing whitespace in the text + /// without trimming it. Be aware that some browsers (Safari) may include leading/trailing whitespace when reading text, + /// which other WebDriver implementations do not. This can result in inconsistent results when + /// operating cross-browser. If cross-browser consistency is important then consider using + /// instead. /// /// + /// See the remarks for the class for two examples of + /// usage. The methods of this type all follow the same broad pattern to create predicates. /// The value to compare against the element's text. /// A , which may be converted to a full predicate. public QueryPredicatePrototype TextWithoutTrimmingWhitespace(string value) @@ -304,6 +487,8 @@ public QueryPredicatePrototype TextWithoutTrimmingWhitespace(string valu /// /// Creates a query predicate based on the element's DOM value. /// + /// See the remarks for the class for two examples of + /// usage. The methods of this type all follow the same broad pattern to create predicates. /// The predicate to apply to the element's DOM value. /// A , which may be converted to a full predicate. public QueryPredicatePrototype Value(Func predicate) @@ -312,42 +497,54 @@ public QueryPredicatePrototype Value(Func predicate) /// /// Creates a query predicate based on the element's DOM value. /// + /// See the remarks for the class for two examples of + /// usage. The methods of this type all follow the same broad pattern to create predicates. /// The value to compare against the element's DOM value. /// A , which may be converted to a full predicate. public QueryPredicatePrototype Value(string value) => CreatePrototype(new ValueQuery(), Spec.Func(x => x == value), t => $"{t.Name} has a DOM value equal to '{value}'"); /// - /// Creates a query predicate based on whether or not the element is visible. + /// Creates a query predicate based on whether the element is visible. /// + /// See the remarks for the class for two examples of + /// usage. The methods of this type all follow the same broad pattern to create predicates. /// The predicate to apply to the element's visibility. /// A , which may be converted to a full predicate. public QueryPredicatePrototype Visibility(Func predicate) => CreatePrototype(new VisibilityQuery(), Spec.Func(predicate), t => $"{t.Name} has visibility matching a predicate"); /// - /// Creates a query predicate based on whether or not the element is visible. + /// Creates a query predicate based on whether the element is visible. /// + /// See the remarks for the class for two examples of + /// usage. The methods of this type all follow the same broad pattern to create predicates. /// The value to compare against the element's visibility. /// A , which may be converted to a full predicate. public QueryPredicatePrototype Visibility(bool value) => CreatePrototype(new VisibilityQuery(), Spec.Func(x => x == value), t => $"{t.Name} {(value ? "is visible" : "is not visible")}"); /// - /// Creates a query predicate based on whether the element is visible. + /// Creates a query predicate for elements that are visible. /// + /// See the remarks for the class for two examples of + /// usage. The methods of this type all follow the same broad pattern to create predicates. /// A , which may be converted to a full predicate. public QueryPredicatePrototype Visible() => Visibility(true); /// - /// Creates a query predicate based on whether the element is not visible. + /// Creates a query predicate for elements that are not visible. /// + /// See the remarks for the class for two examples of + /// usage. The methods of this type all follow the same broad pattern to create predicates. /// A , which may be converted to a full predicate. public QueryPredicatePrototype NotVisible() => Visibility(false); /// /// Creates a query predicate based on the <option> elements, which are children of the current element, which are selected. /// + /// See the remarks for the class for two examples of + /// usage. The methods of this type all follow the same broad pattern to create predicates. /// The predicate to apply to the element's selected options. /// A , which may be converted to a full predicate. public QueryPredicatePrototype> SelectedOptions(Func,bool> predicate) @@ -363,6 +560,8 @@ public QueryPredicatePrototype> SelectedOptions(Func /// + /// See the remarks for the class for two examples of + /// usage. The methods of this type all follow the same broad pattern to create predicates. /// The display text of the options to match against. /// A , which may be converted to a full predicate. public QueryPredicatePrototype> SelectedOptionsWithText(params string[] optionTexts) @@ -380,6 +579,8 @@ public QueryPredicatePrototype> SelectedOptionsWithText(pa /// This method identifies the options by their DOM value. /// /// + /// See the remarks for the class for two examples of + /// usage. The methods of this type all follow the same broad pattern to create predicates. /// The DOM values of the options to match against. /// A , which may be converted to a full predicate. public QueryPredicatePrototype> SelectedOptionsWithValue(params string[] optionValues) @@ -390,6 +591,8 @@ public QueryPredicatePrototype> SelectedOptionsWithValue(p /// /// Creates a query predicate based on the <option> elements, which are children of the current element, which are not selected. /// + /// See the remarks for the class for two examples of + /// usage. The methods of this type all follow the same broad pattern to create predicates. /// The predicate to apply to the element's unselected options. /// A , which may be converted to a full predicate. public QueryPredicatePrototype> UnselectedOptions(Func,bool> predicate) @@ -407,6 +610,8 @@ public QueryPredicatePrototype> UnselectedOptions(Func /// + /// See the remarks for the class for two examples of + /// usage. The methods of this type all follow the same broad pattern to create predicates. /// The display text of the options to match against. /// A , which may be converted to a full predicate. public QueryPredicatePrototype> UnselectedOptionsWithText(params string[] optionTexts) @@ -424,6 +629,8 @@ public QueryPredicatePrototype> UnselectedOptionsWithText( /// This method identifies the options by their DOM value. /// /// + /// See the remarks for the class for two examples of + /// usage. The methods of this type all follow the same broad pattern to create predicates. /// The DOM values of the options to match against. /// A , which may be converted to a full predicate. public QueryPredicatePrototype> UnselectedOptionsWithValue(params string[] optionValues) @@ -434,6 +641,8 @@ public QueryPredicatePrototype> UnselectedOptionsWithValue /// /// Creates a query predicate based on the <option> elements, which are children of the current element, regardless of their selected state. /// + /// See the remarks for the class for two examples of + /// usage. The methods of this type all follow the same broad pattern to create predicates. /// The predicate to apply to the element's options. /// A , which may be converted to a full predicate. public QueryPredicatePrototype> Options(Func,bool> predicate) @@ -449,6 +658,8 @@ public QueryPredicatePrototype> Options(Func /// + /// See the remarks for the class for two examples of + /// usage. The methods of this type all follow the same broad pattern to create predicates. /// The display text of the options to match against. /// A , which may be converted to a full predicate. public QueryPredicatePrototype> OptionsWithText(params string[] optionTexts) @@ -466,6 +677,8 @@ public QueryPredicatePrototype> OptionsWithText(params str /// This method identifies the options by their DOM value. /// /// + /// See the remarks for the class for two examples of + /// usage. The methods of this type all follow the same broad pattern to create predicates. /// The DOM values of the options to match against. /// A , which may be converted to a full predicate. public QueryPredicatePrototype> OptionsWithValue(params string[] optionValues) diff --git a/CSF.Screenplay.Selenium/Builders/QuestionQueryBuilder.cs b/CSF.Screenplay.Selenium/Builders/QuestionQueryBuilder.cs index 3bef9de5..4dfa89f6 100644 --- a/CSF.Screenplay.Selenium/Builders/QuestionQueryBuilder.cs +++ b/CSF.Screenplay.Selenium/Builders/QuestionQueryBuilder.cs @@ -10,6 +10,43 @@ namespace CSF.Screenplay.Selenium.Builders /// Provides methods to build performable questions for a Selenium element, which are based upon /// querying/interrogating values from that element. /// + /// + /// + /// This class is used with the Screenplay questions and/or + /// . The builder methods for these questions, + /// and + /// respectively, return an + /// instance of this builder type. From this type, the developer should select what it is that they wish + /// to read from the element(s). + /// + /// + /// For more information about queries and their usage, see . + /// + /// + /// + /// + /// All the usages of or + /// follow the same pattern, as demonstrated below. This example reads the background-color from a list + /// item. + /// + /// + /// using CSF.Screenplay.Selenium.Elements; + /// using static CSF.Screenplay.Selenium.PerformableBuilder; + /// + /// readonly ITarget warningItem = new CssSelector("li.warning", "the warning item"); + /// + /// // Within the logic of a custom task, deriving from IPerformableWithResult<string> + /// public async ValueTask<string> PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + /// { + /// // ... other performance logic + /// var bgColor = await actor.PerformAsync(ReadFromTheElement(warningItem).TheCssProperty("background-color"), cancellationToken); + /// // ... other performance logic + /// return bgColor; + /// } + /// + /// + /// + /// public class QuestionQueryBuilder { readonly ITarget target; @@ -19,25 +56,65 @@ public class QuestionQueryBuilder /// /// The name of the attribute from which to read the value /// A performable question + /// + /// + /// This example shows how to use the question to read the + /// title attribute from a button. + /// + /// + /// using CSF.Screenplay.Selenium.Elements; + /// using static CSF.Screenplay.Selenium.PerformableBuilder; + /// + /// readonly ITarget cancelButton = new CssSelector("button.cancel", "the cancel button"); + /// + /// // Within the logic of a custom task, deriving from IPerformableWithResult<string> + /// public async ValueTask<string> PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + /// { + /// // ... other performance logic + /// var title = await actor.PerformAsync(ReadFromTheElement(cancelButton).TheAttribute("title"), cancellationToken); + /// // ... other performance logic + /// return title; + /// } + /// + /// public IPerformableWithResult TheAttribute(string attributeName) => FromQuery(new AttributeQuery(attributeName)); /// /// Gets a performable question which checks the clickability of the element. /// + /// + /// + /// An element is considered 'clickable' if it is both visible in the web browser and it is not disabled. + /// + /// /// A performable question + /// See the class for an example; all the methods to this + /// class follow the same pattern. public IPerformableWithResult TheClickability() => FromQuery(new ClickableQuery()); /// /// Gets a performable question which reads the value of the specified CSS property from the element. /// + /// + /// + /// Note that this is not limited to reading the styling of an element/elements as it was defined in + /// a stylesheet. It reads the live styling of the element as it is when the question executes. + /// Thus, if JavaScript or a class change has altered the styling of the element since it was first rendered + /// on-screen, its up-to-date styling information will be returned by this query. + /// + /// /// The name of the CSS property from which to read the value /// A performable question + /// See the class for an example; all the methods to this + /// class follow the same pattern. public IPerformableWithResult TheCssProperty(string propertyName) => FromQuery(new CssPropertyQuery(propertyName)); /// /// Gets a performable question which reads the pixel location (top-left corner) of the element. /// /// A performable question + /// See the class for an example; all the methods to this + /// class follow the same pattern. public IPerformableWithResult TheLocation() => FromQuery(new LocationQuery()); /// @@ -49,6 +126,8 @@ public class QuestionQueryBuilder /// /// /// A performable question + /// See the class for an example; all the methods to this + /// class follow the same pattern. public IPerformableWithResult> SelectedOptions() => FromQuery(new OptionsQuery(excludeUnselectedOptions : true)); /// @@ -60,6 +139,8 @@ public class QuestionQueryBuilder /// /// /// A performable question + /// See the class for an example; all the methods to this + /// class follow the same pattern. public IPerformableWithResult> UnselectedOptions() => FromQuery(new OptionsQuery(excludeSelectedOptions : true)); /// @@ -71,30 +152,76 @@ public class QuestionQueryBuilder /// /// /// A performable question + /// See the class for an example; all the methods to this + /// class follow the same pattern. public IPerformableWithResult> AllOptions() => FromQuery(new OptionsQuery()); /// /// Gets a performable question which reads the pixel size (width and height) of the element. /// /// A performable question + /// See the class for an example; all the methods to this + /// class follow the same pattern. public IPerformableWithResult TheSize() => FromQuery(new SizeQuery()); /// /// Gets a performable question which reads the text content of the element. /// + /// + /// + /// When reading text from the web browser, this query will trim leading/trailing whitespace. + /// This is because some browsers (Safari) + /// include whitespace at the beginning/end of text read from the browser, which isn't visible to the end user. This is typically the + /// space which is inherent in the markup, but which browsers ignore when actually displaying content. + /// + /// + /// Trimming it by default ensures that Screenplay reproduces functionality reliably cross-browser. + /// If this causes an issue and you would like the leading/trailing whitespace included then use + /// instead. + /// + /// /// A performable question + /// See the class for an example; all the methods to this + /// class follow the same pattern. public IPerformableWithResult TheText() => FromQuery(new TextQuery()); + /// + /// Gets a performable question which reads the text content of the element, preserving leading/trailing whitespace if present. + /// + /// + /// + /// Unlike , this method will not trim any leading/trailing whitespace. + /// Be aware that some browsers (Safari) may include leading/trailing whitespace when reading text, + /// which other WebDriver implementations do not. This can result in inconsistent results when + /// operating cross-browser. If cross-browser consistency is important then consider using + /// instead. + /// + /// + /// A performable question + /// See the class for an example; all the methods to this + /// class follow the same pattern. + public IPerformableWithResult TheTextWithoutTrimmingWhitespace() => FromQuery(new TextQuery(false)); + /// /// Gets a performable question which reads the value of the element. /// + /// + /// + /// This query is only meaningful for elements which may have a value. + /// Examples of these include <input>, <textarea> or <select> elements. + /// + /// /// A performable question + /// See the class for an example; all the methods to this + /// class follow the same pattern. public IPerformableWithResult TheValue() => FromQuery(new ValueQuery()); /// /// Gets a performable question which checks the visibility of the element. /// /// A performable question + /// See the class for an example; all the methods to this + /// class follow the same pattern. public IPerformableWithResult TheVisibility() => FromQuery(new VisibilityQuery(), true); IPerformableWithResult FromQuery(IQuery query, bool doNotThrowOnMissingElement = false) diff --git a/CSF.Screenplay.Selenium/Builders/SetTheValueBuilder.cs b/CSF.Screenplay.Selenium/Builders/SetTheValueBuilder.cs index e565b99c..4d076b99 100644 --- a/CSF.Screenplay.Selenium/Builders/SetTheValueBuilder.cs +++ b/CSF.Screenplay.Selenium/Builders/SetTheValueBuilder.cs @@ -1,6 +1,7 @@ using CSF.Screenplay.Performables; using CSF.Screenplay.Selenium.Actions; using CSF.Screenplay.Selenium.Elements; +using CSF.Screenplay.Selenium.Tasks; namespace CSF.Screenplay.Selenium { diff --git a/CSF.Screenplay.Selenium/Elements/ClassName.cs b/CSF.Screenplay.Selenium/Elements/ClassName.cs index dde37ae3..8da5f4b5 100644 --- a/CSF.Screenplay.Selenium/Elements/ClassName.cs +++ b/CSF.Screenplay.Selenium/Elements/ClassName.cs @@ -5,6 +5,14 @@ namespace CSF.Screenplay.Selenium.Elements /// /// Concrete implementation of which locates elements by their HTML class attribute value. /// + /// + /// + /// This class wraps Selenium's functionality. The purpose is provide a type which derives + /// from , compatible with the Selenium extension's performables. This class also provides opportunity + /// to add a human-readable to the element specification. This optional, but recommended, technique + /// improves the readability of Screenplay reports. + /// + /// public class ClassName : Locator { const string defaultNameFormat = "element(s) with the class '{0}'"; diff --git a/CSF.Screenplay.Selenium/Elements/CssSelector.cs b/CSF.Screenplay.Selenium/Elements/CssSelector.cs index 0c2de351..7d264d6e 100644 --- a/CSF.Screenplay.Selenium/Elements/CssSelector.cs +++ b/CSF.Screenplay.Selenium/Elements/CssSelector.cs @@ -5,6 +5,14 @@ namespace CSF.Screenplay.Selenium.Elements /// /// Concrete implementation of which locates elements by a CSS selector value. /// + /// + /// + /// This class wraps Selenium's functionality. The purpose is provide a type which derives + /// from , compatible with the Selenium extension's performables. This class also provides opportunity + /// to add a human-readable to the element specification. This optional, but recommended, technique + /// improves the readability of Screenplay reports. + /// + /// public class CssSelector : Locator { const string defaultNameFormat = "element(s) matching the selector '{0}'"; diff --git a/CSF.Screenplay.Selenium/Elements/ElementId.cs b/CSF.Screenplay.Selenium/Elements/ElementId.cs index 5c6aad45..5201011c 100644 --- a/CSF.Screenplay.Selenium/Elements/ElementId.cs +++ b/CSF.Screenplay.Selenium/Elements/ElementId.cs @@ -7,6 +7,12 @@ namespace CSF.Screenplay.Selenium.Elements /// /// /// + /// This class wraps Selenium's functionality. The purpose is provide a type which derives + /// from , compatible with the Selenium extension's performables. This class also provides opportunity + /// to add a human-readable to the element specification. This optional, but recommended, technique + /// improves the readability of Screenplay reports. + /// + /// /// In normal circumstances (with valid HTML) this locator should match a maximum of only one element. /// /// diff --git a/CSF.Screenplay.Selenium/Elements/ITarget.cs b/CSF.Screenplay.Selenium/Elements/ITarget.cs index 6c6ea8e5..967d7611 100644 --- a/CSF.Screenplay.Selenium/Elements/ITarget.cs +++ b/CSF.Screenplay.Selenium/Elements/ITarget.cs @@ -3,17 +3,29 @@ namespace CSF.Screenplay.Selenium.Elements { /// - /// A target is an object which may be interacted with by Selenium-based Screenplay performables. + /// A target is an object which represents one or more HTML elements and which may serve as parameters for performables + /// which interact with elements on the web page. /// /// /// - /// Targets roughly correspond to the concept of 'elements' in Selenium WebDriver; the targets abstraction may - /// by implemented by objects which contain one or more Selenium IWebElement instances. + /// See for more information. + /// The ITarget interface unifies two Selenium concepts: instances and the + /// class. + /// Targets correspond to the concept of HTML elements on a web page and may be used as the parameters for + /// which interact with those elements. /// /// - /// Targets might alternatively be implemented by objects which contain selector information, such as a CSS selector, XPath - /// an id attribute. In this scenario the target is conceptually 'a specification for finding elements'. - /// A selector/specification-based target may be used to find elements at runtime, via the method. + /// The and classes wrap native Selenium + /// web element instances. They are concrete references to elements which have been retrieved from the WebDriver. + /// In both of these cases, the & + /// methods will often return references to themselves without making any use of the WebDriver parameter. + /// + /// + /// On the other hand, targets which derive from , such as , , + /// and identify HTML element(s) without being a concrete reference to them. + /// A Locator is conceptually 'a specification for finding elements'. + /// Locator-based targets will use the WebDriver parameter in their and + /// methods, in order to retrieve concrete element references. /// /// public interface ITarget : IHasName diff --git a/CSF.Screenplay.Selenium/Elements/Locator.cs b/CSF.Screenplay.Selenium/Elements/Locator.cs index 08d43e59..17ec8769 100644 --- a/CSF.Screenplay.Selenium/Elements/Locator.cs +++ b/CSF.Screenplay.Selenium/Elements/Locator.cs @@ -4,12 +4,15 @@ namespace CSF.Screenplay.Selenium.Elements { /// - /// An object which provides a locator for Selenium elements. + /// Base type for implementations of which serve as a specification for Selenium elements + /// but which do not reference those elements directly. /// /// /// - /// The Selenium By object is a specification of sorts, which can be used to find HTML elements on a web page. - /// Concrete implementations of this class provide various ways to get a By object. + /// All derived types of this class are Screenplay wrappers (adapters) for the Selenium class. + /// The 'By' class is a specification of sorts, describing how a WebDriver may find an element on a page. + /// Implementations of this type provide that specification, but additionally a , providing + /// human-readable content in Screenplay reports. /// /// public abstract class Locator : ITarget diff --git a/CSF.Screenplay.Selenium/Elements/SeleniumElement.cs b/CSF.Screenplay.Selenium/Elements/SeleniumElement.cs index 506d5b59..b0c3c4a7 100644 --- a/CSF.Screenplay.Selenium/Elements/SeleniumElement.cs +++ b/CSF.Screenplay.Selenium/Elements/SeleniumElement.cs @@ -5,6 +5,13 @@ namespace CSF.Screenplay.Selenium.Elements /// /// An implementation of which represents a single native Selenium HTML element. /// + /// + /// + /// This type of contains a reference to a native Selenium . + /// This class provides that element under the ITarget interface, as well as allowing the developer to specify + /// a . This optional, but recommended, technique facilitates human-readable reporting. + /// + /// public class SeleniumElement : ITarget { const string unknownNameFormat = "an HTML {0} element"; diff --git a/CSF.Screenplay.Selenium/Elements/SeleniumElementCollection.cs b/CSF.Screenplay.Selenium/Elements/SeleniumElementCollection.cs index f74c83c0..ae3ac72f 100644 --- a/CSF.Screenplay.Selenium/Elements/SeleniumElementCollection.cs +++ b/CSF.Screenplay.Selenium/Elements/SeleniumElementCollection.cs @@ -11,7 +11,14 @@ namespace CSF.Screenplay.Selenium.Elements /// /// /// - /// It is perfectly acceptable for this collection to contain only a single element, or even to contain zero elements. + /// This type of contains a collection of , each of which + /// contains a native Selenium . + /// This class provides those elements under the ITarget interface, as well as allowing the developer to specify + /// a for this collection of elements. This optional, but recommended, technique facilitates + /// human-readable reporting. + /// + /// + /// It is perfectly acceptable for an element collection to contain zero elements. /// /// public class SeleniumElementCollection : ITarget, IReadOnlyList diff --git a/CSF.Screenplay.Selenium/Elements/SeleniumElementExtensions.cs b/CSF.Screenplay.Selenium/Elements/SeleniumElementExtensions.cs index 104d968b..c198f245 100644 --- a/CSF.Screenplay.Selenium/Elements/SeleniumElementExtensions.cs +++ b/CSF.Screenplay.Selenium/Elements/SeleniumElementExtensions.cs @@ -9,15 +9,17 @@ namespace CSF.Screenplay.Selenium.Elements public static class SeleniumElementExtensions { /// - /// Gets a instance from the specified element. + /// Gets a Selenium instance from the specified element. /// /// /// - /// A SelectElement is a Selenium object which represents an HTML <select> element, providing - /// convenience functionality for reading and manipulating its state. + /// is a Selenium type which represents an HTML <select> element, + /// wrapping the native . + /// The select element class provides convenience functionality for reading and manipulating the state of the + /// underlying element. /// /// - /// A Selenium element + /// A which should represent an HTML <select> element /// A Selenium select element /// If the element is not a <select> element /// If the element is diff --git a/CSF.Screenplay.Selenium/Elements/TargetExtensions.cs b/CSF.Screenplay.Selenium/Elements/TargetExtensions.cs index fd126cde..ac592a26 100644 --- a/CSF.Screenplay.Selenium/Elements/TargetExtensions.cs +++ b/CSF.Screenplay.Selenium/Elements/TargetExtensions.cs @@ -16,11 +16,32 @@ public static class TargetExtensions /// This method is used with the creation of predicates for /// to wait until the current target instance meets the specified conditions. /// + /// + /// The method is functionally identical to this method; the two overloads are + /// provided just so developers may create a more readable fluent method chain. + /// /// /// The target for which to create the query builder. /// A new instance of . public static QueryPredicatePrototypeBuilder Has(this ITarget target) => new QueryPredicatePrototypeBuilder(target, false); + /// + /// Creates a new for the specified target, which represents a single HTML element. + /// + /// + /// + /// This method is used with the creation of predicates for + /// to wait until the current target instance meets the specified conditions. + /// + /// + /// The method is functionally identical to this method; the two overloads are + /// provided just so developers may create a more readable fluent method chain. + /// + /// + /// The target for which to create the query builder. + /// A new instance of . + public static QueryPredicatePrototypeBuilder Is(this ITarget target) => new QueryPredicatePrototypeBuilder(target, false); + /// /// Creates a new for the specified target, which represents a collection of HTML elements. /// @@ -29,9 +50,30 @@ public static class TargetExtensions /// This method is used with the creation of predicates for /// to wait until every element expsed by the current target instance meets the specified conditions. /// + /// + /// The method is functionally identical to this method; the two overloads are + /// provided just so developers may create a more readable fluent method chain. + /// /// /// The target for which to create the query builder. /// A new instance of . public static QueryPredicatePrototypeBuilder AllHave(this ITarget target) => new QueryPredicatePrototypeBuilder(target, true); + + /// + /// Creates a new for the specified target, which represents a collection of HTML elements. + /// + /// + /// + /// This method is used with the creation of predicates for + /// to wait until every element expsed by the current target instance meets the specified conditions. + /// + /// + /// The method is functionally identical to this method; the two overloads are + /// provided just so developers may create a more readable fluent method chain. + /// + /// + /// The target for which to create the query builder. + /// A new instance of . + public static QueryPredicatePrototypeBuilder AreAll(this ITarget target) => new QueryPredicatePrototypeBuilder(target, true); } } \ No newline at end of file diff --git a/CSF.Screenplay.Selenium/Elements/XPath.cs b/CSF.Screenplay.Selenium/Elements/XPath.cs index 2570846e..80b17af7 100644 --- a/CSF.Screenplay.Selenium/Elements/XPath.cs +++ b/CSF.Screenplay.Selenium/Elements/XPath.cs @@ -5,6 +5,14 @@ namespace CSF.Screenplay.Selenium.Elements /// /// Concrete implementation of which locates elements using an XPath query. /// + /// + /// + /// This class wraps Selenium's functionality. The purpose is provide a type which derives + /// from , compatible with the Selenium extension's performables. This class also provides opportunity + /// to add a human-readable to the element specification. This optional, but recommended, technique + /// improves the readability of Screenplay reports. + /// + /// public class XPath : Locator { const string defaultNameFormat = "element(s) matching the XPath '{0}'"; diff --git a/CSF.Screenplay.Selenium/NamedScript.cs b/CSF.Screenplay.Selenium/NamedScript.cs index 1d02c4d2..94ecb09e 100644 --- a/CSF.Screenplay.Selenium/NamedScript.cs +++ b/CSF.Screenplay.Selenium/NamedScript.cs @@ -4,24 +4,7 @@ namespace CSF.Screenplay.Selenium /// /// A model representing a pre-written piece of executable JavaScript, which accepts no parameters and which returns no result. /// - /// - /// - /// Developers are encouraged to represent JavaScripts that they wish to execute in the web browser using this type (and related types, see "See Also"). - /// Where the script accepts parameters and/or returns a result, choose a type which specifies appropriate generic type arguments for those parameters - /// and returned type. - /// - /// - /// For scripts which are created in this manner, getting an or for those - /// scripts is very easy and type safe. Use the ExecuteAScript method from the and the compiler will - /// use the generic type arguments of this type to select an appropriate overload which provides type-safety for specifying the parameter - /// values and the return type, as appropriate. - /// - /// - /// Another benefit is that scripts defined and stored in instances of this type are reusable and easily catalogued. See the helper - /// class for an example of a catalogue of script objects which may be executed. Once again, developers are encouraged to follow this pattern in their - /// own applications/tests which use Screenplay & Selenium. - /// - /// + /// /// /// /// @@ -29,14 +12,7 @@ namespace CSF.Screenplay.Selenium /// /// /// - /// - /// - /// - /// - /// - /// - /// - /// + /// public class NamedScript : NamedScriptBasis { /// @@ -52,24 +28,8 @@ public NamedScript(string scriptBody, string name) : base(scriptBody, name) {} /// A model representing a pre-written piece of executable JavaScript, which accepts 1 parameter and which returns no result. /// /// - /// - /// Whilst the generic type parameters of this type have no direct bearing upon its members (such as properties), they are very useful when - /// combined with the builder methods and its many overloads. - /// Developers are encouraged to represent JavaScripts that they wish to execute in the web browser using this type (and related types, see "See Also"). - /// Where the script accepts parameters and/or returns a result, choose a type which specifies appropriate generic type arguments for those parameters - /// and returned type. - /// - /// - /// For scripts which are created in this manner, getting an or for those - /// scripts is very easy and type safe. Use the ExecuteAScript method from the and the compiler will - /// use the generic type arguments of this type to select an appropriate overload which provides type-safety for specifying the parameter - /// values and the return type, as appropriate. - /// - /// - /// Another benefit is that scripts defined and stored in instances of this type are reusable and easily catalogued. See the helper - /// class for an example of a catalogue of script objects which may be executed. Once again, developers are encouraged to follow this pattern in their - /// own applications/tests which use Screenplay & Selenium. - /// + /// + /// /// /// The type of the first parameter /// @@ -79,14 +39,7 @@ public NamedScript(string scriptBody, string name) : base(scriptBody, name) {} /// /// /// - /// - /// - /// - /// - /// - /// - /// - /// + /// public class NamedScript : NamedScriptBasis { /// @@ -96,7 +49,10 @@ public NamedScript(string scriptBody, string name) : base(scriptBody, name) {} /// /// A model representing a pre-written piece of executable JavaScript, which accepts 2 parameters and which returns no result. /// - /// + /// + /// + /// + /// /// The type of the first parameter /// The type of the second parameter /// @@ -106,14 +62,7 @@ public NamedScript(string scriptBody, string name) : base(scriptBody, name) {} /// /// /// - /// - /// - /// - /// - /// - /// - /// - /// + /// public class NamedScript : NamedScriptBasis { /// @@ -123,7 +72,10 @@ public NamedScript(string scriptBody, string name) : base(scriptBody, name) {} /// /// A model representing a pre-written piece of executable JavaScript, which accepts 3 parameters and which returns no result. /// - /// + /// + /// + /// + /// /// The type of the first parameter /// The type of the second parameter /// The type of the third parameter @@ -134,14 +86,7 @@ public NamedScript(string scriptBody, string name) : base(scriptBody, name) {} /// /// /// - /// - /// - /// - /// - /// - /// - /// - /// + /// public class NamedScript : NamedScriptBasis { /// @@ -151,7 +96,10 @@ public NamedScript(string scriptBody, string name) : base(scriptBody, name) {} /// /// A model representing a pre-written piece of executable JavaScript, which accepts 4 parameters and which returns no result. /// - /// + /// + /// + /// + /// /// The type of the first parameter /// The type of the second parameter /// The type of the third parameter @@ -163,14 +111,7 @@ public NamedScript(string scriptBody, string name) : base(scriptBody, name) {} /// /// /// - /// - /// - /// - /// - /// - /// - /// - /// + /// public class NamedScript : NamedScriptBasis { /// @@ -180,7 +121,10 @@ public NamedScript(string scriptBody, string name) : base(scriptBody, name) {} /// /// A model representing a pre-written piece of executable JavaScript, which accepts 5 parameters and which returns no result. /// - /// + /// + /// + /// + /// /// The type of the first parameter /// The type of the second parameter /// The type of the third parameter @@ -193,14 +137,7 @@ public NamedScript(string scriptBody, string name) : base(scriptBody, name) {} /// /// /// - /// - /// - /// - /// - /// - /// - /// - /// + /// public class NamedScript : NamedScriptBasis { /// @@ -210,7 +147,10 @@ public NamedScript(string scriptBody, string name) : base(scriptBody, name) {} /// /// A model representing a pre-written piece of executable JavaScript, which accepts 6 parameters and which returns no result. /// - /// + /// + /// + /// + /// /// The type of the first parameter /// The type of the second parameter /// The type of the third parameter @@ -224,14 +164,7 @@ public NamedScript(string scriptBody, string name) : base(scriptBody, name) {} /// /// /// - /// - /// - /// - /// - /// - /// - /// - /// + /// public class NamedScript : NamedScriptBasis { /// @@ -241,7 +174,10 @@ public NamedScript(string scriptBody, string name) : base(scriptBody, name) {} /// /// A model representing a pre-written piece of executable JavaScript, which accepts 7 parameters and which returns no result. /// - /// + /// + /// + /// + /// /// The type of the first parameter /// The type of the second parameter /// The type of the third parameter @@ -256,14 +192,7 @@ public NamedScript(string scriptBody, string name) : base(scriptBody, name) {} /// /// /// - /// - /// - /// - /// - /// - /// - /// - /// + /// public class NamedScript : NamedScriptBasis { /// diff --git a/CSF.Screenplay.Selenium/NamedScriptWithResult.cs b/CSF.Screenplay.Selenium/NamedScriptWithResult.cs index 59c512f1..55794274 100644 --- a/CSF.Screenplay.Selenium/NamedScriptWithResult.cs +++ b/CSF.Screenplay.Selenium/NamedScriptWithResult.cs @@ -3,16 +3,12 @@ namespace CSF.Screenplay.Selenium /// /// A model representing a pre-written piece of executable JavaScript, which accepts no parameters, but which is expected to return a result. /// - /// + /// + /// + /// + /// /// The expected result/return type from the script - /// - /// - /// - /// - /// - /// - /// - /// + /// /// /// /// @@ -34,17 +30,13 @@ public NamedScriptWithResult(string scriptBody, string name) : base(scriptBody, /// /// A model representing a pre-written piece of executable JavaScript, which accepts 1 parameter, but which is expected to return a result. /// - /// + /// + /// + /// + /// /// The type of the first parameter /// The expected result/return type from the script - /// - /// - /// - /// - /// - /// - /// - /// + /// /// /// /// @@ -61,18 +53,14 @@ public NamedScriptWithResult(string scriptBody, string name) : base(scriptBody, /// /// A model representing a pre-written piece of executable JavaScript, which accepts 2 parameters, but which is expected to return a result. /// - /// + /// + /// + /// + /// /// The type of the first parameter /// The type of the second parameter /// The expected result/return type from the script - /// - /// - /// - /// - /// - /// - /// - /// + /// /// /// /// @@ -89,19 +77,15 @@ public NamedScriptWithResult(string scriptBody, string name) : base(scriptBody, /// /// A model representing a pre-written piece of executable JavaScript, which accepts 3 parameters, but which is expected to return a result. /// - /// + /// + /// + /// + /// /// The type of the first parameter /// The type of the second parameter /// The type of the third parameter /// The expected result/return type from the script - /// - /// - /// - /// - /// - /// - /// - /// + /// /// /// /// @@ -118,20 +102,16 @@ public NamedScriptWithResult(string scriptBody, string name) : base(scriptBody, /// /// A model representing a pre-written piece of executable JavaScript, which accepts 4 parameters, but which is expected to return a result. /// - /// + /// + /// + /// + /// /// The type of the first parameter /// The type of the second parameter /// The type of the third parameter /// The type of the fourth parameter /// The expected result/return type from the script - /// - /// - /// - /// - /// - /// - /// - /// + /// /// /// /// @@ -148,21 +128,17 @@ public NamedScriptWithResult(string scriptBody, string name) : base(scriptBody, /// /// A model representing a pre-written piece of executable JavaScript, which accepts 5 parameters, but which is expected to return a result. /// - /// + /// + /// + /// + /// /// The type of the first parameter /// The type of the second parameter /// The type of the third parameter /// The type of the fourth parameter /// The type of the fifth parameter /// The expected result/return type from the script - /// - /// - /// - /// - /// - /// - /// - /// + /// /// /// /// @@ -179,7 +155,10 @@ public NamedScriptWithResult(string scriptBody, string name) : base(scriptBody, /// /// A model representing a pre-written piece of executable JavaScript, which accepts 6 parameters, but which is expected to return a result. /// - /// + /// + /// + /// + /// /// The type of the first parameter /// The type of the second parameter /// The type of the third parameter @@ -187,14 +166,7 @@ public NamedScriptWithResult(string scriptBody, string name) : base(scriptBody, /// The type of the fifth parameter /// The type of the sixth parameter /// The expected result/return type from the script - /// - /// - /// - /// - /// - /// - /// - /// + /// /// /// /// @@ -211,7 +183,10 @@ public NamedScriptWithResult(string scriptBody, string name) : base(scriptBody, /// /// A model representing a pre-written piece of executable JavaScript, which accepts 7 parameters, but which is expected to return a result. /// - /// + /// + /// + /// + /// /// The type of the first parameter /// The type of the second parameter /// The type of the third parameter @@ -220,14 +195,7 @@ public NamedScriptWithResult(string scriptBody, string name) : base(scriptBody, /// The type of the sixth parameter /// The type of the seventh parameter /// The expected result/return type from the script - /// - /// - /// - /// - /// - /// - /// - /// + /// /// /// /// diff --git a/CSF.Screenplay.Selenium/NamedScriptsCommonDocs.xml b/CSF.Screenplay.Selenium/NamedScriptsCommonDocs.xml new file mode 100644 index 00000000..10b1a351 --- /dev/null +++ b/CSF.Screenplay.Selenium/NamedScriptsCommonDocs.xml @@ -0,0 +1,53 @@ + + + + + + + Developers are encouraged to store JavaScripts in their apps/tests as instances of this type and other + related types (see the "See Also" section). + This improves the reusability of the script, improves its presence in Screenplay reports and provides type safety. + For more information, see . + + + For scripts which are created in this manner, getting an or for those + scripts is very easy and type safe. Use the ExecuteAScript method from the and + the compiler will use generic type inference to select an appropriate overload. + The parameters and return type will then be type-safe and of the appropriate number. + + + Another benefit is that scripts defined and stored in instances of this type are reusable and easily catalogued. See the helper + class for an example of a catalogue of script objects which may be executed. Once again, developers are encouraged to follow this pattern in their + own applications/tests which use Screenplay & Selenium. + + + + + Whilst the generic type parameters of this type have no direct bearing upon its members (such as properties), they are very useful when + combined with the many overloads of the builder method ExecuteAScript in the class. + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CSF.Screenplay.Selenium/NamedUri.cs b/CSF.Screenplay.Selenium/NamedUri.cs index 364446dd..9269ce1c 100644 --- a/CSF.Screenplay.Selenium/NamedUri.cs +++ b/CSF.Screenplay.Selenium/NamedUri.cs @@ -9,12 +9,29 @@ namespace CSF.Screenplay.Selenium /// /// This model allows for the association of human-readable names with URIs. This is most useful when making use /// of Screenplay reports, where the human-readable name can be used to describe the URI in a more user-friendly way. + /// Named Uris are used with the and performables + /// to facilitate direct web browser navigation. /// /// - /// Note the inclusion of implicit conversion operators which allow for seamless conversion from and/or - /// instances to instances of this type. + /// Note the inclusion of implicit conversion operators which allow for seamless conversion from and/or + /// instances to instances of this type. It is not recommended to use them, though, as neither + /// supplies a name. This would mean that the naked Uri would appear in reports, instead of a human-readable name. /// + /// + /// If you have a need to switch environments at runtime, consider specifying named Uris using relative Uri fragments. + /// This may be combined with the ability to rebase the relative Uri onto + /// a base Uri at runtime. + /// This could be useful in testing, for example, whereby the same test suite must be run against a number of environments: + /// + /// + /// Locally, on a developer's computer, with a base Uri such as https://localhost:8080/ + /// On a testing environment, with a base Uri such as https://testing.example.com/ + /// On a staging environment, with a base Uri such as https://staging.example.com/ + /// /// + /// + /// + /// public sealed class NamedUri : IHasName { /// diff --git a/CSF.Screenplay.Selenium/PerformableBuilder.general.cs b/CSF.Screenplay.Selenium/PerformableBuilder.general.cs index ff984026..19004ebf 100644 --- a/CSF.Screenplay.Selenium/PerformableBuilder.general.cs +++ b/CSF.Screenplay.Selenium/PerformableBuilder.general.cs @@ -129,26 +129,9 @@ public static partial class PerformableBuilder /// /// Gets a performable action which clears the local storage for the current domain. /// - /// - /// - /// This method gets a performable which will throw an exception if the WebDriver does not support the execution of JavaScript. - /// To avoid this, consider using instead. - /// - /// /// A performable action public static IPerformable ClearLocalStorage() => new ClearLocalStorage(); - /// - /// Gets a performable action which clears the local storage for the current domain if the WebDriver supports it. - /// - /// - /// - /// This method gets a performable which will do nothing if the WebDriver does not support execution of JavaScript. - /// - /// - /// A performable action - public static IPerformable ClearLocalStorageIfSupported() => new ClearLocalStorage(false); - /// /// Gets a builder which will create a performable question that waits until a predicate returns a successful result. /// diff --git a/CSF.Screenplay.Selenium/Queries/TextQuery.cs b/CSF.Screenplay.Selenium/Queries/TextQuery.cs index b5294497..178ba82c 100644 --- a/CSF.Screenplay.Selenium/Queries/TextQuery.cs +++ b/CSF.Screenplay.Selenium/Queries/TextQuery.cs @@ -5,6 +5,27 @@ namespace CSF.Screenplay.Selenium.Queries /// /// A query to get the text content of a Selenium element. /// + /// + /// + /// When reading text from the web browser, depending upon the whitespace-trimming parameter, this + /// query will or will not trim leading/trailing whitespace. + /// This functionality is present because some browsers (Safari) include whitespace at the beginning/end + /// of text read from the browser which isn't visible to the end user. This is typically the + /// space which is inherent in the markup, but which browsers ignore when actually displaying content. + /// Other WebDriver implementations do not include this leading/trailing whitespace. + /// + /// + /// When whitespace is trimmed, this provides a more consistent cross-browser experience when reading + /// the text of an element. The consequence is that sometimes, when reading leading/trailing whitespace + /// is intended, we must pass a different parameter value. + /// + /// + /// + /// + /// + /// + /// + /// public class TextQuery : IQuery { readonly bool trimWhitespace; diff --git a/CSF.Screenplay.Selenium/Questions/ElementCollectionQuery.cs b/CSF.Screenplay.Selenium/Questions/ElementCollectionQuery.cs index 4a0574de..cd30c234 100644 --- a/CSF.Screenplay.Selenium/Questions/ElementCollectionQuery.cs +++ b/CSF.Screenplay.Selenium/Questions/ElementCollectionQuery.cs @@ -10,10 +10,41 @@ namespace CSF.Screenplay.Selenium.Questions { /// - /// An implementation of which gets the value from an - /// upon each item within a collection of Selenium elements and returns their results. + /// A question which reads or observes the state from a collection of HTML elements and returns the results. /// /// The type of the result returned by the query. + /// + /// + /// Use this question via the builder method . + /// The builder will then guide you through inspecting the state of + /// . The corresponding state information from + /// each of the elements which are found by the target will be returned as the result of this question. + /// Crucially, to decide which piece of information to retrieve from the target elements, you will build and use + /// . + /// + /// + /// + /// + /// + /// This example gets the text of every list item which has the class todo. + /// + /// + /// using CSF.Screenplay.Selenium.Elements; + /// using static CSF.Screenplay.Selenium.PerformableBuilder; + /// + /// readonly ITarget todoItems = new CssSelector("li.todo", "the todo items"); + /// + /// // Within the logic of a custom task, deriving from IPerformableWithResult<IReadOnlyList<string>> + /// public async ValueTask<IReadOnlyList<string>> PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + /// { + /// // ... other performance logic + /// var texts = await actor.PerformAsync(ReadFromTheCollectionOfElements(todoItems).Text(), cancellationToken); + /// // ... other performance logic + /// return texts; + /// } + /// + /// + /// public class ElementCollectionQuery : IElementCollectionPerformableWithResult { readonly IQuery query; diff --git a/CSF.Screenplay.Selenium/Questions/ElementQueryDocs.xml b/CSF.Screenplay.Selenium/Questions/ElementQueryDocs.xml new file mode 100644 index 00000000..ac5474a6 --- /dev/null +++ b/CSF.Screenplay.Selenium/Questions/ElementQueryDocs.xml @@ -0,0 +1,41 @@ + + + + + + + This class is not a complete , as it relies + upon shared logic to retrieve the which it queries. + It has this in common with a number of Screenplay + questions in the Selenium Plugin which observe a single element, those which derive from + . In order for this class to be used as a full-fledged + , an instance of this type must be wrapped + within an instance of . + The adapter class provides the shared boilerplate logic which provides access to the Selenium Element. + Note that the builder method(s) which create instances of this type include the 'wrap within an adapter' + logic. Normal usage of this performable, when creating it from a builder, does not need to be + concerned with this factor. + + + + + + This class is not a complete , as it relies + upon shared logic to retrieve the which it queries. + It has this in common with a number of Screenplay + questions in the Selenium Plugin which observe collections of elements, those which derive from + . In order for this class to be used as a full-fledged + , an instance of this type must be wrapped + within an instance of . + The adapter class provides the shared boilerplate logic which provides access to the Selenium Element Collection. + Note that the builder method(s) which create instances of this type include the 'wrap within an adapter' + logic. Normal usage of this performable, when creating it from a builder, does not need to be + concerned with this factor. + + + + \ No newline at end of file diff --git a/CSF.Screenplay.Selenium/Questions/ExecuteJavaScriptAndGetResult.cs b/CSF.Screenplay.Selenium/Questions/ExecuteJavaScriptAndGetResult.cs index ff13b6e0..b4080c3c 100644 --- a/CSF.Screenplay.Selenium/Questions/ExecuteJavaScriptAndGetResult.cs +++ b/CSF.Screenplay.Selenium/Questions/ExecuteJavaScriptAndGetResult.cs @@ -5,15 +5,58 @@ namespace CSF.Screenplay.Selenium.Questions { /// - /// An action which executes some arbitrary JavaScript in the web browser and gets a result. + /// A question which executes some arbitrary JavaScript in the web browser and returns the result returned + /// by that script. /// /// /// - /// This performable expects a result from the script. - /// If the script does not return a result then use instead. + /// The best way to use this action is via the builder method + /// or one of its + /// same-named overloads. + /// The class, and its counterparts with additional generic + /// type parameters, provide a manner in which scripts to be executed by this action may be stored within + /// the application or test logic, providing type safety for their parameters and return value. + /// + /// + /// Performing this action, as an actor which has the ability, sends the body of the + /// script and its parameter values to the web browser and executes it. This is roughly equivalent to a user + /// opening the developer tools Console, typing the script and its parameters there, and pressing enter, + /// then reading the result which is returned by the console. + /// This action returns the from the script back to the . If the result is unimportant + /// or you wish to execute a script which does not return a result then use instead. + /// + /// + /// Within the script body, any parameters are accessible via the arguments object. That object exposes the + /// parameter values by their zero-based index (position) in the parameters list. So, a script which takes three + /// parameters will expose these to the script as arguments[0], arguments[1] and + /// arguments[2], respectively for the first, second and third parameter values. /// /// + /// + /// + /// In this example, the script will return the integer value 10 to the performance. + /// + /// + /// using static CSF.Screenplay.Selenium.PerformableBuilder; + /// + /// var addFour = new NamedScriptWithResult<int, int>("4 + arguments[0]", "add four"); + /// + /// // Within the logic of a custom task, deriving from IPerformableWithResult<int> + /// public async ValueTask<int> PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + /// { + /// return await actor.PerformAsync(ExecuteAScript(addFour, 6), cancellationToken); + /// } + /// + /// /// The expected type of the result from the script + /// + /// + /// + /// + /// + /// + /// + /// public class ExecuteJavaScriptAndGetResult : IPerformableWithResult, ICanReport { readonly string script; diff --git a/CSF.Screenplay.Selenium/Questions/FilterElements.cs b/CSF.Screenplay.Selenium/Questions/FilterElements.cs index 71a450c9..ecc97d6e 100644 --- a/CSF.Screenplay.Selenium/Questions/FilterElements.cs +++ b/CSF.Screenplay.Selenium/Questions/FilterElements.cs @@ -8,17 +8,67 @@ namespace CSF.Screenplay.Selenium.Questions { /// - /// A performable question which filters a collection of elements by specified criteria. + /// A question which filters a collection of elements by specified criteria, getting a new collection + /// which contains only those which match. /// /// /// + /// Use this question via the builder method . + /// The builder will then guide you through specifying the + /// criteria by which you wish to filter those elements. There are two primary routes by which to do this, + /// which depend upon which overload of ForThoseWhich you choose. + /// + /// + /// You may specify the criteria within a Specification class. That is a class which implements + /// ISpecificationFunction<SeleniumElement>, from + /// the CSF.Specifications package. + /// This allows you to pass just a variable into the builder, containing an instance of that specification. + /// This is the recommended technique, as the specification class becomes a reusable and first-class named + /// part of the Screenplay. + /// When using this technique, use the + /// overload. + /// + /// + /// Alternatively, you may specify the criteria ad-hoc within the filter-elements builder. + /// To use this technique, use the + /// + /// overload. You must specify a function which interrogates one or more aspects of the element, specifying each + /// criterion individually. This approach allows fast specification of criteria, but at the cost of reusability. + /// + /// /// Note that this question is evaluated with an already-in-memory collection of . - /// It does not interact with the web browser/web driver directly. - /// This could lead to unexpected impacts upon performance. + /// However, each criterion that is evaluated may cause communication with the WebDriver, for each element in the source collection. + /// This nested loop could lead to unexpected impacts upon performance, particularly with larger collections of elements + /// or where the WebDriver is hosted remotely. /// If you are able to find the elements you want using an implementation of , then prefer - /// using instead, which will query the web driver directly for only the elements you want. + /// using instead. Finding elements performs far better than this question. + /// On the other hand, this question offers far greater power in the creation of custom criteria. /// /// + /// + /// + /// This example filters a collection of items in a todo list, only for those with red background colors. + /// + /// + /// using CSF.Screenplay.Selenium; + /// using CSF.Screenplay.Selenium.Elements; + /// using static CSF.Screenplay.Selenium.PerformableBuilder; + /// + /// readonly ITarget todoListItems = new CssSelector("ul#todo li", "the to-do list items"); + /// + /// // Within the logic of a custom task, deriving from IPerformableWithResult<SeleniumElementCollection> + /// public async ValueTask<SeleniumElementCollection> PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + /// { + /// var elements = await actor.PerformAsync(FindElementsOnThePage().WhichMatch(todoListItems), cancellationToken); + /// var redElements = await actor.PerformAsync(Filter(elements) + /// .ForThoseWhich(q => q.CssProperty("background-color", + /// x => x == Colors.RED)), + /// cancellationToken); + /// return redElements; + /// } + /// + /// + /// public class FilterElements : IPerformableWithResult, ICanReport { readonly IReadOnlyCollection elements; diff --git a/CSF.Screenplay.Selenium/Questions/FindElement.cs b/CSF.Screenplay.Selenium/Questions/FindElement.cs index 7f7319ca..4ad27f39 100644 --- a/CSF.Screenplay.Selenium/Questions/FindElement.cs +++ b/CSF.Screenplay.Selenium/Questions/FindElement.cs @@ -7,23 +7,51 @@ namespace CSF.Screenplay.Selenium.Questions { /// - /// Represents a question that finds a single element within a specified target. + /// A question which searches for an HTML element that matches some criteria, optionally within a specified target, + /// returning the element it finds as a . /// /// /// - /// This question is used to find a single element within a target. If more than one element would have been matched by the specified locator - /// then the first matching element (only) will be returned. + /// 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 + /// , 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 + /// instead. /// /// - /// Consumers are encouraged to specify a human-readable name for the collection of elements which are found, this will be - /// kept with the returned collection of elements and will be visible in reports. + /// The criteria by which an element is searched by this question is a class that derives from . + /// Particularly useful are the , and + /// locators. is less likely to be useful, as it should only ever match a single element per + /// web page. /// + /// + /// + /// /// - /// Note that if no element is found, which is a descendent of the target and which is matched by the locator, then the - /// method of this question will return - /// a value task of a reference. + /// This example gets a within the list which has the ID todo which has the class + /// urgent. /// - /// + /// + /// using CSF.Screenplay.Selenium.Elements; + /// using static CSF.Screenplay.Selenium.PerformableBuilder; + /// + /// readonly ITarget todoList = new CssSelector("ul#todo", "the to-do list"); + /// readonly Locator urgent = new ClassName("urgent", "the urgent item"); + /// + /// // Within the logic of a custom task, deriving from IPerformableWithResult<SeleniumElement> + /// public async ValueTask<SeleniumElement> PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + /// { + /// // ... other performance logic + /// var element = await actor.PerformAsync(FindAnElementWithin(todoList).WhichMatches(urgent), cancellationToken); + /// // ... other performance logic + /// return element; + /// } + /// + /// + /// + /// public class FindElement : ISingleElementPerformableWithResult { readonly string elementsName; diff --git a/CSF.Screenplay.Selenium/Questions/FindElements.cs b/CSF.Screenplay.Selenium/Questions/FindElements.cs index 1334d302..304840c9 100644 --- a/CSF.Screenplay.Selenium/Questions/FindElements.cs +++ b/CSF.Screenplay.Selenium/Questions/FindElements.cs @@ -7,18 +7,50 @@ namespace CSF.Screenplay.Selenium.Questions { /// - /// Represents a question that finds a collection of elements within a specified target. + /// A question which searches for HTML elements that matche some criteria, optionally within a specified target, + /// returning the results as a . /// /// /// - /// This question is used to find a collection of elements within a target. The matched/returned elements may be filtered - /// by a if one is specified, otherwise all descendent elements of the target will be returned. + /// 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 + /// 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. /// /// - /// Consumers are encouraged to specify a human-readable name for the collection of elements which are found, this will be - /// kept with the returned collection of elements and will be visible in reports. + /// The criteria by which an element is searched by this question is a class that derives from . + /// Particularly useful are the , and + /// locators. is less likely to be useful, as it should only ever match a single element per + /// web page. /// + /// /// + /// + /// + /// This example gets a which contains every element in the list which has + /// the ID todo, which also the class low_priority. + /// + /// + /// using CSF.Screenplay.Selenium.Elements; + /// using static CSF.Screenplay.Selenium.PerformableBuilder; + /// + /// readonly ITarget todoList = new CssSelector("ul#todo", "the to-do list"); + /// readonly Locator lowPriority = new ClassName("low_priority", "the low priority items"); + /// + /// // Within the logic of a custom task, deriving from IPerformableWithResult<SeleniumElementCollection> + /// public async ValueTask<SeleniumElementCollection> PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + /// { + /// // ... other performance logic + /// var elements = await actor.PerformAsync(FindElementsWithin(todoList).WhichMatch(lowPriority), cancellationToken); + /// // ... other performance logic + /// return elements; + /// } + /// + /// + /// + /// public class FindElements : ISingleElementPerformableWithResult { readonly string elementsName; diff --git a/CSF.Screenplay.Selenium/Questions/GetWindowTitle.cs b/CSF.Screenplay.Selenium/Questions/GetWindowTitle.cs index 9aa7df91..5894e3bf 100644 --- a/CSF.Screenplay.Selenium/Questions/GetWindowTitle.cs +++ b/CSF.Screenplay.Selenium/Questions/GetWindowTitle.cs @@ -4,8 +4,29 @@ namespace CSF.Screenplay.Selenium.Questions { /// - /// A Screenplay question to get the title of the current browser window. + /// A question which reads and returns the title of the current browser window. /// + /// + /// + /// The best way to use this action is via the builder method . + /// + /// + /// This very simple question gets the text which is displayed as the browser window (or tab) title. + /// In a normal web page, that would be what's contained in the <title> element. + /// + /// + /// + /// + /// using static CSF.Screenplay.Selenium.PerformableBuilder; + /// + /// // Within the logic of a custom task, deriving from IPerformableWithResult<string> + /// public async ValueTask<string> PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + /// { + /// return await actor.PerformAsync(ReadTheWindowTitle(), cancellationToken); + /// } + /// + /// + /// public class GetWindowTitle : IPerformableWithResult, ICanReport { /// diff --git a/CSF.Screenplay.Selenium/Questions/ISingleElementPerformableWithResult.cs b/CSF.Screenplay.Selenium/Questions/ISingleElementPerformableWithResult.cs index 0f42a597..9a5cd427 100644 --- a/CSF.Screenplay.Selenium/Questions/ISingleElementPerformableWithResult.cs +++ b/CSF.Screenplay.Selenium/Questions/ISingleElementPerformableWithResult.cs @@ -1,8 +1,6 @@ using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using CSF.Screenplay.Selenium.Actions; using CSF.Screenplay.Selenium.Elements; using OpenQA.Selenium; diff --git a/CSF.Screenplay.Selenium/Questions/SingleElementQuery.cs b/CSF.Screenplay.Selenium/Questions/SingleElementQuery.cs index ef7e1731..0ae9372a 100644 --- a/CSF.Screenplay.Selenium/Questions/SingleElementQuery.cs +++ b/CSF.Screenplay.Selenium/Questions/SingleElementQuery.cs @@ -8,10 +8,41 @@ namespace CSF.Screenplay.Selenium.Questions { /// - /// An implementation of which gets the value from an - /// upon a single Selenium element and returns its result. + /// A question which reads or observes the state of a single HTML element and returns the result. /// /// The type of the result returned by the query. + /// + /// + /// Use this question via the builder method . + /// The builder will then guide you through inspecting the state of + /// . That state information will be returned + /// as the result of this question. + /// Crucially, to decide which piece of information to retrieve from the target, you will build and use + /// . + /// + /// + /// + /// + /// + /// This example gets the background color of a list item which has the class warning. + /// + /// + /// using CSF.Screenplay.Selenium.Elements; + /// using static CSF.Screenplay.Selenium.PerformableBuilder; + /// + /// readonly ITarget warningItem = new CssSelector("li.warning", "the warning item"); + /// + /// // Within the logic of a custom task, deriving from IPerformableWithResult<string> + /// public async ValueTask<string> PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + /// { + /// // ... other performance logic + /// var bgColor = await actor.PerformAsync(ReadFromTheElement(warningItem).TheCssProperty("background-color"), cancellationToken); + /// // ... other performance logic + /// return bgColor; + /// } + /// + /// + /// public class SingleElementQuery : ISingleElementPerformableWithResult { readonly IQuery query; diff --git a/CSF.Screenplay.Selenium/Questions/TakeScreenshot.cs b/CSF.Screenplay.Selenium/Questions/TakeScreenshot.cs index b6324079..a69af7f1 100644 --- a/CSF.Screenplay.Selenium/Questions/TakeScreenshot.cs +++ b/CSF.Screenplay.Selenium/Questions/TakeScreenshot.cs @@ -8,6 +8,40 @@ namespace CSF.Screenplay.Selenium.Questions /// /// A question which represents an actor taking a screenshot of the current web page. /// + /// + /// + /// The best way to use this action is via the builder method + /// or . The second method will not raise an exception + /// if the browser is incapable of providing screenshots. + /// + /// + /// This question instructs the WebDriver to capture the currently-visible area of the browser window (the viewport) + /// in a screenshot, and return that screenshot to the . + /// + /// + /// Since the most common thing to do with a object is to save it to disk, consumers should + /// consider using the task instead, via either the builder methods + /// or + /// . These deal with both the taking and saving of + /// the screenshot in a single step. + /// + /// + /// + /// + /// using OpenQA.Selenium; + /// using static CSF.Screenplay.Selenium.PerformableBuilder; + /// + /// // Within the logic of a custom task, deriving from IPerformableWithResult<Screenshot> + /// public async ValueTask<Screenshot> PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + /// { + /// return await actor.PerformAsync(TakeAScreenshot(), cancellationToken); + /// } + /// + /// + /// + /// + /// + /// public class TakeScreenshot : IPerformableWithResult, ICanReport { readonly bool throwIfUnsupported; diff --git a/CSF.Screenplay.Selenium/Reporting/OptionsFormatter.cs b/CSF.Screenplay.Selenium/Reporting/OptionsFormatter.cs index 219a843e..a69625f1 100644 --- a/CSF.Screenplay.Selenium/Reporting/OptionsFormatter.cs +++ b/CSF.Screenplay.Selenium/Reporting/OptionsFormatter.cs @@ -5,7 +5,7 @@ namespace CSF.Screenplay.Selenium.Reporting { /// - /// A value formatter for collections of for reporting. + /// An for collections of for reporting. /// public class OptionsFormatter : IValueFormatter { diff --git a/CSF.Screenplay.Selenium/Reporting/ScreenshotFormatter.cs b/CSF.Screenplay.Selenium/Reporting/ScreenshotFormatter.cs index a32e66cf..fabd69b4 100644 --- a/CSF.Screenplay.Selenium/Reporting/ScreenshotFormatter.cs +++ b/CSF.Screenplay.Selenium/Reporting/ScreenshotFormatter.cs @@ -4,7 +4,7 @@ namespace CSF.Screenplay.Selenium.Reporting { /// - /// A value formatter for Selenium screenshot objects, for reporting. + /// An for Selenium objects, for reporting. /// public class ScreenshotFormatter : IValueFormatter { diff --git a/CSF.Screenplay.Selenium/Resources/ScriptResources.cs b/CSF.Screenplay.Selenium/Resources/ScriptResources.cs index 37848a88..8e621b1f 100644 --- a/CSF.Screenplay.Selenium/Resources/ScriptResources.cs +++ b/CSF.Screenplay.Selenium/Resources/ScriptResources.cs @@ -3,7 +3,7 @@ namespace CSF.Screenplay.Selenium.Resources { /// - /// A resources class, providing access to pre-written/stock JavaScript strings. + /// A library of pre-written JavaScripts, stored in the assembly as resource strings. /// static class ScriptResources { diff --git a/CSF.Screenplay.Selenium/Scripts.cs b/CSF.Screenplay.Selenium/Scripts.cs index 0077c854..0c14be1b 100644 --- a/CSF.Screenplay.Selenium/Scripts.cs +++ b/CSF.Screenplay.Selenium/Scripts.cs @@ -8,6 +8,8 @@ namespace CSF.Screenplay.Selenium /// Developers who use Screenplay & Selenium and who need to develop their own custom JavaScripts are encouraged /// to devise their own classes similar to this one. /// This provides a rapid and type-safe mechanism of retrieving and reusing well-known/predefined JavaScripts. + /// See for more information about defining + /// scripts with human-readable names and type safety. /// /// public static class Scripts diff --git a/CSF.Screenplay.Selenium/ServiceCollectionExtensions.cs b/CSF.Screenplay.Selenium/ServiceCollectionExtensions.cs index f46a8ad9..6f2c5375 100644 --- a/CSF.Screenplay.Selenium/ServiceCollectionExtensions.cs +++ b/CSF.Screenplay.Selenium/ServiceCollectionExtensions.cs @@ -9,8 +9,7 @@ namespace CSF.Screenplay.Selenium public static class ServiceCollectionExtensions { /// - /// Adds the required services to the service collection, so that performances may incorporate Selenium-based performables - /// and abilities. + /// Activates the Selenium extension for Screenplay by adding relevant services to the dependency injection container. /// /// The service collection /// The same service collection, so calls may be chained. diff --git a/CSF.Screenplay.Selenium/Tasks/ClickAndWaitForDocumentReady.cs b/CSF.Screenplay.Selenium/Tasks/ClickAndWaitForDocumentReady.cs index 61467e5c..65139a1d 100644 --- a/CSF.Screenplay.Selenium/Tasks/ClickAndWaitForDocumentReady.cs +++ b/CSF.Screenplay.Selenium/Tasks/ClickAndWaitForDocumentReady.cs @@ -10,27 +10,71 @@ namespace CSF.Screenplay.Selenium.Tasks { /// - /// Screenplay task similar to but which additionally waits for a page-load to complete after clicking. + /// A Screenplay task which combines a action with cross-browser waiting logic, + /// for navigating to a new web page. /// /// /// - /// Use this task via ClickOn(element).AndWaitForANewPageToLoad(). - /// The benefit of this task is that it ensures that (following a page-load navigation), the incoming page is ready before - /// subsequent performables are executed. + /// Use this task in performance logic using the builder method , following this up + /// with the method. + /// That builder functionality differentiates the returned performable from the normal action. + /// + /// + /// The rationale for this task's existence is primarily to deal with web browsers which are affected by the quirk + /// . See the documentation for that quirk for more information + /// on what it means for web browser. + /// + /// + /// This task deals with browsers affected by that quirk by extending the click behaviour. When the WebDriver is + /// affected by this quirk, as well as clicking upon an element, a reference to that element is saved. The WebDriver + /// is then polled until the element clicked has become stale. A stale element is one which is no longer + /// present in the web browser. The staleness of the clicked element is used as a proxy for the unloading of the + /// 'outgoing' web page. + /// Once the mechanism describe above has determined that the outgoing web page has unloaded, this task begins a second + /// waiting process. The second wait executes the JavaScript repeatedly + /// until it returns the result complete. At that point, the waiting is over and the performance may continue. + /// + /// + /// The mechanism described above also includes an optional (constructor injected) timeout. The timeout is used twice; it applies + /// both to the unloading of the old page the ready-state of the new page returning complete. Thus in a theoretical + /// worst-case scenario, this task could lead to a wait of twice the specified timeout value. + /// If the timeout is not specified then it will use the value from if the actor + /// has that ability. If not then the hardcoded fall-back of 5 seconds will be used. /// /// + /// + /// + /// This example demonstrates clicking a link to a settings page and waiting for the page to be ready. + /// + /// + /// using CSF.Screenplay.Selenium.Elements; + /// using static CSF.Screenplay.Selenium.PerformableBuilder; + /// + /// readonly ITarget settingsLink = new CssSelector("nav#app_navigation .settings a", "the link to the settings page"); + /// + /// // Within the logic of a custom task, deriving from IPerformable + /// public async ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + /// { + /// await actor.PerformAsync(ClickOn(settingsLink).AndWaitForANewPageToLoad(), cancellationToken); + /// // regardless of WebDriver implementation, further performables here will not execute + /// // until the settings page has completely loaded. + /// } + /// + /// /// + /// + /// + /// + /// public class ClickAndWaitForDocumentReady : ISingleElementPerformable { - const string COMPLETE_READY_STATE = "complete"; + const string completeReadyState = "complete"; - static readonly NamedScriptWithResult getReadyState = Scripts.GetTheDocumentReadyState; - - readonly TimeSpan waitTimeout; + readonly TimeSpan? waitTimeout; /// public ReportFragment GetReportFragment(Actor actor, Lazy element, IFormatsReportFragment formatter) - => formatter.Format("{Actor} clicks on {Element} and waits up to {Time} for the next page to load", actor, element.Value, waitTimeout); + => formatter.Format("{Actor} clicks on {Element} and waits up to {Time} for the next page to load", actor, element.Value, GetTimeout(actor)); /// public async ValueTask PerformAsAsync(ICanPerform actor, IWebDriver webDriver, Lazy element, CancellationToken cancellationToken = default) @@ -38,14 +82,28 @@ public async ValueTask PerformAsAsync(ICanPerform actor, IWebDriver webDriver, L await actor.PerformAsync(ClickOn(element.Value), cancellationToken); if(!webDriver.HasQuirk(BrowserQuirks.NeedsToWaitAfterPageLoad)) return; + var timeout = GetTimeout(actor); await actor.PerformAsync(WaitUntil(ElementIsStale(element.Value.WebElement)) - .ForAtMost(waitTimeout) - .Named($"{element.Value.Name} is no longer on the page"), + .ForAtMost(timeout) + .Named($"{element.Value} is no longer on the page"), cancellationToken); - await actor.PerformAsync(WaitUntil(PageIsReady).ForAtMost(waitTimeout).Named("the page is ready"), + await actor.PerformAsync(WaitUntil(PageIsReady).ForAtMost(timeout).Named("the next page is ready"), cancellationToken); } + /// + /// Gets a function which accesses , which forces communication with the WebDriver and verifies the + /// existence of the . + /// + /// + /// + /// The returned function catches and ignores , returning if it is + /// caught. Indeed, the technique used by this task to detect an outgoing page is to "wait until a stale element exception is thrown". + /// + /// + /// The element for which we wish to test the staleness + /// A function which will return if the is stale, or if not. + /// If the is . static Func ElementIsStale(IWebElement element) { if (element is null) throw new ArgumentNullException(nameof(element)); @@ -64,13 +122,26 @@ static Func ElementIsStale(IWebElement element) }; } - static Func PageIsReady => driver => driver.ExecuteJavaScript(getReadyState.ScriptBody) == COMPLETE_READY_STATE; + static Func PageIsReady => driver + => driver.ExecuteJavaScript(Scripts.GetTheDocumentReadyState.ScriptBody) == completeReadyState; + + TimeSpan GetTimeout(ICanPerform actor) + { + if(waitTimeout.HasValue) + return waitTimeout.Value; + + if(actor.TryGetAbility(out var ability)) + return ability.WaitTime; + + return Wait.DefaultTimeout; + } /// /// Initializes a new instance of the class. /// - /// The maximum duration to wait for the document to be ready. - public ClickAndWaitForDocumentReady(TimeSpan waitTimeout) + /// The timeout duration for both the page-unload and document-ready waits. + /// See the remarks on this class for more info. + public ClickAndWaitForDocumentReady(TimeSpan? waitTimeout) { this.waitTimeout = waitTimeout; } diff --git a/CSF.Screenplay.Selenium/Tasks/EnterTheDate.cs b/CSF.Screenplay.Selenium/Tasks/EnterTheDate.cs index 8cbb9bc6..83c589cb 100644 --- a/CSF.Screenplay.Selenium/Tasks/EnterTheDate.cs +++ b/CSF.Screenplay.Selenium/Tasks/EnterTheDate.cs @@ -7,18 +7,41 @@ using OpenQA.Selenium; using static CSF.Screenplay.Selenium.PerformableBuilder; -namespace CSF.Screenplay.Selenium.Actions +namespace CSF.Screenplay.Selenium.Tasks { /// - /// A which represents an actor entering a date value into an <input type="date"> element. + /// A Screenplay task for entering a value into an <input type="date"> element. /// /// /// - /// Note that this task is culture-sensitive. Ensure that the date value is entered into the browser using the culture in which the browser is - /// running. - /// If no culture information is specified then this task defaults to the current culture: . - /// However, this is not certain to be correct, particularly in remote/cloud configurations where the web browser is operating on different - /// infrastructure to the computer which is executing the Screenplay performance. These two computers might be operating in different cultures. + /// Use this task with the builder method . + /// This builder will guide you through picking the element into which the date should be entered. + /// + /// + /// The rationale for this task is that web browsers or WebDriver implementations are inconsistent in the manner in which + /// they support interacting with Date input elements. + /// Browsers which require a JavaScript workaround are marked with the browser quirk + /// . + /// See the documentation for that quirk for more information. + /// This task provides date-setting capabilities in a cross-browser manner. + /// + /// + /// For browsers which are affected by the quirk, the date is set via a separate task: , + /// with the 'simulate setting the value interactively' behaviour enabled. + /// For browsers which are not affected by the quirk, the date is first cleared via the action , + /// and then keypresses are sent to the web browser to enter the date in a locale-specific format via . + /// + /// + /// Note that because entering a date interactively (by sending keys) requires a locale-specific format, that makes this task + /// culture-sensitive. It is important to ensure that the date value entered into the browser is entered using the same culture + /// as which the browser is currently operating. If the culture does not match then this could lead to mistakes in the value; + /// consider muddling up US English and UK English dates. In US English 04/05/2010 means the 5th April 2010. In UK English + /// that same date string means 4th May 2010, because the days and months are transposed in that format. + /// If no culture information is specified in the constructor then this task defaults to the current culture: . + /// That is often sufficient, particularly if the WebDriver is running locally on the same computer as is executing the Screenplay + /// Performance. It could be problematic, though, if a Remote WebDriver is in use. A Remote WebDriver could be hosted in another + /// nation and thus be operating under a different culture to the Screenplay Performance. In this case, be sure to specify the + /// correct culture in the constructor of this task. /// /// /// If the date specified to this task is then this task will clear the date from the target. @@ -26,10 +49,31 @@ namespace CSF.Screenplay.Selenium.Actions /// /// /// - /// For example, a British English browser en-GB expects dates to be entered in the format ddMMyyyy. + /// A British English browser en-GB expects dates to be entered in the format ddMMyyyy. /// However, a US English browser en-US expects dates to be entered in the format MMddyyyy. /// + /// The code sample below shows how to enter the date 5th April 2025 into a date input element with the id due_date, + /// using the British English format. + /// + /// using CSF.Screenplay.Selenium.Elements; + /// using static CSF.Screenplay.Selenium.PerformableBuilder; + /// + /// readonly ITarget dueDate = new ElementId("due_date", "the due date"); + /// + /// // Within the logic of a custom task, deriving from IPerformable + /// public async ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + /// { + /// // ... other performance logic + /// await actor.PerformAsync(EnterTheDate(new DateTime(2025, 4, 5)).Into(dueDate).ForTheCultureNamed("en-GB"), cancellationToken); + /// // ... other performance logic + /// } + /// /// + /// + /// + /// + /// + /// public class EnterTheDate : IPerformable, ICanReport { const string nonNumericPattern = @"\D"; @@ -46,7 +90,7 @@ public class EnterTheDate : IPerformable, ICanReport string FormatDate() => date.HasValue ? date.Value.ToString(GetShortDatePattern()) : null; - string FormatDateAsIso() => date.HasValue ? date.Value.ToString("yyyy-MM-dd") : null; + string FormatDateAsIso() => date.HasValue ? date.Value.ToString("yyyy-MM-dd") : string.Empty; /// public ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) diff --git a/CSF.Screenplay.Selenium/Tasks/OpenUrlRespectingBase.cs b/CSF.Screenplay.Selenium/Tasks/OpenUrlRespectingBase.cs index 1da502b3..45b5f676 100644 --- a/CSF.Screenplay.Selenium/Tasks/OpenUrlRespectingBase.cs +++ b/CSF.Screenplay.Selenium/Tasks/OpenUrlRespectingBase.cs @@ -6,16 +6,58 @@ namespace CSF.Screenplay.Selenium.Tasks { /// - /// Screenplay task which opens (navigates) to a specified URL using the actor's WebDriver - /// This task respects a base URL, chosen via the ability . + /// A Screenplay task which navigates directly to a specified URL using the actor's WebDriver. + /// If the specified Uri is relative then it is made absolute by basing it upon the Uri indicated by the ability. /// /// /// - /// If the specified Uri is a relative Uri, then this task will use the actor's ability (if present) - /// to transform the relative Uri into an absolute one. - /// This task behaves identically to when the specified Uri is already absolute. + /// Use this task via the builder method , which automatically makes + /// use of this task. This task behaves very similarly to the action , except for base-URI-replacement. + /// + /// + /// When this task is used, if the specified named Uri (constructor injected) is already absolute then this task has no effect + /// beyond passing that same absolute Uri to . + /// If the specified Uri is relative then the ability is retrieved from the actor; this will throw if + /// the actor does not possess the ability. + /// If they do, then the Uri specified to this task will be rebased onto the , via + /// . The rebased Uri will then be used with . + /// + /// + /// The purpose of this task is to permit developers to specify instances using relative + /// values instead of absolute ones. When doing so, the performance must grant the ability to the actor + /// who is making use of the WebDriver (via ). This allows the final absolute Uris for the functionality + /// to be determined at runtime, via the combination of a base Uri and a relative part. + /// + /// + /// This is expected to be useful to developers, who may need their Screenplay logic to operate upon a specified environment + /// (determined by a base Uri) at runtime. However, the relative Uri paths to pages within that environment remain identical + /// regardless of the environment. The combination of the ability and this task make such an + /// architecture possible. /// /// + /// + /// + /// This task is used in exactly the same way as . The example below shows the opening of the URL + /// https://example.com/user/shoppingCart. Note that in a real performance, the actor would be granted the + /// ability at a far higher-level performable than the position in which they are performing navigation. + /// This example is 'compressed' for brevity. + /// + /// + /// using static CSF.Screenplay.Selenium.PerformableBuilder; + /// + /// var examplePage = new NamedUri("user/shoppingCart", "the user's shopping cart"); + /// + /// // Within the logic of a custom task, deriving from IPerformable + /// public async ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + /// { + /// actor.IsAbleTo(new UseABaseUri(new Uri("https://example.com"))); + /// await actor.PerformAsync(OpenTheUrl(examplePage), cancellationToken); + /// } + /// + /// + /// + /// + /// public class OpenUrlRespectingBase : IPerformable, ICanReport { readonly NamedUri uri; @@ -23,9 +65,10 @@ public class OpenUrlRespectingBase : IPerformable, ICanReport /// public ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) { - if(!actor.TryGetAbility(out var ability)) + if(uri.Uri.IsAbsoluteUri) return actor.PerformAsync(new OpenUrl(uri), cancellationToken); + var ability = actor.GetAbility(); var rebased = uri.RebaseTo(ability.BaseUri); return actor.PerformAsync(new OpenUrl(rebased), cancellationToken); } diff --git a/CSF.Screenplay.Selenium/Tasks/SetTheElementValue.cs b/CSF.Screenplay.Selenium/Tasks/SetTheElementValue.cs new file mode 100644 index 00000000..757f6007 --- /dev/null +++ b/CSF.Screenplay.Selenium/Tasks/SetTheElementValue.cs @@ -0,0 +1,95 @@ +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 uses JavaScript to directly set the value of an HTML element. + /// + /// + /// + /// Use this task via the builder method , optionally also using the builder method + /// . When AsIfSetInteractively is used then additional JavaScript + /// will be executed in an attempt to simulate a human user interactively setting the value. + /// + /// + /// The rationale for this task is that sometimes, due to limitations of the WebDriver implementation, it is not possible to + /// interact with an HTML element in the same way that a human user would. An example of this is web browsers which are affected + /// by . When these limitations are encountered, the only recourse + /// is to work around them with JavaScript. + /// + /// + /// If this task is not instructed to simulate setting the value interactively then this task does no more than use JavaScript to + /// set the value of the element. However, if it is simulating setting the value interactively then a number of HTML/JavaScript + /// events are manually invoked, to give UI behaviour an opportunity to respond. These events (and the order in which they are + /// triggered) are: focus, input, change and then blur. The element value is actually updated between + /// the focus and input events. + /// + /// + /// Use this task judiciously and sparingly. It is best to interact with the web browser/WebDriver in the same manner in which a human + /// user would, particularly when using Screenplay/Selenium for testing. This task is provided just to work around difficulties/limitations; + /// it is not intended to be the standard way to update elements on a web page. + /// + /// + /// + /// + /// + /// This example sets the value of an element that has id impossible_input to "I worked around it!" and triggers events + /// which simulate a user changing the value interactively. + /// + /// + /// using CSF.Screenplay.Selenium.Elements; + /// using static CSF.Screenplay.Selenium.PerformableBuilder; + /// + /// readonly ITarget impossibleInput = new ElementId("impossible_input", "the input field which a WebDriver cannot reach"); + /// + /// // Within the logic of a custom task, deriving from IPerformable + /// public async ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + /// { + /// // ... other performance logic + /// await actor.PerformAsync(SetTheValueOf(impossibleInput).To("I worked around it!").AsIfSetInteractively(), cancellationToken); + /// // ... other performance logic + /// } + /// + /// + /// + /// + /// + /// + public class SetTheElementValue : Actions.ISingleElementPerformable + { + readonly object value; + readonly bool simulateInteractiveSet; + + /// + public ReportFragment GetReportFragment(Actor actor, Lazy element, IFormatsReportFragment formatter) + { + return simulateInteractiveSet + ? formatter.Format("{Actor} uses JavaScript to simulate setting the value of {Element} to {Value} interactively", actor, element.Value, value) + : formatter.Format("{Actor} uses JavaScript to set the value of {Element} to {Value}", actor, element.Value, value); + } + + /// + public ValueTask PerformAsAsync(ICanPerform actor, IWebDriver webDriver, Lazy element, CancellationToken cancellationToken = default) + { + return simulateInteractiveSet + ? actor.PerformAsync(ExecuteAScript(Scripts.SetElementValueSimulatedInteractively, element.Value.WebElement, value), cancellationToken) + : actor.PerformAsync(ExecuteAScript(Scripts.SetElementValue, element.Value.WebElement, value), cancellationToken); + } + + /// + /// Initializes a new instance of the class. + /// + /// The value to set on the element. + /// If then the JavaScript will fire UI events to simulate having set the element interactively + public SetTheElementValue(object value, bool simulateInteractiveSet) + { + this.value = value; + this.simulateInteractiveSet = simulateInteractiveSet; + } + } +} \ No newline at end of file diff --git a/CSF.Screenplay.Selenium/Tasks/TakeAndSaveScreenshot.cs b/CSF.Screenplay.Selenium/Tasks/TakeAndSaveScreenshot.cs index 01d1cfee..db808c9a 100644 --- a/CSF.Screenplay.Selenium/Tasks/TakeAndSaveScreenshot.cs +++ b/CSF.Screenplay.Selenium/Tasks/TakeAndSaveScreenshot.cs @@ -6,17 +6,38 @@ namespace CSF.Screenplay.Selenium.Tasks { /// - /// Screenplay task which represents an actor taking a screenshot of the current web page and saving it as a report asset. + /// A Screenplay task which combines the taking of a screenshot of the current web browser viewport and saving it + /// as an asset file. /// /// /// + /// Use this task via one of the builder methods or . /// This performable task is a composition of two others: and . - /// It offers an optional name for the screenshot, which is used as part of its asset filename if specified. + /// See the documentation for this question and action for more information. /// /// /// As with , this performable requires the actor to have the ability . /// /// + /// + /// + /// using OpenQA.Selenium; + /// using static CSF.Screenplay.Selenium.PerformableBuilder; + /// + /// // Within the logic of a custom task, deriving from IPerformable + /// public async ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + /// { + /// // ... other performance logic + /// await actor.PerformAsync(TakeAndSaveAScreenshot().WithTheName("Shopping cart items"), cancellationToken); + /// // ... other performance logic + /// } + /// + /// + /// + /// + /// + /// + /// public class TakeAndSaveScreenshot : IPerformable, ICanReport { readonly string name; diff --git a/CSF.Screenplay.Selenium/UseABaseUri.cs b/CSF.Screenplay.Selenium/UseABaseUri.cs index 446044de..69f704f3 100644 --- a/CSF.Screenplay.Selenium/UseABaseUri.cs +++ b/CSF.Screenplay.Selenium/UseABaseUri.cs @@ -3,8 +3,55 @@ namespace CSF.Screenplay.Selenium { /// - /// Screenplay ability which allows an to use a base URI. + /// Screenplay ability which allows an to use a base Uri, 'completing' relative Uris at runtime. /// + /// + /// + /// This ability enables the use of a technique whereby the environment/location of a web application (which is being + /// tested or manipulated by the Selenium Extension for Screenplay) may be set and changed at runtime, instead of hard-coded. + /// This could be particularly useful if you are using this extension for testing and wish to be able to run the same suite + /// of tests against multiple environments. For example: + /// + /// + /// Locally, on a developer's computer, with a base Uri such as https://localhost:8080/ + /// On a testing environment, with a base Uri such as https://testing.example.com/ + /// On a staging environment, with a base Uri such as https://staging.example.com/ + /// + /// + /// If you wish to use this technique then when writing instances of , specify these using relative Uris, such as + /// new NamedUri("user/shoppingCart", "the user's shopping cart"). Then, grant your actor this ability, using + /// : + /// + /// + /// // The following would not realistically be expected to be seen together in one place. + /// // This is a 'compressed' example for brevity. + /// // In a realistic Screenplay, the Actor set-up would be in an IPersona implementation, + /// // the NamedUri would be declared in a library class and the Actor's performance would + /// // be within an IPerformable implementation. + /// + /// using CSF.Screenplay.Selenium; + /// using static CSF.Screenplay.Selenium.PerformableBuilder; + /// + /// var useABaseUri = new UseABaseUri(new Uri("https://testing.example.com/")); + /// myActor.IsAbleTo(useABaseUri); + /// // Details of getting a BrowseTheWeb ability instance are omitted for brevity. + /// myActor.IsAbleTo(GetBrowseTheWeb()); + /// + /// var shoppingCart = new NamedUri("user/shoppingCart", "the user's shopping cart"); + /// await myActor.PerformAsync(OpenTheUrl(shoppingCart)); + /// + /// // Actually opens the URL https://testing.example.com/user/shoppingCart + /// + /// + /// The Uri to use as the base Uri does not need to be hard-coded, it could come from configuration or an environment variable etc. + /// When the actor makes use of the ability and opens a Url using the performable returned from + /// , the specified named Uri will be rebased + /// to the base Uri specified by this ability. + /// + /// + /// + /// + /// public class UseABaseUri : ICanReport { /// diff --git a/CSF.Screenplay.Selenium/UseADefaultWaitTime.cs b/CSF.Screenplay.Selenium/UseADefaultWaitTime.cs index 49647fe4..04361b08 100644 --- a/CSF.Screenplay.Selenium/UseADefaultWaitTime.cs +++ b/CSF.Screenplay.Selenium/UseADefaultWaitTime.cs @@ -3,9 +3,24 @@ namespace CSF.Screenplay.Selenium { /// - /// Screenplay ability that allows an to use a default time for which + /// An ability which specifies a default amount of time for performables which involve waiting. + /// that allows an to use a default time for which /// to wait for WebDriver operations to complete. /// + /// + /// + /// A small number of performables within the Selenium extension for Screenplay involve waiting, + /// along with the opportunity to specify a timeout. If no timeout is specified and the + /// does not have this ability then the default timeout is 5 seconds, as specified by + /// . + /// + /// + /// If the Actor has this ability then, if they perform a waiting-type action and no timeout is explicitly specified, + /// they will use the default timeout which is specified within this ability instead of the 5-second hard-coded default. + /// + /// + /// + /// public class UseADefaultWaitTime : ICanReport { /// diff --git a/CSF.Screenplay/Reporting/AssetPathProvider.cs b/CSF.Screenplay/Reporting/AssetPathProvider.cs index 579911af..fb793b5f 100644 --- a/CSF.Screenplay/Reporting/AssetPathProvider.cs +++ b/CSF.Screenplay/Reporting/AssetPathProvider.cs @@ -28,6 +28,11 @@ namespace CSF.Screenplay.Reporting /// This type is somewhat stateful, because it maintains an internal counter in order to provide the asset numbers (described above). /// It should be consumed from dependency injection as a scoped service, so that each performance has its own instance of this type. /// + /// + /// This class is intended for internal Screenplay usage. Normal consumers of Screenplay should not concern themselves + /// with this detail. Instead they should grant the to actors who should be + /// able to save . + /// /// public class AssetPathProvider : IGetsAssetFilePath { diff --git a/Tests/CSF.Screenplay.Selenium.Tests/Actions/ClearLocalStorageTests.cs b/Tests/CSF.Screenplay.Selenium.Tests/Actions/ClearLocalStorageTests.cs index f883dd5e..abd4ec06 100644 --- a/Tests/CSF.Screenplay.Selenium.Tests/Actions/ClearLocalStorageTests.cs +++ b/Tests/CSF.Screenplay.Selenium.Tests/Actions/ClearLocalStorageTests.cs @@ -32,28 +32,4 @@ public async Task ClearLocalStorageShouldClearItems(IStage stage) Assert.That(items, Is.Empty); } - - [Test, AutoMoqData] - public void PerformAsAsyncShouldThrowIfBrowserDoesNotSupportJavaScriptAndThrowIfUnsupportedIsTrue(Actor actor, - IWebDriver webDriver, - Mock options) - { - var driverAndOptions = new WebDriverAndOptions(webDriver, options.Object); - var ability = new BrowseTheWeb(Mock.Of(x => x.GetDefaultWebDriver(It.IsAny>()) == driverAndOptions)); - actor.IsAbleTo(ability); - var sut = new ClearLocalStorage(true); - Assert.That(async () => await sut.PerformAsAsync(actor), Throws.InstanceOf().And.InnerException.InstanceOf()); - } - - [Test, AutoMoqData] - public void PerformAsAsyncShouldNotThrowIfBrowserDoesNotSupportJavaScriptAndThrowIfUnsupportedIsFalse(Actor actor, - IWebDriver webDriver, - Mock options) - { - var driverAndOptions = new WebDriverAndOptions(webDriver, options.Object); - var ability = new BrowseTheWeb(Mock.Of(x => x.GetDefaultWebDriver(It.IsAny>()) == driverAndOptions)); - actor.IsAbleTo(ability); - var sut = new ClearLocalStorage(false); - Assert.That(async () => await sut.PerformAsAsync(actor), Throws.Nothing); - } } \ No newline at end of file