Skip to content

Commit

Permalink
feat(api): implement Page.waitForNetworkIdle()
Browse files Browse the repository at this point in the history
which will wait for there to be no network requests in progress during the `idleTime` before resolving.
  • Loading branch information
tjenkinson committed Nov 9, 2019
1 parent fd43f9c commit db1b35b
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 0 deletions.
12 changes: 12 additions & 0 deletions docs/api.md
Expand Up @@ -158,6 +158,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)
Expand Down Expand Up @@ -2089,6 +2090,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]<void>> 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.
- `options` <[Object]> Optional waiting parameters
Expand Down
7 changes: 7 additions & 0 deletions lib/NetworkManager.js
Expand Up @@ -89,6 +89,13 @@ class NetworkManager extends EventEmitter {
return Object.assign({}, this._extraHTTPHeaders);
}

/**
* @return {number}
*/
numRequestsInProgress() {
return this._requestIdToRequest.size;
}

/**
* @param {boolean} value
*/
Expand Down
58 changes: 58 additions & 0 deletions lib/Page.js
Expand Up @@ -737,6 +737,64 @@ class Page extends EventEmitter {
}, timeout, this._sessionClosePromise());
}

/**
* @param {!{timeout?: number, idleTime?: number}=} options
* @return {!Promise<void>}
*/
async waitForNetworkIdle(options = {}) {
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 = [];
eventPromises.push(listenToEvent(Events.NetworkManager.Request));
eventPromises.push(listenToEvent(Events.NetworkManager.RequestFinished));
eventPromises.push(listenToEvent(Events.NetworkManager.RequestFailed));

await Promise.race([idlePromise, ...eventPromises, this._sessionClosePromise()]).then(r => {
cleanup();
return r;
}, e => {
cleanup();
throw e;
});
}

/**
* @param {!{timeout?: number, waitUntil?: string|!Array<string>}=} options
* @return {!Promise<?Puppeteer.Response>}
Expand Down
57 changes: 57 additions & 0 deletions test/page.spec.js
Expand Up @@ -571,6 +571,63 @@ module.exports.addTests = function({testRunner, expect, headless, puppeteer, CHR
});
});

describe('Page.waitForNetworkIdle', function() {
it('should work', async({page, server}) => {
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() => {
// TODO why does this break with local url?
const url = 'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png';
await Promise.all([fetch(url, {mode: 'no-cors'}), fetch(url, {mode: 'no-cors'})]);
await new Promise(resolve => setTimeout(resolve, 200));
await fetch(url, {mode: 'no-cors'});
await new Promise(resolve => setTimeout(resolve, 400));
await fetch(url, {mode: 'no-cors'});
})()).then(() => Date.now())
]);
expect(res).toBe(undefined);
expect(t1).toBeGreaterThan(t2);
expect(t1 - t2).toBeGreaterThanOrEqual(400);
});
it('should respect timeout', async({page, server}) => {
let error = null;
await page.waitForNetworkIdle({timeout: 1}).catch(e => error = e);
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
});
it('should respect idleTime', async({page, server}) => {
await page.goto(server.EMPTY_PAGE);
const [t1, t2] = await Promise.all([
page.waitForNetworkIdle({idleTime: 10}).then(() => Date.now()),
page.evaluate(() => (async() => {
// TODO why does this break with local url?
const url = 'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png';
await Promise.all([fetch(url, {mode: 'no-cors'}), fetch(url, {mode: 'no-cors'})]);
await new Promise(resolve => setTimeout(resolve, 250));
})()).then(() => Date.now())
]);
expect(t2).toBeGreaterThan(t1);
});
it('should work with no timeout', async({page, server}) => {
await page.goto(server.EMPTY_PAGE);
const [result] = await Promise.all([
page.waitForNetworkIdle({timeout: 0}),
page.evaluate(() => setTimeout(() => {
// TODO why does this break with local url?
const url = 'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png';
fetch(url, {mode: 'no-cors'});
fetch(url, {mode: 'no-cors'});
fetch(url, {mode: 'no-cors'});
}, 50))
]);
expect(result).toBe(undefined);
});
});

describe('Page.exposeFunction', function() {
it('should work', async({page, server}) => {
await page.exposeFunction('compute', function(a, b) {
Expand Down

0 comments on commit db1b35b

Please sign in to comment.