From 79b910a7742bb7ae44c484b214dec081b79e6fab Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Thu, 15 Oct 2026 12:05:53 -0700 Subject: [PATCH 1/2] fix(evaluate): await page-overridden Promise in evaluateHandle and waitForFunction Fixes: https://github.com/microsoft/playwright/issues/43415 --- packages/injected/src/utilityScript.ts | 14 ++++++++++++-- tests/page/page-evaluate.spec.ts | 6 ++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/injected/src/utilityScript.ts b/packages/injected/src/utilityScript.ts index 5c257ef25d7ca..3d95ae451124f 100644 --- a/packages/injected/src/utilityScript.ts +++ b/packages/injected/src/utilityScript.ts @@ -82,7 +82,13 @@ export class UtilityScript { if (typeof result === 'function') result = result(...parameters); } - return returnByValue ? this._promiseAwareJsonValueNoThrow(result) : result; + if (returnByValue) + return this._promiseAwareJsonValueNoThrow(result); + if (isThenable(result)) { + // Wrap into a native Promise, see _promiseAwareJsonValueNoThrow. + return (async () => await result)(); + } + return result; } jsonValue(returnByValue: true, value: any) { @@ -101,7 +107,7 @@ export class UtilityScript { } }; - if (value && typeof value === 'object' && typeof value.then === 'function') { + if (isThenable(value)) { return (async () => { // By using async function we ensure that return value is a native Promise, // and not some overridden Promise in the page. @@ -114,3 +120,7 @@ export class UtilityScript { return safeJson(value); } } + +function isThenable(value: any): boolean { + return value && typeof value === 'object' && typeof value.then === 'function'; +} diff --git a/tests/page/page-evaluate.spec.ts b/tests/page/page-evaluate.spec.ts index 30cd7a3462c93..85b7246d07a27 100644 --- a/tests/page/page-evaluate.spec.ts +++ b/tests/page/page-evaluate.spec.ts @@ -341,6 +341,12 @@ it('should work with overwritten Promise', async ({ page }) => { // Now, the new promise should be awaitable. expect(await page.evaluate(() => Promise.resolve(42))).toBe(42); + const handle = await page.evaluateHandle(() => new Promise(f => setTimeout(() => f(42), 50))); + expect(await handle.jsonValue()).toBe(42); + const error = await page.waitForFunction(() => false, undefined, { timeout: 500 }).catch(e => e); + expect(error.message).toContain('Timeout 500ms exceeded'); + await page.evaluate(() => setTimeout(() => (window as any).__done = true, 50)); + await page.waitForFunction(() => (window as any).__done); }); it('should throw when passed more than one parameter', async ({ page }) => { From 308b8a347b0574f43fff14befbe925b25ea12c9e Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Thu, 15 Oct 2026 12:15:51 -0700 Subject: [PATCH 2/2] chore: unify thenable handling for both evaluate paths --- packages/injected/src/utilityScript.ts | 38 ++++++++++---------------- 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/packages/injected/src/utilityScript.ts b/packages/injected/src/utilityScript.ts index 3d95ae451124f..5dc12744a417a 100644 --- a/packages/injected/src/utilityScript.ts +++ b/packages/injected/src/utilityScript.ts @@ -82,13 +82,17 @@ export class UtilityScript { if (typeof result === 'function') result = result(...parameters); } - if (returnByValue) - return this._promiseAwareJsonValueNoThrow(result); if (isThenable(result)) { - // Wrap into a native Promise, see _promiseAwareJsonValueNoThrow. - return (async () => await result)(); + return (async () => { + // By using async function we ensure that return value is a native Promise, + // and not some overridden Promise in the page. + // This makes Firefox and WebKit debugging protocols recognize it as a Promise, + // properly await and return the value. + const value = await result; + return returnByValue ? this._jsonValueNoThrow(value) : value; + })(); } - return result; + return returnByValue ? this._jsonValueNoThrow(result) : result; } jsonValue(returnByValue: true, value: any) { @@ -98,26 +102,12 @@ export class UtilityScript { return serializeAsCallArgument(value, (value: any) => ({ fallThrough: value })); } - private _promiseAwareJsonValueNoThrow(value: any) { - const safeJson = (value: any) => { - try { - return this.jsonValue(true, value); - } catch (e) { - return undefined; - } - }; - - if (isThenable(value)) { - return (async () => { - // By using async function we ensure that return value is a native Promise, - // and not some overridden Promise in the page. - // This makes Firefox and WebKit debugging protocols recognize it as a Promise, - // properly await and return the value. - const promiseValue = await value; - return safeJson(promiseValue); - })(); + private _jsonValueNoThrow(value: any) { + try { + return this.jsonValue(true, value); + } catch (e) { + return undefined; } - return safeJson(value); } }