Skip to content

Commit

Permalink
Some improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
kblok committed Apr 21, 2023
1 parent 294c525 commit d66ee38
Show file tree
Hide file tree
Showing 7 changed files with 120 additions and 97 deletions.
25 changes: 12 additions & 13 deletions lib/PuppeteerSharp.Tests/WaitTaskTests/FrameWaitForFunctionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,30 +31,29 @@ public async Task ShouldWorkWhenResolvedRightBeforeExecutionContextDisposal()
[PuppeteerFact]
public async Task ShouldPollOnInterval()
{
var success = false;
var startTime = DateTime.UtcNow;
var polling = 100;
var watchdog = Page.WaitForFunctionAsync("() => window.__FOO === 'hit'", new WaitForFunctionOptions { PollingInterval = polling })
.ContinueWith(_ => success = true);
await Page.EvaluateExpressionAsync("window.__FOO = 'hit'");
Assert.False(success);
var watchdog = Page.WaitForFunctionAsync("() => window.__FOO === 'hit'", new WaitForFunctionOptions { PollingInterval = polling });
// Wait for function will release the execution faster than in node.
// We add some CDP action to wait for the task to start the polling
await Page.EvaluateExpressionAsync("document.body.appendChild(document.createElement('div'))");
await Page.EvaluateFunctionAsync("() => setTimeout(window.__FOO = 'hit', 50)");
await watchdog;
System.Console.WriteLine((DateTime.UtcNow - startTime).TotalMilliseconds);
Assert.True((DateTime.UtcNow - startTime).TotalMilliseconds > polling / 2);
}

[PuppeteerTest("waittask.spec.ts", "Frame.waitForFunction", "should poll on interval async")]
[PuppeteerFact]
public async Task ShouldPollOnIntervalAsync()
{
var success = false;
var startTime = DateTime.UtcNow;
var polling = 100;
var watchdog = Page.WaitForFunctionAsync("async () => window.__FOO === 'hit'", new WaitForFunctionOptions { PollingInterval = polling })
.ContinueWith(_ => success = true);
await Page.EvaluateFunctionAsync("async () => window.__FOO = 'hit'");
Assert.False(success);
var polling = 1000;
var watchdog = Page.WaitForFunctionAsync("async () => window.__FOO === 'hit'", new WaitForFunctionOptions { PollingInterval = polling });
// Wait for function will release the execution faster than in node.
// We add some CDP action to wait for the task to start the polling
await Page.EvaluateExpressionAsync("document.body.appendChild(document.createElement('div'))");
await Page.EvaluateFunctionAsync("async () => setTimeout(window.__FOO = 'hit', 50)");
await watchdog;
Assert.True((DateTime.UtcNow - startTime).TotalMilliseconds > polling / 2);
}
Expand Down Expand Up @@ -162,7 +161,7 @@ public async Task ShouldRespectTimeout()
var exception = await Assert.ThrowsAsync<WaitTaskTimeoutException>(()
=> Page.WaitForExpressionAsync("false", new WaitForFunctionOptions { Timeout = 10 }));

Assert.Contains("waiting for function failed: timeout", exception.Message);
Assert.Contains("Waiting failed: 10ms exceeded", exception.Message);
}

[PuppeteerTest("waittask.spec.ts", "Frame.waitForFunction", "should respect default timeout")]
Expand All @@ -173,7 +172,7 @@ public async Task ShouldRespectDefaultTimeout()
var exception = await Assert.ThrowsAsync<WaitTaskTimeoutException>(()
=> Page.WaitForExpressionAsync("false"));

Assert.Contains("waiting for function failed: timeout", exception.Message);
Assert.Contains("Waiting failed: 1ms exceeded", exception.Message);
}

[PuppeteerTest("waittask.spec.ts", "Frame.waitForFunction", "should disable timeout when its set to 0")]
Expand Down
1 change: 1 addition & 0 deletions lib/PuppeteerSharp/AriaQueryHandlerFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ async Task<IElementHandle> WaitFor(IElementHandle root, string selector, WaitFor

return await frame.PuppeteerWorld.WaitForSelectorInPageAsync(
@"(_, selector) => globalThis.ariaQuerySelector(selector)",
element,
selector,
options,
new[] { binding }).ConfigureAwait(false);
Expand Down
4 changes: 2 additions & 2 deletions lib/PuppeteerSharp/CustomQueriesManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,9 @@ private static InternalQueryHandler MakeQueryHandler(CustomQueryHandler handler)
internalHandler.WaitFor = async (IElementHandle root, string selector, WaitForSelectorOptions options) =>
{
var frame = (root as ElementHandle).Frame;
var element = await frame.PuppeteerWorld.AdoptHandleAsync(root).ConfigureAwait(false);
var element = await frame.PuppeteerWorld.AdoptHandleAsync(root).ConfigureAwait(false) as IElementHandle;
return await frame.PuppeteerWorld.WaitForSelectorInPageAsync(handler.QueryOne, selector, options).ConfigureAwait(false);
return await frame.PuppeteerWorld.WaitForSelectorInPageAsync(handler.QueryOne, element, selector, options).ConfigureAwait(false);
};
}

Expand Down
55 changes: 25 additions & 30 deletions lib/PuppeteerSharp/IsolatedWorld.cs
Original file line number Diff line number Diff line change
Expand Up @@ -265,41 +265,40 @@ internal async Task SetContentAsync(string html, NavigationOptions options = nul
}
}

internal async Task<IElementHandle> WaitForSelectorInPageAsync(string queryOne, string selector, WaitForSelectorOptions options, PageBinding[] bindings = null)
internal async Task<IElementHandle> WaitForSelectorInPageAsync(string queryOne, IElementHandle root, string selector, WaitForSelectorOptions options, PageBinding[] bindings = null)
{
var waitForVisible = options?.Visible ?? false;
var waitForHidden = options?.Hidden ?? false;
var timeout = options?.Timeout ?? _timeoutSettings.Timeout;

var polling = waitForVisible || waitForHidden ? WaitForFunctionPollingOption.Raf : WaitForFunctionPollingOption.Mutation;
var title = $"selector '{selector}'{(waitForHidden ? " to be hidden" : string.Empty)}";

var predicate = @$"async function predicate(root, selector, waitForVisible, waitForHidden) {{
const node = predicateQueryHandler
? ((await predicateQueryHandler(root, selector)))
: root.querySelector(selector);
return checkWaitForOptions(node, waitForVisible, waitForHidden);
var predicate = @$"async (PuppeteerUtil, query, selector, root, visible) => {{
if (!PuppeteerUtil) {{
return;
}}
const node = (await PuppeteerUtil.createFunction(query)(
root || document,
selector
));
return PuppeteerUtil.checkVisibility(node, visible);
}}";

using var waitTask = new WaitTask(
this,
MakePredicateString(predicate, queryOne),
true,
title,
polling,
null,
timeout,
options?.Root,
bindings,
new object[]
var jsHandle = await WaitForFunctionAsync(
predicate,
new()
{
selector,
waitForVisible,
waitForHidden,
Bindings = bindings,
Polling = waitForVisible || waitForHidden ? WaitForFunctionPollingOption.Raf : WaitForFunctionPollingOption.Mutation,
Root = root,
Timeout = timeout,
},
true);
await GetPuppeteerUtilAsync().ConfigureAwait(false),
queryOne,
selector,
root,
waitForVisible ? true : waitForHidden ? false : null).ConfigureAwait(false);

var jsHandle = await waitTask.Task.ConfigureAwait(false);
if (jsHandle is not ElementHandle elementHandle)
{
await jsHandle.DisposeAsync().ConfigureAwait(false);
Expand Down Expand Up @@ -375,12 +374,11 @@ internal async Task<IJSHandle> WaitForFunctionAsync(string script, WaitForFuncti
this,
script,
false,
"function",
options.Polling,
options.PollingInterval,
options.Timeout ?? _timeoutSettings.Timeout,
null,
null,
options.Root,
options.Bindings,
args);

return await waitTask
Expand All @@ -394,14 +392,12 @@ internal async Task<IJSHandle> WaitForExpressionAsync(string script, WaitForFunc
this,
script,
true,
"function",
options.Polling,
options.PollingInterval,
options.Timeout ?? _timeoutSettings.Timeout,
null, // Root
null, // PageBinding
null, // args
false); // predicateAcceptsContextElement
null); // args

return await waitTask
.Task
Expand Down Expand Up @@ -626,7 +622,6 @@ private async Task<IElementHandle> WaitForSelectorOrXPathAsync(string selectorOr
this,
predicate,
false,
$"{(isXPath ? "XPath" : "selector")} '{selectorOrXPath}'{(options.Hidden ? " to be hidden" : string.Empty)}",
polling,
null, // Polling interval
timeout,
Expand Down
10 changes: 10 additions & 0 deletions lib/PuppeteerSharp/WaitForFunctionOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,15 @@ public class WaitForFunctionOptions
/// An interval at which the <c>pageFunction</c> is executed. If no value is specified will use <see cref="Polling"/>.
/// </summary>
public int? PollingInterval { get; set; }

/// <summary>
/// Root element.
/// </summary>
internal IElementHandle Root { get; set; }

/// <summary>
/// Page bindings.
/// </summary>
internal PageBinding[] Bindings { get; set; }
}
}
101 changes: 55 additions & 46 deletions lib/PuppeteerSharp/WaitTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,29 @@ internal sealed class WaitTask : IDisposable
{
private readonly IsolatedWorld _isolatedWorld;
private readonly string _fn;
private readonly WaitForFunctionPollingOption _polling;
private readonly WaitForFunctionPollingOption? _polling;
private readonly int? _pollingInterval;
private readonly object[] _args;
private readonly string _title;
private readonly Task _timeoutTimer;
private readonly IElementHandle _root;
private readonly bool _predicateAcceptsContextElement;
private readonly CancellationTokenSource _cts;
private readonly TaskCompletionSource<IJSHandle> _result;
private readonly PageBinding[] _bindings;

private bool _isDisposed;
private IJSHandle _poller;
private bool _terminated;

internal WaitTask(
IsolatedWorld isolatedWorld,
string fn,
bool isExpression,
string title,
WaitForFunctionPollingOption polling,
int? pollingInterval,
int timeout,
IElementHandle root,
PageBinding[] bidings = null,
object[] args = null,
bool predicateAcceptsContextElement = false)
object[] args = null)
{
if (string.IsNullOrEmpty(fn))
{
Expand All @@ -48,13 +45,11 @@ internal sealed class WaitTask : IDisposable

_isolatedWorld = isolatedWorld;
_fn = isExpression ? $"() => {{return ({fn});}}" : fn;
_polling = polling;
_pollingInterval = pollingInterval;
_polling = _pollingInterval.HasValue ? null : polling;
_args = args ?? Array.Empty<object>();
_title = title;
_root = root;
_cts = new CancellationTokenSource();
_predicateAcceptsContextElement = predicateAcceptsContextElement;
_result = new TaskCompletionSource<IJSHandle>(TaskCreationOptions.RunContinuationsAsynchronously);
_bindings = bidings ?? Array.Empty<PageBinding>();

Expand All @@ -69,7 +64,7 @@ internal sealed class WaitTask : IDisposable
{
_timeoutTimer = System.Threading.Tasks.Task.Delay(timeout, _cts.Token)
.ContinueWith(
_ => TerminateAsync(new WaitTaskTimeoutException(timeout, title)),
_ => TerminateAsync(new WaitTaskTimeoutException(timeout)),
TaskScheduler.Default);
}

Expand Down Expand Up @@ -97,50 +92,57 @@ internal async Task Rerun()
var context = await _isolatedWorld.GetExecutionContextAsync().ConfigureAwait(false);
await System.Threading.Tasks.Task.WhenAll(_bindings.Select(binding => _isolatedWorld.AddBindingToContextAsync(context, binding.Name))).ConfigureAwait(false);

_poller = _polling switch
if (_pollingInterval.HasValue)
{
WaitForFunctionPollingOption.Raf => await _isolatedWorld.EvaluateFunctionHandleAsync(
@"
({RAFPoller, createFunction}, fn, ...args) => {
const fun = createFunction(fn);
return new RAFPoller(() => {
_poller = await _isolatedWorld.EvaluateFunctionHandleAsync(
@"
({IntervalPoller, createFunction}, ms, fn, ...args) => {
const fun = createFunction(fn);
return new IntervalPoller(() => {
return fun(...args);
});
}",
new object[]
{
}, ms);
}",
new object[]
{
await _isolatedWorld.GetPuppeteerUtilAsync().ConfigureAwait(false),
_pollingInterval,
_fn,
}.Concat(_args).ToArray()).ConfigureAwait(false),
WaitForFunctionPollingOption.Mutation => await _isolatedWorld.EvaluateFunctionHandleAsync(
@"
({MutationPoller, createFunction}, root, fn, ...args) => {
const fun = createFunction(fn);
return new MutationPoller(() => {
return fun(...args);
}, root || document);
}",
new object[]
{
}.Concat(_args).ToArray()).ConfigureAwait(false);
}
else if (_polling == WaitForFunctionPollingOption.Raf)
{
_poller = await _isolatedWorld.EvaluateFunctionHandleAsync(
@"
({RAFPoller, createFunction}, fn, ...args) => {
const fun = createFunction(fn);
return new RAFPoller(() => {
return fun(...args);
});
}",
new object[]
{
await _isolatedWorld.GetPuppeteerUtilAsync().ConfigureAwait(false),
_root,
_fn,
}.Concat(_args).ToArray()).ConfigureAwait(false),
_ => await _isolatedWorld.EvaluateFunctionHandleAsync(
@"
({IntervalPoller, createFunction}, ms, fn, ...args) => {
const fun = createFunction(fn);
return new IntervalPoller(() => {
return fun(...args);
}, ms);
}",
new object[]
{
}.Concat(_args).ToArray()).ConfigureAwait(false);
}
else
{
_poller = await _isolatedWorld.EvaluateFunctionHandleAsync(
@"
({MutationPoller, createFunction}, root, fn, ...args) => {
const fun = createFunction(fn);
return new MutationPoller(() => {
return fun(...args);
}, root || document);
}",
new object[]
{
await _isolatedWorld.GetPuppeteerUtilAsync().ConfigureAwait(false),
_pollingInterval,
_root,
_fn,
}.Concat(_args).ToArray()).ConfigureAwait(false),
};
}.Concat(_args).ToArray()).ConfigureAwait(false);
}

await _poller.EvaluateFunctionAsync("poller => poller.start()").ConfigureAwait(false);

var success = await _poller.EvaluateFunctionHandleAsync("poller => poller.result()").ConfigureAwait(false);
Expand All @@ -159,6 +161,13 @@ internal async Task Rerun()

internal async Task TerminateAsync(Exception exception = null)
{
// The timeout timer might call this method on cleanup
if (_terminated)
{
return;
}

_terminated = true;
_isolatedWorld.TaskManager.Delete(this);
Cleanup(); // This matches the clearTimeout upstream

Expand Down
Loading

0 comments on commit d66ee38

Please sign in to comment.