refactor: use RxJS for waitForTarget (#11029)

This commit is contained in:
Nikolay Vitkov 2023-10-11 10:11:22 +02:00 committed by GitHub
parent d63f0cfc61
commit c8feb3e472
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 46 additions and 44 deletions

View File

@ -14,9 +14,7 @@ This will look all open [browser contexts](./puppeteer.browsercontext.md).
class BrowserContext { class BrowserContext {
abstract waitForTarget( abstract waitForTarget(
predicate: (x: Target) => boolean | Promise<boolean>, predicate: (x: Target) => boolean | Promise<boolean>,
options?: { options?: WaitForTargetOptions
timeout?: number;
}
): Promise<Target>; ): Promise<Target>;
} }
``` ```
@ -26,7 +24,7 @@ class BrowserContext {
| Parameter | Type | Description | | Parameter | Type | Description |
| --------- | ---------------------------------------------------------------------------- | ------------ | | --------- | ---------------------------------------------------------------------------- | ------------ |
| predicate | (x: [Target](./puppeteer.target.md)) =&gt; boolean \| Promise&lt;boolean&gt; | | | predicate | (x: [Target](./puppeteer.target.md)) =&gt; boolean \| Promise&lt;boolean&gt; | |
| options | { timeout?: number; } | _(Optional)_ | | options | [WaitForTargetOptions](./puppeteer.waitfortargetoptions.md) | _(Optional)_ |
**Returns:** **Returns:**

View File

@ -18,15 +18,23 @@ import type {ChildProcess} from 'child_process';
import type {Protocol} from 'devtools-protocol'; import type {Protocol} from 'devtools-protocol';
import {
firstValueFrom,
from,
merge,
raceWith,
filterAsync,
fromEvent,
type Observable,
} from '../../third_party/rxjs/rxjs.js';
import {EventEmitter, type EventType} from '../common/EventEmitter.js'; import {EventEmitter, type EventType} from '../common/EventEmitter.js';
import {debugError, waitWithTimeout} from '../common/util.js'; import {debugError} from '../common/util.js';
import {Deferred} from '../util/Deferred.js'; import {timeout} from '../common/util.js';
import {asyncDisposeSymbol, disposeSymbol} from '../util/disposable.js'; import {asyncDisposeSymbol, disposeSymbol} from '../util/disposable.js';
import type {BrowserContext} from './BrowserContext.js'; import type {BrowserContext} from './BrowserContext.js';
import type {Page} from './Page.js'; import type {Page} from './Page.js';
import type {Target} from './Target.js'; import type {Target} from './Target.js';
/** /**
* @public * @public
*/ */
@ -387,31 +395,14 @@ export abstract class Browser extends EventEmitter<BrowserEvents> {
predicate: (x: Target) => boolean | Promise<boolean>, predicate: (x: Target) => boolean | Promise<boolean>,
options: WaitForTargetOptions = {} options: WaitForTargetOptions = {}
): Promise<Target> { ): Promise<Target> {
const {timeout = 30000} = options; const {timeout: ms = 30000} = options;
const targetDeferred = Deferred.create<Target | PromiseLike<Target>>(); return await firstValueFrom(
merge(
this.on(BrowserEvent.TargetCreated, check); fromEvent(this, BrowserEvent.TargetCreated) as Observable<Target>,
this.on(BrowserEvent.TargetChanged, check); fromEvent(this, BrowserEvent.TargetChanged) as Observable<Target>,
try { from(this.targets())
this.targets().forEach(check); ).pipe(filterAsync(predicate), raceWith(timeout(ms)))
if (!timeout) {
return await targetDeferred.valueOrThrow();
}
return await waitWithTimeout(
targetDeferred.valueOrThrow(),
'target',
timeout
); );
} finally {
this.off(BrowserEvent.TargetCreated, check);
this.off(BrowserEvent.TargetChanged, check);
}
async function check(target: Target): Promise<void> {
if ((await predicate(target)) && !targetDeferred.resolved()) {
targetDeferred.resolve(target);
}
}
} }
/** /**

View File

@ -18,7 +18,7 @@ import {EventEmitter, type EventType} from '../common/EventEmitter.js';
import {debugError} from '../common/util.js'; import {debugError} from '../common/util.js';
import {asyncDisposeSymbol, disposeSymbol} from '../util/disposable.js'; import {asyncDisposeSymbol, disposeSymbol} from '../util/disposable.js';
import type {Browser, Permission} from './Browser.js'; import type {Browser, Permission, WaitForTargetOptions} from './Browser.js';
import type {Page} from './Page.js'; import type {Page} from './Page.js';
import type {Target} from './Target.js'; import type {Target} from './Target.js';
@ -128,7 +128,7 @@ export abstract class BrowserContext extends EventEmitter<BrowserContextEvents>
*/ */
abstract waitForTarget( abstract waitForTarget(
predicate: (x: Target) => boolean | Promise<boolean>, predicate: (x: Target) => boolean | Promise<boolean>,
options?: {timeout?: number} options?: WaitForTargetOptions
): Promise<Target>; ): Promise<Target>;
/** /**

View File

@ -16,6 +16,7 @@
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import type {WaitForTargetOptions} from '../api/Browser.js';
import {BrowserContext} from '../api/BrowserContext.js'; import {BrowserContext} from '../api/BrowserContext.js';
import type {Page} from '../api/Page.js'; import type {Page} from '../api/Page.js';
import type {Target} from '../api/Target.js'; import type {Target} from '../api/Target.js';
@ -58,7 +59,7 @@ export class BidiBrowserContext extends BrowserContext {
override waitForTarget( override waitForTarget(
predicate: (x: Target) => boolean | Promise<boolean>, predicate: (x: Target) => boolean | Promise<boolean>,
options: {timeout?: number} = {} options: WaitForTargetOptions = {}
): Promise<Target> { ): Promise<Target> {
return this.#browser.waitForTarget(target => { return this.#browser.waitForTarget(target => {
return target.browserContext() === this && predicate(target); return target.browserContext() === this && predicate(target);

View File

@ -288,8 +288,9 @@ export class BidiConnection extends EventEmitter<BidiEvents> {
return; return;
} }
this.#closed = true; this.#closed = true;
this.#transport.onmessage = undefined; // Both may still be invoked and produce errors
this.#transport.onclose = undefined; this.#transport.onmessage = () => {};
this.#transport.onclose = () => {};
this.#callbacks.clear(); this.#callbacks.clear();
} }

View File

@ -27,6 +27,7 @@ import {
type IsPageTargetCallback, type IsPageTargetCallback,
type Permission, type Permission,
type TargetFilterCallback, type TargetFilterCallback,
type WaitForTargetOptions,
} from '../api/Browser.js'; } from '../api/Browser.js';
import {BrowserContext, BrowserContextEvent} from '../api/BrowserContext.js'; import {BrowserContext, BrowserContextEvent} from '../api/BrowserContext.js';
import {CDPSessionEvent, type CDPSession} from '../api/CDPSession.js'; import {CDPSessionEvent, type CDPSession} from '../api/CDPSession.js';
@ -453,7 +454,7 @@ export class CdpBrowserContext extends BrowserContext {
override waitForTarget( override waitForTarget(
predicate: (x: Target) => boolean | Promise<boolean>, predicate: (x: Target) => boolean | Promise<boolean>,
options: {timeout?: number} = {} options: WaitForTargetOptions = {}
): Promise<Target> { ): Promise<Target> {
return this.#browser.waitForTarget(target => { return this.#browser.waitForTarget(target => {
return target.browserContext() === this && predicate(target); return target.browserContext() === this && predicate(target);

View File

@ -19,6 +19,7 @@ import type {ServerResponse} from 'http';
import expect from 'expect'; import expect from 'expect';
import {type Frame, TimeoutError} from 'puppeteer'; import {type Frame, TimeoutError} from 'puppeteer';
import type {HTTPRequest} from 'puppeteer-core/internal/api/HTTPRequest.js'; import type {HTTPRequest} from 'puppeteer-core/internal/api/HTTPRequest.js';
import type {HTTPResponse} from 'puppeteer-core/internal/api/HTTPResponse.js';
import {Deferred} from 'puppeteer-core/internal/util/Deferred.js'; import {Deferred} from 'puppeteer-core/internal/util/Deferred.js';
import {getTestState, setupTestBrowserHooks} from './mocha-utils.js'; import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
@ -837,19 +838,28 @@ describe('navigation', function () {
server.setRoute('/one-style.html', (_req, res) => { server.setRoute('/one-style.html', (_req, res) => {
return serverResponses.push(res); return serverResponses.push(res);
}); });
const navigations = []; const navigations: Array<Promise<HTTPResponse | null>> = [];
for (let i = 0; i < 3; ++i) { for (let i = 0; i < 3; ++i) {
navigations.push(frames[i]!.goto(server.PREFIX + '/one-style.html')); navigations.push(frames[i]!.goto(server.PREFIX + '/one-style.html'));
await server.waitForRequest('/one-style.html'); await server.waitForRequest('/one-style.html');
} }
// Respond from server out-of-order. // Respond from server out-of-order.
const serverResponseTexts = ['AAA', 'BBB', 'CCC']; const serverResponseTexts = ['AAA', 'BBB', 'CCC'];
try {
for (const i of [1, 2, 0]) { for (const i of [1, 2, 0]) {
serverResponses[i]!.end(serverResponseTexts[i]); const response = await getResponse(i);
const response = (await navigations[i])!;
expect(response.frame()).toBe(frames[i]); expect(response.frame()).toBe(frames[i]);
expect(await response.text()).toBe(serverResponseTexts[i]); expect(await response.text()).toBe(serverResponseTexts[i]);
} }
} catch (error) {
await Promise.all([getResponse(0), getResponse(1), getResponse(2)]);
throw error;
}
async function getResponse(index: number) {
serverResponses[index]!.end(serverResponseTexts[index]);
return (await navigations[index])!;
}
}); });
}); });