fix(evaluate): awaitPromise when Promise is overwritten (#2759)

Firefox and WebKit require native promises to provide awaitPromise
functionality. When the Promise is overwritten, all evaluations
in the main world produce wrong Promise, so we wrap with async
function to get a native promise instead.
This commit is contained in:
Dmitry Gozman 2020-06-29 16:25:52 -07:00 committed by GitHub
parent e154e0837b
commit 1fa9d30992
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 55 additions and 6 deletions

View File

@ -52,9 +52,6 @@ export function serializeAsCallArgument(value: any, jsHandleSerializer: (value:
}
function serialize(value: any, jsHandleSerializer: (value: any) => { fallThrough?: any }, visited: Set<any>): any {
if (value && typeof value === 'object' && typeof value.then === 'function')
return value;
const result = jsHandleSerializer(value);
if ('fallThrough' in result)
value = result.fallThrough;

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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);
});