diff options
Diffstat (limited to 'remote/test/puppeteer/packages/puppeteer-core/src/bidi/BrowserContext.ts')
-rw-r--r-- | remote/test/puppeteer/packages/puppeteer-core/src/bidi/BrowserContext.ts | 245 |
1 files changed, 184 insertions, 61 deletions
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/BrowserContext.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/BrowserContext.ts index feb5e9951d..9976e4cc6a 100644 --- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/BrowserContext.ts +++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/BrowserContext.ts @@ -6,18 +6,25 @@ 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 type {Page} from '../api/Page.js'; +import type {Permission} from '../api/Browser.js'; +import {WEB_PERMISSION_TO_PROTOCOL_PERMISSION} from '../api/Browser.js'; +import type {BrowserContextEvents} from '../api/BrowserContext.js'; +import {BrowserContext, BrowserContextEvent} from '../api/BrowserContext.js'; +import {PageEvent, type Page} from '../api/Page.js'; import type {Target} from '../api/Target.js'; -import {UnsupportedOperation} from '../common/Errors.js'; +import {EventEmitter} from '../common/EventEmitter.js'; import {debugError} from '../common/util.js'; import type {Viewport} from '../common/Viewport.js'; +import {bubble} from '../util/decorators.js'; import type {BidiBrowser} from './Browser.js'; -import type {BidiConnection} from './Connection.js'; +import type {BrowsingContext} from './core/BrowsingContext.js'; import {UserContext} from './core/UserContext.js'; -import type {BidiPage} from './Page.js'; +import type {BidiFrame} from './Frame.js'; +import {BidiPage} from './Page.js'; +import {BidiWorkerTarget} from './Target.js'; +import {BidiFrameTarget, BidiPageTarget} from './Target.js'; +import type {BidiWebWorker} from './WebWorker.js'; /** * @internal @@ -30,56 +37,134 @@ export interface BidiBrowserContextOptions { * @internal */ export class BidiBrowserContext extends BrowserContext { - #browser: BidiBrowser; - #connection: BidiConnection; - #defaultViewport: Viewport | null; - #userContext: UserContext; + static from( + browser: BidiBrowser, + userContext: UserContext, + options: BidiBrowserContextOptions + ): BidiBrowserContext { + const context = new BidiBrowserContext(browser, userContext, options); + context.#initialize(); + return context; + } + + @bubble() + accessor trustedEmitter = new EventEmitter<BrowserContextEvents>(); + + readonly #browser: BidiBrowser; + readonly #defaultViewport: Viewport | null; + // This is public because of cookies. + readonly userContext: UserContext; + readonly #pages = new WeakMap<BrowsingContext, BidiPage>(); + readonly #targets = new Map< + BidiPage, + [ + BidiPageTarget, + Map<BidiFrame | BidiWebWorker, BidiFrameTarget | BidiWorkerTarget>, + ] + >(); - constructor( + #overrides: Array<{origin: string; permission: Permission}> = []; + + private constructor( browser: BidiBrowser, userContext: UserContext, options: BidiBrowserContextOptions ) { super(); this.#browser = browser; - this.#userContext = userContext; - this.#connection = this.#browser.connection; + this.userContext = userContext; this.#defaultViewport = options.defaultViewport; } - override targets(): Target[] { - return this.#browser.targets().filter(target => { - return target.browserContext() === this; + #initialize() { + // Create targets for existing browsing contexts. + for (const browsingContext of this.userContext.browsingContexts) { + this.#createPage(browsingContext); + } + + this.userContext.on('browsingcontext', ({browsingContext}) => { + this.#createPage(browsingContext); + }); + this.userContext.on('closed', () => { + this.trustedEmitter.removeAllListeners(); }); } - override waitForTarget( - predicate: (x: Target) => boolean | Promise<boolean>, - options: WaitForTargetOptions = {} - ): Promise<Target> { - return this.#browser.waitForTarget(target => { - return target.browserContext() === this && predicate(target); - }, options); - } + #createPage(browsingContext: BrowsingContext): BidiPage { + const page = BidiPage.from(this, browsingContext); + this.#pages.set(browsingContext, page); + page.trustedEmitter.on(PageEvent.Close, () => { + this.#pages.delete(browsingContext); + }); - get connection(): BidiConnection { - return this.#connection; - } + // -- Target stuff starts here -- + const pageTarget = new BidiPageTarget(page); + const pageTargets = new Map(); + this.#targets.set(page, [pageTarget, pageTargets]); - override async newPage(): Promise<Page> { - const {result} = await this.#connection.send('browsingContext.create', { - type: Bidi.BrowsingContext.CreateType.Tab, + page.trustedEmitter.on(PageEvent.FrameAttached, frame => { + const bidiFrame = frame as BidiFrame; + const target = new BidiFrameTarget(bidiFrame); + pageTargets.set(bidiFrame, target); + this.trustedEmitter.emit(BrowserContextEvent.TargetCreated, target); + }); + page.trustedEmitter.on(PageEvent.FrameNavigated, frame => { + const bidiFrame = frame as BidiFrame; + const target = pageTargets.get(bidiFrame); + // If there is no target, then this is the page's frame. + if (target === undefined) { + this.trustedEmitter.emit(BrowserContextEvent.TargetChanged, pageTarget); + } else { + this.trustedEmitter.emit(BrowserContextEvent.TargetChanged, target); + } + }); + page.trustedEmitter.on(PageEvent.FrameDetached, frame => { + const bidiFrame = frame as BidiFrame; + const target = pageTargets.get(bidiFrame); + if (target === undefined) { + return; + } + pageTargets.delete(bidiFrame); + this.trustedEmitter.emit(BrowserContextEvent.TargetDestroyed, target); + }); + + page.trustedEmitter.on(PageEvent.WorkerCreated, worker => { + const bidiWorker = worker as BidiWebWorker; + const target = new BidiWorkerTarget(bidiWorker); + pageTargets.set(bidiWorker, target); + this.trustedEmitter.emit(BrowserContextEvent.TargetCreated, target); + }); + page.trustedEmitter.on(PageEvent.WorkerDestroyed, worker => { + const bidiWorker = worker as BidiWebWorker; + const target = pageTargets.get(bidiWorker); + if (target === undefined) { + return; + } + pageTargets.delete(worker); + this.trustedEmitter.emit(BrowserContextEvent.TargetDestroyed, target); }); - const target = this.#browser._getTargetById(result.context); - // TODO: once BiDi has some concept matching BrowserContext, the newly - // created contexts should get automatically assigned to the right - // BrowserContext. For now, we assume that only explicitly created pages go - // to the current BrowserContext. Otherwise, the contexts get assigned to - // the default BrowserContext by the Browser. - target._setBrowserContext(this); + page.trustedEmitter.on(PageEvent.Close, () => { + this.#targets.delete(page); + this.trustedEmitter.emit(BrowserContextEvent.TargetDestroyed, pageTarget); + }); + this.trustedEmitter.emit(BrowserContextEvent.TargetCreated, pageTarget); + // -- Target stuff ends here -- + + return page; + } + + override targets(): Target[] { + return [...this.#targets.values()].flatMap(([target, frames]) => { + return [target, ...frames.values()]; + }); + } - const page = await target.page(); + override async newPage(): Promise<Page> { + const context = await this.userContext.createBrowsingContext( + Bidi.BrowsingContext.CreateType.Tab + ); + const page = this.#pages.get(context)!; if (!page) { throw new Error('Page is not found'); } @@ -99,18 +184,8 @@ export class BidiBrowserContext extends BrowserContext { throw new Error('Default context cannot be closed!'); } - // TODO: Remove once we have adopted the new browsing contexts. - for (const target of this.targets()) { - const page = await target?.page(); - try { - await page?.close(); - } catch (error) { - debugError(error); - } - } - try { - await this.#userContext.remove(); + await this.userContext.remove(); } catch (error) { debugError(error); } @@ -121,25 +196,73 @@ export class BidiBrowserContext extends BrowserContext { } override async pages(): Promise<BidiPage[]> { - const results = await Promise.all( - [...this.targets()].map(t => { - return t.page(); - }) - ); - return results.filter((p): p is BidiPage => { - return p !== null; + return [...this.userContext.browsingContexts].map(context => { + return this.#pages.get(context)!; }); } override isIncognito(): boolean { - return this.#userContext.id !== UserContext.DEFAULT; + return this.userContext.id !== UserContext.DEFAULT; } - override overridePermissions(): never { - throw new UnsupportedOperation(); + override async overridePermissions( + origin: string, + permissions: Permission[] + ): Promise<void> { + const permissionsSet = new Set( + permissions.map(permission => { + const protocolPermission = + WEB_PERMISSION_TO_PROTOCOL_PERMISSION.get(permission); + if (!protocolPermission) { + throw new Error('Unknown permission: ' + permission); + } + return permission; + }) + ); + await Promise.all( + Array.from(WEB_PERMISSION_TO_PROTOCOL_PERMISSION.keys()).map( + permission => { + const result = this.userContext.setPermissions( + origin, + { + name: permission, + }, + permissionsSet.has(permission) + ? Bidi.Permissions.PermissionState.Granted + : Bidi.Permissions.PermissionState.Denied + ); + this.#overrides.push({origin, permission}); + // TODO: some permissions are outdated and setting them to denied does + // not work. + if (!permissionsSet.has(permission)) { + return result.catch(debugError); + } + return result; + } + ) + ); } - override clearPermissionOverrides(): never { - throw new UnsupportedOperation(); + override async clearPermissionOverrides(): Promise<void> { + const promises = this.#overrides.map(({permission, origin}) => { + return this.userContext + .setPermissions( + origin, + { + name: permission, + }, + Bidi.Permissions.PermissionState.Prompt + ) + .catch(debugError); + }); + this.#overrides = []; + await Promise.all(promises); + } + + override get id(): string | undefined { + if (this.userContext.id === UserContext.DEFAULT) { + return undefined; + } + return this.userContext.id; } } |