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
10 changes: 0 additions & 10 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,6 @@
"problemMatcher": [],
"label": "dotnet: test"
},
{
"group": {
"kind": "test",
"isDefault": true
},
"command": "dotnet",
"args": ["test", "-p:TargetFramework=net8.0"],
"problemMatcher": [],
"label": "dotnet: test (net8.0 only)"
},
{
"group": {
"kind": "test"
Expand Down
50 changes: 50 additions & 0 deletions CSF.Screenplay.Selenium/Builders/ClickBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System;
using CSF.Screenplay.Performables;
using CSF.Screenplay.Selenium.Actions;
using CSF.Screenplay.Selenium.Elements;
using CSF.Screenplay.Selenium.Tasks;

namespace CSF.Screenplay.Selenium.Builders
{
/// <summary>
/// Builder for creating click actions on a target element.
/// </summary>
public class ClickBuilder : IGetsPerformable
{
readonly ITarget target;

/// <inheritdoc/>
public IPerformable GetPerformable()
=> SingleElementPerformableAdapter.From(new Click(), target);

/// <summary>
/// Gets a more sophisticated <see cref="IPerformable"/> which waits for a page-load to complete after clicking.
/// </summary>
/// <remarks>
/// <para>
/// 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 <c>DOMContentLoaded</c> 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.
/// </para>
/// <para>
/// Note that the meaning of "a new page loading" is a full Web Browser page load (an entirely new HTML document).
/// It does not mean an SPA/JavaScript-based navigation. This method is not for JavaScrpit/SPA navigation.
/// </para>
/// </remarks>
/// <param name="forAtMost"></param>
/// <returns></returns>
public IPerformable AndWaitForANewPageToLoad(TimeSpan? forAtMost = null)
=> SingleElementPerformableAdapter.From(new ClickAndWaitForDocumentReady(forAtMost ?? TimeSpan.FromSeconds(5)), target);

/// <summary>
/// Initializes a new instance of the <see cref="ClickBuilder"/> class with the specified target.
/// </summary>
/// <param name="target">The target element to click. Cannot be null.</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="target"/> is null.</exception>
public ClickBuilder(ITarget target)
{
this.target = target ?? throw new ArgumentNullException(nameof(target));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public static partial class PerformableBuilder
/// </summary>
/// <param name="target">The target element on which to click.</param>
/// <returns>A performable action</returns>
public static IPerformable ClickOn(ITarget target) => SingleElementPerformableAdapter.From(new Click(), target);
public static ClickBuilder ClickOn(ITarget target) => new ClickBuilder(target);

/// <summary>
/// Gets a builder for creating a performable action which represents an actor typing text into a target element.
Expand Down
2 changes: 2 additions & 0 deletions CSF.Screenplay.Selenium/Resources/ScriptResources.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,7 @@ static class ScriptResources

/// <summary>Gets a short JavaScript for <see cref="Actions.ClearLocalStorage"/>.</summary>
internal static string ClearLocalStorage => resourceManager.GetString("ClearLocalStorage");

internal static string GetDocReadyState => resourceManager.GetString("GetDocReadyState");
}
}
3 changes: 2 additions & 1 deletion CSF.Screenplay.Selenium/Resources/ScriptResources.restext
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
ClearLocalStorage = localStorage.clear()
ClearLocalStorage = localStorage.clear()
GetDocReadyState = return document.readyState
10 changes: 10 additions & 0 deletions CSF.Screenplay.Selenium/Scripts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,15 @@ public static class Scripts
/// <returns>A named script.</returns>
public static NamedScript ClearLocalStorage
=> new NamedScript(Resources.ScriptResources.ClearLocalStorage, "clear the local storage");

/// <summary>
/// Gets a <see cref="NamedScriptWithResult{TResult}"/> which gets the value of <c>document.readyState</c> for the
/// current page.
/// </summary>
/// <remarks>
/// <para>You may use this script to determine whether the page has finished loading.</para>
/// </remarks>
public static NamedScriptWithResult<string> GetTheDocumentReadyState
=> new NamedScriptWithResult<string>(Resources.ScriptResources.GetDocReadyState, "get the readiness of the current page");
}
}
79 changes: 79 additions & 0 deletions CSF.Screenplay.Selenium/Tasks/ClickAndWaitForDocumentReady.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using CSF.Screenplay.Selenium.Actions;
using CSF.Screenplay.Selenium.Elements;
using OpenQA.Selenium;
using OpenQA.Selenium.Support.Extensions;
using static CSF.Screenplay.Selenium.PerformableBuilder;

namespace CSF.Screenplay.Selenium.Tasks
{
/// <summary>
/// Screenplay task similar to <see cref="Actions.Click"/> but which additionally waits for a page-load to complete after clicking.
/// </summary>
/// <remarks>
/// <para>
/// Use this task via <c>ClickOn(element).AndWaitForANewPageToLoad()</c>.
/// 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.
/// </para>
/// </remarks>
public class ClickAndWaitForDocumentReady : ISingleElementPerformable
{
const string COMPLETE_READY_STATE = "complete";

static readonly NamedScriptWithResult<string> getReadyState = Scripts.GetTheDocumentReadyState;
static readonly TimeSpan
pollingInterval = TimeSpan.FromMilliseconds(100),
stalenessTimeout = TimeSpan.FromMilliseconds(500);

readonly TimeSpan waitTimeout;

/// <inheritdoc/>
public ReportFragment GetReportFragment(Actor actor, Lazy<SeleniumElement> element, IFormatsReportFragment formatter)
=> formatter.Format("{Actor} clicks on {Element} and waits up to {Time} for the next page to load", actor, element.Value, waitTimeout);

/// <inheritdoc/>
public async ValueTask PerformAsAsync(ICanPerform actor, IWebDriver webDriver, Lazy<SeleniumElement> element, CancellationToken cancellationToken = default)
{
await actor.PerformAsync(ClickOn(element.Value), cancellationToken);
await actor.PerformAsync(WaitUntil(ElementIsStale(element.Value.WebElement))
.ForAtMost(stalenessTimeout)
.WithPollingInterval(pollingInterval)
.Named($"{element.Value.Name} is no longer on the page"),
cancellationToken);
await actor.PerformAsync(WaitUntil(PageIsReady).ForAtMost(waitTimeout).Named("the page is ready").WithPollingInterval(pollingInterval),
cancellationToken);
}

static Func<IWebDriver,bool> ElementIsStale(IWebElement element)
{
if (element is null) throw new ArgumentNullException(nameof(element));

return driver =>
{
try
{
var _ = element.Enabled;
return false;
}
catch(StaleElementReferenceException)
{
return true;
}
};
}

static Func<IWebDriver,bool> PageIsReady => driver => driver.ExecuteJavaScript<string>(getReadyState.ScriptBody) == COMPLETE_READY_STATE;

/// <summary>
/// Initializes a new instance of the <see cref="ClickAndWaitForDocumentReady"/> class.
/// </summary>
/// <param name="waitTimeout">The maximum duration to wait for the document to be ready.</param>
public ClickAndWaitForDocumentReady(TimeSpan waitTimeout)
{
this.waitTimeout = waitTimeout;
}
}
}

This file was deleted.

65 changes: 0 additions & 65 deletions Old/CSF.Screenplay.Selenium_old/Builders/Navigate.cs

This file was deleted.

This file was deleted.

Loading