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
99 changes: 99 additions & 0 deletions lib/PuppeteerSharp.Tests/WorkerTests/WorkerWaitForFunctionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
using System;
using System.Threading.Tasks;
using NUnit.Framework;
using PuppeteerSharp.Helpers;
using PuppeteerSharp.Nunit;

namespace PuppeteerSharp.Tests.WorkerTests
{
public class WorkerWaitForFunctionTests : PuppeteerPageBaseTest
{
public WorkerWaitForFunctionTests() : base()
{
}

private async Task<WebWorker> CreateWorkerAsync(string workerScript = "1")
{
var workerCreatedTcs = new TaskCompletionSource<WebWorker>();
Page.WorkerCreated += (_, e) => workerCreatedTcs.TrySetResult(e.Worker);
await Page.EvaluateFunctionAsync($"() => new Worker('data:text/javascript,{workerScript}')");
return await workerCreatedTcs.Task;
}

[Test, PuppeteerTest("worker.spec", "Workers waitForFunction", "should wait for a condition")]
public async Task ShouldWaitForACondition()
{
var workerCreatedTcs = new TaskCompletionSource<WebWorker>();
Page.WorkerCreated += (_, e) => workerCreatedTcs.TrySetResult(e.Worker);
await Page.EvaluateFunctionAsync(@"() => new Worker(`data:text/javascript,
setTimeout(() => {
self.foo = true;
}, 500);
`)");
var worker = await workerCreatedTcs.Task;

await worker.WaitForFunctionAsync("() => self.foo === true").WithTimeout();
}

[Test, PuppeteerTest("worker.spec", "Workers waitForFunction", "should timeout if condition is not met")]
public async Task ShouldTimeoutIfConditionIsNotMet()
{
var worker = await CreateWorkerAsync();

Exception error = null;
try
{
await worker.WaitForFunctionAsync(
"() => false",
new WaitForFunctionOptions { Timeout = 50 });
}
catch (Exception ex)
{
error = ex;
}

Assert.That(error, Is.Not.Null);
Assert.That(error.Message, Does.Contain("Waiting failed"));
}

[Test, PuppeteerTest("worker.spec", "Workers waitForFunction", "should return a JSHandle to a string and parse it")]
public async Task ShouldReturnJSHandleToStringAndParseIt()
{
var workerCreatedTcs = new TaskCompletionSource<WebWorker>();
Page.WorkerCreated += (_, e) => workerCreatedTcs.TrySetResult(e.Worker);
await Page.EvaluateFunctionAsync(@"() => new Worker(`data:text/javascript,
setTimeout(() => {
self.status = 'ready';
}, 500);
`)");
var worker = await workerCreatedTcs.Task;

await using var handle = await worker.WaitForFunctionAsync(
"() => self.status === 'ready' ? 'Operation Success' : false").WithTimeout();

var result = await handle.JsonValueAsync<string>();
Assert.That(result, Is.EqualTo("Operation Success"));
}

[Test, PuppeteerTest("worker.spec", "Workers waitForFunction", "should work with JSHandle as an argument")]
public async Task ShouldWorkWithJSHandleAsArgument()
{
var workerCreatedTcs = new TaskCompletionSource<WebWorker>();
Page.WorkerCreated += (_, e) => workerCreatedTcs.TrySetResult(e.Worker);
await Page.EvaluateFunctionAsync(@"() => new Worker(`data:text/javascript,
self.targetValue = 42;
`)");
var worker = await workerCreatedTcs.Task;

// Wait briefly to let the worker initialize
await Task.Delay(200);

await using var argHandle = await worker.EvaluateExpressionHandleAsync("42");

await worker.WaitForFunctionAsync(
"(expected) => self.targetValue === expected",
null,
argHandle).WithTimeout();
}
}
}
20 changes: 15 additions & 5 deletions lib/PuppeteerSharp/Injected/injected.js
Original file line number Diff line number Diff line change
Expand Up @@ -395,11 +395,21 @@ var eraseFromCache = (node) => {
}
};
var observedNodes = /* @__PURE__ */ new WeakSet();
var textChangeObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
eraseFromCache(mutation.target);
var textChangeObserver;
var getTextChangeObserver = () => {
const MutationObserverImpl = globalThis.MutationObserver;
if (!MutationObserverImpl) {
throw new Error("MutationObserver is not available in this environment.");
}
if (!textChangeObserver) {
textChangeObserver = new MutationObserverImpl((mutations) => {
for (const mutation of mutations) {
eraseFromCache(mutation.target);
}
});
}
});
return textChangeObserver;
};
var createTextContent = (root) => {
let value = textContentCache.get(root);
if (value) {
Expand Down Expand Up @@ -442,7 +452,7 @@ var createTextContent = (root) => {
value.full += createTextContent(root.shadowRoot).full;
}
if (!observedNodes.has(root)) {
textChangeObserver.observe(root, {
getTextChangeObserver().observe(root, {
childList: true,
characterData: true,
subtree: true,
Expand Down
27 changes: 27 additions & 0 deletions lib/PuppeteerSharp/WebWorker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,33 @@ public async Task<T> EvaluateFunctionAsync<T>(string script, params object[] arg
public async Task<IJSHandle> EvaluateExpressionHandleAsync(string script)
=> await World.EvaluateExpressionHandleAsync(script).ConfigureAwait(false);

/// <summary>
/// Waits for the provided function, <paramref name="script"/>, to return a truthy value when
/// evaluated in the worker's context.
/// </summary>
/// <param name="script">Function to be evaluated in the worker context until it returns a truthy value.</param>
/// <param name="options">Options for configuring waiting behavior.</param>
/// <param name="args">Arguments to pass to <paramref name="script"/>.</param>
/// <returns>A <see cref="Task"/> that resolves to a <see cref="IJSHandle"/> of the truthy value returned by the function.</returns>
public Task<IJSHandle> WaitForFunctionAsync(string script, WaitForFunctionOptions options = null, params object[] args)
{
var opts = options ?? new WaitForFunctionOptions();

// Default to interval polling (100ms) for workers, matching upstream behavior.
if (!opts.PollingInterval.HasValue && opts.Polling == WaitForFunctionPollingOption.Raf)
{
opts = new WaitForFunctionOptions
{
PollingInterval = 100,
Timeout = opts.Timeout,
Root = opts.Root,
CancellationToken = opts.CancellationToken,
};
}

return World.WaitForFunctionAsync(script, opts, args);
}

/// <summary>
/// Closes the worker.
/// </summary>
Expand Down
Loading