From f24c854bde73d29b5a7a33404527119f4a052a82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Kondratiuk?= Date: Tue, 25 Apr 2023 22:14:38 -0300 Subject: [PATCH] Waittask refactor (#2190) * Waittask refactor * remove typescript code * Fix some javascript calls * Some improvements * cr * Fix some tests * passing tests * WaitTask Refactor - Fix Evaluation failed: TypeError: fun is not a function (#2192) * Fix Evaluation failed: TypeError: fun is not a function - The whitespace is causing the new Function() call to fail. * Alternate fix * Fix ShouldRespectTimeout test * Fix failing test --------- Co-authored-by: Alex Maitland --- lib/PuppeteerSharp.Tests/TestConstants.cs | 8 +- .../FrameWaitForFunctionTests.cs | 25 +- .../FrameWaitForSelectorTests.cs | 6 +- .../WaitTaskTests/FrameWaitForXPathTests.cs | 6 +- .../WaitTaskTests/PageWaitForTests.cs | 2 +- lib/PuppeteerSharp/AriaQueryHandlerFactory.cs | 1 + lib/PuppeteerSharp/CustomQueriesManager.cs | 4 +- lib/PuppeteerSharp/Injected/injected.js | 301 +++++++++++++++++- lib/PuppeteerSharp/IsolatedWorld.cs | 120 +++---- lib/PuppeteerSharp/TaskManager.cs | 31 ++ lib/PuppeteerSharp/WaitForFunctionOptions.cs | 10 + lib/PuppeteerSharp/WaitTask.cs | 297 ++++++++--------- .../WaitTaskTimeoutException.cs | 21 +- 13 files changed, 580 insertions(+), 252 deletions(-) create mode 100644 lib/PuppeteerSharp/TaskManager.cs diff --git a/lib/PuppeteerSharp.Tests/TestConstants.cs b/lib/PuppeteerSharp.Tests/TestConstants.cs index 6a43568fd..3dae21472 100644 --- a/lib/PuppeteerSharp.Tests/TestConstants.cs +++ b/lib/PuppeteerSharp.Tests/TestConstants.cs @@ -40,10 +40,12 @@ public static class TestConstants " http://localhost:/frames/frame.html (aframe)" }; - public static LaunchOptions DefaultBrowserOptions() => new LaunchOptions + public static LaunchOptions DefaultBrowserOptions() => new() { SlowMo = Convert.ToInt32(Environment.GetEnvironmentVariable("SLOW_MO")), - Headless = Convert.ToBoolean(Environment.GetEnvironmentVariable("HEADLESS") ?? "true"), + Headless = Convert.ToBoolean( + Environment.GetEnvironmentVariable("HEADLESS") ?? + (System.Diagnostics.Debugger.IsAttached ? "false" : "true")), Product = IsChrome ? Product.Chrome : Product.Firefox, EnqueueAsyncMessages = Convert.ToBoolean(Environment.GetEnvironmentVariable("ENQUEUE_ASYNC_MESSAGES") ?? "false"), Timeout = 0, @@ -55,7 +57,7 @@ public static class TestConstants #endif }; - public static LaunchOptions BrowserWithExtensionOptions() => new LaunchOptions + public static LaunchOptions BrowserWithExtensionOptions() => new() { Headless = false, Args = new[] diff --git a/lib/PuppeteerSharp.Tests/WaitTaskTests/FrameWaitForFunctionTests.cs b/lib/PuppeteerSharp.Tests/WaitTaskTests/FrameWaitForFunctionTests.cs index e6d678170..57431450c 100644 --- a/lib/PuppeteerSharp.Tests/WaitTaskTests/FrameWaitForFunctionTests.cs +++ b/lib/PuppeteerSharp.Tests/WaitTaskTests/FrameWaitForFunctionTests.cs @@ -31,15 +31,15 @@ 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; + Assert.True((DateTime.UtcNow - startTime).TotalMilliseconds > polling / 2); } @@ -47,14 +47,13 @@ public async Task ShouldPollOnInterval() [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); } @@ -162,7 +161,7 @@ public async Task ShouldRespectTimeout() var exception = await Assert.ThrowsAsync(() => 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")] @@ -173,7 +172,7 @@ public async Task ShouldRespectDefaultTimeout() var exception = await Assert.ThrowsAsync(() => 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")] diff --git a/lib/PuppeteerSharp.Tests/WaitTaskTests/FrameWaitForSelectorTests.cs b/lib/PuppeteerSharp.Tests/WaitTaskTests/FrameWaitForSelectorTests.cs index b35023638..ac6b6100e 100644 --- a/lib/PuppeteerSharp.Tests/WaitTaskTests/FrameWaitForSelectorTests.cs +++ b/lib/PuppeteerSharp.Tests/WaitTaskTests/FrameWaitForSelectorTests.cs @@ -105,7 +105,7 @@ public async Task ShouldThrowWhenFrameIsDetached() var frame = Page.FirstChildFrame(); var waitTask = frame.WaitForSelectorAsync(".box"); await FrameUtils.DetachFrameAsync(Page, "frame1"); - var waitException = await Assert.ThrowsAsync(() => waitTask); + var waitException = await Assert.ThrowsAsync(() => waitTask); Assert.NotNull(waitException); Assert.Contains("waitForFunction failed: frame got detached.", waitException.Message); @@ -206,7 +206,7 @@ public async Task ShouldRespectTimeout() var exception = await Assert.ThrowsAsync(async () => await Page.WaitForSelectorAsync("div", new WaitForSelectorOptions { Timeout = 10 })); - Assert.Contains("waiting for selector 'div' failed: timeout", exception.Message); + Assert.Contains("Waiting for selector `div` failed: Waiting failed: 10ms exceeded", exception.Message); } [PuppeteerTest("waittask.spec.ts", "Frame.waitForSelector", "should have an error message specifically for awaiting an element to be hidden")] @@ -217,7 +217,7 @@ public async Task ShouldHaveAnErrorMessageSpecificallyForAwaitingAnElementToBeHi var exception = await Assert.ThrowsAsync(async () => await Page.WaitForSelectorAsync("div", new WaitForSelectorOptions { Hidden = true, Timeout = 10 })); - Assert.Contains("waiting for selector 'div' to be hidden failed: timeout", exception.Message); + Assert.Contains("Waiting for selector `div` failed: Waiting failed: 10ms exceeded", exception.Message); } [PuppeteerTest("waittask.spec.ts", "Frame.waitForSelector", "should respond to node attribute mutation")] diff --git a/lib/PuppeteerSharp.Tests/WaitTaskTests/FrameWaitForXPathTests.cs b/lib/PuppeteerSharp.Tests/WaitTaskTests/FrameWaitForXPathTests.cs index 61dcb27d0..9f829472a 100644 --- a/lib/PuppeteerSharp.Tests/WaitTaskTests/FrameWaitForXPathTests.cs +++ b/lib/PuppeteerSharp.Tests/WaitTaskTests/FrameWaitForXPathTests.cs @@ -100,10 +100,12 @@ public async Task ShouldAllowYouToSelectAnElementWithSingleSlash() [PuppeteerFact] public async Task ShouldRespectTimeout() { + const int timeout = 10; + var exception = await Assert.ThrowsAsync(() - => Page.WaitForXPathAsync("//div", new WaitForSelectorOptions { Timeout = 10 })); + => Page.WaitForXPathAsync("//div", new WaitForSelectorOptions { Timeout = timeout })); - Assert.Contains("waiting for XPath '//div' failed: timeout", exception.Message); + Assert.Contains($"Waiting failed: {timeout}ms exceeded", exception.Message); } } } \ No newline at end of file diff --git a/lib/PuppeteerSharp.Tests/WaitTaskTests/PageWaitForTests.cs b/lib/PuppeteerSharp.Tests/WaitTaskTests/PageWaitForTests.cs index 5b3406e64..69dbb9623 100644 --- a/lib/PuppeteerSharp.Tests/WaitTaskTests/PageWaitForTests.cs +++ b/lib/PuppeteerSharp.Tests/WaitTaskTests/PageWaitForTests.cs @@ -47,7 +47,7 @@ public async Task ShouldWaitForAnXpath() public async Task ShouldNotAllowYouToSelectAnElementWithSingleSlashXpath() { await Page.SetContentAsync("
some text
"); - var exception = await Assert.ThrowsAsync(() => + var exception = await Assert.ThrowsAsync(() => Page.WaitForSelectorAsync("/html/body/div")); Assert.NotNull(exception); } diff --git a/lib/PuppeteerSharp/AriaQueryHandlerFactory.cs b/lib/PuppeteerSharp/AriaQueryHandlerFactory.cs index 5c2d2db03..7cf8de344 100644 --- a/lib/PuppeteerSharp/AriaQueryHandlerFactory.cs +++ b/lib/PuppeteerSharp/AriaQueryHandlerFactory.cs @@ -52,6 +52,7 @@ async Task WaitFor(IElementHandle root, string selector, WaitFor return await frame.PuppeteerWorld.WaitForSelectorInPageAsync( @"(_, selector) => globalThis.ariaQuerySelector(selector)", + element, selector, options, new[] { binding }).ConfigureAwait(false); diff --git a/lib/PuppeteerSharp/CustomQueriesManager.cs b/lib/PuppeteerSharp/CustomQueriesManager.cs index d62e83e9d..43f0f87ab 100644 --- a/lib/PuppeteerSharp/CustomQueriesManager.cs +++ b/lib/PuppeteerSharp/CustomQueriesManager.cs @@ -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); }; } diff --git a/lib/PuppeteerSharp/Injected/injected.js b/lib/PuppeteerSharp/Injected/injected.js index fb2fb0715..f3c2d880a 100644 --- a/lib/PuppeteerSharp/Injected/injected.js +++ b/lib/PuppeteerSharp/Injected/injected.js @@ -1,7 +1,304 @@ (() => { const module = {}; - "use strict"; var C = Object.defineProperty; var ne = Object.getOwnPropertyDescriptor; var oe = Object.getOwnPropertyNames; var se = Object.prototype.hasOwnProperty; var u = (e, t) => { for (var n in t) C(e, n, { get: t[n], enumerable: !0 }) }, ie = (e, t, n, r) => { if (t && typeof t == "object" || typeof t == "function") for (let o of oe(t)) !se.call(e, o) && o !== n && C(e, o, { get: () => t[o], enumerable: !(r = ne(t, o)) || r.enumerable }); return e }; var le = e => ie(C({}, "__esModule", { value: !0 }), e); var Oe = {}; u(Oe, { default: () => Re }); module.exports = le(Oe); var P = class extends Error { constructor(t) { super(t), this.name = this.constructor.name, Error.captureStackTrace(this, this.constructor) } }, S = class extends P { }, I = class extends P { #e; #r = ""; set code(t) { this.#e = t } get code() { return this.#e } set originalMessage(t) { this.#r = t } get originalMessage() { return this.#r } }, De = Object.freeze({ TimeoutError: S, ProtocolError: I }); function p(e) { let t = !1, n = !1, r, o, i = new Promise((l, a) => { r = l, o = a }), s = e && e.timeout > 0 ? setTimeout(() => { n = !0, o(new S(e.message)) }, e.timeout) : void 0; return Object.assign(i, { resolved: () => t, finished: () => t || n, resolve: l => { s && clearTimeout(s), t = !0, r(l) }, reject: l => { clearTimeout(s), n = !0, o(l) } }) } var G = new Map, X = e => { let t = G.get(e); return t || (t = new Function(`return ${e}`)(), G.set(e, t), t) }; var R = {}; u(R, { ariaQuerySelector: () => ae, ariaQuerySelectorAll: () => k }); var ae = (e, t) => window.__ariaQuerySelector(e, t), k = async function* (e, t) { yield* await window.__ariaQuerySelectorAll(e, t) }; var D = {}; u(D, { customQuerySelectors: () => _ }); var O = class { #e = new Map; register(t, n) { if (!n.queryOne && n.queryAll) { let r = n.queryAll; n.queryOne = (o, i) => { for (let s of r(o, i)) return s; return null } } else if (n.queryOne && !n.queryAll) { let r = n.queryOne; n.queryAll = (o, i) => { let s = r(o, i); return s ? [s] : [] } } else if (!n.queryOne || !n.queryAll) throw new Error("At least one query method must be defined."); this.#e.set(t, { querySelector: n.queryOne, querySelectorAll: n.queryAll }) } unregister(t) { this.#e.delete(t) } get(t) { return this.#e.get(t) } clear() { this.#e.clear() } }, _ = new O; var M = {}; u(M, { pierceQuerySelector: () => ce, pierceQuerySelectorAll: () => ue }); var ce = (e, t) => { let n = null, r = o => { let i = document.createTreeWalker(o, NodeFilter.SHOW_ELEMENT); do { let s = i.currentNode; s.shadowRoot && r(s.shadowRoot), !(s instanceof ShadowRoot) && s !== o && !n && s.matches(t) && (n = s) } while (!n && i.nextNode()) }; return e instanceof Document && (e = e.documentElement), r(e), n }, ue = (e, t) => { let n = [], r = o => { let i = document.createTreeWalker(o, NodeFilter.SHOW_ELEMENT); do { let s = i.currentNode; s.shadowRoot && r(s.shadowRoot), !(s instanceof ShadowRoot) && s !== o && s.matches(t) && n.push(s) } while (i.nextNode()) }; return e instanceof Document && (e = e.documentElement), r(e), n }; var m = (e, t) => { if (!e) throw new Error(t) }; var T = class { #e; #r; #n; #t; constructor(t, n) { this.#e = t, this.#r = n } async start() { let t = this.#t = p(), n = await this.#e(); if (n) { t.resolve(n); return } this.#n = new MutationObserver(async () => { let r = await this.#e(); r && (t.resolve(r), await this.stop()) }), this.#n.observe(this.#r, { childList: !0, subtree: !0, attributes: !0 }) } async stop() { m(this.#t, "Polling never started."), this.#t.finished() || this.#t.reject(new Error("Polling stopped")), this.#n && (this.#n.disconnect(), this.#n = void 0) } result() { return m(this.#t, "Polling never started."), this.#t } }, x = class { #e; #r; constructor(t) { this.#e = t } async start() { let t = this.#r = p(), n = await this.#e(); if (n) { t.resolve(n); return } let r = async () => { if (t.finished()) return; let o = await this.#e(); if (!o) { window.requestAnimationFrame(r); return } t.resolve(o), await this.stop() }; window.requestAnimationFrame(r) } async stop() { m(this.#r, "Polling never started."), this.#r.finished() || this.#r.reject(new Error("Polling stopped")) } result() { return m(this.#r, "Polling never started."), this.#r } }, E = class { #e; #r; #n; #t; constructor(t, n) { this.#e = t, this.#r = n } async start() { let t = this.#t = p(), n = await this.#e(); if (n) { t.resolve(n); return } this.#n = setInterval(async () => { let r = await this.#e(); r && (t.resolve(r), await this.stop()) }, this.#r) } async stop() { m(this.#t, "Polling never started."), this.#t.finished() || this.#t.reject(new Error("Polling stopped")), this.#n && (clearInterval(this.#n), this.#n = void 0) } result() { return m(this.#t, "Polling never started."), this.#t } }; var H = {}; u(H, { pQuerySelector: () => Ie, pQuerySelectorAll: () => re }); var c = class { static async*map(t, n) { for await (let r of t) yield await n(r) } static async*flatMap(t, n) { for await (let r of t) yield* n(r) } static async collect(t) { let n = []; for await (let r of t) n.push(r); return n } static async first(t) { for await (let n of t) return n } }; var h = { attribute: /\[\s*(?:(?\*|[-\w\P{ASCII}]*)\|)?(?[-\w\P{ASCII}]+)\s*(?:(?\W?=)\s*(?.+?)\s*(\s(?[iIsS]))?\s*)?\]/gu, id: /#(?[-\w\P{ASCII}]+)/gu, class: /\.(?[-\w\P{ASCII}]+)/gu, comma: /\s*,\s*/g, combinator: /\s*[\s>+~]\s*/g, "pseudo-element": /::(?[-\w\P{ASCII}]+)(?:\((?¶+)\))?/gu, "pseudo-class": /:(?[-\w\P{ASCII}]+)(?:\((?¶+)\))?/gu, universal: /(?:(?\*|[-\w\P{ASCII}]*)\|)?\*/gu, type: /(?:(?\*|[-\w\P{ASCII}]*)\|)?(?[-\w\P{ASCII}]+)/gu }, fe = new Set(["combinator", "comma"]); var me = e => { switch (e) { case "pseudo-element": case "pseudo-class": return new RegExp(h[e].source.replace("(?\xB6+)", "(?.+)"), "gu"); default: return h[e] } }; function de(e, t) { let n = 0, r = ""; for (; t < e.length; t++) { let o = e[t]; switch (o) { case "(": ++n; break; case ")": --n; break }if (r += o, n === 0) return r } return r } function pe(e, t = h) { if (!e) return []; let n = [e]; for (let [o, i] of Object.entries(t)) for (let s = 0; s < n.length; s++) { let l = n[s]; if (typeof l != "string") continue; i.lastIndex = 0; let a = i.exec(l); if (!a) continue; let d = a.index - 1, f = [], V = a[0], B = l.slice(0, d + 1); B && f.push(B), f.push({ ...a.groups, type: o, content: V }); let z = l.slice(d + V.length + 1); z && f.push(z), n.splice(s, 1, ...f) } let r = 0; for (let o of n) switch (typeof o) { case "string": throw new Error(`Unexpected sequence ${o} found at index ${r}`); case "object": r += o.content.length, o.pos = [r - o.content.length, r], fe.has(o.type) && (o.content = o.content.trim() || " "); break }return n } var he = /(['"])([^\\\n]+?)\1/g, ge = /\\./g; function K(e, t = h) { if (e = e.trim(), e === "") return []; let n = []; e = e.replace(ge, (i, s) => (n.push({ value: i, offset: s }), "\uE000".repeat(i.length))), e = e.replace(he, (i, s, l, a) => (n.push({ value: i, offset: a }), `${s}${"\uE001".repeat(l.length)}${s}`)); { let i = 0, s; for (; (s = e.indexOf("(", i)) > -1;) { let l = de(e, s); n.push({ value: l, offset: s }), e = `${e.substring(0, s)}(${"\xB6".repeat(l.length - 2)})${e.substring(s + l.length)}`, i = s + l.length } } let r = pe(e, t), o = new Set; for (let i of n.reverse()) for (let s of r) { let { offset: l, value: a } = i; if (!(s.pos[0] <= l && l + a.length <= s.pos[1])) continue; let { content: d } = s, f = l - s.pos[0]; s.content = d.slice(0, f) + a + d.slice(f + a.length), s.content !== d && o.add(s) } for (let i of o) { let s = me(i.type); if (!s) throw new Error(`Unknown token type: ${i.type}`); s.lastIndex = 0; let l = s.exec(i.content); if (!l) throw new Error(`Unable to parse content for ${i.type}: ${i.content}`); Object.assign(i, l.groups) } return r } function* N(e, t) { switch (e.type) { case "list": for (let n of e.list) yield* N(n, e); break; case "complex": yield* N(e.left, e), yield* N(e.right, e); break; case "compound": yield* e.list.map(n => [n, e]); break; default: yield [e, t] } } function g(e) { let t; return Array.isArray(e) ? t = e : t = [...N(e)].map(([n]) => n), t.map(n => n.content).join("") } h.combinator = /\s*(>>>>?|[\s>+~])\s*/g; var ye = /\\[\s\S]/g, we = e => { if (e.length > 1) { for (let t of ['"', "'"]) if (!(!e.startsWith(t) || !e.endsWith(t))) return e.slice(t.length, -t.length).replace(ye, n => n.slice(1)) } return e }; function Y(e) { let t = !0, n = K(e); if (n.length === 0) return [[], t]; let r = [], o = [r], i = [o], s = []; for (let l of n) { switch (l.type) { case "combinator": switch (l.content) { case ">>>": t = !1, s.length && (r.push(g(s)), s.splice(0)), r = [], o.push(">>>"), o.push(r); continue; case ">>>>": t = !1, s.length && (r.push(g(s)), s.splice(0)), r = [], o.push(">>>>"), o.push(r); continue }break; case "pseudo-element": if (!l.name.startsWith("-p-")) break; t = !1, s.length && (r.push(g(s)), s.splice(0)), r.push({ name: l.name.slice(3), value: we(l.argument ?? "") }); continue; case "comma": s.length && (r.push(g(s)), s.splice(0)), r = [], o = [r], i.push(o); continue }s.push(l) } return s.length && r.push(g(s)), [i, t] } var Q = {}; u(Q, { textQuerySelectorAll: () => b }); var Se = new Set(["checkbox", "image", "radio"]), be = e => e instanceof HTMLSelectElement || e instanceof HTMLTextAreaElement || e instanceof HTMLInputElement && !Se.has(e.type), Pe = new Set(["SCRIPT", "STYLE"]), w = e => !Pe.has(e.nodeName) && !document.head?.contains(e), q = new WeakMap, Z = e => { for (; e;)q.delete(e), e instanceof ShadowRoot ? e = e.host : e = e.parentNode }, J = new WeakSet, Te = new MutationObserver(e => { for (let t of e) Z(t.target) }), y = e => { let t = q.get(e); if (t || (t = { full: "", immediate: [] }, !w(e))) return t; let n = ""; if (be(e)) t.full = e.value, t.immediate.push(e.value), e.addEventListener("input", r => { Z(r.target) }, { once: !0, capture: !0 }); else { for (let r = e.firstChild; r; r = r.nextSibling) { if (r.nodeType === Node.TEXT_NODE) { t.full += r.nodeValue ?? "", n += r.nodeValue ?? ""; continue } n && t.immediate.push(n), n = "", r.nodeType === Node.ELEMENT_NODE && (t.full += y(r).full) } n && t.immediate.push(n), e instanceof Element && e.shadowRoot && (t.full += y(e.shadowRoot).full), J.has(e) || (Te.observe(e, { childList: !0, characterData: !0 }), J.add(e)) } return q.set(e, t), t }; var b = function* (e, t) { let n = !1; for (let r of e.childNodes) if (r instanceof Element && w(r)) { let o; r.shadowRoot ? o = b(r.shadowRoot, t) : o = b(r, t); for (let i of o) yield i, n = !0 } n || e instanceof Element && w(e) && y(e).full.includes(t) && (yield e) }; var $ = {}; u($, { checkVisibility: () => Ee, pierce: () => A, pierceAll: () => L }); var xe = ["hidden", "collapse"], Ee = (e, t) => { if (!e) return t === !1; if (t === void 0) return e; let n = e.nodeType === Node.TEXT_NODE ? e.parentElement : e, r = window.getComputedStyle(n), o = r && !xe.includes(r.visibility) && !Ne(n); return t === o ? e : !1 }; function Ne(e) { let t = e.getBoundingClientRect(); return t.width === 0 || t.height === 0 } var Ae = e => "shadowRoot" in e && e.shadowRoot instanceof ShadowRoot; function* A(e) { Ae(e) ? yield e.shadowRoot : yield e } function* L(e) { e = A(e).next().value, yield e; let t = [document.createTreeWalker(e, NodeFilter.SHOW_ELEMENT)]; for (let n of t) { let r; for (; r = n.nextNode();)r.shadowRoot && (yield r.shadowRoot, t.push(document.createTreeWalker(r.shadowRoot, NodeFilter.SHOW_ELEMENT))) } } var U = {}; u(U, { xpathQuerySelectorAll: () => j }); var j = function* (e, t) { let r = (e.ownerDocument || document).evaluate(t, e, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE), o; for (; o = r.iterateNext();)yield o }; var ve = /[-\w\P{ASCII}*]/, ee = e => "querySelectorAll" in e, v = class extends Error { constructor(t, n) { super(`${t} is not a valid selector: ${n}`) } }, F = class { #e; #r; #n = []; #t = void 0; elements; constructor(t, n, r) { this.elements = [t], this.#e = n, this.#r = r, this.#o() } async run() { if (typeof this.#t == "string") switch (this.#t.trimStart()) { case ":scope": this.#o(); break }for (; this.#t !== void 0; this.#o()) { let t = this.#t, n = this.#e; typeof t == "string" ? t[0] && ve.test(t[0]) ? this.elements = c.flatMap(this.elements, async function* (r) { ee(r) && (yield* r.querySelectorAll(t)) }) : this.elements = c.flatMap(this.elements, async function* (r) { if (!r.parentElement) { if (!ee(r)) return; yield* r.querySelectorAll(t); return } let o = 0; for (let i of r.parentElement.children) if (++o, i === r) break; yield* r.parentElement.querySelectorAll(`:scope>:nth-child(${o})${t}`) }) : this.elements = c.flatMap(this.elements, async function* (r) { switch (t.name) { case "text": yield* b(r, t.value); break; case "xpath": yield* j(r, t.value); break; case "aria": yield* k(r, t.value); break; default: let o = _.get(t.name); if (!o) throw new v(n, `Unknown selector type: ${t.name}`); yield* o.querySelectorAll(r, t.value) } }) } } #o() { if (this.#n.length !== 0) { this.#t = this.#n.shift(); return } if (this.#r.length === 0) { this.#t = void 0; return } let t = this.#r.shift(); switch (t) { case ">>>>": { this.elements = c.flatMap(this.elements, A), this.#o(); break } case ">>>": { this.elements = c.flatMap(this.elements, L), this.#o(); break } default: this.#n = t, this.#o(); break } } }, W = class { #e = new WeakMap; calculate(t, n = []) { if (t === null) return n; t instanceof ShadowRoot && (t = t.host); let r = this.#e.get(t); if (r) return [...r, ...n]; let o = 0; for (let s = t.previousSibling; s; s = s.previousSibling)++o; let i = this.calculate(t.parentNode, [o]); return this.#e.set(t, i), [...i, ...n] } }, te = (e, t) => { if (e.length + t.length === 0) return 0; let [n = -1, ...r] = e, [o = -1, ...i] = t; return n === o ? te(r, i) : n < o ? -1 : 1 }, Ce = async function* (e) { let t = new Set; for await (let r of e) t.add(r); let n = new W; yield* [...t.values()].map(r => [r, n.calculate(r)]).sort(([, r], [, o]) => te(r, o)).map(([r]) => r) }, re = function (e, t) { let n, r; try { [n, r] = Y(t) } catch { return e.querySelectorAll(t) } if (r) return e.querySelectorAll(t); if (n.some(o => { let i = 0; return o.some(s => (typeof s == "string" ? ++i : i = 0, i > 1)) })) throw new v(t, "Multiple deep combinators found in sequence."); return Ce(c.flatMap(n, o => { let i = new F(e, t, o); return i.run(), i.elements })) }, Ie = async function (e, t) { for await (let n of re(e, t)) return n; return null }; var ke = Object.freeze({ ...R, ...D, ...M, ...H, ...Q, ...$, ...U, createDeferredPromise: p, createFunction: X, createTextContent: y, IntervalPoller: E, isSuitableNodeForTextMatching: w, MutationPoller: T, RAFPoller: x }), Re = ke; + + "use strict"; + var __defProp = Object.defineProperty; + var __getOwnPropDesc = Object.getOwnPropertyDescriptor; + var __getOwnPropNames = Object.getOwnPropertyNames; + var __hasOwnProp = Object.prototype.hasOwnProperty; + var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); + }; + var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; + }; + var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + var __accessCheck = (obj, member, msg) => { + if (!member.has(obj)) + throw TypeError("Cannot " + msg); + }; + var __privateGet = (obj, member, getter) => { + __accessCheck(obj, member, "read from private field"); + return getter ? getter.call(obj) : member.get(obj); + }; + var __privateAdd = (obj, member, value) => { + if (member.has(obj)) + throw TypeError("Cannot add the same private member more than once"); + member instanceof WeakSet ? member.add(obj) : member.set(obj, value); + }; + var __privateSet = (obj, member, value, setter) => { + __accessCheck(obj, member, "write to private field"); + setter ? setter.call(obj, value) : member.set(obj, value); + return value; + }; + + // src/injected/injected.ts + var injected_exports = {}; + __export(injected_exports, { + default: () => injected_default + }); + module.exports = __toCommonJS(injected_exports); + + // src/common/Errors.ts + var CustomError = class extends Error { + constructor(message) { + super(message); + this.name = this.constructor.name; + Error.captureStackTrace(this, this.constructor); + } + }; + var TimeoutError = class extends CustomError { + }; + var ProtocolError = class extends CustomError { + constructor() { + super(...arguments); + this.originalMessage = ""; + } + }; + var errors = Object.freeze({ + TimeoutError, + ProtocolError + }); + + // src/util/DeferredPromise.ts + function createDeferredPromise(opts) { + let isResolved = false; + let isRejected = false; + let resolver; + let rejector; + const taskPromise = new Promise((resolve, reject) => { + resolver = resolve; + rejector = reject; + }); + const timeoutId = opts && opts.timeout > 0 ? setTimeout(() => { + isRejected = true; + rejector(new TimeoutError(opts.message)); + }, opts.timeout) : void 0; + return Object.assign(taskPromise, { + resolved: () => { + return isResolved; + }, + finished: () => { + return isResolved || isRejected; + }, + resolve: (value) => { + if (timeoutId) { + clearTimeout(timeoutId); + } + isResolved = true; + resolver(value); + }, + reject: (err) => { + clearTimeout(timeoutId); + isRejected = true; + rejector(err); + } + }); + } + + // src/injected/util.ts + var util_exports = {}; + __export(util_exports, { + checkVisibility: () => checkVisibility, + createFunction: () => createFunction + }); + var createdFunctions = /* @__PURE__ */ new Map(); + var createFunction = (functionValue) => { + let fn = createdFunctions.get(functionValue); + if (fn) { + return fn; + } + fn = new Function(`return ${functionValue}`)(); + createdFunctions.set(functionValue, fn); + return fn; + }; + var checkVisibility = (node, visible) => { + if (!node) { + return visible === false; + } + if (visible === void 0) { + return node; + } + const element = node.nodeType === Node.TEXT_NODE ? node.parentElement : node; + const style = window.getComputedStyle(element); + const isVisible = style && style.visibility !== "hidden" && isBoundingBoxVisible(element); + return visible === isVisible ? node : false; + }; + function isBoundingBoxVisible(element) { + const rect = element.getBoundingClientRect(); + return !!(rect.top || rect.bottom || rect.width || rect.height); + } + + // src/injected/Poller.ts + var Poller_exports = {}; + __export(Poller_exports, { + IntervalPoller: () => IntervalPoller, + MutationPoller: () => MutationPoller, + RAFPoller: () => RAFPoller + }); + + // src/util/assert.ts + var assert = (value, message) => { + if (!value) { + throw new Error(message); + } + }; + + // src/injected/Poller.ts + var _fn, _root, _observer, _promise; + var MutationPoller = class { + constructor(fn, root) { + __privateAdd(this, _fn, void 0); + __privateAdd(this, _root, void 0); + __privateAdd(this, _observer, void 0); + __privateAdd(this, _promise, void 0); + __privateSet(this, _fn, fn); + __privateSet(this, _root, root); + } + async start() { + const promise = __privateSet(this, _promise, createDeferredPromise()); + const result = await __privateGet(this, _fn).call(this); + if (result) { + promise.resolve(result); + return; + } + __privateSet(this, _observer, new MutationObserver(async () => { + console.log(1); + const result2 = await __privateGet(this, _fn).call(this); + if (!result2) { + return; + } + promise.resolve(result2); + await this.stop(); + })); + __privateGet(this, _observer).observe(__privateGet(this, _root), { + childList: true, + subtree: true, + attributes: true + }); + } + async stop() { + assert(__privateGet(this, _promise), "Polling never started."); + if (!__privateGet(this, _promise).finished()) { + __privateGet(this, _promise).reject(new Error("Polling stopped")); + } + if (__privateGet(this, _observer)) { + __privateGet(this, _observer).disconnect(); + __privateSet(this, _observer, void 0); + } + } + result() { + assert(__privateGet(this, _promise), "Polling never started."); + return __privateGet(this, _promise); + } + }; + _fn = new WeakMap(); + _root = new WeakMap(); + _observer = new WeakMap(); + _promise = new WeakMap(); + var _fn2, _promise2; + var RAFPoller = class { + constructor(fn) { + __privateAdd(this, _fn2, void 0); + __privateAdd(this, _promise2, void 0); + __privateSet(this, _fn2, fn); + } + async start() { + const promise = __privateSet(this, _promise2, createDeferredPromise()); + const result = await __privateGet(this, _fn2).call(this); + if (result) { + promise.resolve(result); + return; + } + const poll = async () => { + if (promise.finished()) { + return; + } + const result2 = await __privateGet(this, _fn2).call(this); + if (!result2) { + window.requestAnimationFrame(poll); + return; + } + promise.resolve(result2); + await this.stop(); + }; + window.requestAnimationFrame(poll); + } + async stop() { + assert(__privateGet(this, _promise2), "Polling never started."); + if (!__privateGet(this, _promise2).finished()) { + __privateGet(this, _promise2).reject(new Error("Polling stopped")); + } + } + result() { + assert(__privateGet(this, _promise2), "Polling never started."); + return __privateGet(this, _promise2); + } + }; + _fn2 = new WeakMap(); + _promise2 = new WeakMap(); + var _fn3, _ms, _interval, _promise3; + var IntervalPoller = class { + constructor(fn, ms) { + __privateAdd(this, _fn3, void 0); + __privateAdd(this, _ms, void 0); + __privateAdd(this, _interval, void 0); + __privateAdd(this, _promise3, void 0); + __privateSet(this, _fn3, fn); + __privateSet(this, _ms, ms); + } + async start() { + const promise = __privateSet(this, _promise3, createDeferredPromise()); + const result = await __privateGet(this, _fn3).call(this); + if (result) { + promise.resolve(result); + return; + } + __privateSet(this, _interval, setInterval(async () => { + const result2 = await __privateGet(this, _fn3).call(this); + if (!result2) { + return; + } + promise.resolve(result2); + await this.stop(); + }, __privateGet(this, _ms))); + } + async stop() { + assert(__privateGet(this, _promise3), "Polling never started."); + if (!__privateGet(this, _promise3).finished()) { + __privateGet(this, _promise3).reject(new Error("Polling stopped")); + } + if (__privateGet(this, _interval)) { + clearInterval(__privateGet(this, _interval)); + __privateSet(this, _interval, void 0); + } + } + result() { + assert(__privateGet(this, _promise3), "Polling never started."); + return __privateGet(this, _promise3); + } + }; + _fn3 = new WeakMap(); + _ms = new WeakMap(); + _interval = new WeakMap(); + _promise3 = new WeakMap(); + + // src/injected/injected.ts + var PuppeteerUtil = Object.freeze({ + ...util_exports, + ...Poller_exports, + createDeferredPromise + }); + var injected_default = PuppeteerUtil; + return module.exports.default; -})() \ No newline at end of file +})() + diff --git a/lib/PuppeteerSharp/IsolatedWorld.cs b/lib/PuppeteerSharp/IsolatedWorld.cs index 8434f7e2d..01cec406e 100644 --- a/lib/PuppeteerSharp/IsolatedWorld.cs +++ b/lib/PuppeteerSharp/IsolatedWorld.cs @@ -2,12 +2,10 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Reflection; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; -using PuppeteerSharp.Helpers; using PuppeteerSharp.Helpers.Json; using PuppeteerSharp.Input; using PuppeteerSharp.Messaging; @@ -48,13 +46,12 @@ internal class IsolatedWorld Frame = frame; _timeoutSettings = timeoutSettings; - WaitTasks = new ConcurrentSet(); _detached = false; _client.MessageReceived += Client_MessageReceived; _logger = _client.Connection.LoggerFactory.CreateLogger(); } - internal ConcurrentSet WaitTasks { get; set; } + internal TaskManager TaskManager { get; set; } = new(); internal Frame Frame { get; } @@ -160,10 +157,7 @@ internal async Task AdoptHandleAsync(IJSHandle handle) internal void Detach() { _detached = true; - while (!WaitTasks.IsEmpty) - { - WaitTasks.First().Terminate(new Exception("waitForFunction failed: frame got detached.")); - } + TaskManager.TerminateAll(new Exception("waitForFunction failed: frame got detached.")); } internal Task GetExecutionContextAsync() @@ -271,48 +265,70 @@ internal async Task SetContentAsync(string html, NavigationOptions options = nul } } - internal async Task WaitForSelectorInPageAsync(string queryOne, string selector, WaitForSelectorOptions options, PageBinding[] bindings = null) + internal async Task 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)}"; + try + { + var waitForVisible = options?.Visible ?? false; + var waitForHidden = options?.Hidden ?? false; + var timeout = options?.Timeout ?? _timeoutSettings.Timeout; + + var predicate = @$"async (PuppeteerUtil, query, selector, root, visible) => {{ + if(visible === undefined) {{ + console.log(PuppeteerUtil, query, selector, root, visible); + }} + if (!PuppeteerUtil) {{ + return; + }} + const node = (await PuppeteerUtil.createFunction(query)( + root || document, + selector + )); + if(visible === undefined) {{ + console.log(visible, node, PuppeteerUtil.checkVisibility(node, visible)); + }} + return PuppeteerUtil.checkVisibility(node, visible); + }}"; + + var args = new List + { + await GetPuppeteerUtilAsync().ConfigureAwait(false), + queryOne, + selector, + root, + }; - var predicate = @$"async function predicate(root, selector, waitForVisible, waitForHidden) {{ - const node = predicateQueryHandler - ? ((await predicateQueryHandler(root, selector))) - : root.querySelector(selector); - return checkWaitForOptions(node, waitForVisible, waitForHidden); - }}"; + // Puppeteer's injected code checks for visible to be undefined + // As we don't support passing undefined values we need to ignore sending this value + // if visible is false + if (waitForVisible || waitForHidden) + { + args.Add(waitForVisible); + } - 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() + { + Bindings = bindings, + Polling = waitForVisible || waitForHidden ? WaitForFunctionPollingOption.Raf : WaitForFunctionPollingOption.Mutation, + Root = root, + Timeout = timeout, + }, + args.ToArray()).ConfigureAwait(false); + + if (jsHandle is not ElementHandle elementHandle) { - selector, - waitForVisible, - waitForHidden, - }, - true); + await jsHandle.DisposeAsync().ConfigureAwait(false); + return null; + } - var jsHandle = await waitTask.Task.ConfigureAwait(false); - if (jsHandle is not ElementHandle elementHandle) + return elementHandle; + } + catch (Exception ex) { - await jsHandle.DisposeAsync().ConfigureAwait(false); - return null; + throw new WaitTaskTimeoutException($"Waiting for selector `{selector}` failed: {ex.Message}", ex); } - - return elementHandle; } internal async Task ClickAsync(string selector, ClickOptions options = null) @@ -381,12 +397,11 @@ internal async Task 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 @@ -400,14 +415,12 @@ internal async Task 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 @@ -458,10 +471,7 @@ internal void SetContext(ExecutionContext context) _ = InjectPuppeteerUtil(context); _ctxBindings.Clear(); _contextResolveTaskWrapper.TrySetResult(context); - foreach (var waitTask in WaitTasks) - { - _ = waitTask.Rerun(); - } + TaskManager.RerunAll(); } private static string GetInjectedSource() @@ -608,8 +618,7 @@ private async Task WaitForSelectorOrXPathAsync(string selectorOr options ??= new WaitForSelectorOptions(); var timeout = options.Timeout ?? _timeoutSettings.Timeout; - const string predicate = @" - function predicate(selectorOrXPath, isXPath, waitForVisible, waitForHidden) { + const string predicate = @"function predicate(selectorOrXPath, isXPath, waitForVisible, waitForHidden) { const node = isXPath ? document.evaluate(selectorOrXPath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue : document.querySelector(selectorOrXPath); @@ -635,7 +644,6 @@ private async Task WaitForSelectorOrXPathAsync(string selectorOr this, predicate, false, - $"{(isXPath ? "XPath" : "selector")} '{selectorOrXPath}'{(options.Hidden ? " to be hidden" : string.Empty)}", polling, null, // Polling interval timeout, diff --git a/lib/PuppeteerSharp/TaskManager.cs b/lib/PuppeteerSharp/TaskManager.cs new file mode 100644 index 000000000..b016f3373 --- /dev/null +++ b/lib/PuppeteerSharp/TaskManager.cs @@ -0,0 +1,31 @@ +using System; +using System.Linq; +using PuppeteerSharp.Helpers; + +namespace PuppeteerSharp +{ + internal class TaskManager + { + private ConcurrentSet WaitTasks { get; } = new ConcurrentSet(); + + internal void Add(WaitTask waitTask) => WaitTasks.Add(waitTask); + + internal void Delete(WaitTask waitTask) => WaitTasks.Remove(waitTask); + + internal void RerunAll() + { + foreach (var waitTask in WaitTasks) + { + _ = waitTask.Rerun(); + } + } + + internal void TerminateAll(Exception exception) + { + while (!WaitTasks.IsEmpty) + { + _ = WaitTasks.First().TerminateAsync(exception); + } + } + } +} \ No newline at end of file diff --git a/lib/PuppeteerSharp/WaitForFunctionOptions.cs b/lib/PuppeteerSharp/WaitForFunctionOptions.cs index ed751d627..a93511309 100644 --- a/lib/PuppeteerSharp/WaitForFunctionOptions.cs +++ b/lib/PuppeteerSharp/WaitForFunctionOptions.cs @@ -23,5 +23,15 @@ public class WaitForFunctionOptions /// An interval at which the pageFunction is executed. If no value is specified will use . /// public int? PollingInterval { get; set; } + + /// + /// Root element. + /// + internal IElementHandle Root { get; set; } + + /// + /// Page bindings. + /// + internal PageBinding[] Bindings { get; set; } } } diff --git a/lib/PuppeteerSharp/WaitTask.cs b/lib/PuppeteerSharp/WaitTask.cs index 680352af7..835b5ed92 100644 --- a/lib/PuppeteerSharp/WaitTask.cs +++ b/lib/PuppeteerSharp/WaitTask.cs @@ -7,128 +7,35 @@ namespace PuppeteerSharp { internal sealed class WaitTask : IDisposable { - private const string WaitForPredicatePageFunction = @" -async function waitForPredicatePageFunction( - root, - predicateBody, - predicateAcceptsContextElement, - polling, - timeout, - ...args -) { - root = root || document; - const predicate = new Function('...args', predicateBody); - let timedOut = false; - if (timeout) setTimeout(() => (timedOut = true), timeout); - if (polling === 'raf') return await pollRaf(); - if (polling === 'mutation') return await pollMutation(); - if (typeof polling === 'number') return await pollInterval(polling); - - /** - * @returns {!Promise<*>} - */ - async function pollMutation() { - const success = predicateAcceptsContextElement - ? await predicate(root, ...args) - : await predicate(...args); - if (success) return Promise.resolve(success); - - let fulfill; - const result = new Promise((x) => (fulfill = x)); - const observer = new MutationObserver(async () => { - if (timedOut) { - observer.disconnect(); - fulfill(); - } - const success = predicateAcceptsContextElement - ? await predicate(root, ...args) - : await predicate(...args); - if (success) { - observer.disconnect(); - fulfill(success); - } - }); - observer.observe(root, { - childList: true, - subtree: true, - attributes: true, - }); - return result; - } - - async function pollRaf() { - let fulfill; - const result = new Promise((x) => (fulfill = x)); - await onRaf(); - return result; - - async function onRaf() { - if (timedOut) { - fulfill(); - return; - } - const success = predicateAcceptsContextElement - ? await predicate(root, ...args) - : await predicate(...args); - if (success) fulfill(success); - else requestAnimationFrame(onRaf); - } - } - - async function pollInterval(pollInterval) { - let fulfill; - const result = new Promise((x) => (fulfill = x)); - await onTimeout(); - return result; - - async function onTimeout() { - if (timedOut) { - fulfill(); - return; - } - const success = predicateAcceptsContextElement - ? await predicate(root, ...args) - : await predicate(...args); - if (success) fulfill(success); - else setTimeout(onTimeout, pollInterval); - } - } -}"; - private readonly IsolatedWorld _isolatedWorld; - private readonly string _predicateBody; - private readonly WaitForFunctionPollingOption _polling; + private readonly string _fn; + private readonly WaitForFunctionPollingOption? _polling; private readonly int? _pollingInterval; - private readonly int _timeout; 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 _taskCompletion; + private readonly TaskCompletionSource _result; private readonly PageBinding[] _bindings; - private int _runCount; - private bool _terminated; private bool _isDisposed; + private IJSHandle _poller; + private bool _terminated; internal WaitTask( IsolatedWorld isolatedWorld, - string predicateBody, + 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(predicateBody)) + if (string.IsNullOrEmpty(fn)) { - throw new ArgumentNullException(nameof(predicateBody)); + throw new ArgumentNullException(nameof(fn)); } if (pollingInterval <= 0) @@ -137,16 +44,13 @@ internal sealed class WaitTask : IDisposable } _isolatedWorld = isolatedWorld; - _predicateBody = isExpression ? $"return ({predicateBody})" : $"return ({predicateBody})(...args)"; - _polling = polling; + _fn = isExpression ? $"() => {{return ({fn});}}" : fn; _pollingInterval = pollingInterval; - _timeout = timeout; + _polling = _pollingInterval.HasValue ? null : polling; _args = args ?? Array.Empty(); - _title = title; _root = root; _cts = new CancellationTokenSource(); - _predicateAcceptsContextElement = predicateAcceptsContextElement; - _taskCompletion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + _result = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); _bindings = bidings ?? Array.Empty(); foreach (var binding in _bindings) @@ -154,20 +58,20 @@ internal sealed class WaitTask : IDisposable _isolatedWorld.BoundFunctions.AddOrUpdate(binding.Name, binding.Function, (_, __) => binding.Function); } - _isolatedWorld.WaitTasks.Add(this); + _isolatedWorld.TaskManager.Add(this); if (timeout > 0) { _timeoutTimer = System.Threading.Tasks.Task.Delay(timeout, _cts.Token) .ContinueWith( - _ => Terminate(new WaitTaskTimeoutException(timeout, title)), + _ => TerminateAsync(new WaitTaskTimeoutException(timeout)), TaskScheduler.Default); } _ = Rerun(); } - internal Task Task => _taskCompletion.Task; + internal Task Task => _result.Task; public void Dispose() { @@ -183,84 +87,151 @@ public void Dispose() internal async Task Rerun() { - var runCount = Interlocked.Increment(ref _runCount); - IJSHandle success = null; - Exception exception = null; - - var context = await _isolatedWorld.GetExecutionContextAsync().ConfigureAwait(false); - await System.Threading.Tasks.Task.WhenAll(_bindings.Select(binding => _isolatedWorld.AddBindingToContextAsync(context, binding.Name))).ConfigureAwait(false); - try { - success = await context.EvaluateFunctionHandleAsync( - WaitForPredicatePageFunction, - new object[] - { - _root, - _predicateBody, - _predicateAcceptsContextElement, - _pollingInterval ?? (object)_polling, - _timeout, - }.Concat(_args).ToArray()).ConfigureAwait(false); + if (_bindings.Length > 0) + { + var context = await _isolatedWorld.GetExecutionContextAsync().ConfigureAwait(false); + await System.Threading.Tasks.Task.WhenAll(_bindings.Select(binding => _isolatedWorld.AddBindingToContextAsync(context, binding.Name))).ConfigureAwait(false); + } + + if (_pollingInterval.HasValue) + { + _poller = await _isolatedWorld.EvaluateFunctionHandleAsync( + @" + ({IntervalPoller, createFunction}, ms, fn, ...args) => { + const fun = createFunction(fn); + return new IntervalPoller(() => { + return fun(...args); + }, ms); + }", + new object[] + { + await _isolatedWorld.GetPuppeteerUtilAsync().ConfigureAwait(false), + _pollingInterval, + _fn, + }.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), + _fn, + }.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), + _root, + _fn, + }.Concat(_args).ToArray()).ConfigureAwait(false); + } + + await _poller.EvaluateFunctionAsync("poller => poller.start()").ConfigureAwait(false); + + var success = await _poller.EvaluateFunctionHandleAsync("poller => poller.result()").ConfigureAwait(false); + _result.TrySetResult(success); + await TerminateAsync().ConfigureAwait(false); } catch (Exception ex) { - exception = ex; - } - - if (_terminated || runCount != _runCount) - { - if (success != null) + var exception = GetBadException(ex); + if (exception != null) { - await success.DisposeAsync().ConfigureAwait(false); + await TerminateAsync(exception).ConfigureAwait(false); } + } + } + internal async Task TerminateAsync(Exception exception = null) + { + // The timeout timer might call this method on cleanup + if (_terminated) + { return; } - if (exception == null && - await _isolatedWorld.EvaluateFunctionAsync("s => !s", success) - .ContinueWith( - task => task.IsFaulted || task.Result, - TaskScheduler.Default) - .ConfigureAwait(false)) + _terminated = true; + _isolatedWorld.TaskManager.Delete(this); + Cleanup(); // This matches the clearTimeout upstream + + if (exception != null) + { + _result.TrySetException(exception); + } + + if (_poller is { } poller) { - if (success != null) + await using (poller.ConfigureAwait(false)) { - await success.DisposeAsync().ConfigureAwait(false); - } + try + { + await poller.EvaluateFunctionAsync(@"async poller => { + await poller.stop(); + }").ConfigureAwait(false); - return; + poller = null; + } + catch (Exception) + { + // swallow error. + } + } } + } - if (exception?.Message.Contains("Execution context was destroyed") == true) + private Exception GetBadException(Exception exception) + { + // When frame is detached the task should have been terminated by the IsolatedWorld. + // This can fail if we were adding this task while the frame was detached, + // so we terminate here instead. + if (exception.Message.Contains("Execution context is not available in detached frame")) { - _ = Rerun(); - return; + return new PuppeteerException("Waiting failed: Frame detached", exception); } - if (exception?.Message.Contains("Cannot find context with specified id") == true) + // When the page is navigated, the promise is rejected. + // We will try again in the new execution context. + if (exception.Message.Contains("Execution context was destroyed")) { - return; + return null; } - if (exception != null) + // We could have tried to evaluate in a context which was already destroyed. + if (exception.Message.Contains("Cannot find context with specified id")) { - _taskCompletion.TrySetException(exception); + return null; } - else + + // We don't have this check upstream. + // We have a situation in our async code where a new navigation could be executed + // before the WaitForFunction completes its initialization + // See FrameWaitForSelectorTests.ShouldSurviveCrossProssNavigation + if (exception.Message.Contains("JSHandles can be evaluated only in the context they were created!")) { - _taskCompletion.TrySetResult(success); + return null; } - Cleanup(); - } - - internal void Terminate(Exception exception) - { - _terminated = true; - _taskCompletion.TrySetException(exception); - Cleanup(); + return exception; } private void Cleanup() @@ -276,8 +247,6 @@ private void Cleanup() // Ignore } } - - _isolatedWorld.WaitTasks.Remove(this); } } } diff --git a/lib/PuppeteerSharp/WaitTaskTimeoutException.cs b/lib/PuppeteerSharp/WaitTaskTimeoutException.cs index d7ff3ebd9..899ca6252 100644 --- a/lib/PuppeteerSharp/WaitTaskTimeoutException.cs +++ b/lib/PuppeteerSharp/WaitTaskTimeoutException.cs @@ -10,14 +10,14 @@ namespace PuppeteerSharp public class WaitTaskTimeoutException : PuppeteerException { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public WaitTaskTimeoutException() { } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// Message. public WaitTaskTimeoutException(string message) : base(message) @@ -25,18 +25,27 @@ public WaitTaskTimeoutException(string message) : base(message) } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. + /// + /// Timeout. + public WaitTaskTimeoutException(int timeout) : base($"Waiting failed: {timeout}ms exceeded") + { + Timeout = timeout; + } + + /// + /// Initializes a new instance of the class. /// /// Timeout. /// Element type. - public WaitTaskTimeoutException(int timeout, string elementType) : base($"waiting for {elementType} failed: timeout {timeout} ms exceeded") + public WaitTaskTimeoutException(int timeout, string elementType) : base($"waiting for {elementType} failed: timeout {timeout}ms exceeded") { Timeout = timeout; ElementType = elementType; } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// Message. /// Inner exception. @@ -45,7 +54,7 @@ public WaitTaskTimeoutException(string message, Exception innerException) : base } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// Info. /// Context.