Skip to content

Commit 90df69c

Browse files
authored
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
1 parent 7e19846 commit 90df69c

3 files changed

Lines changed: 34 additions & 9 deletions

File tree

lib/ExecutionContext.js

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,16 +45,15 @@ class ExecutionContext {
4545
* @return {!Promise<(!Object|undefined)>}
4646
*/
4747
async evaluate(pageFunction, ...args) {
48-
const handle = await this.evaluateHandle(pageFunction, ...args);
49-
const result = await handle.jsonValue().catch(error => {
48+
try {
49+
return await this._evaluateInternal(true /* returnByValue */, pageFunction, ...args);
50+
} catch (error) {
5051
if (error.message.includes('Object reference chain is too long'))
5152
return;
5253
if (error.message.includes('Object couldn\'t be returned by value'))
5354
return;
5455
throw error;
55-
});
56-
await handle.dispose();
57-
return result;
56+
}
5857
}
5958

6059
/**
@@ -63,6 +62,15 @@ class ExecutionContext {
6362
* @return {!Promise<!JSHandle>}
6463
*/
6564
async evaluateHandle(pageFunction, ...args) {
65+
return this._evaluateInternal(false /* returnByValue */, pageFunction, ...args);
66+
}
67+
68+
/**
69+
* @param {Function|string} pageFunction
70+
* @param {...*} args
71+
* @return {!Promise<!JSHandle>}
72+
*/
73+
async _evaluateInternal(returnByValue, pageFunction, ...args) {
6674
const suffix = `//# sourceURL=${EVALUATION_SCRIPT_URL}`;
6775

6876
if (helper.isString(pageFunction)) {
@@ -72,13 +80,13 @@ class ExecutionContext {
7280
const {exceptionDetails, result: remoteObject} = await this._client.send('Runtime.evaluate', {
7381
expression: expressionWithSourceUrl,
7482
contextId,
75-
returnByValue: false,
83+
returnByValue,
7684
awaitPromise: true,
7785
userGesture: true
7886
}).catch(rewriteError);
7987
if (exceptionDetails)
8088
throw new Error('Evaluation failed: ' + helper.getExceptionMessage(exceptionDetails));
81-
return createJSHandle(this, remoteObject);
89+
return returnByValue ? helper.valueFromRemoteObject(remoteObject) : createJSHandle(this, remoteObject);
8290
}
8391

8492
if (typeof pageFunction !== 'function')
@@ -107,7 +115,7 @@ class ExecutionContext {
107115
functionDeclaration: functionText + '\n' + suffix + '\n',
108116
executionContextId: this._contextId,
109117
arguments: args.map(convertArgument.bind(this)),
110-
returnByValue: false,
118+
returnByValue,
111119
awaitPromise: true,
112120
userGesture: true
113121
});
@@ -119,7 +127,7 @@ class ExecutionContext {
119127
const { exceptionDetails, result: remoteObject } = await callFunctionOnPromise.catch(rewriteError);
120128
if (exceptionDetails)
121129
throw new Error('Evaluation failed: ' + helper.getExceptionMessage(exceptionDetails));
122-
return createJSHandle(this, remoteObject);
130+
return returnByValue ? helper.valueFromRemoteObject(remoteObject) : createJSHandle(this, remoteObject);
123131

124132
/**
125133
* @param {*} arg

test/evaluation.spec.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,14 @@ module.exports.addTests = function({testRunner, expect}) {
231231
const error = await executionContext.evaluate(() => null).catch(e => e);
232232
expect(error.message).toContain('navigation');
233233
});
234+
it_fails_ffox('should not throw an error when evaluation does a navigation', async({page, server}) => {
235+
await page.goto(server.PREFIX + '/one-style.html');
236+
const result = await page.evaluate(() => {
237+
window.location = '/empty.html';
238+
return [42];
239+
});
240+
expect(result).toEqual([42]);
241+
});
234242
});
235243

236244
describe('Page.evaluateOnNewDocument', function() {

test/page.spec.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1067,6 +1067,15 @@ module.exports.addTests = function({testRunner, expect, headless, puppeteer, CHR
10671067
expect(await page.evaluate(() => result.onInput)).toEqual(['blue']);
10681068
expect(await page.evaluate(() => result.onChange)).toEqual(['blue']);
10691069
});
1070+
it_fails_ffox('should not throw when select causes navigation', async({page, server}) => {
1071+
await page.goto(server.PREFIX + '/input/select.html');
1072+
await page.$eval('select', select => select.addEventListener('input', () => window.location = '/empty.html'));
1073+
await Promise.all([
1074+
page.select('select', 'blue'),
1075+
page.waitForNavigation(),
1076+
]);
1077+
expect(page.url()).toContain('empty.html');
1078+
});
10701079
it('should select multiple options', async({page, server}) => {
10711080
await page.goto(server.PREFIX + '/input/select.html');
10721081
await page.evaluate(() => makeMultiple());

0 commit comments

Comments
 (0)