From 7c7caf9dbd34aca8461211e42b5ff09d1469487c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Kondratiuk?= Date: Tue, 23 Jun 2026 10:48:00 -0300 Subject: [PATCH] feat: add WaitForFunctionAsync to WebWorker (upstream #15100) Ports upstream puppeteer#15100 which adds waitForFunction support to WebWorkers. Uses interval-based polling (100ms default) matching upstream behavior, since workers don't have requestAnimationFrame. Also fixes injected.js to lazily initialize MutationObserver so it doesn't throw when loaded in a worker context where MutationObserver is unavailable. Co-Authored-By: Claude Sonnet 4.6 --- .../WorkerTests/WorkerWaitForFunctionTests.cs | 99 +++++++++++++++++++ lib/PuppeteerSharp/Injected/injected.js | 20 +++- lib/PuppeteerSharp/WebWorker.cs | 27 +++++ 3 files changed, 141 insertions(+), 5 deletions(-) create mode 100644 lib/PuppeteerSharp.Tests/WorkerTests/WorkerWaitForFunctionTests.cs diff --git a/lib/PuppeteerSharp.Tests/WorkerTests/WorkerWaitForFunctionTests.cs b/lib/PuppeteerSharp.Tests/WorkerTests/WorkerWaitForFunctionTests.cs new file mode 100644 index 000000000..a1efc85c9 --- /dev/null +++ b/lib/PuppeteerSharp.Tests/WorkerTests/WorkerWaitForFunctionTests.cs @@ -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 CreateWorkerAsync(string workerScript = "1") + { + var workerCreatedTcs = new TaskCompletionSource(); + 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(); + 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(); + 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(); + 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(); + 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(); + } + } +} diff --git a/lib/PuppeteerSharp/Injected/injected.js b/lib/PuppeteerSharp/Injected/injected.js index 3ca5b20fd..3dddc138f 100644 --- a/lib/PuppeteerSharp/Injected/injected.js +++ b/lib/PuppeteerSharp/Injected/injected.js @@ -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) { @@ -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, diff --git a/lib/PuppeteerSharp/WebWorker.cs b/lib/PuppeteerSharp/WebWorker.cs index 815bb71a8..6c161ef38 100644 --- a/lib/PuppeteerSharp/WebWorker.cs +++ b/lib/PuppeteerSharp/WebWorker.cs @@ -99,6 +99,33 @@ public async Task EvaluateFunctionAsync(string script, params object[] arg public async Task EvaluateExpressionHandleAsync(string script) => await World.EvaluateExpressionHandleAsync(script).ConfigureAwait(false); + /// + /// Waits for the provided function, , to return a truthy value when + /// evaluated in the worker's context. + /// + /// Function to be evaluated in the worker context until it returns a truthy value. + /// Options for configuring waiting behavior. + /// Arguments to pass to . + /// A that resolves to a of the truthy value returned by the function. + public Task 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); + } + /// /// Closes the worker. ///