diff options
Diffstat (limited to 'remote/test/puppeteer/packages/puppeteer-core/src/bidi/core')
8 files changed, 404 insertions, 160 deletions
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Browser.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Browser.ts index 7c4a8ed01c..efeabc3a59 100644 --- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Browser.ts +++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Browser.ts @@ -11,7 +11,7 @@ import {inertIfDisposed, throwIfDisposed} from '../../util/decorators.js'; import {DisposableStack, disposeSymbol} from '../../util/disposable.js'; import type {BrowsingContext} from './BrowsingContext.js'; -import type {SharedWorkerRealm} from './Realm.js'; +import {SharedWorkerRealm} from './Realm.js'; import type {Session} from './Session.js'; import {UserContext} from './UserContext.js'; @@ -57,6 +57,7 @@ export class Browser extends EventEmitter<{ readonly #disposables = new DisposableStack(); readonly #userContexts = new Map<string, UserContext>(); readonly session: Session; + readonly #sharedWorkers = new Map<string, SharedWorkerRealm>(); // keep-sorted end private constructor(session: Session) { @@ -64,11 +65,6 @@ export class Browser extends EventEmitter<{ // keep-sorted start this.session = session; // keep-sorted end - - this.#userContexts.set( - UserContext.DEFAULT, - UserContext.create(this, UserContext.DEFAULT) - ); } async #initialize() { @@ -80,14 +76,29 @@ export class Browser extends EventEmitter<{ }); sessionEmitter.on('script.realmCreated', info => { - if (info.type === 'shared-worker') { - // TODO: Create a SharedWorkerRealm. + if (info.type !== 'shared-worker') { + return; } + this.#sharedWorkers.set( + info.realm, + SharedWorkerRealm.from(this, info.realm, info.origin) + ); }); + await this.#syncUserContexts(); await this.#syncBrowsingContexts(); } + async #syncUserContexts() { + const { + result: {userContexts}, + } = await this.session.send('browser.getUserContexts', {}); + + for (const context of userContexts) { + this.#createUserContext(context.userContext); + } + } + async #syncBrowsingContexts() { // In case contexts are created or destroyed during `getTree`, we use this // set to detect them. @@ -99,16 +110,13 @@ export class Browser extends EventEmitter<{ sessionEmitter.on('browsingContext.contextCreated', info => { contextIds.add(info.context); }); - sessionEmitter.on('browsingContext.contextDestroyed', info => { - contextIds.delete(info.context); - }); const {result} = await this.session.send('browsingContext.getTree', {}); contexts = result.contexts; } // Simulating events so contexts are created naturally. for (const info of contexts) { - if (contextIds.has(info.context)) { + if (!contextIds.has(info.context)) { this.session.emit('browsingContext.contextCreated', info); } if (info.children) { @@ -117,6 +125,22 @@ export class Browser extends EventEmitter<{ } } + #createUserContext(id: string) { + const userContext = UserContext.create(this, id); + this.#userContexts.set(userContext.id, userContext); + + const userContextEmitter = this.#disposables.use( + new EventEmitter(userContext) + ); + userContextEmitter.once('closed', () => { + userContextEmitter.removeAllListeners(); + + this.#userContexts.delete(userContext.id); + }); + + return userContext; + } + // keep-sorted start block=yes get closed(): boolean { return this.#closed; @@ -185,30 +209,15 @@ 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; + const { + result: {userContext: context}, + } = await this.session.send('browser.createUserContext', {}); + return this.#createUserContext(context); } [disposeSymbol](): void { diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/BrowsingContext.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/BrowsingContext.ts index 9bec2a506c..07309576a3 100644 --- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/BrowsingContext.ts +++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/BrowsingContext.ts @@ -12,6 +12,7 @@ import {DisposableStack, disposeSymbol} from '../../util/disposable.js'; import type {AddPreloadScriptOptions} from './Browser.js'; import {Navigation} from './Navigation.js'; +import type {DedicatedWorkerRealm} from './Realm.js'; import {WindowRealm} from './Realm.js'; import {Request} from './Request.js'; import type {UserContext} from './UserContext.js'; @@ -60,6 +61,14 @@ export type SetViewportOptions = Omit< /** * @internal */ +export type GetCookiesOptions = Omit< + Bidi.Storage.GetCookiesParameters, + 'partition' +>; + +/** + * @internal + */ export class BrowsingContext extends EventEmitter<{ /** Emitted when this context is closed. */ closed: { @@ -95,6 +104,11 @@ export class BrowsingContext extends EventEmitter<{ DOMContentLoaded: void; /** Emitted whenever the frame emits `load` */ load: void; + /** Emitted whenever a dedicated worker is created */ + worker: { + /** The realm for the new dedicated worker */ + realm: DedicatedWorkerRealm; + }; }> { static from( userContext: UserContext, @@ -135,7 +149,7 @@ export class BrowsingContext extends EventEmitter<{ this.userContext = context; // keep-sorted end - this.defaultRealm = WindowRealm.from(this); + this.defaultRealm = this.#createWindowRealm(); } #initialize() { @@ -202,7 +216,16 @@ export class BrowsingContext extends EventEmitter<{ } this.#url = info.url; - this.#requests.clear(); + for (const [id, request] of this.#requests) { + if (request.disposed) { + this.#requests.delete(id); + } + } + // If the navigation hasn't finished, then this is nested navigation. The + // current navigation will handle this. + if (this.#navigation !== undefined && !this.#navigation.disposed) { + return; + } // Note the navigation ID is null for this event. this.#navigation = Navigation.from(this); @@ -224,7 +247,8 @@ export class BrowsingContext extends EventEmitter<{ if (event.context !== this.id) { return; } - if (this.#requests.has(event.request.request)) { + if (event.redirectCount !== 0) { + // Means the request is a redirect. This is handled in Request. return; } @@ -265,7 +289,12 @@ export class BrowsingContext extends EventEmitter<{ return this.closed; } get realms(): Iterable<WindowRealm> { - return this.#realms.values(); + // eslint-disable-next-line @typescript-eslint/no-this-alias -- Required + const self = this; + return (function* () { + yield self.defaultRealm; + yield* self.#realms.values(); + })(); } get top(): BrowsingContext { let context = this as BrowsingContext; @@ -279,6 +308,14 @@ export class BrowsingContext extends EventEmitter<{ } // keep-sorted end + #createWindowRealm(sandbox?: string) { + const realm = WindowRealm.from(this, sandbox); + realm.on('worker', realm => { + this.emit('worker', {realm}); + }); + return realm; + } + @inertIfDisposed private dispose(reason?: string): void { this.#reason = reason; @@ -345,33 +382,23 @@ export class BrowsingContext extends EventEmitter<{ async navigate( url: string, wait?: Bidi.BrowsingContext.ReadinessState - ): Promise<Navigation> { + ): Promise<void> { await this.#session.send('browsingContext.navigate', { context: this.id, url, wait, }); - return await new Promise(resolve => { - this.once('navigation', ({navigation}) => { - resolve(navigation); - }); - }); } @throwIfDisposed<BrowsingContext>(context => { // SAFETY: Disposal implies this exists. return context.#reason!; }) - async reload(options: ReloadOptions = {}): Promise<Navigation> { + async reload(options: ReloadOptions = {}): Promise<void> { await this.#session.send('browsingContext.reload', { context: this.id, ...options, }); - return await new Promise(resolve => { - this.once('navigation', ({navigation}) => { - resolve(navigation); - }); - }); } @throwIfDisposed<BrowsingContext>(context => { @@ -436,7 +463,7 @@ export class BrowsingContext extends EventEmitter<{ return context.#reason!; }) createWindowRealm(sandbox: string): WindowRealm { - return WindowRealm.from(this, sandbox); + return this.#createWindowRealm(sandbox); } @throwIfDisposed<BrowsingContext>(context => { @@ -464,6 +491,54 @@ export class BrowsingContext extends EventEmitter<{ await this.userContext.browser.removePreloadScript(script); } + @throwIfDisposed<BrowsingContext>(context => { + // SAFETY: Disposal implies this exists. + return context.#reason!; + }) + async getCookies( + options: GetCookiesOptions = {} + ): Promise<Bidi.Network.Cookie[]> { + const { + result: {cookies}, + } = await this.#session.send('storage.getCookies', { + ...options, + partition: { + type: 'context', + context: this.id, + }, + }); + return cookies; + } + + @throwIfDisposed<BrowsingContext>(context => { + // SAFETY: Disposal implies this exists. + return context.#reason!; + }) + async setCookie(cookie: Bidi.Storage.PartialCookie): Promise<void> { + await this.#session.send('storage.setCookie', { + cookie, + partition: { + type: 'context', + context: this.id, + }, + }); + } + + @throwIfDisposed<BrowsingContext>(context => { + // SAFETY: Disposal implies this exists. + return context.#reason!; + }) + async setFiles( + element: Bidi.Script.SharedReference, + files: string[] + ): Promise<void> { + await this.#session.send('input.setFiles', { + context: this.id, + element, + files, + }); + } + [disposeSymbol](): void { this.#reason ??= 'Browsing context already closed, probably because the user context closed.'; @@ -472,4 +547,24 @@ export class BrowsingContext extends EventEmitter<{ this.#disposables.dispose(); super[disposeSymbol](); } + + @throwIfDisposed<BrowsingContext>(context => { + // SAFETY: Disposal implies this exists. + return context.#reason!; + }) + async deleteCookie( + ...cookieFilters: Bidi.Storage.CookieFilter[] + ): Promise<void> { + await Promise.all( + cookieFilters.map(async filter => { + await this.#session.send('storage.deleteCookies', { + filter: filter, + partition: { + type: 'context', + context: this.id, + }, + }); + }) + ); + } } diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Connection.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Connection.ts index b9de14372b..9c26a03503 100644 --- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Connection.ts +++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Connection.ts @@ -38,6 +38,21 @@ export interface Commands { returnType: Bidi.EmptyResult; }; + 'browser.createUserContext': { + params: Bidi.EmptyParams; + returnType: Bidi.Browser.CreateUserContextResult; + }; + 'browser.getUserContexts': { + params: Bidi.EmptyParams; + returnType: Bidi.Browser.GetUserContextsResult; + }; + 'browser.removeUserContext': { + params: { + userContext: Bidi.Browser.UserContext; + }; + returnType: Bidi.Browser.RemoveUserContext; + }; + 'browsingContext.activate': { params: Bidi.BrowsingContext.ActivateParameters; returnType: Bidi.EmptyResult; @@ -91,6 +106,15 @@ export interface Commands { params: Bidi.Input.ReleaseActionsParameters; returnType: Bidi.EmptyResult; }; + 'input.setFiles': { + params: Bidi.Input.SetFilesParameters; + returnType: Bidi.EmptyResult; + }; + + 'permissions.setPermission': { + params: Bidi.Permissions.SetPermissionParameters; + returnType: Bidi.EmptyResult; + }; 'session.end': { params: Bidi.EmptyParams; @@ -112,6 +136,19 @@ export interface Commands { params: Bidi.Session.SubscriptionRequest; returnType: Bidi.EmptyResult; }; + + 'storage.deleteCookies': { + params: Bidi.Storage.DeleteCookiesParameters; + returnType: Bidi.Storage.DeleteCookiesResult; + }; + 'storage.getCookies': { + params: Bidi.Storage.GetCookiesParameters; + returnType: Bidi.Storage.GetCookiesResult; + }; + 'storage.setCookie': { + params: Bidi.Storage.SetCookieParameters; + returnType: Bidi.Storage.SetCookieParameters; + }; } /** @@ -133,7 +170,4 @@ export interface Connection<Events extends BidiEvents = BidiEvents> method: T, params: Commands[T]['params'] ): Promise<{result: Commands[T]['returnType']}>; - - // This will pipe events into the provided emitter. - pipeTo<Events extends BidiEvents>(emitter: EventEmitter<Events>): void; } diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Navigation.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Navigation.ts index a7efbfeb2c..50040164a5 100644 --- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Navigation.ts +++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Navigation.ts @@ -41,9 +41,10 @@ export class Navigation extends EventEmitter<{ // keep-sorted start #request: Request | undefined; + #navigation: Navigation | undefined; readonly #browsingContext: BrowsingContext; readonly #disposables = new DisposableStack(); - readonly #id = new Deferred<string>(); + readonly #id = new Deferred<string | null>(); // keep-sorted end private constructor(context: BrowsingContext) { @@ -65,31 +66,48 @@ export class Navigation extends EventEmitter<{ this.dispose(); }); - this.#browsingContext.on('request', ({request}) => { - if (request.navigation === this.#id.value()) { - this.#request = request; - this.emit('request', request); + browsingContextEmitter.on('request', ({request}) => { + if ( + request.navigation === undefined || + this.#request !== undefined || + // If a request with a navigation ID comes in, then the navigation ID is + // for this navigation. + !this.#matches(request.navigation) + ) { + return; } + + this.#request = request; + this.emit('request', request); }); const sessionEmitter = this.#disposables.use( new EventEmitter(this.#session) ); - // To get the navigation ID if any. + sessionEmitter.on('browsingContext.navigationStarted', info => { + if ( + info.context !== this.#browsingContext.id || + this.#navigation !== undefined + ) { + return; + } + this.#navigation = Navigation.from(this.#browsingContext); + }); + for (const eventName of [ 'browsingContext.domContentLoaded', 'browsingContext.load', ] as const) { sessionEmitter.on(eventName, info => { - if (info.context !== this.#browsingContext.id) { - return; - } - if (!info.navigation) { + if ( + info.context !== this.#browsingContext.id || + info.navigation === null || + !this.#matches(info.navigation) + ) { return; } - if (!this.#id.resolved()) { - this.#id.resolve(info.navigation); - } + + this.dispose(); }); } @@ -99,18 +117,15 @@ export class Navigation extends EventEmitter<{ ['browsingContext.navigationAborted', 'aborted'], ] as const) { sessionEmitter.on(eventName, info => { - if (info.context !== this.#browsingContext.id) { - return; - } - if (!info.navigation) { - return; - } - if (!this.#id.resolved()) { - this.#id.resolve(info.navigation); - } - if (this.#id.value() !== info.navigation) { + if ( + info.context !== this.#browsingContext.id || + // Note we don't check if `navigation` is null since `null` means the + // fragment navigated. + !this.#matches(info.navigation) + ) { return; } + this.emit(event, { url: info.url, timestamp: new Date(info.timestamp), @@ -120,6 +135,17 @@ export class Navigation extends EventEmitter<{ } } + #matches(navigation: string | null): boolean { + if (this.#navigation !== undefined && !this.#navigation.disposed) { + return false; + } + if (!this.#id.resolved()) { + this.#id.resolve(navigation); + return true; + } + return this.#id.value() === navigation; + } + // keep-sorted start block=yes get #session() { return this.#browsingContext.userContext.browser.session; @@ -130,6 +156,9 @@ export class Navigation extends EventEmitter<{ get request(): Request | undefined { return this.#request; } + get navigation(): Navigation | undefined { + return this.#navigation; + } // keep-sorted end @inertIfDisposed diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Realm.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Realm.ts index d9bbbede50..392194cec8 100644 --- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Realm.ts +++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Realm.ts @@ -9,7 +9,9 @@ import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; import {EventEmitter} from '../../common/EventEmitter.js'; import {inertIfDisposed, throwIfDisposed} from '../../util/decorators.js'; import {DisposableStack, disposeSymbol} from '../../util/disposable.js'; +import type {BidiConnection} from '../Connection.js'; +import type {Browser} from './Browser.js'; import type {BrowsingContext} from './BrowsingContext.js'; import type {Session} from './Session.js'; @@ -33,6 +35,8 @@ export type EvaluateOptions = Omit< * @internal */ export abstract class Realm extends EventEmitter<{ + /** Emitted whenever the realm has updated. */ + updated: Realm; /** Emitted when the realm is destroyed. */ destroyed: {reason: string}; /** Emitted when a dedicated worker is created in the realm. */ @@ -55,22 +59,12 @@ export abstract class Realm extends EventEmitter<{ // keep-sorted end } - protected initialize(): void { - const sessionEmitter = this.disposables.use(new EventEmitter(this.session)); - sessionEmitter.on('script.realmDestroyed', info => { - if (info.realm !== this.id) { - return; - } - this.dispose('Realm already destroyed.'); - }); - } - // keep-sorted start block=yes get disposed(): boolean { return this.#reason !== undefined; } protected abstract get session(): Session; - protected get target(): Bidi.Script.Target { + get target(): Bidi.Script.Target { return {realm: this.id}; } // keep-sorted end @@ -128,6 +122,18 @@ export abstract class Realm extends EventEmitter<{ return result; } + @throwIfDisposed<Realm>(realm => { + // SAFETY: Disposal implies this exists. + return realm.#reason!; + }) + async resolveExecutionContextId(): Promise<number> { + const {result} = await (this.session.connection as BidiConnection).send( + 'cdp.resolveRealm', + {realm: this.id} + ); + return result.executionContextId; + } + [disposeSymbol](): void { this.#reason ??= 'Realm already destroyed, probably because all associated browsing contexts closed.'; @@ -144,7 +150,7 @@ export abstract class Realm extends EventEmitter<{ export class WindowRealm extends Realm { static from(context: BrowsingContext, sandbox?: string): WindowRealm { const realm = new WindowRealm(context, sandbox); - realm.initialize(); + realm.#initialize(); return realm; } @@ -153,13 +159,7 @@ export class WindowRealm extends Realm { readonly sandbox?: string; // keep-sorted end - readonly #workers: { - dedicated: Map<string, DedicatedWorkerRealm>; - shared: Map<string, SharedWorkerRealm>; - } = { - dedicated: new Map(), - shared: new Map(), - }; + readonly #workers = new Map<string, DedicatedWorkerRealm>(); private constructor(context: BrowsingContext, sandbox?: string) { super('', ''); @@ -169,16 +169,26 @@ export class WindowRealm extends Realm { // keep-sorted end } - override initialize(): void { - super.initialize(); + #initialize(): void { + const browsingContextEmitter = this.disposables.use( + new EventEmitter(this.browsingContext) + ); + browsingContextEmitter.on('closed', ({reason}) => { + this.dispose(reason); + }); const sessionEmitter = this.disposables.use(new EventEmitter(this.session)); sessionEmitter.on('script.realmCreated', info => { - if (info.type !== 'window') { + if ( + info.type !== 'window' || + info.context !== this.browsingContext.id || + info.sandbox !== this.sandbox + ) { return; } (this as any).id = info.realm; (this as any).origin = info.origin; + this.emit('updated', this); }); sessionEmitter.on('script.realmCreated', info => { if (info.type !== 'dedicated-worker') { @@ -189,32 +199,16 @@ export class WindowRealm extends Realm { } const realm = DedicatedWorkerRealm.from(this, info.realm, info.origin); - this.#workers.dedicated.set(realm.id, realm); + this.#workers.set(realm.id, realm); const realmEmitter = this.disposables.use(new EventEmitter(realm)); realmEmitter.once('destroyed', () => { realmEmitter.removeAllListeners(); - this.#workers.dedicated.delete(realm.id); + this.#workers.delete(realm.id); }); this.emit('worker', realm); }); - - this.browsingContext.userContext.browser.on('sharedworker', ({realm}) => { - if (!realm.owners.has(this)) { - return; - } - - this.#workers.shared.set(realm.id, realm); - - const realmEmitter = this.disposables.use(new EventEmitter(realm)); - realmEmitter.once('destroyed', () => { - realmEmitter.removeAllListeners(); - this.#workers.shared.delete(realm.id); - }); - - this.emit('sharedworker', realm); - }); } override get session(): Session { @@ -244,7 +238,7 @@ export class DedicatedWorkerRealm extends Realm { origin: string ): DedicatedWorkerRealm { const realm = new DedicatedWorkerRealm(owner, id, origin); - realm.initialize(); + realm.#initialize(); return realm; } @@ -262,10 +256,14 @@ export class DedicatedWorkerRealm extends Realm { this.owners = new Set([owner]); } - override initialize(): void { - super.initialize(); - + #initialize(): void { const sessionEmitter = this.disposables.use(new EventEmitter(this.session)); + sessionEmitter.on('script.realmDestroyed', info => { + if (info.realm !== this.id) { + return; + } + this.dispose('Realm already destroyed.'); + }); sessionEmitter.on('script.realmCreated', info => { if (info.type !== 'dedicated-worker') { return; @@ -296,34 +294,30 @@ export class DedicatedWorkerRealm extends Realm { * @internal */ export class SharedWorkerRealm extends Realm { - static from( - owners: [WindowRealm, ...WindowRealm[]], - id: string, - origin: string - ): SharedWorkerRealm { - const realm = new SharedWorkerRealm(owners, id, origin); - realm.initialize(); + static from(browser: Browser, id: string, origin: string): SharedWorkerRealm { + const realm = new SharedWorkerRealm(browser, id, origin); + realm.#initialize(); return realm; } // keep-sorted start readonly #workers = new Map<string, DedicatedWorkerRealm>(); - readonly owners: Set<WindowRealm>; + readonly browser: Browser; // keep-sorted end - private constructor( - owners: [WindowRealm, ...WindowRealm[]], - id: string, - origin: string - ) { + private constructor(browser: Browser, id: string, origin: string) { super(id, origin); - this.owners = new Set(owners); + this.browser = browser; } - override initialize(): void { - super.initialize(); - + #initialize(): void { const sessionEmitter = this.disposables.use(new EventEmitter(this.session)); + sessionEmitter.on('script.realmDestroyed', info => { + if (info.realm !== this.id) { + return; + } + this.dispose('Realm already destroyed.'); + }); sessionEmitter.on('script.realmCreated', info => { if (info.type !== 'dedicated-worker') { return; @@ -345,7 +339,6 @@ export class SharedWorkerRealm extends Realm { } override get session(): Session { - // SAFETY: At least one owner will exist. - return this.owners.values().next().value.session; + return this.browser.session; } } diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Request.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Request.ts index 2a445f7d87..fd616b668d 100644 --- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Request.ts +++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Request.ts @@ -66,10 +66,11 @@ export class Request extends EventEmitter<{ new EventEmitter(this.#session) ); sessionEmitter.on('network.beforeRequestSent', event => { - if (event.context !== this.#browsingContext.id) { - return; - } - if (event.request.request !== this.id) { + if ( + event.context !== this.#browsingContext.id || + event.request.request !== this.id || + event.redirectCount !== this.#event.redirectCount + 1 + ) { return; } this.#redirect = Request.from(this.#browsingContext, event); @@ -77,10 +78,11 @@ export class Request extends EventEmitter<{ this.dispose(); }); sessionEmitter.on('network.fetchError', event => { - if (event.context !== this.#browsingContext.id) { - return; - } - if (event.request.request !== this.id) { + if ( + event.context !== this.#browsingContext.id || + event.request.request !== this.id || + this.#event.redirectCount !== event.redirectCount + ) { return; } this.#error = event.errorText; @@ -88,14 +90,19 @@ export class Request extends EventEmitter<{ this.dispose(); }); sessionEmitter.on('network.responseCompleted', event => { - if (event.context !== this.#browsingContext.id) { - return; - } - if (event.request.request !== this.id) { + if ( + event.context !== this.#browsingContext.id || + event.request.request !== this.id || + this.#event.redirectCount !== event.redirectCount + ) { return; } this.#response = event.response; this.emit('success', this.#response); + // In case this is a redirect. + if (this.#response.status >= 300 && this.#response.status < 400) { + return; + } this.dispose(); }); } @@ -126,7 +133,7 @@ export class Request extends EventEmitter<{ return this.#event.navigation ?? undefined; } get redirect(): Request | undefined { - return this.redirect; + return this.#redirect; } get response(): Bidi.Network.ResponseData | undefined { return this.#response; diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Session.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Session.ts index b6e28061f1..ffd39769e7 100644 --- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Session.ts +++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Session.ts @@ -8,7 +8,11 @@ import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; import {EventEmitter} from '../../common/EventEmitter.js'; import {debugError} from '../../common/util.js'; -import {inertIfDisposed, throwIfDisposed} from '../../util/decorators.js'; +import { + bubble, + inertIfDisposed, + throwIfDisposed, +} from '../../util/decorators.js'; import {DisposableStack, disposeSymbol} from '../../util/disposable.js'; import {Browser} from './Browser.js'; @@ -81,7 +85,8 @@ export class Session readonly #disposables = new DisposableStack(); readonly #info: Bidi.Session.NewResult; readonly browser!: Browser; - readonly connection: Connection; + @bubble() + accessor connection: Connection; // keep-sorted end private constructor(connection: Connection, info: Bidi.Session.NewResult) { @@ -93,8 +98,6 @@ export class Session } async #initialize(): Promise<void> { - this.connection.pipeTo(this); - // SAFETY: We use `any` to allow assignment of the readonly property. (this as any).browser = await Browser.from(this); @@ -102,6 +105,19 @@ export class Session browserEmitter.once('closed', ({reason}) => { this.dispose(reason); }); + + // TODO: Currently, some implementations do not emit navigationStarted event + // for fragment navigations (as per spec) and some do. This could emits a + // synthetic navigationStarted to work around this inconsistency. + const seen = new WeakSet(); + this.on('browsingContext.fragmentNavigated', info => { + if (seen.has(info)) { + return; + } + seen.add(info); + this.emit('browsingContext.navigationStarted', info); + this.emit('browsingContext.fragmentNavigated', info); + }); } // keep-sorted start block=yes @@ -125,10 +141,6 @@ export class Session this[disposeSymbol](); } - pipeTo<Events extends BidiEvents>(emitter: EventEmitter<Events>): void { - this.connection.pipeTo(emitter); - } - /** * Currently, there is a 1:1 relationship between the session and the * session. In the future, we might support multiple sessions and in that diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/UserContext.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/UserContext.ts index 01ee5c7649..72859c6a53 100644 --- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/UserContext.ts +++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/UserContext.ts @@ -12,6 +12,7 @@ import {inertIfDisposed, throwIfDisposed} from '../../util/decorators.js'; import {DisposableStack, disposeSymbol} from '../../util/disposable.js'; import type {Browser} from './Browser.js'; +import type {GetCookiesOptions} from './BrowsingContext.js'; import {BrowsingContext} from './BrowsingContext.js'; /** @@ -43,7 +44,7 @@ export class UserContext extends EventEmitter<{ reason: string; }; }> { - static DEFAULT = 'default'; + static DEFAULT = 'default' as const; static create(browser: Browser, id: string): UserContext { const context = new UserContext(browser, id); @@ -84,6 +85,10 @@ export class UserContext extends EventEmitter<{ return; } + if (info.userContext !== this.#id) { + return; + } + const browsingContext = BrowsingContext.from( this, undefined, @@ -143,6 +148,7 @@ export class UserContext extends EventEmitter<{ type, ...options, referenceContext: options.referenceContext?.id, + userContext: this.#id, }); const browsingContext = this.#browsingContexts.get(contextId); @@ -161,12 +167,71 @@ export class UserContext extends EventEmitter<{ }) async remove(): Promise<void> { try { - // TODO: Call `removeUserContext` once available. + await this.#session.send('browser.removeUserContext', { + userContext: this.#id, + }); } finally { this.dispose('User context already closed.'); } } + @throwIfDisposed<UserContext>(context => { + // SAFETY: Disposal implies this exists. + return context.#reason!; + }) + async getCookies( + options: GetCookiesOptions = {}, + sourceOrigin: string | undefined = undefined + ): Promise<Bidi.Network.Cookie[]> { + const { + result: {cookies}, + } = await this.#session.send('storage.getCookies', { + ...options, + partition: { + type: 'storageKey', + userContext: this.#id, + sourceOrigin, + }, + }); + return cookies; + } + + @throwIfDisposed<UserContext>(context => { + // SAFETY: Disposal implies this exists. + return context.#reason!; + }) + async setCookie( + cookie: Bidi.Storage.PartialCookie, + sourceOrigin?: string + ): Promise<void> { + await this.#session.send('storage.setCookie', { + cookie, + partition: { + type: 'storageKey', + sourceOrigin, + userContext: this.id, + }, + }); + } + + @throwIfDisposed<UserContext>(context => { + // SAFETY: Disposal implies this exists. + return context.#reason!; + }) + async setPermissions( + origin: string, + descriptor: Bidi.Permissions.PermissionDescriptor, + state: Bidi.Permissions.PermissionState + ): Promise<void> { + await this.#session.send('permissions.setPermission', { + origin, + descriptor, + state, + // @ts-expect-error not standard implementation. + 'goog:userContext': this.#id, + }); + } + [disposeSymbol](): void { this.#reason ??= 'User context already closed, probably because the browser disconnected/closed.'; |