Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 9 commits into from Sep 11, 2021
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'));
tjenkinson marked this conversation as resolved.
Show resolved Hide resolved
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