From 55432f88e99598c41b8cd73f6aa9c27faee51873 Mon Sep 17 00:00:00 2001 From: Andrey Lushnikov Date: Mon, 28 Jan 2019 12:24:27 -0800 Subject: [PATCH] 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 --- lib/ExecutionContext.js | 16 ++++++++++++++++ lib/FrameManager.js | 20 ++++++++++++++++---- test/waittask.spec.js | 25 +++++++------------------ 3 files changed, 39 insertions(+), 22 deletions(-) diff --git a/lib/ExecutionContext.js b/lib/ExecutionContext.js index c17a5de1e3271..d1217a2f95918 100644 --- a/lib/ExecutionContext.js +++ b/lib/ExecutionContext.js @@ -173,6 +173,22 @@ class ExecutionContext { }); return createJSHandle(this, response.objects); } + + /** + * @param {Puppeteer.ElementHandle} elementHandle + * @return {Promise} + */ + 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}; diff --git a/lib/FrameManager.js b/lib/FrameManager.js index 126fdb1d39ded..0b68ca4ec9e93 100644 --- a/lib/FrameManager.js +++ b/lib/FrameManager.js @@ -608,8 +608,14 @@ class Frame { * @param {!{visible?: boolean, hidden?: boolean, timeout?: number}=} options * @return {!Promise} */ - 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} */ - 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; } /** diff --git a/test/waittask.spec.js b/test/waittask.spec.js index 2b77eec9739b9..ad5dca91c25ad 100644 --- a/test/waittask.spec.js +++ b/test/waittask.spec.js @@ -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(`
anything
`); + 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];