diff --git a/docs/api.md b/docs/api.md index ab1c2c6ec0d1e..6d73a1c2457be 100644 --- a/docs/api.md +++ b/docs/api.md @@ -197,6 +197,7 @@ * [page.waitForFileChooser([options])](#pagewaitforfilechooseroptions) * [page.waitForFunction(pageFunction[, options[, ...args]])](#pagewaitforfunctionpagefunction-options-args) * [page.waitForNavigation([options])](#pagewaitfornavigationoptions) + * [page.waitForNetworkIdle([options])](#pagewaitfornetworkidleoptions) * [page.waitForRequest(urlOrPredicate[, options])](#pagewaitforrequesturlorpredicate-options) * [page.waitForResponse(urlOrPredicate[, options])](#pagewaitforresponseurlorpredicate-options) * [page.waitForSelector(selector[, options])](#pagewaitforselectorselector-options) @@ -2846,6 +2847,17 @@ const [response] = await Promise.all([ Shortcut for [page.mainFrame().waitForNavigation(options)](#framewaitfornavigationoptions). +#### page.waitForNetworkIdle([options]) +- `options` <[Object]> Optional waiting parameters + - `timeout` <[number]> Maximum wait time in milliseconds, defaults to 30 seconds, pass `0` to disable the timeout. The default value can be changed by using the [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) method. + - `idleTime` <[number]> How long to wait for no network requests in milliseconds, defaults to 500 milliseconds. +- returns: <[Promise]> Promise which resolves when network is idle. + +```js +page.evaluate(() => fetch('some-url')); +page.waitForNetworkIdle(); // The promise resolves after fetch above finishes +``` + #### page.waitForRequest(urlOrPredicate[, options]) - `urlOrPredicate` <[string]|[Function]> A URL or predicate to wait for. diff --git a/src/common/NetworkManager.ts b/src/common/NetworkManager.ts index dda138e693fd9..73ae84a9d512e 100644 --- a/src/common/NetworkManager.ts +++ b/src/common/NetworkManager.ts @@ -188,6 +188,12 @@ export class NetworkManager extends EventEmitter { return Object.assign({}, this._extraHTTPHeaders); } + numRequestsInProgress(): number { + return [...this._requestIdToRequest].filter(([, request]) => { + return !request.response(); + }).length; + } + async setOfflineMode(value: boolean): Promise { this._emulatedNetworkConditions.offline = value; await this._updateNetworkConditions(); diff --git a/src/common/Page.ts b/src/common/Page.ts index c5caaaf57bacf..b2aa354d9eb30 100644 --- a/src/common/Page.ts +++ b/src/common/Page.ts @@ -1894,6 +1894,79 @@ export class Page extends EventEmitter { ); } + /** + * @param options - Optional waiting parameters + * @returns Promise which resolves when network is idle + */ + async waitForNetworkIdle( + options: { idleTime?: number; timeout?: number } = {} + ): Promise { + const { idleTime = 500, timeout = this._timeoutSettings.timeout() } = + options; + + const networkManager = this._frameManager.networkManager(); + + let idleResolveCallback; + const idlePromise = new Promise((resolve) => { + idleResolveCallback = resolve; + }); + + let abortRejectCallback; + const abortPromise = new Promise((_, reject) => { + abortRejectCallback = reject; + }); + + let idleTimer; + const onIdle = () => idleResolveCallback(); + + const cleanup = () => { + idleTimer && clearTimeout(idleTimer); + abortRejectCallback(new Error('abort')); + }; + + const evaluate = () => { + idleTimer && clearTimeout(idleTimer); + if (networkManager.numRequestsInProgress() === 0) + idleTimer = setTimeout(onIdle, idleTime); + }; + + evaluate(); + + const eventHandler = () => { + evaluate(); + return false; + }; + + const listenToEvent = (event) => + helper.waitForEvent( + networkManager, + event, + eventHandler, + timeout, + abortPromise + ); + + const eventPromises = [ + listenToEvent(NetworkManagerEmittedEvents.Request), + listenToEvent(NetworkManagerEmittedEvents.Response), + ]; + + await Promise.race([ + idlePromise, + ...eventPromises, + this._sessionClosePromise(), + ]).then( + (r) => { + cleanup(); + return r; + }, + (error) => { + cleanup(); + throw error; + } + ); + } + /** * This method navigate to the previous page in history. * @param options - Navigation parameters diff --git a/test/page.spec.ts b/test/page.spec.ts index f81851c5fad4b..5c9ef3a09af1d 100644 --- a/test/page.spec.ts +++ b/test/page.spec.ts @@ -825,6 +825,79 @@ describe('Page', function () { }); }); + describe('Page.waitForNetworkIdle', function () { + it('should work', async () => { + const { page, server } = getTestState(); + await page.goto(server.EMPTY_PAGE); + let res; + const [t1, t2] = await Promise.all([ + page.waitForNetworkIdle().then((r) => { + res = r; + return Date.now(); + }), + page + .evaluate(() => + (async () => { + await Promise.all([ + fetch('/digits/1.png'), + fetch('/digits/2.png'), + ]); + await new Promise((resolve) => setTimeout(resolve, 200)); + await fetch('/digits/3.png'); + await new Promise((resolve) => setTimeout(resolve, 400)); + await fetch('/digits/4.png'); + })() + ) + .then(() => Date.now()), + ]); + expect(res).toBe(undefined); + expect(t1).toBeGreaterThan(t2); + expect(t1 - t2).toBeGreaterThanOrEqual(400); + }); + it('should respect timeout', async () => { + const { page, puppeteer } = getTestState(); + let error = null; + await page + .waitForNetworkIdle({ timeout: 1 }) + .catch((error_) => (error = error_)); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should respect idleTime', async () => { + const { page, server } = getTestState(); + await page.goto(server.EMPTY_PAGE); + const [t1, t2] = await Promise.all([ + page.waitForNetworkIdle({ idleTime: 10 }).then(() => Date.now()), + page + .evaluate(() => + (async () => { + await Promise.all([ + fetch('/digits/1.png'), + fetch('/digits/2.png'), + ]); + await new Promise((resolve) => setTimeout(resolve, 250)); + })() + ) + .then(() => Date.now()), + ]); + expect(t2).toBeGreaterThan(t1); + }); + it('should work with no timeout', async () => { + const { page, server } = getTestState(); + await page.goto(server.EMPTY_PAGE); + const [result] = await Promise.all([ + page.waitForNetworkIdle({ timeout: 0 }), + page.evaluate(() => + setTimeout(() => { + fetch('/digits/1.png'); + fetch('/digits/2.png'); + fetch('/digits/3.png'); + }, 50) + ), + ]); + expect(result).toBe(undefined); + }); + }); + describeFailsFirefox('Page.exposeFunction', function () { it('should work', async () => { const { page } = getTestState();