Skip to content

Commit

Permalink
fix(page): execute frame.waitFor{Selector,XPath} in secondary world (#…
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
aslushnikov committed Jan 28, 2019
1 parent 2061dd4 commit 55432f8
Show file tree
Hide file tree
Showing 3 changed files with 39 additions and 22 deletions.
16 changes: 16 additions & 0 deletions lib/ExecutionContext.js
Expand Up @@ -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};
20 changes: 16 additions & 4 deletions lib/FrameManager.js
Expand Up @@ -608,17 +608,29 @@ 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;
}

/**
* @param {string} xpath
* @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;
}

/**
Expand Down
25 changes: 7 additions & 18 deletions test/waittask.spec.js
Expand Up @@ -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();
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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];
Expand Down

0 comments on commit 55432f8

Please sign in to comment.