From 996d53fc65a696351dacc5304ca852b3099b3398 Mon Sep 17 00:00:00 2001 From: Alex Rudenko Date: Mon, 24 Jul 2023 12:23:39 +0200 Subject: [PATCH] chore: support targets for bidi (#10615) --- packages/puppeteer-core/src/api/Browser.ts | 35 ++++++- packages/puppeteer-core/src/common/Browser.ts | 57 +---------- .../puppeteer-core/src/common/bidi/Browser.ts | 7 ++ .../src/common/bidi/BrowserContext.ts | 44 +++++++-- .../puppeteer-core/src/common/bidi/Target.ts | 94 +++++++++++++++++++ 5 files changed, 169 insertions(+), 68 deletions(-) create mode 100644 packages/puppeteer-core/src/common/bidi/Target.ts diff --git a/packages/puppeteer-core/src/api/Browser.ts b/packages/puppeteer-core/src/api/Browser.ts index 113da5a2..560c4649 100644 --- a/packages/puppeteer-core/src/api/Browser.ts +++ b/packages/puppeteer-core/src/api/Browser.ts @@ -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, - options?: WaitForTargetOptions - ): Promise; - waitForTarget(): Promise { - throw new Error('Not implemented'); + options: WaitForTargetOptions = {} + ): Promise { + const {timeout = 30000} = options; + const targetDeferred = Deferred.create>(); + + 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 { + if ((await predicate(target)) && !targetDeferred.resolved()) { + targetDeferred.resolve(target); + } + } } /** diff --git a/packages/puppeteer-core/src/common/Browser.ts b/packages/puppeteer-core/src/common/Browser.ts index 6989ee1e..6a0df709 100644 --- a/packages/puppeteer-core/src/common/Browser.ts +++ b/packages/puppeteer-core/src/common/Browser.ts @@ -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, - options: WaitForTargetOptions = {} - ): Promise { - const {timeout = 30000} = options; - const targetDeferred = Deferred.create< - CDPTarget | PromiseLike - >(); - - 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 { - if ((await predicate(target)) && !targetDeferred.resolved()) { - targetDeferred.resolve(target); - } - } - } - override async version(): Promise { 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, + predicate: (x: Target) => boolean | Promise, options: {timeout?: number} = {} - ): Promise { + ): Promise { return this.#browser.waitForTarget(target => { return target.browserContext() === this && predicate(target); }, options); diff --git a/packages/puppeteer-core/src/common/bidi/Browser.ts b/packages/puppeteer-core/src/common/bidi/Browser.ts index bf0e8cdd..9ab94a9d 100644 --- a/packages/puppeteer-core/src/common/bidi/Browser.ts +++ b/packages/puppeteer-core/src/common/bidi/Browser.ts @@ -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 { return this.#defaultContext.newPage(); } + + override targets(): Target[] { + return this.browserContexts().flatMap(c => { + return c.targets(); + }); + } } interface Options { diff --git a/packages/puppeteer-core/src/common/bidi/BrowserContext.ts b/packages/puppeteer-core/src/common/bidi/BrowserContext.ts index 1eac6045..0fb4449c 100644 --- a/packages/puppeteer-core/src/common/bidi/BrowserContext.ts +++ b/packages/puppeteer-core/src/common/bidi/BrowserContext.ts @@ -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(); + #targets = new Map(); #onContextDestroyedBind = this.#onContextDestroyed.bind(this); #init = Deferred.create(); #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, + options: {timeout?: number} = {} + ): Promise { + 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 { @@ -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 { 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 { diff --git a/packages/puppeteer-core/src/common/bidi/Target.ts b/packages/puppeteer-core/src/common/bidi/Target.ts new file mode 100644 index 00000000..873c0e85 --- /dev/null +++ b/packages/puppeteer-core/src/common/bidi/Target.ts @@ -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 { + return null; + } + + override async page(): Promise { + return this.#page; + } + + override url(): string { + return this.#browsingContext.url; + } + + /** + * Creates a Chrome Devtools Protocol session attached to the target. + */ + override async createCDPSession(): Promise { + 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'); + } +}