fix(page): execute frame.waitFor{Selector,XPath} in secondary world (#3856)

This patch starts executing frame.waitForSelector and frame.waitForXPath
in secondary world. As a result, websites that mutate page global
context (e.g. removing global MutationObserver) don't break Puppeteer's
behavior.

Fixes #609
This commit is contained in:
Andrey Lushnikov 2019-01-28 12:24:27 -08:00 committed by GitHub
parent 2061dd4718
commit 55432f88e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 39 additions and 22 deletions

View File

@ -173,6 +173,22 @@ class ExecutionContext {
});
return createJSHandle(this, response.objects);
}
/**
* @param {Puppeteer.ElementHandle} elementHandle
* @return {Promise<Puppeteer.ElementHandle>}
*/
async _adoptElementHandle(elementHandle) {
assert(elementHandle.executionContext() !== this, 'Cannot adopt handle that already belongs to this execution context');
assert(this._world, 'Cannot adopt handle without DOMWorld');
const nodeInfo = await this._client.send('DOM.describeNode', {
objectId: elementHandle._remoteObject.objectId,
});
const {object} = await this._client.send('DOM.resolveNode', {
backendNodeId: nodeInfo.node.backendNodeId,
});
return /** @type {Puppeteer.ElementHandle}*/(createJSHandle(this, object));
}
}
module.exports = {ExecutionContext, EVALUATION_SCRIPT_URL};

View File

@ -608,8 +608,14 @@ class Frame {
* @param {!{visible?: boolean, hidden?: boolean, timeout?: number}=} options
* @return {!Promise<?Puppeteer.ElementHandle>}
*/
waitForSelector(selector, options) {
return this._mainWorld.waitForSelector(selector, options);
async waitForSelector(selector, options) {
const handle = await this._secondaryWorld.waitForSelector(selector, options);
if (!handle)
return null;
const mainExecutionContext = await this._mainWorld.executionContext();
const result = await mainExecutionContext._adoptElementHandle(handle);
await handle.dispose();
return result;
}
/**
@ -617,8 +623,14 @@ class Frame {
* @param {!{visible?: boolean, hidden?: boolean, timeout?: number}=} options
* @return {!Promise<?Puppeteer.ElementHandle>}
*/
waitForXPath(xpath, options) {
return this._mainWorld.waitForXPath(xpath, options);
async waitForXPath(xpath, options) {
const handle = await this._secondaryWorld.waitForXPath(xpath, options);
if (!handle)
return null;
const mainExecutionContext = await this._mainWorld.executionContext();
const result = await mainExecutionContext._adoptElementHandle(handle);
await handle.dispose();
return result;
}
/**

View File

@ -205,6 +205,13 @@ module.exports.addTests = function({testRunner, expect, product}) {
await frame.waitForSelector('div');
});
it('should work with removed MutationObserver', async({page, server}) => {
await page.evaluate(() => delete window.MutationObserver);
const waitForSelector = page.waitForSelector('.zombo');
await page.setContent(`<div class='zombo'>anything</div>`);
expect(await page.evaluate(x => x.textContent, await waitForSelector)).toBe('anything');
});
it('should resolve promise when node is added', async({page, server}) => {
await page.goto(server.EMPTY_PAGE);
const frame = page.mainFrame();
@ -247,15 +254,6 @@ module.exports.addTests = function({testRunner, expect, product}) {
expect(eHandle.executionContext().frame()).toBe(frame2);
});
it('should throw if evaluation failed', async({page, server}) => {
await page.evaluateOnNewDocument(function() {
document.querySelector = null;
});
await page.goto(server.EMPTY_PAGE);
let error = null;
await page.waitForSelector('*').catch(e => error = e);
expect(error.message).toContain('document.querySelector is not a function');
});
it('should throw when frame is detached', async({page, server}) => {
await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
const frame = page.frames()[1];
@ -394,15 +392,6 @@ module.exports.addTests = function({testRunner, expect, product}) {
const eHandle = await waitForXPathPromise;
expect(eHandle.executionContext().frame()).toBe(frame2);
});
it('should throw if evaluation failed', async({page, server}) => {
await page.evaluateOnNewDocument(function() {
document.evaluate = null;
});
await page.goto(server.EMPTY_PAGE);
let error = null;
await page.waitForXPath('*').catch(e => error = e);
expect(error.message).toContain('document.evaluate is not a function');
});
it('should throw when frame is detached', async({page, server}) => {
await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
const frame = page.frames()[1];