From 98c3894c84a4e524f2670bbe23a8c4c9520307c7 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 18 Jul 2017 18:54:24 -0700 Subject: [PATCH] Introduce Page.waitForNavigation (#94) This patch introduces Page.waitForNavigation which allows to wait for render-initiated navigation. This patch also does a nice refactoring, replacing Navigator with NavigatorWatcher which is not a part of a page state. References #89 --- docs/api.md | 10 ++++- lib/{Navigator.js => NavigatorWatcher.js} | 40 ++++++++++++++------ lib/Page.js | 46 ++++++++++++----------- test/test.js | 11 ++++++ utils/doclint/lint.js | 2 +- 5 files changed, 74 insertions(+), 35 deletions(-) rename lib/{Navigator.js => NavigatorWatcher.js} (81%) diff --git a/docs/api.md b/docs/api.md index 46f97183..518197bc 100644 --- a/docs/api.md +++ b/docs/api.md @@ -42,7 +42,7 @@ * [page.navigate(url, options)](#pagenavigateurl-options) * [page.pdf(options)](#pagepdfoptions) * [page.plainText()](#pageplaintext) - * [page.reload()](#pagereload) + * [page.reload(options)](#pagereloadoptions) * [page.screenshot([options])](#pagescreenshotoptions) * [page.setContent(html)](#pagesetcontenthtml) * [page.setHTTPHeaders(headers)](#pagesethttpheadersheaders) @@ -57,6 +57,7 @@ * [page.userAgent()](#pageuseragent) * [page.viewport()](#pageviewport) * [page.waitFor(selector)](#pagewaitforselector) + * [page.waitForNavigation(options)](#pagewaitfornavigationoptions) - [class: Keyboard](#class-keyboard) * [keyboard.hold(key[, options])](#keyboardholdkey-options) * [keyboard.modifiers()](#keyboardmodifiers) @@ -422,7 +423,8 @@ The `format` options are: #### page.plainText() - returns: <[Promise]<[string]>> Returns page's inner text. -#### page.reload() +#### page.reload(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. #### page.screenshot([options]) @@ -535,6 +537,10 @@ 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. +#### 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. + Shortcut for [page.mainFrame().waitFor(selector)](#framewaitforselector). ### class: Keyboard diff --git a/lib/Navigator.js b/lib/NavigatorWatcher.js similarity index 81% rename from lib/Navigator.js rename to lib/NavigatorWatcher.js index e30bffeb..f05de177 100644 --- a/lib/Navigator.js +++ b/lib/NavigatorWatcher.js @@ -14,30 +14,31 @@ * limitations under the License. */ -class Navigator { +const NetworkManager = require('./NetworkManager'); + +class NavigatorWatcher { /** * @param {!Connection} client - * @param {string} url - * @param {string=} referrer + * @param {!NetworkManager} networkManager * @param {!Object=} options */ - constructor(client, url, referrer, options = {}) { + constructor(client, networkManager, options = {}) { this._client = client; - this._url = url; - this._referrer = referrer; + this._networkManager = networkManager; this._maxTime = typeof options['maxTime'] === 'number' ? options['maxTime'] : 30000; this._idleTime = typeof options['networkIdleTimeout'] === 'number' ? options['networkIdleTimeout'] : 1000; this._idleInflight = typeof options['networkIdleInflight'] === 'number' ? options['networkIdleInflight'] : 2; this._waitUntil = typeof options['waitUntil'] === 'string' ? options['waitUntil'] : 'load'; - console.assert(this._waitUntil === 'load' || this._waitUntil === 'networkidle', 'Unknown value for options.waitUntil: ' + this._waitUntil); } + /** - * @return {!Promise} + * @return {!Promise>} */ - async navigate() { + async waitForNavigation() { this._init(); + let certificateError = new Promise(fulfill => this._client.once('Security.certificateError', fulfill)) .then(error => new Error('SSL Certiciate error: ' + error.errorType)); let networkIdle = new Promise(fulfill => this._networkIdleCallback = fulfill).then(() => null); @@ -46,15 +47,19 @@ class Navigator { try { // Await for the command to throw exception in case of illegal arguments. - await this._client.send('Page.navigate', {url: this._url, referrer: this._referrer}); const error = await Promise.race([certificateError, watchdog, this._waitUntil === 'load' ? loadEventFired : networkIdle]); if (error) throw error; + return this._responses; } finally { this._cleanup(); } } + cancel() { + this._cleanup(); + } + /** * @param {!Object} event */ @@ -83,14 +88,18 @@ class Navigator { _init() { this._loadingStartedHandler = this._onLoadingStarted.bind(this); this._loadingCompletedHandler = this._onLoadingCompleted.bind(this); + this._onResponseHandler = this._onResponse.bind(this); this._client.on('Network.requestWillBeSent', this._loadingStartedHandler); this._client.on('Network.loadingFinished', this._loadingCompletedHandler); this._client.on('Network.loadingFailed', this._loadingCompletedHandler); this._client.on('Network.webSocketCreated', this._loadingStartedHandler); this._client.on('Network.webSocketClosed', this._loadingCompletedHandler); + this._networkManager.on(NetworkManager.Events.Response, this._onResponseHandler); this._inflightRequests = 0; this._requestIds = new Set(); + /** @type {!Map} */ + this._responses = new Map(); } _cleanup() { @@ -99,10 +108,19 @@ class Navigator { this._client.removeListener('Network.loadingFailed', this._loadingCompletedHandler); this._client.removeListener('Network.webSocketCreated', this._loadingStartedHandler); this._client.removeListener('Network.webSocketClosed', this._loadingCompletedHandler); + this._networkManager.removeListener(NetworkManager.Events.Response, this._onResponseHandler); clearTimeout(this._idleTimer); clearTimeout(this._maximumTimer); + this._responses = new Map(); + } + + /** + * @param {!Response} response + */ + _onResponse(response) { + this._responses.set(response.url, response); } } -module.exports = Navigator; +module.exports = NavigatorWatcher; diff --git a/lib/Page.js b/lib/Page.js index 76652030..7dc79d94 100644 --- a/lib/Page.js +++ b/lib/Page.js @@ -18,7 +18,7 @@ let fs = require('fs'); let EventEmitter = require('events'); let mime = require('mime'); let NetworkManager = require('./NetworkManager'); -let Navigator = require('./Navigator'); +let NavigatorWatcher = require('./NavigatorWatcher'); let Dialog = require('./Dialog'); let EmulationManager = require('./EmulationManager'); let FrameManager = require('./FrameManager'); @@ -267,32 +267,36 @@ class Page extends EventEmitter { * @return {!Promise} */ async navigate(url, options) { + const watcher = new NavigatorWatcher(this._client, this._networkManager, options); + const result = watcher.waitForNavigation(); const referrer = this._networkManager.httpHeaders()['referer']; - this._navigator = new Navigator(this._client, url, referrer, options); - return this.reload(); + try { + await this._client.send('Page.navigate', {url, referrer}); + } catch (e) { + watcher.cancel(); + throw e; + } + const responses = await result; + return responses.get(this.mainFrame().url()); } /** - * @return {!Promise} + * @param {!Object=} options + * @return {!Promise} */ - async reload() { - if (!this._navigator) - return; - /** @type {!Map} */ - const responses = new Map(); - const onResponse = response => responses.set(response.url, response); - this._networkManager.on(NetworkManager.Events.Response, onResponse); - try { - await this._navigator.navigate(); - } finally { - this._networkManager.removeListener(NetworkManager.Events.Response, onResponse); - } - const response = responses.get(this.mainFrame().url()); - console.assert(response); + async reload(options) { + this._client.send('Page.reload'); + return this.waitForNavigation(options); + } - // Await for a single raf rountrip to ensure basic rasterization is complete. - await this.evaluate(() => new Promise(fulfill => requestAnimationFrame(fulfill))); - return response; + /** + * @param {!Object=} options + * @return {!Promise} + */ + async waitForNavigation(options) { + const watcher = new NavigatorWatcher(this._client, this._networkManager, options); + const responses = await watcher.waitForNavigation(); + return responses.get(this.mainFrame().url()) || null; } /** diff --git a/test/test.js b/test/test.js index 7ce19159..7df8ef52 100644 --- a/test/test.js +++ b/test/test.js @@ -427,6 +427,17 @@ describe('Puppeteer', function() { })); }); + describe('Page.waitForNavigation', function() { + it('should work', SX(async function() { + await page.navigate(EMPTY_PAGE); + const result = page.waitForNavigation(); + page.evaluate(url => window.location.href = url, PREFIX + '/grid.html'); + const response = await result; + expect(response.ok).toBe(true); + expect(response.url).toContain('grid.html'); + })); + }); + describe('Page.setInPageCallback', function() { it('should work', SX(async function() { await page.setInPageCallback('callController', function(a, b) { diff --git a/utils/doclint/lint.js b/utils/doclint/lint.js index 643f3675..284d692c 100644 --- a/utils/doclint/lint.js +++ b/utils/doclint/lint.js @@ -11,7 +11,7 @@ let EXCLUDE_CLASSES = new Set([ 'EmulationManager', 'FrameManager', 'Helper', - 'Navigator', + 'NavigatorWatcher', 'NetworkManager', 'ProxyStream' ]);