mirror of
https://github.com/puppeteer/puppeteer
synced 2024-06-14 14:02:48 +00:00
refactor: implement bidi/core (#11649)
This commit is contained in:
parent
4cb013000f
commit
0abede9a5f
179
packages/puppeteer-core/src/bidi/core/Browser.ts
Normal file
179
packages/puppeteer-core/src/bidi/core/Browser.ts
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2024 Google Inc.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
||||||
|
|
||||||
|
import {EventEmitter} from '../../common/EventEmitter.js';
|
||||||
|
import {throwIfDisposed} from '../../util/decorators.js';
|
||||||
|
|
||||||
|
import type {BrowsingContext} from './BrowsingContext.js';
|
||||||
|
import type {SharedWorkerRealm} from './Realm.js';
|
||||||
|
import type {Session} from './Session.js';
|
||||||
|
import {UserContext} from './UserContext.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export type AddPreloadScriptOptions = Omit<
|
||||||
|
Bidi.Script.AddPreloadScriptParameters,
|
||||||
|
'functionDeclaration' | 'contexts'
|
||||||
|
> & {
|
||||||
|
contexts?: [BrowsingContext, ...BrowsingContext[]];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export class Browser extends EventEmitter<{
|
||||||
|
/** Emitted after the browser closes. */
|
||||||
|
closed: {
|
||||||
|
/** The reason for closing the browser. */
|
||||||
|
reason: string;
|
||||||
|
};
|
||||||
|
/** Emitted after the browser disconnects. */
|
||||||
|
disconnected: {
|
||||||
|
/** The reason for disconnecting the browser. */
|
||||||
|
reason: string;
|
||||||
|
};
|
||||||
|
/** Emitted when a shared worker is created. */
|
||||||
|
sharedworker: {
|
||||||
|
/** The realm of the shared worker. */
|
||||||
|
realm: SharedWorkerRealm;
|
||||||
|
};
|
||||||
|
}> {
|
||||||
|
static async from(session: Session): Promise<Browser> {
|
||||||
|
const browser = new Browser(session);
|
||||||
|
await browser.#initialize();
|
||||||
|
return browser;
|
||||||
|
}
|
||||||
|
|
||||||
|
// keep-sorted start
|
||||||
|
#reason: string | undefined;
|
||||||
|
readonly #userContexts = new Map();
|
||||||
|
readonly session: Session;
|
||||||
|
// keep-sorted end
|
||||||
|
|
||||||
|
private constructor(session: Session) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
// keep-sorted start
|
||||||
|
this.session = session;
|
||||||
|
// keep-sorted end
|
||||||
|
|
||||||
|
this.#userContexts.set('', UserContext.create(this, ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
async #initialize() {
|
||||||
|
// ///////////////////////
|
||||||
|
// Session listeners //
|
||||||
|
// ///////////////////////
|
||||||
|
const session = this.#session;
|
||||||
|
session.on('script.realmCreated', info => {
|
||||||
|
if (info.type === 'shared-worker') {
|
||||||
|
// TODO: Create a SharedWorkerRealm.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ///////////////////
|
||||||
|
// Parent listeners //
|
||||||
|
// ///////////////////
|
||||||
|
this.session.once('ended', ({reason}) => {
|
||||||
|
this.#reason = reason;
|
||||||
|
this.emit('disconnected', {reason});
|
||||||
|
this.removeAllListeners();
|
||||||
|
});
|
||||||
|
|
||||||
|
// //////////////////////////////
|
||||||
|
// Asynchronous initialization //
|
||||||
|
// //////////////////////////////
|
||||||
|
// In case contexts are created or destroyed during `getTree`, we use this
|
||||||
|
// set to detect them.
|
||||||
|
const contextIds = new Set<string>();
|
||||||
|
const created = (info: {context: string}) => {
|
||||||
|
contextIds.add(info.context);
|
||||||
|
};
|
||||||
|
const destroyed = (info: {context: string}) => {
|
||||||
|
contextIds.delete(info.context);
|
||||||
|
};
|
||||||
|
session.on('browsingContext.contextCreated', created);
|
||||||
|
session.on('browsingContext.contextDestroyed', destroyed);
|
||||||
|
|
||||||
|
const {
|
||||||
|
result: {contexts},
|
||||||
|
} = await session.send('browsingContext.getTree', {});
|
||||||
|
|
||||||
|
session.off('browsingContext.contextDestroyed', destroyed);
|
||||||
|
session.off('browsingContext.contextCreated', created);
|
||||||
|
|
||||||
|
// Simulating events so contexts are created naturally.
|
||||||
|
for (const info of contexts) {
|
||||||
|
if (contextIds.has(info.context)) {
|
||||||
|
session.emit('browsingContext.contextCreated', info);
|
||||||
|
}
|
||||||
|
if (info.children) {
|
||||||
|
contexts.push(...info.children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get #session() {
|
||||||
|
return this.session;
|
||||||
|
}
|
||||||
|
|
||||||
|
get disposed(): boolean {
|
||||||
|
return this.#reason !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
get defaultUserContext(): UserContext {
|
||||||
|
// SAFETY: A UserContext is always created for the default context.
|
||||||
|
return this.#userContexts.get('')!;
|
||||||
|
}
|
||||||
|
|
||||||
|
get userContexts(): Iterable<UserContext> {
|
||||||
|
return this.#userContexts.values();
|
||||||
|
}
|
||||||
|
|
||||||
|
@throwIfDisposed((browser: Browser) => {
|
||||||
|
// SAFETY: By definition of `disposed`, `#reason` is defined.
|
||||||
|
return browser.#reason!;
|
||||||
|
})
|
||||||
|
async close(): Promise<void> {
|
||||||
|
await this.#session.send('browser.close', {});
|
||||||
|
this.#reason = `Browser has already closed.`;
|
||||||
|
this.emit('closed', {reason: this.#reason});
|
||||||
|
this.removeAllListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
@throwIfDisposed((browser: Browser) => {
|
||||||
|
// SAFETY: By definition of `disposed`, `#reason` is defined.
|
||||||
|
return browser.#reason!;
|
||||||
|
})
|
||||||
|
async addPreloadScript(
|
||||||
|
functionDeclaration: string,
|
||||||
|
options: AddPreloadScriptOptions = {}
|
||||||
|
): Promise<string> {
|
||||||
|
const {
|
||||||
|
result: {script},
|
||||||
|
} = await this.#session.send('script.addPreloadScript', {
|
||||||
|
functionDeclaration,
|
||||||
|
...options,
|
||||||
|
contexts: options.contexts?.map(context => {
|
||||||
|
return context.id;
|
||||||
|
}) as [string, ...string[]],
|
||||||
|
});
|
||||||
|
return script;
|
||||||
|
}
|
||||||
|
|
||||||
|
@throwIfDisposed((browser: Browser) => {
|
||||||
|
// SAFETY: By definition of `disposed`, `#reason` is defined.
|
||||||
|
return browser.#reason!;
|
||||||
|
})
|
||||||
|
async removePreloadScript(script: string): Promise<void> {
|
||||||
|
await this.#session.send('script.removePreloadScript', {
|
||||||
|
script,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
390
packages/puppeteer-core/src/bidi/core/BrowsingContext.ts
Normal file
390
packages/puppeteer-core/src/bidi/core/BrowsingContext.ts
Normal file
@ -0,0 +1,390 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2024 Google Inc.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
||||||
|
|
||||||
|
import {EventEmitter} from '../../common/EventEmitter.js';
|
||||||
|
import {throwIfDisposed} from '../../util/decorators.js';
|
||||||
|
|
||||||
|
import type {AddPreloadScriptOptions} from './Browser.js';
|
||||||
|
import {Navigation} from './Navigation.js';
|
||||||
|
import {WindowRealm} from './Realm.js';
|
||||||
|
import {Request} from './Request.js';
|
||||||
|
import type {UserContext} from './UserContext.js';
|
||||||
|
import {UserPrompt} from './UserPrompt.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export type CaptureScreenshotOptions = Omit<
|
||||||
|
Bidi.BrowsingContext.CaptureScreenshotParameters,
|
||||||
|
'context'
|
||||||
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export type ReloadOptions = Omit<
|
||||||
|
Bidi.BrowsingContext.ReloadParameters,
|
||||||
|
'context'
|
||||||
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export type PrintOptions = Omit<
|
||||||
|
Bidi.BrowsingContext.PrintParameters,
|
||||||
|
'context'
|
||||||
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export type HandleUserPromptOptions = Omit<
|
||||||
|
Bidi.BrowsingContext.HandleUserPromptParameters,
|
||||||
|
'context'
|
||||||
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export type SetViewportOptions = Omit<
|
||||||
|
Bidi.BrowsingContext.SetViewportParameters,
|
||||||
|
'context'
|
||||||
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export class BrowsingContext extends EventEmitter<{
|
||||||
|
/** Emitted when this context is destroyed. */
|
||||||
|
destroyed: void;
|
||||||
|
/** Emitted when a child browsing context is created. */
|
||||||
|
browsingcontext: {
|
||||||
|
/** The newly created child browsing context. */
|
||||||
|
browsingContext: BrowsingContext;
|
||||||
|
};
|
||||||
|
/** Emitted whenever a navigation occurs. */
|
||||||
|
navigation: {
|
||||||
|
/** The navigation that occurred. */
|
||||||
|
navigation: Navigation;
|
||||||
|
};
|
||||||
|
/** Emitted whenever a request is made. */
|
||||||
|
request: {
|
||||||
|
/** The request that was made. */
|
||||||
|
request: Request;
|
||||||
|
};
|
||||||
|
/** Emitted whenever a log entry is added. */
|
||||||
|
log: {
|
||||||
|
/** Entry added to the log. */
|
||||||
|
entry: Bidi.Log.Entry;
|
||||||
|
};
|
||||||
|
/** Emitted whenever a prompt is opened. */
|
||||||
|
userprompt: {
|
||||||
|
/** The prompt that was opened. */
|
||||||
|
userPrompt: UserPrompt;
|
||||||
|
};
|
||||||
|
/** Emitted whenever the frame emits `DOMContentLoaded` */
|
||||||
|
DOMContentLoaded: void;
|
||||||
|
/** Emitted whenever the frame emits `load` */
|
||||||
|
load: void;
|
||||||
|
}> {
|
||||||
|
static from(
|
||||||
|
userContext: UserContext,
|
||||||
|
parent: BrowsingContext | undefined,
|
||||||
|
id: string,
|
||||||
|
url: string
|
||||||
|
): BrowsingContext {
|
||||||
|
const browsingContext = new BrowsingContext(userContext, parent, id, url);
|
||||||
|
browsingContext.#initialize();
|
||||||
|
return browsingContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
// keep-sorted start
|
||||||
|
#navigation: Navigation | undefined;
|
||||||
|
#url: string;
|
||||||
|
readonly #children = new Map<string, BrowsingContext>();
|
||||||
|
readonly #realms = new Map<string, WindowRealm>();
|
||||||
|
readonly #requests = new Map<string, Request>();
|
||||||
|
readonly defaultRealm: WindowRealm;
|
||||||
|
readonly id: string;
|
||||||
|
readonly parent: BrowsingContext | undefined;
|
||||||
|
readonly userContext: UserContext;
|
||||||
|
disposed = false;
|
||||||
|
// keep-sorted end
|
||||||
|
|
||||||
|
private constructor(
|
||||||
|
context: UserContext,
|
||||||
|
parent: BrowsingContext | undefined,
|
||||||
|
id: string,
|
||||||
|
url: string
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
// keep-sorted start
|
||||||
|
this.#url = url;
|
||||||
|
this.id = id;
|
||||||
|
this.parent = parent;
|
||||||
|
this.userContext = context;
|
||||||
|
// keep-sorted end
|
||||||
|
|
||||||
|
this.defaultRealm = WindowRealm.from(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
#initialize() {
|
||||||
|
// ///////////////////////
|
||||||
|
// Session listeners //
|
||||||
|
// ///////////////////////
|
||||||
|
const session = this.#session;
|
||||||
|
session.on('browsingContext.contextCreated', info => {
|
||||||
|
if (info.parent !== this.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const browsingContext = BrowsingContext.from(
|
||||||
|
this.userContext,
|
||||||
|
this,
|
||||||
|
info.context,
|
||||||
|
info.url
|
||||||
|
);
|
||||||
|
browsingContext.on('destroyed', () => {
|
||||||
|
this.#children.delete(browsingContext.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.#children.set(info.context, browsingContext);
|
||||||
|
|
||||||
|
this.emit('browsingcontext', {browsingContext});
|
||||||
|
});
|
||||||
|
session.on('browsingContext.contextDestroyed', info => {
|
||||||
|
if (info.context !== this.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.disposed = true;
|
||||||
|
this.emit('destroyed', undefined);
|
||||||
|
this.removeAllListeners();
|
||||||
|
});
|
||||||
|
|
||||||
|
session.on('browsingContext.domContentLoaded', info => {
|
||||||
|
if (info.context !== this.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#url = info.url;
|
||||||
|
this.emit('DOMContentLoaded', undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
session.on('browsingContext.load', info => {
|
||||||
|
if (info.context !== this.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#url = info.url;
|
||||||
|
this.emit('load', undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
session.on('browsingContext.navigationStarted', info => {
|
||||||
|
if (info.context !== this.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#requests.clear();
|
||||||
|
|
||||||
|
// Note the navigation ID is null for this event.
|
||||||
|
this.#navigation = Navigation.from(this, info.url);
|
||||||
|
this.#navigation.on('fragment', ({url}) => {
|
||||||
|
this.#url = url;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.emit('navigation', {navigation: this.#navigation});
|
||||||
|
});
|
||||||
|
session.on('network.beforeRequestSent', event => {
|
||||||
|
if (event.context !== this.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.#requests.has(event.request.request)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = new Request(this, event);
|
||||||
|
this.#requests.set(request.id, request);
|
||||||
|
this.emit('request', {request});
|
||||||
|
});
|
||||||
|
|
||||||
|
session.on('log.entryAdded', entry => {
|
||||||
|
if (entry.source.context !== this.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emit('log', {entry});
|
||||||
|
});
|
||||||
|
|
||||||
|
session.on('browsingContext.userPromptOpened', info => {
|
||||||
|
if (info.context !== this.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userPrompt = UserPrompt.from(this, info);
|
||||||
|
this.emit('userprompt', {userPrompt});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// keep-sorted start block=yes
|
||||||
|
get #session() {
|
||||||
|
return this.userContext.browser.session;
|
||||||
|
}
|
||||||
|
get children(): Iterable<BrowsingContext> {
|
||||||
|
return this.#children.values();
|
||||||
|
}
|
||||||
|
get realms(): Iterable<WindowRealm> {
|
||||||
|
return this.#realms.values();
|
||||||
|
}
|
||||||
|
get top(): BrowsingContext {
|
||||||
|
let context = this as BrowsingContext;
|
||||||
|
for (let {parent} = context; parent; {parent} = context) {
|
||||||
|
context = parent;
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
get url(): string {
|
||||||
|
return this.#url;
|
||||||
|
}
|
||||||
|
// keep-sorted end
|
||||||
|
|
||||||
|
@throwIfDisposed()
|
||||||
|
async activate(): Promise<void> {
|
||||||
|
await this.#session.send('browsingContext.activate', {
|
||||||
|
context: this.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@throwIfDisposed()
|
||||||
|
async captureScreenshot(
|
||||||
|
options: CaptureScreenshotOptions = {}
|
||||||
|
): Promise<string> {
|
||||||
|
const {
|
||||||
|
result: {data},
|
||||||
|
} = await this.#session.send('browsingContext.captureScreenshot', {
|
||||||
|
context: this.id,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
@throwIfDisposed()
|
||||||
|
async close(promptUnload?: boolean): Promise<void> {
|
||||||
|
await Promise.all(
|
||||||
|
[...this.#children.values()].map(async child => {
|
||||||
|
await child.close(promptUnload);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await this.#session.send('browsingContext.close', {
|
||||||
|
context: this.id,
|
||||||
|
promptUnload,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@throwIfDisposed()
|
||||||
|
async traverseHistory(delta: number): Promise<void> {
|
||||||
|
await this.#session.send('browsingContext.traverseHistory', {
|
||||||
|
context: this.id,
|
||||||
|
delta,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@throwIfDisposed()
|
||||||
|
async navigate(
|
||||||
|
url: string,
|
||||||
|
wait?: Bidi.BrowsingContext.ReadinessState
|
||||||
|
): Promise<Navigation> {
|
||||||
|
await this.#session.send('browsingContext.navigate', {
|
||||||
|
context: this.id,
|
||||||
|
url,
|
||||||
|
wait,
|
||||||
|
});
|
||||||
|
return await new Promise(resolve => {
|
||||||
|
this.once('navigation', ({navigation}) => {
|
||||||
|
resolve(navigation);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@throwIfDisposed()
|
||||||
|
async reload(options: ReloadOptions = {}): Promise<Navigation> {
|
||||||
|
await this.#session.send('browsingContext.reload', {
|
||||||
|
context: this.id,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
return await new Promise(resolve => {
|
||||||
|
this.once('navigation', ({navigation}) => {
|
||||||
|
resolve(navigation);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@throwIfDisposed()
|
||||||
|
async print(options: PrintOptions = {}): Promise<string> {
|
||||||
|
const {
|
||||||
|
result: {data},
|
||||||
|
} = await this.#session.send('browsingContext.print', {
|
||||||
|
context: this.id,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
@throwIfDisposed()
|
||||||
|
async handleUserPrompt(options: HandleUserPromptOptions = {}): Promise<void> {
|
||||||
|
await this.#session.send('browsingContext.handleUserPrompt', {
|
||||||
|
context: this.id,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@throwIfDisposed()
|
||||||
|
async setViewport(options: SetViewportOptions = {}): Promise<void> {
|
||||||
|
await this.#session.send('browsingContext.setViewport', {
|
||||||
|
context: this.id,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@throwIfDisposed()
|
||||||
|
async performActions(actions: Bidi.Input.SourceActions[]): Promise<void> {
|
||||||
|
await this.#session.send('input.performActions', {
|
||||||
|
context: this.id,
|
||||||
|
actions,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@throwIfDisposed()
|
||||||
|
async releaseActions(): Promise<void> {
|
||||||
|
await this.#session.send('input.releaseActions', {
|
||||||
|
context: this.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@throwIfDisposed()
|
||||||
|
createWindowRealm(sandbox: string): WindowRealm {
|
||||||
|
return WindowRealm.from(this, sandbox);
|
||||||
|
}
|
||||||
|
|
||||||
|
@throwIfDisposed()
|
||||||
|
async addPreloadScript(
|
||||||
|
functionDeclaration: string,
|
||||||
|
options: AddPreloadScriptOptions = {}
|
||||||
|
): Promise<string> {
|
||||||
|
return await this.userContext.browser.addPreloadScript(
|
||||||
|
functionDeclaration,
|
||||||
|
{
|
||||||
|
...options,
|
||||||
|
contexts: [this, ...(options.contexts ?? [])],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@throwIfDisposed()
|
||||||
|
async removePreloadScript(script: string): Promise<void> {
|
||||||
|
await this.userContext.browser.removePreloadScript(script);
|
||||||
|
}
|
||||||
|
}
|
139
packages/puppeteer-core/src/bidi/core/Connection.ts
Normal file
139
packages/puppeteer-core/src/bidi/core/Connection.ts
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2024 Google Inc.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
||||||
|
|
||||||
|
import type {EventEmitter} from '../../common/EventEmitter.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export interface Commands {
|
||||||
|
'script.evaluate': {
|
||||||
|
params: Bidi.Script.EvaluateParameters;
|
||||||
|
returnType: Bidi.Script.EvaluateResult;
|
||||||
|
};
|
||||||
|
'script.callFunction': {
|
||||||
|
params: Bidi.Script.CallFunctionParameters;
|
||||||
|
returnType: Bidi.Script.EvaluateResult;
|
||||||
|
};
|
||||||
|
'script.disown': {
|
||||||
|
params: Bidi.Script.DisownParameters;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
'script.addPreloadScript': {
|
||||||
|
params: Bidi.Script.AddPreloadScriptParameters;
|
||||||
|
returnType: Bidi.Script.AddPreloadScriptResult;
|
||||||
|
};
|
||||||
|
'script.removePreloadScript': {
|
||||||
|
params: Bidi.Script.RemovePreloadScriptParameters;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
'browser.close': {
|
||||||
|
params: Bidi.EmptyParams;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
'browsingContext.activate': {
|
||||||
|
params: Bidi.BrowsingContext.ActivateParameters;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
'browsingContext.create': {
|
||||||
|
params: Bidi.BrowsingContext.CreateParameters;
|
||||||
|
returnType: Bidi.BrowsingContext.CreateResult;
|
||||||
|
};
|
||||||
|
'browsingContext.close': {
|
||||||
|
params: Bidi.BrowsingContext.CloseParameters;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
'browsingContext.getTree': {
|
||||||
|
params: Bidi.BrowsingContext.GetTreeParameters;
|
||||||
|
returnType: Bidi.BrowsingContext.GetTreeResult;
|
||||||
|
};
|
||||||
|
'browsingContext.navigate': {
|
||||||
|
params: Bidi.BrowsingContext.NavigateParameters;
|
||||||
|
returnType: Bidi.BrowsingContext.NavigateResult;
|
||||||
|
};
|
||||||
|
'browsingContext.reload': {
|
||||||
|
params: Bidi.BrowsingContext.ReloadParameters;
|
||||||
|
returnType: Bidi.BrowsingContext.NavigateResult;
|
||||||
|
};
|
||||||
|
'browsingContext.print': {
|
||||||
|
params: Bidi.BrowsingContext.PrintParameters;
|
||||||
|
returnType: Bidi.BrowsingContext.PrintResult;
|
||||||
|
};
|
||||||
|
'browsingContext.captureScreenshot': {
|
||||||
|
params: Bidi.BrowsingContext.CaptureScreenshotParameters;
|
||||||
|
returnType: Bidi.BrowsingContext.CaptureScreenshotResult;
|
||||||
|
};
|
||||||
|
'browsingContext.handleUserPrompt': {
|
||||||
|
params: Bidi.BrowsingContext.HandleUserPromptParameters;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
'browsingContext.setViewport': {
|
||||||
|
params: Bidi.BrowsingContext.SetViewportParameters;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
'browsingContext.traverseHistory': {
|
||||||
|
params: Bidi.BrowsingContext.TraverseHistoryParameters;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
'input.performActions': {
|
||||||
|
params: Bidi.Input.PerformActionsParameters;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
'input.releaseActions': {
|
||||||
|
params: Bidi.Input.ReleaseActionsParameters;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
'session.end': {
|
||||||
|
params: Bidi.EmptyParams;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
'session.new': {
|
||||||
|
params: Bidi.Session.NewParameters;
|
||||||
|
returnType: Bidi.Session.NewResult;
|
||||||
|
};
|
||||||
|
'session.status': {
|
||||||
|
params: object;
|
||||||
|
returnType: Bidi.Session.StatusResult;
|
||||||
|
};
|
||||||
|
'session.subscribe': {
|
||||||
|
params: Bidi.Session.SubscriptionRequest;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
'session.unsubscribe': {
|
||||||
|
params: Bidi.Session.SubscriptionRequest;
|
||||||
|
returnType: Bidi.EmptyResult;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export type BidiEvents = {
|
||||||
|
[K in Bidi.ChromiumBidi.Event['method']]: Extract<
|
||||||
|
Bidi.ChromiumBidi.Event,
|
||||||
|
{method: K}
|
||||||
|
>['params'];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export interface Connection<Events extends BidiEvents = BidiEvents>
|
||||||
|
extends EventEmitter<Events> {
|
||||||
|
send<T extends keyof Commands>(
|
||||||
|
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;
|
||||||
|
}
|
105
packages/puppeteer-core/src/bidi/core/Navigation.ts
Normal file
105
packages/puppeteer-core/src/bidi/core/Navigation.ts
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2024 Google Inc.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
||||||
|
|
||||||
|
import {EventEmitter} from '../../common/EventEmitter.js';
|
||||||
|
import {Deferred} from '../../util/Deferred.js';
|
||||||
|
|
||||||
|
import type {BrowsingContext} from './BrowsingContext.js';
|
||||||
|
import type {Request} from './Request.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export interface NavigationInfo {
|
||||||
|
url: string;
|
||||||
|
timestamp: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export class Navigation extends EventEmitter<{
|
||||||
|
fragment: NavigationInfo;
|
||||||
|
failed: NavigationInfo;
|
||||||
|
aborted: NavigationInfo;
|
||||||
|
}> {
|
||||||
|
static from(context: BrowsingContext, url: string): Navigation {
|
||||||
|
const navigation = new Navigation(context, url);
|
||||||
|
navigation.#initialize();
|
||||||
|
return navigation;
|
||||||
|
}
|
||||||
|
|
||||||
|
// keep-sorted start
|
||||||
|
#context: BrowsingContext;
|
||||||
|
#id = new Deferred<string>();
|
||||||
|
#request: Request | undefined;
|
||||||
|
#url: string;
|
||||||
|
// keep-sorted end
|
||||||
|
|
||||||
|
private constructor(context: BrowsingContext, url: string) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
// keep-sorted start
|
||||||
|
this.#context = context;
|
||||||
|
this.#url = url;
|
||||||
|
// keep-sorted end
|
||||||
|
}
|
||||||
|
|
||||||
|
#initialize() {
|
||||||
|
// ///////////////////////
|
||||||
|
// Session listeners //
|
||||||
|
// ///////////////////////
|
||||||
|
const session = this.#session;
|
||||||
|
for (const [bidiEvent, event] of [
|
||||||
|
['browsingContext.fragmentNavigated', 'fragment'],
|
||||||
|
['browsingContext.navigationFailed', 'failed'],
|
||||||
|
['browsingContext.navigationAborted', 'aborted'],
|
||||||
|
] as const) {
|
||||||
|
session.on(bidiEvent, (info: Bidi.BrowsingContext.NavigationInfo) => {
|
||||||
|
if (info.context !== this.#context.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!info.navigation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.#id.resolved()) {
|
||||||
|
this.#id.resolve(info.navigation);
|
||||||
|
}
|
||||||
|
if (this.#id.value() !== info.navigation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#url = info.url;
|
||||||
|
this.emit(event, {
|
||||||
|
url: this.#url,
|
||||||
|
timestamp: new Date(info.timestamp),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ///////////////////
|
||||||
|
// Parent listeners //
|
||||||
|
// ///////////////////
|
||||||
|
this.#context.on('request', ({request}) => {
|
||||||
|
if (request.navigation === this.#id.value()) {
|
||||||
|
this.#request = request;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get #session() {
|
||||||
|
return this.#context.userContext.browser.session;
|
||||||
|
}
|
||||||
|
|
||||||
|
get url(): string {
|
||||||
|
return this.#url;
|
||||||
|
}
|
||||||
|
|
||||||
|
request(): Request | undefined {
|
||||||
|
return this.#request;
|
||||||
|
}
|
||||||
|
}
|
283
packages/puppeteer-core/src/bidi/core/Realm.ts
Normal file
283
packages/puppeteer-core/src/bidi/core/Realm.ts
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2024 Google Inc.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
||||||
|
|
||||||
|
import {EventEmitter} from '../../common/EventEmitter.js';
|
||||||
|
|
||||||
|
import type {BrowsingContext} from './BrowsingContext.js';
|
||||||
|
import type {Session} from './Session.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export type CallFunctionOptions = Omit<
|
||||||
|
Bidi.Script.CallFunctionParameters,
|
||||||
|
'functionDeclaration' | 'awaitPromise' | 'target'
|
||||||
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export type EvaluateOptions = Omit<
|
||||||
|
Bidi.Script.EvaluateParameters,
|
||||||
|
'expression' | 'awaitPromise' | 'target'
|
||||||
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export abstract class Realm extends EventEmitter<{
|
||||||
|
/** Emitted when the realm is destroyed. */
|
||||||
|
destroyed: void;
|
||||||
|
/** Emitted when a dedicated worker is created in the realm. */
|
||||||
|
worker: DedicatedWorkerRealm;
|
||||||
|
/** Emitted when a shared worker is created in the realm. */
|
||||||
|
sharedworker: SharedWorkerRealm;
|
||||||
|
}> {
|
||||||
|
readonly id: string;
|
||||||
|
readonly origin: string;
|
||||||
|
protected constructor(id: string, origin: string) {
|
||||||
|
super();
|
||||||
|
this.id = id;
|
||||||
|
this.origin = origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected initialize(): void {
|
||||||
|
this.session.on('script.realmDestroyed', info => {
|
||||||
|
if (info.realm === this.id) {
|
||||||
|
this.emit('destroyed', undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract get session(): Session;
|
||||||
|
|
||||||
|
protected get target(): Bidi.Script.Target {
|
||||||
|
return {realm: this.id};
|
||||||
|
}
|
||||||
|
|
||||||
|
async disown(handles: string[]): Promise<void> {
|
||||||
|
await this.session.send('script.disown', {
|
||||||
|
target: this.target,
|
||||||
|
handles,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async callFunction(
|
||||||
|
functionDeclaration: string,
|
||||||
|
awaitPromise: boolean,
|
||||||
|
options: CallFunctionOptions = {}
|
||||||
|
): Promise<Bidi.Script.EvaluateResult> {
|
||||||
|
const {result} = await this.session.send('script.callFunction', {
|
||||||
|
functionDeclaration,
|
||||||
|
awaitPromise,
|
||||||
|
target: this.target,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async evaluate(
|
||||||
|
expression: string,
|
||||||
|
awaitPromise: boolean,
|
||||||
|
options: EvaluateOptions = {}
|
||||||
|
): Promise<Bidi.Script.EvaluateResult> {
|
||||||
|
const {result} = await this.session.send('script.evaluate', {
|
||||||
|
expression,
|
||||||
|
awaitPromise,
|
||||||
|
target: this.target,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WindowRealm extends Realm {
|
||||||
|
static from(context: BrowsingContext, sandbox?: string): WindowRealm {
|
||||||
|
const realm = new WindowRealm(context, sandbox);
|
||||||
|
realm.initialize();
|
||||||
|
return realm;
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly browsingContext: BrowsingContext;
|
||||||
|
readonly sandbox?: string;
|
||||||
|
|
||||||
|
readonly #workers: {
|
||||||
|
dedicated: Map<string, DedicatedWorkerRealm>;
|
||||||
|
shared: Map<string, SharedWorkerRealm>;
|
||||||
|
} = {
|
||||||
|
dedicated: new Map(),
|
||||||
|
shared: new Map(),
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(context: BrowsingContext, sandbox?: string) {
|
||||||
|
super('', '');
|
||||||
|
this.browsingContext = context;
|
||||||
|
this.sandbox = sandbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
override initialize(): void {
|
||||||
|
super.initialize();
|
||||||
|
|
||||||
|
// ///////////////////////
|
||||||
|
// Session listeners //
|
||||||
|
// ///////////////////////
|
||||||
|
this.session.on('script.realmCreated', info => {
|
||||||
|
if (info.type === 'window') {
|
||||||
|
// SAFETY: This is the only time we allow mutations.
|
||||||
|
(this as any).id = info.realm;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (info.type === 'dedicated-worker') {
|
||||||
|
if (!info.owners.includes(this.id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const realm = DedicatedWorkerRealm.from(this, info.realm, info.origin);
|
||||||
|
realm.on('destroyed', () => {
|
||||||
|
this.#workers.dedicated.delete(realm.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.#workers.dedicated.set(realm.id, realm);
|
||||||
|
|
||||||
|
this.emit('worker', realm);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ///////////////////
|
||||||
|
// Parent listeners //
|
||||||
|
// ///////////////////
|
||||||
|
this.browsingContext.userContext.browser.on('sharedworker', ({realm}) => {
|
||||||
|
if (realm.owners.has(this)) {
|
||||||
|
realm.on('destroyed', () => {
|
||||||
|
this.#workers.shared.delete(realm.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.#workers.shared.set(realm.id, realm);
|
||||||
|
|
||||||
|
this.emit('sharedworker', realm);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
override get session(): Session {
|
||||||
|
return this.browsingContext.userContext.browser.session;
|
||||||
|
}
|
||||||
|
|
||||||
|
override get target(): Bidi.Script.Target {
|
||||||
|
return {context: this.browsingContext.id, sandbox: this.sandbox};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DedicatedWorkerOwnerRealm =
|
||||||
|
| DedicatedWorkerRealm
|
||||||
|
| SharedWorkerRealm
|
||||||
|
| WindowRealm;
|
||||||
|
|
||||||
|
export class DedicatedWorkerRealm extends Realm {
|
||||||
|
static from(
|
||||||
|
owner: DedicatedWorkerOwnerRealm,
|
||||||
|
id: string,
|
||||||
|
origin: string
|
||||||
|
): DedicatedWorkerRealm {
|
||||||
|
const realm = new DedicatedWorkerRealm(owner, id, origin);
|
||||||
|
realm.initialize();
|
||||||
|
return realm;
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly owners: Set<DedicatedWorkerOwnerRealm>;
|
||||||
|
|
||||||
|
readonly #workers = new Map<string, DedicatedWorkerRealm>();
|
||||||
|
|
||||||
|
constructor(owner: DedicatedWorkerOwnerRealm, id: string, origin: string) {
|
||||||
|
super(id, origin);
|
||||||
|
this.owners = new Set([owner]);
|
||||||
|
}
|
||||||
|
|
||||||
|
override initialize(): void {
|
||||||
|
super.initialize();
|
||||||
|
|
||||||
|
// ///////////////////////
|
||||||
|
// Session listeners //
|
||||||
|
// ///////////////////////
|
||||||
|
this.session.on('script.realmCreated', info => {
|
||||||
|
if (info.type === 'dedicated-worker') {
|
||||||
|
if (!info.owners.includes(this.id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const realm = DedicatedWorkerRealm.from(this, info.realm, info.origin);
|
||||||
|
realm.on('destroyed', () => {
|
||||||
|
this.#workers.delete(realm.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.#workers.set(realm.id, realm);
|
||||||
|
|
||||||
|
this.emit('worker', realm);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
override get session(): Session {
|
||||||
|
// SAFETY: At least one owner will exist.
|
||||||
|
return this.owners.values().next().value.session;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SharedWorkerRealm extends Realm {
|
||||||
|
static from(
|
||||||
|
owners: [WindowRealm, ...WindowRealm[]],
|
||||||
|
id: string,
|
||||||
|
origin: string
|
||||||
|
): SharedWorkerRealm {
|
||||||
|
const realm = new SharedWorkerRealm(owners, id, origin);
|
||||||
|
realm.initialize();
|
||||||
|
return realm;
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly owners: Set<WindowRealm>;
|
||||||
|
|
||||||
|
readonly #workers = new Map<string, DedicatedWorkerRealm>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
owners: [WindowRealm, ...WindowRealm[]],
|
||||||
|
id: string,
|
||||||
|
origin: string
|
||||||
|
) {
|
||||||
|
super(id, origin);
|
||||||
|
this.owners = new Set(owners);
|
||||||
|
}
|
||||||
|
|
||||||
|
override initialize(): void {
|
||||||
|
super.initialize();
|
||||||
|
|
||||||
|
// ///////////////////////
|
||||||
|
// Session listeners //
|
||||||
|
// ///////////////////////
|
||||||
|
this.session.on('script.realmCreated', info => {
|
||||||
|
if (info.type === 'dedicated-worker') {
|
||||||
|
if (!info.owners.includes(this.id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const realm = DedicatedWorkerRealm.from(this, info.realm, info.origin);
|
||||||
|
realm.on('destroyed', () => {
|
||||||
|
this.#workers.delete(realm.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.#workers.set(realm.id, realm);
|
||||||
|
|
||||||
|
this.emit('worker', realm);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
override get session(): Session {
|
||||||
|
// SAFETY: At least one owner will exist.
|
||||||
|
return this.owners.values().next().value.session;
|
||||||
|
}
|
||||||
|
}
|
114
packages/puppeteer-core/src/bidi/core/Request.ts
Normal file
114
packages/puppeteer-core/src/bidi/core/Request.ts
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2024 Google Inc.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
||||||
|
|
||||||
|
import {EventEmitter} from '../../common/EventEmitter.js';
|
||||||
|
|
||||||
|
import type {BrowsingContext} from './BrowsingContext.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export class Request extends EventEmitter<{
|
||||||
|
// Emitted whenever a redirect is received.
|
||||||
|
redirect: Request;
|
||||||
|
// Emitted when when the request succeeds.
|
||||||
|
success: Bidi.Network.ResponseData;
|
||||||
|
// Emitted when when the request errors.
|
||||||
|
error: string;
|
||||||
|
}> {
|
||||||
|
readonly #context: BrowsingContext;
|
||||||
|
readonly #event: Bidi.Network.BeforeRequestSentParameters;
|
||||||
|
|
||||||
|
#response?: Bidi.Network.ResponseData;
|
||||||
|
#redirect?: Request;
|
||||||
|
#error?: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
context: BrowsingContext,
|
||||||
|
event: Bidi.Network.BeforeRequestSentParameters
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
this.#context = context;
|
||||||
|
this.#event = event;
|
||||||
|
|
||||||
|
const session = this.#session;
|
||||||
|
session.on('network.beforeRequestSent', event => {
|
||||||
|
if (event.context !== this.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.request.request !== this.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.#redirect) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#redirect = new Request(this.#context, event);
|
||||||
|
this.emit('redirect', this.#redirect);
|
||||||
|
});
|
||||||
|
session.on('network.fetchError', event => {
|
||||||
|
if (event.context !== this.#context.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.request.request !== this.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#error = event.errorText;
|
||||||
|
this.emit('error', this.#error);
|
||||||
|
});
|
||||||
|
session.on('network.responseCompleted', event => {
|
||||||
|
if (event.context !== this.#context.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.request.request !== this.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#response = event.response;
|
||||||
|
this.emit('success', this.#response);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get #session() {
|
||||||
|
return this.#context.userContext.browser.session;
|
||||||
|
}
|
||||||
|
|
||||||
|
get id(): string {
|
||||||
|
return this.#event.request.request;
|
||||||
|
}
|
||||||
|
|
||||||
|
get url(): string {
|
||||||
|
return this.#event.request.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
get initiator(): Bidi.Network.Initiator {
|
||||||
|
return this.#event.initiator;
|
||||||
|
}
|
||||||
|
|
||||||
|
get method(): string {
|
||||||
|
return this.#event.request.method;
|
||||||
|
}
|
||||||
|
|
||||||
|
get headers(): Bidi.Network.Header[] {
|
||||||
|
return this.#event.request.headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
get navigation(): string | undefined {
|
||||||
|
return this.#event.navigation ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
get redirect(): Request | undefined {
|
||||||
|
return this.redirect;
|
||||||
|
}
|
||||||
|
|
||||||
|
get response(): Bidi.Network.ResponseData | undefined {
|
||||||
|
return this.#response;
|
||||||
|
}
|
||||||
|
|
||||||
|
get error(): string | undefined {
|
||||||
|
return this.#error;
|
||||||
|
}
|
||||||
|
}
|
164
packages/puppeteer-core/src/bidi/core/Session.ts
Normal file
164
packages/puppeteer-core/src/bidi/core/Session.ts
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2024 Google Inc.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 {throwIfDisposed} from '../../util/decorators.js';
|
||||||
|
import type {BidiEvents} from '../Connection.js';
|
||||||
|
|
||||||
|
import {Browser} from './Browser.js';
|
||||||
|
import type {Connection} from './Connection.js';
|
||||||
|
import type {Commands} from './Connection.js';
|
||||||
|
|
||||||
|
const MAX_RETRIES = 5;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export class Session
|
||||||
|
extends EventEmitter<BidiEvents & {ended: {reason: string}}>
|
||||||
|
implements Connection<BidiEvents & {ended: {reason: string}}>
|
||||||
|
{
|
||||||
|
static async from(
|
||||||
|
connection: Connection,
|
||||||
|
capabilities: Bidi.Session.CapabilitiesRequest
|
||||||
|
): Promise<Session> {
|
||||||
|
// Wait until the session is ready.
|
||||||
|
let status = {message: '', ready: false};
|
||||||
|
for (let i = 0; i < MAX_RETRIES; ++i) {
|
||||||
|
status = (await connection.send('session.status', {})).result;
|
||||||
|
if (status.ready) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Backoff a little bit each time.
|
||||||
|
await new Promise(resolve => {
|
||||||
|
return setTimeout(resolve, (1 << i) * 100);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!status.ready) {
|
||||||
|
throw new Error(status.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
result = (
|
||||||
|
await connection.send('session.new', {
|
||||||
|
capabilities,
|
||||||
|
})
|
||||||
|
).result;
|
||||||
|
} catch (err) {
|
||||||
|
// Chrome does not support session.new.
|
||||||
|
debugError(err);
|
||||||
|
result = {
|
||||||
|
sessionId: '',
|
||||||
|
capabilities: {
|
||||||
|
acceptInsecureCerts: false,
|
||||||
|
browserName: 'chrome',
|
||||||
|
browserVersion: '',
|
||||||
|
platformName: '',
|
||||||
|
setWindowRect: false,
|
||||||
|
webSocketUrl: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = new Session(connection, result);
|
||||||
|
await session.#initialize();
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly #connection: Connection;
|
||||||
|
|
||||||
|
readonly #info: Bidi.Session.NewResult;
|
||||||
|
readonly browser!: Browser;
|
||||||
|
|
||||||
|
#reason: string | undefined;
|
||||||
|
|
||||||
|
private constructor(connection: Connection, info: Bidi.Session.NewResult) {
|
||||||
|
super();
|
||||||
|
this.#connection = connection;
|
||||||
|
this.#info = info;
|
||||||
|
}
|
||||||
|
|
||||||
|
async #initialize(): Promise<void> {
|
||||||
|
// ///////////////////////
|
||||||
|
// Connection listeners //
|
||||||
|
// ///////////////////////
|
||||||
|
this.#connection.pipeTo(this);
|
||||||
|
|
||||||
|
// //////////////////////////////
|
||||||
|
// Asynchronous initialization //
|
||||||
|
// //////////////////////////////
|
||||||
|
// SAFETY: We use `any` to allow assignment of the readonly property.
|
||||||
|
(this as any).browser = await Browser.from(this);
|
||||||
|
|
||||||
|
// //////////////////
|
||||||
|
// Child listeners //
|
||||||
|
// //////////////////
|
||||||
|
this.browser.once('closed', ({reason}) => {
|
||||||
|
this.#reason = reason;
|
||||||
|
this.emit('ended', {reason});
|
||||||
|
this.removeAllListeners();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get disposed(): boolean {
|
||||||
|
return this.#reason !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
get id(): string {
|
||||||
|
return this.#info.sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
get capabilities(): Bidi.Session.NewResult['capabilities'] {
|
||||||
|
return this.#info.capabilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
* case we always needs to make sure that the session for the right session
|
||||||
|
* object is used, so we implement this method here, although it's not defined
|
||||||
|
* in the spec.
|
||||||
|
*/
|
||||||
|
@throwIfDisposed((session: Session) => {
|
||||||
|
// SAFETY: By definition of `disposed`, `#reason` is defined.
|
||||||
|
return session.#reason!;
|
||||||
|
})
|
||||||
|
async send<T extends keyof Commands>(
|
||||||
|
method: T,
|
||||||
|
params: Commands[T]['params']
|
||||||
|
): Promise<{result: Commands[T]['returnType']}> {
|
||||||
|
return await this.#connection.send(method, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
@throwIfDisposed((session: Session) => {
|
||||||
|
// SAFETY: By definition of `disposed`, `#reason` is defined.
|
||||||
|
return session.#reason!;
|
||||||
|
})
|
||||||
|
async subscribe(events: string[]): Promise<void> {
|
||||||
|
await this.send('session.subscribe', {
|
||||||
|
events,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@throwIfDisposed((session: Session) => {
|
||||||
|
// SAFETY: By definition of `disposed`, `#reason` is defined.
|
||||||
|
return session.#reason!;
|
||||||
|
})
|
||||||
|
async end(): Promise<void> {
|
||||||
|
await this.send('session.end', {});
|
||||||
|
this.#reason = `Session (${this.id}) has already ended.`;
|
||||||
|
this.emit('ended', {reason: this.#reason});
|
||||||
|
this.removeAllListeners();
|
||||||
|
}
|
||||||
|
}
|
125
packages/puppeteer-core/src/bidi/core/UserContext.ts
Normal file
125
packages/puppeteer-core/src/bidi/core/UserContext.ts
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2024 Google Inc.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
||||||
|
|
||||||
|
import {EventEmitter} from '../../common/EventEmitter.js';
|
||||||
|
import {assert} from '../../util/assert.js';
|
||||||
|
|
||||||
|
import type {Browser} from './Browser.js';
|
||||||
|
import {BrowsingContext} from './BrowsingContext.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export type CreateBrowsingContextOptions = Omit<
|
||||||
|
Bidi.BrowsingContext.CreateParameters,
|
||||||
|
'type' | 'referenceContext'
|
||||||
|
> & {
|
||||||
|
referenceContext?: BrowsingContext;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export class UserContext extends EventEmitter<{
|
||||||
|
/**
|
||||||
|
* Emitted when a new browsing context is created.
|
||||||
|
*/
|
||||||
|
browsingcontext: {
|
||||||
|
/** The new browsing context. */
|
||||||
|
browsingContext: BrowsingContext;
|
||||||
|
};
|
||||||
|
}> {
|
||||||
|
static create(browser: Browser, id: string): UserContext {
|
||||||
|
const context = new UserContext(browser, id);
|
||||||
|
context.#initialize();
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
// keep-sorted start
|
||||||
|
// Note these are only top-level contexts.
|
||||||
|
readonly #browsingContexts = new Map<string, BrowsingContext>();
|
||||||
|
// @ts-expect-error -- TODO: This will be used once the WebDriver BiDi
|
||||||
|
// protocol supports it.
|
||||||
|
readonly #id: string;
|
||||||
|
readonly browser: Browser;
|
||||||
|
// keep-sorted end
|
||||||
|
|
||||||
|
private constructor(browser: Browser, id: string) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
// keep-sorted start
|
||||||
|
this.#id = id;
|
||||||
|
this.browser = browser;
|
||||||
|
// keep-sorted end
|
||||||
|
}
|
||||||
|
|
||||||
|
#initialize() {
|
||||||
|
// ///////////////////////
|
||||||
|
// Session listeners //
|
||||||
|
// ///////////////////////
|
||||||
|
const session = this.#session;
|
||||||
|
session.on('browsingContext.contextCreated', info => {
|
||||||
|
if (info.parent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const browsingContext = BrowsingContext.from(
|
||||||
|
this,
|
||||||
|
undefined,
|
||||||
|
info.context,
|
||||||
|
info.url
|
||||||
|
);
|
||||||
|
browsingContext.on('destroyed', () => {
|
||||||
|
this.#browsingContexts.delete(browsingContext.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.#browsingContexts.set(browsingContext.id, browsingContext);
|
||||||
|
|
||||||
|
this.emit('browsingcontext', {browsingContext});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// keep-sorted start block=yes
|
||||||
|
get #session() {
|
||||||
|
return this.browser.session;
|
||||||
|
}
|
||||||
|
get browsingContexts(): Iterable<BrowsingContext> {
|
||||||
|
return this.#browsingContexts.values();
|
||||||
|
}
|
||||||
|
// keep-sorted end
|
||||||
|
|
||||||
|
async createBrowsingContext(
|
||||||
|
type: Bidi.BrowsingContext.CreateType,
|
||||||
|
options: CreateBrowsingContextOptions = {}
|
||||||
|
): Promise<BrowsingContext> {
|
||||||
|
const {
|
||||||
|
result: {context: contextId},
|
||||||
|
} = await this.#session.send('browsingContext.create', {
|
||||||
|
type,
|
||||||
|
...options,
|
||||||
|
referenceContext: options.referenceContext?.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const browsingContext = this.#browsingContexts.get(contextId);
|
||||||
|
assert(
|
||||||
|
browsingContext,
|
||||||
|
'The WebDriver BiDi implementation is failing to create a browsing context correctly.'
|
||||||
|
);
|
||||||
|
|
||||||
|
// We use an array to avoid the promise from being awaited.
|
||||||
|
return browsingContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
async close(): Promise<void> {
|
||||||
|
const promises = [];
|
||||||
|
for (const browsingContext of this.#browsingContexts.values()) {
|
||||||
|
promises.push(browsingContext.close());
|
||||||
|
}
|
||||||
|
await Promise.all(promises);
|
||||||
|
}
|
||||||
|
}
|
93
packages/puppeteer-core/src/bidi/core/UserPrompt.ts
Normal file
93
packages/puppeteer-core/src/bidi/core/UserPrompt.ts
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2024 Google Inc.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
||||||
|
|
||||||
|
import {EventEmitter} from '../../common/EventEmitter.js';
|
||||||
|
|
||||||
|
import type {BrowsingContext} from './BrowsingContext.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export type HandleOptions = Omit<
|
||||||
|
Bidi.BrowsingContext.HandleUserPromptParameters,
|
||||||
|
'context'
|
||||||
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export type UserPromptResult = Omit<
|
||||||
|
Bidi.BrowsingContext.UserPromptClosedParameters,
|
||||||
|
'context'
|
||||||
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export class UserPrompt extends EventEmitter<{
|
||||||
|
handled: UserPromptResult;
|
||||||
|
}> {
|
||||||
|
static from(
|
||||||
|
browsingContext: BrowsingContext,
|
||||||
|
info: Bidi.BrowsingContext.UserPromptOpenedParameters
|
||||||
|
): UserPrompt {
|
||||||
|
const userPrompt = new UserPrompt(browsingContext, info);
|
||||||
|
userPrompt.#initialize();
|
||||||
|
return userPrompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// keep-sorted start
|
||||||
|
#result?: UserPromptResult;
|
||||||
|
readonly info: Bidi.BrowsingContext.UserPromptOpenedParameters;
|
||||||
|
readonly browsingContext: BrowsingContext;
|
||||||
|
// keep-sorted end
|
||||||
|
|
||||||
|
private constructor(
|
||||||
|
context: BrowsingContext,
|
||||||
|
info: Bidi.BrowsingContext.UserPromptOpenedParameters
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
// keep-sorted start
|
||||||
|
this.info = info;
|
||||||
|
this.browsingContext = context;
|
||||||
|
// keep-sorted end
|
||||||
|
}
|
||||||
|
|
||||||
|
#initialize() {
|
||||||
|
// ///////////////////////
|
||||||
|
// Session listeners //
|
||||||
|
// ///////////////////////
|
||||||
|
this.#session.on('browsingContext.userPromptClosed', parameters => {
|
||||||
|
if (parameters.context !== this.browsingContext.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#result = parameters;
|
||||||
|
this.emit('handled', parameters);
|
||||||
|
this.removeAllListeners();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// keep-sorted start block=yes
|
||||||
|
get #session() {
|
||||||
|
return this.browsingContext.userContext.browser.session;
|
||||||
|
}
|
||||||
|
get result(): UserPromptResult | undefined {
|
||||||
|
return this.#result;
|
||||||
|
}
|
||||||
|
// keep-sorted end
|
||||||
|
|
||||||
|
async handle(options: HandleOptions = {}): Promise<UserPromptResult> {
|
||||||
|
await this.#session.send('browsingContext.handleUserPrompt', {
|
||||||
|
...options,
|
||||||
|
context: this.info.context,
|
||||||
|
});
|
||||||
|
// SAFETY: `handled` is triggered before the above promise resolved.
|
||||||
|
return this.#result!;
|
||||||
|
}
|
||||||
|
}
|
15
packages/puppeteer-core/src/bidi/core/core.ts
Normal file
15
packages/puppeteer-core/src/bidi/core/core.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2024 Google Inc.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from './Browser.js';
|
||||||
|
export * from './BrowsingContext.js';
|
||||||
|
export * from './Connection.js';
|
||||||
|
export * from './Navigation.js';
|
||||||
|
export * from './Realm.js';
|
||||||
|
export * from './Request.js';
|
||||||
|
export * from './Session.js';
|
||||||
|
export * from './UserContext.js';
|
||||||
|
export * from './UserPrompt.js';
|
Loading…
Reference in New Issue
Block a user