Skip to content

Commit

Permalink
chore: add support for waitForNetworkIdle (#10261)
Browse files Browse the repository at this point in the history
  • Loading branch information
Lightning00Blade committed May 30, 2023
1 parent 2741b76 commit b03acac
Show file tree
Hide file tree
Showing 8 changed files with 134 additions and 69 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/changed-packages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
with:
fetch-depth: 2
- name: Check if branch is out of date
if: ${{ inputs.check-mergeable-state }}
if: ${{ inputs.check-mergeable-state && github.base_ref == 'main' }}
run: |
git fetch origin main --depth 1 &&
git merge-base --is-ancestor origin/main @;
Expand Down
82 changes: 80 additions & 2 deletions packages/puppeteer-core/src/api/Page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,17 @@ import type {Coverage} from '../common/Coverage.js';
import {Device} from '../common/Device.js';
import {DeviceRequestPrompt} from '../common/DeviceRequestPrompt.js';
import type {Dialog} from '../common/Dialog.js';
import {TargetCloseError} from '../common/Errors.js';
import {EventEmitter, Handler} from '../common/EventEmitter.js';
import type {FileChooser} from '../common/FileChooser.js';
import type {Keyboard, Mouse, Touchscreen} from '../common/Input.js';
import type {WaitForSelectorOptions} from '../common/IsolatedWorld.js';
import type {PuppeteerLifeCycleEvent} from '../common/LifecycleWatcher.js';
import type {Credentials, NetworkConditions} from '../common/NetworkManager.js';
import {
Credentials,
NetworkConditions,
NetworkManagerEmittedEvents,
} from '../common/NetworkManager.js';
import {
LowerCasePaperFormat,
paperFormats,
Expand All @@ -47,9 +52,16 @@ import type {
HandleFor,
NodeFor,
} from '../common/types.js';
import {importFSPromises, isNumber, isString} from '../common/util.js';
import {
importFSPromises,
isNumber,
isString,
waitForEvent,
} from '../common/util.js';
import type {WebWorker} from '../common/WebWorker.js';
import {assert} from '../util/assert.js';
import {Deferred} from '../util/Deferred.js';
import {createDeferred} from '../util/Deferred.js';

import type {Browser} from './Browser.js';
import type {BrowserContext} from './BrowserContext.js';
Expand Down Expand Up @@ -1615,6 +1627,72 @@ export class Page extends EventEmitter {
throw new Error('Not implemented');
}

/**
* @internal
*/
protected async _waitForNetworkIdle(
networkManager: EventEmitter & {
inFlightRequestsCount: () => number;
},
idleTime: number,
timeout: number,
closedDeferred: Deferred<TargetCloseError>
): Promise<void> {
const idleDeferred = createDeferred<void>();
const abortDeferred = createDeferred<Error>();

let idleTimer: NodeJS.Timeout;
const cleanup = () => {
idleTimer && clearTimeout(idleTimer);
abortDeferred.reject(new Error('abort'));
};

const evaluate = () => {
idleTimer && clearTimeout(idleTimer);
if (networkManager.inFlightRequestsCount() === 0) {
idleTimer = setTimeout(idleDeferred.resolve, idleTime);
}
};

evaluate();

const eventHandler = () => {
evaluate();
return false;
};

const listenToEvent = (event: symbol) => {
return waitForEvent(
networkManager,
event,
eventHandler,
timeout,
abortDeferred.valueOrThrow()
);
};

const eventPromises = [
listenToEvent(NetworkManagerEmittedEvents.Request),
listenToEvent(NetworkManagerEmittedEvents.Response),
listenToEvent(NetworkManagerEmittedEvents.RequestFailed),
];

await Promise.race([
idleDeferred.valueOrThrow(),
...eventPromises,
closedDeferred.valueOrThrow(),
]).then(
r => {
cleanup();
return r;
},
error => {
cleanup();
throw error;
}
);
}

/**
* @param urlOrPredicate - A URL or predicate to wait for.
* @param options - Optional waiting parameters
Expand Down
12 changes: 8 additions & 4 deletions packages/puppeteer-core/src/common/NetworkEventManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,14 @@ export class NetworkEventManager {
return this.queuedRedirectInfo(fetchRequestId).shift();
}

numRequestsInProgress(): number {
return [...this.#httpRequestsMap].filter(([, request]) => {
return !request.response();
}).length;
inFlightRequestsCount(): number {
let inProgressRequestCounter = 0;
for (const [, request] of this.#httpRequestsMap) {
if (!request.response()) {
inProgressRequestCounter++;
}
}
return inProgressRequestCounter;
}

storeRequestWillBeSent(
Expand Down
4 changes: 2 additions & 2 deletions packages/puppeteer-core/src/common/NetworkManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,8 @@ export class NetworkManager extends EventEmitter {
return Object.assign({}, this.#extraHTTPHeaders);
}

numRequestsInProgress(): number {
return this.#networkEventManager.numRequestsInProgress();
inFlightRequestsCount(): number {
return this.#networkEventManager.inFlightRequestsCount();
}

async setOfflineMode(value: boolean): Promise<void> {
Expand Down
65 changes: 6 additions & 59 deletions packages/puppeteer-core/src/common/Page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export class CDPPage extends Page {
#screenshotTaskQueue: TaskQueue;
#workers = new Map<string, WebWorker>();
#fileChooserDeferreds = new Set<Deferred<FileChooser>>();
#sessionCloseDeferred = createDeferred<Error>();
#sessionCloseDeferred = createDeferred<TargetCloseError>();
#serviceWorkerBypassed = false;
#userDragInterceptionEnabled = false;

Expand Down Expand Up @@ -1000,64 +1000,11 @@ export class CDPPage extends Page {
): Promise<void> {
const {idleTime = 500, timeout = this.#timeoutSettings.timeout()} = options;

const networkManager = this.#frameManager.networkManager;

const idleDeferred = createDeferred<void>();

let abortRejectCallback: (error: Error) => void;
const abortPromise = new Promise<Error>((_, reject) => {
abortRejectCallback = reject;
});

let idleTimer: NodeJS.Timeout;
const cleanup = () => {
idleTimer && clearTimeout(idleTimer);
abortRejectCallback(new Error('abort'));
};

const evaluate = () => {
idleTimer && clearTimeout(idleTimer);
if (networkManager.numRequestsInProgress() === 0) {
idleTimer = setTimeout(idleDeferred.resolve, idleTime);
}
};

evaluate();

const eventHandler = () => {
evaluate();
return false;
};

const listenToEvent = (event: symbol) => {
return waitForEvent(
networkManager,
event,
eventHandler,
timeout,
abortPromise
);
};

const eventPromises = [
listenToEvent(NetworkManagerEmittedEvents.Request),
listenToEvent(NetworkManagerEmittedEvents.Response),
listenToEvent(NetworkManagerEmittedEvents.RequestFailed),
];

await Promise.race([
idleDeferred.valueOrThrow(),
...eventPromises,
this.#sessionCloseDeferred.valueOrThrow(),
]).then(
r => {
cleanup();
return r;
},
error => {
cleanup();
throw error;
}
await this._waitForNetworkIdle(
this.#frameManager.networkManager,
idleTime,
timeout,
this.#sessionCloseDeferred
);
}

Expand Down
13 changes: 12 additions & 1 deletion packages/puppeteer-core/src/common/bidi/NetworkManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export class NetworkManager extends EventEmitter {
}
}

#onFetchError(event: any) {
#onFetchError(event: Bidi.Network.FetchErrorParams) {
const request = this.#requestMap.get(event.request.request);
if (!request) {
return;
Expand All @@ -101,6 +101,17 @@ export class NetworkManager extends EventEmitter {
return this.#navigationMap.get(navigationId ?? '') ?? null;
}

inFlightRequestsCount(): number {
let inFlightRequestCounter = 0;
for (const [, request] of this.#requestMap) {
if (!request.response() || request._failureText) {
inFlightRequestCounter++;
}
}

return inFlightRequestCounter;
}

dispose(): void {
this.removeAllListeners();
this.#requestMap.clear();
Expand Down
13 changes: 13 additions & 0 deletions packages/puppeteer-core/src/common/bidi/Page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,19 @@ export class Page extends PageBase {
);
}

override async waitForNetworkIdle(
options: {idleTime?: number; timeout?: number} = {}
): Promise<void> {
const {idleTime = 500, timeout = this.#timeoutSettings.timeout()} = options;

await this._waitForNetworkIdle(
this.#networkManager,
idleTime,
timeout,
this.#closedDeferred
);
}

override title(): Promise<string> {
return this.mainFrame().title();
}
Expand Down
12 changes: 12 additions & 0 deletions test/TestExpectations.json
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,12 @@
"parameters": ["webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[page.spec] Page Page.waitForNetworkIdle *",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[page.spec] Page Page.waitForRequest *",
"platforms": ["darwin", "linux", "win32"],
Expand Down Expand Up @@ -587,6 +593,12 @@
"parameters": ["webDriverBiDi"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[page.spec] Page Page.waitForNetworkIdle should work with aborted requests",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[proxy.spec] *",
"platforms": ["darwin", "linux", "win32"],
Expand Down

0 comments on commit b03acac

Please sign in to comment.