fix(page): do evaluations with one roundtrip (#4539)

This patch teaches page.evaluate to do 1 hop instead of 2 hops.
As a result, things such as `page.select` will not throw an
unfortunate exception when they schedule a navigation.

Fix #4537
This commit is contained in:
Andrey Lushnikov 2019-06-07 13:46:43 -07:00 committed by GitHub
parent 7e1984615a
commit 90df69cf77
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 34 additions and 9 deletions

View File

@ -45,16 +45,15 @@ class ExecutionContext {
* @return {!Promise<(!Object|undefined)>} * @return {!Promise<(!Object|undefined)>}
*/ */
async evaluate(pageFunction, ...args) { async evaluate(pageFunction, ...args) {
const handle = await this.evaluateHandle(pageFunction, ...args); try {
const result = await handle.jsonValue().catch(error => { return await this._evaluateInternal(true /* returnByValue */, pageFunction, ...args);
} catch (error) {
if (error.message.includes('Object reference chain is too long')) if (error.message.includes('Object reference chain is too long'))
return; return;
if (error.message.includes('Object couldn\'t be returned by value')) if (error.message.includes('Object couldn\'t be returned by value'))
return; return;
throw error; throw error;
}); }
await handle.dispose();
return result;
} }
/** /**
@ -63,6 +62,15 @@ class ExecutionContext {
* @return {!Promise<!JSHandle>} * @return {!Promise<!JSHandle>}
*/ */
async evaluateHandle(pageFunction, ...args) { async evaluateHandle(pageFunction, ...args) {
return this._evaluateInternal(false /* returnByValue */, pageFunction, ...args);
}
/**
* @param {Function|string} pageFunction
* @param {...*} args
* @return {!Promise<!JSHandle>}
*/
async _evaluateInternal(returnByValue, pageFunction, ...args) {
const suffix = `//# sourceURL=${EVALUATION_SCRIPT_URL}`; const suffix = `//# sourceURL=${EVALUATION_SCRIPT_URL}`;
if (helper.isString(pageFunction)) { if (helper.isString(pageFunction)) {
@ -72,13 +80,13 @@ class ExecutionContext {
const {exceptionDetails, result: remoteObject} = await this._client.send('Runtime.evaluate', { const {exceptionDetails, result: remoteObject} = await this._client.send('Runtime.evaluate', {
expression: expressionWithSourceUrl, expression: expressionWithSourceUrl,
contextId, contextId,
returnByValue: false, returnByValue,
awaitPromise: true, awaitPromise: true,
userGesture: true userGesture: true
}).catch(rewriteError); }).catch(rewriteError);
if (exceptionDetails) if (exceptionDetails)
throw new Error('Evaluation failed: ' + helper.getExceptionMessage(exceptionDetails)); throw new Error('Evaluation failed: ' + helper.getExceptionMessage(exceptionDetails));
return createJSHandle(this, remoteObject); return returnByValue ? helper.valueFromRemoteObject(remoteObject) : createJSHandle(this, remoteObject);
} }
if (typeof pageFunction !== 'function') if (typeof pageFunction !== 'function')
@ -107,7 +115,7 @@ class ExecutionContext {
functionDeclaration: functionText + '\n' + suffix + '\n', functionDeclaration: functionText + '\n' + suffix + '\n',
executionContextId: this._contextId, executionContextId: this._contextId,
arguments: args.map(convertArgument.bind(this)), arguments: args.map(convertArgument.bind(this)),
returnByValue: false, returnByValue,
awaitPromise: true, awaitPromise: true,
userGesture: true userGesture: true
}); });
@ -119,7 +127,7 @@ class ExecutionContext {
const { exceptionDetails, result: remoteObject } = await callFunctionOnPromise.catch(rewriteError); const { exceptionDetails, result: remoteObject } = await callFunctionOnPromise.catch(rewriteError);
if (exceptionDetails) if (exceptionDetails)
throw new Error('Evaluation failed: ' + helper.getExceptionMessage(exceptionDetails)); throw new Error('Evaluation failed: ' + helper.getExceptionMessage(exceptionDetails));
return createJSHandle(this, remoteObject); return returnByValue ? helper.valueFromRemoteObject(remoteObject) : createJSHandle(this, remoteObject);
/** /**
* @param {*} arg * @param {*} arg

View File

@ -231,6 +231,14 @@ module.exports.addTests = function({testRunner, expect}) {
const error = await executionContext.evaluate(() => null).catch(e => e); const error = await executionContext.evaluate(() => null).catch(e => e);
expect(error.message).toContain('navigation'); expect(error.message).toContain('navigation');
}); });
it_fails_ffox('should not throw an error when evaluation does a navigation', async({page, server}) => {
await page.goto(server.PREFIX + '/one-style.html');
const result = await page.evaluate(() => {
window.location = '/empty.html';
return [42];
});
expect(result).toEqual([42]);
});
}); });
describe('Page.evaluateOnNewDocument', function() { describe('Page.evaluateOnNewDocument', function() {

View File

@ -1067,6 +1067,15 @@ module.exports.addTests = function({testRunner, expect, headless, puppeteer, CHR
expect(await page.evaluate(() => result.onInput)).toEqual(['blue']); expect(await page.evaluate(() => result.onInput)).toEqual(['blue']);
expect(await page.evaluate(() => result.onChange)).toEqual(['blue']); expect(await page.evaluate(() => result.onChange)).toEqual(['blue']);
}); });
it_fails_ffox('should not throw when select causes navigation', async({page, server}) => {
await page.goto(server.PREFIX + '/input/select.html');
await page.$eval('select', select => select.addEventListener('input', () => window.location = '/empty.html'));
await Promise.all([
page.select('select', 'blue'),
page.waitForNavigation(),
]);
expect(page.url()).toContain('empty.html');
});
it('should select multiple options', async({page, server}) => { it('should select multiple options', async({page, server}) => {
await page.goto(server.PREFIX + '/input/select.html'); await page.goto(server.PREFIX + '/input/select.html');
await page.evaluate(() => makeMultiple()); await page.evaluate(() => makeMultiple());