Skip to content

Commit

Permalink
feat(api): implement Page.waitForNetworkIdle() (#5140)
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 Sep 11, 2021
1 parent b5020dc commit 3c6029c
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 0 deletions.
12 changes: 12 additions & 0 deletions docs/api.md
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
6 changes: 6 additions & 0 deletions src/common/NetworkManager.ts
Expand Up @@ -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();
Expand Down
73 changes: 73 additions & 0 deletions src/common/Page.ts
Expand Up @@ -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
Expand Down
73 changes: 73 additions & 0 deletions test/page.spec.ts
Expand Up @@ -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();
Expand Down

0 comments on commit 3c6029c

Please sign in to comment.