Skip to content

Commit c8c8928

Browse files
authored
Merge pull request #264 from csf-dev/craigfowler/issue252
Resolve #252 - Wait until page loads
2 parents aad0b39 + 0da3197 commit c8c8928

35 files changed

Lines changed: 226 additions & 6912 deletions

.vscode/tasks.json

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,6 @@
4848
"problemMatcher": [],
4949
"label": "dotnet: test"
5050
},
51-
{
52-
"group": {
53-
"kind": "test",
54-
"isDefault": true
55-
},
56-
"command": "dotnet",
57-
"args": ["test", "-p:TargetFramework=net8.0"],
58-
"problemMatcher": [],
59-
"label": "dotnet: test (net8.0 only)"
60-
},
6151
{
6252
"group": {
6353
"kind": "test"
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using System;
2+
using CSF.Screenplay.Performables;
3+
using CSF.Screenplay.Selenium.Actions;
4+
using CSF.Screenplay.Selenium.Elements;
5+
using CSF.Screenplay.Selenium.Tasks;
6+
7+
namespace CSF.Screenplay.Selenium.Builders
8+
{
9+
/// <summary>
10+
/// Builder for creating click actions on a target element.
11+
/// </summary>
12+
public class ClickBuilder : IGetsPerformable
13+
{
14+
readonly ITarget target;
15+
16+
/// <inheritdoc/>
17+
public IPerformable GetPerformable()
18+
=> SingleElementPerformableAdapter.From(new Click(), target);
19+
20+
/// <summary>
21+
/// Gets a more sophisticated <see cref="IPerformable"/> which waits for a page-load to complete after clicking.
22+
/// </summary>
23+
/// <remarks>
24+
/// <para>
25+
/// Use this method when the click is expected to cause a new web page to load into the browser.
26+
/// In that case, the performable returned by this method will not only click on the target element.
27+
/// It will also wait for the <c>DOMContentLoaded</c> event from the web page which is loaded following that click.
28+
/// This ensures that subsequent interactions with the Web Browser are not performed upon a page which is not yet loaded.
29+
/// </para>
30+
/// <para>
31+
/// Note that the meaning of "a new page loading" is a full Web Browser page load (an entirely new HTML document).
32+
/// It does not mean an SPA/JavaScript-based navigation. This method is not for JavaScrpit/SPA navigation.
33+
/// </para>
34+
/// </remarks>
35+
/// <param name="forAtMost"></param>
36+
/// <returns></returns>
37+
public IPerformable AndWaitForANewPageToLoad(TimeSpan? forAtMost = null)
38+
=> SingleElementPerformableAdapter.From(new ClickAndWaitForDocumentReady(forAtMost ?? TimeSpan.FromSeconds(5)), target);
39+
40+
/// <summary>
41+
/// Initializes a new instance of the <see cref="ClickBuilder"/> class with the specified target.
42+
/// </summary>
43+
/// <param name="target">The target element to click. Cannot be null.</param>
44+
/// <exception cref="ArgumentNullException">Thrown when <paramref name="target"/> is null.</exception>
45+
public ClickBuilder(ITarget target)
46+
{
47+
this.target = target ?? throw new ArgumentNullException(nameof(target));
48+
}
49+
}
50+
}

CSF.Screenplay.Selenium/PerformableBuilder.elementPerformables.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ public static partial class PerformableBuilder
1111
/// </summary>
1212
/// <param name="target">The target element on which to click.</param>
1313
/// <returns>A performable action</returns>
14-
public static IPerformable ClickOn(ITarget target) => SingleElementPerformableAdapter.From(new Click(), target);
14+
public static ClickBuilder ClickOn(ITarget target) => new ClickBuilder(target);
1515

1616
/// <summary>
1717
/// Gets a builder for creating a performable action which represents an actor typing text into a target element.

CSF.Screenplay.Selenium/Resources/ScriptResources.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,7 @@ static class ScriptResources
1111

1212
/// <summary>Gets a short JavaScript for <see cref="Actions.ClearLocalStorage"/>.</summary>
1313
internal static string ClearLocalStorage => resourceManager.GetString("ClearLocalStorage");
14+
15+
internal static string GetDocReadyState => resourceManager.GetString("GetDocReadyState");
1416
}
1517
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
ClearLocalStorage = localStorage.clear()
1+
ClearLocalStorage = localStorage.clear()
2+
GetDocReadyState = return document.readyState

CSF.Screenplay.Selenium/Scripts.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,15 @@ public static class Scripts
1818
/// <returns>A named script.</returns>
1919
public static NamedScript ClearLocalStorage
2020
=> new NamedScript(Resources.ScriptResources.ClearLocalStorage, "clear the local storage");
21+
22+
/// <summary>
23+
/// Gets a <see cref="NamedScriptWithResult{TResult}"/> which gets the value of <c>document.readyState</c> for the
24+
/// current page.
25+
/// </summary>
26+
/// <remarks>
27+
/// <para>You may use this script to determine whether the page has finished loading.</para>
28+
/// </remarks>
29+
public static NamedScriptWithResult<string> GetTheDocumentReadyState
30+
=> new NamedScriptWithResult<string>(Resources.ScriptResources.GetDocReadyState, "get the readiness of the current page");
2131
}
2232
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
using System;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
using CSF.Screenplay.Selenium.Actions;
5+
using CSF.Screenplay.Selenium.Elements;
6+
using OpenQA.Selenium;
7+
using OpenQA.Selenium.Support.Extensions;
8+
using static CSF.Screenplay.Selenium.PerformableBuilder;
9+
10+
namespace CSF.Screenplay.Selenium.Tasks
11+
{
12+
/// <summary>
13+
/// Screenplay task similar to <see cref="Actions.Click"/> but which additionally waits for a page-load to complete after clicking.
14+
/// </summary>
15+
/// <remarks>
16+
/// <para>
17+
/// Use this task via <c>ClickOn(element).AndWaitForANewPageToLoad()</c>.
18+
/// The benefit of this task is that it ensures that (following a page-load navigation), the incoming page is ready before
19+
/// subsequent performables are executed.
20+
/// </para>
21+
/// </remarks>
22+
public class ClickAndWaitForDocumentReady : ISingleElementPerformable
23+
{
24+
const string COMPLETE_READY_STATE = "complete";
25+
26+
static readonly NamedScriptWithResult<string> getReadyState = Scripts.GetTheDocumentReadyState;
27+
static readonly TimeSpan
28+
pollingInterval = TimeSpan.FromMilliseconds(100),
29+
stalenessTimeout = TimeSpan.FromMilliseconds(500);
30+
31+
readonly TimeSpan waitTimeout;
32+
33+
/// <inheritdoc/>
34+
public ReportFragment GetReportFragment(Actor actor, Lazy<SeleniumElement> element, IFormatsReportFragment formatter)
35+
=> formatter.Format("{Actor} clicks on {Element} and waits up to {Time} for the next page to load", actor, element.Value, waitTimeout);
36+
37+
/// <inheritdoc/>
38+
public async ValueTask PerformAsAsync(ICanPerform actor, IWebDriver webDriver, Lazy<SeleniumElement> element, CancellationToken cancellationToken = default)
39+
{
40+
await actor.PerformAsync(ClickOn(element.Value), cancellationToken);
41+
await actor.PerformAsync(WaitUntil(ElementIsStale(element.Value.WebElement))
42+
.ForAtMost(stalenessTimeout)
43+
.WithPollingInterval(pollingInterval)
44+
.Named($"{element.Value.Name} is no longer on the page"),
45+
cancellationToken);
46+
await actor.PerformAsync(WaitUntil(PageIsReady).ForAtMost(waitTimeout).Named("the page is ready").WithPollingInterval(pollingInterval),
47+
cancellationToken);
48+
}
49+
50+
static Func<IWebDriver,bool> ElementIsStale(IWebElement element)
51+
{
52+
if (element is null) throw new ArgumentNullException(nameof(element));
53+
54+
return driver =>
55+
{
56+
try
57+
{
58+
var _ = element.Enabled;
59+
return false;
60+
}
61+
catch(StaleElementReferenceException)
62+
{
63+
return true;
64+
}
65+
};
66+
}
67+
68+
static Func<IWebDriver,bool> PageIsReady => driver => driver.ExecuteJavaScript<string>(getReadyState.ScriptBody) == COMPLETE_READY_STATE;
69+
70+
/// <summary>
71+
/// Initializes a new instance of the <see cref="ClickAndWaitForDocumentReady"/> class.
72+
/// </summary>
73+
/// <param name="waitTimeout">The maximum duration to wait for the document to be ready.</param>
74+
public ClickAndWaitForDocumentReady(TimeSpan waitTimeout)
75+
{
76+
this.waitTimeout = waitTimeout;
77+
}
78+
}
79+
}

Old/CSF.Screenplay.Selenium.Tests_old/Tasks/NavigateToNewPageByClickingTests.cs

Lines changed: 0 additions & 32 deletions
This file was deleted.

Old/CSF.Screenplay.Selenium_old/Builders/Navigate.cs

Lines changed: 0 additions & 65 deletions
This file was deleted.

Old/CSF.Screenplay.Selenium_old/Tasks/NavigateToNewPageByClicking.cs

Lines changed: 0 additions & 96 deletions
This file was deleted.

0 commit comments

Comments
 (0)