From a63a0198de6e743492827f677ee12737011645b3 Mon Sep 17 00:00:00 2001 From: Andrey Lushnikov Date: Wed, 19 Jul 2017 19:04:51 -0700 Subject: [PATCH] Implement waitFor which survives navigation (#99) This patch implements page.waitFor method which survives navigation. References #89. --- docs/api.md | 14 ++++++++++ lib/FrameManager.js | 60 ++++++++++++++++++++++++++++++++++++++++--- test/test.js | 21 +++++++++++++++ utils/doclint/lint.js | 1 + 4 files changed, 92 insertions(+), 4 deletions(-) diff --git a/docs/api.md b/docs/api.md index 7df54e2a..95176e46 100644 --- a/docs/api.md +++ b/docs/api.md @@ -560,6 +560,20 @@ This is a shortcut for [page.mainFrame().url()](#frameurl) - `selector` <[string]> A query selector to wait for on the page. - returns: <[Promise]> Promise which resolves when the element matching `selector` appears in the page. +The `page.waitFor` successfully survives page navigations: +```js +const {Browser} = new require('puppeteer'); +const browser = new Browser(); + +browser.newPage().then(async page => { + let currentURL; + page.waitFor('img').then(() => console.log('First URL with image: ' + currentURL)); + for (currentURL of ['https://example.com', 'https://google.com', 'https://bbc.com']) + await page.navigate(currentURL); + browser.close(); +}); +``` + #### page.waitForNavigation(options) - `options` <[Object]> Navigation parameters, same as in [page.navigate](#pagenavigateurl-options). - returns: <[Promise]<[Response]>> Promise which resolves to the main resource response. In case of multiple redirects, the navigation will resolve with the response of the last redirect. diff --git a/lib/FrameManager.js b/lib/FrameManager.js index c1552168..b12f300e 100644 --- a/lib/FrameManager.js +++ b/lib/FrameManager.js @@ -177,7 +177,7 @@ class FrameManager extends EventEmitter { * @param {!Frame} frame * @return {!Promise} */ - async _waitForInFrame(selector, frame) { + async _waitForSelector(selector, frame) { function code(selector) { if (document.querySelector(selector)) @@ -193,7 +193,7 @@ class FrameManager extends EventEmitter { return; } }); - mo.observe(document.documentElement, { + mo.observe(document, { childList: true, subtree: true }); @@ -240,6 +240,9 @@ class Frame { this._parentFrame = parentFrame; this._url = ''; this._id = frameId; + /** @type {!Set} */ + this._awaitedElements = new Set(); + this._adoptPayload(payload); /** @type {!Set} */ @@ -301,10 +304,15 @@ class Frame { /** * @param {string} selector - * @return {!Promise} + * @return {!Promise} */ async waitFor(selector) { - await this._frameManager._waitForInFrame(selector, this); + const awaitedElement = new AwaitedElement(() => this._frameManager._waitForSelector(selector, this)); + + this._awaitedElements.add(awaitedElement); + let cleanup = () => this._awaitedElements.delete(awaitedElement); + awaitedElement.promise.then(cleanup, cleanup); + return awaitedElement.promise; } /** @@ -349,9 +357,13 @@ class Frame { }; this._name = framePayload.name; this._url = framePayload.url; + for (let awaitedElement of this._awaitedElements) + awaitedElement.startWaiting(); } _detach() { + for (let awaitedElement of this._awaitedElements) + awaitedElement.terminate(new Error('waitForSelector failed: frame got detached.')); this._detached = true; if (this._parentFrame) this._parentFrame._childFrames.delete(this); @@ -360,4 +372,44 @@ class Frame { } helper.tracePublicAPI(Frame); +class AwaitedElement { + /** + * @param {function():!Promise} waitInPageCallback + */ + constructor(waitInPageCallback) { + this.promise = new Promise((resolve, reject) => { + this._resolve = resolve; + this._reject = reject; + }); + this._waitInPageCallback = waitInPageCallback; + this._waitPromise = null; + this.startWaiting(); + } + + /** + * @param {!Error} error + */ + terminate(error) { + this._reject(error); + this._waitTaskPromise = null; + } + + startWaiting() { + let waitPromise = this._waitInPageCallback.call(null).then(finish.bind(this), finish.bind(this)); + this._waitPromise = waitPromise; + + /** + * @param {?Error} error + */ + function finish(error) { + if (this._waitPromise !== waitPromise) + return; + if (error) + this._reject(error); + else + this._resolve(); + } + } +} + module.exports = FrameManager; diff --git a/test/test.js b/test/test.js index ed798bc2..612acf67 100644 --- a/test/test.js +++ b/test/test.js @@ -239,6 +239,27 @@ describe('Puppeteer', function() { expect(e.message).toContain('Evaluation failed: document.querySelector is not a function'); } })); + it('should throw when frame is detached', SX(async function() { + await FrameUtils.attachFrame(page, 'frame1', EMPTY_PAGE); + let frame = page.frames()[1]; + let waitError = null; + let waitPromise = frame.waitFor('.box').catch(e => waitError = e); + await FrameUtils.detachFrame(page, 'frame1'); + await waitPromise; + expect(waitError).toBeTruthy(); + expect(waitError.message).toContain('waitForSelector failed: frame got detached.'); + })); + it('should survive navigation', SX(async function() { + let boxFound = false; + let waitFor = page.waitFor('.box').then(() => boxFound = true); + await page.navigate(EMPTY_PAGE); + expect(boxFound).toBe(false); + await page.reload(); + expect(boxFound).toBe(false); + await page.navigate(PREFIX + '/grid.html'); + await waitFor; + expect(boxFound).toBe(true); + })); }); describe('Page.Events.Console', function() { diff --git a/utils/doclint/lint.js b/utils/doclint/lint.js index 729dee7f..d89b26bf 100644 --- a/utils/doclint/lint.js +++ b/utils/doclint/lint.js @@ -7,6 +7,7 @@ const Browser = require('../../lib/Browser'); const PROJECT_DIR = path.join(__dirname, '..', '..'); let EXCLUDE_CLASSES = new Set([ + 'AwaitedElement', 'Connection', 'EmulationManager', 'FrameManager',