Source code
Revision control
Copy as Markdown
Other Tools
/**
* @license
* Copyright 2022 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.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 {EventEmitter} from '../common/EventEmitter.js';
import {debugError} from '../common/util.js';
import type {Viewport} from '../common/Viewport.js';
import {assert} from '../util/assert.js';
import {bubble} from '../util/decorators.js';
import type {BidiBrowser} from './Browser.js';
import type {BrowsingContext} from './core/BrowsingContext.js';
import {UserContext} from './core/UserContext.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
*/
export interface BidiBrowserContextOptions {
defaultViewport: Viewport | null;
}
/**
* @internal
*/
export class BidiBrowserContext extends BrowserContext {
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>,
]
>();
#overrides: Array<{origin: string; permission: Permission}> = [];
private constructor(
browser: BidiBrowser,
userContext: UserContext,
options: BidiBrowserContextOptions
) {
super();
this.#browser = browser;
this.userContext = userContext;
this.#defaultViewport = options.defaultViewport;
}
#initialize() {
// Create targets for existing browsing contexts.
for (const browsingContext of this.userContext.browsingContexts) {
this.#createPage(browsingContext);
}
this.userContext.on('browsingcontext', ({browsingContext}) => {
const page = this.#createPage(browsingContext);
// We need to wait for the DOMContentLoaded as the
// browsingContext still may be navigating from the about:blank
browsingContext.once('DOMContentLoaded', () => {
if (browsingContext.originalOpener) {
for (const context of this.userContext.browsingContexts) {
if (context.id === browsingContext.originalOpener) {
this.#pages
.get(context)!
.trustedEmitter.emit(PageEvent.Popup, page);
}
}
}
});
});
this.userContext.on('closed', () => {
this.trustedEmitter.removeAllListeners();
});
}
#createPage(browsingContext: BrowsingContext): BidiPage {
const page = BidiPage.from(this, browsingContext);
this.#pages.set(browsingContext, page);
page.trustedEmitter.on(PageEvent.Close, () => {
this.#pages.delete(browsingContext);
});
// -- Target stuff starts here --
const pageTarget = new BidiPageTarget(page);
const pageTargets = new Map();
this.#targets.set(page, [pageTarget, pageTargets]);
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);
});
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()];
});
}
override async newPage(): Promise<Page> {
using _guard = await this.waitForScreenshotOperations();
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');
}
if (this.#defaultViewport) {
try {
await page.setViewport(this.#defaultViewport);
} catch {
// No support for setViewport in Firefox.
}
}
return page;
}
override async close(): Promise<void> {
assert(
this.userContext.id !== UserContext.DEFAULT,
'Default BrowserContext cannot be closed!'
);
try {
await this.userContext.remove();
} catch (error) {
debugError(error);
}
this.#targets.clear();
}
override browser(): BidiBrowser {
return this.#browser;
}
override async pages(): Promise<BidiPage[]> {
return [...this.userContext.browsingContexts].map(context => {
return this.#pages.get(context)!;
});
}
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 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;
}
}