chore: add support for waitForNetworkIdle (#10261)

This commit is contained in:
Nikolay Vitkov 2023-05-30 13:07:55 +02:00 committed by GitHub
parent 2741b76d30
commit b03acac30f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 134 additions and 69 deletions

View File

@ -24,7 +24,7 @@ jobs:
with: with:
fetch-depth: 2 fetch-depth: 2
- name: Check if branch is out of date - name: Check if branch is out of date
if: ${{ inputs.check-mergeable-state }} if: ${{ inputs.check-mergeable-state && github.base_ref == 'main' }}
run: | run: |
git fetch origin main --depth 1 && git fetch origin main --depth 1 &&
git merge-base --is-ancestor origin/main @; git merge-base --is-ancestor origin/main @;

View File

@ -26,12 +26,17 @@ import type {Coverage} from '../common/Coverage.js';
import {Device} from '../common/Device.js'; import {Device} from '../common/Device.js';
import {DeviceRequestPrompt} from '../common/DeviceRequestPrompt.js'; import {DeviceRequestPrompt} from '../common/DeviceRequestPrompt.js';
import type {Dialog} from '../common/Dialog.js'; import type {Dialog} from '../common/Dialog.js';
import {TargetCloseError} from '../common/Errors.js';
import {EventEmitter, Handler} from '../common/EventEmitter.js'; import {EventEmitter, Handler} from '../common/EventEmitter.js';
import type {FileChooser} from '../common/FileChooser.js'; import type {FileChooser} from '../common/FileChooser.js';
import type {Keyboard, Mouse, Touchscreen} from '../common/Input.js'; import type {Keyboard, Mouse, Touchscreen} from '../common/Input.js';
import type {WaitForSelectorOptions} from '../common/IsolatedWorld.js'; import type {WaitForSelectorOptions} from '../common/IsolatedWorld.js';
import type {PuppeteerLifeCycleEvent} from '../common/LifecycleWatcher.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 { import {
LowerCasePaperFormat, LowerCasePaperFormat,
paperFormats, paperFormats,
@ -47,9 +52,16 @@ import type {
HandleFor, HandleFor,
NodeFor, NodeFor,
} from '../common/types.js'; } 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 type {WebWorker} from '../common/WebWorker.js';
import {assert} from '../util/assert.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 {Browser} from './Browser.js';
import type {BrowserContext} from './BrowserContext.js'; import type {BrowserContext} from './BrowserContext.js';
@ -1615,6 +1627,72 @@ export class Page extends EventEmitter {
throw new Error('Not implemented'); 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 urlOrPredicate - A URL or predicate to wait for.
* @param options - Optional waiting parameters * @param options - Optional waiting parameters

View File

@ -149,10 +149,14 @@ export class NetworkEventManager {
return this.queuedRedirectInfo(fetchRequestId).shift(); return this.queuedRedirectInfo(fetchRequestId).shift();
} }
numRequestsInProgress(): number { inFlightRequestsCount(): number {
return [...this.#httpRequestsMap].filter(([, request]) => { let inProgressRequestCounter = 0;
return !request.response(); for (const [, request] of this.#httpRequestsMap) {
}).length; if (!request.response()) {
inProgressRequestCounter++;
}
}
return inProgressRequestCounter;
} }
storeRequestWillBeSent( storeRequestWillBeSent(

View File

@ -181,8 +181,8 @@ export class NetworkManager extends EventEmitter {
return Object.assign({}, this.#extraHTTPHeaders); return Object.assign({}, this.#extraHTTPHeaders);
} }
numRequestsInProgress(): number { inFlightRequestsCount(): number {
return this.#networkEventManager.numRequestsInProgress(); return this.#networkEventManager.inFlightRequestsCount();
} }
async setOfflineMode(value: boolean): Promise<void> { async setOfflineMode(value: boolean): Promise<void> {

View File

@ -153,7 +153,7 @@ export class CDPPage extends Page {
#screenshotTaskQueue: TaskQueue; #screenshotTaskQueue: TaskQueue;
#workers = new Map<string, WebWorker>(); #workers = new Map<string, WebWorker>();
#fileChooserDeferreds = new Set<Deferred<FileChooser>>(); #fileChooserDeferreds = new Set<Deferred<FileChooser>>();
#sessionCloseDeferred = createDeferred<Error>(); #sessionCloseDeferred = createDeferred<TargetCloseError>();
#serviceWorkerBypassed = false; #serviceWorkerBypassed = false;
#userDragInterceptionEnabled = false; #userDragInterceptionEnabled = false;
@ -1000,64 +1000,11 @@ export class CDPPage extends Page {
): Promise<void> { ): Promise<void> {
const {idleTime = 500, timeout = this.#timeoutSettings.timeout()} = options; const {idleTime = 500, timeout = this.#timeoutSettings.timeout()} = options;
const networkManager = this.#frameManager.networkManager; await this._waitForNetworkIdle(
this.#frameManager.networkManager,
const idleDeferred = createDeferred<void>(); idleTime,
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, timeout,
abortPromise this.#sessionCloseDeferred
);
};
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;
}
); );
} }

View File

@ -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); const request = this.#requestMap.get(event.request.request);
if (!request) { if (!request) {
return; return;
@ -101,6 +101,17 @@ export class NetworkManager extends EventEmitter {
return this.#navigationMap.get(navigationId ?? '') ?? null; 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 { dispose(): void {
this.removeAllListeners(); this.removeAllListeners();
this.#requestMap.clear(); this.#requestMap.clear();

View File

@ -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> { override title(): Promise<string> {
return this.mainFrame().title(); return this.mainFrame().title();
} }

View File

@ -167,6 +167,12 @@
"parameters": ["webDriverBiDi"], "parameters": ["webDriverBiDi"],
"expectations": ["PASS"] "expectations": ["PASS"]
}, },
{
"testIdPattern": "[page.spec] Page Page.waitForNetworkIdle *",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["PASS"]
},
{ {
"testIdPattern": "[page.spec] Page Page.waitForRequest *", "testIdPattern": "[page.spec] Page Page.waitForRequest *",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
@ -587,6 +593,12 @@
"parameters": ["webDriverBiDi"], "parameters": ["webDriverBiDi"],
"expectations": ["FAIL"] "expectations": ["FAIL"]
}, },
{
"testIdPattern": "[page.spec] Page Page.waitForNetworkIdle should work with aborted requests",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["FAIL"]
},
{ {
"testIdPattern": "[proxy.spec] *", "testIdPattern": "[proxy.spec] *",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],