Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CSF.Screenplay.Docs/docs/extensions/selenium/Actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,16 @@ 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 |
| [`ExecuteJavaScript`] | Executes a JavaScript directly in the browser |
| [`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
Expand Down
18 changes: 14 additions & 4 deletions CSF.Screenplay.Docs/docs/extensions/selenium/Questions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -57,10 +63,14 @@ These questions don't neccesarily involve elements, instead they query the web b
| Question | Usage |
| -------- | ----- |
| [`ExecuteJavaScriptAndGetResult<TResult>`] | 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<TResult>`]: 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

13 changes: 11 additions & 2 deletions CSF.Screenplay.Docs/docs/extensions/selenium/Tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<input type="date">` 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
Original file line number Diff line number Diff line change
Expand Up @@ -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
35 changes: 33 additions & 2 deletions CSF.Screenplay.Docs/docs/writingPerformables/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,22 @@ namespace CSF.Screenplay.Selenium.Actions
/// <remarks>
/// <para>
/// When used, this action should be executed as soon as possible after the current page has completed loading.
/// Ideally, directly after <see cref="Tasks.ClickAndWaitForDocumentReady"/>.
/// Any log messages which have been sent to the native browser console before this action is executed will be missed
/// Ideally, directly after <see cref="Tasks.ClickAndWaitForDocumentReady"/> or <see cref="OpenUrl"/>.
/// Any log messages which have been sent to the native browser console <em>before this action is executed</em> will be missed
/// and will not be available to the counterpart question which retrieves log messages: <see cref="Questions.GetLogsWithJavaScript"/>.
/// </para>
/// <para>
/// 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 <em>does not need to be re-run</em> 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).
/// </para>
/// <para>
/// This action is for use only with web browsers which have the <see cref="BrowserQuirks.CanGetLogsWithJavascriptWorkaround"/> quirk.
/// </para>
/// </remarks>
/// <seealso cref="BrowseTheWeb.ShouldCollectLogs"/>
public class BeginCollectingLogsWithJavaScript : IPerformable, ICanReport
{
/// <inheritdoc/>
Expand Down
41 changes: 37 additions & 4 deletions CSF.Screenplay.Selenium/BrowseTheWeb.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// </para>
/// <code>
/// <code language="csharp">
/// using CSF.Extensions.WebDriver;
/// using CSF.Screenplay.Selenium;
///
Expand Down Expand Up @@ -97,9 +97,42 @@ public class BrowseTheWeb : ICanReport, IDisposable
/// </summary>
/// <remarks>
/// <para>
/// When this value is set to <see langword="true"/>, this will trigger usage of <see cref="Actions.BeginCollectingLogsWithJavaScript"/>
/// at points where it is required. This is applicable only when the current <see cref="WebDriver"/> implementation has the
/// quirk <see cref="BrowserQuirks.CanGetLogsWithJavascriptWorkaround"/>.
/// 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 <see cref="OpenQA.Selenium.Chrome.ChromeDriver"/> and <see cref="OpenQA.Selenium.Edge.EdgeDriver"/>.
/// For other web browsers/drivers, as well as all remote web drivers, Selenium is unable to directly access their
/// logs using a native API.
/// </para>
/// <para>
/// When this property is set to <see langword="true"/>, 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 <see cref="Tasks.BeginCollectingLogsWithJavaScriptIfApplicable"/>
/// and ultimately the Action <see cref="Actions.BeginCollectingLogsWithJavaScript"/>.
/// This functionality is consumed by the logic of two Tasks:
/// </para>
/// <list type="bullet">
/// <item><description><see cref="Tasks.NavigateToUrl"/></description></item>
/// <item><description><see cref="Tasks.ClickAndWaitForDocumentReady"/></description></item>
/// </list>
/// <para>
/// In both cases, these tasks will execute the logic of <see cref="Tasks.BeginCollectingLogsWithJavaScriptIfApplicable"/>.
/// In determining whether the JavaScript approach is applicable, the task verifies that this setting is <see langword="true"/>,
/// and whether the current web driver implementation requires the JavaScript workaround to get logs.
/// That is - the <see cref="WebDriver"/> is <b>not</b> remote, and it <b>does</b> have the quirk
/// <see cref="BrowserQuirks.CanGetLogsWithJavascriptWorkaround"/> and it <b>does not</b> have the quirk
/// <see cref="BrowserQuirks.HasNativeLogsSupport"/>. If all these criteria are satisfied, then the action
/// <see cref="Actions.BeginCollectingLogsWithJavaScript"/> is executed, to begin collecting logs in JavaScript.
/// </para>
/// <para>
/// It's important to understand that the JavaScript workaround for getting console logs <em>is imperfect</em>.
/// 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.
/// </para>
/// </remarks>
public bool ShouldCollectLogs => collectLogs;
Expand Down
Loading