refactor: adopt core/UserContext on BidiBrowserContext (#11734)

This commit is contained in:
jrandolf 2024-01-24 15:07:45 +01:00 committed by GitHub
parent d57b1044f2
commit 398b31de26
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 113 additions and 51 deletions

View File

@ -27,6 +27,7 @@ import {BrowsingContext, BrowsingContextEvent} from './BrowsingContext.js';
import type {BidiConnection} from './Connection.js'; import type {BidiConnection} from './Connection.js';
import type {Browser as BrowserCore} from './core/Browser.js'; import type {Browser as BrowserCore} from './core/Browser.js';
import {Session} from './core/Session.js'; import {Session} from './core/Session.js';
import type {UserContext} from './core/UserContext.js';
import { import {
BiDiBrowserTarget, BiDiBrowserTarget,
BiDiBrowsingContextTarget, BiDiBrowsingContextTarget,
@ -95,9 +96,8 @@ export class BidiBrowser extends Browser {
#closeCallback?: BrowserCloseCallback; #closeCallback?: BrowserCloseCallback;
#browserCore: BrowserCore; #browserCore: BrowserCore;
#defaultViewport: Viewport | null; #defaultViewport: Viewport | null;
#defaultContext: BidiBrowserContext;
#targets = new Map<string, BidiTarget>(); #targets = new Map<string, BidiTarget>();
#contexts: BidiBrowserContext[] = []; #browserContexts = new WeakMap<UserContext, BidiBrowserContext>();
#browserTarget: BiDiBrowserTarget; #browserTarget: BiDiBrowserTarget;
#connectionEventHandlers = new Map< #connectionEventHandlers = new Map<
@ -111,25 +111,21 @@ export class BidiBrowser extends Browser {
['browsingContext.navigationStarted', this.#onContextNavigation.bind(this)], ['browsingContext.navigationStarted', this.#onContextNavigation.bind(this)],
]); ]);
constructor(browserCore: BrowserCore, opts: BidiBrowserOptions) { private constructor(browserCore: BrowserCore, opts: BidiBrowserOptions) {
super(); super();
this.#process = opts.process; this.#process = opts.process;
this.#closeCallback = opts.closeCallback; this.#closeCallback = opts.closeCallback;
this.#browserCore = browserCore; this.#browserCore = browserCore;
this.#defaultViewport = opts.defaultViewport; this.#defaultViewport = opts.defaultViewport;
this.#defaultContext = new BidiBrowserContext(this, { this.#browserTarget = new BiDiBrowserTarget(this);
defaultViewport: this.#defaultViewport, this.#createBrowserContext(this.#browserCore.defaultUserContext);
isDefault: true,
});
this.#browserTarget = new BiDiBrowserTarget(this.#defaultContext);
this.#contexts.push(this.#defaultContext);
} }
#initialize() { #initialize() {
this.#browserCore.once('disconnected', () => { this.#browserCore.once('disconnected', () => {
this.emit(BrowserEvent.Disconnected, undefined); this.emit(BrowserEvent.Disconnected, undefined);
}); });
this.#process?.once('close', async () => { this.#process?.once('close', () => {
this.#browserCore.dispose('Browser process exited.', true); this.#browserCore.dispose('Browser process exited.', true);
this.connection.dispose(); this.connection.dispose();
}); });
@ -150,6 +146,14 @@ export class BidiBrowser extends Browser {
throw new UnsupportedOperation(); throw new UnsupportedOperation();
} }
#createBrowserContext(userContext: UserContext) {
const browserContext = new BidiBrowserContext(this, userContext, {
defaultViewport: this.#defaultViewport,
});
this.#browserContexts.set(userContext, browserContext);
return browserContext;
}
#onContextDomLoaded(event: Bidi.BrowsingContext.Info) { #onContextDomLoaded(event: Bidi.BrowsingContext.Info) {
const target = this.#targets.get(event.context); const target = this.#targets.get(event.context);
if (target) { if (target) {
@ -255,13 +259,8 @@ export class BidiBrowser extends Browser {
override async createIncognitoBrowserContext( override async createIncognitoBrowserContext(
_options?: BrowserContextOptions _options?: BrowserContextOptions
): Promise<BidiBrowserContext> { ): Promise<BidiBrowserContext> {
// TODO: implement incognito context https://github.com/w3c/webdriver-bidi/issues/289. const userContext = await this.#browserCore.createUserContext();
const context = new BidiBrowserContext(this, { return this.#createBrowserContext(userContext);
defaultViewport: this.#defaultViewport,
isDefault: false,
});
this.#contexts.push(context);
return context;
} }
override async version(): Promise<string> { override async version(): Promise<string> {
@ -269,28 +268,17 @@ export class BidiBrowser extends Browser {
} }
override browserContexts(): BidiBrowserContext[] { override browserContexts(): BidiBrowserContext[] {
// TODO: implement incognito context https://github.com/w3c/webdriver-bidi/issues/289. return [...this.#browserCore.userContexts].map(context => {
return this.#contexts; return this.#browserContexts.get(context)!;
}
async _closeContext(browserContext: BidiBrowserContext): Promise<void> {
this.#contexts = this.#contexts.filter(c => {
return c !== browserContext;
}); });
for (const target of browserContext.targets()) {
const page = await target?.page();
await page?.close().catch(error => {
debugError(error);
});
}
} }
override defaultBrowserContext(): BidiBrowserContext { override defaultBrowserContext(): BidiBrowserContext {
return this.#defaultContext; return this.#browserContexts.get(this.#browserCore.defaultUserContext)!;
} }
override newPage(): Promise<Page> { override newPage(): Promise<Page> {
return this.#defaultContext.newPage(); return this.defaultBrowserContext().newPage();
} }
override targets(): Target[] { override targets(): Target[] {

View File

@ -11,10 +11,12 @@ 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';
import {UnsupportedOperation} from '../common/Errors.js'; import {UnsupportedOperation} from '../common/Errors.js';
import {debugError} from '../common/util.js';
import type {Viewport} from '../common/Viewport.js'; import type {Viewport} from '../common/Viewport.js';
import type {BidiBrowser} from './Browser.js'; import type {BidiBrowser} from './Browser.js';
import type {BidiConnection} from './Connection.js'; import type {BidiConnection} from './Connection.js';
import {UserContext} from './core/UserContext.js';
import type {BidiPage} from './Page.js'; import type {BidiPage} from './Page.js';
/** /**
@ -22,7 +24,6 @@ import type {BidiPage} from './Page.js';
*/ */
export interface BidiBrowserContextOptions { export interface BidiBrowserContextOptions {
defaultViewport: Viewport | null; defaultViewport: Viewport | null;
isDefault: boolean;
} }
/** /**
@ -32,14 +33,18 @@ export class BidiBrowserContext extends BrowserContext {
#browser: BidiBrowser; #browser: BidiBrowser;
#connection: BidiConnection; #connection: BidiConnection;
#defaultViewport: Viewport | null; #defaultViewport: Viewport | null;
#isDefault = false; #userContext: UserContext;
constructor(browser: BidiBrowser, options: BidiBrowserContextOptions) { constructor(
browser: BidiBrowser,
userContext: UserContext,
options: BidiBrowserContextOptions
) {
super(); super();
this.#browser = browser; this.#browser = browser;
this.#userContext = userContext;
this.#connection = this.#browser.connection; this.#connection = this.#browser.connection;
this.#defaultViewport = options.defaultViewport; this.#defaultViewport = options.defaultViewport;
this.#isDefault = options.isDefault;
} }
override targets(): Target[] { override targets(): Target[] {
@ -90,11 +95,25 @@ export class BidiBrowserContext extends BrowserContext {
} }
override async close(): Promise<void> { override async close(): Promise<void> {
if (this.#isDefault) { if (!this.isIncognito()) {
throw new Error('Default context cannot be closed!'); throw new Error('Default context cannot be closed!');
} }
await this.#browser._closeContext(this); // 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();
} catch (error) {
debugError(error);
}
} }
override browser(): BidiBrowser { override browser(): BidiBrowser {
@ -113,7 +132,7 @@ export class BidiBrowserContext extends BrowserContext {
} }
override isIncognito(): boolean { override isIncognito(): boolean {
return !this.#isDefault; return this.#userContext.id !== UserContext.DEFAULT;
} }
override overridePermissions(): never { override overridePermissions(): never {

View File

@ -53,7 +53,14 @@ export abstract class BidiTarget extends Target {
/** /**
* @internal * @internal
*/ */
export class BiDiBrowserTarget extends BidiTarget { export class BiDiBrowserTarget extends Target {
#browser: BidiBrowser;
constructor(browser: BidiBrowser) {
super();
this.#browser = browser;
}
override url(): string { override url(): string {
return ''; return '';
} }
@ -61,6 +68,26 @@ export class BiDiBrowserTarget extends BidiTarget {
override type(): TargetType { override type(): TargetType {
return TargetType.BROWSER; return TargetType.BROWSER;
} }
override asPage(): Promise<Page> {
throw new UnsupportedOperation();
}
override browser(): BidiBrowser {
return this.#browser;
}
override browserContext(): BidiBrowserContext {
return this.#browser.defaultBrowserContext();
}
override opener(): never {
throw new UnsupportedOperation();
}
override createCDPSession(): Promise<CDPSession> {
throw new UnsupportedOperation();
}
} }
/** /**

View File

@ -55,7 +55,7 @@ export class Browser extends EventEmitter<{
#closed = false; #closed = false;
#reason: string | undefined; #reason: string | undefined;
readonly #disposables = new DisposableStack(); readonly #disposables = new DisposableStack();
readonly #userContexts = new Map(); readonly #userContexts = new Map<string, UserContext>();
readonly session: Session; readonly session: Session;
// keep-sorted end // keep-sorted end
@ -65,7 +65,10 @@ export class Browser extends EventEmitter<{
this.session = session; this.session = session;
// keep-sorted end // keep-sorted end
this.#userContexts.set('', UserContext.create(this, '')); this.#userContexts.set(
UserContext.DEFAULT,
UserContext.create(this, UserContext.DEFAULT)
);
} }
async #initialize() { async #initialize() {
@ -120,7 +123,7 @@ export class Browser extends EventEmitter<{
} }
get defaultUserContext(): UserContext { get defaultUserContext(): UserContext {
// SAFETY: A UserContext is always created for the default context. // SAFETY: A UserContext is always created for the default context.
return this.#userContexts.get('')!; return this.#userContexts.get(UserContext.DEFAULT)!;
} }
get disconnected(): boolean { get disconnected(): boolean {
return this.#reason !== undefined; return this.#reason !== undefined;
@ -182,6 +185,32 @@ export class Browser extends EventEmitter<{
}); });
} }
static userContextId = 0;
@throwIfDisposed<Browser>(browser => {
// SAFETY: By definition of `disposed`, `#reason` is defined.
return browser.#reason!;
})
async createUserContext(): Promise<UserContext> {
// TODO: implement incognito context https://github.com/w3c/webdriver-bidi/issues/289.
// TODO: Call `createUserContext` once available.
// Generating a monotonically increasing context id.
const context = `${++Browser.userContextId}`;
const userContext = UserContext.create(this, context);
this.#userContexts.set(userContext.id, userContext);
const userContextEmitter = this.#disposables.use(
new EventEmitter(userContext)
);
userContextEmitter.once('closed', () => {
userContextEmitter.removeAllListeners();
this.#userContexts.delete(context);
});
return userContext;
}
[disposeSymbol](): void { [disposeSymbol](): void {
this.#reason ??= this.#reason ??=
'Browser was disconnected, probably because the session ended.'; 'Browser was disconnected, probably because the session ended.';

View File

@ -43,6 +43,8 @@ export class UserContext extends EventEmitter<{
reason: string; reason: string;
}; };
}> { }> {
static DEFAULT = 'default';
static create(browser: Browser, id: string): UserContext { static create(browser: Browser, id: string): UserContext {
const context = new UserContext(browser, id); const context = new UserContext(browser, id);
context.#initialize(); context.#initialize();
@ -54,8 +56,6 @@ export class UserContext extends EventEmitter<{
// Note these are only top-level contexts. // Note these are only top-level contexts.
readonly #browsingContexts = new Map<string, BrowsingContext>(); readonly #browsingContexts = new Map<string, BrowsingContext>();
readonly #disposables = new DisposableStack(); readonly #disposables = new DisposableStack();
// @ts-expect-error -- TODO: This will be used once the WebDriver BiDi
// protocol supports it.
readonly #id: string; readonly #id: string;
readonly browser: Browser; readonly browser: Browser;
// keep-sorted end // keep-sorted end
@ -118,6 +118,9 @@ export class UserContext extends EventEmitter<{
get disposed(): boolean { get disposed(): boolean {
return this.closed; return this.closed;
} }
get id(): string {
return this.#id;
}
// keep-sorted end // keep-sorted end
@inertIfDisposed @inertIfDisposed
@ -156,13 +159,9 @@ export class UserContext extends EventEmitter<{
// SAFETY: Disposal implies this exists. // SAFETY: Disposal implies this exists.
return context.#reason!; return context.#reason!;
}) })
async close(): Promise<void> { async remove(): Promise<void> {
try { try {
const promises = []; // TODO: Call `removeUserContext` once available.
for (const browsingContext of this.#browsingContexts.values()) {
promises.push(browsingContext.close());
}
await Promise.all(promises);
} finally { } finally {
this.dispose('User context already closed.'); this.dispose('User context already closed.');
} }