feat(api): implement Page.waitForNetworkIdle() (#5140)

which will wait for there to be no network requests in progress during the `idleTime` before resolving.
This commit is contained in:
Tom Jenkinson 2021-09-11 21:28:12 +01:00 committed by GitHub
parent b5020dc041
commit 3c6029c702
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 164 additions and 0 deletions

View File

@ -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]<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.

View File

@ -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<void> {
this._emulatedNetworkConditions.offline = value;
await this._updateNetworkConditions();

View File

@ -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<void> {
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<Error>((_, 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

View File

@ -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();