refactor: use generic implementation for waitForNetworkIdle (#11757)

This commit is contained in:
jrandolf 2024-01-26 16:06:41 +01:00 committed by GitHub
parent 932b010932
commit d085127bba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 260 additions and 161 deletions

View File

@ -135,6 +135,7 @@ sidebar_label: API
| [SnapshotOptions](./puppeteer.snapshotoptions.md) | | | [SnapshotOptions](./puppeteer.snapshotoptions.md) | |
| [TracingOptions](./puppeteer.tracingoptions.md) | | | [TracingOptions](./puppeteer.tracingoptions.md) | |
| [Viewport](./puppeteer.viewport.md) | | | [Viewport](./puppeteer.viewport.md) | |
| [WaitForNetworkIdleOptions](./puppeteer.waitfornetworkidleoptions.md) | |
| [WaitForOptions](./puppeteer.waitforoptions.md) | | | [WaitForOptions](./puppeteer.waitforoptions.md) | |
| [WaitForSelectorOptions](./puppeteer.waitforselectoroptions.md) | | | [WaitForSelectorOptions](./puppeteer.waitforselectoroptions.md) | |
| [WaitForTargetOptions](./puppeteer.waitfortargetoptions.md) | | | [WaitForTargetOptions](./puppeteer.waitfortargetoptions.md) | |

View File

@ -157,7 +157,7 @@ page.off('request', logRequest);
| [waitForFrame](./puppeteer.page.waitforframe.md) | | Waits for a frame matching the given conditions to appear. | | [waitForFrame](./puppeteer.page.waitforframe.md) | | Waits for a frame matching the given conditions to appear. |
| [waitForFunction](./puppeteer.page.waitforfunction.md) | | Waits for the provided function, <code>pageFunction</code>, to return a truthy value when evaluated in the page's context. | | [waitForFunction](./puppeteer.page.waitforfunction.md) | | Waits for the provided function, <code>pageFunction</code>, to return a truthy value when evaluated in the page's context. |
| [waitForNavigation](./puppeteer.page.waitfornavigation.md) | | Waits for the page to navigate to a new URL or to reload. It is useful when you run code that will indirectly cause the page to navigate. | | [waitForNavigation](./puppeteer.page.waitfornavigation.md) | | Waits for the page to navigate to a new URL or to reload. It is useful when you run code that will indirectly cause the page to navigate. |
| [waitForNetworkIdle](./puppeteer.page.waitfornetworkidle.md) | | | | [waitForNetworkIdle](./puppeteer.page.waitfornetworkidle.md) | | Waits for the network to be idle. |
| [waitForRequest](./puppeteer.page.waitforrequest.md) | | | | [waitForRequest](./puppeteer.page.waitforrequest.md) | | |
| [waitForResponse](./puppeteer.page.waitforresponse.md) | | | | [waitForResponse](./puppeteer.page.waitforresponse.md) | | |
| [waitForSelector](./puppeteer.page.waitforselector.md) | | Wait for the <code>selector</code> to appear in page. If at the moment of calling the method the <code>selector</code> already exists, the method will return immediately. If the <code>selector</code> doesn't appear after the <code>timeout</code> milliseconds of waiting, the function will throw. | | [waitForSelector](./puppeteer.page.waitforselector.md) | | Wait for the <code>selector</code> to appear in page. If at the moment of calling the method the <code>selector</code> already exists, the method will return immediately. If the <code>selector</code> doesn't appear after the <code>timeout</code> milliseconds of waiting, the function will throw. |

View File

@ -4,25 +4,24 @@ sidebar_label: Page.waitForNetworkIdle
# Page.waitForNetworkIdle() method # Page.waitForNetworkIdle() method
Waits for the network to be idle.
#### Signature: #### Signature:
```typescript ```typescript
class Page { class Page {
abstract waitForNetworkIdle(options?: { waitForNetworkIdle(options?: WaitForNetworkIdleOptions): Promise<void>;
idleTime?: number;
timeout?: number;
}): Promise<void>;
} }
``` ```
## Parameters ## Parameters
| Parameter | Type | Description | | Parameter | Type | Description |
| --------- | -------------------------------------------------- | ---------------------------------------- | | --------- | --------------------------------------------------------------------- | --------------------------------------------------- |
| options | &#123; idleTime?: number; timeout?: number; &#125; | _(Optional)_ Optional waiting parameters | | options | [WaitForNetworkIdleOptions](./puppeteer.waitfornetworkidleoptions.md) | _(Optional)_ Options to configure waiting behavior. |
**Returns:** **Returns:**
Promise&lt;void&gt; Promise&lt;void&gt;
Promise which resolves when network is idle A promise which resolves once the network is idle.

View File

@ -0,0 +1,20 @@
---
sidebar_label: WaitForNetworkIdleOptions
---
# WaitForNetworkIdleOptions interface
#### Signature:
```typescript
export interface WaitForNetworkIdleOptions extends WaitTimeoutOptions
```
**Extends:** [WaitTimeoutOptions](./puppeteer.waittimeoutoptions.md)
## Properties
| Property | Modifiers | Type | Description | Default |
| ----------- | --------------------- | ------ | --------------------------------------------------------------------------- | ---------------- |
| concurrency | <code>optional</code> | number | Maximum number concurrent of network connections to be considered inactive. | <code>0</code> |
| idleTime | <code>optional</code> | number | Time (in milliseconds) the network should be idle. | <code>500</code> |

View File

@ -9,7 +9,8 @@ import type {Readable} from 'stream';
import type {Protocol} from 'devtools-protocol'; import type {Protocol} from 'devtools-protocol';
import { import {
delay, concat,
EMPTY,
filter, filter,
filterAsync, filterAsync,
first, first,
@ -17,23 +18,22 @@ import {
from, from,
map, map,
merge, merge,
mergeMap,
of, of,
race,
raceWith, raceWith,
startWith, startWith,
switchMap, switchMap,
takeUntil,
timer,
type Observable, type Observable,
} from '../../third_party/rxjs/rxjs.js'; } from '../../third_party/rxjs/rxjs.js';
import type {HTTPRequest} from '../api/HTTPRequest.js'; import type {HTTPRequest} from '../api/HTTPRequest.js';
import type {HTTPResponse} from '../api/HTTPResponse.js'; import type {HTTPResponse} from '../api/HTTPResponse.js';
import type {BidiNetworkManager} from '../bidi/NetworkManager.js';
import type {Accessibility} from '../cdp/Accessibility.js'; import type {Accessibility} from '../cdp/Accessibility.js';
import type {Coverage} from '../cdp/Coverage.js'; import type {Coverage} from '../cdp/Coverage.js';
import type {DeviceRequestPrompt} from '../cdp/DeviceRequestPrompt.js'; import type {DeviceRequestPrompt} from '../cdp/DeviceRequestPrompt.js';
import type { import type {Credentials, NetworkConditions} from '../cdp/NetworkManager.js';
NetworkManager as CdpNetworkManager,
Credentials,
NetworkConditions,
} from '../cdp/NetworkManager.js';
import type {Tracing} from '../cdp/Tracing.js'; import type {Tracing} from '../cdp/Tracing.js';
import type {ConsoleMessage} from '../common/ConsoleMessage.js'; import type {ConsoleMessage} from '../common/ConsoleMessage.js';
import type {Device} from '../common/Device.js'; import type {Device} from '../common/Device.js';
@ -45,7 +45,6 @@ import {
type Handler, type Handler,
} from '../common/EventEmitter.js'; } from '../common/EventEmitter.js';
import type {FileChooser} from '../common/FileChooser.js'; import type {FileChooser} from '../common/FileChooser.js';
import {NetworkManagerEvent} from '../common/NetworkManagerEvents.js';
import type {PDFOptions} from '../common/PDFOptions.js'; import type {PDFOptions} from '../common/PDFOptions.js';
import {TimeoutSettings} from '../common/TimeoutSettings.js'; import {TimeoutSettings} from '../common/TimeoutSettings.js';
import type { import type {
@ -61,6 +60,7 @@ import {
fromEmitterEvent, fromEmitterEvent,
importFSPromises, importFSPromises,
isString, isString,
NETWORK_IDLE_TIME,
timeout, timeout,
withSourcePuppeteerURLIfNone, withSourcePuppeteerURLIfNone,
} from '../common/util.js'; } from '../common/util.js';
@ -126,6 +126,24 @@ export interface Metrics {
JSHeapTotalSize?: number; JSHeapTotalSize?: number;
} }
/**
* @public
*/
export interface WaitForNetworkIdleOptions extends WaitTimeoutOptions {
/**
* Time (in milliseconds) the network should be idle.
*
* @defaultValue `500`
*/
idleTime?: number;
/**
* Maximum number concurrent of network connections to be considered inactive.
*
* @defaultValue `0`
*/
concurrency?: number;
}
/** /**
* @public * @public
*/ */
@ -586,11 +604,48 @@ export abstract class Page extends EventEmitter<PageEvents> {
#requestHandlers = new WeakMap<Handler<HTTPRequest>, Handler<HTTPRequest>>(); #requestHandlers = new WeakMap<Handler<HTTPRequest>, Handler<HTTPRequest>>();
#requestsInFlight = 0;
#inflight$: Observable<number>;
/** /**
* @internal * @internal
*/ */
constructor() { constructor() {
super(); super();
this.#inflight$ = fromEmitterEvent(this, PageEvent.Request).pipe(
takeUntil(fromEmitterEvent(this, PageEvent.Close)),
mergeMap(request => {
return concat(
of(1),
race(
fromEmitterEvent(this, PageEvent.Response).pipe(
filter(response => {
return response.request()._requestId === request._requestId;
})
),
fromEmitterEvent(this, PageEvent.RequestFailed).pipe(
filter(failure => {
return failure._requestId === request._requestId;
})
),
fromEmitterEvent(this, PageEvent.RequestFinished).pipe(
filter(success => {
return success._requestId === request._requestId;
})
)
).pipe(
map(() => {
return -1;
})
)
);
})
);
this.#inflight$.subscribe(count => {
this.#requestsInFlight += count;
});
} }
/** /**
@ -1699,34 +1754,45 @@ export abstract class Page extends EventEmitter<PageEvents> {
} }
/** /**
* @param options - Optional waiting parameters * Waits for the network to be idle.
* @returns Promise which resolves when network is idle *
* @param options - Options to configure waiting behavior.
* @returns A promise which resolves once the network is idle.
*/ */
abstract waitForNetworkIdle(options?: { waitForNetworkIdle(options: WaitForNetworkIdleOptions = {}): Promise<void> {
idleTime?: number; return firstValueFrom(this.waitForNetworkIdle$(options));
timeout?: number; }
}): Promise<void>;
/** /**
* @internal * @internal
*/ */
_waitForNetworkIdle( waitForNetworkIdle$(
networkManager: BidiNetworkManager | CdpNetworkManager, options: WaitForNetworkIdleOptions = {}
idleTime: number,
requestsInFlight = 0
): Observable<void> { ): Observable<void> {
return merge( const {
fromEmitterEvent(networkManager, NetworkManagerEvent.Request), timeout: ms = this._timeoutSettings.timeout(),
fromEmitterEvent(networkManager, NetworkManagerEvent.Response), idleTime = NETWORK_IDLE_TIME,
fromEmitterEvent(networkManager, NetworkManagerEvent.RequestFailed) concurrency = 0,
).pipe( } = options;
startWith(undefined),
filter(() => { return this.#inflight$.pipe(
return networkManager.inFlightRequestsCount() <= requestsInFlight; startWith(this.#requestsInFlight),
}),
switchMap(() => { switchMap(() => {
return of(undefined).pipe(delay(idleTime)); if (this.#requestsInFlight > concurrency) {
}) return EMPTY;
} else {
return timer(idleTime);
}
}),
map(() => {}),
raceWith(
timeout(ms),
fromEmitterEvent(this, PageEvent.Close).pipe(
map(() => {
throw new TargetCloseError('Page closed!');
})
)
)
); );
} }

View File

@ -14,6 +14,7 @@ import {
map, map,
merge, merge,
raceWith, raceWith,
zip,
} 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 type {ElementHandle} from '../api/ElementHandle.js'; import type {ElementHandle} from '../api/ElementHandle.js';
@ -27,7 +28,12 @@ import type {WaitForSelectorOptions} from '../api/Page.js';
import {UnsupportedOperation} from '../common/Errors.js'; import {UnsupportedOperation} from '../common/Errors.js';
import type {TimeoutSettings} from '../common/TimeoutSettings.js'; import type {TimeoutSettings} from '../common/TimeoutSettings.js';
import type {Awaitable, NodeFor} from '../common/types.js'; import type {Awaitable, NodeFor} from '../common/types.js';
import {fromEmitterEvent, timeout, UTILITY_WORLD_NAME} from '../common/util.js'; import {
fromEmitterEvent,
NETWORK_IDLE_TIME,
timeout,
UTILITY_WORLD_NAME,
} 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';
@ -128,21 +134,33 @@ export class BidiFrame extends Frame {
const [readiness, networkIdle] = getBiDiReadinessState(waitUntil); const [readiness, networkIdle] = getBiDiReadinessState(waitUntil);
const response = await firstValueFrom( const result$ = zip(
this.#page from(
._waitWithNetworkIdle( this.#context.connection.send('browsingContext.navigate', {
this.#context.connection.send('browsingContext.navigate', { context: this.#context.id,
context: this.#context.id, url,
url, wait: readiness,
wait: readiness, })
}), ),
networkIdle ...(networkIdle !== null
) ? [
.pipe(raceWith(timeout(ms), from(this.#abortDeferred.valueOrThrow()))) this.#page.waitForNetworkIdle$({
.pipe(rewriteNavigationError(url, ms)) timeout: ms,
concurrency: networkIdle === 'networkidle2' ? 2 : 0,
idleTime: NETWORK_IDLE_TIME,
}),
]
: [])
).pipe(
map(([{result}]) => {
return result;
}),
raceWith(timeout(ms), from(this.#abortDeferred.valueOrThrow())),
rewriteNavigationError(url, ms)
); );
return this.#page.getNavigationResponse(response?.result.navigation); const result = await firstValueFrom(result$);
return this.#page.getNavigationResponse(result.navigation);
} }
@throwIfDetached @throwIfDetached
@ -157,22 +175,30 @@ export class BidiFrame extends Frame {
const [waitEvent, networkIdle] = getBiDiLifecycleEvent(waitUntil); const [waitEvent, networkIdle] = getBiDiLifecycleEvent(waitUntil);
await firstValueFrom( const result$ = zip(
this.#page forkJoin([
._waitWithNetworkIdle( fromEmitterEvent(this.#context, waitEvent).pipe(first()),
forkJoin([ from(this.setFrameContent(html)),
fromEmitterEvent(this.#context, waitEvent).pipe(first()), ]).pipe(
from(this.setFrameContent(html)), map(() => {
]).pipe( return null;
map(() => { })
return null; ),
}) ...(networkIdle !== null
), ? [
networkIdle this.#page.waitForNetworkIdle$({
) timeout: ms,
.pipe(raceWith(timeout(ms), from(this.#abortDeferred.valueOrThrow()))) concurrency: networkIdle === 'networkidle2' ? 2 : 0,
.pipe(rewriteNavigationError('setContent', ms)) idleTime: NETWORK_IDLE_TIME,
}),
]
: [])
).pipe(
raceWith(timeout(ms), from(this.#abortDeferred.valueOrThrow())),
rewriteNavigationError('setContent', ms)
); );
await firstValueFrom(result$);
} }
context(): BrowsingContext { context(): BrowsingContext {
@ -190,7 +216,7 @@ export class BidiFrame extends Frame {
const [waitUntilEvent, networkIdle] = getBiDiLifecycleEvent(waitUntil); const [waitUntilEvent, networkIdle] = getBiDiLifecycleEvent(waitUntil);
const navigatedObservable = merge( const navigation$ = merge(
forkJoin([ forkJoin([
fromEmitterEvent( fromEmitterEvent(
this.#context, this.#context,
@ -211,13 +237,26 @@ export class BidiFrame extends Frame {
}) })
); );
const response = await firstValueFrom( const result$ = zip(
this.#page navigation$,
._waitWithNetworkIdle(navigatedObservable, networkIdle) ...(networkIdle !== null
.pipe(raceWith(timeout(ms), from(this.#abortDeferred.valueOrThrow()))) ? [
this.#page.waitForNetworkIdle$({
timeout: ms,
concurrency: networkIdle === 'networkidle2' ? 2 : 0,
idleTime: NETWORK_IDLE_TIME,
}),
]
: [])
).pipe(
map(([{result}]) => {
return result;
}),
raceWith(timeout(ms), from(this.#abortDeferred.valueOrThrow()))
); );
return this.#page.getNavigationResponse(response?.result.navigation); const result = await firstValueFrom(result$);
return this.#page.getNavigationResponse(result.navigation);
} }
override waitForDevicePrompt(): never { override waitForDevicePrompt(): never {

View File

@ -9,14 +9,12 @@ import type {Readable} from 'stream';
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import type Protocol from 'devtools-protocol'; import type Protocol from 'devtools-protocol';
import type {Observable, ObservableInput} from '../../third_party/rxjs/rxjs.js';
import { import {
first,
firstValueFrom, firstValueFrom,
forkJoin,
from, from,
map, map,
raceWith, raceWith,
zip,
} 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 type {BoundingBox} from '../api/ElementHandle.js'; import type {BoundingBox} from '../api/ElementHandle.js';
@ -75,7 +73,6 @@ 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 type {BiDiNetworkIdle} from './lifecycle.js';
import {getBiDiReadinessState, rewriteNavigationError} from './lifecycle.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';
@ -497,19 +494,32 @@ export class BidiPage extends Page {
const [readiness, networkIdle] = getBiDiReadinessState(waitUntil); const [readiness, networkIdle] = getBiDiReadinessState(waitUntil);
const response = await firstValueFrom( const result$ = zip(
this._waitWithNetworkIdle( from(
this.#connection.send('browsingContext.reload', { this.#connection.send('browsingContext.reload', {
context: this.mainFrame()._id, context: this.mainFrame()._id,
wait: readiness, wait: readiness,
}), })
networkIdle ),
) ...(networkIdle !== null
.pipe(raceWith(timeout(ms), from(this.#closedDeferred.valueOrThrow()))) ? [
.pipe(rewriteNavigationError(this.url(), ms)) this.waitForNetworkIdle$({
timeout: ms,
concurrency: networkIdle === 'networkidle2' ? 2 : 0,
idleTime: NETWORK_IDLE_TIME,
}),
]
: [])
).pipe(
map(([{result}]) => {
return result;
}),
raceWith(timeout(ms), from(this.#closedDeferred.valueOrThrow())),
rewriteNavigationError(this.url(), ms)
); );
return this.getNavigationResponse(response?.result.navigation); const result = await firstValueFrom(result$);
return this.getNavigationResponse(result.navigation);
} }
override setDefaultNavigationTimeout(timeout: number): void { override setDefaultNavigationTimeout(timeout: number): void {
@ -701,48 +711,6 @@ export class BidiPage extends Page {
return data; return data;
} }
override async waitForNetworkIdle(
options: {idleTime?: number; timeout?: number} = {}
): Promise<void> {
const {
idleTime = NETWORK_IDLE_TIME,
timeout: ms = this._timeoutSettings.timeout(),
} = options;
await firstValueFrom(
this._waitForNetworkIdle(this.#networkManager, idleTime).pipe(
raceWith(timeout(ms), from(this.#closedDeferred.valueOrThrow()))
)
);
}
/** @internal */
_waitWithNetworkIdle(
observableInput: ObservableInput<{
result: Bidi.BrowsingContext.NavigateResult;
} | null>,
networkIdle: BiDiNetworkIdle
): Observable<{
result: Bidi.BrowsingContext.NavigateResult;
} | null> {
const delay = networkIdle
? this._waitForNetworkIdle(
this.#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;
})
);
}
override async createCDPSession(): Promise<CDPSession> { override async createCDPSession(): Promise<CDPSession> {
const {sessionId} = await this.mainFrame() const {sessionId} = await this.mainFrame()
.context() .context()

View File

@ -42,7 +42,6 @@ import {
evaluationString, evaluationString,
getReadableAsBuffer, getReadableAsBuffer,
getReadableFromProtocolStream, getReadableFromProtocolStream,
NETWORK_IDLE_TIME,
parsePDFOptions, parsePDFOptions,
timeout, timeout,
validateDialogType, validateDialogType,
@ -909,24 +908,6 @@ export class CdpPage extends Page {
return await this.target().createCDPSession(); return await this.target().createCDPSession();
} }
override async waitForNetworkIdle(
options: {idleTime?: number; timeout?: number} = {}
): Promise<void> {
const {
idleTime = NETWORK_IDLE_TIME,
timeout: ms = this._timeoutSettings.timeout(),
} = options;
await firstValueFrom(
this._waitForNetworkIdle(
this.#frameManager.networkManager,
idleTime
).pipe(
raceWith(timeout(ms), from(this.#sessionCloseDeferred.valueOrThrow()))
)
);
}
override async goBack( override async goBack(
options: WaitForOptions = {} options: WaitForOptions = {}
): Promise<HTTPResponse | null> { ): Promise<HTTPResponse | null> {

View File

@ -6,6 +6,7 @@
export { export {
bufferCount, bufferCount,
catchError, catchError,
concat,
concatMap, concatMap,
defaultIfEmpty, defaultIfEmpty,
defer, defer,
@ -37,6 +38,7 @@ export {
tap, tap,
throwIfEmpty, throwIfEmpty,
timer, timer,
zip,
} from 'rxjs'; } from 'rxjs';
export type * from 'rxjs'; export type * from 'rxjs';

View File

@ -851,21 +851,16 @@ describe('Page', function () {
return Date.now(); return Date.now();
}), }),
page page
.evaluate(() => { .evaluate(async () => {
return (async () => { await Promise.all([fetch('/digits/1.png'), fetch('/digits/2.png')]);
await Promise.all([ await new Promise(resolve => {
fetch('/digits/1.png'), return setTimeout(resolve, 200);
fetch('/digits/2.png'), });
]); await fetch('/digits/3.png');
await new Promise(resolve => { await new Promise(resolve => {
return setTimeout(resolve, 200); return setTimeout(resolve, 200);
}); });
await fetch('/digits/3.png'); await fetch('/digits/4.png');
await new Promise(resolve => {
return setTimeout(resolve, 200);
});
await fetch('/digits/4.png');
})();
}) })
.then(() => { .then(() => {
return Date.now(); return Date.now();
@ -938,6 +933,34 @@ describe('Page', function () {
expect(error).toBe(false); expect(error).toBe(false);
}); });
it('should work with delayed response', async () => {
const {page, server} = await getTestState();
await page.goto(server.EMPTY_PAGE);
let response!: ServerResponse;
server.setRoute('/fetch-request-b.js', (_req, res) => {
response = res;
});
const t0 = Date.now();
const [t1, t2] = await Promise.all([
page.waitForNetworkIdle({idleTime: 100}).then(() => {
return Date.now();
}),
new Promise<number>(res => {
setTimeout(() => {
response.end();
res(Date.now());
}, 300);
}),
page.evaluate(async () => {
await fetch('/fetch-request-b.js');
}),
]);
expect(t1).toBeGreaterThan(t2);
// request finished + idle time.
expect(t1 - t0).toBeGreaterThan(400);
// request finished + idle time - request finished.
expect(t1 - t2).toBeGreaterThanOrEqual(100);
});
}); });
describe('Page.exposeFunction', function () { describe('Page.exposeFunction', function () {