chore: support targets for bidi (#10615)

This commit is contained in:
Alex Rudenko 2023-07-24 12:23:39 +02:00 committed by GitHub
parent 1cf231de7f
commit 996d53fc65
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 169 additions and 68 deletions

View File

@ -21,6 +21,8 @@ import {ChildProcess} from 'child_process';
import {Protocol} from 'devtools-protocol';
import {EventEmitter} from '../common/EventEmitter.js';
import {waitWithTimeout} from '../common/util.js';
import {Deferred} from '../util/Deferred.js';
import type {BrowserContext} from './BrowserContext.js';
import type {Page} from './Page.js';
@ -376,12 +378,35 @@ export class Browser extends EventEmitter {
* );
* ```
*/
waitForTarget(
async waitForTarget(
predicate: (x: Target) => boolean | Promise<boolean>,
options?: WaitForTargetOptions
): Promise<Target>;
waitForTarget(): Promise<Target> {
throw new Error('Not implemented');
options: WaitForTargetOptions = {}
): Promise<Target> {
const {timeout = 30000} = options;
const targetDeferred = Deferred.create<Target | PromiseLike<Target>>();
this.on(BrowserEmittedEvents.TargetCreated, check);
this.on(BrowserEmittedEvents.TargetChanged, check);
try {
this.targets().forEach(check);
if (!timeout) {
return await targetDeferred.valueOrThrow();
}
return await waitWithTimeout(
targetDeferred.valueOrThrow(),
'target',
timeout
);
} finally {
this.off(BrowserEmittedEvents.TargetCreated, check);
this.off(BrowserEmittedEvents.TargetChanged, check);
}
async function check(target: Target): Promise<void> {
if ((await predicate(target)) && !targetDeferred.resolved()) {
targetDeferred.resolve(target);
}
}
}
/**

View File

@ -27,14 +27,12 @@ import {
BrowserContextEmittedEvents,
BrowserContextOptions,
WEB_PERMISSION_TO_PROTOCOL_PERMISSION,
WaitForTargetOptions,
Permission,
} from '../api/Browser.js';
import {BrowserContext} from '../api/BrowserContext.js';
import {Page} from '../api/Page.js';
import {Target} from '../api/Target.js';
import {assert} from '../util/assert.js';
import {Deferred} from '../util/Deferred.js';
import {ChromeTargetManager} from './ChromeTargetManager.js';
import {CDPSession, Connection, ConnectionEmittedEvents} from './Connection.js';
@ -49,7 +47,6 @@ import {
} from './Target.js';
import {TargetManager, TargetManagerEmittedEvents} from './TargetManager.js';
import {TaskQueue} from './TaskQueue.js';
import {waitWithTimeout} from './util.js';
/**
* @internal
@ -487,56 +484,6 @@ export class CDPBrowser extends BrowserBase {
return browserTarget;
}
/**
* Searches for a target in all browser contexts.
*
* @param predicate - A function to be run for every target.
* @returns The first target found that matches the `predicate` function.
*
* @example
*
* An example of finding a target for a page opened via `window.open`:
*
* ```ts
* await page.evaluate(() => window.open('https://www.example.com/'));
* const newWindowTarget = await browser.waitForTarget(
* target => target.url() === 'https://www.example.com/'
* );
* ```
*/
override async waitForTarget(
predicate: (x: CDPTarget) => boolean | Promise<boolean>,
options: WaitForTargetOptions = {}
): Promise<CDPTarget> {
const {timeout = 30000} = options;
const targetDeferred = Deferred.create<
CDPTarget | PromiseLike<CDPTarget>
>();
this.on(BrowserEmittedEvents.TargetCreated, check);
this.on(BrowserEmittedEvents.TargetChanged, check);
try {
this.targets().forEach(check);
if (!timeout) {
return await targetDeferred.valueOrThrow();
}
return await waitWithTimeout(
targetDeferred.valueOrThrow(),
'target',
timeout
);
} finally {
this.off(BrowserEmittedEvents.TargetCreated, check);
this.off(BrowserEmittedEvents.TargetChanged, check);
}
async function check(target: CDPTarget): Promise<void> {
if ((await predicate(target)) && !targetDeferred.resolved()) {
targetDeferred.resolve(target);
}
}
}
override async version(): Promise<string> {
const version = await this.#getVersion();
return version.product;
@ -626,9 +573,9 @@ export class CDPBrowserContext extends BrowserContext {
* that matches the `predicate` function.
*/
override waitForTarget(
predicate: (x: CDPTarget) => boolean | Promise<boolean>,
predicate: (x: Target) => boolean | Promise<boolean>,
options: {timeout?: number} = {}
): Promise<CDPTarget> {
): Promise<Target> {
return this.#browser.waitForTarget(target => {
return target.browserContext() === this && predicate(target);
}, options);

View File

@ -26,6 +26,7 @@ import {
} from '../../api/Browser.js';
import {BrowserContext as BrowserContextBase} from '../../api/BrowserContext.js';
import {Page} from '../../api/Page.js';
import {Target} from '../../puppeteer-core.js';
import {Viewport} from '../PuppeteerViewport.js';
import {BrowserContext} from './BrowserContext.js';
@ -178,6 +179,12 @@ export class Browser extends BrowserBase {
override newPage(): Promise<Page> {
return this.#defaultContext.newPage();
}
override targets(): Target[] {
return this.browserContexts().flatMap(c => {
return c.targets();
});
}
}
interface Options {

View File

@ -18,12 +18,14 @@ import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import {BrowserContext as BrowserContextBase} from '../../api/BrowserContext.js';
import {Page as PageBase} from '../../api/Page.js';
import {Target} from '../../api/Target.js';
import {Deferred} from '../../util/Deferred.js';
import {Viewport} from '../PuppeteerViewport.js';
import {Browser} from './Browser.js';
import {Connection} from './Connection.js';
import {Page} from './Page.js';
import {BiDiTarget} from './Target.js';
import {debugError} from './utils.js';
interface BrowserContextOptions {
@ -38,7 +40,7 @@ export class BrowserContext extends BrowserContextBase {
#browser: Browser;
#connection: Connection;
#defaultViewport: Viewport | null;
#pages = new Map<string, Page>();
#targets = new Map<string, BiDiTarget>();
#onContextDestroyedBind = this.#onContextDestroyed.bind(this);
#init = Deferred.create<void>();
#isDefault = false;
@ -56,6 +58,21 @@ export class BrowserContext extends BrowserContextBase {
this.#getTree().catch(debugError);
}
override targets(): Target[] {
return this.#browser.targets().filter(target => {
return target.browserContext() === this;
});
}
override waitForTarget(
predicate: (x: Target) => boolean | Promise<boolean>,
options: {timeout?: number} = {}
): Promise<Target> {
return this.#browser.waitForTarget(target => {
return target.browserContext() === this && predicate(target);
}, options);
}
get connection(): Connection {
return this.#connection;
}
@ -72,7 +89,8 @@ export class BrowserContext extends BrowserContextBase {
);
for (const context of result.contexts) {
const page = new Page(this, context);
this.#pages.set(context.context, page);
const target = new BiDiTarget(page.mainFrame().context(), page);
this.#targets.set(context.context, target);
}
this.#init.resolve();
} catch (err) {
@ -83,11 +101,12 @@ export class BrowserContext extends BrowserContextBase {
async #onContextDestroyed(
event: Bidi.BrowsingContext.ContextDestroyedEvent['params']
) {
const page = this.#pages.get(event.context);
const target = this.#targets.get(event.context);
const page = await target?.page();
await page?.close().catch(error => {
debugError(error);
});
this.#pages.delete(event.context);
this.#targets.delete(event.context);
}
override async newPage(): Promise<PageBase> {
@ -100,6 +119,7 @@ export class BrowserContext extends BrowserContextBase {
context: result.context,
children: [],
});
const target = new BiDiTarget(page.mainFrame().context(), page);
if (this.#defaultViewport) {
try {
await page.setViewport(this.#defaultViewport);
@ -108,7 +128,7 @@ export class BrowserContext extends BrowserContextBase {
}
}
this.#pages.set(result.context, page);
this.#targets.set(result.context, target);
return page;
}
@ -120,12 +140,13 @@ export class BrowserContext extends BrowserContextBase {
throw new Error('Default context cannot be closed!');
}
for (const page of this.#pages.values()) {
for (const target of this.#targets.values()) {
const page = await target?.page();
await page?.close().catch(error => {
debugError(error);
});
}
this.#pages.clear();
this.#targets.clear();
}
override browser(): Browser {
@ -134,7 +155,14 @@ export class BrowserContext extends BrowserContextBase {
override async pages(): Promise<PageBase[]> {
await this.#init.valueOrThrow();
return [...this.#pages.values()];
const results = await Promise.all(
[...this.#targets.values()].map(t => {
return t.page();
})
);
return results.filter((p): p is Page => {
return p !== null;
});
}
override isIncognito(): boolean {

View File

@ -0,0 +1,94 @@
/**
* 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 {Target, TargetType} from '../../api/Target.js';
import {CDPSession} from '../Connection.js';
import type {WebWorker} from '../WebWorker.js';
import {Browser} from './Browser.js';
import {BrowserContext} from './BrowserContext.js';
import {BrowsingContext, CDPSessionWrapper} from './BrowsingContext.js';
import {Page} from './Page.js';
export class BiDiTarget extends Target {
#browsingContext: BrowsingContext;
#page: Page;
constructor(browsingContext: BrowsingContext, page: Page) {
super();
this.#browsingContext = browsingContext;
this.#page = page;
}
override async worker(): Promise<WebWorker | null> {
return null;
}
override async page(): Promise<Page | null> {
return this.#page;
}
override url(): string {
return this.#browsingContext.url;
}
/**
* Creates a Chrome Devtools Protocol session attached to the target.
*/
override async createCDPSession(): Promise<CDPSession> {
const {sessionId} = await this.#browsingContext.cdpSession.send(
'Target.attachToTarget',
{
targetId: this.#page.mainFrame()._id,
flatten: true,
}
);
return new CDPSessionWrapper(this.#browsingContext, sessionId);
}
/**
* Identifies what kind of target this is.
*
* @remarks
*
* See {@link https://developer.chrome.com/extensions/background_pages | docs} for more info about background pages.
*/
override type(): TargetType {
return TargetType.PAGE;
}
/**
* Get the browser the target belongs to.
*/
override browser(): Browser {
throw new Error('Not implemented');
}
/**
* Get the browser context the target belongs to.
*/
override browserContext(): BrowserContext {
throw new Error('Not implemented');
}
/**
* Get the target that opened this target. Top-level targets return `null`.
*/
override opener(): Target | undefined {
throw new Error('Not implemented');
}
}