diff --git a/.github/workflows/changed-packages.yml b/.github/workflows/changed-packages.yml index cb1fb823..5d4d8d05 100644 --- a/.github/workflows/changed-packages.yml +++ b/.github/workflows/changed-packages.yml @@ -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 @; diff --git a/packages/puppeteer-core/src/api/Page.ts b/packages/puppeteer-core/src/api/Page.ts index 97946ca4..a26d6d19 100644 --- a/packages/puppeteer-core/src/api/Page.ts +++ b/packages/puppeteer-core/src/api/Page.ts @@ -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, @@ -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'; @@ -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 + ): Promise { + const idleDeferred = createDeferred(); + const abortDeferred = createDeferred(); + + 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 diff --git a/packages/puppeteer-core/src/common/NetworkEventManager.ts b/packages/puppeteer-core/src/common/NetworkEventManager.ts index da550a00..2f2d6d6b 100644 --- a/packages/puppeteer-core/src/common/NetworkEventManager.ts +++ b/packages/puppeteer-core/src/common/NetworkEventManager.ts @@ -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( diff --git a/packages/puppeteer-core/src/common/NetworkManager.ts b/packages/puppeteer-core/src/common/NetworkManager.ts index df0d6b8d..cad0ce2a 100644 --- a/packages/puppeteer-core/src/common/NetworkManager.ts +++ b/packages/puppeteer-core/src/common/NetworkManager.ts @@ -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 { diff --git a/packages/puppeteer-core/src/common/Page.ts b/packages/puppeteer-core/src/common/Page.ts index 82eb37b7..80ca119c 100644 --- a/packages/puppeteer-core/src/common/Page.ts +++ b/packages/puppeteer-core/src/common/Page.ts @@ -153,7 +153,7 @@ export class CDPPage extends Page { #screenshotTaskQueue: TaskQueue; #workers = new Map(); #fileChooserDeferreds = new Set>(); - #sessionCloseDeferred = createDeferred(); + #sessionCloseDeferred = createDeferred(); #serviceWorkerBypassed = false; #userDragInterceptionEnabled = false; @@ -1000,64 +1000,11 @@ export class CDPPage extends Page { ): Promise { const {idleTime = 500, timeout = this.#timeoutSettings.timeout()} = options; - const networkManager = this.#frameManager.networkManager; - - const idleDeferred = createDeferred(); - - let abortRejectCallback: (error: Error) => void; - const abortPromise = new Promise((_, 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 ); } diff --git a/packages/puppeteer-core/src/common/bidi/NetworkManager.ts b/packages/puppeteer-core/src/common/bidi/NetworkManager.ts index 8f0c2dcd..9a7895ae 100644 --- a/packages/puppeteer-core/src/common/bidi/NetworkManager.ts +++ b/packages/puppeteer-core/src/common/bidi/NetworkManager.ts @@ -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; @@ -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(); diff --git a/packages/puppeteer-core/src/common/bidi/Page.ts b/packages/puppeteer-core/src/common/bidi/Page.ts index f1b849cf..99b4c70b 100644 --- a/packages/puppeteer-core/src/common/bidi/Page.ts +++ b/packages/puppeteer-core/src/common/bidi/Page.ts @@ -497,6 +497,19 @@ export class Page extends PageBase { ); } + override async waitForNetworkIdle( + options: {idleTime?: number; timeout?: number} = {} + ): Promise { + const {idleTime = 500, timeout = this.#timeoutSettings.timeout()} = options; + + await this._waitForNetworkIdle( + this.#networkManager, + idleTime, + timeout, + this.#closedDeferred + ); + } + override title(): Promise { return this.mainFrame().title(); } diff --git a/test/TestExpectations.json b/test/TestExpectations.json index 817aafeb..a1ce497c 100644 --- a/test/TestExpectations.json +++ b/test/TestExpectations.json @@ -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"], @@ -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"],