summaryrefslogtreecommitdiffstats
path: root/remote/test/puppeteer/packages/puppeteer-core/src/bidi
diff options
context:
space:
mode:
Diffstat (limited to 'remote/test/puppeteer/packages/puppeteer-core/src/bidi')
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/Browser.ts158
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/BrowserContext.ts245
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/BrowsingContext.ts187
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/CDPSession.ts103
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/Connection.ts59
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/Deserializer.ts58
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/Dialog.ts32
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/ElementHandle.ts72
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/EmulationManager.ts35
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/ExposedFunction.ts363
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/Frame.ts669
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/HTTPRequest.ts126
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/HTTPResponse.ts84
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/Input.ts341
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/JSHandle.ts34
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/NetworkManager.ts155
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/Page.ts846
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/Realm.ts344
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/Sandbox.ts123
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/Serializer.ts110
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/Target.ts173
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/WebWorker.ts48
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/bidi.ts4
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Browser.ts71
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/BrowsingContext.ts129
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Connection.ts40
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Navigation.ts75
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Realm.ts119
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Request.ts33
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/Session.ts28
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/core/UserContext.ts69
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/lifecycle.ts119
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/util.ts43
33 files changed, 2557 insertions, 2538 deletions
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Browser.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Browser.ts
index 42979790c9..8798d8325d 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Browser.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Browser.ts
@@ -8,6 +8,7 @@ import type {ChildProcess} from 'child_process';
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+import type {BrowserEvents} from '../api/Browser.js';
import {
Browser,
BrowserEvent,
@@ -19,22 +20,17 @@ import {BrowserContextEvent} from '../api/BrowserContext.js';
import type {Page} from '../api/Page.js';
import type {Target} from '../api/Target.js';
import {UnsupportedOperation} from '../common/Errors.js';
-import type {Handler} from '../common/EventEmitter.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 {BidiBrowserContext} from './BrowserContext.js';
-import {BrowsingContext, BrowsingContextEvent} from './BrowsingContext.js';
import type {BidiConnection} from './Connection.js';
import type {Browser as BrowserCore} from './core/Browser.js';
import {Session} from './core/Session.js';
import type {UserContext} from './core/UserContext.js';
-import {
- BiDiBrowserTarget,
- BiDiBrowsingContextTarget,
- BiDiPageTarget,
- type BidiTarget,
-} from './Target.js';
+import {BidiBrowserTarget} from './Target.js';
/**
* @internal
@@ -89,28 +85,18 @@ export class BidiBrowser extends Browser {
const browser = new BidiBrowser(session.browser, opts);
browser.#initialize();
- await browser.#getTree();
return browser;
}
+ @bubble()
+ accessor #trustedEmitter = new EventEmitter<BrowserEvents>();
+
#process?: ChildProcess;
#closeCallback?: BrowserCloseCallback;
#browserCore: BrowserCore;
#defaultViewport: Viewport | null;
- #targets = new Map<string, BidiTarget>();
#browserContexts = new WeakMap<UserContext, BidiBrowserContext>();
- #browserTarget: BiDiBrowserTarget;
-
- #connectionEventHandlers = new Map<
- Bidi.BrowsingContextEvent['method'],
- Handler<any>
- >([
- ['browsingContext.contextCreated', this.#onContextCreated.bind(this)],
- ['browsingContext.contextDestroyed', this.#onContextDestroyed.bind(this)],
- ['browsingContext.domContentLoaded', this.#onContextDomLoaded.bind(this)],
- ['browsingContext.fragmentNavigated', this.#onContextNavigation.bind(this)],
- ['browsingContext.navigationStarted', this.#onContextNavigation.bind(this)],
- ]);
+ #target = new BidiBrowserTarget(this);
private constructor(browserCore: BrowserCore, opts: BidiBrowserOptions) {
super();
@@ -118,22 +104,22 @@ export class BidiBrowser extends Browser {
this.#closeCallback = opts.closeCallback;
this.#browserCore = browserCore;
this.#defaultViewport = opts.defaultViewport;
- this.#browserTarget = new BiDiBrowserTarget(this);
- this.#createBrowserContext(this.#browserCore.defaultUserContext);
}
#initialize() {
+ // Initializing existing contexts.
+ for (const userContext of this.#browserCore.userContexts) {
+ this.#createBrowserContext(userContext);
+ }
+
this.#browserCore.once('disconnected', () => {
- this.emit(BrowserEvent.Disconnected, undefined);
+ this.#trustedEmitter.emit(BrowserEvent.Disconnected, undefined);
+ this.#trustedEmitter.removeAllListeners();
});
this.#process?.once('close', () => {
this.#browserCore.dispose('Browser process exited.', true);
this.connection.dispose();
});
-
- for (const [eventName, handler] of this.#connectionEventHandlers) {
- this.connection.on(eventName, handler);
- }
}
get #browserName() {
@@ -143,82 +129,40 @@ export class BidiBrowser extends Browser {
return this.#browserCore.session.capabilities.browserVersion;
}
+ get cdpSupported(): boolean {
+ return !this.#browserName.toLocaleLowerCase().includes('firefox');
+ }
+
override userAgent(): never {
throw new UnsupportedOperation();
}
#createBrowserContext(userContext: UserContext) {
- const browserContext = new BidiBrowserContext(this, userContext, {
+ const browserContext = BidiBrowserContext.from(this, userContext, {
defaultViewport: this.#defaultViewport,
});
this.#browserContexts.set(userContext, browserContext);
- return browserContext;
- }
-
- #onContextDomLoaded(event: Bidi.BrowsingContext.Info) {
- const target = this.#targets.get(event.context);
- if (target) {
- this.emit(BrowserEvent.TargetChanged, target);
- }
- }
-
- #onContextNavigation(event: Bidi.BrowsingContext.NavigationInfo) {
- const target = this.#targets.get(event.context);
- if (target) {
- this.emit(BrowserEvent.TargetChanged, target);
- target.browserContext().emit(BrowserContextEvent.TargetChanged, target);
- }
- }
- #onContextCreated(event: Bidi.BrowsingContext.ContextCreated['params']) {
- const context = new BrowsingContext(
- this.connection,
- event,
- this.#browserName
+ browserContext.trustedEmitter.on(
+ BrowserContextEvent.TargetCreated,
+ target => {
+ this.#trustedEmitter.emit(BrowserEvent.TargetCreated, target);
+ }
+ );
+ browserContext.trustedEmitter.on(
+ BrowserContextEvent.TargetChanged,
+ target => {
+ this.#trustedEmitter.emit(BrowserEvent.TargetChanged, target);
+ }
+ );
+ browserContext.trustedEmitter.on(
+ BrowserContextEvent.TargetDestroyed,
+ target => {
+ this.#trustedEmitter.emit(BrowserEvent.TargetDestroyed, target);
+ }
);
- this.connection.registerBrowsingContexts(context);
- // TODO: once more browsing context types are supported, this should be
- // updated to support those. Currently, all top-level contexts are treated
- // as pages.
- const browserContext = this.browserContexts().at(-1);
- if (!browserContext) {
- throw new Error('Missing browser contexts');
- }
- const target = !context.parent
- ? new BiDiPageTarget(browserContext, context)
- : new BiDiBrowsingContextTarget(browserContext, context);
- this.#targets.set(event.context, target);
-
- this.emit(BrowserEvent.TargetCreated, target);
- target.browserContext().emit(BrowserContextEvent.TargetCreated, target);
-
- if (context.parent) {
- const topLevel = this.connection.getTopLevelContext(context.parent);
- topLevel.emit(BrowsingContextEvent.Created, context);
- }
- }
-
- async #getTree(): Promise<void> {
- const {result} = await this.connection.send('browsingContext.getTree', {});
- for (const context of result.contexts) {
- this.#onContextCreated(context);
- }
- }
- async #onContextDestroyed(
- event: Bidi.BrowsingContext.ContextDestroyed['params']
- ) {
- const context = this.connection.getBrowsingContext(event.context);
- const topLevelContext = this.connection.getTopLevelContext(event.context);
- topLevelContext.emit(BrowsingContextEvent.Destroyed, context);
- const target = this.#targets.get(event.context);
- const page = await target?.page();
- await page?.close().catch(debugError);
- this.#targets.delete(event.context);
- if (target) {
- this.emit(BrowserEvent.TargetDestroyed, target);
- target.browserContext().emit(BrowserContextEvent.TargetDestroyed, target);
- }
+ return browserContext;
}
get connection(): BidiConnection {
@@ -231,9 +175,6 @@ export class BidiBrowser extends Browser {
}
override async close(): Promise<void> {
- for (const [eventName, handler] of this.#connectionEventHandlers) {
- this.connection.off(eventName, handler);
- }
if (this.connection.closed) {
return;
}
@@ -250,14 +191,14 @@ export class BidiBrowser extends Browser {
}
override get connected(): boolean {
- return !this.#browserCore.disposed;
+ return !this.#browserCore.disconnected;
}
override process(): ChildProcess | null {
return this.#process ?? null;
}
- override async createIncognitoBrowserContext(
+ override async createBrowserContext(
_options?: BrowserContextOptions
): Promise<BidiBrowserContext> {
const userContext = await this.#browserCore.createUserContext();
@@ -283,19 +224,16 @@ export class BidiBrowser extends Browser {
}
override targets(): Target[] {
- return [this.#browserTarget, ...Array.from(this.#targets.values())];
- }
-
- _getTargetById(id: string): BidiTarget {
- const target = this.#targets.get(id);
- if (!target) {
- throw new Error('Target not found');
- }
- return target;
+ return [
+ this.#target,
+ ...this.browserContexts().flatMap(context => {
+ return context.targets();
+ }),
+ ];
}
- override target(): Target {
- return this.#browserTarget;
+ override target(): BidiBrowserTarget {
+ return this.#target;
}
override async disconnect(): Promise<void> {
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;
}
}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/BrowsingContext.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/BrowsingContext.ts
deleted file mode 100644
index 0804628c06..0000000000
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/BrowsingContext.ts
+++ /dev/null
@@ -1,187 +0,0 @@
-import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
-import type ProtocolMapping from 'devtools-protocol/types/protocol-mapping.js';
-
-import {CDPSession} from '../api/CDPSession.js';
-import type {Connection as CdpConnection} from '../cdp/Connection.js';
-import {TargetCloseError, UnsupportedOperation} from '../common/Errors.js';
-import type {EventType} from '../common/EventEmitter.js';
-import {debugError} from '../common/util.js';
-import {Deferred} from '../util/Deferred.js';
-
-import type {BidiConnection} from './Connection.js';
-import {BidiRealm} from './Realm.js';
-
-/**
- * @internal
- */
-export const cdpSessions = new Map<string, CdpSessionWrapper>();
-
-/**
- * @internal
- */
-export class CdpSessionWrapper extends CDPSession {
- #context: BrowsingContext;
- #sessionId = Deferred.create<string>();
- #detached = false;
-
- constructor(context: BrowsingContext, sessionId?: string) {
- super();
- this.#context = context;
- if (!this.#context.supportsCdp()) {
- return;
- }
- if (sessionId) {
- this.#sessionId.resolve(sessionId);
- cdpSessions.set(sessionId, this);
- } else {
- context.connection
- .send('cdp.getSession', {
- context: context.id,
- })
- .then(session => {
- this.#sessionId.resolve(session.result.session!);
- cdpSessions.set(session.result.session!, this);
- })
- .catch(err => {
- this.#sessionId.reject(err);
- });
- }
- }
-
- override connection(): CdpConnection | undefined {
- return undefined;
- }
-
- override async send<T extends keyof ProtocolMapping.Commands>(
- method: T,
- ...paramArgs: ProtocolMapping.Commands[T]['paramsType']
- ): Promise<ProtocolMapping.Commands[T]['returnType']> {
- if (!this.#context.supportsCdp()) {
- throw new UnsupportedOperation(
- 'CDP support is required for this feature. The current browser does not support CDP.'
- );
- }
- if (this.#detached) {
- throw new TargetCloseError(
- `Protocol error (${method}): Session closed. Most likely the page has been closed.`
- );
- }
- const session = await this.#sessionId.valueOrThrow();
- const {result} = await this.#context.connection.send('cdp.sendCommand', {
- method: method,
- params: paramArgs[0],
- session,
- });
- return result.result;
- }
-
- override async detach(): Promise<void> {
- cdpSessions.delete(this.id());
- if (!this.#detached && this.#context.supportsCdp()) {
- await this.#context.cdpSession.send('Target.detachFromTarget', {
- sessionId: this.id(),
- });
- }
- this.#detached = true;
- }
-
- override id(): string {
- const val = this.#sessionId.value();
- return val instanceof Error || val === undefined ? '' : val;
- }
-}
-
-/**
- * Internal events that the BrowsingContext class emits.
- *
- * @internal
- */
-// eslint-disable-next-line @typescript-eslint/no-namespace
-export namespace BrowsingContextEvent {
- /**
- * Emitted on the top-level context, when a descendant context is created.
- */
- export const Created = Symbol('BrowsingContext.created');
- /**
- * Emitted on the top-level context, when a descendant context or the
- * top-level context itself is destroyed.
- */
- export const Destroyed = Symbol('BrowsingContext.destroyed');
-}
-
-/**
- * @internal
- */
-export interface BrowsingContextEvents extends Record<EventType, unknown> {
- [BrowsingContextEvent.Created]: BrowsingContext;
- [BrowsingContextEvent.Destroyed]: BrowsingContext;
-}
-
-/**
- * @internal
- */
-export class BrowsingContext extends BidiRealm {
- #id: string;
- #url: string;
- #cdpSession: CDPSession;
- #parent?: string | null;
- #browserName = '';
-
- constructor(
- connection: BidiConnection,
- info: Bidi.BrowsingContext.Info,
- browserName: string
- ) {
- super(connection);
- this.#id = info.context;
- this.#url = info.url;
- this.#parent = info.parent;
- this.#browserName = browserName;
- this.#cdpSession = new CdpSessionWrapper(this, undefined);
-
- this.on('browsingContext.domContentLoaded', this.#updateUrl.bind(this));
- this.on('browsingContext.fragmentNavigated', this.#updateUrl.bind(this));
- this.on('browsingContext.load', this.#updateUrl.bind(this));
- }
-
- supportsCdp(): boolean {
- return !this.#browserName.toLowerCase().includes('firefox');
- }
-
- #updateUrl(info: Bidi.BrowsingContext.NavigationInfo) {
- this.#url = info.url;
- }
-
- createRealmForSandbox(): BidiRealm {
- return new BidiRealm(this.connection);
- }
-
- get url(): string {
- return this.#url;
- }
-
- get id(): string {
- return this.#id;
- }
-
- get parent(): string | undefined | null {
- return this.#parent;
- }
-
- get cdpSession(): CDPSession {
- return this.#cdpSession;
- }
-
- async sendCdpCommand<T extends keyof ProtocolMapping.Commands>(
- method: T,
- ...paramArgs: ProtocolMapping.Commands[T]['paramsType']
- ): Promise<ProtocolMapping.Commands[T]['returnType']> {
- return await this.#cdpSession.send(method, ...paramArgs);
- }
-
- dispose(): void {
- this.removeAllListeners();
- this.connection.unregisterBrowsingContexts(this.#id);
- void this.#cdpSession.detach().catch(debugError);
- }
-}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/CDPSession.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/CDPSession.ts
new file mode 100644
index 0000000000..1e0c503498
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/CDPSession.ts
@@ -0,0 +1,103 @@
+/**
+ * @license
+ * Copyright 2024 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import type ProtocolMapping from 'devtools-protocol/types/protocol-mapping.js';
+
+import {CDPSession} from '../api/CDPSession.js';
+import type {Connection as CdpConnection} from '../cdp/Connection.js';
+import {TargetCloseError, UnsupportedOperation} from '../common/Errors.js';
+import {Deferred} from '../util/Deferred.js';
+
+import type {BidiConnection} from './Connection.js';
+import type {BidiFrame} from './Frame.js';
+
+/**
+ * @internal
+ */
+export class BidiCdpSession extends CDPSession {
+ static sessions = new Map<string, BidiCdpSession>();
+
+ #detached = false;
+ readonly #connection: BidiConnection | undefined = undefined;
+ readonly #sessionId = Deferred.create<string>();
+ readonly frame: BidiFrame;
+
+ constructor(frame: BidiFrame, sessionId?: string) {
+ super();
+ this.frame = frame;
+ if (!this.frame.page().browser().cdpSupported) {
+ return;
+ }
+
+ const connection = this.frame.page().browser().connection;
+ this.#connection = connection;
+
+ if (sessionId) {
+ this.#sessionId.resolve(sessionId);
+ BidiCdpSession.sessions.set(sessionId, this);
+ } else {
+ (async () => {
+ try {
+ const session = await connection.send('cdp.getSession', {
+ context: frame._id,
+ });
+ this.#sessionId.resolve(session.result.session!);
+ BidiCdpSession.sessions.set(session.result.session!, this);
+ } catch (error) {
+ this.#sessionId.reject(error as Error);
+ }
+ })();
+ }
+
+ // SAFETY: We never throw #sessionId.
+ BidiCdpSession.sessions.set(this.#sessionId.value() as string, this);
+ }
+
+ override connection(): CdpConnection | undefined {
+ return undefined;
+ }
+
+ override async send<T extends keyof ProtocolMapping.Commands>(
+ method: T,
+ params?: ProtocolMapping.Commands[T]['paramsType'][0]
+ ): Promise<ProtocolMapping.Commands[T]['returnType']> {
+ if (this.#connection === undefined) {
+ throw new UnsupportedOperation(
+ 'CDP support is required for this feature. The current browser does not support CDP.'
+ );
+ }
+ if (this.#detached) {
+ throw new TargetCloseError(
+ `Protocol error (${method}): Session closed. Most likely the page has been closed.`
+ );
+ }
+ const session = await this.#sessionId.valueOrThrow();
+ const {result} = await this.#connection.send('cdp.sendCommand', {
+ method: method,
+ params: params,
+ session,
+ });
+ return result.result;
+ }
+
+ override async detach(): Promise<void> {
+ if (this.#connection === undefined || this.#detached) {
+ return;
+ }
+ try {
+ await this.frame.client.send('Target.detachFromTarget', {
+ sessionId: this.id(),
+ });
+ } finally {
+ BidiCdpSession.sessions.delete(this.id());
+ this.#detached = true;
+ }
+ }
+
+ override id(): string {
+ const value = this.#sessionId.value();
+ return typeof value === 'string' ? value : '';
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Connection.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Connection.ts
index bce952ba39..dd688c309a 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Connection.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Connection.ts
@@ -14,10 +14,10 @@ import {EventEmitter} from '../common/EventEmitter.js';
import {debugError} from '../common/util.js';
import {assert} from '../util/assert.js';
-import {cdpSessions, type BrowsingContext} from './BrowsingContext.js';
+import {BidiCdpSession} from './CDPSession.js';
import type {
- BidiEvents,
Commands as BidiCommands,
+ BidiEvents,
Connection,
} from './core/Connection.js';
@@ -36,6 +36,10 @@ export interface Commands extends BidiCommands {
params: Bidi.Cdp.GetSessionParameters;
returnType: Bidi.Cdp.GetSessionResult;
};
+ 'cdp.resolveRealm': {
+ params: Bidi.Cdp.ResolveRealmParameters;
+ returnType: Bidi.Cdp.ResolveRealmResult;
+ };
}
/**
@@ -51,7 +55,6 @@ export class BidiConnection
#timeout? = 0;
#closed = false;
#callbacks = new CallbackRegistry();
- #browsingContexts = new Map<string, BrowsingContext>();
#emitters: Array<EventEmitter<any>> = [];
constructor(
@@ -137,12 +140,11 @@ export class BidiConnection
return;
case 'event':
if (isCdpEvent(object)) {
- cdpSessions
+ BidiCdpSession.sessions
.get(object.params.session)
?.emit(object.params.event, object.params.params);
return;
}
- this.#maybeEmitOnContext(object);
// SAFETY: We know the method and parameter still match here.
this.emit(
object.method,
@@ -163,52 +165,6 @@ export class BidiConnection
debugError(object);
}
- #maybeEmitOnContext(event: Bidi.ChromiumBidi.Event) {
- let context: BrowsingContext | undefined;
- // Context specific events
- if ('context' in event.params && event.params.context !== null) {
- context = this.#browsingContexts.get(event.params.context);
- // `log.entryAdded` specific context
- } else if (
- 'source' in event.params &&
- event.params.source.context !== undefined
- ) {
- context = this.#browsingContexts.get(event.params.source.context);
- }
- context?.emit(event.method, event.params);
- }
-
- registerBrowsingContexts(context: BrowsingContext): void {
- this.#browsingContexts.set(context.id, context);
- }
-
- getBrowsingContext(contextId: string): BrowsingContext {
- const currentContext = this.#browsingContexts.get(contextId);
- if (!currentContext) {
- throw new Error(`BrowsingContext ${contextId} does not exist.`);
- }
- return currentContext;
- }
-
- getTopLevelContext(contextId: string): BrowsingContext {
- let currentContext = this.#browsingContexts.get(contextId);
- if (!currentContext) {
- throw new Error(`BrowsingContext ${contextId} does not exist.`);
- }
- while (currentContext.parent) {
- contextId = currentContext.parent;
- currentContext = this.#browsingContexts.get(contextId);
- if (!currentContext) {
- throw new Error(`BrowsingContext ${contextId} does not exist.`);
- }
- }
- return currentContext;
- }
-
- unregisterBrowsingContexts(id: string): void {
- this.#browsingContexts.delete(id);
- }
-
/**
* Unbinds the connection, but keeps the transport open. Useful when the transport will
* be reused by other connection e.g. with different protocol.
@@ -223,7 +179,6 @@ export class BidiConnection
this.#transport.onmessage = () => {};
this.#transport.onclose = () => {};
- this.#browsingContexts.clear();
this.#callbacks.clear();
}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Deserializer.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Deserializer.ts
index 14b87d403b..20dc8d9fc9 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Deserializer.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Deserializer.ts
@@ -12,40 +12,30 @@ import {debugError} from '../common/util.js';
* @internal
*/
export class BidiDeserializer {
- static deserializeNumber(value: Bidi.Script.SpecialNumber | number): number {
- switch (value) {
- case '-0':
- return -0;
- case 'NaN':
- return NaN;
- case 'Infinity':
- return Infinity;
- case '-Infinity':
- return -Infinity;
- default:
- return value;
+ static deserialize(result: Bidi.Script.RemoteValue): any {
+ if (!result) {
+ debugError('Service did not produce a result.');
+ return undefined;
}
- }
- static deserializeLocalValue(result: Bidi.Script.RemoteValue): unknown {
switch (result.type) {
case 'array':
return result.value?.map(value => {
- return BidiDeserializer.deserializeLocalValue(value);
+ return this.deserialize(value);
});
case 'set':
return result.value?.reduce((acc: Set<unknown>, value) => {
- return acc.add(BidiDeserializer.deserializeLocalValue(value));
+ return acc.add(this.deserialize(value));
}, new Set());
case 'object':
return result.value?.reduce((acc: Record<any, unknown>, tuple) => {
- const {key, value} = BidiDeserializer.deserializeTuple(tuple);
+ const {key, value} = this.#deserializeTuple(tuple);
acc[key as any] = value;
return acc;
}, {});
case 'map':
return result.value?.reduce((acc: Map<unknown, unknown>, tuple) => {
- const {key, value} = BidiDeserializer.deserializeTuple(tuple);
+ const {key, value} = this.#deserializeTuple(tuple);
return acc.set(key, value);
}, new Map());
case 'promise':
@@ -59,7 +49,7 @@ export class BidiDeserializer {
case 'null':
return null;
case 'number':
- return BidiDeserializer.deserializeNumber(result.value);
+ return this.#deserializeNumber(result.value);
case 'bigint':
return BigInt(result.value);
case 'boolean':
@@ -72,25 +62,31 @@ export class BidiDeserializer {
return undefined;
}
- static deserializeTuple([serializedKey, serializedValue]: [
+ static #deserializeNumber(value: Bidi.Script.SpecialNumber | number): number {
+ switch (value) {
+ case '-0':
+ return -0;
+ case 'NaN':
+ return NaN;
+ case 'Infinity':
+ return Infinity;
+ case '-Infinity':
+ return -Infinity;
+ default:
+ return value;
+ }
+ }
+
+ static #deserializeTuple([serializedKey, serializedValue]: [
Bidi.Script.RemoteValue | string,
Bidi.Script.RemoteValue,
]): {key: unknown; value: unknown} {
const key =
typeof serializedKey === 'string'
? serializedKey
- : BidiDeserializer.deserializeLocalValue(serializedKey);
- const value = BidiDeserializer.deserializeLocalValue(serializedValue);
+ : this.deserialize(serializedKey);
+ const value = this.deserialize(serializedValue);
return {key, value};
}
-
- static deserialize(result: Bidi.Script.RemoteValue): any {
- if (!result) {
- debugError('Service did not produce a result.');
- return undefined;
- }
-
- return BidiDeserializer.deserializeLocalValue(result);
- }
}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Dialog.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Dialog.ts
index ce22223461..1774a29f6b 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Dialog.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Dialog.ts
@@ -4,40 +4,26 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
-
import {Dialog} from '../api/Dialog.js';
-import type {BrowsingContext} from './BrowsingContext.js';
+import type {UserPrompt} from './core/UserPrompt.js';
-/**
- * @internal
- */
export class BidiDialog extends Dialog {
- #context: BrowsingContext;
+ static from(prompt: UserPrompt): BidiDialog {
+ return new BidiDialog(prompt);
+ }
- /**
- * @internal
- */
- constructor(
- context: BrowsingContext,
- type: Bidi.BrowsingContext.UserPromptOpenedParameters['type'],
- message: string,
- defaultValue?: string
- ) {
- super(type, message, defaultValue);
- this.#context = context;
+ #prompt: UserPrompt;
+ private constructor(prompt: UserPrompt) {
+ super(prompt.info.type, prompt.info.message, prompt.info.defaultValue);
+ this.#prompt = prompt;
}
- /**
- * @internal
- */
override async handle(options: {
accept: boolean;
text?: string;
}): Promise<void> {
- await this.#context.connection.send('browsingContext.handleUserPrompt', {
- context: this.#context.id,
+ await this.#prompt.handle({
accept: options.accept,
userText: options.text,
});
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/ElementHandle.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/ElementHandle.ts
index fd886e8c26..4263697671 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/ElementHandle.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/ElementHandle.ts
@@ -6,14 +6,12 @@
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
-import {type AutofillData, ElementHandle} from '../api/ElementHandle.js';
-import {UnsupportedOperation} from '../common/Errors.js';
+import {ElementHandle, type AutofillData} from '../api/ElementHandle.js';
import {throwIfDisposed} from '../util/decorators.js';
import type {BidiFrame} from './Frame.js';
import {BidiJSHandle} from './JSHandle.js';
-import type {BidiRealm} from './Realm.js';
-import type {Sandbox} from './Sandbox.js';
+import type {BidiFrameRealm} from './Realm.js';
/**
* @internal
@@ -21,28 +19,28 @@ import type {Sandbox} from './Sandbox.js';
export class BidiElementHandle<
ElementType extends Node = Element,
> extends ElementHandle<ElementType> {
+ static from<ElementType extends Node = Element>(
+ value: Bidi.Script.RemoteValue,
+ realm: BidiFrameRealm
+ ): BidiElementHandle<ElementType> {
+ return new BidiElementHandle(value, realm);
+ }
+
declare handle: BidiJSHandle<ElementType>;
- constructor(sandbox: Sandbox, remoteValue: Bidi.Script.RemoteValue) {
- super(new BidiJSHandle(sandbox, remoteValue));
+ constructor(value: Bidi.Script.RemoteValue, realm: BidiFrameRealm) {
+ super(BidiJSHandle.from(value, realm));
}
- override get realm(): Sandbox {
- return this.handle.realm;
+ override get realm(): BidiFrameRealm {
+ // SAFETY: See the super call in the constructor.
+ return this.handle.realm as BidiFrameRealm;
}
override get frame(): BidiFrame {
return this.realm.environment;
}
- context(): BidiRealm {
- return this.handle.context();
- }
-
- get isPrimitiveValue(): boolean {
- return this.handle.isPrimitiveValue;
- }
-
remoteValue(): Bidi.Script.RemoteValue {
return this.handle.remoteValue();
}
@@ -69,19 +67,53 @@ export class BidiElementHandle<
@ElementHandle.bindIsolatedHandle
override async contentFrame(): Promise<BidiFrame | null> {
using handle = (await this.evaluateHandle(element => {
- if (element instanceof HTMLIFrameElement) {
+ if (
+ element instanceof HTMLIFrameElement ||
+ element instanceof HTMLFrameElement
+ ) {
return element.contentWindow;
}
return;
})) as BidiJSHandle;
const value = handle.remoteValue();
if (value.type === 'window') {
- return this.frame.page().frame(value.value.context);
+ return (
+ this.frame
+ .page()
+ .frames()
+ .find(frame => {
+ return frame._id === value.value.context;
+ }) ?? null
+ );
}
return null;
}
- override uploadFile(this: ElementHandle<HTMLInputElement>): never {
- throw new UnsupportedOperation();
+ override async uploadFile(
+ this: BidiElementHandle<HTMLInputElement>,
+ ...files: string[]
+ ): Promise<void> {
+ // Locate all files and confirm that they exist.
+ // eslint-disable-next-line @typescript-eslint/consistent-type-imports
+ let path: typeof import('path');
+ try {
+ path = await import('path');
+ } catch (error) {
+ if (error instanceof TypeError) {
+ throw new Error(
+ `JSHandle#uploadFile can only be used in Node-like environments.`
+ );
+ }
+ throw error;
+ }
+
+ files = files.map(file => {
+ if (path.win32.isAbsolute(file) || path.posix.isAbsolute(file)) {
+ return file;
+ } else {
+ return path.resolve(file);
+ }
+ });
+ await this.frame.setFiles(this, files);
}
}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/EmulationManager.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/EmulationManager.ts
deleted file mode 100644
index de95695785..0000000000
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/EmulationManager.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-/**
- * @license
- * Copyright 2023 Google Inc.
- * SPDX-License-Identifier: Apache-2.0
- */
-import type {Viewport} from '../common/Viewport.js';
-
-import type {BrowsingContext} from './BrowsingContext.js';
-
-/**
- * @internal
- */
-export class EmulationManager {
- #browsingContext: BrowsingContext;
-
- constructor(browsingContext: BrowsingContext) {
- this.#browsingContext = browsingContext;
- }
-
- async emulateViewport(viewport: Viewport): Promise<void> {
- await this.#browsingContext.connection.send('browsingContext.setViewport', {
- context: this.#browsingContext.id,
- viewport:
- viewport.width && viewport.height
- ? {
- width: viewport.width,
- height: viewport.height,
- }
- : null,
- devicePixelRatio: viewport.deviceScaleFactor
- ? viewport.deviceScaleFactor
- : null,
- });
- }
-}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/ExposedFunction.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/ExposedFunction.ts
index 62c6b5e37e..f6e1304a55 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/ExposedFunction.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/ExposedFunction.ts
@@ -6,97 +6,91 @@
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+import {EventEmitter} from '../common/EventEmitter.js';
import type {Awaitable, FlattenHandle} from '../common/types.js';
import {debugError} from '../common/util.js';
-import {assert} from '../util/assert.js';
-import {Deferred} from '../util/Deferred.js';
+import {DisposableStack} from '../util/disposable.js';
import {interpolateFunction, stringifyFunction} from '../util/Function.js';
-import type {BidiConnection} from './Connection.js';
-import {BidiDeserializer} from './Deserializer.js';
+import type {Connection} from './core/Connection.js';
+import {BidiElementHandle} from './ElementHandle.js';
import type {BidiFrame} from './Frame.js';
-import {BidiSerializer} from './Serializer.js';
-
-type SendArgsChannel<Args> = (value: [id: number, args: Args]) => void;
-type SendResolveChannel<Ret> = (
- value: [id: number, resolve: (ret: FlattenHandle<Awaited<Ret>>) => void]
-) => void;
-type SendRejectChannel = (
- value: [id: number, reject: (error: unknown) => void]
+import {BidiJSHandle} from './JSHandle.js';
+
+type CallbackChannel<Args, Ret> = (
+ value: [
+ resolve: (ret: FlattenHandle<Awaited<Ret>>) => void,
+ reject: (error: unknown) => void,
+ args: Args,
+ ]
) => void;
-interface RemotePromiseCallbacks {
- resolve: Deferred<Bidi.Script.RemoteValue>;
- reject: Deferred<Bidi.Script.RemoteValue>;
-}
-
/**
* @internal
*/
export class ExposeableFunction<Args extends unknown[], Ret> {
+ static async from<Args extends unknown[], Ret>(
+ frame: BidiFrame,
+ name: string,
+ apply: (...args: Args) => Awaitable<Ret>,
+ isolate = false
+ ): Promise<ExposeableFunction<Args, Ret>> {
+ const func = new ExposeableFunction(frame, name, apply, isolate);
+ await func.#initialize();
+ return func;
+ }
+
readonly #frame;
readonly name;
readonly #apply;
+ readonly #isolate;
- readonly #channels;
- readonly #callerInfos = new Map<
- string,
- Map<number, RemotePromiseCallbacks>
- >();
+ readonly #channel;
- #preloadScriptId?: Bidi.Script.PreloadScript;
+ #scripts: Array<[BidiFrame, Bidi.Script.PreloadScript]> = [];
+ #disposables = new DisposableStack();
constructor(
frame: BidiFrame,
name: string,
- apply: (...args: Args) => Awaitable<Ret>
+ apply: (...args: Args) => Awaitable<Ret>,
+ isolate = false
) {
this.#frame = frame;
this.name = name;
this.#apply = apply;
+ this.#isolate = isolate;
- this.#channels = {
- args: `__puppeteer__${this.#frame._id}_page_exposeFunction_${this.name}_args`,
- resolve: `__puppeteer__${this.#frame._id}_page_exposeFunction_${this.name}_resolve`,
- reject: `__puppeteer__${this.#frame._id}_page_exposeFunction_${this.name}_reject`,
- };
+ this.#channel = `__puppeteer__${this.#frame._id}_page_exposeFunction_${this.name}`;
}
- async expose(): Promise<void> {
+ async #initialize() {
const connection = this.#connection;
- const channelArguments = this.#channelArguments;
+ const channel = {
+ type: 'channel' as const,
+ value: {
+ channel: this.#channel,
+ ownership: Bidi.Script.ResultOwnership.Root,
+ },
+ };
- // TODO(jrandolf): Implement cleanup with removePreloadScript.
- connection.on(
- Bidi.ChromiumBidi.Script.EventNames.Message,
- this.#handleArgumentsMessage
+ const connectionEmitter = this.#disposables.use(
+ new EventEmitter(connection)
);
- connection.on(
+ connectionEmitter.on(
Bidi.ChromiumBidi.Script.EventNames.Message,
- this.#handleResolveMessage
- );
- connection.on(
- Bidi.ChromiumBidi.Script.EventNames.Message,
- this.#handleRejectMessage
+ this.#handleMessage
);
const functionDeclaration = stringifyFunction(
interpolateFunction(
- (
- sendArgs: SendArgsChannel<Args>,
- sendResolve: SendResolveChannel<Ret>,
- sendReject: SendRejectChannel
- ) => {
- let id = 0;
+ (callback: CallbackChannel<Args, Ret>) => {
Object.assign(globalThis, {
[PLACEHOLDER('name') as string]: function (...args: Args) {
return new Promise<FlattenHandle<Awaited<Ret>>>(
(resolve, reject) => {
- sendArgs([id, args]);
- sendResolve([id, resolve]);
- sendReject([id, reject]);
- ++id;
+ callback([resolve, reject, args]);
}
);
},
@@ -106,179 +100,133 @@ export class ExposeableFunction<Args extends unknown[], Ret> {
)
);
- const {result} = await connection.send('script.addPreloadScript', {
- functionDeclaration,
- arguments: channelArguments,
- contexts: [this.#frame.page().mainFrame()._id],
- });
- this.#preloadScriptId = result.script;
+ const frames = [this.#frame];
+ for (const frame of frames) {
+ frames.push(...frame.childFrames());
+ }
await Promise.all(
- this.#frame
- .page()
- .frames()
- .map(async frame => {
- return await connection.send('script.callFunction', {
- functionDeclaration,
- arguments: channelArguments,
- awaitPromise: false,
- target: frame.mainRealm().realm.target,
- });
- })
+ frames.map(async frame => {
+ const realm = this.#isolate ? frame.isolatedRealm() : frame.mainRealm();
+ try {
+ const [script] = await Promise.all([
+ frame.browsingContext.addPreloadScript(functionDeclaration, {
+ arguments: [channel],
+ sandbox: realm.sandbox,
+ }),
+ realm.realm.callFunction(functionDeclaration, false, {
+ arguments: [channel],
+ }),
+ ]);
+ this.#scripts.push([frame, script]);
+ } catch (error) {
+ // If it errors, the frame probably doesn't support call function. We
+ // fail gracefully.
+ debugError(error);
+ }
+ })
);
}
- #handleArgumentsMessage = async (params: Bidi.Script.MessageParameters) => {
- if (params.channel !== this.#channels.args) {
+ get #connection(): Connection {
+ return this.#frame.page().browser().connection;
+ }
+
+ #handleMessage = async (params: Bidi.Script.MessageParameters) => {
+ if (params.channel !== this.#channel) {
return;
}
- const connection = this.#connection;
- const {callbacks, remoteValue} = this.#getCallbacksAndRemoteValue(params);
- const args = remoteValue.value?.[1];
- assert(args);
+ const realm = this.#getRealm(params.source);
+ if (!realm) {
+ // Unrelated message.
+ return;
+ }
+
+ using dataHandle = BidiJSHandle.from<
+ [
+ resolve: (ret: FlattenHandle<Awaited<Ret>>) => void,
+ reject: (error: unknown) => void,
+ args: Args,
+ ]
+ >(params.data, realm);
+
+ using argsHandle = await dataHandle.evaluateHandle(([, , args]) => {
+ return args;
+ });
+
+ using stack = new DisposableStack();
+ const args = [];
+ for (const [index, handle] of await argsHandle.getProperties()) {
+ stack.use(handle);
+
+ // Element handles are passed as is.
+ if (handle instanceof BidiElementHandle) {
+ args[+index] = handle;
+ stack.use(handle);
+ continue;
+ }
+
+ // Everything else is passed as the JS value.
+ args[+index] = handle.jsonValue();
+ }
+
+ let result;
try {
- const result = await this.#apply(...BidiDeserializer.deserialize(args));
- await connection.send('script.callFunction', {
- functionDeclaration: stringifyFunction(([_, resolve]: any, result) => {
- resolve(result);
- }),
- arguments: [
- (await callbacks.resolve.valueOrThrow()) as Bidi.Script.LocalValue,
- BidiSerializer.serializeRemoteValue(result),
- ],
- awaitPromise: false,
- target: {
- realm: params.source.realm,
- },
- });
+ result = await this.#apply(...((await Promise.all(args)) as Args));
} catch (error) {
try {
if (error instanceof Error) {
- await connection.send('script.callFunction', {
- functionDeclaration: stringifyFunction(
- (
- [_, reject]: [unknown, (error: Error) => void],
- name: string,
- message: string,
- stack?: string
- ) => {
- const error = new Error(message);
- error.name = name;
- if (stack) {
- error.stack = stack;
- }
- reject(error);
+ await dataHandle.evaluate(
+ ([, reject], name, message, stack) => {
+ const error = new Error(message);
+ error.name = name;
+ if (stack) {
+ error.stack = stack;
}
- ),
- arguments: [
- (await callbacks.reject.valueOrThrow()) as Bidi.Script.LocalValue,
- BidiSerializer.serializeRemoteValue(error.name),
- BidiSerializer.serializeRemoteValue(error.message),
- BidiSerializer.serializeRemoteValue(error.stack),
- ],
- awaitPromise: false,
- target: {
- realm: params.source.realm,
+ reject(error);
},
- });
+ error.name,
+ error.message,
+ error.stack
+ );
} else {
- await connection.send('script.callFunction', {
- functionDeclaration: stringifyFunction(
- (
- [_, reject]: [unknown, (error: unknown) => void],
- error: unknown
- ) => {
- reject(error);
- }
- ),
- arguments: [
- (await callbacks.reject.valueOrThrow()) as Bidi.Script.LocalValue,
- BidiSerializer.serializeRemoteValue(error),
- ],
- awaitPromise: false,
- target: {
- realm: params.source.realm,
- },
- });
+ await dataHandle.evaluate(([, reject], error) => {
+ reject(error);
+ }, error);
}
} catch (error) {
debugError(error);
}
- }
- };
-
- get #connection(): BidiConnection {
- return this.#frame.context().connection;
- }
-
- get #channelArguments() {
- return [
- {
- type: 'channel' as const,
- value: {
- channel: this.#channels.args,
- ownership: Bidi.Script.ResultOwnership.Root,
- },
- },
- {
- type: 'channel' as const,
- value: {
- channel: this.#channels.resolve,
- ownership: Bidi.Script.ResultOwnership.Root,
- },
- },
- {
- type: 'channel' as const,
- value: {
- channel: this.#channels.reject,
- ownership: Bidi.Script.ResultOwnership.Root,
- },
- },
- ];
- }
-
- #handleResolveMessage = (params: Bidi.Script.MessageParameters) => {
- if (params.channel !== this.#channels.resolve) {
return;
}
- const {callbacks, remoteValue} = this.#getCallbacksAndRemoteValue(params);
- callbacks.resolve.resolve(remoteValue);
- };
- #handleRejectMessage = (params: Bidi.Script.MessageParameters) => {
- if (params.channel !== this.#channels.reject) {
- return;
+ try {
+ await dataHandle.evaluate(([resolve], result) => {
+ resolve(result);
+ }, result);
+ } catch (error) {
+ debugError(error);
}
- const {callbacks, remoteValue} = this.#getCallbacksAndRemoteValue(params);
- callbacks.reject.resolve(remoteValue);
};
- #getCallbacksAndRemoteValue(params: Bidi.Script.MessageParameters) {
- const {data, source} = params;
- assert(data.type === 'array');
- assert(data.value);
-
- const callerIdRemote = data.value[0];
- assert(callerIdRemote);
- assert(callerIdRemote.type === 'number');
- assert(typeof callerIdRemote.value === 'number');
-
- let bindingMap = this.#callerInfos.get(source.realm);
- if (!bindingMap) {
- bindingMap = new Map();
- this.#callerInfos.set(source.realm, bindingMap);
+ #getRealm(source: Bidi.Script.Source) {
+ const frame = this.#findFrame(source.context as string);
+ if (!frame) {
+ // Unrelated message.
+ return;
}
+ return frame.realm(source.realm);
+ }
- const callerId = callerIdRemote.value;
- let callbacks = bindingMap.get(callerId);
- if (!callbacks) {
- callbacks = {
- resolve: new Deferred(),
- reject: new Deferred(),
- };
- bindingMap.set(callerId, callbacks);
+ #findFrame(id: string) {
+ const frames = [this.#frame];
+ for (const frame of frames) {
+ if (frame._id === id) {
+ return frame;
+ }
+ frames.push(...frame.childFrames());
}
- return {callbacks, remoteValue: data};
+ return;
}
[Symbol.dispose](): void {
@@ -286,10 +234,21 @@ export class ExposeableFunction<Args extends unknown[], Ret> {
}
async [Symbol.asyncDispose](): Promise<void> {
- if (this.#preloadScriptId) {
- await this.#connection.send('script.removePreloadScript', {
- script: this.#preloadScriptId,
- });
- }
+ this.#disposables.dispose();
+ await Promise.all(
+ this.#scripts.map(async ([frame, script]) => {
+ const realm = this.#isolate ? frame.isolatedRealm() : frame.mainRealm();
+ try {
+ await Promise.all([
+ realm.evaluate(name => {
+ delete (globalThis as any)[name];
+ }, this.name),
+ frame.browsingContext.removePreloadScript(script),
+ ]);
+ } catch (error) {
+ debugError(error);
+ }
+ })
+ );
}
}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Frame.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Frame.ts
index 1638c2cbdf..f2bfd5f64e 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Frame.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Frame.ts
@@ -6,15 +6,18 @@
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+import type {Observable} from '../../third_party/rxjs/rxjs.js';
import {
+ combineLatest,
+ defer,
+ delayWhen,
+ filter,
first,
firstValueFrom,
- forkJoin,
- from,
map,
- merge,
+ of,
raceWith,
- zip,
+ switchMap,
} from '../../third_party/rxjs/rxjs.js';
import type {CDPSession} from '../api/CDPSession.js';
import type {ElementHandle} from '../api/ElementHandle.js';
@@ -25,85 +28,228 @@ import {
type WaitForOptions,
} from '../api/Frame.js';
import type {WaitForSelectorOptions} from '../api/Page.js';
-import {UnsupportedOperation} from '../common/Errors.js';
+import {PageEvent} from '../api/Page.js';
+import {
+ ConsoleMessage,
+ type ConsoleMessageLocation,
+} from '../common/ConsoleMessage.js';
+import {TargetCloseError, UnsupportedOperation} from '../common/Errors.js';
import type {TimeoutSettings} from '../common/TimeoutSettings.js';
import type {Awaitable, NodeFor} from '../common/types.js';
-import {
- fromEmitterEvent,
- NETWORK_IDLE_TIME,
- timeout,
- UTILITY_WORLD_NAME,
-} from '../common/util.js';
-import {Deferred} from '../util/Deferred.js';
-import {disposeSymbol} from '../util/disposable.js';
-
-import type {BrowsingContext} from './BrowsingContext.js';
+import {debugError, fromEmitterEvent, timeout} from '../common/util.js';
+
+import {BidiCdpSession} from './CDPSession.js';
+import type {BrowsingContext} from './core/BrowsingContext.js';
+import type {Navigation} from './core/Navigation.js';
+import type {Request} from './core/Request.js';
+import {BidiDeserializer} from './Deserializer.js';
+import {BidiDialog} from './Dialog.js';
+import type {BidiElementHandle} from './ElementHandle.js';
import {ExposeableFunction} from './ExposedFunction.js';
+import {BidiHTTPRequest, requests} from './HTTPRequest.js';
import type {BidiHTTPResponse} from './HTTPResponse.js';
-import {
- getBiDiLifecycleEvent,
- getBiDiReadinessState,
- rewriteNavigationError,
-} from './lifecycle.js';
+import {BidiJSHandle} from './JSHandle.js';
import type {BidiPage} from './Page.js';
-import {
- MAIN_SANDBOX,
- PUPPETEER_SANDBOX,
- Sandbox,
- type SandboxChart,
-} from './Sandbox.js';
+import type {BidiRealm} from './Realm.js';
+import {BidiFrameRealm} from './Realm.js';
+import {rewriteNavigationError} from './util.js';
+import {BidiWebWorker} from './WebWorker.js';
-/**
- * Puppeteer's Frame class could be viewed as a BiDi BrowsingContext implementation
- * @internal
- */
export class BidiFrame extends Frame {
- #page: BidiPage;
- #context: BrowsingContext;
- #timeoutSettings: TimeoutSettings;
- #abortDeferred = Deferred.create<never>();
- #disposed = false;
- sandboxes: SandboxChart;
- override _id: string;
-
- constructor(
- page: BidiPage,
- context: BrowsingContext,
- timeoutSettings: TimeoutSettings,
- parentId?: string | null
+ static from(
+ parent: BidiPage | BidiFrame,
+ browsingContext: BrowsingContext
+ ): BidiFrame {
+ const frame = new BidiFrame(parent, browsingContext);
+ frame.#initialize();
+ return frame;
+ }
+
+ readonly #parent: BidiPage | BidiFrame;
+ readonly browsingContext: BrowsingContext;
+ readonly #frames = new WeakMap<BrowsingContext, BidiFrame>();
+ readonly realms: {default: BidiFrameRealm; internal: BidiFrameRealm};
+
+ override readonly _id: string;
+ override readonly client: BidiCdpSession;
+
+ private constructor(
+ parent: BidiPage | BidiFrame,
+ browsingContext: BrowsingContext
) {
super();
- this.#page = page;
- this.#context = context;
- this.#timeoutSettings = timeoutSettings;
- this._id = this.#context.id;
- this._parentId = parentId ?? undefined;
-
- this.sandboxes = {
- [MAIN_SANDBOX]: new Sandbox(undefined, this, context, timeoutSettings),
- [PUPPETEER_SANDBOX]: new Sandbox(
- UTILITY_WORLD_NAME,
- this,
- context.createRealmForSandbox(),
- timeoutSettings
+ this.#parent = parent;
+ this.browsingContext = browsingContext;
+
+ this._id = browsingContext.id;
+ this.client = new BidiCdpSession(this);
+ this.realms = {
+ default: BidiFrameRealm.from(this.browsingContext.defaultRealm, this),
+ internal: BidiFrameRealm.from(
+ this.browsingContext.createWindowRealm(
+ `__puppeteer_internal_${Math.ceil(Math.random() * 10000)}`
+ ),
+ this
),
};
}
- override get client(): CDPSession {
- return this.context().cdpSession;
+ #initialize(): void {
+ for (const browsingContext of this.browsingContext.children) {
+ this.#createFrameTarget(browsingContext);
+ }
+
+ this.browsingContext.on('browsingcontext', ({browsingContext}) => {
+ this.#createFrameTarget(browsingContext);
+ });
+ this.browsingContext.on('closed', () => {
+ for (const session of BidiCdpSession.sessions.values()) {
+ if (session.frame === this) {
+ void session.detach().catch(debugError);
+ }
+ }
+ this.page().trustedEmitter.emit(PageEvent.FrameDetached, this);
+ });
+
+ this.browsingContext.on('request', ({request}) => {
+ const httpRequest = BidiHTTPRequest.from(request, this);
+ request.once('success', () => {
+ // SAFETY: BidiHTTPRequest will create this before here.
+ this.page().trustedEmitter.emit(PageEvent.RequestFinished, httpRequest);
+ });
+
+ request.once('error', () => {
+ this.page().trustedEmitter.emit(PageEvent.RequestFailed, httpRequest);
+ });
+ });
+
+ this.browsingContext.on('navigation', ({navigation}) => {
+ navigation.once('fragment', () => {
+ this.page().trustedEmitter.emit(PageEvent.FrameNavigated, this);
+ });
+ });
+ this.browsingContext.on('load', () => {
+ this.page().trustedEmitter.emit(PageEvent.Load, undefined);
+ });
+ this.browsingContext.on('DOMContentLoaded', () => {
+ this._hasStartedLoading = true;
+ this.page().trustedEmitter.emit(PageEvent.DOMContentLoaded, undefined);
+ this.page().trustedEmitter.emit(PageEvent.FrameNavigated, this);
+ });
+
+ this.browsingContext.on('userprompt', ({userPrompt}) => {
+ this.page().trustedEmitter.emit(
+ PageEvent.Dialog,
+ BidiDialog.from(userPrompt)
+ );
+ });
+
+ this.browsingContext.on('log', ({entry}) => {
+ if (this._id !== entry.source.context) {
+ return;
+ }
+ if (isConsoleLogEntry(entry)) {
+ const args = entry.args.map(arg => {
+ return this.mainRealm().createHandle(arg);
+ });
+
+ const text = args
+ .reduce((value, arg) => {
+ const parsedValue =
+ arg instanceof BidiJSHandle && arg.isPrimitiveValue
+ ? BidiDeserializer.deserialize(arg.remoteValue())
+ : arg.toString();
+ return `${value} ${parsedValue}`;
+ }, '')
+ .slice(1);
+
+ this.page().trustedEmitter.emit(
+ PageEvent.Console,
+ new ConsoleMessage(
+ entry.method as any,
+ text,
+ args,
+ getStackTraceLocations(entry.stackTrace)
+ )
+ );
+ } else if (isJavaScriptLogEntry(entry)) {
+ const error = new Error(entry.text ?? '');
+
+ const messageHeight = error.message.split('\n').length;
+ const messageLines = error.stack!.split('\n').splice(0, messageHeight);
+
+ const stackLines = [];
+ if (entry.stackTrace) {
+ for (const frame of entry.stackTrace.callFrames) {
+ // Note we need to add `1` because the values are 0-indexed.
+ stackLines.push(
+ ` at ${frame.functionName || '<anonymous>'} (${frame.url}:${
+ frame.lineNumber + 1
+ }:${frame.columnNumber + 1})`
+ );
+ if (stackLines.length >= Error.stackTraceLimit) {
+ break;
+ }
+ }
+ }
+
+ error.stack = [...messageLines, ...stackLines].join('\n');
+ this.page().trustedEmitter.emit(PageEvent.PageError, error);
+ } else {
+ debugError(
+ `Unhandled LogEntry with type "${entry.type}", text "${entry.text}" and level "${entry.level}"`
+ );
+ }
+ });
+
+ this.browsingContext.on('worker', ({realm}) => {
+ const worker = BidiWebWorker.from(this, realm);
+ realm.on('destroyed', () => {
+ this.page().trustedEmitter.emit(PageEvent.WorkerDestroyed, worker);
+ });
+ this.page().trustedEmitter.emit(PageEvent.WorkerCreated, worker);
+ });
+ }
+
+ #createFrameTarget(browsingContext: BrowsingContext) {
+ const frame = BidiFrame.from(this, browsingContext);
+ this.#frames.set(browsingContext, frame);
+ this.page().trustedEmitter.emit(PageEvent.FrameAttached, frame);
+
+ browsingContext.on('closed', () => {
+ this.#frames.delete(browsingContext);
+ });
+
+ return frame;
+ }
+
+ get timeoutSettings(): TimeoutSettings {
+ return this.page()._timeoutSettings;
}
- override mainRealm(): Sandbox {
- return this.sandboxes[MAIN_SANDBOX];
+ override mainRealm(): BidiFrameRealm {
+ return this.realms.default;
}
- override isolatedRealm(): Sandbox {
- return this.sandboxes[PUPPETEER_SANDBOX];
+ override isolatedRealm(): BidiFrameRealm {
+ return this.realms.internal;
+ }
+
+ realm(id: string): BidiRealm | undefined {
+ for (const realm of Object.values(this.realms)) {
+ if (realm.realm.id === id) {
+ return realm;
+ }
+ }
+ return;
}
override page(): BidiPage {
- return this.#page;
+ let parent = this.#parent;
+ while (parent instanceof BidiFrame) {
+ parent = parent.#parent;
+ }
+ return parent;
}
override isOOPFrame(): never {
@@ -111,15 +257,36 @@ export class BidiFrame extends Frame {
}
override url(): string {
- return this.#context.url;
+ return this.browsingContext.url;
}
override parentFrame(): BidiFrame | null {
- return this.#page.frame(this._parentId ?? '');
+ if (this.#parent instanceof BidiFrame) {
+ return this.#parent;
+ }
+ return null;
}
override childFrames(): BidiFrame[] {
- return this.#page.childFrames(this.#context.id);
+ return [...this.browsingContext.children].map(child => {
+ return this.#frames.get(child)!;
+ });
+ }
+
+ #detached$() {
+ return defer(() => {
+ if (this.detached) {
+ return of(this as Frame);
+ }
+ return fromEmitterEvent(
+ this.page().trustedEmitter,
+ PageEvent.FrameDetached
+ ).pipe(
+ filter(detachedFrame => {
+ return detachedFrame === this;
+ })
+ );
+ });
}
@throwIfDetached
@@ -127,40 +294,23 @@ export class BidiFrame extends Frame {
url: string,
options: GoToOptions = {}
): Promise<BidiHTTPResponse | null> {
- const {
- waitUntil = 'load',
- timeout: ms = this.#timeoutSettings.navigationTimeout(),
- } = options;
-
- const [readiness, networkIdle] = getBiDiReadinessState(waitUntil);
-
- const result$ = zip(
- from(
- this.#context.connection.send('browsingContext.navigate', {
- context: this.#context.id,
- url,
- wait: readiness,
- })
+ const [response] = await Promise.all([
+ this.waitForNavigation(options),
+ // Some implementations currently only report errors when the
+ // readiness=interactive.
+ //
+ // Related: https://bugzilla.mozilla.org/show_bug.cgi?id=1846601
+ this.browsingContext.navigate(
+ url,
+ Bidi.BrowsingContext.ReadinessState.Interactive
),
- ...(networkIdle !== null
- ? [
- this.#page.waitForNetworkIdle$({
- timeout: ms,
- concurrency: networkIdle === 'networkidle2' ? 2 : 0,
- idleTime: NETWORK_IDLE_TIME,
- }),
- ]
- : [])
- ).pipe(
- map(([{result}]) => {
- return result;
- }),
- raceWith(timeout(ms), from(this.#abortDeferred.valueOrThrow())),
- rewriteNavigationError(url, ms)
+ ]).catch(
+ rewriteNavigationError(
+ url,
+ options.timeout ?? this.timeoutSettings.navigationTimeout()
+ )
);
-
- const result = await firstValueFrom(result$);
- return this.#page.getNavigationResponse(result.navigation);
+ return response;
}
@throwIfDetached
@@ -168,95 +318,105 @@ export class BidiFrame extends Frame {
html: string,
options: WaitForOptions = {}
): Promise<void> {
- const {
- waitUntil = 'load',
- timeout: ms = this.#timeoutSettings.navigationTimeout(),
- } = options;
-
- const [waitEvent, networkIdle] = getBiDiLifecycleEvent(waitUntil);
-
- const result$ = zip(
- forkJoin([
- fromEmitterEvent(this.#context, waitEvent).pipe(first()),
- from(this.setFrameContent(html)),
- ]).pipe(
- map(() => {
- return null;
- })
+ await Promise.all([
+ this.setFrameContent(html),
+ firstValueFrom(
+ combineLatest([
+ this.#waitForLoad$(options),
+ this.#waitForNetworkIdle$(options),
+ ])
),
- ...(networkIdle !== null
- ? [
- this.#page.waitForNetworkIdle$({
- timeout: ms,
- concurrency: networkIdle === 'networkidle2' ? 2 : 0,
- idleTime: NETWORK_IDLE_TIME,
- }),
- ]
- : [])
- ).pipe(
- raceWith(timeout(ms), from(this.#abortDeferred.valueOrThrow())),
- rewriteNavigationError('setContent', ms)
- );
-
- await firstValueFrom(result$);
- }
-
- context(): BrowsingContext {
- return this.#context;
+ ]);
}
@throwIfDetached
override async waitForNavigation(
options: WaitForOptions = {}
): Promise<BidiHTTPResponse | null> {
- const {
- waitUntil = 'load',
- timeout: ms = this.#timeoutSettings.navigationTimeout(),
- } = options;
-
- const [waitUntilEvent, networkIdle] = getBiDiLifecycleEvent(waitUntil);
-
- const navigation$ = merge(
- forkJoin([
- fromEmitterEvent(
- this.#context,
- Bidi.ChromiumBidi.BrowsingContext.EventNames.NavigationStarted
- ).pipe(first()),
- fromEmitterEvent(this.#context, waitUntilEvent).pipe(first()),
- ]),
- fromEmitterEvent(
- this.#context,
- Bidi.ChromiumBidi.BrowsingContext.EventNames.FragmentNavigated
+ const {timeout: ms = this.timeoutSettings.navigationTimeout()} = options;
+
+ const frames = this.childFrames().map(frame => {
+ return frame.#detached$();
+ });
+ return await firstValueFrom(
+ combineLatest([
+ fromEmitterEvent(this.browsingContext, 'navigation').pipe(
+ switchMap(({navigation}) => {
+ return this.#waitForLoad$(options).pipe(
+ delayWhen(() => {
+ if (frames.length === 0) {
+ return of(undefined);
+ }
+ return combineLatest(frames);
+ }),
+ raceWith(
+ fromEmitterEvent(navigation, 'fragment'),
+ fromEmitterEvent(navigation, 'failed').pipe(
+ map(({url}) => {
+ throw new Error(`Navigation failed: ${url}`);
+ })
+ ),
+ fromEmitterEvent(navigation, 'aborted').pipe(
+ map(({url}) => {
+ throw new Error(`Navigation aborted: ${url}`);
+ })
+ )
+ ),
+ switchMap(() => {
+ if (navigation.request) {
+ function requestFinished$(
+ request: Request
+ ): Observable<Navigation> {
+ // Reduces flakiness if the response events arrive after
+ // the load event.
+ // Usually, the response or error is already there at this point.
+ if (request.response || request.error) {
+ return of(navigation);
+ }
+ if (request.redirect) {
+ return requestFinished$(request.redirect);
+ }
+ return fromEmitterEvent(request, 'success')
+ .pipe(
+ raceWith(fromEmitterEvent(request, 'error')),
+ raceWith(fromEmitterEvent(request, 'redirect'))
+ )
+ .pipe(
+ switchMap(() => {
+ return requestFinished$(request);
+ })
+ );
+ }
+ return requestFinished$(navigation.request);
+ }
+ return of(navigation);
+ })
+ );
+ })
+ ),
+ this.#waitForNetworkIdle$(options),
+ ]).pipe(
+ map(([navigation]) => {
+ const request = navigation.request;
+ if (!request) {
+ return null;
+ }
+ const httpRequest = requests.get(request)!;
+ const lastRedirect = httpRequest.redirectChain().at(-1);
+ return (
+ lastRedirect !== undefined ? lastRedirect : httpRequest
+ ).response();
+ }),
+ raceWith(
+ timeout(ms),
+ this.#detached$().pipe(
+ map(() => {
+ throw new TargetCloseError('Frame detached.');
+ })
+ )
+ )
)
- ).pipe(
- map(result => {
- if (Array.isArray(result)) {
- return {result: result[1]};
- }
- return {result};
- })
);
-
- const result$ = zip(
- navigation$,
- ...(networkIdle !== null
- ? [
- this.#page.waitForNetworkIdle$({
- timeout: ms,
- concurrency: networkIdle === 'networkidle2' ? 2 : 0,
- idleTime: NETWORK_IDLE_TIME,
- }),
- ]
- : [])
- ).pipe(
- map(([{result}]) => {
- return result;
- }),
- raceWith(timeout(ms), from(this.#abortDeferred.valueOrThrow()))
- );
-
- const result = await firstValueFrom(result$);
- return this.#page.getNavigationResponse(result.navigation);
}
override waitForDevicePrompt(): never {
@@ -264,18 +424,7 @@ export class BidiFrame extends Frame {
}
override get detached(): boolean {
- return this.#disposed;
- }
-
- [disposeSymbol](): void {
- if (this.#disposed) {
- return;
- }
- this.#disposed = true;
- this.#abortDeferred.reject(new Error('Frame detached'));
- this.#context.dispose();
- this.sandboxes[MAIN_SANDBOX][disposeSymbol]();
- this.sandboxes[PUPPETEER_SANDBOX][disposeSymbol]();
+ return this.browsingContext.closed;
}
#exposedFunctions = new Map<string, ExposeableFunction<never[], unknown>>();
@@ -288,21 +437,27 @@ export class BidiFrame extends Frame {
`Failed to add page binding with name ${name}: globalThis['${name}'] already exists!`
);
}
- const exposeable = new ExposeableFunction(this, name, apply);
+ const exposeable = await ExposeableFunction.from(this, name, apply);
this.#exposedFunctions.set(name, exposeable);
- try {
- await exposeable.expose();
- } catch (error) {
- this.#exposedFunctions.delete(name);
- throw error;
+ }
+
+ async removeExposedFunction(name: string): Promise<void> {
+ const exposedFunction = this.#exposedFunctions.get(name);
+ if (!exposedFunction) {
+ throw new Error(
+ `Failed to remove page binding with name ${name}: window['${name}'] does not exists!`
+ );
}
+
+ this.#exposedFunctions.delete(name);
+ await exposedFunction[Symbol.asyncDispose]();
}
override waitForSelector<Selector extends string>(
selector: Selector,
options?: WaitForSelectorOptions
): Promise<ElementHandle<NodeFor<Selector>> | null> {
- if (selector.startsWith('aria')) {
+ if (selector.startsWith('aria') && !this.page().browser().cdpSupported) {
throw new UnsupportedOperation(
'ARIA selector is not supported for BiDi!'
);
@@ -310,4 +465,124 @@ export class BidiFrame extends Frame {
return super.waitForSelector(selector, options);
}
+
+ async createCDPSession(): Promise<CDPSession> {
+ const {sessionId} = await this.client.send('Target.attachToTarget', {
+ targetId: this._id,
+ flatten: true,
+ });
+ return new BidiCdpSession(this, sessionId);
+ }
+
+ @throwIfDetached
+ #waitForLoad$(options: WaitForOptions = {}): Observable<void> {
+ let {waitUntil = 'load'} = options;
+ const {timeout: ms = this.timeoutSettings.navigationTimeout()} = options;
+
+ if (!Array.isArray(waitUntil)) {
+ waitUntil = [waitUntil];
+ }
+
+ const events = new Set<'load' | 'DOMContentLoaded'>();
+ for (const lifecycleEvent of waitUntil) {
+ switch (lifecycleEvent) {
+ case 'load': {
+ events.add('load');
+ break;
+ }
+ case 'domcontentloaded': {
+ events.add('DOMContentLoaded');
+ break;
+ }
+ }
+ }
+ if (events.size === 0) {
+ return of(undefined);
+ }
+
+ return combineLatest(
+ [...events].map(event => {
+ return fromEmitterEvent(this.browsingContext, event);
+ })
+ ).pipe(
+ map(() => {}),
+ first(),
+ raceWith(
+ timeout(ms),
+ this.#detached$().pipe(
+ map(() => {
+ throw new Error('Frame detached.');
+ })
+ )
+ )
+ );
+ }
+
+ @throwIfDetached
+ #waitForNetworkIdle$(options: WaitForOptions = {}): Observable<void> {
+ let {waitUntil = 'load'} = options;
+ if (!Array.isArray(waitUntil)) {
+ waitUntil = [waitUntil];
+ }
+
+ let concurrency = Infinity;
+ for (const event of waitUntil) {
+ switch (event) {
+ case 'networkidle0': {
+ concurrency = Math.min(0, concurrency);
+ break;
+ }
+ case 'networkidle2': {
+ concurrency = Math.min(2, concurrency);
+ break;
+ }
+ }
+ }
+ if (concurrency === Infinity) {
+ return of(undefined);
+ }
+
+ return this.page().waitForNetworkIdle$({
+ idleTime: 500,
+ timeout: options.timeout ?? this.timeoutSettings.timeout(),
+ concurrency,
+ });
+ }
+
+ @throwIfDetached
+ async setFiles(element: BidiElementHandle, files: string[]): Promise<void> {
+ await this.browsingContext.setFiles(
+ // SAFETY: ElementHandles are always remote references.
+ element.remoteValue() as Bidi.Script.SharedReference,
+ files
+ );
+ }
+}
+
+function isConsoleLogEntry(
+ event: Bidi.Log.Entry
+): event is Bidi.Log.ConsoleLogEntry {
+ return event.type === 'console';
+}
+
+function isJavaScriptLogEntry(
+ event: Bidi.Log.Entry
+): event is Bidi.Log.JavascriptLogEntry {
+ return event.type === 'javascript';
+}
+
+function getStackTraceLocations(
+ stackTrace?: Bidi.Script.StackTrace
+): ConsoleMessageLocation[] {
+ const stackTraceLocations: ConsoleMessageLocation[] = [];
+ if (stackTrace) {
+ for (const callFrame of stackTrace.callFrames) {
+ stackTraceLocations.push({
+ url: callFrame.url,
+ lineNumber: callFrame.lineNumber,
+ columnNumber: callFrame.columnNumber,
+ });
+ }
+ }
+ return stackTraceLocations;
}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/HTTPRequest.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/HTTPRequest.ts
index 57cb801b8c..e75bb0cf3c 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/HTTPRequest.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/HTTPRequest.ts
@@ -5,106 +5,126 @@
*/
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
-import type {Frame} from '../api/Frame.js';
+import type {CDPSession} from '../api/CDPSession.js';
import type {
ContinueRequestOverrides,
ResponseForRequest,
} from '../api/HTTPRequest.js';
import {HTTPRequest, type ResourceType} from '../api/HTTPRequest.js';
+import {PageEvent} from '../api/Page.js';
import {UnsupportedOperation} from '../common/Errors.js';
-import type {BidiHTTPResponse} from './HTTPResponse.js';
+import type {Request} from './core/Request.js';
+import type {BidiFrame} from './Frame.js';
+import {BidiHTTPResponse} from './HTTPResponse.js';
+
+export const requests = new WeakMap<Request, BidiHTTPRequest>();
/**
* @internal
*/
export class BidiHTTPRequest extends HTTPRequest {
- override _response: BidiHTTPResponse | null = null;
- override _redirectChain: BidiHTTPRequest[];
- _navigationId: string | null;
-
- #url: string;
- #resourceType: ResourceType;
-
- #method: string;
- #postData?: string;
- #headers: Record<string, string> = {};
- #initiator: Bidi.Network.Initiator;
- #frame: Frame | null;
-
- constructor(
- event: Bidi.Network.BeforeRequestSentParameters,
- frame: Frame | null,
- redirectChain: BidiHTTPRequest[] = []
- ) {
+ static from(
+ bidiRequest: Request,
+ frame: BidiFrame | undefined
+ ): BidiHTTPRequest {
+ const request = new BidiHTTPRequest(bidiRequest, frame);
+ request.#initialize();
+ return request;
+ }
+
+ #redirect: BidiHTTPRequest | undefined;
+ #response: BidiHTTPResponse | null = null;
+ override readonly id: string;
+ readonly #frame: BidiFrame | undefined;
+ readonly #request: Request;
+
+ private constructor(request: Request, frame: BidiFrame | undefined) {
super();
+ requests.set(request, this);
- this.#url = event.request.url;
- this.#resourceType = event.initiator.type.toLowerCase() as ResourceType;
- this.#method = event.request.method;
- this.#postData = undefined;
- this.#initiator = event.initiator;
+ this.#request = request;
this.#frame = frame;
-
- this._requestId = event.request.request;
- this._redirectChain = redirectChain;
- this._navigationId = event.navigation;
-
- for (const header of event.request.headers) {
- // TODO: How to handle Binary Headers
- // https://w3c.github.io/webdriver-bidi/#type-network-Header
- if (header.value.type === 'string') {
- this.#headers[header.name.toLowerCase()] = header.value.value;
- }
- }
+ this.id = request.id;
}
- override get client(): never {
+ override get client(): CDPSession {
throw new UnsupportedOperation();
}
+ #initialize() {
+ this.#request.on('redirect', request => {
+ this.#redirect = BidiHTTPRequest.from(request, this.#frame);
+ });
+ this.#request.once('success', data => {
+ this.#response = BidiHTTPResponse.from(data, this);
+ });
+
+ this.#frame?.page().trustedEmitter.emit(PageEvent.Request, this);
+ }
+
override url(): string {
- return this.#url;
+ return this.#request.url;
}
override resourceType(): ResourceType {
- return this.#resourceType;
+ return this.initiator().type.toLowerCase() as ResourceType;
}
override method(): string {
- return this.#method;
+ return this.#request.method;
}
override postData(): string | undefined {
- return this.#postData;
+ throw new UnsupportedOperation();
}
override hasPostData(): boolean {
- return this.#postData !== undefined;
+ throw new UnsupportedOperation();
}
override async fetchPostData(): Promise<string | undefined> {
- return this.#postData;
+ throw new UnsupportedOperation();
}
override headers(): Record<string, string> {
- return this.#headers;
+ const headers: Record<string, string> = {};
+ for (const header of this.#request.headers) {
+ headers[header.name.toLowerCase()] = header.value.value;
+ }
+ return headers;
}
override response(): BidiHTTPResponse | null {
- return this._response;
+ return this.#response;
+ }
+
+ override failure(): {errorText: string} | null {
+ if (this.#request.error === undefined) {
+ return null;
+ }
+ return {errorText: this.#request.error};
}
override isNavigationRequest(): boolean {
- return Boolean(this._navigationId);
+ return this.#request.navigation !== undefined;
}
override initiator(): Bidi.Network.Initiator {
- return this.#initiator;
+ return this.#request.initiator;
}
override redirectChain(): BidiHTTPRequest[] {
- return this._redirectChain.slice();
+ if (this.#redirect === undefined) {
+ return [];
+ }
+ const redirects = [this.#redirect];
+ for (const redirect of redirects) {
+ if (redirect.#redirect !== undefined) {
+ redirects.push(redirect.#redirect);
+ }
+ }
+ return redirects;
}
override enqueueInterceptAction(
@@ -114,8 +134,8 @@ export class BidiHTTPRequest extends HTTPRequest {
void pendingHandler();
}
- override frame(): Frame | null {
- return this.#frame;
+ override frame(): BidiFrame | null {
+ return this.#frame ?? null;
}
override continueRequestOverrides(): never {
@@ -156,8 +176,4 @@ export class BidiHTTPRequest extends HTTPRequest {
): never {
throw new UnsupportedOperation();
}
-
- override failure(): never {
- throw new UnsupportedOperation();
- }
}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/HTTPResponse.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/HTTPResponse.ts
index ce28820a65..bad44ff089 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/HTTPResponse.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/HTTPResponse.ts
@@ -7,11 +7,10 @@ import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import type Protocol from 'devtools-protocol';
import type {Frame} from '../api/Frame.js';
-import {
- HTTPResponse as HTTPResponse,
- type RemoteAddress,
-} from '../api/HTTPResponse.js';
+import {HTTPResponse, type RemoteAddress} from '../api/HTTPResponse.js';
+import {PageEvent} from '../api/Page.js';
import {UnsupportedOperation} from '../common/Errors.js';
+import {invokeAtMostOnceForArguments} from '../util/decorators.js';
import type {BidiHTTPRequest} from './HTTPRequest.js';
@@ -19,62 +18,62 @@ import type {BidiHTTPRequest} from './HTTPRequest.js';
* @internal
*/
export class BidiHTTPResponse extends HTTPResponse {
+ static from(
+ data: Bidi.Network.ResponseData,
+ request: BidiHTTPRequest
+ ): BidiHTTPResponse {
+ const response = new BidiHTTPResponse(data, request);
+ response.#initialize();
+ return response;
+ }
+
+ #data: Bidi.Network.ResponseData;
#request: BidiHTTPRequest;
- #remoteAddress: RemoteAddress;
- #status: number;
- #statusText: string;
- #url: string;
- #fromCache: boolean;
- #headers: Record<string, string> = {};
- #timings: Record<string, string> | null;
-
- constructor(
- request: BidiHTTPRequest,
- {response}: Bidi.Network.ResponseCompletedParameters
+
+ private constructor(
+ data: Bidi.Network.ResponseData,
+ request: BidiHTTPRequest
) {
super();
+ this.#data = data;
this.#request = request;
+ }
- this.#remoteAddress = {
- ip: '',
- port: -1,
- };
-
- this.#url = response.url;
- this.#fromCache = response.fromCache;
- this.#status = response.status;
- this.#statusText = response.statusText;
- // TODO: File and issue with BiDi spec
- this.#timings = null;
-
- // TODO: Removed once the Firefox implementation is compliant with https://w3c.github.io/webdriver-bidi/#get-the-response-data.
- for (const header of response.headers || []) {
- // TODO: How to handle Binary Headers
- // https://w3c.github.io/webdriver-bidi/#type-network-Header
- if (header.value.type === 'string') {
- this.#headers[header.name.toLowerCase()] = header.value.value;
- }
- }
+ #initialize() {
+ this.#request.frame()?.page().trustedEmitter.emit(PageEvent.Response, this);
}
+ @invokeAtMostOnceForArguments
override remoteAddress(): RemoteAddress {
- return this.#remoteAddress;
+ return {
+ ip: '',
+ port: -1,
+ };
}
override url(): string {
- return this.#url;
+ return this.#data.url;
}
override status(): number {
- return this.#status;
+ return this.#data.status;
}
override statusText(): string {
- return this.#statusText;
+ return this.#data.statusText;
}
override headers(): Record<string, string> {
- return this.#headers;
+ const headers: Record<string, string> = {};
+ // TODO: Remove once the Firefox implementation is compliant with https://w3c.github.io/webdriver-bidi/#get-the-response-data.
+ for (const header of this.#data.headers || []) {
+ // TODO: How to handle Binary Headers
+ // https://w3c.github.io/webdriver-bidi/#type-network-Header
+ if (header.value.type === 'string') {
+ headers[header.name.toLowerCase()] = header.value.value;
+ }
+ }
+ return headers;
}
override request(): BidiHTTPRequest {
@@ -82,11 +81,12 @@ export class BidiHTTPResponse extends HTTPResponse {
}
override fromCache(): boolean {
- return this.#fromCache;
+ return this.#data.fromCache;
}
override timing(): Protocol.Network.ResourceTiming | null {
- return this.#timings as any;
+ // TODO: File and issue with BiDi spec
+ throw new UnsupportedOperation();
}
override frame(): Frame | null {
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Input.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Input.ts
index 5406556d64..dc70850c12 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Input.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Input.ts
@@ -12,9 +12,9 @@ import {
Mouse,
MouseButton,
Touchscreen,
+ type KeyboardTypeOptions,
type KeyDownOptions,
type KeyPressOptions,
- type KeyboardTypeOptions,
type MouseClickOptions,
type MouseMoveOptions,
type MouseOptions,
@@ -23,7 +23,6 @@ import {
import {UnsupportedOperation} from '../common/Errors.js';
import type {KeyInput} from '../common/USKeyboardLayout.js';
-import type {BrowsingContext} from './BrowsingContext.js';
import type {BidiPage} from './Page.js';
const enum InputId {
@@ -288,39 +287,33 @@ export class BidiKeyboard extends Keyboard {
key: KeyInput,
_options?: Readonly<KeyDownOptions>
): Promise<void> {
- await this.#page.connection.send('input.performActions', {
- context: this.#page.mainFrame()._id,
- actions: [
- {
- type: SourceActionsType.Key,
- id: InputId.Keyboard,
- actions: [
- {
- type: ActionType.KeyDown,
- value: getBidiKeyValue(key),
- },
- ],
- },
- ],
- });
+ await this.#page.mainFrame().browsingContext.performActions([
+ {
+ type: SourceActionsType.Key,
+ id: InputId.Keyboard,
+ actions: [
+ {
+ type: ActionType.KeyDown,
+ value: getBidiKeyValue(key),
+ },
+ ],
+ },
+ ]);
}
override async up(key: KeyInput): Promise<void> {
- await this.#page.connection.send('input.performActions', {
- context: this.#page.mainFrame()._id,
- actions: [
- {
- type: SourceActionsType.Key,
- id: InputId.Keyboard,
- actions: [
- {
- type: ActionType.KeyUp,
- value: getBidiKeyValue(key),
- },
- ],
- },
- ],
- });
+ await this.#page.mainFrame().browsingContext.performActions([
+ {
+ type: SourceActionsType.Key,
+ id: InputId.Keyboard,
+ actions: [
+ {
+ type: ActionType.KeyUp,
+ value: getBidiKeyValue(key),
+ },
+ ],
+ },
+ ]);
}
override async press(
@@ -344,16 +337,13 @@ export class BidiKeyboard extends Keyboard {
type: ActionType.KeyUp,
value: getBidiKeyValue(key),
});
- await this.#page.connection.send('input.performActions', {
- context: this.#page.mainFrame()._id,
- actions: [
- {
- type: SourceActionsType.Key,
- id: InputId.Keyboard,
- actions,
- },
- ],
- });
+ await this.#page.mainFrame().browsingContext.performActions([
+ {
+ type: SourceActionsType.Key,
+ id: InputId.Keyboard,
+ actions,
+ },
+ ]);
}
override async type(
@@ -396,16 +386,13 @@ export class BidiKeyboard extends Keyboard {
);
}
}
- await this.#page.connection.send('input.performActions', {
- context: this.#page.mainFrame()._id,
- actions: [
- {
- type: SourceActionsType.Key,
- id: InputId.Keyboard,
- actions,
- },
- ],
- });
+ await this.#page.mainFrame().browsingContext.performActions([
+ {
+ type: SourceActionsType.Key,
+ id: InputId.Keyboard,
+ actions,
+ },
+ ]);
}
override async sendCharacter(char: string): Promise<void> {
@@ -460,19 +447,17 @@ const getBidiButton = (button: MouseButton) => {
* @internal
*/
export class BidiMouse extends Mouse {
- #context: BrowsingContext;
+ #page: BidiPage;
#lastMovePoint: Point = {x: 0, y: 0};
- constructor(context: BrowsingContext) {
+ constructor(page: BidiPage) {
super();
- this.#context = context;
+ this.#page = page;
}
override async reset(): Promise<void> {
this.#lastMovePoint = {x: 0, y: 0};
- await this.#context.connection.send('input.releaseActions', {
- context: this.#context.id,
- });
+ await this.#page.mainFrame().browsingContext.releaseActions();
}
override async move(
@@ -502,52 +487,43 @@ export class BidiMouse extends Mouse {
});
// https://w3c.github.io/webdriver-bidi/#command-input-performActions:~:text=input.PointerMoveAction%20%3D%20%7B%0A%20%20type%3A%20%22pointerMove%22%2C%0A%20%20x%3A%20js%2Dint%2C
this.#lastMovePoint = to;
- await this.#context.connection.send('input.performActions', {
- context: this.#context.id,
- actions: [
- {
- type: SourceActionsType.Pointer,
- id: InputId.Mouse,
- actions,
- },
- ],
- });
+ await this.#page.mainFrame().browsingContext.performActions([
+ {
+ type: SourceActionsType.Pointer,
+ id: InputId.Mouse,
+ actions,
+ },
+ ]);
}
override async down(options: Readonly<MouseOptions> = {}): Promise<void> {
- await this.#context.connection.send('input.performActions', {
- context: this.#context.id,
- actions: [
- {
- type: SourceActionsType.Pointer,
- id: InputId.Mouse,
- actions: [
- {
- type: ActionType.PointerDown,
- button: getBidiButton(options.button ?? MouseButton.Left),
- },
- ],
- },
- ],
- });
+ await this.#page.mainFrame().browsingContext.performActions([
+ {
+ type: SourceActionsType.Pointer,
+ id: InputId.Mouse,
+ actions: [
+ {
+ type: ActionType.PointerDown,
+ button: getBidiButton(options.button ?? MouseButton.Left),
+ },
+ ],
+ },
+ ]);
}
override async up(options: Readonly<MouseOptions> = {}): Promise<void> {
- await this.#context.connection.send('input.performActions', {
- context: this.#context.id,
- actions: [
- {
- type: SourceActionsType.Pointer,
- id: InputId.Mouse,
- actions: [
- {
- type: ActionType.PointerUp,
- button: getBidiButton(options.button ?? MouseButton.Left),
- },
- ],
- },
- ],
- });
+ await this.#page.mainFrame().browsingContext.performActions([
+ {
+ type: SourceActionsType.Pointer,
+ id: InputId.Mouse,
+ actions: [
+ {
+ type: ActionType.PointerUp,
+ button: getBidiButton(options.button ?? MouseButton.Left),
+ },
+ ],
+ },
+ ]);
}
override async click(
@@ -582,41 +558,35 @@ export class BidiMouse extends Mouse {
});
}
actions.push(pointerUpAction);
- await this.#context.connection.send('input.performActions', {
- context: this.#context.id,
- actions: [
- {
- type: SourceActionsType.Pointer,
- id: InputId.Mouse,
- actions,
- },
- ],
- });
+ await this.#page.mainFrame().browsingContext.performActions([
+ {
+ type: SourceActionsType.Pointer,
+ id: InputId.Mouse,
+ actions,
+ },
+ ]);
}
override async wheel(
options: Readonly<MouseWheelOptions> = {}
): Promise<void> {
- await this.#context.connection.send('input.performActions', {
- context: this.#context.id,
- actions: [
- {
- type: SourceActionsType.Wheel,
- id: InputId.Wheel,
- actions: [
- {
- type: ActionType.Scroll,
- ...(this.#lastMovePoint ?? {
- x: 0,
- y: 0,
- }),
- deltaX: options.deltaX ?? 0,
- deltaY: options.deltaY ?? 0,
- },
- ],
- },
- ],
- });
+ await this.#page.mainFrame().browsingContext.performActions([
+ {
+ type: SourceActionsType.Wheel,
+ id: InputId.Wheel,
+ actions: [
+ {
+ type: ActionType.Scroll,
+ ...(this.#lastMovePoint ?? {
+ x: 0,
+ y: 0,
+ }),
+ deltaX: options.deltaX ?? 0,
+ deltaY: options.deltaY ?? 0,
+ },
+ ],
+ },
+ ]);
}
override drag(): never {
@@ -644,11 +614,11 @@ export class BidiMouse extends Mouse {
* @internal
*/
export class BidiTouchscreen extends Touchscreen {
- #context: BrowsingContext;
+ #page: BidiPage;
- constructor(context: BrowsingContext) {
+ constructor(page: BidiPage) {
super();
- this.#context = context;
+ this.#page = page;
}
override async touchStart(
@@ -656,30 +626,27 @@ export class BidiTouchscreen extends Touchscreen {
y: number,
options: BidiTouchMoveOptions = {}
): Promise<void> {
- await this.#context.connection.send('input.performActions', {
- context: this.#context.id,
- actions: [
- {
- type: SourceActionsType.Pointer,
- id: InputId.Finger,
- parameters: {
- pointerType: Bidi.Input.PointerType.Touch,
- },
- actions: [
- {
- type: ActionType.PointerMove,
- x: Math.round(x),
- y: Math.round(y),
- origin: options.origin,
- },
- {
- type: ActionType.PointerDown,
- button: 0,
- },
- ],
+ await this.#page.mainFrame().browsingContext.performActions([
+ {
+ type: SourceActionsType.Pointer,
+ id: InputId.Finger,
+ parameters: {
+ pointerType: Bidi.Input.PointerType.Touch,
},
- ],
- });
+ actions: [
+ {
+ type: ActionType.PointerMove,
+ x: Math.round(x),
+ y: Math.round(y),
+ origin: options.origin,
+ },
+ {
+ type: ActionType.PointerDown,
+ button: 0,
+ },
+ ],
+ },
+ ]);
}
override async touchMove(
@@ -687,46 +654,40 @@ export class BidiTouchscreen extends Touchscreen {
y: number,
options: BidiTouchMoveOptions = {}
): Promise<void> {
- await this.#context.connection.send('input.performActions', {
- context: this.#context.id,
- actions: [
- {
- type: SourceActionsType.Pointer,
- id: InputId.Finger,
- parameters: {
- pointerType: Bidi.Input.PointerType.Touch,
- },
- actions: [
- {
- type: ActionType.PointerMove,
- x: Math.round(x),
- y: Math.round(y),
- origin: options.origin,
- },
- ],
+ await this.#page.mainFrame().browsingContext.performActions([
+ {
+ type: SourceActionsType.Pointer,
+ id: InputId.Finger,
+ parameters: {
+ pointerType: Bidi.Input.PointerType.Touch,
},
- ],
- });
+ actions: [
+ {
+ type: ActionType.PointerMove,
+ x: Math.round(x),
+ y: Math.round(y),
+ origin: options.origin,
+ },
+ ],
+ },
+ ]);
}
override async touchEnd(): Promise<void> {
- await this.#context.connection.send('input.performActions', {
- context: this.#context.id,
- actions: [
- {
- type: SourceActionsType.Pointer,
- id: InputId.Finger,
- parameters: {
- pointerType: Bidi.Input.PointerType.Touch,
- },
- actions: [
- {
- type: ActionType.PointerUp,
- button: 0,
- },
- ],
+ await this.#page.mainFrame().browsingContext.performActions([
+ {
+ type: SourceActionsType.Pointer,
+ id: InputId.Finger,
+ parameters: {
+ pointerType: Bidi.Input.PointerType.Touch,
},
- ],
- });
+ actions: [
+ {
+ type: ActionType.PointerUp,
+ button: 0,
+ },
+ ],
+ },
+ ]);
}
}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/JSHandle.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/JSHandle.ts
index 7104601553..10f564f78a 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/JSHandle.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/JSHandle.ts
@@ -12,29 +12,28 @@ import {UnsupportedOperation} from '../common/Errors.js';
import {BidiDeserializer} from './Deserializer.js';
import type {BidiRealm} from './Realm.js';
-import type {Sandbox} from './Sandbox.js';
-import {releaseReference} from './util.js';
/**
* @internal
*/
export class BidiJSHandle<T = unknown> extends JSHandle<T> {
- #disposed = false;
- readonly #sandbox: Sandbox;
+ static from<T>(
+ value: Bidi.Script.RemoteValue,
+ realm: BidiRealm
+ ): BidiJSHandle<T> {
+ return new BidiJSHandle(value, realm);
+ }
+
readonly #remoteValue: Bidi.Script.RemoteValue;
- constructor(sandbox: Sandbox, remoteValue: Bidi.Script.RemoteValue) {
- super();
- this.#sandbox = sandbox;
- this.#remoteValue = remoteValue;
- }
+ override readonly realm: BidiRealm;
- context(): BidiRealm {
- return this.realm.environment.context();
- }
+ #disposed = false;
- override get realm(): Sandbox {
- return this.#sandbox;
+ constructor(value: Bidi.Script.RemoteValue, realm: BidiRealm) {
+ super();
+ this.#remoteValue = value;
+ this.realm = realm;
}
override get disposed(): boolean {
@@ -56,12 +55,7 @@ export class BidiJSHandle<T = unknown> extends JSHandle<T> {
return;
}
this.#disposed = true;
- if ('handle' in this.#remoteValue) {
- await releaseReference(
- this.context(),
- this.#remoteValue as Bidi.Script.RemoteReference
- );
- }
+ await this.realm.destroyHandles([this]);
}
get isPrimitiveValue(): boolean {
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/NetworkManager.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/NetworkManager.ts
deleted file mode 100644
index 2caaf0ad50..0000000000
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/NetworkManager.ts
+++ /dev/null
@@ -1,155 +0,0 @@
-/**
- * @license
- * Copyright 2023 Google Inc.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
-
-import {EventEmitter, EventSubscription} from '../common/EventEmitter.js';
-import {
- NetworkManagerEvent,
- type NetworkManagerEvents,
-} from '../common/NetworkManagerEvents.js';
-import {DisposableStack} from '../util/disposable.js';
-
-import type {BidiConnection} from './Connection.js';
-import type {BidiFrame} from './Frame.js';
-import {BidiHTTPRequest} from './HTTPRequest.js';
-import {BidiHTTPResponse} from './HTTPResponse.js';
-import type {BidiPage} from './Page.js';
-
-/**
- * @internal
- */
-export class BidiNetworkManager extends EventEmitter<NetworkManagerEvents> {
- #connection: BidiConnection;
- #page: BidiPage;
- #subscriptions = new DisposableStack();
-
- #requestMap = new Map<string, BidiHTTPRequest>();
- #navigationMap = new Map<string, BidiHTTPResponse>();
-
- constructor(connection: BidiConnection, page: BidiPage) {
- super();
- this.#connection = connection;
- this.#page = page;
-
- // TODO: Subscribe to the Frame individually
- this.#subscriptions.use(
- new EventSubscription(
- this.#connection,
- 'network.beforeRequestSent',
- this.#onBeforeRequestSent.bind(this)
- )
- );
- this.#subscriptions.use(
- new EventSubscription(
- this.#connection,
- 'network.responseStarted',
- this.#onResponseStarted.bind(this)
- )
- );
- this.#subscriptions.use(
- new EventSubscription(
- this.#connection,
- 'network.responseCompleted',
- this.#onResponseCompleted.bind(this)
- )
- );
- this.#subscriptions.use(
- new EventSubscription(
- this.#connection,
- 'network.fetchError',
- this.#onFetchError.bind(this)
- )
- );
- }
-
- #onBeforeRequestSent(event: Bidi.Network.BeforeRequestSentParameters): void {
- const frame = this.#page.frame(event.context ?? '');
- if (!frame) {
- return;
- }
- const request = this.#requestMap.get(event.request.request);
- let upsertRequest: BidiHTTPRequest;
- if (request) {
- request._redirectChain.push(request);
- upsertRequest = new BidiHTTPRequest(event, frame, request._redirectChain);
- } else {
- upsertRequest = new BidiHTTPRequest(event, frame, []);
- }
- this.#requestMap.set(event.request.request, upsertRequest);
- this.emit(NetworkManagerEvent.Request, upsertRequest);
- }
-
- #onResponseStarted(_event: Bidi.Network.ResponseStartedParameters) {}
-
- #onResponseCompleted(event: Bidi.Network.ResponseCompletedParameters): void {
- const request = this.#requestMap.get(event.request.request);
- if (!request) {
- return;
- }
- const response = new BidiHTTPResponse(request, event);
- request._response = response;
- if (event.navigation) {
- this.#navigationMap.set(event.navigation, response);
- }
- if (response.fromCache()) {
- this.emit(NetworkManagerEvent.RequestServedFromCache, request);
- }
- this.emit(NetworkManagerEvent.Response, response);
- this.emit(NetworkManagerEvent.RequestFinished, request);
- }
-
- #onFetchError(event: Bidi.Network.FetchErrorParameters) {
- const request = this.#requestMap.get(event.request.request);
- if (!request) {
- return;
- }
- request._failureText = event.errorText;
- this.emit(NetworkManagerEvent.RequestFailed, request);
- this.#requestMap.delete(event.request.request);
- }
-
- getNavigationResponse(navigationId?: string | null): BidiHTTPResponse | null {
- if (!navigationId) {
- return null;
- }
- const response = this.#navigationMap.get(navigationId);
-
- return response ?? null;
- }
-
- inFlightRequestsCount(): number {
- let inFlightRequestCounter = 0;
- for (const request of this.#requestMap.values()) {
- if (!request.response() || request._failureText) {
- inFlightRequestCounter++;
- }
- }
-
- return inFlightRequestCounter;
- }
-
- clearMapAfterFrameDispose(frame: BidiFrame): void {
- for (const [id, request] of this.#requestMap.entries()) {
- if (request.frame() === frame) {
- this.#requestMap.delete(id);
- }
- }
-
- for (const [id, response] of this.#navigationMap.entries()) {
- if (response.frame() === frame) {
- this.#navigationMap.delete(id);
- }
- }
- }
-
- dispose(): void {
- this.removeAllListeners();
- this.#requestMap.clear();
- this.#navigationMap.clear();
- this.#subscriptions.dispose();
- }
-}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Page.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Page.ts
index 053d23b63a..c662496a18 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Page.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Page.ts
@@ -4,210 +4,115 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import type {Readable} from 'stream';
-
-import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import type Protocol from 'devtools-protocol';
-import {
- firstValueFrom,
- from,
- map,
- raceWith,
- zip,
-} from '../../third_party/rxjs/rxjs.js';
+import {firstValueFrom, from, raceWith} from '../../third_party/rxjs/rxjs.js';
import type {CDPSession} from '../api/CDPSession.js';
import type {BoundingBox} from '../api/ElementHandle.js';
import type {WaitForOptions} from '../api/Frame.js';
import type {HTTPResponse} from '../api/HTTPResponse.js';
+import type {
+ MediaFeature,
+ GeolocationOptions,
+ PageEvents,
+} from '../api/Page.js';
import {
Page,
PageEvent,
- type GeolocationOptions,
- type MediaFeature,
type NewDocumentScriptEvaluation,
type ScreenshotOptions,
} from '../api/Page.js';
import {Accessibility} from '../cdp/Accessibility.js';
import {Coverage} from '../cdp/Coverage.js';
-import {EmulationManager as CdpEmulationManager} from '../cdp/EmulationManager.js';
-import {FrameTree} from '../cdp/FrameTree.js';
+import {EmulationManager} from '../cdp/EmulationManager.js';
import {Tracing} from '../cdp/Tracing.js';
-import {
- ConsoleMessage,
- type ConsoleMessageLocation,
-} from '../common/ConsoleMessage.js';
-import {TargetCloseError, UnsupportedOperation} from '../common/Errors.js';
-import type {Handler} from '../common/EventEmitter.js';
-import {NetworkManagerEvent} from '../common/NetworkManagerEvents.js';
+import type {Cookie, CookieParam, CookieSameSite} from '../common/Cookie.js';
+import type {DeleteCookiesRequest} from '../common/Cookie.js';
+import {UnsupportedOperation} from '../common/Errors.js';
+import {EventEmitter} from '../common/EventEmitter.js';
import type {PDFOptions} from '../common/PDFOptions.js';
import type {Awaitable} from '../common/types.js';
-import {
- debugError,
- evaluationString,
- NETWORK_IDLE_TIME,
- parsePDFOptions,
- timeout,
- validateDialogType,
-} from '../common/util.js';
+import {evaluationString, parsePDFOptions, timeout} from '../common/util.js';
import type {Viewport} from '../common/Viewport.js';
import {assert} from '../util/assert.js';
-import {Deferred} from '../util/Deferred.js';
-import {disposeSymbol} from '../util/disposable.js';
+import {bubble} from '../util/decorators.js';
import {isErrorLike} from '../util/ErrorLike.js';
import type {BidiBrowser} from './Browser.js';
import type {BidiBrowserContext} from './BrowserContext.js';
-import {
- BrowsingContextEvent,
- CdpSessionWrapper,
- type BrowsingContext,
-} from './BrowsingContext.js';
-import type {BidiConnection} from './Connection.js';
-import {BidiDeserializer} from './Deserializer.js';
-import {BidiDialog} from './Dialog.js';
+import type {BidiCdpSession} from './CDPSession.js';
+import type {BrowsingContext} from './core/BrowsingContext.js';
import {BidiElementHandle} from './ElementHandle.js';
-import {EmulationManager} from './EmulationManager.js';
import {BidiFrame} from './Frame.js';
-import type {BidiHTTPRequest} from './HTTPRequest.js';
import type {BidiHTTPResponse} from './HTTPResponse.js';
import {BidiKeyboard, BidiMouse, BidiTouchscreen} from './Input.js';
import type {BidiJSHandle} from './JSHandle.js';
-import {getBiDiReadinessState, rewriteNavigationError} from './lifecycle.js';
-import {BidiNetworkManager} from './NetworkManager.js';
-import {createBidiHandle} from './Realm.js';
-import type {BiDiPageTarget} from './Target.js';
+import {rewriteNavigationError} from './util.js';
+import type {BidiWebWorker} from './WebWorker.js';
/**
* @internal
*/
export class BidiPage extends Page {
- #accessibility: Accessibility;
- #connection: BidiConnection;
- #frameTree = new FrameTree<BidiFrame>();
- #networkManager: BidiNetworkManager;
- #viewport: Viewport | null = null;
- #closedDeferred = Deferred.create<never, TargetCloseError>();
- #subscribedEvents = new Map<Bidi.Event['method'], Handler<any>>([
- ['log.entryAdded', this.#onLogEntryAdded.bind(this)],
- ['browsingContext.load', this.#onFrameLoaded.bind(this)],
- [
- 'browsingContext.fragmentNavigated',
- this.#onFrameFragmentNavigated.bind(this),
- ],
- [
- 'browsingContext.domContentLoaded',
- this.#onFrameDOMContentLoaded.bind(this),
- ],
- ['browsingContext.userPromptOpened', this.#onDialog.bind(this)],
- ]);
- readonly #networkManagerEvents = [
- [
- NetworkManagerEvent.Request,
- (request: BidiHTTPRequest) => {
- this.emit(PageEvent.Request, request);
- },
- ],
- [
- NetworkManagerEvent.RequestServedFromCache,
- (request: BidiHTTPRequest) => {
- this.emit(PageEvent.RequestServedFromCache, request);
- },
- ],
- [
- NetworkManagerEvent.RequestFailed,
- (request: BidiHTTPRequest) => {
- this.emit(PageEvent.RequestFailed, request);
- },
- ],
- [
- NetworkManagerEvent.RequestFinished,
- (request: BidiHTTPRequest) => {
- this.emit(PageEvent.RequestFinished, request);
- },
- ],
- [
- NetworkManagerEvent.Response,
- (response: BidiHTTPResponse) => {
- this.emit(PageEvent.Response, response);
- },
- ],
- ] as const;
-
- readonly #browsingContextEvents = new Map<symbol, Handler<any>>([
- [BrowsingContextEvent.Created, this.#onContextCreated.bind(this)],
- [BrowsingContextEvent.Destroyed, this.#onContextDestroyed.bind(this)],
- ]);
- #tracing: Tracing;
- #coverage: Coverage;
- #cdpEmulationManager: CdpEmulationManager;
- #emulationManager: EmulationManager;
- #mouse: BidiMouse;
- #touchscreen: BidiTouchscreen;
- #keyboard: BidiKeyboard;
- #browsingContext: BrowsingContext;
- #browserContext: BidiBrowserContext;
- #target: BiDiPageTarget;
-
- _client(): CDPSession {
- return this.mainFrame().context().cdpSession;
- }
-
- constructor(
- browsingContext: BrowsingContext,
+ static from(
browserContext: BidiBrowserContext,
- target: BiDiPageTarget
- ) {
- super();
- this.#browsingContext = browsingContext;
- this.#browserContext = browserContext;
- this.#target = target;
- this.#connection = browsingContext.connection;
+ browsingContext: BrowsingContext
+ ): BidiPage {
+ const page = new BidiPage(browserContext, browsingContext);
+ page.#initialize();
+ return page;
+ }
- for (const [event, subscriber] of this.#browsingContextEvents) {
- this.#browsingContext.on(event, subscriber);
- }
+ @bubble()
+ accessor trustedEmitter = new EventEmitter<PageEvents>();
- this.#networkManager = new BidiNetworkManager(this.#connection, this);
+ readonly #browserContext: BidiBrowserContext;
+ readonly #frame: BidiFrame;
+ #viewport: Viewport | null = null;
+ readonly #workers = new Set<BidiWebWorker>();
- for (const [event, subscriber] of this.#subscribedEvents) {
- this.#connection.on(event, subscriber);
- }
+ readonly keyboard: BidiKeyboard;
+ readonly mouse: BidiMouse;
+ readonly touchscreen: BidiTouchscreen;
+ readonly accessibility: Accessibility;
+ readonly tracing: Tracing;
+ readonly coverage: Coverage;
+ readonly #cdpEmulationManager: EmulationManager;
- for (const [event, subscriber] of this.#networkManagerEvents) {
- // TODO: remove any
- this.#networkManager.on(event, subscriber as any);
- }
+ _client(): BidiCdpSession {
+ return this.#frame.client;
+ }
- const frame = new BidiFrame(
- this,
- this.#browsingContext,
- this._timeoutSettings,
- this.#browsingContext.parent
- );
- this.#frameTree.addFrame(frame);
- this.emit(PageEvent.FrameAttached, frame);
+ private constructor(
+ browserContext: BidiBrowserContext,
+ browsingContext: BrowsingContext
+ ) {
+ super();
+ this.#browserContext = browserContext;
+ this.#frame = BidiFrame.from(this, browsingContext);
- // TODO: https://github.com/w3c/webdriver-bidi/issues/443
- this.#accessibility = new Accessibility(
- this.mainFrame().context().cdpSession
- );
- this.#tracing = new Tracing(this.mainFrame().context().cdpSession);
- this.#coverage = new Coverage(this.mainFrame().context().cdpSession);
- this.#cdpEmulationManager = new CdpEmulationManager(
- this.mainFrame().context().cdpSession
- );
- this.#emulationManager = new EmulationManager(browsingContext);
- this.#mouse = new BidiMouse(this.mainFrame().context());
- this.#touchscreen = new BidiTouchscreen(this.mainFrame().context());
- this.#keyboard = new BidiKeyboard(this);
+ this.#cdpEmulationManager = new EmulationManager(this.#frame.client);
+ this.accessibility = new Accessibility(this.#frame.client);
+ this.tracing = new Tracing(this.#frame.client);
+ this.coverage = new Coverage(this.#frame.client);
+ this.keyboard = new BidiKeyboard(this);
+ this.mouse = new BidiMouse(this);
+ this.touchscreen = new BidiTouchscreen(this);
}
- /**
- * @internal
- */
- get connection(): BidiConnection {
- return this.#connection;
+ #initialize() {
+ this.#frame.browsingContext.on('closed', () => {
+ this.trustedEmitter.emit(PageEvent.Close, undefined);
+ this.trustedEmitter.removeAllListeners();
+ });
+
+ this.trustedEmitter.on(PageEvent.WorkerCreated, worker => {
+ this.#workers.add(worker as BidiWebWorker);
+ });
+ this.trustedEmitter.on(PageEvent.WorkerDestroyed, worker => {
+ this.#workers.delete(worker as BidiWebWorker);
+ });
}
override async setUserAgent(
@@ -234,46 +139,15 @@ export class BidiPage extends Page {
prototypeHandle.id,
'Prototype JSHandle must not be referencing primitive value'
);
- const response = await this.mainFrame().client.send(
- 'Runtime.queryObjects',
- {
- prototypeObjectId: prototypeHandle.id,
- }
- );
- return createBidiHandle(this.mainFrame().mainRealm(), {
+ const response = await this.#frame.client.send('Runtime.queryObjects', {
+ prototypeObjectId: prototypeHandle.id,
+ });
+ return this.#frame.mainRealm().createHandle({
type: 'array',
handle: response.objects.objectId,
}) as BidiJSHandle<Prototype[]>;
}
- _setBrowserContext(browserContext: BidiBrowserContext): void {
- this.#browserContext = browserContext;
- }
-
- override get accessibility(): Accessibility {
- return this.#accessibility;
- }
-
- override get tracing(): Tracing {
- return this.#tracing;
- }
-
- override get coverage(): Coverage {
- return this.#coverage;
- }
-
- override get mouse(): BidiMouse {
- return this.#mouse;
- }
-
- override get touchscreen(): BidiTouchscreen {
- return this.#touchscreen;
- }
-
- override get keyboard(): BidiKeyboard {
- return this.#keyboard;
- }
-
override browser(): BidiBrowser {
return this.browserContext().browser();
}
@@ -283,14 +157,9 @@ export class BidiPage extends Page {
}
override mainFrame(): BidiFrame {
- const mainFrame = this.#frameTree.getMainFrame();
- assert(mainFrame, 'Requesting main frame too early!');
- return mainFrame;
+ return this.#frame;
}
- /**
- * @internal
- */
async focusedFrame(): Promise<BidiFrame> {
using frame = await this.mainFrame()
.isolatedRealm()
@@ -310,216 +179,38 @@ export class BidiPage extends Page {
}
override frames(): BidiFrame[] {
- return Array.from(this.#frameTree.frames());
- }
-
- frame(frameId?: string): BidiFrame | null {
- return this.#frameTree.getById(frameId ?? '') || null;
- }
-
- childFrames(frameId: string): BidiFrame[] {
- return this.#frameTree.childFrames(frameId);
- }
-
- #onFrameLoaded(info: Bidi.BrowsingContext.NavigationInfo): void {
- const frame = this.frame(info.context);
- if (frame && this.mainFrame() === frame) {
- this.emit(PageEvent.Load, undefined);
- }
- }
-
- #onFrameFragmentNavigated(info: Bidi.BrowsingContext.NavigationInfo): void {
- const frame = this.frame(info.context);
- if (frame) {
- this.emit(PageEvent.FrameNavigated, frame);
- }
- }
-
- #onFrameDOMContentLoaded(info: Bidi.BrowsingContext.NavigationInfo): void {
- const frame = this.frame(info.context);
- if (frame) {
- frame._hasStartedLoading = true;
- if (this.mainFrame() === frame) {
- this.emit(PageEvent.DOMContentLoaded, undefined);
- }
- this.emit(PageEvent.FrameNavigated, frame);
- }
- }
-
- #onContextCreated(context: BrowsingContext): void {
- if (
- !this.frame(context.id) &&
- (this.frame(context.parent ?? '') || !this.#frameTree.getMainFrame())
- ) {
- const frame = new BidiFrame(
- this,
- context,
- this._timeoutSettings,
- context.parent
- );
- this.#frameTree.addFrame(frame);
- if (frame !== this.mainFrame()) {
- this.emit(PageEvent.FrameAttached, frame);
- }
- }
- }
-
- #onContextDestroyed(context: BrowsingContext): void {
- const frame = this.frame(context.id);
-
- if (frame) {
- if (frame === this.mainFrame()) {
- this.emit(PageEvent.Close, undefined);
- }
- this.#removeFramesRecursively(frame);
- }
- }
-
- #removeFramesRecursively(frame: BidiFrame): void {
- for (const child of frame.childFrames()) {
- this.#removeFramesRecursively(child);
- }
- frame[disposeSymbol]();
- this.#networkManager.clearMapAfterFrameDispose(frame);
- this.#frameTree.removeFrame(frame);
- this.emit(PageEvent.FrameDetached, frame);
- }
-
- #onLogEntryAdded(event: Bidi.Log.Entry): void {
- const frame = this.frame(event.source.context);
- if (!frame) {
- return;
- }
- if (isConsoleLogEntry(event)) {
- const args = event.args.map(arg => {
- return createBidiHandle(frame.mainRealm(), arg);
- });
-
- const text = args
- .reduce((value, arg) => {
- const parsedValue = arg.isPrimitiveValue
- ? BidiDeserializer.deserialize(arg.remoteValue())
- : arg.toString();
- return `${value} ${parsedValue}`;
- }, '')
- .slice(1);
-
- this.emit(
- PageEvent.Console,
- new ConsoleMessage(
- event.method as any,
- text,
- args,
- getStackTraceLocations(event.stackTrace)
- )
- );
- } else if (isJavaScriptLogEntry(event)) {
- const error = new Error(event.text ?? '');
-
- const messageHeight = error.message.split('\n').length;
- const messageLines = error.stack!.split('\n').splice(0, messageHeight);
-
- const stackLines = [];
- if (event.stackTrace) {
- for (const frame of event.stackTrace.callFrames) {
- // Note we need to add `1` because the values are 0-indexed.
- stackLines.push(
- ` at ${frame.functionName || '<anonymous>'} (${frame.url}:${
- frame.lineNumber + 1
- }:${frame.columnNumber + 1})`
- );
- if (stackLines.length >= Error.stackTraceLimit) {
- break;
- }
- }
- }
-
- error.stack = [...messageLines, ...stackLines].join('\n');
- this.emit(PageEvent.PageError, error);
- } else {
- debugError(
- `Unhandled LogEntry with type "${event.type}", text "${event.text}" and level "${event.level}"`
- );
- }
- }
-
- #onDialog(event: Bidi.BrowsingContext.UserPromptOpenedParameters): void {
- const frame = this.frame(event.context);
- if (!frame) {
- return;
+ const frames = [this.#frame];
+ for (const frame of frames) {
+ frames.push(...frame.childFrames());
}
- const type = validateDialogType(event.type);
-
- const dialog = new BidiDialog(
- frame.context(),
- type,
- event.message,
- event.defaultValue
- );
- this.emit(PageEvent.Dialog, dialog);
- }
-
- getNavigationResponse(id?: string | null): BidiHTTPResponse | null {
- return this.#networkManager.getNavigationResponse(id);
+ return frames;
}
override isClosed(): boolean {
- return this.#closedDeferred.finished();
+ return this.#frame.detached;
}
override async close(options?: {runBeforeUnload?: boolean}): Promise<void> {
- if (this.#closedDeferred.finished()) {
+ try {
+ await this.#frame.browsingContext.close(options?.runBeforeUnload);
+ } catch {
return;
}
-
- this.#closedDeferred.reject(new TargetCloseError('Page closed!'));
- this.#networkManager.dispose();
-
- await this.#connection.send('browsingContext.close', {
- context: this.mainFrame()._id,
- promptUnload: options?.runBeforeUnload ?? false,
- });
-
- this.emit(PageEvent.Close, undefined);
- this.removeAllListeners();
}
override async reload(
options: WaitForOptions = {}
): Promise<BidiHTTPResponse | null> {
- const {
- waitUntil = 'load',
- timeout: ms = this._timeoutSettings.navigationTimeout(),
- } = options;
-
- const [readiness, networkIdle] = getBiDiReadinessState(waitUntil);
-
- const result$ = zip(
- from(
- this.#connection.send('browsingContext.reload', {
- context: this.mainFrame()._id,
- wait: readiness,
- })
- ),
- ...(networkIdle !== null
- ? [
- this.waitForNetworkIdle$({
- timeout: ms,
- concurrency: networkIdle === 'networkidle2' ? 2 : 0,
- idleTime: NETWORK_IDLE_TIME,
- }),
- ]
- : [])
- ).pipe(
- map(([{result}]) => {
- return result;
- }),
- raceWith(timeout(ms), from(this.#closedDeferred.valueOrThrow())),
- rewriteNavigationError(this.url(), ms)
+ const [response] = await Promise.all([
+ this.#frame.waitForNavigation(options),
+ this.#frame.browsingContext.reload(),
+ ]).catch(
+ rewriteNavigationError(
+ this.url(),
+ options.timeout ?? this._timeoutSettings.navigationTimeout()
+ )
);
-
- const result = await firstValueFrom(result$);
- return this.getNavigationResponse(result.navigation);
+ return response;
}
override setDefaultNavigationTimeout(timeout: number): void {
@@ -578,8 +269,19 @@ export class BidiPage extends Page {
}
override async setViewport(viewport: Viewport): Promise<void> {
- if (!this.#browsingContext.supportsCdp()) {
- await this.#emulationManager.emulateViewport(viewport);
+ if (!this.browser().cdpSupported) {
+ await this.#frame.browsingContext.setViewport({
+ viewport:
+ viewport.width && viewport.height
+ ? {
+ width: viewport.width,
+ height: viewport.height,
+ }
+ : null,
+ devicePixelRatio: viewport.deviceScaleFactor
+ ? viewport.deviceScaleFactor
+ : null,
+ });
this.#viewport = viewport;
return;
}
@@ -609,10 +311,9 @@ export class BidiPage extends Page {
preferCSSPageSize,
} = parsePDFOptions(options, 'cm');
const pageRanges = ranges ? ranges.split(', ') : [];
- const {result} = await firstValueFrom(
+ const data = await firstValueFrom(
from(
- this.#connection.send('browsingContext.print', {
- context: this.mainFrame()._id,
+ this.#frame.browsingContext.print({
background,
margin,
orientation: landscape ? 'landscape' : 'portrait',
@@ -627,7 +328,7 @@ export class BidiPage extends Page {
).pipe(raceWith(timeout(ms)))
);
- const buffer = Buffer.from(result.data, 'base64');
+ const buffer = Buffer.from(data, 'base64');
await this._maybeWriteBufferToFile(path, buffer);
@@ -636,19 +337,15 @@ export class BidiPage extends Page {
override async createPDFStream(
options?: PDFOptions | undefined
- ): Promise<Readable> {
+ ): Promise<ReadableStream<Uint8Array>> {
const buffer = await this.pdf(options);
- try {
- const {Readable} = await import('stream');
- return Readable.from(buffer);
- } catch (error) {
- if (error instanceof TypeError) {
- throw new Error(
- 'Can only pass a file path in a Node-like environment.'
- );
- }
- throw error;
- }
+
+ return new ReadableStream({
+ start(controller) {
+ controller.enqueue(buffer);
+ controller.close();
+ },
+ });
}
override async _screenshot(
@@ -697,10 +394,7 @@ export class BidiPage extends Page {
}
}
- const {
- result: {data},
- } = await this.#connection.send('browsingContext.captureScreenshot', {
- context: this.mainFrame()._id,
+ const data = await this.#frame.browsingContext.captureScreenshot({
origin: captureBeyondViewport ? 'document' : 'viewport',
format: {
type: `image/${type}`,
@@ -712,19 +406,11 @@ export class BidiPage extends Page {
}
override async createCDPSession(): Promise<CDPSession> {
- const {sessionId} = await this.mainFrame()
- .context()
- .cdpSession.send('Target.attachToTarget', {
- targetId: this.mainFrame()._id,
- flatten: true,
- });
- return new CdpSessionWrapper(this.mainFrame().context(), sessionId);
+ return await this.#frame.createCDPSession();
}
override async bringToFront(): Promise<void> {
- await this.#connection.send('browsingContext.activate', {
- context: this.mainFrame()._id,
- });
+ await this.#frame.browsingContext.activate();
}
override async evaluateOnNewDocument<
@@ -735,20 +421,16 @@ export class BidiPage extends Page {
...args: Params
): Promise<NewDocumentScriptEvaluation> {
const expression = evaluationExpression(pageFunction, ...args);
- const {result} = await this.#connection.send('script.addPreloadScript', {
- functionDeclaration: expression,
- contexts: [this.mainFrame()._id],
- });
+ const script =
+ await this.#frame.browsingContext.addPreloadScript(expression);
- return {identifier: result.script};
+ return {identifier: script};
}
override async removeScriptToEvaluateOnNewDocument(
id: string
): Promise<void> {
- await this.#connection.send('script.removePreloadScript', {
- script: id,
- });
+ await this.#frame.browsingContext.removePreloadScript(id);
}
override async exposeFunction<Args extends unknown[], Ret>(
@@ -774,20 +456,37 @@ export class BidiPage extends Page {
});
}
+ override async cookies(...urls: string[]): Promise<Cookie[]> {
+ const normalizedUrls = (urls.length ? urls : [this.url()]).map(url => {
+ return new URL(url);
+ });
+
+ const cookies = await this.#frame.browsingContext.getCookies();
+ return cookies
+ .map(cookie => {
+ return bidiToPuppeteerCookie(cookie);
+ })
+ .filter(cookie => {
+ return normalizedUrls.some(url => {
+ return testUrlMatchCookie(cookie, url);
+ });
+ });
+ }
+
override isServiceWorkerBypassed(): never {
throw new UnsupportedOperation();
}
- override target(): BiDiPageTarget {
- return this.#target;
+ override target(): never {
+ throw new UnsupportedOperation();
}
override waitForFileChooser(): never {
throw new UnsupportedOperation();
}
- override workers(): never {
- throw new UnsupportedOperation();
+ override workers(): BidiWebWorker[] {
+ return [...this.#workers];
}
override setRequestInterception(): never {
@@ -810,21 +509,98 @@ export class BidiPage extends Page {
throw new UnsupportedOperation();
}
- override cookies(): never {
- throw new UnsupportedOperation();
- }
+ override async setCookie(...cookies: CookieParam[]): Promise<void> {
+ const pageURL = this.url();
+ const pageUrlStartsWithHTTP = pageURL.startsWith('http');
+ for (const cookie of cookies) {
+ let cookieUrl = cookie.url || '';
+ if (!cookieUrl && pageUrlStartsWithHTTP) {
+ cookieUrl = pageURL;
+ }
+ assert(
+ cookieUrl !== 'about:blank',
+ `Blank page can not have cookie "${cookie.name}"`
+ );
+ assert(
+ !String.prototype.startsWith.call(cookieUrl || '', 'data:'),
+ `Data URL page can not have cookie "${cookie.name}"`
+ );
- override setCookie(): never {
- throw new UnsupportedOperation();
+ const normalizedUrl = URL.canParse(cookieUrl)
+ ? new URL(cookieUrl)
+ : undefined;
+
+ const domain = cookie.domain ?? normalizedUrl?.hostname;
+ assert(
+ domain !== undefined,
+ `At least one of the url and domain needs to be specified`
+ );
+
+ const bidiCookie: Bidi.Storage.PartialCookie = {
+ domain: domain,
+ name: cookie.name,
+ value: {
+ type: 'string',
+ value: cookie.value,
+ },
+ ...(cookie.path !== undefined ? {path: cookie.path} : {}),
+ ...(cookie.httpOnly !== undefined ? {httpOnly: cookie.httpOnly} : {}),
+ ...(cookie.secure !== undefined ? {secure: cookie.secure} : {}),
+ ...(cookie.sameSite !== undefined
+ ? {sameSite: convertCookiesSameSiteCdpToBiDi(cookie.sameSite)}
+ : {}),
+ ...(cookie.expires !== undefined ? {expiry: cookie.expires} : {}),
+ // Chrome-specific properties.
+ ...cdpSpecificCookiePropertiesFromPuppeteerToBidi(
+ cookie,
+ 'sameParty',
+ 'sourceScheme',
+ 'priority',
+ 'url'
+ ),
+ };
+
+ if (cookie.partitionKey !== undefined) {
+ await this.browserContext().userContext.setCookie(
+ bidiCookie,
+ cookie.partitionKey
+ );
+ } else {
+ await this.#frame.browsingContext.setCookie(bidiCookie);
+ }
+ }
}
- override deleteCookie(): never {
- throw new UnsupportedOperation();
+ override async deleteCookie(
+ ...cookies: DeleteCookiesRequest[]
+ ): Promise<void> {
+ await Promise.all(
+ cookies.map(async deleteCookieRequest => {
+ const cookieUrl = deleteCookieRequest.url ?? this.url();
+ const normalizedUrl = URL.canParse(cookieUrl)
+ ? new URL(cookieUrl)
+ : undefined;
+
+ const domain = deleteCookieRequest.domain ?? normalizedUrl?.hostname;
+ assert(
+ domain !== undefined,
+ `At least one of the url and domain needs to be specified`
+ );
+
+ const filter = {
+ domain: domain,
+ name: deleteCookieRequest.name,
+ ...(deleteCookieRequest.path !== undefined
+ ? {path: deleteCookieRequest.path}
+ : {}),
+ };
+ await this.#frame.browsingContext.deleteCookie(filter);
+ })
+ );
}
- override removeExposedFunction(): never {
- // TODO: Quick win?
- throw new UnsupportedOperation();
+ override async removeExposedFunction(name: string): Promise<void> {
+ await this.#frame.removeExposedFunction(name);
}
override authenticate(): never {
@@ -848,7 +624,7 @@ export class BidiPage extends Page {
override async goForward(
options: WaitForOptions = {}
): Promise<HTTPResponse | null> {
- return await this.#go(+1, options);
+ return await this.#go(1, options);
}
async #go(
@@ -856,22 +632,19 @@ export class BidiPage extends Page {
options: WaitForOptions
): Promise<HTTPResponse | null> {
try {
- const result = await Promise.all([
+ const [response] = await Promise.all([
this.waitForNavigation(options),
- this.#connection.send('browsingContext.traverseHistory', {
- delta,
- context: this.mainFrame()._id,
- }),
+ this.#frame.browsingContext.traverseHistory(delta),
]);
- return result[0];
- } catch (err) {
+ return response;
+ } catch (error) {
// TODO: waitForNavigation should be cancelled if an error happens.
- if (isErrorLike(err)) {
- if (err.message.includes('no such history entry')) {
+ if (isErrorLike(error)) {
+ if (error.message.includes('no such history entry')) {
return null;
}
}
- throw err;
+ throw error;
}
}
@@ -880,34 +653,137 @@ export class BidiPage extends Page {
}
}
-function isConsoleLogEntry(
- event: Bidi.Log.Entry
-): event is Bidi.Log.ConsoleLogEntry {
- return event.type === 'console';
+function evaluationExpression(fun: Function | string, ...args: unknown[]) {
+ return `() => {${evaluationString(fun, ...args)}}`;
}
-function isJavaScriptLogEntry(
- event: Bidi.Log.Entry
-): event is Bidi.Log.JavascriptLogEntry {
- return event.type === 'javascript';
+/**
+ * Check domains match.
+ * According to cookies spec, this check should match subdomains as well, but CDP
+ * implementation does not do that, so this method matches only the exact domains, not
+ * what is written in the spec:
+ * https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.3
+ */
+function testUrlMatchCookieHostname(
+ cookie: Cookie,
+ normalizedUrl: URL
+): boolean {
+ const cookieDomain = cookie.domain.toLowerCase();
+ const urlHostname = normalizedUrl.hostname.toLowerCase();
+ return cookieDomain === urlHostname;
}
-function getStackTraceLocations(
- stackTrace?: Bidi.Script.StackTrace
-): ConsoleMessageLocation[] {
- const stackTraceLocations: ConsoleMessageLocation[] = [];
- if (stackTrace) {
- for (const callFrame of stackTrace.callFrames) {
- stackTraceLocations.push({
- url: callFrame.url,
- lineNumber: callFrame.lineNumber,
- columnNumber: callFrame.columnNumber,
- });
+/**
+ * Check paths match.
+ * Spec: https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.4
+ */
+function testUrlMatchCookiePath(cookie: Cookie, normalizedUrl: URL): boolean {
+ const uriPath = normalizedUrl.pathname;
+ const cookiePath = cookie.path;
+
+ if (uriPath === cookiePath) {
+ // The cookie-path and the request-path are identical.
+ return true;
+ }
+ if (uriPath.startsWith(cookiePath)) {
+ // The cookie-path is a prefix of the request-path.
+ if (cookiePath.endsWith('/')) {
+ // The last character of the cookie-path is %x2F ("/").
+ return true;
+ }
+ if (uriPath[cookiePath.length] === '/') {
+ // The first character of the request-path that is not included in the cookie-path
+ // is a %x2F ("/") character.
+ return true;
}
}
- return stackTraceLocations;
+ return false;
}
-function evaluationExpression(fun: Function | string, ...args: unknown[]) {
- return `() => {${evaluationString(fun, ...args)}}`;
+/**
+ * Checks the cookie matches the URL according to the spec:
+ */
+function testUrlMatchCookie(cookie: Cookie, url: URL): boolean {
+ const normalizedUrl = new URL(url);
+ assert(cookie !== undefined);
+ if (!testUrlMatchCookieHostname(cookie, normalizedUrl)) {
+ return false;
+ }
+ return testUrlMatchCookiePath(cookie, normalizedUrl);
+}
+
+function bidiToPuppeteerCookie(bidiCookie: Bidi.Network.Cookie): Cookie {
+ return {
+ name: bidiCookie.name,
+ // Presents binary value as base64 string.
+ value: bidiCookie.value.value,
+ domain: bidiCookie.domain,
+ path: bidiCookie.path,
+ size: bidiCookie.size,
+ httpOnly: bidiCookie.httpOnly,
+ secure: bidiCookie.secure,
+ sameSite: convertCookiesSameSiteBiDiToCdp(bidiCookie.sameSite),
+ expires: bidiCookie.expiry ?? -1,
+ session: bidiCookie.expiry === undefined || bidiCookie.expiry <= 0,
+ // Extending with CDP-specific properties with `goog:` prefix.
+ ...cdpSpecificCookiePropertiesFromBidiToPuppeteer(
+ bidiCookie,
+ 'sameParty',
+ 'sourceScheme',
+ 'partitionKey',
+ 'partitionKeyOpaque',
+ 'priority'
+ ),
+ };
+}
+
+const CDP_SPECIFIC_PREFIX = 'goog:';
+
+/**
+ * Gets CDP-specific properties from the BiDi cookie and returns them as a new object.
+ */
+function cdpSpecificCookiePropertiesFromBidiToPuppeteer(
+ bidiCookie: Bidi.Network.Cookie,
+ ...propertyNames: Array<keyof Cookie>
+): Partial<Cookie> {
+ const result: Partial<Cookie> = {};
+ for (const property of propertyNames) {
+ if (bidiCookie[CDP_SPECIFIC_PREFIX + property] !== undefined) {
+ result[property] = bidiCookie[CDP_SPECIFIC_PREFIX + property];
+ }
+ }
+ return result;
+}
+
+/**
+ * Gets CDP-specific properties from the cookie, adds CDP-specific prefixes and returns
+ * them as a new object which can be used in BiDi.
+ */
+function cdpSpecificCookiePropertiesFromPuppeteerToBidi(
+ cookieParam: CookieParam,
+ ...propertyNames: Array<keyof CookieParam>
+): Record<string, unknown> {
+ const result: Record<string, unknown> = {};
+ for (const property of propertyNames) {
+ if (cookieParam[property] !== undefined) {
+ result[CDP_SPECIFIC_PREFIX + property] = cookieParam[property];
+ }
+ }
+ return result;
+}
+
+function convertCookiesSameSiteBiDiToCdp(
+ sameSite: Bidi.Network.SameSite | undefined
+): CookieSameSite {
+ return sameSite === 'strict' ? 'Strict' : sameSite === 'lax' ? 'Lax' : 'None';
+}
+
+function convertCookiesSameSiteCdpToBiDi(
+ sameSite: CookieSameSite | undefined
+): Bidi.Network.SameSite {
+ return sameSite === 'Strict'
+ ? Bidi.Network.SameSite.Strict
+ : sameSite === 'Lax'
+ ? Bidi.Network.SameSite.Lax
+ : Bidi.Network.SameSite.None;
}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Realm.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Realm.ts
index 84f13bc703..1027941e2f 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Realm.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Realm.ts
@@ -1,80 +1,63 @@
+/**
+ * @license
+ * Copyright 2024 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
-import {EventEmitter, type EventType} from '../common/EventEmitter.js';
+import type {JSHandle} from '../api/JSHandle.js';
+import {Realm} from '../api/Realm.js';
+import {ARIAQueryHandler} from '../cdp/AriaQueryHandler.js';
+import {LazyArg} from '../common/LazyArg.js';
import {scriptInjector} from '../common/ScriptInjector.js';
+import type {TimeoutSettings} from '../common/TimeoutSettings.js';
import type {EvaluateFunc, HandleFor} from '../common/types.js';
import {
- PuppeteerURL,
- SOURCE_URL_REGEX,
+ debugError,
getSourcePuppeteerURLIfAvailable,
getSourceUrlComment,
isString,
+ PuppeteerURL,
+ SOURCE_URL_REGEX,
} from '../common/util.js';
import type PuppeteerUtil from '../injected/injected.js';
-import {disposeSymbol} from '../util/disposable.js';
+import {AsyncIterableUtil} from '../util/AsyncIterableUtil.js';
import {stringifyFunction} from '../util/Function.js';
-import type {BidiConnection} from './Connection.js';
+import type {
+ Realm as BidiRealmCore,
+ DedicatedWorkerRealm,
+ SharedWorkerRealm,
+} from './core/Realm.js';
+import type {WindowRealm} from './core/Realm.js';
import {BidiDeserializer} from './Deserializer.js';
import {BidiElementHandle} from './ElementHandle.js';
+import {ExposeableFunction} from './ExposedFunction.js';
+import type {BidiFrame} from './Frame.js';
import {BidiJSHandle} from './JSHandle.js';
-import type {Sandbox} from './Sandbox.js';
import {BidiSerializer} from './Serializer.js';
import {createEvaluationError} from './util.js';
+import type {BidiWebWorker} from './WebWorker.js';
/**
* @internal
*/
-export class BidiRealm extends EventEmitter<Record<EventType, any>> {
- readonly connection: BidiConnection;
-
- #id!: string;
- #sandbox!: Sandbox;
+export abstract class BidiRealm extends Realm {
+ readonly realm: BidiRealmCore;
- constructor(connection: BidiConnection) {
- super();
- this.connection = connection;
+ constructor(realm: BidiRealmCore, timeoutSettings: TimeoutSettings) {
+ super(timeoutSettings);
+ this.realm = realm;
}
- get target(): Bidi.Script.Target {
- return {
- context: this.#sandbox.environment._id,
- sandbox: this.#sandbox.name,
- };
- }
-
- handleRealmDestroyed = async (
- params: Bidi.Script.RealmDestroyed['params']
- ): Promise<void> => {
- if (params.realm === this.#id) {
- // Note: The Realm is destroyed, so in theory the handle should be as
- // well.
+ protected initialize(): void {
+ this.realm.on('destroyed', ({reason}) => {
+ this.taskManager.terminateAll(new Error(reason));
+ });
+ this.realm.on('updated', () => {
this.internalPuppeteerUtil = undefined;
- this.#sandbox.environment.clearDocumentHandle();
- }
- };
-
- handleRealmCreated = (params: Bidi.Script.RealmCreated['params']): void => {
- if (
- params.type === 'window' &&
- params.context === this.#sandbox.environment._id &&
- params.sandbox === this.#sandbox.name
- ) {
- this.#id = params.realm;
- void this.#sandbox.taskManager.rerunAll();
- }
- };
-
- setSandbox(sandbox: Sandbox): void {
- this.#sandbox = sandbox;
- this.connection.on(
- Bidi.ChromiumBidi.Script.EventNames.RealmCreated,
- this.handleRealmCreated
- );
- this.connection.on(
- Bidi.ChromiumBidi.Script.EventNames.RealmDestroyed,
- this.handleRealmDestroyed
- );
+ void this.taskManager.rerunAll();
+ });
}
protected internalPuppeteerUtil?: Promise<BidiJSHandle<PuppeteerUtil>>;
@@ -95,7 +78,7 @@ export class BidiRealm extends EventEmitter<Record<EventType, any>> {
return this.internalPuppeteerUtil as Promise<BidiJSHandle<PuppeteerUtil>>;
}
- async evaluateHandle<
+ override async evaluateHandle<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(
@@ -105,7 +88,7 @@ export class BidiRealm extends EventEmitter<Record<EventType, any>> {
return await this.#evaluate(false, pageFunction, ...args);
}
- async evaluate<
+ override async evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(
@@ -144,8 +127,6 @@ export class BidiRealm extends EventEmitter<Record<EventType, any>> {
PuppeteerURL.INTERNAL_URL
);
- const sandbox = this.#sandbox;
-
let responsePromise;
const resultOwnership = returnByValue
? Bidi.Script.ResultOwnership.None
@@ -161,11 +142,8 @@ export class BidiRealm extends EventEmitter<Record<EventType, any>> {
? pageFunction
: `${pageFunction}\n${sourceUrlComment}\n`;
- responsePromise = this.connection.send('script.evaluate', {
- expression,
- target: this.target,
+ responsePromise = this.realm.evaluate(expression, true, {
resultOwnership,
- awaitPromise: true,
userActivation: true,
serializationOptions,
});
@@ -174,24 +152,25 @@ export class BidiRealm extends EventEmitter<Record<EventType, any>> {
functionDeclaration = SOURCE_URL_REGEX.test(functionDeclaration)
? functionDeclaration
: `${functionDeclaration}\n${sourceUrlComment}\n`;
- responsePromise = this.connection.send('script.callFunction', {
+ responsePromise = this.realm.callFunction(
functionDeclaration,
- arguments: args.length
- ? await Promise.all(
- args.map(arg => {
- return BidiSerializer.serialize(sandbox, arg);
- })
- )
- : [],
- target: this.target,
- resultOwnership,
- awaitPromise: true,
- userActivation: true,
- serializationOptions,
- });
+ /* awaitPromise= */ true,
+ {
+ arguments: args.length
+ ? await Promise.all(
+ args.map(arg => {
+ return this.serialize(arg);
+ })
+ )
+ : [],
+ resultOwnership,
+ userActivation: true,
+ serializationOptions,
+ }
+ );
}
- const {result} = await responsePromise;
+ const result = await responsePromise;
if ('type' in result && result.type === 'exception') {
throw createEvaluationError(result.exceptionDetails);
@@ -199,30 +178,211 @@ export class BidiRealm extends EventEmitter<Record<EventType, any>> {
return returnByValue
? BidiDeserializer.deserialize(result.result)
- : createBidiHandle(sandbox, result.result);
+ : this.createHandle(result.result);
}
- [disposeSymbol](): void {
- this.connection.off(
- Bidi.ChromiumBidi.Script.EventNames.RealmCreated,
- this.handleRealmCreated
- );
- this.connection.off(
- Bidi.ChromiumBidi.Script.EventNames.RealmDestroyed,
- this.handleRealmDestroyed
+ createHandle(
+ result: Bidi.Script.RemoteValue
+ ): BidiJSHandle<unknown> | BidiElementHandle<Node> {
+ if (
+ (result.type === 'node' || result.type === 'window') &&
+ this instanceof BidiFrameRealm
+ ) {
+ return BidiElementHandle.from(result, this);
+ }
+ return BidiJSHandle.from(result, this);
+ }
+
+ async serialize(arg: unknown): Promise<Bidi.Script.LocalValue> {
+ if (arg instanceof LazyArg) {
+ arg = await arg.get(this);
+ }
+
+ if (arg instanceof BidiJSHandle || arg instanceof BidiElementHandle) {
+ if (arg.realm !== this) {
+ if (
+ !(arg.realm instanceof BidiFrameRealm) ||
+ !(this instanceof BidiFrameRealm)
+ ) {
+ throw new Error(
+ "Trying to evaluate JSHandle from different global types. Usually this means you're using a handle from a worker in a page or vice versa."
+ );
+ }
+ if (arg.realm.environment !== this.environment) {
+ throw new Error(
+ "Trying to evaluate JSHandle from different frames. Usually this means you're using a handle from a page on a different page."
+ );
+ }
+ }
+ if (arg.disposed) {
+ throw new Error('JSHandle is disposed!');
+ }
+ return arg.remoteValue() as Bidi.Script.RemoteReference;
+ }
+
+ return BidiSerializer.serialize(arg);
+ }
+
+ async destroyHandles(handles: Array<BidiJSHandle<unknown>>): Promise<void> {
+ const handleIds = handles
+ .map(({id}) => {
+ return id;
+ })
+ .filter((id): id is string => {
+ return id !== undefined;
+ });
+
+ if (handleIds.length === 0) {
+ return;
+ }
+
+ await this.realm.disown(handleIds).catch(error => {
+ // Exceptions might happen in case of a page been navigated or closed.
+ // Swallow these since they are harmless and we don't leak anything in this case.
+ debugError(error);
+ });
+ }
+
+ override async adoptHandle<T extends JSHandle<Node>>(handle: T): Promise<T> {
+ return (await this.evaluateHandle(node => {
+ return node;
+ }, handle)) as unknown as T;
+ }
+
+ override async transferHandle<T extends JSHandle<Node>>(
+ handle: T
+ ): Promise<T> {
+ if (handle.realm === this) {
+ return handle;
+ }
+ const transferredHandle = this.adoptHandle(handle);
+ await handle.dispose();
+ return await transferredHandle;
+ }
+}
+
+/**
+ * @internal
+ */
+export class BidiFrameRealm extends BidiRealm {
+ static from(realm: WindowRealm, frame: BidiFrame): BidiFrameRealm {
+ const frameRealm = new BidiFrameRealm(realm, frame);
+ frameRealm.#initialize();
+ return frameRealm;
+ }
+ declare readonly realm: WindowRealm;
+
+ readonly #frame: BidiFrame;
+
+ private constructor(realm: WindowRealm, frame: BidiFrame) {
+ super(realm, frame.timeoutSettings);
+ this.#frame = frame;
+ }
+
+ #initialize() {
+ super.initialize();
+
+ // This should run first.
+ this.realm.on('updated', () => {
+ this.environment.clearDocumentHandle();
+ this.#bindingsInstalled = false;
+ });
+ }
+
+ #bindingsInstalled = false;
+ override get puppeteerUtil(): Promise<BidiJSHandle<PuppeteerUtil>> {
+ let promise = Promise.resolve() as Promise<unknown>;
+ if (!this.#bindingsInstalled) {
+ promise = Promise.all([
+ ExposeableFunction.from(
+ this.environment as BidiFrame,
+ '__ariaQuerySelector',
+ ARIAQueryHandler.queryOne,
+ !!this.sandbox
+ ),
+ ExposeableFunction.from(
+ this.environment as BidiFrame,
+ '__ariaQuerySelectorAll',
+ async (
+ element: BidiElementHandle<Node>,
+ selector: string
+ ): Promise<JSHandle<Node[]>> => {
+ const results = ARIAQueryHandler.queryAll(element, selector);
+ return await element.realm.evaluateHandle(
+ (...elements) => {
+ return elements;
+ },
+ ...(await AsyncIterableUtil.collect(results))
+ );
+ },
+ !!this.sandbox
+ ),
+ ]);
+ this.#bindingsInstalled = true;
+ }
+ return promise.then(() => {
+ return super.puppeteerUtil;
+ });
+ }
+
+ get sandbox(): string | undefined {
+ return this.realm.sandbox;
+ }
+
+ override get environment(): BidiFrame {
+ return this.#frame;
+ }
+
+ override async adoptBackendNode(
+ backendNodeId?: number | undefined
+ ): Promise<JSHandle<Node>> {
+ const {object} = await this.#frame.client.send('DOM.resolveNode', {
+ backendNodeId,
+ executionContextId: await this.realm.resolveExecutionContextId(),
+ });
+ using handle = BidiElementHandle.from(
+ {
+ handle: object.objectId,
+ type: 'node',
+ },
+ this
);
+ // We need the sharedId, so we perform the following to obtain it.
+ return await handle.evaluateHandle(element => {
+ return element;
+ });
}
}
/**
* @internal
*/
-export function createBidiHandle(
- sandbox: Sandbox,
- result: Bidi.Script.RemoteValue
-): BidiJSHandle<unknown> | BidiElementHandle<Node> {
- if (result.type === 'node' || result.type === 'window') {
- return new BidiElementHandle(sandbox, result);
- }
- return new BidiJSHandle(sandbox, result);
+export class BidiWorkerRealm extends BidiRealm {
+ static from(
+ realm: DedicatedWorkerRealm | SharedWorkerRealm,
+ worker: BidiWebWorker
+ ): BidiWorkerRealm {
+ const workerRealm = new BidiWorkerRealm(realm, worker);
+ workerRealm.initialize();
+ return workerRealm;
+ }
+ declare readonly realm: DedicatedWorkerRealm | SharedWorkerRealm;
+
+ readonly #worker: BidiWebWorker;
+
+ private constructor(
+ realm: DedicatedWorkerRealm | SharedWorkerRealm,
+ frame: BidiWebWorker
+ ) {
+ super(realm, frame.timeoutSettings);
+ this.#worker = frame;
+ }
+
+ override get environment(): BidiWebWorker {
+ return this.#worker;
+ }
+
+ override async adoptBackendNode(): Promise<JSHandle<Node>> {
+ throw new Error('Cannot adopt DOM nodes into a worker.');
+ }
}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Sandbox.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Sandbox.ts
deleted file mode 100644
index 4411b3dbcd..0000000000
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Sandbox.ts
+++ /dev/null
@@ -1,123 +0,0 @@
-/**
- * @license
- * Copyright 2023 Google Inc.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import type {JSHandle} from '../api/JSHandle.js';
-import {Realm} from '../api/Realm.js';
-import type {TimeoutSettings} from '../common/TimeoutSettings.js';
-import type {EvaluateFunc, HandleFor} from '../common/types.js';
-import {withSourcePuppeteerURLIfNone} from '../common/util.js';
-
-import type {BrowsingContext} from './BrowsingContext.js';
-import {BidiElementHandle} from './ElementHandle.js';
-import type {BidiFrame} from './Frame.js';
-import type {BidiRealm as BidiRealm} from './Realm.js';
-/**
- * A unique key for {@link SandboxChart} to denote the default world.
- * Realms are automatically created in the default sandbox.
- *
- * @internal
- */
-export const MAIN_SANDBOX = Symbol('mainSandbox');
-/**
- * A unique key for {@link SandboxChart} to denote the puppeteer sandbox.
- * This world contains all puppeteer-internal bindings/code.
- *
- * @internal
- */
-export const PUPPETEER_SANDBOX = Symbol('puppeteerSandbox');
-
-/**
- * @internal
- */
-export interface SandboxChart {
- [key: string]: Sandbox;
- [MAIN_SANDBOX]: Sandbox;
- [PUPPETEER_SANDBOX]: Sandbox;
-}
-
-/**
- * @internal
- */
-export class Sandbox extends Realm {
- readonly name: string | undefined;
- readonly realm: BidiRealm;
- #frame: BidiFrame;
-
- constructor(
- name: string | undefined,
- frame: BidiFrame,
- // TODO: We should split the Realm and BrowsingContext
- realm: BidiRealm | BrowsingContext,
- timeoutSettings: TimeoutSettings
- ) {
- super(timeoutSettings);
- this.name = name;
- this.realm = realm;
- this.#frame = frame;
- this.realm.setSandbox(this);
- }
-
- override get environment(): BidiFrame {
- return this.#frame;
- }
-
- async evaluateHandle<
- Params extends unknown[],
- Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
- >(
- pageFunction: Func | string,
- ...args: Params
- ): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
- pageFunction = withSourcePuppeteerURLIfNone(
- this.evaluateHandle.name,
- pageFunction
- );
- return await this.realm.evaluateHandle(pageFunction, ...args);
- }
-
- async evaluate<
- Params extends unknown[],
- Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
- >(
- pageFunction: Func | string,
- ...args: Params
- ): Promise<Awaited<ReturnType<Func>>> {
- pageFunction = withSourcePuppeteerURLIfNone(
- this.evaluate.name,
- pageFunction
- );
- return await this.realm.evaluate(pageFunction, ...args);
- }
-
- async adoptHandle<T extends JSHandle<Node>>(handle: T): Promise<T> {
- return (await this.evaluateHandle(node => {
- return node;
- }, handle)) as unknown as T;
- }
-
- async transferHandle<T extends JSHandle<Node>>(handle: T): Promise<T> {
- if (handle.realm === this) {
- return handle;
- }
- const transferredHandle = await this.evaluateHandle(node => {
- return node;
- }, handle);
- await handle.dispose();
- return transferredHandle as unknown as T;
- }
-
- override async adoptBackendNode(
- backendNodeId?: number
- ): Promise<JSHandle<Node>> {
- const {object} = await this.environment.client.send('DOM.resolveNode', {
- backendNodeId: backendNodeId,
- });
- return new BidiElementHandle(this, {
- handle: object.objectId,
- type: 'node',
- });
- }
-}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Serializer.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Serializer.ts
index c147ec9281..523380782b 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Serializer.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Serializer.ts
@@ -6,13 +6,8 @@
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
-import {LazyArg} from '../common/LazyArg.js';
import {isDate, isPlainObject, isRegExp} from '../common/util.js';
-import {BidiElementHandle} from './ElementHandle.js';
-import {BidiJSHandle} from './JSHandle.js';
-import type {Sandbox} from './Sandbox.js';
-
/**
* @internal
*/
@@ -22,7 +17,39 @@ class UnserializableError extends Error {}
* @internal
*/
export class BidiSerializer {
- static serializeNumber(arg: number): Bidi.Script.LocalValue {
+ static serialize(arg: unknown): Bidi.Script.LocalValue {
+ switch (typeof arg) {
+ case 'symbol':
+ case 'function':
+ throw new UnserializableError(`Unable to serializable ${typeof arg}`);
+ case 'object':
+ return this.#serializeObject(arg);
+
+ case 'undefined':
+ return {
+ type: 'undefined',
+ };
+ case 'number':
+ return this.#serializeNumber(arg);
+ case 'bigint':
+ return {
+ type: 'bigint',
+ value: arg.toString(),
+ };
+ case 'string':
+ return {
+ type: 'string',
+ value: arg,
+ };
+ case 'boolean':
+ return {
+ type: 'boolean',
+ value: arg,
+ };
+ }
+ }
+
+ static #serializeNumber(arg: number): Bidi.Script.LocalValue {
let value: Bidi.Script.SpecialNumber | number;
if (Object.is(arg, -0)) {
value = '-0';
@@ -41,14 +68,14 @@ export class BidiSerializer {
};
}
- static serializeObject(arg: object | null): Bidi.Script.LocalValue {
+ static #serializeObject(arg: object | null): Bidi.Script.LocalValue {
if (arg === null) {
return {
type: 'null',
};
} else if (Array.isArray(arg)) {
const parsedArray = arg.map(subArg => {
- return BidiSerializer.serializeRemoteValue(subArg);
+ return this.serialize(subArg);
});
return {
@@ -70,10 +97,7 @@ export class BidiSerializer {
const parsedObject: Bidi.Script.MappingLocalValue = [];
for (const key in arg) {
- parsedObject.push([
- BidiSerializer.serializeRemoteValue(key),
- BidiSerializer.serializeRemoteValue(arg[key]),
- ]);
+ parsedObject.push([this.serialize(key), this.serialize(arg[key])]);
}
return {
@@ -99,66 +123,4 @@ export class BidiSerializer {
'Custom object sterilization not possible. Use plain objects instead.'
);
}
-
- static serializeRemoteValue(arg: unknown): Bidi.Script.LocalValue {
- switch (typeof arg) {
- case 'symbol':
- case 'function':
- throw new UnserializableError(`Unable to serializable ${typeof arg}`);
- case 'object':
- return BidiSerializer.serializeObject(arg);
-
- case 'undefined':
- return {
- type: 'undefined',
- };
- case 'number':
- return BidiSerializer.serializeNumber(arg);
- case 'bigint':
- return {
- type: 'bigint',
- value: arg.toString(),
- };
- case 'string':
- return {
- type: 'string',
- value: arg,
- };
- case 'boolean':
- return {
- type: 'boolean',
- value: arg,
- };
- }
- }
-
- static async serialize(
- sandbox: Sandbox,
- arg: unknown
- ): Promise<Bidi.Script.LocalValue> {
- if (arg instanceof LazyArg) {
- arg = await arg.get(sandbox.realm);
- }
- // eslint-disable-next-line rulesdir/use-using -- We want this to continue living.
- const objectHandle =
- arg && (arg instanceof BidiJSHandle || arg instanceof BidiElementHandle)
- ? arg
- : null;
- if (objectHandle) {
- if (
- objectHandle.realm.environment.context() !==
- sandbox.environment.context()
- ) {
- throw new Error(
- 'JSHandles can be evaluated only in the context they were created!'
- );
- }
- if (objectHandle.disposed) {
- throw new Error('JSHandle is disposed!');
- }
- return objectHandle.remoteValue() as Bidi.Script.RemoteReference;
- }
-
- return BidiSerializer.serializeRemoteValue(arg);
- }
}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Target.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Target.ts
index fb01c34638..b9d78538aa 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Target.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Target.ts
@@ -4,48 +4,46 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import type {CDPSession} from '../api/CDPSession.js';
-import type {Page} from '../api/Page.js';
import {Target, TargetType} from '../api/Target.js';
import {UnsupportedOperation} from '../common/Errors.js';
+import type {CDPSession} from '../puppeteer-core.js';
import type {BidiBrowser} from './Browser.js';
import type {BidiBrowserContext} from './BrowserContext.js';
-import {type BrowsingContext, CdpSessionWrapper} from './BrowsingContext.js';
+import type {BidiFrame} from './Frame.js';
import {BidiPage} from './Page.js';
+import type {BidiWebWorker} from './WebWorker.js';
/**
* @internal
*/
-export abstract class BidiTarget extends Target {
- protected _browserContext: BidiBrowserContext;
+export class BidiBrowserTarget extends Target {
+ #browser: BidiBrowser;
- constructor(browserContext: BidiBrowserContext) {
+ constructor(browser: BidiBrowser) {
super();
- this._browserContext = browserContext;
+ this.#browser = browser;
}
- _setBrowserContext(browserContext: BidiBrowserContext): void {
- this._browserContext = browserContext;
+ override asPage(): Promise<BidiPage> {
+ throw new UnsupportedOperation();
}
-
- override asPage(): Promise<Page> {
+ override url(): string {
+ return '';
+ }
+ override createCDPSession(): Promise<CDPSession> {
throw new UnsupportedOperation();
}
-
+ override type(): TargetType {
+ return TargetType.BROWSER;
+ }
override browser(): BidiBrowser {
- return this._browserContext.browser();
+ return this.#browser;
}
-
override browserContext(): BidiBrowserContext {
- return this._browserContext;
- }
-
- override opener(): never {
- throw new UnsupportedOperation();
+ return this.#browser.defaultBrowserContext();
}
-
- override createCDPSession(): Promise<CDPSession> {
+ override opener(): Target | undefined {
throw new UnsupportedOperation();
}
}
@@ -53,39 +51,39 @@ export abstract class BidiTarget extends Target {
/**
* @internal
*/
-export class BiDiBrowserTarget extends Target {
- #browser: BidiBrowser;
+export class BidiPageTarget extends Target {
+ #page: BidiPage;
- constructor(browser: BidiBrowser) {
+ constructor(page: BidiPage) {
super();
- this.#browser = browser;
+ this.#page = page;
}
+ override async page(): Promise<BidiPage> {
+ return this.#page;
+ }
+ override async asPage(): Promise<BidiPage> {
+ return BidiPage.from(
+ this.browserContext(),
+ this.#page.mainFrame().browsingContext
+ );
+ }
override url(): string {
- return '';
+ return this.#page.url();
}
-
- override type(): TargetType {
- return TargetType.BROWSER;
+ override createCDPSession(): Promise<CDPSession> {
+ return this.#page.createCDPSession();
}
-
- override asPage(): Promise<Page> {
- throw new UnsupportedOperation();
+ override type(): TargetType {
+ return TargetType.PAGE;
}
-
override browser(): BidiBrowser {
- return this.#browser;
+ return this.browserContext().browser();
}
-
override browserContext(): BidiBrowserContext {
- return this.#browser.defaultBrowserContext();
- }
-
- override opener(): never {
- throw new UnsupportedOperation();
+ return this.#page.browserContext();
}
-
- override createCDPSession(): Promise<CDPSession> {
+ override opener(): Target | undefined {
throw new UnsupportedOperation();
}
}
@@ -93,59 +91,80 @@ export class BiDiBrowserTarget extends Target {
/**
* @internal
*/
-export class BiDiBrowsingContextTarget extends BidiTarget {
- protected _browsingContext: BrowsingContext;
+export class BidiFrameTarget extends Target {
+ #frame: BidiFrame;
+ #page: BidiPage | undefined;
- constructor(
- browserContext: BidiBrowserContext,
- browsingContext: BrowsingContext
- ) {
- super(browserContext);
-
- this._browsingContext = browsingContext;
+ constructor(frame: BidiFrame) {
+ super();
+ this.#frame = frame;
}
+ override async page(): Promise<BidiPage> {
+ if (this.#page === undefined) {
+ this.#page = BidiPage.from(
+ this.browserContext(),
+ this.#frame.browsingContext
+ );
+ }
+ return this.#page;
+ }
+ override async asPage(): Promise<BidiPage> {
+ return BidiPage.from(this.browserContext(), this.#frame.browsingContext);
+ }
override url(): string {
- return this._browsingContext.url;
+ return this.#frame.url();
}
-
- override async createCDPSession(): Promise<CDPSession> {
- const {sessionId} = await this._browsingContext.cdpSession.send(
- 'Target.attachToTarget',
- {
- targetId: this._browsingContext.id,
- flatten: true,
- }
- );
- return new CdpSessionWrapper(this._browsingContext, sessionId);
+ override createCDPSession(): Promise<CDPSession> {
+ return this.#frame.createCDPSession();
}
-
override type(): TargetType {
return TargetType.PAGE;
}
+ override browser(): BidiBrowser {
+ return this.browserContext().browser();
+ }
+ override browserContext(): BidiBrowserContext {
+ return this.#frame.page().browserContext();
+ }
+ override opener(): Target | undefined {
+ throw new UnsupportedOperation();
+ }
}
/**
* @internal
*/
-export class BiDiPageTarget extends BiDiBrowsingContextTarget {
- #page: BidiPage;
-
- constructor(
- browserContext: BidiBrowserContext,
- browsingContext: BrowsingContext
- ) {
- super(browserContext, browsingContext);
+export class BidiWorkerTarget extends Target {
+ #worker: BidiWebWorker;
- this.#page = new BidiPage(browsingContext, browserContext, this);
+ constructor(worker: BidiWebWorker) {
+ super();
+ this.#worker = worker;
}
override async page(): Promise<BidiPage> {
- return this.#page;
+ throw new UnsupportedOperation();
}
-
- override _setBrowserContext(browserContext: BidiBrowserContext): void {
- super._setBrowserContext(browserContext);
- this.#page._setBrowserContext(browserContext);
+ override async asPage(): Promise<BidiPage> {
+ throw new UnsupportedOperation();
+ }
+ override url(): string {
+ return this.#worker.url();
+ }
+ override createCDPSession(): Promise<CDPSession> {
+ throw new UnsupportedOperation();
+ }
+ override type(): TargetType {
+ return TargetType.OTHER;
+ }
+ override browser(): BidiBrowser {
+ return this.browserContext().browser();
+ }
+ override browserContext(): BidiBrowserContext {
+ return this.#worker.frame.page().browserContext();
+ }
+ override opener(): Target | undefined {
+ throw new UnsupportedOperation();
}
}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/WebWorker.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/WebWorker.ts
new file mode 100644
index 0000000000..a8b0e28846
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/WebWorker.ts
@@ -0,0 +1,48 @@
+/**
+ * @license
+ * Copyright 2024 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {WebWorker} from '../api/WebWorker.js';
+import {UnsupportedOperation} from '../common/Errors.js';
+import type {CDPSession} from '../puppeteer-core.js';
+
+import type {DedicatedWorkerRealm, SharedWorkerRealm} from './core/Realm.js';
+import type {BidiFrame} from './Frame.js';
+import {BidiWorkerRealm} from './Realm.js';
+
+/**
+ * @internal
+ */
+export class BidiWebWorker extends WebWorker {
+ static from(
+ frame: BidiFrame,
+ realm: DedicatedWorkerRealm | SharedWorkerRealm
+ ): BidiWebWorker {
+ const worker = new BidiWebWorker(frame, realm);
+ return worker;
+ }
+
+ readonly #frame: BidiFrame;
+ readonly #realm: BidiWorkerRealm;
+ private constructor(
+ frame: BidiFrame,
+ realm: DedicatedWorkerRealm | SharedWorkerRealm
+ ) {
+ super(realm.origin);
+ this.#frame = frame;
+ this.#realm = BidiWorkerRealm.from(realm, this);
+ }
+
+ get frame(): BidiFrame {
+ return this.#frame;
+ }
+
+ mainRealm(): BidiWorkerRealm {
+ return this.#realm;
+ }
+
+ get client(): CDPSession {
+ throw new UnsupportedOperation();
+ }
+}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/bidi.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/bidi.ts
index 373d6d999c..4279ba96fd 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/bidi.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/bidi.ts
@@ -7,7 +7,6 @@
export * from './BidiOverCdp.js';
export * from './Browser.js';
export * from './BrowserContext.js';
-export * from './BrowsingContext.js';
export * from './Connection.js';
export * from './ElementHandle.js';
export * from './Frame.js';
@@ -15,8 +14,5 @@ export * from './HTTPRequest.js';
export * from './HTTPResponse.js';
export * from './Input.js';
export * from './JSHandle.js';
-export * from './NetworkManager.js';
export * from './Page.js';
export * from './Realm.js';
-export * from './Sandbox.js';
-export * from './Target.js';
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.';
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/lifecycle.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/lifecycle.ts
deleted file mode 100644
index 73b86cba9c..0000000000
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/lifecycle.ts
+++ /dev/null
@@ -1,119 +0,0 @@
-/**
- * @license
- * Copyright 2023 Google Inc.
- * SPDX-License-Identifier: Apache-2.0
- */
-import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
-
-import type {
- ObservableInput,
- ObservedValueOf,
- OperatorFunction,
-} from '../../third_party/rxjs/rxjs.js';
-import {catchError} from '../../third_party/rxjs/rxjs.js';
-import type {PuppeteerLifeCycleEvent} from '../cdp/LifecycleWatcher.js';
-import {ProtocolError, TimeoutError} from '../common/Errors.js';
-
-/**
- * @internal
- */
-export type BiDiNetworkIdle = Extract<
- PuppeteerLifeCycleEvent,
- 'networkidle0' | 'networkidle2'
-> | null;
-
-/**
- * @internal
- */
-export function getBiDiLifeCycles(
- event: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]
-): [
- Extract<PuppeteerLifeCycleEvent, 'load' | 'domcontentloaded'>,
- BiDiNetworkIdle,
-] {
- if (Array.isArray(event)) {
- const pageLifeCycle = event.some(lifeCycle => {
- return lifeCycle !== 'domcontentloaded';
- })
- ? 'load'
- : 'domcontentloaded';
-
- const networkLifeCycle = event.reduce((acc, lifeCycle) => {
- if (lifeCycle === 'networkidle0') {
- return lifeCycle;
- } else if (acc !== 'networkidle0' && lifeCycle === 'networkidle2') {
- return lifeCycle;
- }
- return acc;
- }, null as BiDiNetworkIdle);
-
- return [pageLifeCycle, networkLifeCycle];
- }
-
- if (event === 'networkidle0' || event === 'networkidle2') {
- return ['load', event];
- }
-
- return [event, null];
-}
-
-/**
- * @internal
- */
-export const lifeCycleToReadinessState = new Map<
- PuppeteerLifeCycleEvent,
- Bidi.BrowsingContext.ReadinessState
->([
- ['load', Bidi.BrowsingContext.ReadinessState.Complete],
- ['domcontentloaded', Bidi.BrowsingContext.ReadinessState.Interactive],
-]);
-
-export function getBiDiReadinessState(
- event: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]
-): [Bidi.BrowsingContext.ReadinessState, BiDiNetworkIdle] {
- const lifeCycles = getBiDiLifeCycles(event);
- const readiness = lifeCycleToReadinessState.get(lifeCycles[0])!;
- return [readiness, lifeCycles[1]];
-}
-
-/**
- * @internal
- */
-export const lifeCycleToSubscribedEvent = new Map<
- PuppeteerLifeCycleEvent,
- 'browsingContext.load' | 'browsingContext.domContentLoaded'
->([
- ['load', 'browsingContext.load'],
- ['domcontentloaded', 'browsingContext.domContentLoaded'],
-]);
-
-/**
- * @internal
- */
-export function getBiDiLifecycleEvent(
- event: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]
-): [
- 'browsingContext.load' | 'browsingContext.domContentLoaded',
- BiDiNetworkIdle,
-] {
- const lifeCycles = getBiDiLifeCycles(event);
- const bidiEvent = lifeCycleToSubscribedEvent.get(lifeCycles[0])!;
- return [bidiEvent, lifeCycles[1]];
-}
-
-/**
- * @internal
- */
-export function rewriteNavigationError<T, R extends ObservableInput<T>>(
- message: string,
- ms: number
-): OperatorFunction<T, T | ObservedValueOf<R>> {
- return catchError<T, R>(error => {
- if (error instanceof ProtocolError) {
- error.message += ` at ${message}`;
- } else if (error instanceof TimeoutError) {
- error.message = `Navigation timeout of ${ms} ms exceeded`;
- }
- throw error;
- });
-}
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/util.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/util.ts
index 41e88e26c2..e1d64c2f4c 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/util.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/util.ts
@@ -6,32 +6,10 @@
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
-import {PuppeteerURL, debugError} from '../common/util.js';
+import {ProtocolError, TimeoutError} from '../common/Errors.js';
+import {PuppeteerURL} from '../common/util.js';
import {BidiDeserializer} from './Deserializer.js';
-import type {BidiRealm} from './Realm.js';
-
-/**
- * @internal
- */
-export async function releaseReference(
- client: BidiRealm,
- remoteReference: Bidi.Script.RemoteReference
-): Promise<void> {
- if (!remoteReference.handle) {
- return;
- }
- await client.connection
- .send('script.disown', {
- target: client.target,
- handles: [remoteReference.handle],
- })
- .catch(error => {
- // Exceptions might happen in case of a page been navigated or closed.
- // Swallow these since they are harmless and we don't leak anything in this case.
- debugError(error);
- });
-}
/**
* @internal
@@ -79,3 +57,20 @@ export function createEvaluationError(
error.stack = [details.text, ...stackLines].join('\n');
return error;
}
+
+/**
+ * @internal
+ */
+export function rewriteNavigationError(
+ message: string,
+ ms: number
+): (error: unknown) => never {
+ return error => {
+ if (error instanceof ProtocolError) {
+ error.message += ` at ${message}`;
+ } else if (error instanceof TimeoutError) {
+ error.message = `Navigation timeout of ${ms} ms exceeded`;
+ }
+ throw error;
+ };
+}