chore: use internal method for networkidle with BiDi (#11167)

This commit is contained in:
Nikolay Vitkov 2023-10-24 10:07:35 +02:00 committed by GitHub
parent f0af79a1d6
commit 5278de9276
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 305 additions and 259 deletions

View File

@ -84,7 +84,6 @@ import type {Viewport} from '../common/Viewport.js';
import type {ScreenRecorder} from '../node/ScreenRecorder.js'; import type {ScreenRecorder} from '../node/ScreenRecorder.js';
import {assert} from '../util/assert.js'; import {assert} from '../util/assert.js';
import {guarded} from '../util/decorators.js'; import {guarded} from '../util/decorators.js';
import type {Deferred} from '../util/Deferred.js';
import { import {
AsyncDisposableStack, AsyncDisposableStack,
asyncDisposeSymbol, asyncDisposeSymbol,
@ -1741,36 +1740,32 @@ export abstract class Page extends EventEmitter<PageEvents> {
/** /**
* @internal * @internal
*/ */
protected async _waitForNetworkIdle( _waitForNetworkIdle(
networkManager: BidiNetworkManager | CdpNetworkManager, networkManager: BidiNetworkManager | CdpNetworkManager,
idleTime: number, idleTime: number,
ms: number, requestsInFlight = 0
closedDeferred: Deferred<TargetCloseError> ): Observable<void> {
): Promise<void> { return merge(
await firstValueFrom( fromEvent(
merge( networkManager,
fromEvent( NetworkManagerEvent.Request as unknown as string
networkManager, ) as Observable<void>,
NetworkManagerEvent.Request as unknown as string fromEvent(
), networkManager,
fromEvent( NetworkManagerEvent.Response as unknown as string
networkManager, ) as Observable<void>,
NetworkManagerEvent.Response as unknown as string fromEvent(
), networkManager,
fromEvent( NetworkManagerEvent.RequestFailed as unknown as string
networkManager, ) as Observable<void>
NetworkManagerEvent.RequestFailed as unknown as string ).pipe(
) startWith(undefined),
).pipe( filter(() => {
startWith(null), return networkManager.inFlightRequestsCount() <= requestsInFlight;
filter(() => { }),
return networkManager.inFlightRequestsCount() === 0; switchMap(v => {
}), return of(v).pipe(delay(idleTime));
switchMap(v => { })
return of(v).pipe(delay(idleTime));
}),
raceWith(timeout(ms), from(closedDeferred.valueOrThrow()))
)
); );
} }

View File

@ -3,27 +3,14 @@ import type ProtocolMapping from 'devtools-protocol/types/protocol-mapping.js';
import {CDPSession} from '../api/CDPSession.js'; import {CDPSession} from '../api/CDPSession.js';
import type {Connection as CdpConnection} from '../cdp/Connection.js'; import type {Connection as CdpConnection} from '../cdp/Connection.js';
import type {PuppeteerLifeCycleEvent} from '../cdp/LifecycleWatcher.js';
import {TargetCloseError} from '../common/Errors.js'; import {TargetCloseError} from '../common/Errors.js';
import type {EventType} from '../common/EventEmitter.js'; import type {EventType} from '../common/EventEmitter.js';
import {debugError} from '../common/util.js'; import {debugError} from '../common/util.js';
import {assert} from '../util/assert.js';
import {Deferred} from '../util/Deferred.js'; import {Deferred} from '../util/Deferred.js';
import type {BidiConnection} from './Connection.js'; import type {BidiConnection} from './Connection.js';
import {BidiRealm} from './Realm.js'; import {BidiRealm} from './Realm.js';
/**
* @internal
*/
export const lifeCycleToSubscribedEvent = new Map<
PuppeteerLifeCycleEvent,
string
>([
['load', 'browsingContext.load'],
['domcontentloaded', 'browsingContext.domContentLoaded'],
]);
/** /**
* @internal * @internal
*/ */
@ -198,30 +185,3 @@ export class BrowsingContext extends BidiRealm {
void this.#cdpSession.detach().catch(debugError); void this.#cdpSession.detach().catch(debugError);
} }
} }
/**
* @internal
*/
export function getWaitUntilSingle(
event: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]
): Extract<PuppeteerLifeCycleEvent, 'load' | 'domcontentloaded'> {
if (Array.isArray(event) && event.length > 1) {
throw new Error('BiDi support only single `waitUntil` argument');
}
const waitUntilSingle = Array.isArray(event)
? (event.find(lifecycle => {
return lifecycle === 'domcontentloaded' || lifecycle === 'load';
}) as PuppeteerLifeCycleEvent)
: event;
if (
waitUntilSingle === 'networkidle0' ||
waitUntilSingle === 'networkidle2'
) {
throw new Error(`BiDi does not support 'waitUntil' ${waitUntilSingle}`);
}
assert(waitUntilSingle, `Invalid waitUntil option ${waitUntilSingle}`);
return waitUntilSingle;
}

View File

@ -16,16 +16,17 @@
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import type {ObservableInput} from '../../third_party/rxjs/rxjs.js';
import { import {
type Observable, type Observable,
firstValueFrom,
from, from,
fromEvent, fromEvent,
merge, merge,
raceWith, map,
switchMap,
forkJoin, forkJoin,
first, first,
firstValueFrom,
raceWith,
} from '../../third_party/rxjs/rxjs.js'; } from '../../third_party/rxjs/rxjs.js';
import type {CDPSession} from '../api/CDPSession.js'; import type {CDPSession} from '../api/CDPSession.js';
import { import {
@ -34,26 +35,26 @@ import {
type WaitForOptions, type WaitForOptions,
throwIfDetached, throwIfDetached,
} from '../api/Frame.js'; } from '../api/Frame.js';
import type {PuppeteerLifeCycleEvent} from '../cdp/LifecycleWatcher.js';
import {ProtocolError, TimeoutError} from '../common/Errors.js';
import type {TimeoutSettings} from '../common/TimeoutSettings.js'; import type {TimeoutSettings} from '../common/TimeoutSettings.js';
import type {Awaitable} from '../common/types.js'; import type {Awaitable} from '../common/types.js';
import { import {
NETWORK_IDLE_TIME,
UTILITY_WORLD_NAME, UTILITY_WORLD_NAME,
setPageContent, setPageContent,
waitWithTimeout, timeout,
} from '../common/util.js'; } from '../common/util.js';
import {timeout} from '../common/util.js';
import {Deferred} from '../util/Deferred.js'; import {Deferred} from '../util/Deferred.js';
import {disposeSymbol} from '../util/disposable.js'; import {disposeSymbol} from '../util/disposable.js';
import { import type {BrowsingContext} from './BrowsingContext.js';
getWaitUntilSingle,
lifeCycleToSubscribedEvent,
type BrowsingContext,
} from './BrowsingContext.js';
import {ExposeableFunction} from './ExposedFunction.js'; import {ExposeableFunction} from './ExposedFunction.js';
import type {BidiHTTPResponse} from './HTTPResponse.js'; import type {BidiHTTPResponse} from './HTTPResponse.js';
import type {BiDiNetworkIdle} from './lifecycle.js';
import {
getBiDiLifecycleEvent,
getBiDiReadinessState,
rewriteNavigationError,
} from './lifecycle.js';
import type {BidiPage} from './Page.js'; import type {BidiPage} from './Page.js';
import { import {
MAIN_SANDBOX, MAIN_SANDBOX,
@ -62,17 +63,6 @@ import {
type SandboxChart, type SandboxChart,
} from './Sandbox.js'; } from './Sandbox.js';
/**
* @internal
*/
export const lifeCycleToReadinessState = new Map<
PuppeteerLifeCycleEvent,
Bidi.BrowsingContext.ReadinessState
>([
['load', Bidi.BrowsingContext.ReadinessState.Complete],
['domcontentloaded', Bidi.BrowsingContext.ReadinessState.Interactive],
]);
/** /**
* Puppeteer's Frame class could be viewed as a BiDi BrowsingContext implementation * Puppeteer's Frame class could be viewed as a BiDi BrowsingContext implementation
* @internal * @internal
@ -145,33 +135,25 @@ export class BidiFrame extends Frame {
): Promise<BidiHTTPResponse | null> { ): Promise<BidiHTTPResponse | null> {
const { const {
waitUntil = 'load', waitUntil = 'load',
timeout = this.#timeoutSettings.navigationTimeout(), timeout: ms = this.#timeoutSettings.navigationTimeout(),
} = options; } = options;
const readinessState = lifeCycleToReadinessState.get( const [readiness, networkIdle] = getBiDiReadinessState(waitUntil);
getWaitUntilSingle(waitUntil)
) as Bidi.BrowsingContext.ReadinessState;
try { const response = await firstValueFrom(
const {result} = await waitWithTimeout( this._waitWithNetworkIdle(
this.#context.connection.send('browsingContext.navigate', { this.#context.connection.send('browsingContext.navigate', {
url: url, context: this.#context.id,
context: this._id, url,
wait: readinessState, wait: readiness,
}), }),
'Navigation', networkIdle
timeout )
); .pipe(raceWith(timeout(ms), from(this.#abortDeferred.valueOrThrow())))
.pipe(rewriteNavigationError(url, ms))
);
return this.#page.getNavigationResponse(result.navigation); return this.#page.getNavigationResponse(response?.result.navigation);
} catch (error) {
if (error instanceof ProtocolError) {
error.message += ` at ${url}`;
} else if (error instanceof TimeoutError) {
error.message = 'Navigation timeout of ' + timeout + ' ms exceeded';
}
throw error;
}
} }
@throwIfDetached @throwIfDetached
@ -184,15 +166,22 @@ export class BidiFrame extends Frame {
timeout: ms = this.#timeoutSettings.navigationTimeout(), timeout: ms = this.#timeoutSettings.navigationTimeout(),
} = options; } = options;
const waitUntilEvent = lifeCycleToSubscribedEvent.get( const [waitEvent, networkIdle] = getBiDiLifecycleEvent(waitUntil);
getWaitUntilSingle(waitUntil)
) as string;
await firstValueFrom( await firstValueFrom(
forkJoin([ this._waitWithNetworkIdle(
fromEvent(this.#context, waitUntilEvent).pipe(first()), forkJoin([
from(setPageContent(this, html)), fromEvent(this.#context, waitEvent).pipe(first()),
]).pipe(raceWith(timeout(ms))) from(setPageContent(this, html)),
]).pipe(
map(() => {
return null;
})
),
networkIdle
)
.pipe(raceWith(timeout(ms), from(this.#abortDeferred.valueOrThrow())))
.pipe(rewriteNavigationError('setContent', ms))
); );
} }
@ -209,31 +198,38 @@ export class BidiFrame extends Frame {
timeout: ms = this.#timeoutSettings.navigationTimeout(), timeout: ms = this.#timeoutSettings.navigationTimeout(),
} = options; } = options;
const waitUntilEvent = lifeCycleToSubscribedEvent.get( const [waitUntilEvent, networkIdle] = getBiDiLifecycleEvent(waitUntil);
getWaitUntilSingle(waitUntil)
) as string;
const info = await firstValueFrom( const navigatedObservable = merge(
merge( forkJoin([
fromEvent( fromEvent(
this.#context, this.#context,
Bidi.ChromiumBidi.BrowsingContext.EventNames.NavigationStarted Bidi.ChromiumBidi.BrowsingContext.EventNames.NavigationStarted
).pipe( ).pipe(first()),
switchMap(() => { fromEvent(this.#context, waitUntilEvent).pipe(
return fromEvent( first()
this.#context, ) as Observable<Bidi.BrowsingContext.NavigationInfo>,
waitUntilEvent ]),
) as Observable<Bidi.BrowsingContext.NavigationInfo>; fromEvent(
}) this.#context,
), Bidi.ChromiumBidi.BrowsingContext.EventNames.FragmentNavigated
fromEvent( ) as Observable<Bidi.BrowsingContext.NavigationInfo>
this.#context, ).pipe(
Bidi.ChromiumBidi.BrowsingContext.EventNames.FragmentNavigated map(result => {
) as Observable<Bidi.BrowsingContext.NavigationInfo> if (Array.isArray(result)) {
).pipe(raceWith(timeout(ms), from(this.#abortDeferred.valueOrThrow()))) return {result: result[1]};
}
return {result};
})
); );
return this.#page.getNavigationResponse(info.navigation); const response = await firstValueFrom(
this._waitWithNetworkIdle(navigatedObservable, networkIdle).pipe(
raceWith(timeout(ms), from(this.#abortDeferred.valueOrThrow()))
)
);
return this.#page.getNavigationResponse(response?.result.navigation);
} }
override get detached(): boolean { override get detached(): boolean {
@ -270,4 +266,31 @@ export class BidiFrame extends Frame {
throw error; throw error;
} }
} }
/** @internal */
_waitWithNetworkIdle(
observableInput: ObservableInput<{
result: Bidi.BrowsingContext.NavigateResult;
} | null>,
networkIdle: BiDiNetworkIdle
): Observable<{
result: Bidi.BrowsingContext.NavigateResult;
} | null> {
const delay = networkIdle
? this.#page._waitForNetworkIdle(
this.#page._networkManager,
NETWORK_IDLE_TIME,
networkIdle === 'networkidle0' ? 0 : 2
)
: from(Promise.resolve());
return forkJoin([
from(observableInput).pipe(first()),
delay.pipe(first()),
]).pipe(
map(([response]) => {
return response;
})
);
}
} }

View File

@ -122,7 +122,7 @@ export class BidiNetworkManager extends EventEmitter<NetworkManagerEvents> {
this.#requestMap.delete(event.request.request); this.#requestMap.delete(event.request.request);
} }
getNavigationResponse(navigationId: string | null): BidiHTTPResponse | null { getNavigationResponse(navigationId?: string | null): BidiHTTPResponse | null {
if (!navigationId) { if (!navigationId) {
return null; return null;
} }

View File

@ -39,11 +39,7 @@ import {
ConsoleMessage, ConsoleMessage,
type ConsoleMessageLocation, type ConsoleMessageLocation,
} from '../common/ConsoleMessage.js'; } from '../common/ConsoleMessage.js';
import { import {TargetCloseError} from '../common/Errors.js';
ProtocolError,
TargetCloseError,
TimeoutError,
} from '../common/Errors.js';
import type {Handler} from '../common/EventEmitter.js'; import type {Handler} from '../common/EventEmitter.js';
import {NetworkManagerEvent} from '../common/NetworkManagerEvents.js'; import {NetworkManagerEvent} from '../common/NetworkManagerEvents.js';
import type {PDFOptions} from '../common/PDFOptions.js'; import type {PDFOptions} from '../common/PDFOptions.js';
@ -51,10 +47,10 @@ import type {Awaitable} from '../common/types.js';
import { import {
debugError, debugError,
evaluationString, evaluationString,
NETWORK_IDLE_TIME,
timeout, timeout,
validateDialogType, validateDialogType,
waitForHTTP, waitForHTTP,
waitWithTimeout,
} from '../common/util.js'; } from '../common/util.js';
import type {Viewport} from '../common/Viewport.js'; import type {Viewport} from '../common/Viewport.js';
import {assert} from '../util/assert.js'; import {assert} from '../util/assert.js';
@ -66,7 +62,6 @@ import type {BidiBrowserContext} from './BrowserContext.js';
import { import {
BrowsingContextEvent, BrowsingContextEvent,
CdpSessionWrapper, CdpSessionWrapper,
getWaitUntilSingle,
type BrowsingContext, type BrowsingContext,
} from './BrowsingContext.js'; } from './BrowsingContext.js';
import type {BidiConnection} from './Connection.js'; import type {BidiConnection} from './Connection.js';
@ -74,11 +69,12 @@ import {BidiDeserializer} from './Deserializer.js';
import {BidiDialog} from './Dialog.js'; import {BidiDialog} from './Dialog.js';
import {BidiElementHandle} from './ElementHandle.js'; import {BidiElementHandle} from './ElementHandle.js';
import {EmulationManager} from './EmulationManager.js'; import {EmulationManager} from './EmulationManager.js';
import {BidiFrame, lifeCycleToReadinessState} from './Frame.js'; import {BidiFrame} from './Frame.js';
import type {BidiHTTPRequest} from './HTTPRequest.js'; import type {BidiHTTPRequest} from './HTTPRequest.js';
import type {BidiHTTPResponse} from './HTTPResponse.js'; import type {BidiHTTPResponse} from './HTTPResponse.js';
import {BidiKeyboard, BidiMouse, BidiTouchscreen} from './Input.js'; import {BidiKeyboard, BidiMouse, BidiTouchscreen} from './Input.js';
import type {BidiJSHandle} from './JSHandle.js'; import type {BidiJSHandle} from './JSHandle.js';
import {getBiDiReadinessState, rewriteNavigationError} from './lifecycle.js';
import {BidiNetworkManager} from './NetworkManager.js'; import {BidiNetworkManager} from './NetworkManager.js';
import {createBidiHandle} from './Realm.js'; import {createBidiHandle} from './Realm.js';
@ -89,7 +85,7 @@ export class BidiPage extends Page {
#accessibility: Accessibility; #accessibility: Accessibility;
#connection: BidiConnection; #connection: BidiConnection;
#frameTree = new FrameTree<BidiFrame>(); #frameTree = new FrameTree<BidiFrame>();
#networkManager: BidiNetworkManager; _networkManager: BidiNetworkManager;
#viewport: Viewport | null = null; #viewport: Viewport | null = null;
#closedDeferred = Deferred.create<never, TargetCloseError>(); #closedDeferred = Deferred.create<never, TargetCloseError>();
#subscribedEvents = new Map<Bidi.Event['method'], Handler<any>>([ #subscribedEvents = new Map<Bidi.Event['method'], Handler<any>>([
@ -169,7 +165,7 @@ export class BidiPage extends Page {
this.#browsingContext.on(event, subscriber); this.#browsingContext.on(event, subscriber);
} }
this.#networkManager = new BidiNetworkManager(this.#connection, this); this._networkManager = new BidiNetworkManager(this.#connection, this);
for (const [event, subscriber] of this.#subscribedEvents) { for (const [event, subscriber] of this.#subscribedEvents) {
this.#connection.on(event, subscriber); this.#connection.on(event, subscriber);
@ -177,7 +173,7 @@ export class BidiPage extends Page {
for (const [event, subscriber] of this.#networkManagerEvents) { for (const [event, subscriber] of this.#networkManagerEvents) {
// TODO: remove any // TODO: remove any
this.#networkManager.on(event, subscriber as any); this._networkManager.on(event, subscriber as any);
} }
const frame = new BidiFrame( const frame = new BidiFrame(
@ -381,7 +377,7 @@ export class BidiPage extends Page {
this.#removeFramesRecursively(child); this.#removeFramesRecursively(child);
} }
frame[disposeSymbol](); frame[disposeSymbol]();
this.#networkManager.clearMapAfterFrameDispose(frame); this._networkManager.clearMapAfterFrameDispose(frame);
this.#frameTree.removeFrame(frame); this.#frameTree.removeFrame(frame);
this.emit(PageEvent.FrameDetached, frame); this.emit(PageEvent.FrameDetached, frame);
} }
@ -460,8 +456,8 @@ export class BidiPage extends Page {
this.emit(PageEvent.Dialog, dialog); this.emit(PageEvent.Dialog, dialog);
} }
getNavigationResponse(id: string | null): BidiHTTPResponse | null { getNavigationResponse(id?: string | null): BidiHTTPResponse | null {
return this.#networkManager.getNavigationResponse(id); return this._networkManager.getNavigationResponse(id);
} }
override isClosed(): boolean { override isClosed(): boolean {
@ -474,7 +470,7 @@ export class BidiPage extends Page {
} }
this.#closedDeferred.reject(new TargetCloseError('Page closed!')); this.#closedDeferred.reject(new TargetCloseError('Page closed!'));
this.#networkManager.dispose(); this._networkManager.dispose();
await this.#connection.send('browsingContext.close', { await this.#connection.send('browsingContext.close', {
context: this.mainFrame()._id, context: this.mainFrame()._id,
@ -489,32 +485,25 @@ export class BidiPage extends Page {
): Promise<BidiHTTPResponse | null> { ): Promise<BidiHTTPResponse | null> {
const { const {
waitUntil = 'load', waitUntil = 'load',
timeout = this._timeoutSettings.navigationTimeout(), timeout: ms = this._timeoutSettings.navigationTimeout(),
} = options; } = options;
const readinessState = lifeCycleToReadinessState.get( const [readiness, networkIdle] = getBiDiReadinessState(waitUntil);
getWaitUntilSingle(waitUntil)
) as Bidi.BrowsingContext.ReadinessState;
try { const response = await firstValueFrom(
const {result} = await waitWithTimeout( this.mainFrame()
this.#connection.send('browsingContext.reload', { ._waitWithNetworkIdle(
context: this.mainFrame()._id, this.#connection.send('browsingContext.reload', {
wait: readinessState, context: this.mainFrame()._id,
}), wait: readiness,
'Navigation', }),
timeout networkIdle
); )
.pipe(raceWith(timeout(ms), from(this.#closedDeferred.valueOrThrow())))
.pipe(rewriteNavigationError(this.url(), ms))
);
return this.getNavigationResponse(result.navigation); return this.getNavigationResponse(response?.result.navigation);
} catch (error) {
if (error instanceof ProtocolError) {
error.message += ` at ${this.url}`;
} else if (error instanceof TimeoutError) {
error.message = 'Navigation timeout of ' + timeout + ' ms exceeded';
}
throw error;
}
} }
override setDefaultNavigationTimeout(timeout: number): void { override setDefaultNavigationTimeout(timeout: number): void {
@ -692,7 +681,7 @@ export class BidiPage extends Page {
): Promise<BidiHTTPRequest> { ): Promise<BidiHTTPRequest> {
const {timeout = this._timeoutSettings.timeout()} = options; const {timeout = this._timeoutSettings.timeout()} = options;
return await waitForHTTP( return await waitForHTTP(
this.#networkManager, this._networkManager,
NetworkManagerEvent.Request, NetworkManagerEvent.Request,
urlOrPredicate, urlOrPredicate,
timeout, timeout,
@ -708,7 +697,7 @@ export class BidiPage extends Page {
): Promise<BidiHTTPResponse> { ): Promise<BidiHTTPResponse> {
const {timeout = this._timeoutSettings.timeout()} = options; const {timeout = this._timeoutSettings.timeout()} = options;
return await waitForHTTP( return await waitForHTTP(
this.#networkManager, this._networkManager,
NetworkManagerEvent.Response, NetworkManagerEvent.Response,
urlOrPredicate, urlOrPredicate,
timeout, timeout,
@ -719,13 +708,15 @@ export class BidiPage extends Page {
override async waitForNetworkIdle( override async waitForNetworkIdle(
options: {idleTime?: number; timeout?: number} = {} options: {idleTime?: number; timeout?: number} = {}
): Promise<void> { ): Promise<void> {
const {idleTime = 500, timeout = this._timeoutSettings.timeout()} = options; const {
idleTime = NETWORK_IDLE_TIME,
timeout: ms = this._timeoutSettings.timeout(),
} = options;
await this._waitForNetworkIdle( await firstValueFrom(
this.#networkManager, this._waitForNetworkIdle(this._networkManager, idleTime).pipe(
idleTime, raceWith(timeout(ms), from(this.#closedDeferred.valueOrThrow()))
timeout, )
this.#closedDeferred
); );
} }

View File

@ -0,0 +1,120 @@
/**
* Copyright 2023 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import type {
ObservableInput,
ObservedValueOf,
OperatorFunction,
} from '../../third_party/rxjs/rxjs';
import {catchError} from '../../third_party/rxjs/rxjs';
import type {PuppeteerLifeCycleEvent} from '../cdp/LifecycleWatcher';
import {ProtocolError, TimeoutError} from '../common/Errors';
export type BiDiNetworkIdle = Extract<
PuppeteerLifeCycleEvent,
'networkidle0' | 'networkidle2'
> | null;
/**
* @internal
*/
export function getBiDiLifeCycles(
event: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]
): [
Extract<PuppeteerLifeCycleEvent, 'load' | 'domcontentloaded'>,
BiDiNetworkIdle,
] {
if (Array.isArray(event)) {
const pageLifeCycle = event.some(lifeCycle => {
return lifeCycle !== 'domcontentloaded';
})
? 'load'
: 'domcontentloaded';
const networkLifeCycle = event.reduce((acc, lifeCycle) => {
if (lifeCycle === 'networkidle0') {
return lifeCycle;
} else if (acc !== 'networkidle0' && lifeCycle === 'networkidle2') {
return lifeCycle;
}
return acc;
}, null as BiDiNetworkIdle);
return [pageLifeCycle, networkLifeCycle];
}
if (event === 'networkidle0' || event === 'networkidle2') {
return ['load', event];
}
return [event, null];
}
/**
* @internal
*/
export const lifeCycleToReadinessState = new Map<
PuppeteerLifeCycleEvent,
Bidi.BrowsingContext.ReadinessState
>([
['load', Bidi.BrowsingContext.ReadinessState.Complete],
['domcontentloaded', Bidi.BrowsingContext.ReadinessState.Interactive],
]);
export function getBiDiReadinessState(
event: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]
): [Bidi.BrowsingContext.ReadinessState, BiDiNetworkIdle] {
const lifeCycles = getBiDiLifeCycles(event);
const readiness = lifeCycleToReadinessState.get(lifeCycles[0])!;
return [readiness, lifeCycles[1]];
}
/**
* @internal
*/
export const lifeCycleToSubscribedEvent = new Map<
PuppeteerLifeCycleEvent,
'browsingContext.load' | 'browsingContext.domContentLoaded'
>([
['load', 'browsingContext.load'],
['domcontentloaded', 'browsingContext.domContentLoaded'],
]);
export function getBiDiLifecycleEvent(
event: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]
): [
'browsingContext.load' | 'browsingContext.domContentLoaded',
BiDiNetworkIdle,
] {
const lifeCycles = getBiDiLifeCycles(event);
const bidiEvent = lifeCycleToSubscribedEvent.get(lifeCycles[0])!;
return [bidiEvent, lifeCycles[1]];
}
export function rewriteNavigationError<T, R extends ObservableInput<T>>(
message: string,
ms: number
): OperatorFunction<T, T | ObservedValueOf<R>> {
return catchError<T, R>(error => {
if (error instanceof ProtocolError) {
error.message += ` at ${message}`;
} else if (error instanceof TimeoutError) {
error.message = `Navigation timeout of ${ms} ms exceeded`;
}
throw error;
});
}

View File

@ -53,6 +53,7 @@ import {
evaluationString, evaluationString,
getReadableAsBuffer, getReadableAsBuffer,
getReadableFromProtocolStream, getReadableFromProtocolStream,
NETWORK_IDLE_TIME,
pageBindingInitString, pageBindingInitString,
timeout, timeout,
validateDialogType, validateDialogType,
@ -936,13 +937,18 @@ export class CdpPage extends Page {
override async waitForNetworkIdle( override async waitForNetworkIdle(
options: {idleTime?: number; timeout?: number} = {} options: {idleTime?: number; timeout?: number} = {}
): Promise<void> { ): Promise<void> {
const {idleTime = 500, timeout = this._timeoutSettings.timeout()} = options; const {
idleTime = NETWORK_IDLE_TIME,
timeout: ms = this._timeoutSettings.timeout(),
} = options;
await this._waitForNetworkIdle( await firstValueFrom(
this.#frameManager.networkManager, this._waitForNetworkIdle(
idleTime, this.#frameManager.networkManager,
timeout, idleTime
this.#sessionCloseDeferred ).pipe(
raceWith(timeout(ms), from(this.#sessionCloseDeferred.valueOrThrow()))
)
); );
} }

View File

@ -34,7 +34,7 @@ import type {CDPSession} from '../api/CDPSession.js';
import type {Page} from '../api/Page.js'; import type {Page} from '../api/Page.js';
import {isNode} from '../environment.js'; import {isNode} from '../environment.js';
import {assert} from '../util/assert.js'; import {assert} from '../util/assert.js';
import {Deferred} from '../util/Deferred.js'; import type {Deferred} from '../util/Deferred.js';
import {isErrorLike} from '../util/ErrorLike.js'; import {isErrorLike} from '../util/ErrorLike.js';
import {debug} from './Debug.js'; import {debug} from './Debug.js';
@ -409,22 +409,6 @@ export function pageBindingInitString(type: string, name: string): string {
return evaluationString(addPageBinding, type, name); return evaluationString(addPageBinding, type, name);
} }
/**
* @internal
*/
export async function waitWithTimeout<T>(
promise: Promise<T>,
taskName: string,
timeout: number
): Promise<T> {
const deferred = Deferred.create<never>({
message: `waiting for ${taskName} failed: timeout ${timeout}ms exceeded`,
timeout,
});
return await Deferred.race([promise, deferred]);
}
/** /**
* @internal * @internal
*/ */
@ -633,3 +617,8 @@ export async function waitForHTTP<T extends {url(): string}>(
) )
); );
} }
/**
* @internal
*/
export const NETWORK_IDLE_TIME = 500;

View File

@ -24,6 +24,7 @@ export {
filter, filter,
first, first,
firstValueFrom, firstValueFrom,
forkJoin,
from, from,
fromEvent, fromEvent,
identity, identity,
@ -34,9 +35,7 @@ export {
mergeMap, mergeMap,
NEVER, NEVER,
noop, noop,
Observable,
of, of,
OperatorFunction,
pipe, pipe,
race, race,
raceWith, raceWith,
@ -47,9 +46,10 @@ export {
tap, tap,
throwIfEmpty, throwIfEmpty,
timer, timer,
forkJoin,
} from 'rxjs'; } from 'rxjs';
export type * from 'rxjs';
import {filter, from, map, mergeMap, type Observable} from 'rxjs'; import {filter, from, map, mergeMap, type Observable} from 'rxjs';
export function filterAsync<T>( export function filterAsync<T>(

View File

@ -155,18 +155,6 @@
"parameters": ["webDriverBiDi"], "parameters": ["webDriverBiDi"],
"expectations": ["PASS"] "expectations": ["PASS"]
}, },
{
"testIdPattern": "[navigation.spec] navigation Frame.goto *",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[navigation.spec] navigation Frame.waitForNavigation *",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["PASS"]
},
{ {
"testIdPattern": "[navigation.spec] navigation Page.goBack *", "testIdPattern": "[navigation.spec] navigation Page.goBack *",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
@ -195,7 +183,7 @@
"testIdPattern": "[network.spec] network Page.setBypassServiceWorker *", "testIdPattern": "[network.spec] network Page.setBypassServiceWorker *",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"], "parameters": ["webDriverBiDi"],
"expectations": ["FAIL"] "expectations": ["FAIL", "TIMEOUT"]
}, },
{ {
"testIdPattern": "[network.spec] network Page.setBypassServiceWorker *", "testIdPattern": "[network.spec] network Page.setBypassServiceWorker *",
@ -792,10 +780,10 @@
"expectations": ["FAIL"] "expectations": ["FAIL"]
}, },
{ {
"testIdPattern": "[navigation.spec] navigation Frame.goto should reject when frame detaches", "testIdPattern": "[navigation.spec] navigation Frame.goto should return matching responses",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"], "parameters": ["webDriverBiDi"],
"expectations": ["PASS"] "expectations": ["FAIL"]
}, },
{ {
"testIdPattern": "[navigation.spec] navigation Page.goto should fail when navigating to bad SSL", "testIdPattern": "[navigation.spec] navigation Page.goto should fail when navigating to bad SSL",
@ -803,42 +791,12 @@
"parameters": ["firefox"], "parameters": ["firefox"],
"expectations": ["FAIL"] "expectations": ["FAIL"]
}, },
{
"testIdPattern": "[navigation.spec] navigation Page.goto should navigate to empty page with networkidle0",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[navigation.spec] navigation Page.goto should navigate to empty page with networkidle2",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[navigation.spec] navigation Page.goto should navigate to page with iframe and networkidle0",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["FAIL"]
},
{ {
"testIdPattern": "[navigation.spec] navigation Page.goto should send referer", "testIdPattern": "[navigation.spec] navigation Page.goto should send referer",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"], "parameters": ["webDriverBiDi"],
"expectations": ["FAIL"] "expectations": ["FAIL"]
}, },
{
"testIdPattern": "[navigation.spec] navigation Page.goto should wait for network idle to succeed navigation",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[navigation.spec] navigation Page.waitForNavigation *",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["PASS"]
},
{ {
"testIdPattern": "[network.spec] network Network Events Page.Events.RequestFinished", "testIdPattern": "[network.spec] network Network Events Page.Events.RequestFinished",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
@ -897,7 +855,7 @@
"testIdPattern": "[network.spec] network Response.fromServiceWorker Response.fromServiceWorker", "testIdPattern": "[network.spec] network Response.fromServiceWorker Response.fromServiceWorker",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"], "parameters": ["webDriverBiDi"],
"expectations": ["FAIL"] "expectations": ["FAIL", "TIMEOUT"]
}, },
{ {
"testIdPattern": "[network.spec] network Response.timing returns timing information", "testIdPattern": "[network.spec] network Response.timing returns timing information",
@ -2336,8 +2294,8 @@
{ {
"testIdPattern": "[navigation.spec] navigation Frame.goto should navigate subframes", "testIdPattern": "[navigation.spec] navigation Frame.goto should navigate subframes",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"], "parameters": ["firefox", "webDriverBiDi"],
"expectations": ["PASS"] "expectations": ["FAIL"]
}, },
{ {
"testIdPattern": "[navigation.spec] navigation Frame.goto should reject when frame detaches", "testIdPattern": "[navigation.spec] navigation Frame.goto should reject when frame detaches",

View File

@ -817,6 +817,7 @@ describe('navigation', function () {
const error = await navigationPromise; const error = await navigationPromise;
expect(error.message).atLeastOneToContain([ expect(error.message).atLeastOneToContain([
'Navigating frame was detached', 'Navigating frame was detached',
'Frame detached',
'Error: NS_BINDING_ABORTED', 'Error: NS_BINDING_ABORTED',
'net::ERR_ABORTED', 'net::ERR_ABORTED',
]); ]);

View File

@ -21,6 +21,9 @@
"output": [ "output": [
"bin/**", "bin/**",
"tsconfig.tsbuildinfo" "tsconfig.tsbuildinfo"
],
"dependencies": [
"../../packages/puppeteer-core:build"
] ]
}, },
"test": { "test": {