From 9ec60e7ebb6a24bf82a9d63f6e17356814065f32 Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Sun, 14 Jun 2026 13:08:05 +0100 Subject: [PATCH 1/2] WIP #225 - Add some missing docs I forgot to include these when I first wrote them. So, now they're listed from the Selenium extension docs. --- .../docs/extensions/selenium/Actions.md | 3 ++ .../docs/extensions/selenium/Questions.md | 18 ++++++-- .../docs/extensions/selenium/Tasks.md | 13 +++++- .../BeginCollectingLogsWithJavaScript.cs | 10 +++-- CSF.Screenplay.Selenium/BrowseTheWeb.cs | 41 +++++++++++++++++-- 5 files changed, 71 insertions(+), 14 deletions(-) diff --git a/CSF.Screenplay.Docs/docs/extensions/selenium/Actions.md b/CSF.Screenplay.Docs/docs/extensions/selenium/Actions.md index 36fa7a20..270bd78f 100644 --- a/CSF.Screenplay.Docs/docs/extensions/selenium/Actions.md +++ b/CSF.Screenplay.Docs/docs/extensions/selenium/Actions.md @@ -45,6 +45,7 @@ Below is a summary of each of these actions' usage, with a link to their detaile | Action | Usage | | ------ | ----- | +| [`BeginCollectingLogsWithJavaScript`] | Runs a JavaScript workaround to collect browser logs, see [`ShouldCollectLogs`] for more info | | [`ClearCookies`] | Clear the web browser cookies for the current site (domain) | | [`ClearLocalStorage`] | Clear the web browser [local storage] for the current site (domain) | | [`DeleteTheCookie`] | Delete a single named cookie | @@ -52,6 +53,8 @@ Below is a summary of each of these actions' usage, with a link to their detaile | [`OpenUrl`] | Directs the browser to open a specified URL | | [`SaveScreenshot`] | Saves a screenshot to a file | +[`BeginCollectingLogsWithJavaScript`]: xref:CSF.Screenplay.Selenium.Actions.BeginCollectingLogsWithJavaScript +[`ShouldCollectLogs`]: xref:CSF.Screenplay.Selenium.BrowseTheWeb.ShouldCollectLogs [`OpenUrl`]: xref:CSF.Screenplay.Selenium.Actions.OpenUrl [`SaveScreenshot`]: xref:CSF.Screenplay.Selenium.Actions.SaveScreenshot [`ClearCookies`]: xref:CSF.Screenplay.Selenium.Actions.ClearCookies diff --git a/CSF.Screenplay.Docs/docs/extensions/selenium/Questions.md b/CSF.Screenplay.Docs/docs/extensions/selenium/Questions.md index 7a56c59e..af6c4baf 100644 --- a/CSF.Screenplay.Docs/docs/extensions/selenium/Questions.md +++ b/CSF.Screenplay.Docs/docs/extensions/selenium/Questions.md @@ -38,17 +38,23 @@ The possibilities are explained in detail on the questions' detailed documentati | Question | Usage | | -------- | ----- | +| [`FilterElements`] | Filters [a collection of elements] for those which match a [query] | | [`FindElement`] | Gets [an element] which matches a [`Locator`] | | [`FindElements`] | Gets [a collection of elements] which match a [`Locator`] | -| [`FilterElements`] | Filters [a collection of elements] for those which match a [query] | +| [`GetShadowRootNatively`] | Uses a native technique to get a [Shadow DOM] [root element] | +| [`GetShadowRootWithJavaScript`] | Uses a JavaScript-based workaround to get a [Shadow DOM] [root element] | +[`FilterElements`]: xref:CSF.Screenplay.Selenium.Questions.FilterElements [`FindElement`]: xref:CSF.Screenplay.Selenium.Questions.FindElement [`FindElements`]: xref:CSF.Screenplay.Selenium.Questions.FindElements -[`FilterElements`]: xref:CSF.Screenplay.Selenium.Questions.FilterElements [`Locator`]: xref:CSF.Screenplay.Selenium.Elements.Locator [query]: Queries.md [an element]: xref:CSF.Screenplay.Selenium.Elements.SeleniumElement [a collection of elements]: xref:CSF.Screenplay.Selenium.Elements.SeleniumElementCollection +[`GetShadowRootNatively`]: xref:CSF.Screenplay.Selenium.Questions.GetShadowRootNatively +[Shadow DOM]: https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM +[root element]: https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot +[`GetShadowRootWithJavaScript`]: xref:CSF.Screenplay.Selenium.Questions.GetShadowRootWithJavaScript ## Questions for the web browser @@ -57,10 +63,14 @@ These questions don't neccesarily involve elements, instead they query the web b | Question | Usage | | -------- | ----- | | [`ExecuteJavaScriptAndGetResult`] | Executes a JavaScript directly in the browser and reads the result | +| [`GetLogsNatively`] | Reads the browser console logs _(note that web driver support is limited)_ | +| [`GetLogsWithJavaScript`] | Reads the browser console logs using a JavaScript workaround, activated by [`BeginCollectingLogsWithJavaScript`] | | [`GetWindowTitle`] | Reads the text of the Window/Tab title | -| [`TakeScreenshot`] | Takes a Screenshot of the browser window | +| [`TakeScreenshot`] | Takes a Screenshot of the browser window | [`ExecuteJavaScriptAndGetResult`]: xref:CSF.Screenplay.Selenium.Questions.ExecuteJavaScriptAndGetResult`1 +[`GetLogsNatively`]: xref:CSF.Screenplay.Selenium.Questions.GetLogsNatively +[`GetLogsWithJavaScript`]: xref:CSF.Screenplay.Selenium.Questions.GetLogsWithJavaScript +[`BeginCollectingLogsWithJavaScript`]: xref:CSF.Screenplay.Selenium.Actions.BeginCollectingLogsWithJavaScript [`GetWindowTitle`]: xref:CSF.Screenplay.Selenium.Questions.GetWindowTitle [`TakeScreenshot`]: xref:CSF.Screenplay.Selenium.Questions.TakeScreenshot - diff --git a/CSF.Screenplay.Docs/docs/extensions/selenium/Tasks.md b/CSF.Screenplay.Docs/docs/extensions/selenium/Tasks.md index 1149b463..cddaf2b8 100644 --- a/CSF.Screenplay.Docs/docs/extensions/selenium/Tasks.md +++ b/CSF.Screenplay.Docs/docs/extensions/selenium/Tasks.md @@ -8,15 +8,24 @@ The table below serves as a list, along with a brief summary of the purpose of e | Task | Usage | | ---- | ----- | +| [`BeginCollectingLogsWithJavaScriptIfApplicable`] | Conditionally executes [`BeginCollectingLogsWithJavaScript`] | | [`ClickAndWaitForDocumentReady`] | Clicks a link which navigates to a new page, waiting until it is ready | | [`EnterTheDate`] | Enters a value into an `` in a cross-browser manner | -| [`OpenUrlRespectingBase`] | Navigates to a URL, using a base URL provided by the [`UseABaseUri`] ability | +| [`GetShadowRoot`] | Uses the best technique available to get [the root element] of a [Shadow DOM] | +| [`GetTheBrowserLogs`] | Uses the best technique available to get the browser console logs | +| [`NavigateToUrl`] | Navigates to a specified URL, which may be relative to a base specified 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 | +[`BeginCollectingLogsWithJavaScriptIfApplicable`]: xref:CSF.Screenplay.Selenium.Tasks.BeginCollectingLogsWithJavaScriptIfApplicable +[`BeginCollectingLogsWithJavaScript`]: xref:CSF.Screenplay.Selenium.Actions.BeginCollectingLogsWithJavaScript +[`GetShadowRoot`]: xref:CSF.Screenplay.Selenium.Tasks.GetShadowRoot +[Shadow DOM]: https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM +[the root element]: https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot +[`GetTheBrowserLogs`]: xref:CSF.Screenplay.Selenium.Tasks.GetTheBrowserLogs [`ClickAndWaitForDocumentReady`]: xref:CSF.Screenplay.Selenium.Tasks.ClickAndWaitForDocumentReady [`EnterTheDate`]: xref:CSF.Screenplay.Selenium.Tasks.EnterTheDate -[`OpenUrlRespectingBase`]: xref:CSF.Screenplay.Selenium.Tasks.OpenUrlRespectingBase +[`NavigateToUrl`]: xref:CSF.Screenplay.Selenium.Tasks.NavigateToUrl [`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/BeginCollectingLogsWithJavaScript.cs b/CSF.Screenplay.Selenium/Actions/BeginCollectingLogsWithJavaScript.cs index 0b794551..1d11e244 100644 --- a/CSF.Screenplay.Selenium/Actions/BeginCollectingLogsWithJavaScript.cs +++ b/CSF.Screenplay.Selenium/Actions/BeginCollectingLogsWithJavaScript.cs @@ -11,20 +11,22 @@ namespace CSF.Screenplay.Selenium.Actions /// /// /// When used, this action should be executed as soon as possible after the current page has completed loading. - /// Ideally, directly after . - /// Any log messages which have been sent to the native browser console before this action is executed will be missed + /// Ideally, directly after or . + /// Any log messages which have been sent to the native browser console before this action is executed will be missed /// and will not be available to the counterpart question which retrieves log messages: . /// /// /// Note that this action/the script needs to be re-run after each traditional web page navigation/reload. /// However, due to the nature of SPAs, it does not need to be re-run following an SPA-style navigation. - /// On supported browsers, there is no harm in re-running this script when it is not needed, except for the impact on - /// performance (wasted network roundtrips). + /// Please note that each time this script is run, it will have a consequence of clearing any logs which have already + /// been collected. So, it is advised to execute it only after actions/tasks which would cause a full web browser page + /// reload (as noted above). /// /// /// This action is for use only with web browsers which have the quirk. /// /// + /// public class BeginCollectingLogsWithJavaScript : IPerformable, ICanReport { /// diff --git a/CSF.Screenplay.Selenium/BrowseTheWeb.cs b/CSF.Screenplay.Selenium/BrowseTheWeb.cs index 83962159..fd29e8f6 100644 --- a/CSF.Screenplay.Selenium/BrowseTheWeb.cs +++ b/CSF.Screenplay.Selenium/BrowseTheWeb.cs @@ -60,7 +60,7 @@ namespace CSF.Screenplay.Selenium /// 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; /// @@ -97,9 +97,42 @@ public class BrowseTheWeb : ICanReport, IDisposable /// /// /// - /// When this value is set to , this will trigger usage of - /// at points where it is required. This is applicable only when the current implementation has the - /// quirk . + /// Different web driver implementations have varying levels of support for providing access to web browser's console logs. + /// As of June 2026, the only implementations which provide native access are local implementations of Chromium-based + /// web drivers, such as and . + /// For other web browsers/drivers, as well as all remote web drivers, Selenium is unable to directly access their + /// logs using a native API. + /// + /// + /// When this property is set to , it triggers behaviour which causes Screenplay to attempt to access + /// the browser's console logs more aggressively. Screenplay activates a JavaScript-based workaround when native log-collection is not viable. + /// That JavaScript-based approach is coordinated from the Task + /// and ultimately the Action . + /// This functionality is consumed by the logic of two Tasks: + /// + /// + /// + /// + /// + /// + /// In both cases, these tasks will execute the logic of . + /// In determining whether the JavaScript approach is applicable, the task verifies that this setting is , + /// and whether the current web driver implementation requires the JavaScript workaround to get logs. + /// That is - the is not remote, and it does have the quirk + /// and it does not have the quirk + /// . If all these criteria are satisfied, then the action + /// is executed, to begin collecting logs in JavaScript. + /// + /// + /// It's important to understand that the JavaScript workaround for getting console logs is imperfect. + /// Logs are only collected from the point at which the 'begin collection' script is sent to the web browser. + /// Any logs which were written before that script arrives will be missed and will be unavailable for reading. + /// This is why the collection begins immediately after two tasks which cause page reloads. + /// Even so, imagine the loading of a new web page, and immediately as the page loads, there is an important console message written. + /// It's quite possible that - in the brief (perhaps milliseconds) delay between the page load completing and the "Begin + /// collecting logs" script arriving, that the important message was already written to the logs. In this circumstance + /// the important message will not be available to Screenplay. Unfortunately there is no better solution or workaround to + /// this problem when using Selenium. /// /// public bool ShouldCollectLogs => collectLogs; From fce0e0869ce91faa1740022768b3f6a78f584071 Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Sun, 14 Jun 2026 14:13:27 +0100 Subject: [PATCH 2/2] Resolve #225 - Add sample task docs --- .../AvoidBranchingLogic.md | 15 +++++--- .../docs/writingPerformables/index.md | 35 +++++++++++++++++-- 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/CSF.Screenplay.Docs/docs/writingPerformables/AvoidBranchingLogic.md b/CSF.Screenplay.Docs/docs/writingPerformables/AvoidBranchingLogic.md index 67cc91af..83321f79 100644 --- a/CSF.Screenplay.Docs/docs/writingPerformables/AvoidBranchingLogic.md +++ b/CSF.Screenplay.Docs/docs/writingPerformables/AvoidBranchingLogic.md @@ -3,17 +3,22 @@ [Performances] are written _a lot like scripts_. Where possible, [Performables] should avoid branching or looping logic. _This is particularly true_ when Screenplay is being used [as a tool for testing]. -Good test logic has [a cyclomatic complexity] of precisely one. +Good test logic has [a cyclomatic complexity] as close to one as possible. Sometimes looping logic is unavoidable and desirable in a Screenplay, imagine a performable which has an [Actor] repeat a process `N` times. -This is acceptable if used judiciously. +Sometimes branching logic is required to deal with external systems which present differing behaviour. +The [Selenium Extension] makes use of this technique to accomodate different web browsers and web driver implementations. +Such logic is acceptable if used judiciously. -Performables should always avoid branching logic like `if` or `switch` though, and _should never_ contain such logic when being used for tests. -If more than one mode of operation is required then write more than one performable. -The path through a Performance should be completely deterministic, short of an unexpected error or failure. +Perhaps the best guideline is for the developer to ask themselves: + +> Could I separate this into two or more tasks and decide which I need at the point of usage? + +If the answer is "yes", then it's often better to do just that, rather than write a multi-function Task. [Performances]: xref:CSF.Screenplay.IPerformance [Performables]: ../../glossary/Performable.md [as a tool for testing]: ../bestPractice/SuitabilityAsATestingTool.md [a cyclomatic complexity]: https://en.wikipedia.org/wiki/Cyclomatic_complexity [Actor]: xref:CSF.Screenplay.Actor +[Selenium Extension]: ../extensions/selenium/index.md \ No newline at end of file diff --git a/CSF.Screenplay.Docs/docs/writingPerformables/index.md b/CSF.Screenplay.Docs/docs/writingPerformables/index.md index bbbffb3c..b34b85b6 100644 --- a/CSF.Screenplay.Docs/docs/writingPerformables/index.md +++ b/CSF.Screenplay.Docs/docs/writingPerformables/index.md @@ -12,9 +12,40 @@ On the other hand, it is very likely that developers will need to write [Tasks], ## A sample Task -Here is an annotated example of a [Task] which makes use of a fictitious Action and a fictitious Question. +Here is an annotated example of a [Task] which makes use of a fictitious Action and Question. -TODO: Write this docco +```csharp +// A parameter is constructor injected, so that this task may be parameterised. +// This is entirely optional, if no parameters are needed then none need be added. +public class BuyMilkIfNeeded(int cupsOfCoffee) : IPerformable, ICanReport +{ + // Always implement the ICanReport interface if possible. + // Write a human-readable report of what the Task does. + // There's no need to detail the work done by consumed performables; they will get their own reports. + public ReportFragment GetReportFragment(Actor actor, IFormatsReportFragment formatter) + => formatter.Format("{Actor} adds milk to their shopping list, if they need it to make {Cups} cup(s) of coffee", actor, cupsOfCoffee); + + public async ValueTask PerformAsAsync(ICanPerform actor, CancellationToken cancellationToken = default) + { + // Use of a fictitious Question, remember to await it and to pass the cancellation token. + // This question, which looks in the refridgerator and gets a decimal which represents the + // quantity of an item inside, measured in litres, is ficitious. + // It is consumed here from a fictitious builder method. + var litresOfMilk = await actor.PerformAsync(LookInTheRefridgeratorFor("Milk").AndTellMeHowMuchRemainsInLitres(), cancellationToken); + var isEnough = IsThisEnoughMilkToMakeCoffee(cupsOfCoffee, litresOfMilk); + if(!isEnough) + // Use of a fictitious Action, again remember to await it and pass the cancellation token. + await actor.PerformAsync(AddToMyShoppingList("Milk"), cancellationToken); + + } + + static bool IsThisEnoughMilkToMakeCoffee(int cupsOfCoffee, decimal litresOfMilk) + { + // Implementation omitted, determines if the litresOfMilk param is enough milk to make + // the indicated number of cupsOfCoffee. + } +} +``` [Task]: ../../glossary/Task.md