diff --git a/src/common/utilityScriptSerializers.ts b/src/common/utilityScriptSerializers.ts index e35a1a11d0..43b5db5273 100644 --- a/src/common/utilityScriptSerializers.ts +++ b/src/common/utilityScriptSerializers.ts @@ -52,9 +52,6 @@ export function serializeAsCallArgument(value: any, jsHandleSerializer: (value: } function serialize(value: any, jsHandleSerializer: (value: any) => { fallThrough?: any }, visited: Set): any { - if (value && typeof value === 'object' && typeof value.then === 'function') - return value; - const result = jsHandleSerializer(value); if ('fallThrough' in result) value = result.fallThrough; diff --git a/src/injected/utilityScript.ts b/src/injected/utilityScript.ts index c1cc401130..eae4d33ac0 100644 --- a/src/injected/utilityScript.ts +++ b/src/injected/utilityScript.ts @@ -47,8 +47,16 @@ export default class UtilityScript { } }; - if (value && typeof value === 'object' && typeof value.then === 'function') - return value.then(safeJson); + if (value && typeof value === 'object' && typeof value.then === 'function') { + return (async () => { + // By using async function we ensure that return value is a native Promise, + // and not some overriden 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); + })(); + } return safeJson(value); } } diff --git a/test/evaluation.spec.js b/test/evaluation.spec.js index 8924585d87..0acc46d384 100644 --- a/test/evaluation.spec.js +++ b/test/evaluation.spec.js @@ -200,6 +200,50 @@ describe('Page.evaluate', function() { const result = await page.evaluate(() => -Infinity); expect(Object.is(result, -Infinity)).toBe(true); }); + it('should work with overwritten Promise', async({page, server}) => { + await page.evaluate(() => { + const originalPromise = window.Promise; + class Promise2 { + static all(...arg) { + return wrap(originalPromise.all(...arg)); + } + static race(...arg) { + return wrap(originalPromise.race(...arg)); + } + static resolve(...arg) { + return wrap(originalPromise.resolve(...arg)); + } + constructor(f, r) { + this._promise = new originalPromise(f, r); + } + then(f, r) { + return wrap(this._promise.then(f, r)); + } + catch(f) { + return wrap(this._promise.catch(f)); + } + finally(f) { + return wrap(this._promise.finally(f)); + } + }; + const wrap = p => { + const result = new Promise2(() => {}, () => {}); + result._promise = p; + return result; + }; + window.Promise = Promise2; + window.__Promise2 = Promise2; + }); + + // Sanity check. + expect(await page.evaluate(() => { + const p = Promise.all([Promise.race([]), new Promise(() => {}).then(() => {})]); + return p instanceof window.__Promise2; + })).toBe(true); + + // Now, the new promise should be awaitable. + expect(await page.evaluate(() => Promise.resolve(42))).toBe(42); + }); it('should throw when passed more than one parameter', async({page, server}) => { const expectThrow = async f => { let error; diff --git a/test/frame.spec.js b/test/frame.spec.js index 911d72f96a..827c18d946 100644 --- a/test/frame.spec.js +++ b/test/frame.spec.js @@ -115,7 +115,7 @@ describe('Frame.evaluate', function() { iframe.contentDocument.close(); }); // Main world should work. - expect(await page.frames()[1].evaluate(() => window.top.location.href)).toBe('http://localhost:8907/empty.html'); + expect(await page.frames()[1].evaluate(() => window.top.location.href)).toBe(server.EMPTY_PAGE); // Utility world should work. expect(await page.frames()[1].$('div')).toBeTruthy(null); });