From 7992108426b8f5fbfc1e27bf776d27e0cfbf9e6f Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Tue, 10 Feb 2026 18:29:57 +0000 Subject: [PATCH 01/14] Resolve some broken UIDs This happened when I swapped Screenplay to using the Options pattern. --- CSF.Screenplay.Docs/docs/GettingReports.md | 2 +- .../docs/concepts/HowScreenplayAndPerformanceRelate.md | 2 +- .../docs/dependencyInjection/AddingServices.md | 4 ++-- CSF.Screenplay.Docs/docs/dependencyInjection/index.md | 4 ++-- .../docs/extendingScreenplay/TestIntegrations.md | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) 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 From dc1a4e0fca4a40d8d35e096ce0a8257a7380ec2d Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Tue, 10 Feb 2026 21:32:32 +0000 Subject: [PATCH 02/14] WIP #258 - Begin writing docs for Selenium Also remove some redundant tests. This covers a few actions. There's plenty more to do. --- .../docs/extensions/selenium/Actions.md | 69 +++++++++++++++++++ .../docs/extensions/selenium/index.md | 57 ++++++++++++++- .../docs/extensions/selenium/toc.yml | 4 ++ .../Actions/ClearCookies.cs | 23 ++++++- .../Actions/ClearLocalStorage.cs | 53 +++++++------- .../Actions/DeleteTheCookie.cs | 26 ++++++- .../Actions/ExecuteJavaScript.cs | 42 ++++++++++- .../PerformableBuilder.general.cs | 17 ----- .../Actions/ClearLocalStorageTests.cs | 24 ------- 9 files changed, 245 insertions(+), 70 deletions(-) create mode 100644 CSF.Screenplay.Docs/docs/extensions/selenium/Actions.md create mode 100644 CSF.Screenplay.Docs/docs/extensions/selenium/toc.yml 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..89c8eac1 --- /dev/null +++ b/CSF.Screenplay.Docs/docs/extensions/selenium/Actions.md @@ -0,0 +1,69 @@ +# 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"_. + +Below is a summary of each of these actions' usage, with a link to their detailed documentation page. + +[Actions]: ../../../glossary/Action.md +[a target]: Elements.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.Selenium/Actions/SingleElementPerformableDocs.xml b/CSF.Screenplay.Selenium/Actions/SingleElementPerformableDocs.xml index d02b8648..5131efb1 100644 --- a/CSF.Screenplay.Selenium/Actions/SingleElementPerformableDocs.xml +++ b/CSF.Screenplay.Selenium/Actions/SingleElementPerformableDocs.xml @@ -2,17 +2,17 @@ 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 most of the Screenplay - actions in the Selenium Plugin which operate upon a single element, those which derive from - . In order for this class to be used as a full-fledged + 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 . + 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 diff --git a/CSF.Screenplay.Selenium/BrowserQuirks.cs b/CSF.Screenplay.Selenium/BrowserQuirks.cs index 5408b388..90152c8f 100644 --- a/CSF.Screenplay.Selenium/BrowserQuirks.cs +++ b/CSF.Screenplay.Selenium/BrowserQuirks.cs @@ -9,34 +9,54 @@ namespace CSF.Screenplay.Selenium 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"; /// diff --git a/CSF.Screenplay.Selenium/Builders/EnterTheDateBuilder.cs b/CSF.Screenplay.Selenium/Builders/EnterTheDateBuilder.cs index 9ccf2e8f..abbb4f7b 100644 --- a/CSF.Screenplay.Selenium/Builders/EnterTheDateBuilder.cs +++ b/CSF.Screenplay.Selenium/Builders/EnterTheDateBuilder.cs @@ -1,7 +1,6 @@ using System; using System.Globalization; using CSF.Screenplay.Performables; -using CSF.Screenplay.Selenium.Actions; using CSF.Screenplay.Selenium.Elements; using CSF.Screenplay.Selenium.Tasks; 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/Questions/ElementQueryDocs.xml b/CSF.Screenplay.Selenium/Questions/ElementQueryDocs.xml index f164297c..ac5474a6 100644 --- a/CSF.Screenplay.Selenium/Questions/ElementQueryDocs.xml +++ b/CSF.Screenplay.Selenium/Questions/ElementQueryDocs.xml @@ -19,6 +19,9 @@ concerned with this factor. + This class is not a complete , as it relies diff --git a/CSF.Screenplay.Selenium/Tasks/ClickAndWaitForDocumentReady.cs b/CSF.Screenplay.Selenium/Tasks/ClickAndWaitForDocumentReady.cs index 61467e5c..15f9180b 100644 --- a/CSF.Screenplay.Selenium/Tasks/ClickAndWaitForDocumentReady.cs +++ b/CSF.Screenplay.Selenium/Tasks/ClickAndWaitForDocumentReady.cs @@ -10,21 +10,61 @@ 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 a (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. /// /// + /// + /// + /// 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"; - - static readonly NamedScriptWithResult getReadyState = Scripts.GetTheDocumentReadyState; + const string completeReadyState = "complete"; readonly TimeSpan waitTimeout; @@ -40,12 +80,25 @@ public async ValueTask PerformAsAsync(ICanPerform actor, IWebDriver webDriver, L await actor.PerformAsync(WaitUntil(ElementIsStale(element.Value.WebElement)) .ForAtMost(waitTimeout) - .Named($"{element.Value.Name} is no longer on the page"), + .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(waitTimeout).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,12 +117,14 @@ 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; /// /// Initializes a new instance of the class. /// - /// The maximum duration to wait for the document to be ready. + /// 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 baaf8053..83c589cb 100644 --- a/CSF.Screenplay.Selenium/Tasks/EnterTheDate.cs +++ b/CSF.Screenplay.Selenium/Tasks/EnterTheDate.cs @@ -10,15 +10,38 @@ 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.Tasks /// /// /// - /// 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 index ca054802..89167ab4 100644 --- a/CSF.Screenplay.Selenium/Tasks/SetTheElementValue.cs +++ b/CSF.Screenplay.Selenium/Tasks/SetTheElementValue.cs @@ -8,8 +8,58 @@ namespace CSF.Screenplay.Selenium.Tasks { /// - /// An action that sets the value of a web element using JavaScript. + /// 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; 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; From 0a9ab00f0ad2a88b41f17f43e7ccd48ee572b819 Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Sat, 14 Feb 2026 10:10:08 +0000 Subject: [PATCH 11/14] WIP #258 - further docco --- CSF.Screenplay.Selenium/BrowseTheWeb.cs | 142 +++++++++++++----- .../BrowseTheWebExtensions.cs | 24 +++ CSF.Screenplay.Selenium/NamedUri.cs | 21 ++- CSF.Screenplay.Selenium/UseABaseUri.cs | 49 +++++- 4 files changed, 198 insertions(+), 38 deletions(-) create mode 100644 CSF.Screenplay.Selenium/BrowseTheWebExtensions.cs diff --git a/CSF.Screenplay.Selenium/BrowseTheWeb.cs b/CSF.Screenplay.Selenium/BrowseTheWeb.cs index ca229ade..fc9ba437 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() + 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/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/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 { /// From 76970496122211039733ce9a6a51cadff9229ac4 Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Sat, 14 Feb 2026 14:36:26 +0000 Subject: [PATCH 12/14] Provisionally resolve #258 - Finish docs for queries --- .../docs/extensions/selenium/Queries.md | 42 ++- .../QueryPredicatePrototypeBuilder.cs | 299 +++++++++++++++--- .../Builders/QuestionQueryBuilder.cs | 127 ++++++++ 3 files changed, 424 insertions(+), 44 deletions(-) diff --git a/CSF.Screenplay.Docs/docs/extensions/selenium/Queries.md b/CSF.Screenplay.Docs/docs/extensions/selenium/Queries.md index 0c60b90c..388cda1d 100644 --- a/CSF.Screenplay.Docs/docs/extensions/selenium/Queries.md +++ b/CSF.Screenplay.Docs/docs/extensions/selenium/Queries.md @@ -4,4 +4,44 @@ uid: SeleniumQueriesArticle # Queries -TODO: Write this docco \ No newline at end of file +Queries are a unified mechanism by which [Elements] are interrogated/observed. +Queries broadly serve two purposes, as described below. + +[Elements]: xref:CSF.Screenplay.Selenium.Elements.SeleniumElement + +## Observing the state of elements + +Queries are used directly by the [`SingleElementQuery`] and [`ElementCollectionQuery`] questions which are included in the Selenium extension for Screenplay. +In this usage, the query indicates which piece of state is to be retrieved from the element(s) and returned to the consuming performable. + +[`SingleElementQuery`]: xref:CSF.Screenplay.Selenium.Questions.SingleElementQuery`1 +[`ElementCollectionQuery`]: xref:CSF.Screenplay.Selenium.Questions.ElementCollectionQuery`1 + +## Specifying criteria + +A mechanism which parallels queries is present in the class [`QueryPredicatePrototypeBuilder`]. +This mechanism uses the combination of a query and a predicate to specify criteria by which element(s) may be matched. +This applies to two use-cases: + +The methods of [`TargetExtensions`] may be used to build instances of the [`Wait`] action. +Together, the query and predicate specify the condition for the wait to end. + +The query prototype builder is also used by one of the overloads of the [`ForThoseWhich`] builder method. +This builder may be used to create the [`FilterElements`] question, to specify the criteria by which to match elements which should be accepted by the filter. + +[`QueryPredicatePrototypeBuilder`]: xref:CSF.Screenplay.Selenium.Builders.QueryPredicatePrototypeBuilder +[`TargetExtensions`]: xref:CSF.Screenplay.Selenium.Elements.TargetExtensions +[`Wait`]: xref:CSF.Screenplay.Selenium.Actions.Wait +[`ForThoseWhich`]: xref:CSF.Screenplay.Selenium.Builders.FilterElementsBuilder.ForThoseWhich(System.Func{CSF.Screenplay.Selenium.Builders.QueryPredicatePrototypeBuilder,CSF.Screenplay.Selenium.Builders.IBuildsElementPredicates}) +[`FilterElements`]: xref:CSF.Screenplay.Selenium.Questions.FilterElements + +## List of queries + +The available queries are best found by reading the list of methods available on two classes. +For usages of queries to observe/read a piece of information, see the functionality present upon the [`QuestionQueryBuilder`] class. +For usages relating the the creation of criteria, see the members of the [`QueryPredicatePrototypeBuilder`] class. +Recall, each query reads a piece of information from [an HTML element] (or [elements in a collection]), or it creates a criteria for matching either one or a collection of elements. + +[`QuestionQueryBuilder`]: xref:CSF.Screenplay.Selenium.Builders.QuestionQueryBuilder +[an HTML element]: xref:CSF.Screenplay.Selenium.Elements.SeleniumElement +[elements in a collection]: xref:CSF.Screenplay.Selenium.Elements.SeleniumElementCollection 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) From 9ef5e348aa77f61d195c3d93eb33a871e229e995 Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Sat, 14 Feb 2026 15:59:19 +0000 Subject: [PATCH 13/14] Resolve #258 - Complete docs for the Selenium extension --- .../docs/extensions/selenium/index.md | 5 +++- CSF.Screenplay.Selenium/Actions/Wait.cs | 17 +++++++++-- CSF.Screenplay.Selenium/ActorExtensions.cs | 4 +-- CSF.Screenplay.Selenium/BrowserQuirks.cs | 12 ++++++-- .../Builders/ClickBuilder.cs | 7 ++--- CSF.Screenplay.Selenium/Elements/ClassName.cs | 8 ++++++ .../Elements/CssSelector.cs | 8 ++++++ CSF.Screenplay.Selenium/Elements/ElementId.cs | 6 ++++ CSF.Screenplay.Selenium/Elements/Locator.cs | 9 ++++-- .../Elements/SeleniumElement.cs | 7 +++++ .../Elements/SeleniumElementCollection.cs | 9 +++++- .../Elements/SeleniumElementExtensions.cs | 10 ++++--- CSF.Screenplay.Selenium/Elements/XPath.cs | 8 ++++++ CSF.Screenplay.Selenium/Queries/TextQuery.cs | 21 ++++++++++++++ .../ISingleElementPerformableWithResult.cs | 2 -- .../Reporting/OptionsFormatter.cs | 2 +- .../Reporting/ScreenshotFormatter.cs | 2 +- .../Resources/ScriptResources.cs | 2 +- CSF.Screenplay.Selenium/Scripts.cs | 2 ++ .../ServiceCollectionExtensions.cs | 3 +- .../Tasks/ClickAndWaitForDocumentReady.cs | 28 +++++++++++++++---- .../UseADefaultWaitTime.cs | 17 ++++++++++- 22 files changed, 156 insertions(+), 33 deletions(-) diff --git a/CSF.Screenplay.Docs/docs/extensions/selenium/index.md b/CSF.Screenplay.Docs/docs/extensions/selenium/index.md index 39cd2a94..dbde6967 100644 --- a/CSF.Screenplay.Docs/docs/extensions/selenium/index.md +++ b/CSF.Screenplay.Docs/docs/extensions/selenium/index.md @@ -24,7 +24,7 @@ Like many extensions, the Selenium extension provides: * 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 action upon Y element"_. +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. @@ -45,7 +45,10 @@ The types listed above are the core of the Selenium plugin, but there's more ava * 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.Selenium/Actions/Wait.cs b/CSF.Screenplay.Selenium/Actions/Wait.cs index 580dab58..fd6c8d9d 100644 --- a/CSF.Screenplay.Selenium/Actions/Wait.cs +++ b/CSF.Screenplay.Selenium/Actions/Wait.cs @@ -86,9 +86,22 @@ namespace CSF.Screenplay.Selenium.Actions /// /// /// + /// 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; @@ -122,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/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/BrowserQuirks.cs b/CSF.Screenplay.Selenium/BrowserQuirks.cs index 90152c8f..46b8354e 100644 --- a/CSF.Screenplay.Selenium/BrowserQuirks.cs +++ b/CSF.Screenplay.Selenium/BrowserQuirks.cs @@ -6,6 +6,14 @@ 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 { /// @@ -65,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/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/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/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/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/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/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 15f9180b..65139a1d 100644 --- a/CSF.Screenplay.Selenium/Tasks/ClickAndWaitForDocumentReady.cs +++ b/CSF.Screenplay.Selenium/Tasks/ClickAndWaitForDocumentReady.cs @@ -35,9 +35,11 @@ namespace CSF.Screenplay.Selenium.Tasks /// until it returns the result complete. At that point, the waiting is over and the performance may continue. /// /// - /// The mechanism described above also includes a (constructor injected) timeout. The timeout is used twice; it applies + /// 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. /// /// /// @@ -62,15 +64,17 @@ namespace CSF.Screenplay.Selenium.Tasks /// /// /// + /// + /// public class ClickAndWaitForDocumentReady : ISingleElementPerformable { const string completeReadyState = "complete"; - 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) @@ -78,11 +82,12 @@ 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) + .ForAtMost(timeout) .Named($"{element.Value} is no longer on the page"), cancellationToken); - await actor.PerformAsync(WaitUntil(PageIsReady).ForAtMost(waitTimeout).Named("the next page is ready"), + await actor.PerformAsync(WaitUntil(PageIsReady).ForAtMost(timeout).Named("the next page is ready"), cancellationToken); } @@ -120,12 +125,23 @@ static Func ElementIsStale(IWebElement element) 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 timeout duration for both the page-unload and document-ready waits. /// See the remarks on this class for more info. - public ClickAndWaitForDocumentReady(TimeSpan waitTimeout) + public ClickAndWaitForDocumentReady(TimeSpan? waitTimeout) { this.waitTimeout = waitTimeout; } 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 { /// From 8c338a6f13916334ab6994af2f9f75deb9c92e96 Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Sat, 14 Feb 2026 17:18:26 +0000 Subject: [PATCH 14/14] Fix some tech debt issues --- CSF.Screenplay.Selenium/BrowseTheWeb.cs | 2 +- CSF.Screenplay.Selenium/Tasks/SetTheElementValue.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CSF.Screenplay.Selenium/BrowseTheWeb.cs b/CSF.Screenplay.Selenium/BrowseTheWeb.cs index fc9ba437..e93d785d 100644 --- a/CSF.Screenplay.Selenium/BrowseTheWeb.cs +++ b/CSF.Screenplay.Selenium/BrowseTheWeb.cs @@ -102,7 +102,7 @@ public class BrowseTheWeb : ICanReport, IDisposable /// public DriverOptions DriverOptions => webDriverAndOptions.Value.DriverOptions; - Lazy GetLazyWebDriverAndOptions(IGetsWebDriver webDriverFactory, string webDriverName) + static Lazy GetLazyWebDriverAndOptions(IGetsWebDriver webDriverFactory, string webDriverName) { if (webDriverFactory is null) throw new ArgumentNullException(nameof(webDriverFactory)); diff --git a/CSF.Screenplay.Selenium/Tasks/SetTheElementValue.cs b/CSF.Screenplay.Selenium/Tasks/SetTheElementValue.cs index 89167ab4..757f6007 100644 --- a/CSF.Screenplay.Selenium/Tasks/SetTheElementValue.cs +++ b/CSF.Screenplay.Selenium/Tasks/SetTheElementValue.cs @@ -34,7 +34,7 @@ namespace CSF.Screenplay.Selenium.Tasks /// 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. /// - /// + /// /// /// ///