Initial commit: Torrent search and download application

This commit is contained in:
vrubelroman 2025-10-05 22:20:49 +03:00
commit e38be704ff
4313 changed files with 791544 additions and 0 deletions

207
node_modules/puppeteer-core/src/bidi/BidiOverCdp.ts generated vendored Normal file
View file

@ -0,0 +1,207 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import * as BidiMapper from 'chromium-bidi/lib/cjs/bidiMapper/BidiMapper.js';
import type {ProtocolMapping} from 'devtools-protocol/types/protocol-mapping.js';
import type {CDPEvents, CDPSession} from '../api/CDPSession.js';
import type {Connection as CdpConnection} from '../cdp/Connection.js';
import {debug} from '../common/Debug.js';
import {TargetCloseError} from '../common/Errors.js';
import type {Handler} from '../common/EventEmitter.js';
import {BidiConnection} from './Connection.js';
const bidiServerLogger = (prefix: string, ...args: unknown[]): void => {
debug(`bidi:${prefix}`)(args);
};
/**
* @internal
*/
export async function connectBidiOverCdp(
cdp: CdpConnection,
): Promise<BidiConnection> {
const transportBiDi = new NoOpTransport();
const cdpConnectionAdapter = new CdpConnectionAdapter(cdp);
const pptrTransport = {
send(message: string): void {
// Forwards a BiDi command sent by Puppeteer to the input of the BidiServer.
transportBiDi.emitMessage(JSON.parse(message));
},
close(): void {
bidiServer.close();
cdpConnectionAdapter.close();
cdp.dispose();
},
onmessage(_message: string): void {
// The method is overridden by the Connection.
},
};
transportBiDi.on('bidiResponse', (message: object) => {
// Forwards a BiDi event sent by BidiServer to Puppeteer.
pptrTransport.onmessage(JSON.stringify(message));
});
const pptrBiDiConnection = new BidiConnection(
cdp.url(),
pptrTransport,
cdp.delay,
cdp.timeout,
);
const bidiServer = await BidiMapper.BidiServer.createAndStart(
transportBiDi,
cdpConnectionAdapter,
cdpConnectionAdapter.browserClient(),
/* selfTargetId= */ '',
undefined,
bidiServerLogger,
);
return pptrBiDiConnection;
}
/**
* Manages CDPSessions for BidiServer.
* @internal
*/
class CdpConnectionAdapter {
#cdp: CdpConnection;
#adapters = new Map<CDPSession, CDPClientAdapter<CDPSession>>();
#browserCdpConnection: CDPClientAdapter<CdpConnection>;
constructor(cdp: CdpConnection) {
this.#cdp = cdp;
this.#browserCdpConnection = new CDPClientAdapter(cdp);
}
browserClient(): CDPClientAdapter<CdpConnection> {
return this.#browserCdpConnection;
}
getCdpClient(id: string) {
const session = this.#cdp.session(id);
if (!session) {
throw new Error(`Unknown CDP session with id ${id}`);
}
if (!this.#adapters.has(session)) {
const adapter = new CDPClientAdapter(
session,
id,
this.#browserCdpConnection,
);
this.#adapters.set(session, adapter);
return adapter;
}
return this.#adapters.get(session)!;
}
close() {
this.#browserCdpConnection.close();
for (const adapter of this.#adapters.values()) {
adapter.close();
}
}
}
/**
* Wrapper on top of CDPSession/CDPConnection to satisfy CDP interface that
* BidiServer needs.
*
* @internal
*/
class CDPClientAdapter<T extends CDPSession | CdpConnection>
extends BidiMapper.EventEmitter<CDPEvents>
implements BidiMapper.CdpClient
{
#closed = false;
#client: T;
sessionId: string | undefined = undefined;
#browserClient?: BidiMapper.CdpClient;
constructor(
client: T,
sessionId?: string,
browserClient?: BidiMapper.CdpClient,
) {
super();
this.#client = client;
this.sessionId = sessionId;
this.#browserClient = browserClient;
this.#client.on('*', this.#forwardMessage as Handler<any>);
}
browserClient(): BidiMapper.CdpClient {
return this.#browserClient!;
}
#forwardMessage = <T extends keyof CDPEvents>(
method: T,
event: CDPEvents[T],
) => {
this.emit(method, event);
};
async sendCommand<T extends keyof ProtocolMapping.Commands>(
method: T,
...params: ProtocolMapping.Commands[T]['paramsType']
): Promise<ProtocolMapping.Commands[T]['returnType']> {
if (this.#closed) {
return;
}
try {
return await this.#client.send(method, ...params);
} catch (err) {
if (this.#closed) {
return;
}
throw err;
}
}
close() {
this.#client.off('*', this.#forwardMessage as Handler<any>);
this.#closed = true;
}
isCloseError(error: unknown): boolean {
return error instanceof TargetCloseError;
}
}
/**
* This transport is given to the BiDi server instance and allows Puppeteer
* to send and receive commands to the BiDiServer.
* @internal
*/
class NoOpTransport
extends BidiMapper.EventEmitter<{
bidiResponse: any;
}>
implements BidiMapper.BidiTransport
{
#onMessage: (message: any) => Promise<void> | void = async (
_m: any,
): Promise<void> => {
return;
};
emitMessage(message: any) {
void this.#onMessage(message);
}
setOnMessage(onMessage: (message: any) => Promise<void> | void): void {
this.#onMessage = onMessage;
}
async sendMessage(message: any): Promise<void> {
this.emit('bidiResponse', message);
}
close() {
this.#onMessage = async (_m: any): Promise<void> => {
return;
};
}
}

315
node_modules/puppeteer-core/src/bidi/Browser.ts generated vendored Normal file
View file

@ -0,0 +1,315 @@
/**
* @license
* Copyright 2022 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {ChildProcess} from 'node:child_process';
import * as Bidi from 'webdriver-bidi-protocol';
import type {BrowserEvents} from '../api/Browser.js';
import {
Browser,
BrowserEvent,
type BrowserCloseCallback,
type BrowserContextOptions,
type DebugInfo,
} from '../api/Browser.js';
import {BrowserContextEvent} from '../api/BrowserContext.js';
import type {Page} from '../api/Page.js';
import type {Target} from '../api/Target.js';
import type {Connection as CdpConnection} from '../cdp/Connection.js';
import type {SupportedWebDriverCapabilities} from '../common/ConnectOptions.js';
import {ProtocolError} 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 {BidiBrowserContext} from './BrowserContext.js';
import type {BidiConnection, CdpEvent} 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} from './Target.js';
/**
* @internal
*/
export interface BidiBrowserOptions {
process?: ChildProcess;
closeCallback?: BrowserCloseCallback;
connection: BidiConnection;
cdpConnection?: CdpConnection;
defaultViewport: Viewport | null;
acceptInsecureCerts?: boolean;
capabilities?: SupportedWebDriverCapabilities;
networkEnabled: boolean;
}
/**
* @internal
*/
export class BidiBrowser extends Browser {
readonly protocol = 'webDriverBiDi';
static readonly subscribeModules: [string, ...string[]] = [
'browsingContext',
'network',
'log',
'script',
'input',
];
static readonly subscribeCdpEvents: Array<CdpEvent['method']> = [
// Coverage
'goog:cdp.Debugger.scriptParsed',
'goog:cdp.CSS.styleSheetAdded',
'goog:cdp.Runtime.executionContextsCleared',
// Tracing
'goog:cdp.Tracing.tracingComplete',
// TODO: subscribe to all CDP events in the future.
'goog:cdp.Network.requestWillBeSent',
'goog:cdp.Debugger.scriptParsed',
'goog:cdp.Page.screencastFrame',
];
static async create(opts: BidiBrowserOptions): Promise<BidiBrowser> {
const session = await Session.from(opts.connection, {
firstMatch: opts.capabilities?.firstMatch,
alwaysMatch: {
...opts.capabilities?.alwaysMatch,
// Capabilities that come from Puppeteer's API take precedence.
acceptInsecureCerts: opts.acceptInsecureCerts,
unhandledPromptBehavior: {
default: Bidi.Session.UserPromptHandlerType.Ignore,
},
webSocketUrl: true,
// Puppeteer with WebDriver BiDi does not support prerendering
// yet because WebDriver BiDi behavior is not specified. See
// https://github.com/w3c/webdriver-bidi/issues/321.
'goog:prerenderingDisabled': true,
},
});
// Subscribe to all WebDriver BiDi events. Also subscribe to CDP events if CDP
// connection is available.
await session.subscribe(
(opts.cdpConnection
? [...BidiBrowser.subscribeModules, ...BidiBrowser.subscribeCdpEvents]
: BidiBrowser.subscribeModules
).filter(module => {
if (!opts.networkEnabled) {
return (
module !== 'network' &&
module !== 'goog:cdp.Network.requestWillBeSent'
);
}
return true;
}) as [string, ...string[]],
);
try {
await session.send('network.addDataCollector', {
dataTypes: [Bidi.Network.DataType.Response],
// Buffer size of 20 MB is equivalent to the CDP:
maxEncodedDataSize: 20 * 1000 * 1000, // 20 MB
});
} catch (err) {
if (err instanceof ProtocolError) {
// Ignore protocol errors, as the data collectors can be not implemented.
debugError(err);
} else {
throw err;
}
}
const browser = new BidiBrowser(session.browser, opts);
browser.#initialize();
return browser;
}
@bubble()
accessor #trustedEmitter = new EventEmitter<BrowserEvents>();
#process?: ChildProcess;
#closeCallback?: BrowserCloseCallback;
#browserCore: BrowserCore;
#defaultViewport: Viewport | null;
#browserContexts = new WeakMap<UserContext, BidiBrowserContext>();
#target = new BidiBrowserTarget(this);
#cdpConnection?: CdpConnection;
#networkEnabled: boolean;
private constructor(browserCore: BrowserCore, opts: BidiBrowserOptions) {
super();
this.#process = opts.process;
this.#closeCallback = opts.closeCallback;
this.#browserCore = browserCore;
this.#defaultViewport = opts.defaultViewport;
this.#cdpConnection = opts.cdpConnection;
this.#networkEnabled = opts.networkEnabled;
}
#initialize() {
// Initializing existing contexts.
for (const userContext of this.#browserCore.userContexts) {
this.#createBrowserContext(userContext);
}
this.#browserCore.once('disconnected', () => {
this.#trustedEmitter.emit(BrowserEvent.Disconnected, undefined);
this.#trustedEmitter.removeAllListeners();
});
this.#process?.once('close', () => {
this.#browserCore.dispose('Browser process exited.', true);
this.connection.dispose();
});
}
get #browserName() {
return this.#browserCore.session.capabilities.browserName;
}
get #browserVersion() {
return this.#browserCore.session.capabilities.browserVersion;
}
get cdpSupported(): boolean {
return this.#cdpConnection !== undefined;
}
get cdpConnection(): CdpConnection | undefined {
return this.#cdpConnection;
}
override async userAgent(): Promise<string> {
return this.#browserCore.session.capabilities.userAgent;
}
#createBrowserContext(userContext: UserContext) {
const browserContext = BidiBrowserContext.from(this, userContext, {
defaultViewport: this.#defaultViewport,
});
this.#browserContexts.set(userContext, browserContext);
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);
},
);
return browserContext;
}
get connection(): BidiConnection {
// SAFETY: We only have one implementation.
return this.#browserCore.session.connection as BidiConnection;
}
override wsEndpoint(): string {
return this.connection.url;
}
override async close(): Promise<void> {
if (this.connection.closed) {
return;
}
try {
await this.#browserCore.close();
await this.#closeCallback?.call(null);
} catch (error) {
// Fail silently.
debugError(error);
} finally {
this.connection.dispose();
}
}
override get connected(): boolean {
return !this.#browserCore.disconnected;
}
override process(): ChildProcess | null {
return this.#process ?? null;
}
override async createBrowserContext(
options: BrowserContextOptions = {},
): Promise<BidiBrowserContext> {
const userContext = await this.#browserCore.createUserContext(options);
return this.#createBrowserContext(userContext);
}
override async version(): Promise<string> {
return `${this.#browserName}/${this.#browserVersion}`;
}
override browserContexts(): BidiBrowserContext[] {
return [...this.#browserCore.userContexts].map(context => {
return this.#browserContexts.get(context)!;
});
}
override defaultBrowserContext(): BidiBrowserContext {
return this.#browserContexts.get(this.#browserCore.defaultUserContext)!;
}
override newPage(): Promise<Page> {
return this.defaultBrowserContext().newPage();
}
override installExtension(path: string): Promise<string> {
return this.#browserCore.installExtension(path);
}
override async uninstallExtension(id: string): Promise<void> {
await this.#browserCore.uninstallExtension(id);
}
override targets(): Target[] {
return [
this.#target,
...this.browserContexts().flatMap(context => {
return context.targets();
}),
];
}
override target(): BidiBrowserTarget {
return this.#target;
}
override async disconnect(): Promise<void> {
try {
await this.#browserCore.session.end();
} catch (error) {
// Fail silently.
debugError(error);
} finally {
this.connection.dispose();
}
}
override get debugInfo(): DebugInfo {
return {
pendingProtocolErrors: this.connection.getPendingProtocolErrors(),
};
}
override isNetworkEnabled(): boolean {
return this.#networkEnabled;
}
}

View file

@ -0,0 +1,123 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {BrowserCloseCallback} from '../api/Browser.js';
import {Connection} from '../cdp/Connection.js';
import type {ConnectionTransport} from '../common/ConnectionTransport.js';
import type {ConnectOptions} from '../common/ConnectOptions.js';
import {ProtocolError, UnsupportedOperation} from '../common/Errors.js';
import {debugError, DEFAULT_VIEWPORT} from '../common/util.js';
import type {BidiBrowser} from './Browser.js';
import type {BidiConnection} from './Connection.js';
/**
* Users should never call this directly; it's called when calling `puppeteer.connect`
* with `protocol: 'webDriverBiDi'`. This method attaches Puppeteer to an existing browser
* instance. First it tries to connect to the browser using pure BiDi. If the protocol is
* not supported, connects to the browser using BiDi over CDP.
*
* @internal
*/
export async function _connectToBiDiBrowser(
connectionTransport: ConnectionTransport,
url: string,
options: ConnectOptions,
): Promise<BidiBrowser> {
const {
acceptInsecureCerts = false,
networkEnabled = true,
defaultViewport = DEFAULT_VIEWPORT,
} = options;
const {bidiConnection, cdpConnection, closeCallback} =
await getBiDiConnection(connectionTransport, url, options);
const BiDi = await import(/* webpackIgnore: true */ './bidi.js');
const bidiBrowser = await BiDi.BidiBrowser.create({
connection: bidiConnection,
cdpConnection,
closeCallback,
process: undefined,
defaultViewport: defaultViewport,
acceptInsecureCerts: acceptInsecureCerts,
networkEnabled,
capabilities: options.capabilities,
});
return bidiBrowser;
}
/**
* Returns a BiDiConnection established to the endpoint specified by the options and a
* callback closing the browser. Callback depends on whether the connection is pure BiDi
* or BiDi over CDP.
* The method tries to connect to the browser using pure BiDi protocol, and falls back
* to BiDi over CDP.
*/
async function getBiDiConnection(
connectionTransport: ConnectionTransport,
url: string,
options: ConnectOptions,
): Promise<{
cdpConnection?: Connection;
bidiConnection: BidiConnection;
closeCallback: BrowserCloseCallback;
}> {
const BiDi = await import(/* webpackIgnore: true */ './bidi.js');
const {slowMo = 0, protocolTimeout} = options;
// Try pure BiDi first.
const pureBidiConnection = new BiDi.BidiConnection(
url,
connectionTransport,
slowMo,
protocolTimeout,
);
try {
const result = await pureBidiConnection.send('session.status', {});
if ('type' in result && result.type === 'success') {
// The `browserWSEndpoint` points to an endpoint supporting pure WebDriver BiDi.
return {
bidiConnection: pureBidiConnection,
closeCallback: async () => {
await pureBidiConnection.send('browser.close', {}).catch(debugError);
},
};
}
} catch (e) {
if (!(e instanceof ProtocolError)) {
// Unexpected exception not related to BiDi / CDP. Rethrow.
throw e;
}
}
// Unbind the connection to avoid memory leaks.
pureBidiConnection.unbind();
// Fall back to CDP over BiDi reusing the WS connection.
const cdpConnection = new Connection(
url,
connectionTransport,
slowMo,
protocolTimeout,
/* rawErrors= */ true,
);
const version = await cdpConnection.send('Browser.getVersion');
if (version.product.toLowerCase().includes('firefox')) {
throw new UnsupportedOperation(
'Firefox is not supported in BiDi over CDP mode.',
);
}
const bidiOverCdpConnection = await BiDi.connectBidiOverCdp(cdpConnection);
return {
cdpConnection,
bidiConnection: bidiOverCdpConnection,
closeCallback: async () => {
// In case of BiDi over CDP, we need to close browser via CDP.
await cdpConnection.send('Browser.close').catch(debugError);
},
};
}

331
node_modules/puppeteer-core/src/bidi/BrowserContext.ts generated vendored Normal file
View file

@ -0,0 +1,331 @@
/**
* @license
* Copyright 2022 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import * as Bidi from 'webdriver-bidi-protocol';
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 type {Cookie, CookieData} from '../common/Cookie.js';
import {EventEmitter} from '../common/EventEmitter.js';
import {debugError} from '../common/util.js';
import type {Viewport} from '../common/Viewport.js';
import {assert} from '../util/assert.js';
import {bubble} from '../util/decorators.js';
import type {BidiBrowser} from './Browser.js';
import type {BrowsingContext} from './core/BrowsingContext.js';
import {UserContext} from './core/UserContext.js';
import type {BidiFrame} from './Frame.js';
import {
BidiPage,
bidiToPuppeteerCookie,
cdpSpecificCookiePropertiesFromPuppeteerToBidi,
convertCookiesExpiryCdpToBiDi,
convertCookiesPartitionKeyFromPuppeteerToBiDi,
convertCookiesSameSiteCdpToBiDi,
} from './Page.js';
import {BidiWorkerTarget} from './Target.js';
import {BidiFrameTarget, BidiPageTarget} from './Target.js';
import type {BidiWebWorker} from './WebWorker.js';
/**
* @internal
*/
export interface BidiBrowserContextOptions {
defaultViewport: Viewport | null;
}
/**
* @internal
*/
export class BidiBrowserContext extends BrowserContext {
static from(
browser: BidiBrowser,
userContext: UserContext,
options: BidiBrowserContextOptions,
): BidiBrowserContext {
const context = new BidiBrowserContext(browser, userContext, options);
context.#initialize();
return context;
}
@bubble()
accessor trustedEmitter = new EventEmitter<BrowserContextEvents>();
readonly #browser: BidiBrowser;
readonly #defaultViewport: Viewport | null;
// This is public because of cookies.
readonly userContext: UserContext;
readonly #pages = new WeakMap<BrowsingContext, BidiPage>();
readonly #targets = new Map<
BidiPage,
[
BidiPageTarget,
Map<BidiFrame | BidiWebWorker, BidiFrameTarget | BidiWorkerTarget>,
]
>();
#overrides: Array<{origin: string; permission: Permission}> = [];
private constructor(
browser: BidiBrowser,
userContext: UserContext,
options: BidiBrowserContextOptions,
) {
super();
this.#browser = browser;
this.userContext = userContext;
this.#defaultViewport = options.defaultViewport;
}
#initialize() {
// Create targets for existing browsing contexts.
for (const browsingContext of this.userContext.browsingContexts) {
this.#createPage(browsingContext);
}
this.userContext.on('browsingcontext', ({browsingContext}) => {
const page = this.#createPage(browsingContext);
// We need to wait for the DOMContentLoaded as the
// browsingContext still may be navigating from the about:blank
if (browsingContext.originalOpener) {
for (const context of this.userContext.browsingContexts) {
if (context.id === browsingContext.originalOpener) {
this.#pages
.get(context)!
.trustedEmitter.emit(PageEvent.Popup, page);
}
}
}
});
this.userContext.on('closed', () => {
this.trustedEmitter.removeAllListeners();
});
}
#createPage(browsingContext: BrowsingContext): BidiPage {
const page = BidiPage.from(this, browsingContext);
this.#pages.set(browsingContext, page);
page.trustedEmitter.on(PageEvent.Close, () => {
this.#pages.delete(browsingContext);
});
// -- Target stuff starts here --
const pageTarget = new BidiPageTarget(page);
const pageTargets = new Map();
this.#targets.set(page, [pageTarget, pageTargets]);
page.trustedEmitter.on(PageEvent.FrameAttached, frame => {
const bidiFrame = frame as BidiFrame;
const target = new BidiFrameTarget(bidiFrame);
pageTargets.set(bidiFrame, target);
this.trustedEmitter.emit(BrowserContextEvent.TargetCreated, target);
});
page.trustedEmitter.on(PageEvent.FrameNavigated, frame => {
const bidiFrame = frame as BidiFrame;
const target = pageTargets.get(bidiFrame);
// If there is no target, then this is the page's frame.
if (target === undefined) {
this.trustedEmitter.emit(BrowserContextEvent.TargetChanged, pageTarget);
} else {
this.trustedEmitter.emit(BrowserContextEvent.TargetChanged, target);
}
});
page.trustedEmitter.on(PageEvent.FrameDetached, frame => {
const bidiFrame = frame as BidiFrame;
const target = pageTargets.get(bidiFrame);
if (target === undefined) {
return;
}
pageTargets.delete(bidiFrame);
this.trustedEmitter.emit(BrowserContextEvent.TargetDestroyed, target);
});
page.trustedEmitter.on(PageEvent.WorkerCreated, worker => {
const bidiWorker = worker as BidiWebWorker;
const target = new BidiWorkerTarget(bidiWorker);
pageTargets.set(bidiWorker, target);
this.trustedEmitter.emit(BrowserContextEvent.TargetCreated, target);
});
page.trustedEmitter.on(PageEvent.WorkerDestroyed, worker => {
const bidiWorker = worker as BidiWebWorker;
const target = pageTargets.get(bidiWorker);
if (target === undefined) {
return;
}
pageTargets.delete(worker);
this.trustedEmitter.emit(BrowserContextEvent.TargetDestroyed, target);
});
page.trustedEmitter.on(PageEvent.Close, () => {
this.#targets.delete(page);
this.trustedEmitter.emit(BrowserContextEvent.TargetDestroyed, pageTarget);
});
this.trustedEmitter.emit(BrowserContextEvent.TargetCreated, pageTarget);
// -- Target stuff ends here --
return page;
}
override targets(): Target[] {
return [...this.#targets.values()].flatMap(([target, frames]) => {
return [target, ...frames.values()];
});
}
override async newPage(): Promise<Page> {
using _guard = await this.waitForScreenshotOperations();
const context = await this.userContext.createBrowsingContext(
Bidi.BrowsingContext.CreateType.Tab,
);
const page = this.#pages.get(context)!;
if (!page) {
throw new Error('Page is not found');
}
if (this.#defaultViewport) {
try {
await page.setViewport(this.#defaultViewport);
} catch {
// No support for setViewport in Firefox.
}
}
return page;
}
override async close(): Promise<void> {
assert(
this.userContext.id !== UserContext.DEFAULT,
'Default BrowserContext cannot be closed!',
);
try {
await this.userContext.remove();
} catch (error) {
debugError(error);
}
this.#targets.clear();
}
override browser(): BidiBrowser {
return this.#browser;
}
override async pages(): Promise<BidiPage[]> {
return [...this.userContext.browsingContexts].map(context => {
return this.#pages.get(context)!;
});
}
override async overridePermissions(
origin: string,
permissions: Permission[],
): Promise<void> {
const permissionsSet = new Set(
permissions.map(permission => {
const protocolPermission =
WEB_PERMISSION_TO_PROTOCOL_PERMISSION.get(permission);
if (!protocolPermission) {
throw new Error('Unknown permission: ' + permission);
}
return permission;
}),
);
await Promise.all(
Array.from(WEB_PERMISSION_TO_PROTOCOL_PERMISSION.keys()).map(
permission => {
const result = this.userContext.setPermissions(
origin,
{
name: permission,
},
permissionsSet.has(permission)
? Bidi.Permissions.PermissionState.Granted
: Bidi.Permissions.PermissionState.Denied,
);
this.#overrides.push({origin, permission});
// TODO: some permissions are outdated and setting them to denied does
// not work.
if (!permissionsSet.has(permission)) {
return result.catch(debugError);
}
return result;
},
),
);
}
override async clearPermissionOverrides(): Promise<void> {
const promises = this.#overrides.map(({permission, origin}) => {
return this.userContext
.setPermissions(
origin,
{
name: permission,
},
Bidi.Permissions.PermissionState.Prompt,
)
.catch(debugError);
});
this.#overrides = [];
await Promise.all(promises);
}
override get id(): string | undefined {
if (this.userContext.id === UserContext.DEFAULT) {
return undefined;
}
return this.userContext.id;
}
override async cookies(): Promise<Cookie[]> {
const cookies = await this.userContext.getCookies();
return cookies.map(cookie => {
return bidiToPuppeteerCookie(cookie, true);
});
}
override async setCookie(...cookies: CookieData[]): Promise<void> {
await Promise.all(
cookies.map(async cookie => {
const bidiCookie: Bidi.Storage.PartialCookie = {
domain: cookie.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)}
: {}),
...{expiry: convertCookiesExpiryCdpToBiDi(cookie.expires)},
// Chrome-specific properties.
...cdpSpecificCookiePropertiesFromPuppeteerToBidi(
cookie,
'sameParty',
'sourceScheme',
'priority',
'url',
),
};
return await this.userContext.setCookie(
bidiCookie,
convertCookiesPartitionKeyFromPuppeteerToBiDi(cookie.partitionKey),
);
}),
);
}
}

124
node_modules/puppeteer-core/src/bidi/CDPSession.ts generated vendored Normal file
View file

@ -0,0 +1,124 @@
/**
* @license
* Copyright 2024 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type ProtocolMapping from 'devtools-protocol/types/protocol-mapping.js';
import type {CommandOptions} from '../api/CDPSession.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;
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 {result} = await connection.send('goog:cdp.getSession', {
context: frame._id,
});
this.#sessionId.resolve(result.session!);
BidiCdpSession.sessions.set(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 get detached(): boolean {
return this.#detached;
}
override async send<T extends keyof ProtocolMapping.Commands>(
method: T,
params?: ProtocolMapping.Commands[T]['paramsType'][0],
options?: CommandOptions,
): 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(
'goog:cdp.sendCommand',
{
method: method,
params: params,
session,
},
options?.timeout,
);
return result.result;
}
override async detach(): Promise<void> {
if (
this.#connection === undefined ||
this.#connection.closed ||
this.#detached
) {
return;
}
try {
await this.frame.client.send('Target.detachFromTarget', {
sessionId: this.id(),
});
} finally {
this.onClose();
}
}
/**
* @internal
*/
onClose = (): void => {
BidiCdpSession.sessions.delete(this.id());
this.#detached = true;
};
override id(): string {
const value = this.#sessionId.value();
return typeof value === 'string' ? value : '';
}
}

213
node_modules/puppeteer-core/src/bidi/Connection.ts generated vendored Normal file
View file

@ -0,0 +1,213 @@
/**
* @license
* Copyright 2017 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type * as ChromiumBidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import type * as Bidi from 'webdriver-bidi-protocol';
import {CallbackRegistry} from '../common/CallbackRegistry.js';
import type {ConnectionTransport} from '../common/ConnectionTransport.js';
import {debug} from '../common/Debug.js';
import {ConnectionClosedError} from '../common/Errors.js';
import type {EventsWithWildcard} from '../common/EventEmitter.js';
import {EventEmitter} from '../common/EventEmitter.js';
import {debugError} from '../common/util.js';
import {BidiCdpSession} from './CDPSession.js';
import type {
BidiEvents,
Commands as BidiCommands,
Connection,
} from './core/Connection.js';
const debugProtocolSend = debug('puppeteer:webDriverBiDi:SEND ►');
const debugProtocolReceive = debug('puppeteer:webDriverBiDi:RECV ◀');
export type CdpEvent = ChromiumBidi.Cdp.Event;
/**
* @internal
*/
export interface Commands extends BidiCommands {
'goog:cdp.sendCommand': {
params: ChromiumBidi.Cdp.SendCommandParameters;
returnType: ChromiumBidi.Cdp.SendCommandResult;
};
'goog:cdp.getSession': {
params: ChromiumBidi.Cdp.GetSessionParameters;
returnType: ChromiumBidi.Cdp.GetSessionResult;
};
'goog:cdp.resolveRealm': {
params: ChromiumBidi.Cdp.ResolveRealmParameters;
returnType: ChromiumBidi.Cdp.ResolveRealmResult;
};
}
/**
* @internal
*/
export class BidiConnection
extends EventEmitter<BidiEvents>
implements Connection
{
#url: string;
#transport: ConnectionTransport;
#delay: number;
#timeout = 0;
#closed = false;
#callbacks = new CallbackRegistry();
#emitters: Array<EventEmitter<any>> = [];
constructor(
url: string,
transport: ConnectionTransport,
delay = 0,
timeout?: number,
) {
super();
this.#url = url;
this.#delay = delay;
this.#timeout = timeout ?? 180_000;
this.#transport = transport;
this.#transport.onmessage = this.onMessage.bind(this);
this.#transport.onclose = this.unbind.bind(this);
}
get closed(): boolean {
return this.#closed;
}
get url(): string {
return this.#url;
}
pipeTo<Events extends BidiEvents>(emitter: EventEmitter<Events>): void {
this.#emitters.push(emitter);
}
override emit<Key extends keyof EventsWithWildcard<BidiEvents>>(
type: Key,
event: EventsWithWildcard<BidiEvents>[Key],
): boolean {
for (const emitter of this.#emitters) {
emitter.emit(type, event);
}
return super.emit(type, event);
}
send<T extends keyof Commands>(
method: T,
params: Commands[T]['params'],
timeout?: number,
): Promise<{result: Commands[T]['returnType']}> {
if (this.#closed) {
return Promise.reject(new ConnectionClosedError('Connection closed.'));
}
return this.#callbacks.create(method, timeout ?? this.#timeout, id => {
const stringifiedMessage = JSON.stringify({
id,
method,
params,
} as Bidi.Command);
debugProtocolSend(stringifiedMessage);
this.#transport.send(stringifiedMessage);
}) as Promise<{result: Commands[T]['returnType']}>;
}
/**
* @internal
*/
protected async onMessage(message: string): Promise<void> {
if (this.#delay) {
await new Promise(f => {
return setTimeout(f, this.#delay);
});
}
debugProtocolReceive(message);
const object: Bidi.Message | CdpEvent = JSON.parse(message);
if ('type' in object) {
switch (object.type) {
case 'success':
this.#callbacks.resolve(object.id, object);
return;
case 'error':
if (object.id === null) {
break;
}
this.#callbacks.reject(
object.id,
createProtocolError(object),
`${object.error}: ${object.message}`,
);
return;
case 'event':
if (isCdpEvent(object)) {
BidiCdpSession.sessions
.get(object.params.session)
?.emit(object.params.event, object.params.params);
return;
}
// SAFETY: We know the method and parameter still match here.
this.emit(object.method, object.params);
return;
}
}
// Even if the response in not in BiDi protocol format but `id` is provided, reject
// the callback. This can happen if the endpoint supports CDP instead of BiDi.
if ('id' in object) {
this.#callbacks.reject(
(object as {id: number}).id,
`Protocol Error. Message is not in BiDi protocol format: '${message}'`,
object.message,
);
}
debugError(object);
}
/**
* Unbinds the connection, but keeps the transport open. Useful when the transport will
* be reused by other connection e.g. with different protocol.
* @internal
*/
unbind(): void {
if (this.#closed) {
return;
}
this.#closed = true;
// Both may still be invoked and produce errors
this.#transport.onmessage = () => {};
this.#transport.onclose = () => {};
this.#callbacks.clear();
}
/**
* Unbinds the connection and closes the transport.
*/
dispose(): void {
this.unbind();
this.#transport.close();
}
getPendingProtocolErrors(): Error[] {
return this.#callbacks.getPendingProtocolErrors();
}
}
/**
* @internal
*/
function createProtocolError(object: Bidi.ErrorResponse): string {
let message = `${object.error} ${object.message}`;
if (object.stacktrace) {
message += ` ${object.stacktrace}`;
}
return message;
}
function isCdpEvent(event: Bidi.Event | CdpEvent): event is CdpEvent {
return event.method.startsWith('goog:cdp.');
}

92
node_modules/puppeteer-core/src/bidi/Deserializer.ts generated vendored Normal file
View file

@ -0,0 +1,92 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type * as Bidi from 'webdriver-bidi-protocol';
import {debugError} from '../common/util.js';
/**
* @internal
*/
export class BidiDeserializer {
static deserialize(result: Bidi.Script.RemoteValue): any {
if (!result) {
debugError('Service did not produce a result.');
return undefined;
}
switch (result.type) {
case 'array':
return result.value?.map(value => {
return this.deserialize(value);
});
case 'set':
return result.value?.reduce((acc: Set<unknown>, value) => {
return acc.add(this.deserialize(value));
}, new Set());
case 'object':
return result.value?.reduce((acc: Record<any, unknown>, 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} = this.#deserializeTuple(tuple);
return acc.set(key, value);
}, new Map());
case 'promise':
return {};
case 'regexp':
return new RegExp(result.value.pattern, result.value.flags);
case 'date':
return new Date(result.value);
case 'undefined':
return undefined;
case 'null':
return null;
case 'number':
return this.#deserializeNumber(result.value);
case 'bigint':
return BigInt(result.value);
case 'boolean':
return Boolean(result.value);
case 'string':
return result.value;
}
debugError(`Deserialization of type ${result.type} not supported.`);
return undefined;
}
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
: this.deserialize(serializedKey);
const value = this.deserialize(serializedValue);
return {key, value};
}
}

32
node_modules/puppeteer-core/src/bidi/Dialog.ts generated vendored Normal file
View file

@ -0,0 +1,32 @@
/**
* @license
* Copyright 2017 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import {Dialog} from '../api/Dialog.js';
import type {UserPrompt} from './core/UserPrompt.js';
export class BidiDialog extends Dialog {
static from(prompt: UserPrompt): BidiDialog {
return new BidiDialog(prompt);
}
#prompt: UserPrompt;
private constructor(prompt: UserPrompt) {
super(prompt.info.type, prompt.info.message, prompt.info.defaultValue);
this.#prompt = prompt;
this.handled = prompt.handled;
}
override async handle(options: {
accept: boolean;
text?: string;
}): Promise<void> {
await this.#prompt.handle({
accept: options.accept,
userText: options.text,
});
}
}

152
node_modules/puppeteer-core/src/bidi/ElementHandle.ts generated vendored Normal file
View file

@ -0,0 +1,152 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type * as Bidi from 'webdriver-bidi-protocol';
import {
bindIsolatedHandle,
ElementHandle,
type AutofillData,
} from '../api/ElementHandle.js';
import {UnsupportedOperation} from '../common/Errors.js';
import type {AwaitableIterable} from '../common/types.js';
import {environment} from '../environment.js';
import {AsyncIterableUtil} from '../util/AsyncIterableUtil.js';
import {throwIfDisposed} from '../util/decorators.js';
import type {BidiFrame} from './Frame.js';
import {BidiJSHandle} from './JSHandle.js';
import type {BidiFrameRealm} from './Realm.js';
/**
* @internal
*/
export class BidiElementHandle<
ElementType extends Node = Element,
> extends ElementHandle<ElementType> {
#backendNodeId?: number;
static from<ElementType extends Node = Element>(
value: Bidi.Script.RemoteValue,
realm: BidiFrameRealm,
): BidiElementHandle<ElementType> {
return new BidiElementHandle(value, realm);
}
declare handle: BidiJSHandle<ElementType>;
constructor(value: Bidi.Script.RemoteValue, realm: BidiFrameRealm) {
super(BidiJSHandle.from(value, 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;
}
remoteValue(): Bidi.Script.RemoteValue {
return this.handle.remoteValue();
}
@throwIfDisposed()
override async autofill(data: AutofillData): Promise<void> {
const client = this.frame.client;
const nodeInfo = await client.send('DOM.describeNode', {
objectId: this.handle.id,
});
const fieldId = nodeInfo.node.backendNodeId;
const frameId = this.frame._id;
await client.send('Autofill.trigger', {
fieldId,
frameId,
card: data.creditCard,
});
}
override async contentFrame(
this: BidiElementHandle<HTMLIFrameElement>,
): Promise<BidiFrame>;
@throwIfDisposed()
@bindIsolatedHandle
override async contentFrame(): Promise<BidiFrame | null> {
using handle = (await this.evaluateHandle(element => {
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()
.frames()
.find(frame => {
return frame._id === value.value.context;
}) ?? null
);
}
return null;
}
override async uploadFile(
this: BidiElementHandle<HTMLInputElement>,
...files: string[]
): Promise<void> {
// Locate all files and confirm that they exist.
const path = environment.value.path;
if (path) {
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);
}
override async *queryAXTree(
this: BidiElementHandle<HTMLElement>,
name?: string | undefined,
role?: string | undefined,
): AwaitableIterable<ElementHandle<Node>> {
const results = await this.frame.locateNodes(this, {
type: 'accessibility',
value: {
role,
name,
},
});
return yield* AsyncIterableUtil.map(results, node => {
// TODO: maybe change ownership since the default ownership is probably none.
return Promise.resolve(BidiElementHandle.from(node, this.realm));
});
}
override async backendNodeId(): Promise<number> {
if (!this.frame.page().browser().cdpSupported) {
throw new UnsupportedOperation();
}
if (this.#backendNodeId) {
return this.#backendNodeId;
}
const {node} = await this.frame.client.send('DOM.describeNode', {
objectId: this.handle.id,
});
this.#backendNodeId = node.backendNodeId;
return this.#backendNodeId;
}
}

256
node_modules/puppeteer-core/src/bidi/ExposedFunction.ts generated vendored Normal file
View file

@ -0,0 +1,256 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import * as Bidi from 'webdriver-bidi-protocol';
import {EventEmitter} from '../common/EventEmitter.js';
import type {Awaitable, FlattenHandle} from '../common/types.js';
import {debugError} from '../common/util.js';
import {DisposableStack} from '../util/disposable.js';
import {interpolateFunction, stringifyFunction} from '../util/Function.js';
import type {Connection} from './core/Connection.js';
import {BidiElementHandle} from './ElementHandle.js';
import type {BidiFrame} from './Frame.js';
import {BidiJSHandle} from './JSHandle.js';
type CallbackChannel<Args, Ret> = (
value: [
resolve: (ret: FlattenHandle<Awaited<Ret>>) => void,
reject: (error: unknown) => void,
args: Args,
],
) => void;
/**
* @internal
*/
export class ExposableFunction<Args extends unknown[], Ret> {
static async from<Args extends unknown[], Ret>(
frame: BidiFrame,
name: string,
apply: (...args: Args) => Awaitable<Ret>,
isolate = false,
): Promise<ExposableFunction<Args, Ret>> {
const func = new ExposableFunction(frame, name, apply, isolate);
await func.#initialize();
return func;
}
readonly #frame;
readonly name;
readonly #apply;
readonly #isolate;
readonly #channel;
#scripts: Array<[BidiFrame, Bidi.Script.PreloadScript]> = [];
#disposables = new DisposableStack();
constructor(
frame: BidiFrame,
name: string,
apply: (...args: Args) => Awaitable<Ret>,
isolate = false,
) {
this.#frame = frame;
this.name = name;
this.#apply = apply;
this.#isolate = isolate;
this.#channel = `__puppeteer__${this.#frame._id}_page_exposeFunction_${this.name}`;
}
async #initialize() {
const connection = this.#connection;
const channel = {
type: 'channel' as const,
value: {
channel: this.#channel,
ownership: Bidi.Script.ResultOwnership.Root,
},
};
const connectionEmitter = this.#disposables.use(
new EventEmitter(connection),
);
connectionEmitter.on('script.message', this.#handleMessage);
const functionDeclaration = stringifyFunction(
interpolateFunction(
(callback: CallbackChannel<Args, Ret>) => {
Object.assign(globalThis, {
[PLACEHOLDER('name') as string]: function (...args: Args) {
return new Promise<FlattenHandle<Awaited<Ret>>>(
(resolve, reject) => {
callback([resolve, reject, args]);
},
);
},
});
},
{name: JSON.stringify(this.name)},
),
);
const frames = [this.#frame];
for (const frame of frames) {
frames.push(...frame.childFrames());
}
await Promise.all(
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);
}
}),
);
}
get #connection(): Connection {
return this.#frame.page().browser().connection;
}
#handleMessage = async (params: Bidi.Script.MessageParameters) => {
if (params.channel !== this.#channel) {
return;
}
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 stack = new DisposableStack();
const args = [];
let result;
try {
using argsHandle = await dataHandle.evaluateHandle(([, , args]) => {
return 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();
}
result = await this.#apply(...((await Promise.all(args)) as Args));
} catch (error) {
try {
if (error instanceof Error) {
await dataHandle.evaluate(
([, reject], name, message, stack) => {
const error = new Error(message);
error.name = name;
if (stack) {
error.stack = stack;
}
reject(error);
},
error.name,
error.message,
error.stack,
);
} else {
await dataHandle.evaluate(([, reject], error) => {
reject(error);
}, error);
}
} catch (error) {
debugError(error);
}
return;
}
try {
await dataHandle.evaluate(([resolve], result) => {
resolve(result);
}, result);
} catch (error) {
debugError(error);
}
};
#getRealm(source: Bidi.Script.Source) {
const frame = this.#findFrame(source.context as string);
if (!frame) {
// Unrelated message.
return;
}
return frame.realm(source.realm);
}
#findFrame(id: string) {
const frames = [this.#frame];
for (const frame of frames) {
if (frame._id === id) {
return frame;
}
frames.push(...frame.childFrames());
}
return;
}
[Symbol.dispose](): void {
void this[Symbol.asyncDispose]().catch(debugError);
}
async [Symbol.asyncDispose](): Promise<void> {
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.childFrames().map(childFrame => {
return childFrame.evaluate(name => {
delete (globalThis as any)[name];
}, this.name);
}),
frame.browsingContext.removePreloadScript(script),
]);
} catch (error) {
debugError(error);
}
}),
);
}
}

643
node_modules/puppeteer-core/src/bidi/Frame.ts generated vendored Normal file
View file

@ -0,0 +1,643 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import * as Bidi from 'webdriver-bidi-protocol';
import type {Observable} from '../../third_party/rxjs/rxjs.js';
import {
combineLatest,
defer,
delayWhen,
filter,
first,
firstValueFrom,
map,
of,
race,
raceWith,
switchMap,
} from '../../third_party/rxjs/rxjs.js';
import type {CDPSession} from '../api/CDPSession.js';
import {
Frame,
throwIfDetached,
type GoToOptions,
type WaitForOptions,
} from '../api/Frame.js';
import {PageEvent} from '../api/Page.js';
import {Accessibility} from '../cdp/Accessibility.js';
import type {ConsoleMessageType} from '../common/ConsoleMessage.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} from '../common/types.js';
import {
debugError,
fromAbortSignal,
fromEmitterEvent,
timeout,
} from '../common/util.js';
import {isErrorLike} from '../util/ErrorLike.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 {ExposableFunction} from './ExposedFunction.js';
import {BidiHTTPRequest, requests} from './HTTPRequest.js';
import type {BidiHTTPResponse} from './HTTPResponse.js';
import {BidiJSHandle} from './JSHandle.js';
import type {BidiPage} from './Page.js';
import type {BidiRealm} from './Realm.js';
import {BidiFrameRealm} from './Realm.js';
import {rewriteNavigationError} from './util.js';
import {BidiWebWorker} from './WebWorker.js';
// TODO: Remove this and map CDP the correct method.
// Requires breaking change.
function convertConsoleMessageLevel(method: string): ConsoleMessageType {
switch (method) {
case 'group':
return 'startGroup';
case 'groupCollapsed':
return 'startGroupCollapsed';
case 'groupEnd':
return 'endGroup';
default:
return method as ConsoleMessageType;
}
}
export class BidiFrame extends Frame {
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;
override readonly accessibility: Accessibility;
private constructor(
parent: BidiPage | BidiFrame,
browsingContext: BrowsingContext,
) {
super();
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,
),
};
this.accessibility = new Accessibility(this.realms.default, this._id);
}
#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) {
session.onClose();
}
}
this.page().trustedEmitter.emit(PageEvent.FrameDetached, this);
});
this.browsingContext.on('request', ({request}) => {
const httpRequest = BidiHTTPRequest.from(
request,
this,
this.page().isNetworkInterceptionEnabled,
);
request.once('success', () => {
this.page().trustedEmitter.emit(PageEvent.RequestFinished, httpRequest);
});
request.once('error', () => {
this.page().trustedEmitter.emit(PageEvent.RequestFailed, httpRequest);
});
void httpRequest.finalizeInterceptions();
});
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(
convertConsoleMessageLevel(entry.method),
text,
args,
getStackTraceLocations(entry.stackTrace),
this,
),
);
} 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(): BidiFrameRealm {
return this.realms.default;
}
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 {
let parent = this.#parent;
while (parent instanceof BidiFrame) {
parent = parent.#parent;
}
return parent;
}
override url(): string {
return this.browsingContext.url;
}
override parentFrame(): BidiFrame | null {
if (this.#parent instanceof BidiFrame) {
return this.#parent;
}
return null;
}
override childFrames(): BidiFrame[] {
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
override async goto(
url: string,
options: GoToOptions = {},
): Promise<BidiHTTPResponse | null> {
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)
.catch(error => {
if (
isErrorLike(error) &&
error.message.includes('net::ERR_HTTP_RESPONSE_CODE_FAILURE')
) {
return;
}
if (error.message.includes('navigation canceled')) {
return;
}
if (
error.message.includes(
'Navigation was aborted by another navigation',
)
) {
return;
}
throw error;
}),
]).catch(
rewriteNavigationError(
url,
options.timeout ?? this.timeoutSettings.navigationTimeout(),
),
);
return response;
}
@throwIfDetached
override async setContent(
html: string,
options: WaitForOptions = {},
): Promise<void> {
await Promise.all([
this.setFrameContent(html),
firstValueFrom(
combineLatest([
this.#waitForLoad$(options),
this.#waitForNetworkIdle$(options),
]),
),
]);
}
@throwIfDetached
override async waitForNavigation(
options: WaitForOptions = {},
): Promise<BidiHTTPResponse | null> {
const {timeout: ms = this.timeoutSettings.navigationTimeout(), signal} =
options;
const frames = this.childFrames().map(frame => {
return frame.#detached$();
});
return await firstValueFrom(
combineLatest([
race(
fromEmitterEvent(this.browsingContext, 'navigation'),
fromEmitterEvent(this.browsingContext, 'historyUpdated').pipe(
map(() => {
return {navigation: null};
}),
),
)
.pipe(first())
.pipe(
switchMap(({navigation}) => {
if (navigation === null) {
return of(null);
}
return this.#waitForLoad$(options).pipe(
delayWhen(() => {
if (frames.length === 0) {
return of(undefined);
}
return combineLatest(frames);
}),
raceWith(
fromEmitterEvent(navigation, 'fragment'),
fromEmitterEvent(navigation, 'failed'),
fromEmitterEvent(navigation, 'aborted'),
),
switchMap(() => {
if (navigation.request) {
function requestFinished$(
request: Request,
): Observable<Navigation | null> {
if (navigation === null) {
return of(null);
}
// 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]) => {
if (!navigation) {
return null;
}
const request = navigation.request;
if (!request) {
return null;
}
const lastRequest = request.lastRedirect ?? request;
const httpRequest = requests.get(lastRequest)!;
return httpRequest.response();
}),
raceWith(
timeout(ms),
fromAbortSignal(signal),
this.#detached$().pipe(
map(() => {
throw new TargetCloseError('Frame detached.');
}),
),
),
),
);
}
override waitForDevicePrompt(): never {
throw new UnsupportedOperation();
}
override get detached(): boolean {
return this.browsingContext.closed;
}
#exposedFunctions = new Map<string, ExposableFunction<never[], unknown>>();
async exposeFunction<Args extends unknown[], Ret>(
name: string,
apply: (...args: Args) => Awaitable<Ret>,
): Promise<void> {
if (this.#exposedFunctions.has(name)) {
throw new Error(
`Failed to add page binding with name ${name}: globalThis['${name}'] already exists!`,
);
}
const exposable = await ExposableFunction.from(this, name, apply);
this.#exposedFunctions.set(name, exposable);
}
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]();
}
async createCDPSession(): Promise<CDPSession> {
if (!this.page().browser().cdpSupported) {
throw new UnsupportedOperation();
}
const cdpConnection = this.page().browser().cdpConnection!;
return await cdpConnection._createSession({targetId: this._id});
}
@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,
);
}
@throwIfDetached
async locateNodes(
element: BidiElementHandle,
locator: Bidi.BrowsingContext.Locator,
): Promise<Bidi.Script.NodeRemoteValue[]> {
return await this.browsingContext.locateNodes(
locator,
// SAFETY: ElementHandles are always remote references.
[element.remoteValue() as Bidi.Script.SharedReference],
);
}
}
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;
}

384
node_modules/puppeteer-core/src/bidi/HTTPRequest.ts generated vendored Normal file
View file

@ -0,0 +1,384 @@
/**
* @license
* Copyright 2020 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {Protocol} from 'devtools-protocol';
import type * as Bidi from 'webdriver-bidi-protocol';
import type {CDPSession} from '../api/CDPSession.js';
import type {
ContinueRequestOverrides,
InterceptResolutionState,
ResponseForRequest,
} from '../api/HTTPRequest.js';
import {
HTTPRequest,
STATUS_TEXTS,
type ResourceType,
handleError,
InterceptResolutionAction,
} from '../api/HTTPRequest.js';
import {PageEvent} from '../api/Page.js';
import {UnsupportedOperation} from '../common/Errors.js';
import {stringToBase64} from '../util/encoding.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 {
static from(
bidiRequest: Request,
frame: BidiFrame,
isNetworkInterceptionEnabled: boolean,
redirect?: BidiHTTPRequest,
): BidiHTTPRequest {
const request = new BidiHTTPRequest(
bidiRequest,
frame,
isNetworkInterceptionEnabled,
redirect,
);
request.#initialize();
return request;
}
#redirectChain: BidiHTTPRequest[];
#response: BidiHTTPResponse | null = null;
override readonly id: string;
readonly #frame: BidiFrame;
readonly #request: Request;
private constructor(
request: Request,
frame: BidiFrame,
isNetworkInterceptionEnabled: boolean,
redirect?: BidiHTTPRequest,
) {
super();
requests.set(request, this);
this.interception.enabled = isNetworkInterceptionEnabled;
this.#request = request;
this.#frame = frame;
this.#redirectChain = redirect ? redirect.#redirectChain : [];
this.id = request.id;
}
override get client(): CDPSession {
return this.#frame.client;
}
#initialize() {
this.#request.on('redirect', request => {
const httpRequest = BidiHTTPRequest.from(
request,
this.#frame,
this.interception.enabled,
this,
);
this.#redirectChain.push(this);
request.once('success', () => {
this.#frame
.page()
.trustedEmitter.emit(PageEvent.RequestFinished, httpRequest);
});
request.once('error', () => {
this.#frame
.page()
.trustedEmitter.emit(PageEvent.RequestFailed, httpRequest);
});
void httpRequest.finalizeInterceptions();
});
this.#request.once('success', data => {
this.#response = BidiHTTPResponse.from(
data,
this,
this.#frame.page().browser().cdpSupported,
);
});
this.#request.on('authenticate', this.#handleAuthentication);
this.#frame.page().trustedEmitter.emit(PageEvent.Request, this);
if (this.#hasInternalHeaderOverwrite) {
this.interception.handlers.push(async () => {
await this.continue(
{
headers: this.headers(),
},
0,
);
});
}
}
protected canBeIntercepted(): boolean {
return this.#request.isBlocked;
}
override interceptResolutionState(): InterceptResolutionState {
if (!this.#request.isBlocked) {
return {action: InterceptResolutionAction.Disabled};
}
return super.interceptResolutionState();
}
override url(): string {
return this.#request.url;
}
override resourceType(): ResourceType {
if (!this.#frame.page().browser().cdpSupported) {
throw new UnsupportedOperation();
}
return (
this.#request.resourceType || 'other'
).toLowerCase() as ResourceType;
}
override method(): string {
return this.#request.method;
}
override postData(): string | undefined {
if (!this.#frame.page().browser().cdpSupported) {
throw new UnsupportedOperation();
}
return this.#request.postData;
}
override hasPostData(): boolean {
if (!this.#frame.page().browser().cdpSupported) {
throw new UnsupportedOperation();
}
return this.#request.hasPostData;
}
override async fetchPostData(): Promise<string | undefined> {
throw new UnsupportedOperation();
}
get #hasInternalHeaderOverwrite(): boolean {
return Boolean(
Object.keys(this.#extraHTTPHeaders).length ||
Object.keys(this.#userAgentHeaders).length,
);
}
get #extraHTTPHeaders(): Record<string, string> {
return this.#frame?.page()._extraHTTPHeaders ?? {};
}
get #userAgentHeaders(): Record<string, string> {
return this.#frame?.page()._userAgentHeaders ?? {};
}
override headers(): Record<string, string> {
const headers: Record<string, string> = {};
for (const header of this.#request.headers) {
headers[header.name.toLowerCase()] = header.value.value;
}
return {
...headers,
...this.#extraHTTPHeaders,
...this.#userAgentHeaders,
};
}
override response(): BidiHTTPResponse | null {
return this.#response;
}
override failure(): {errorText: string} | null {
if (this.#request.error === undefined) {
return null;
}
return {errorText: this.#request.error};
}
override isNavigationRequest(): boolean {
return this.#request.navigation !== undefined;
}
override initiator(): Protocol.Network.Initiator | undefined {
return {
...this.#request.initiator,
type: this.#request.initiator?.type ?? 'other',
};
}
override redirectChain(): BidiHTTPRequest[] {
return this.#redirectChain.slice();
}
override frame(): BidiFrame {
return this.#frame;
}
override async continue(
overrides?: ContinueRequestOverrides,
priority?: number | undefined,
): Promise<void> {
return await super.continue(
{
headers: this.#hasInternalHeaderOverwrite ? this.headers() : undefined,
...overrides,
},
priority,
);
}
override async _continue(
overrides: ContinueRequestOverrides = {},
): Promise<void> {
const headers: Bidi.Network.Header[] = getBidiHeaders(overrides.headers);
this.interception.handled = true;
return await this.#request
.continueRequest({
url: overrides.url,
method: overrides.method,
body: overrides.postData
? {
type: 'base64',
value: stringToBase64(overrides.postData),
}
: undefined,
headers: headers.length > 0 ? headers : undefined,
})
.catch(error => {
this.interception.handled = false;
return handleError(error);
});
}
override async _abort(): Promise<void> {
this.interception.handled = true;
return await this.#request.failRequest().catch(error => {
this.interception.handled = false;
throw error;
});
}
override async _respond(
response: Partial<ResponseForRequest>,
_priority?: number,
): Promise<void> {
this.interception.handled = true;
let parsedBody:
| {
contentLength: number;
base64: string;
}
| undefined;
if (response.body) {
parsedBody = HTTPRequest.getResponse(response.body);
}
const headers: Bidi.Network.Header[] = getBidiHeaders(response.headers);
const hasContentLength = headers.some(header => {
return header.name === 'content-length';
});
if (response.contentType) {
headers.push({
name: 'content-type',
value: {
type: 'string',
value: response.contentType,
},
});
}
if (parsedBody?.contentLength && !hasContentLength) {
headers.push({
name: 'content-length',
value: {
type: 'string',
value: String(parsedBody.contentLength),
},
});
}
const status = response.status || 200;
return await this.#request
.provideResponse({
statusCode: status,
headers: headers.length > 0 ? headers : undefined,
reasonPhrase: STATUS_TEXTS[status],
body: parsedBody?.base64
? {
type: 'base64',
value: parsedBody?.base64,
}
: undefined,
})
.catch(error => {
this.interception.handled = false;
throw error;
});
}
#authenticationHandled = false;
#handleAuthentication = async () => {
if (!this.#frame) {
return;
}
const credentials = this.#frame.page()._credentials;
if (credentials && !this.#authenticationHandled) {
this.#authenticationHandled = true;
void this.#request.continueWithAuth({
action: 'provideCredentials',
credentials: {
type: 'password',
username: credentials.username,
password: credentials.password,
},
});
} else {
void this.#request.continueWithAuth({
action: 'cancel',
});
}
};
timing(): Bidi.Network.FetchTimingInfo {
return this.#request.timing();
}
getResponseContent(): Promise<Uint8Array> {
return this.#request.getResponseContent();
}
}
function getBidiHeaders(rawHeaders?: Record<string, unknown>) {
const headers: Bidi.Network.Header[] = [];
for (const [name, value] of Object.entries(rawHeaders ?? [])) {
if (!Object.is(value, undefined)) {
const values = Array.isArray(value) ? value : [value];
for (const value of values) {
headers.push({
name: name.toLowerCase(),
value: {
type: 'string',
value: String(value),
},
});
}
}
}
return headers;
}

152
node_modules/puppeteer-core/src/bidi/HTTPResponse.ts generated vendored Normal file
View file

@ -0,0 +1,152 @@
/**
* @license
* Copyright 2020 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {Protocol} from 'devtools-protocol';
import type * as Bidi from 'webdriver-bidi-protocol';
import type {Frame} from '../api/Frame.js';
import {HTTPResponse, type RemoteAddress} from '../api/HTTPResponse.js';
import {PageEvent} from '../api/Page.js';
import {UnsupportedOperation} from '../common/Errors.js';
import {SecurityDetails} from '../common/SecurityDetails.js';
import {invokeAtMostOnceForArguments} from '../util/decorators.js';
import type {BidiHTTPRequest} from './HTTPRequest.js';
/**
* @internal
*/
export class BidiHTTPResponse extends HTTPResponse {
static from(
data: Bidi.Network.ResponseData,
request: BidiHTTPRequest,
cdpSupported: boolean,
): BidiHTTPResponse {
const response = new BidiHTTPResponse(data, request, cdpSupported);
response.#initialize();
return response;
}
#data: Bidi.Network.ResponseData;
#request: BidiHTTPRequest;
#securityDetails?: SecurityDetails;
#cdpSupported = false;
private constructor(
data: Bidi.Network.ResponseData,
request: BidiHTTPRequest,
cdpSupported: boolean,
) {
super();
this.#data = data;
this.#request = request;
this.#cdpSupported = cdpSupported;
// @ts-expect-error non-standard property.
const securityDetails = data['goog:securityDetails'];
if (cdpSupported && securityDetails) {
this.#securityDetails = new SecurityDetails(
securityDetails as Protocol.Network.SecurityDetails,
);
}
}
#initialize() {
if (this.#data.fromCache) {
this.#request._fromMemoryCache = true;
this.#request
.frame()
?.page()
.trustedEmitter.emit(PageEvent.RequestServedFromCache, this.#request);
}
this.#request.frame()?.page().trustedEmitter.emit(PageEvent.Response, this);
}
@invokeAtMostOnceForArguments
override remoteAddress(): RemoteAddress {
return {
ip: '',
port: -1,
};
}
override url(): string {
return this.#data.url;
}
override status(): number {
return this.#data.status;
}
override statusText(): string {
return this.#data.statusText;
}
override headers(): Record<string, string> {
const headers: Record<string, string> = {};
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 {
return this.#request;
}
override fromCache(): boolean {
return this.#data.fromCache;
}
override timing(): Protocol.Network.ResourceTiming | null {
const bidiTiming = this.#request.timing();
return {
requestTime: bidiTiming.requestTime,
proxyStart: -1,
proxyEnd: -1,
dnsStart: bidiTiming.dnsStart,
dnsEnd: bidiTiming.dnsEnd,
connectStart: bidiTiming.connectStart,
connectEnd: bidiTiming.connectEnd,
sslStart: bidiTiming.tlsStart,
sslEnd: -1,
workerStart: -1,
workerReady: -1,
workerFetchStart: -1,
workerRespondWithSettled: -1,
workerRouterEvaluationStart: -1,
workerCacheLookupStart: -1,
sendStart: bidiTiming.requestStart,
sendEnd: -1,
pushStart: -1,
pushEnd: -1,
receiveHeadersStart: bidiTiming.responseStart,
receiveHeadersEnd: bidiTiming.responseEnd,
};
}
override frame(): Frame | null {
return this.#request.frame();
}
override fromServiceWorker(): boolean {
return false;
}
override securityDetails(): SecurityDetails | null {
if (!this.#cdpSupported) {
throw new UnsupportedOperation();
}
return this.#securityDetails ?? null;
}
async content(): Promise<Uint8Array> {
return await this.#request.getResponseContent();
}
}

742
node_modules/puppeteer-core/src/bidi/Input.ts generated vendored Normal file
View file

@ -0,0 +1,742 @@
/**
* @license
* Copyright 2017 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import * as Bidi from 'webdriver-bidi-protocol';
import type {Point} from '../api/ElementHandle.js';
import {
Keyboard,
Mouse,
MouseButton,
Touchscreen,
type TouchHandle,
type KeyboardTypeOptions,
type KeyDownOptions,
type KeyPressOptions,
type MouseClickOptions,
type MouseMoveOptions,
type MouseOptions,
type MouseWheelOptions,
} from '../api/Input.js';
import {UnsupportedOperation} from '../common/Errors.js';
import {TouchError} from '../common/Errors.js';
import type {KeyInput} from '../common/USKeyboardLayout.js';
import type {BidiPage} from './Page.js';
const enum InputId {
Mouse = '__puppeteer_mouse',
Keyboard = '__puppeteer_keyboard',
Wheel = '__puppeteer_wheel',
Finger = '__puppeteer_finger',
}
enum SourceActionsType {
None = 'none',
Key = 'key',
Pointer = 'pointer',
Wheel = 'wheel',
}
enum ActionType {
Pause = 'pause',
KeyDown = 'keyDown',
KeyUp = 'keyUp',
PointerUp = 'pointerUp',
PointerDown = 'pointerDown',
PointerMove = 'pointerMove',
Scroll = 'scroll',
}
const getBidiKeyValue = (key: KeyInput) => {
switch (key) {
case '\r':
case '\n':
key = 'Enter';
break;
}
// Measures the number of code points rather than UTF-16 code units.
if ([...key].length === 1) {
return key;
}
switch (key) {
case 'Cancel':
return '\uE001';
case 'Help':
return '\uE002';
case 'Backspace':
return '\uE003';
case 'Tab':
return '\uE004';
case 'Clear':
return '\uE005';
case 'Enter':
return '\uE007';
case 'Shift':
case 'ShiftLeft':
return '\uE008';
case 'Control':
case 'ControlLeft':
return '\uE009';
case 'Alt':
case 'AltLeft':
return '\uE00A';
case 'Pause':
return '\uE00B';
case 'Escape':
return '\uE00C';
case 'PageUp':
return '\uE00E';
case 'PageDown':
return '\uE00F';
case 'End':
return '\uE010';
case 'Home':
return '\uE011';
case 'ArrowLeft':
return '\uE012';
case 'ArrowUp':
return '\uE013';
case 'ArrowRight':
return '\uE014';
case 'ArrowDown':
return '\uE015';
case 'Insert':
return '\uE016';
case 'Delete':
return '\uE017';
case 'NumpadEqual':
return '\uE019';
case 'Numpad0':
return '\uE01A';
case 'Numpad1':
return '\uE01B';
case 'Numpad2':
return '\uE01C';
case 'Numpad3':
return '\uE01D';
case 'Numpad4':
return '\uE01E';
case 'Numpad5':
return '\uE01F';
case 'Numpad6':
return '\uE020';
case 'Numpad7':
return '\uE021';
case 'Numpad8':
return '\uE022';
case 'Numpad9':
return '\uE023';
case 'NumpadMultiply':
return '\uE024';
case 'NumpadAdd':
return '\uE025';
case 'NumpadSubtract':
return '\uE027';
case 'NumpadDecimal':
return '\uE028';
case 'NumpadDivide':
return '\uE029';
case 'F1':
return '\uE031';
case 'F2':
return '\uE032';
case 'F3':
return '\uE033';
case 'F4':
return '\uE034';
case 'F5':
return '\uE035';
case 'F6':
return '\uE036';
case 'F7':
return '\uE037';
case 'F8':
return '\uE038';
case 'F9':
return '\uE039';
case 'F10':
return '\uE03A';
case 'F11':
return '\uE03B';
case 'F12':
return '\uE03C';
case 'Meta':
case 'MetaLeft':
return '\uE03D';
case 'ShiftRight':
return '\uE050';
case 'ControlRight':
return '\uE051';
case 'AltRight':
return '\uE052';
case 'MetaRight':
return '\uE053';
case 'Digit0':
return '0';
case 'Digit1':
return '1';
case 'Digit2':
return '2';
case 'Digit3':
return '3';
case 'Digit4':
return '4';
case 'Digit5':
return '5';
case 'Digit6':
return '6';
case 'Digit7':
return '7';
case 'Digit8':
return '8';
case 'Digit9':
return '9';
case 'KeyA':
return 'a';
case 'KeyB':
return 'b';
case 'KeyC':
return 'c';
case 'KeyD':
return 'd';
case 'KeyE':
return 'e';
case 'KeyF':
return 'f';
case 'KeyG':
return 'g';
case 'KeyH':
return 'h';
case 'KeyI':
return 'i';
case 'KeyJ':
return 'j';
case 'KeyK':
return 'k';
case 'KeyL':
return 'l';
case 'KeyM':
return 'm';
case 'KeyN':
return 'n';
case 'KeyO':
return 'o';
case 'KeyP':
return 'p';
case 'KeyQ':
return 'q';
case 'KeyR':
return 'r';
case 'KeyS':
return 's';
case 'KeyT':
return 't';
case 'KeyU':
return 'u';
case 'KeyV':
return 'v';
case 'KeyW':
return 'w';
case 'KeyX':
return 'x';
case 'KeyY':
return 'y';
case 'KeyZ':
return 'z';
case 'Semicolon':
return ';';
case 'Equal':
return '=';
case 'Comma':
return ',';
case 'Minus':
return '-';
case 'Period':
return '.';
case 'Slash':
return '/';
case 'Backquote':
return '`';
case 'BracketLeft':
return '[';
case 'Backslash':
return '\\';
case 'BracketRight':
return ']';
case 'Quote':
return '"';
default:
throw new Error(`Unknown key: "${key}"`);
}
};
/**
* @internal
*/
export class BidiKeyboard extends Keyboard {
#page: BidiPage;
constructor(page: BidiPage) {
super();
this.#page = page;
}
override async down(
key: KeyInput,
_options?: Readonly<KeyDownOptions>,
): Promise<void> {
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.mainFrame().browsingContext.performActions([
{
type: SourceActionsType.Key,
id: InputId.Keyboard,
actions: [
{
type: ActionType.KeyUp,
value: getBidiKeyValue(key),
},
],
},
]);
}
override async press(
key: KeyInput,
options: Readonly<KeyPressOptions> = {},
): Promise<void> {
const {delay = 0} = options;
const actions: Bidi.Input.KeySourceAction[] = [
{
type: ActionType.KeyDown,
value: getBidiKeyValue(key),
},
];
if (delay > 0) {
actions.push({
type: ActionType.Pause,
duration: delay,
});
}
actions.push({
type: ActionType.KeyUp,
value: getBidiKeyValue(key),
});
await this.#page.mainFrame().browsingContext.performActions([
{
type: SourceActionsType.Key,
id: InputId.Keyboard,
actions,
},
]);
}
override async type(
text: string,
options: Readonly<KeyboardTypeOptions> = {},
): Promise<void> {
const {delay = 0} = options;
// This spread separates the characters into code points rather than UTF-16
// code units.
const values = ([...text] as KeyInput[]).map(getBidiKeyValue);
const actions: Bidi.Input.KeySourceAction[] = [];
if (delay <= 0) {
for (const value of values) {
actions.push(
{
type: ActionType.KeyDown,
value,
},
{
type: ActionType.KeyUp,
value,
},
);
}
} else {
for (const value of values) {
actions.push(
{
type: ActionType.KeyDown,
value,
},
{
type: ActionType.Pause,
duration: delay,
},
{
type: ActionType.KeyUp,
value,
},
);
}
}
await this.#page.mainFrame().browsingContext.performActions([
{
type: SourceActionsType.Key,
id: InputId.Keyboard,
actions,
},
]);
}
override async sendCharacter(char: string): Promise<void> {
// Measures the number of code points rather than UTF-16 code units.
if ([...char].length > 1) {
throw new Error('Cannot send more than 1 character.');
}
const frame = await this.#page.focusedFrame();
await frame.isolatedRealm().evaluate(async char => {
document.execCommand('insertText', false, char);
}, char);
}
}
/**
* @internal
*/
export interface BidiMouseClickOptions extends MouseClickOptions {
origin?: Bidi.Input.Origin;
}
/**
* @internal
*/
export interface BidiMouseMoveOptions extends MouseMoveOptions {
origin?: Bidi.Input.Origin;
}
/**
* @internal
*/
export interface BidiTouchMoveOptions {
origin?: Bidi.Input.Origin;
}
const getBidiButton = (button: MouseButton) => {
switch (button) {
case MouseButton.Left:
return 0;
case MouseButton.Middle:
return 1;
case MouseButton.Right:
return 2;
case MouseButton.Back:
return 3;
case MouseButton.Forward:
return 4;
}
};
/**
* @internal
*/
export class BidiMouse extends Mouse {
#page: BidiPage;
#lastMovePoint: Point = {x: 0, y: 0};
constructor(page: BidiPage) {
super();
this.#page = page;
}
override async reset(): Promise<void> {
this.#lastMovePoint = {x: 0, y: 0};
await this.#page.mainFrame().browsingContext.releaseActions();
}
override async move(
x: number,
y: number,
options: Readonly<BidiMouseMoveOptions> = {},
): Promise<void> {
const from = this.#lastMovePoint;
const to = {
x: Math.round(x),
y: Math.round(y),
};
const actions: Bidi.Input.PointerSourceAction[] = [];
const steps = options.steps ?? 0;
for (let i = 0; i < steps; ++i) {
actions.push({
type: ActionType.PointerMove,
x: from.x + (to.x - from.x) * (i / steps),
y: from.y + (to.y - from.y) * (i / steps),
origin: options.origin,
});
}
actions.push({
type: ActionType.PointerMove,
...to,
origin: options.origin,
});
// 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.#page.mainFrame().browsingContext.performActions([
{
type: SourceActionsType.Pointer,
id: InputId.Mouse,
actions,
},
]);
}
override async down(options: Readonly<MouseOptions> = {}): Promise<void> {
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.#page.mainFrame().browsingContext.performActions([
{
type: SourceActionsType.Pointer,
id: InputId.Mouse,
actions: [
{
type: ActionType.PointerUp,
button: getBidiButton(options.button ?? MouseButton.Left),
},
],
},
]);
}
override async click(
x: number,
y: number,
options: Readonly<BidiMouseClickOptions> = {},
): Promise<void> {
const actions: Bidi.Input.PointerSourceAction[] = [
{
type: ActionType.PointerMove,
x: Math.round(x),
y: Math.round(y),
origin: options.origin,
},
];
const pointerDownAction = {
type: ActionType.PointerDown,
button: getBidiButton(options.button ?? MouseButton.Left),
} as const;
const pointerUpAction = {
type: ActionType.PointerUp,
button: pointerDownAction.button,
} as const;
for (let i = 1; i < (options.count ?? 1); ++i) {
actions.push(pointerDownAction, pointerUpAction);
}
actions.push(pointerDownAction);
if (options.delay) {
actions.push({
type: ActionType.Pause,
duration: options.delay,
});
}
actions.push(pointerUpAction);
await this.#page.mainFrame().browsingContext.performActions([
{
type: SourceActionsType.Pointer,
id: InputId.Mouse,
actions,
},
]);
}
override async wheel(
options: Readonly<MouseWheelOptions> = {},
): Promise<void> {
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 {
throw new UnsupportedOperation();
}
override dragOver(): never {
throw new UnsupportedOperation();
}
override dragEnter(): never {
throw new UnsupportedOperation();
}
override drop(): never {
throw new UnsupportedOperation();
}
override dragAndDrop(): never {
throw new UnsupportedOperation();
}
}
/**
* @internal
*/
class BidiTouchHandle implements TouchHandle {
#started = false;
#x: number;
#y: number;
#bidiId: string;
#page: BidiPage;
#touchScreen: BidiTouchscreen;
#properties: Bidi.Input.PointerCommonProperties;
constructor(
page: BidiPage,
touchScreen: BidiTouchscreen,
id: number,
x: number,
y: number,
properties: Bidi.Input.PointerCommonProperties,
) {
this.#page = page;
this.#touchScreen = touchScreen;
this.#x = Math.round(x);
this.#y = Math.round(y);
this.#properties = properties;
this.#bidiId = `${InputId.Finger}_${id}`;
}
async start(options: BidiTouchMoveOptions = {}): Promise<void> {
if (this.#started) {
throw new TouchError('Touch has already started');
}
await this.#page.mainFrame().browsingContext.performActions([
{
type: SourceActionsType.Pointer,
id: this.#bidiId,
parameters: {
pointerType: Bidi.Input.PointerType.Touch,
},
actions: [
{
type: ActionType.PointerMove,
x: this.#x,
y: this.#y,
origin: options.origin,
},
{
...this.#properties,
type: ActionType.PointerDown,
button: 0,
},
],
},
]);
this.#started = true;
}
move(x: number, y: number): Promise<void> {
const newX = Math.round(x);
const newY = Math.round(y);
return this.#page.mainFrame().browsingContext.performActions([
{
type: SourceActionsType.Pointer,
id: this.#bidiId,
parameters: {
pointerType: Bidi.Input.PointerType.Touch,
},
actions: [
{
...this.#properties,
type: ActionType.PointerMove,
x: newX,
y: newY,
},
],
},
]);
}
async end(): Promise<void> {
await this.#page.mainFrame().browsingContext.performActions([
{
type: SourceActionsType.Pointer,
id: this.#bidiId,
parameters: {
pointerType: Bidi.Input.PointerType.Touch,
},
actions: [
{
type: ActionType.PointerUp,
button: 0,
},
],
},
]);
this.#touchScreen.removeHandle(this);
}
}
/**
* @internal
*/
export class BidiTouchscreen extends Touchscreen {
#page: BidiPage;
declare touches: BidiTouchHandle[];
constructor(page: BidiPage) {
super();
this.#page = page;
}
override async touchStart(
x: number,
y: number,
options: BidiTouchMoveOptions = {},
): Promise<TouchHandle> {
const id = this.idGenerator();
const properties: Bidi.Input.PointerCommonProperties = {
width: 0.5 * 2, // 2 times default touch radius.
height: 0.5 * 2, // 2 times default touch radius.
pressure: 0.5,
altitudeAngle: Math.PI / 2,
};
const touch = new BidiTouchHandle(this.#page, this, id, x, y, properties);
await touch.start(options);
this.touches.push(touch);
return touch;
}
}

95
node_modules/puppeteer-core/src/bidi/JSHandle.ts generated vendored Normal file
View file

@ -0,0 +1,95 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type * as Bidi from 'webdriver-bidi-protocol';
import type {ElementHandle} from '../api/ElementHandle.js';
import {JSHandle} from '../api/JSHandle.js';
import {UnsupportedOperation} from '../common/Errors.js';
import {BidiDeserializer} from './Deserializer.js';
import type {BidiRealm} from './Realm.js';
/**
* @internal
*/
export class BidiJSHandle<T = unknown> extends JSHandle<T> {
static from<T>(
value: Bidi.Script.RemoteValue,
realm: BidiRealm,
): BidiJSHandle<T> {
return new BidiJSHandle(value, realm);
}
readonly #remoteValue: Bidi.Script.RemoteValue;
override readonly realm: BidiRealm;
#disposed = false;
constructor(value: Bidi.Script.RemoteValue, realm: BidiRealm) {
super();
this.#remoteValue = value;
this.realm = realm;
}
override get disposed(): boolean {
return this.#disposed;
}
override async jsonValue(): Promise<T> {
return await this.evaluate(value => {
return value;
});
}
override asElement(): ElementHandle<Node> | null {
return null;
}
override async dispose(): Promise<void> {
if (this.#disposed) {
return;
}
this.#disposed = true;
await this.realm.destroyHandles([this]);
}
get isPrimitiveValue(): boolean {
switch (this.#remoteValue.type) {
case 'string':
case 'number':
case 'bigint':
case 'boolean':
case 'undefined':
case 'null':
return true;
default:
return false;
}
}
override toString(): string {
if (this.isPrimitiveValue) {
return 'JSHandle:' + BidiDeserializer.deserialize(this.#remoteValue);
}
return 'JSHandle@' + this.#remoteValue.type;
}
override get id(): string | undefined {
return 'handle' in this.#remoteValue ? this.#remoteValue.handle : undefined;
}
remoteValue(): Bidi.Script.RemoteValue {
return this.#remoteValue;
}
override remoteObject(): never {
throw new UnsupportedOperation('Not available in WebDriver BiDi');
}
}

1181
node_modules/puppeteer-core/src/bidi/Page.ts generated vendored Normal file

File diff suppressed because it is too large Load diff

410
node_modules/puppeteer-core/src/bidi/Realm.ts generated vendored Normal file
View file

@ -0,0 +1,410 @@
/**
* @license
* Copyright 2024 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import * as Bidi from 'webdriver-bidi-protocol';
import type {JSHandle} from '../api/JSHandle.js';
import {Realm} from '../api/Realm.js';
import {ARIAQueryHandler} from '../common/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 {
debugError,
getSourcePuppeteerURLIfAvailable,
getSourceUrlComment,
isString,
PuppeteerURL,
SOURCE_URL_REGEX,
} from '../common/util.js';
import type {PuppeteerInjectedUtil} from '../injected/injected.js';
import {AsyncIterableUtil} from '../util/AsyncIterableUtil.js';
import {stringifyFunction} from '../util/Function.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 {ExposableFunction} from './ExposedFunction.js';
import type {BidiFrame} from './Frame.js';
import {BidiJSHandle} from './JSHandle.js';
import {BidiSerializer} from './Serializer.js';
import {createEvaluationError} from './util.js';
import type {BidiWebWorker} from './WebWorker.js';
/**
* @internal
*/
export abstract class BidiRealm extends Realm {
readonly realm: BidiRealmCore;
constructor(realm: BidiRealmCore, timeoutSettings: TimeoutSettings) {
super(timeoutSettings);
this.realm = realm;
}
protected initialize(): void {
this.realm.on('destroyed', ({reason}) => {
this.taskManager.terminateAll(new Error(reason));
this.dispose();
});
this.realm.on('updated', () => {
this.internalPuppeteerUtil = undefined;
void this.taskManager.rerunAll();
});
}
protected internalPuppeteerUtil?: Promise<
BidiJSHandle<PuppeteerInjectedUtil>
>;
get puppeteerUtil(): Promise<BidiJSHandle<PuppeteerInjectedUtil>> {
const promise = Promise.resolve() as Promise<unknown>;
scriptInjector.inject(script => {
if (this.internalPuppeteerUtil) {
void this.internalPuppeteerUtil.then(handle => {
void handle.dispose();
});
}
this.internalPuppeteerUtil = promise.then(() => {
return this.evaluateHandle(script) as Promise<
BidiJSHandle<PuppeteerInjectedUtil>
>;
});
}, !this.internalPuppeteerUtil);
return this.internalPuppeteerUtil as Promise<
BidiJSHandle<PuppeteerInjectedUtil>
>;
}
override async evaluateHandle<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(
pageFunction: Func | string,
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
return await this.#evaluate(false, pageFunction, ...args);
}
override async evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(
pageFunction: Func | string,
...args: Params
): Promise<Awaited<ReturnType<Func>>> {
return await this.#evaluate(true, pageFunction, ...args);
}
async #evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(
returnByValue: true,
pageFunction: Func | string,
...args: Params
): Promise<Awaited<ReturnType<Func>>>;
async #evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(
returnByValue: false,
pageFunction: Func | string,
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>>;
async #evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(
returnByValue: boolean,
pageFunction: Func | string,
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>> | Awaited<ReturnType<Func>>> {
const sourceUrlComment = getSourceUrlComment(
getSourcePuppeteerURLIfAvailable(pageFunction)?.toString() ??
PuppeteerURL.INTERNAL_URL,
);
let responsePromise;
const resultOwnership = returnByValue
? Bidi.Script.ResultOwnership.None
: Bidi.Script.ResultOwnership.Root;
const serializationOptions: Bidi.Script.SerializationOptions = returnByValue
? {}
: {
maxObjectDepth: 0,
maxDomDepth: 0,
};
if (isString(pageFunction)) {
const expression = SOURCE_URL_REGEX.test(pageFunction)
? pageFunction
: `${pageFunction}\n${sourceUrlComment}\n`;
responsePromise = this.realm.evaluate(expression, true, {
resultOwnership,
userActivation: true,
serializationOptions,
});
} else {
let functionDeclaration = stringifyFunction(pageFunction);
functionDeclaration = SOURCE_URL_REGEX.test(functionDeclaration)
? functionDeclaration
: `${functionDeclaration}\n${sourceUrlComment}\n`;
responsePromise = this.realm.callFunction(
functionDeclaration,
/* awaitPromise= */ true,
{
// LazyArgs are used only internally and should not affect the order
// evaluate calls for the public APIs.
arguments: args.some(arg => {
return arg instanceof LazyArg;
})
? await Promise.all(
args.map(arg => {
return this.serializeAsync(arg);
}),
)
: args.map(arg => {
return this.serialize(arg);
}),
resultOwnership,
userActivation: true,
serializationOptions,
},
);
}
const result = await responsePromise;
if ('type' in result && result.type === 'exception') {
throw createEvaluationError(result.exceptionDetails);
}
if (returnByValue) {
return BidiDeserializer.deserialize(result.result);
}
return this.createHandle(result.result) as unknown as HandleFor<
Awaited<ReturnType<Func>>
>;
}
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 serializeAsync(arg: unknown): Promise<Bidi.Script.LocalValue> {
if (arg instanceof LazyArg) {
arg = await arg.get(this);
}
return this.serialize(arg);
}
serialize(arg: unknown): Bidi.Script.LocalValue {
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> {
if (this.disposed) {
return;
}
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<PuppeteerInjectedUtil>> {
let promise = Promise.resolve() as Promise<unknown>;
if (!this.#bindingsInstalled) {
promise = Promise.all([
ExposableFunction.from(
this.environment,
'__ariaQuerySelector',
ARIAQueryHandler.queryOne,
!!this.sandbox,
),
ExposableFunction.from(
this.environment,
'__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 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.');
}
}

126
node_modules/puppeteer-core/src/bidi/Serializer.ts generated vendored Normal file
View file

@ -0,0 +1,126 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type * as Bidi from 'webdriver-bidi-protocol';
import {isDate, isPlainObject, isRegExp} from '../common/util.js';
/**
* @internal
*/
class UnserializableError extends Error {}
/**
* @internal
*/
export class BidiSerializer {
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';
} else if (Object.is(arg, Infinity)) {
value = 'Infinity';
} else if (Object.is(arg, -Infinity)) {
value = '-Infinity';
} else if (Object.is(arg, NaN)) {
value = 'NaN';
} else {
value = arg;
}
return {
type: 'number',
value,
};
}
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 this.serialize(subArg);
});
return {
type: 'array',
value: parsedArray,
};
} else if (isPlainObject(arg)) {
try {
JSON.stringify(arg);
} catch (error) {
if (
error instanceof TypeError &&
error.message.startsWith('Converting circular structure to JSON')
) {
error.message += ' Recursive objects are not allowed.';
}
throw error;
}
const parsedObject: Bidi.Script.MappingLocalValue = [];
for (const key in arg) {
parsedObject.push([this.serialize(key), this.serialize(arg[key])]);
}
return {
type: 'object',
value: parsedObject,
};
} else if (isRegExp(arg)) {
return {
type: 'regexp',
value: {
pattern: arg.source,
flags: arg.flags,
},
};
} else if (isDate(arg)) {
return {
type: 'date',
value: arg.toISOString(),
};
}
throw new UnserializableError(
'Custom object serialization not possible. Use plain objects instead.',
);
}
}

170
node_modules/puppeteer-core/src/bidi/Target.ts generated vendored Normal file
View file

@ -0,0 +1,170 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
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 {BidiFrame} from './Frame.js';
import {BidiPage} from './Page.js';
import type {BidiWebWorker} from './WebWorker.js';
/**
* @internal
*/
export class BidiBrowserTarget extends Target {
#browser: BidiBrowser;
constructor(browser: BidiBrowser) {
super();
this.#browser = browser;
}
override asPage(): Promise<BidiPage> {
throw new UnsupportedOperation();
}
override url(): string {
return '';
}
override createCDPSession(): Promise<CDPSession> {
throw new UnsupportedOperation();
}
override type(): TargetType {
return TargetType.BROWSER;
}
override browser(): BidiBrowser {
return this.#browser;
}
override browserContext(): BidiBrowserContext {
return this.#browser.defaultBrowserContext();
}
override opener(): Target | undefined {
throw new UnsupportedOperation();
}
}
/**
* @internal
*/
export class BidiPageTarget extends Target {
#page: BidiPage;
constructor(page: BidiPage) {
super();
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 this.#page.url();
}
override createCDPSession(): Promise<CDPSession> {
return this.#page.createCDPSession();
}
override type(): TargetType {
return TargetType.PAGE;
}
override browser(): BidiBrowser {
return this.browserContext().browser();
}
override browserContext(): BidiBrowserContext {
return this.#page.browserContext();
}
override opener(): Target | undefined {
throw new UnsupportedOperation();
}
}
/**
* @internal
*/
export class BidiFrameTarget extends Target {
#frame: BidiFrame;
#page: BidiPage | undefined;
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.#frame.url();
}
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 BidiWorkerTarget extends Target {
#worker: BidiWebWorker;
constructor(worker: BidiWebWorker) {
super();
this.#worker = worker;
}
override async page(): Promise<BidiPage> {
throw new UnsupportedOperation();
}
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();
}
}

48
node_modules/puppeteer-core/src/bidi/WebWorker.ts generated vendored Normal file
View file

@ -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();
}
}

18
node_modules/puppeteer-core/src/bidi/bidi.ts generated vendored Normal file
View file

@ -0,0 +1,18 @@
/**
* @license
* Copyright 2022 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
export * from './BidiOverCdp.js';
export * from './Browser.js';
export * from './BrowserContext.js';
export * from './Connection.js';
export * from './ElementHandle.js';
export * from './Frame.js';
export * from './HTTPRequest.js';
export * from './HTTPResponse.js';
export * from './Input.js';
export * from './JSHandle.js';
export * from './Page.js';
export * from './Realm.js';

300
node_modules/puppeteer-core/src/bidi/core/Browser.ts generated vendored Normal file
View file

@ -0,0 +1,300 @@
/**
* @license
* Copyright 2024 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type * as Bidi from 'webdriver-bidi-protocol';
import type {BrowserContextOptions} from '../../api/Browser.js';
import {UnsupportedOperation} from '../../common/Errors.js';
import {EventEmitter} from '../../common/EventEmitter.js';
import {inertIfDisposed, throwIfDisposed} from '../../util/decorators.js';
import {DisposableStack, disposeSymbol} from '../../util/disposable.js';
import type {BrowsingContext} from './BrowsingContext.js';
import {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 before 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;
}
#closed = false;
#reason: string | undefined;
readonly #disposables = new DisposableStack();
readonly #userContexts = new Map<string, UserContext>();
readonly session: Session;
readonly #sharedWorkers = new Map<string, SharedWorkerRealm>();
private constructor(session: Session) {
super();
this.session = session;
}
async #initialize() {
const sessionEmitter = this.#disposables.use(
new EventEmitter(this.session),
);
sessionEmitter.once('ended', ({reason}) => {
this.dispose(reason);
});
sessionEmitter.on('script.realmCreated', info => {
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.
const contextIds = new Set<string>();
let contexts: Bidi.BrowsingContext.Info[];
{
using sessionEmitter = new EventEmitter(this.session);
sessionEmitter.on('browsingContext.contextCreated', info => {
contextIds.add(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)) {
this.session.emit('browsingContext.contextCreated', info);
}
if (info.children) {
contexts.push(...info.children);
}
}
}
#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;
}
get closed(): boolean {
return this.#closed;
}
get defaultUserContext(): UserContext {
// SAFETY: A UserContext is always created for the default context.
return this.#userContexts.get(UserContext.DEFAULT)!;
}
get disconnected(): boolean {
return this.#reason !== undefined;
}
get disposed(): boolean {
return this.disconnected;
}
get userContexts(): Iterable<UserContext> {
return this.#userContexts.values();
}
@inertIfDisposed
dispose(reason?: string, closed = false): void {
this.#closed = closed;
this.#reason = reason;
this[disposeSymbol]();
}
@throwIfDisposed<Browser>(browser => {
// SAFETY: By definition of `disposed`, `#reason` is defined.
return browser.#reason!;
})
async close(): Promise<void> {
try {
await this.session.send('browser.close', {});
} finally {
this.dispose('Browser already closed.', true);
}
}
@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 removeIntercept(intercept: Bidi.Network.Intercept): Promise<void> {
await this.session.send('network.removeIntercept', {
intercept,
});
}
@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,
});
}
@throwIfDisposed<Browser>(browser => {
// SAFETY: By definition of `disposed`, `#reason` is defined.
return browser.#reason!;
})
async createUserContext(
options: BrowserContextOptions,
): Promise<UserContext> {
const proxyConfig: Bidi.Session.ProxyConfiguration | undefined =
options.proxyServer === undefined
? undefined
: {
proxyType: 'manual',
httpProxy: options.proxyServer,
sslProxy: options.proxyServer,
noProxy: options.proxyBypassList,
};
const {
result: {userContext},
} = await this.session.send('browser.createUserContext', {
proxy: proxyConfig,
});
if (options.downloadBehavior?.policy === 'allowAndName') {
throw new UnsupportedOperation(
'`allowAndName` is not supported in WebDriver BiDi',
);
}
if (options.downloadBehavior?.policy === 'allow') {
if (options.downloadBehavior.downloadPath === undefined) {
throw new UnsupportedOperation(
'`downloadPath` is required in `allow` download behavior',
);
}
await this.session.send('browser.setDownloadBehavior', {
downloadBehavior: {
type: 'allowed',
destinationFolder: options.downloadBehavior.downloadPath,
},
userContexts: [userContext],
});
}
if (options.downloadBehavior?.policy === 'deny') {
await this.session.send('browser.setDownloadBehavior', {
downloadBehavior: {type: 'denied'},
userContexts: [userContext],
});
}
return this.#createUserContext(userContext);
}
@throwIfDisposed<Browser>(browser => {
// SAFETY: By definition of `disposed`, `#reason` is defined.
return browser.#reason!;
})
async installExtension(path: string): Promise<string> {
const {
result: {extension},
} = await this.session.send('webExtension.install', {
extensionData: {type: 'path', path},
});
return extension;
}
@throwIfDisposed<Browser>(browser => {
// SAFETY: By definition of `disposed`, `#reason` is defined.
return browser.#reason!;
})
async uninstallExtension(id: string): Promise<void> {
await this.session.send('webExtension.uninstall', {extension: id});
}
override [disposeSymbol](): void {
this.#reason ??=
'Browser was disconnected, probably because the session ended.';
if (this.closed) {
this.emit('closed', {reason: this.#reason});
}
this.emit('disconnected', {reason: this.#reason});
this.#disposables.dispose();
super[disposeSymbol]();
}
}

View file

@ -0,0 +1,729 @@
/**
* @license
* Copyright 2024 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type * as Bidi from 'webdriver-bidi-protocol';
import {EventEmitter} from '../../common/EventEmitter.js';
import {inertIfDisposed, throwIfDisposed} from '../../util/decorators.js';
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';
import {UserPrompt} from './UserPrompt.js';
/**
* @internal
*/
export type AddInterceptOptions = Omit<
Bidi.Network.AddInterceptParameters,
'contexts'
>;
/**
* @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 type GetCookiesOptions = Omit<
Bidi.Storage.GetCookiesParameters,
'partition'
>;
/**
* @internal
*/
export type SetGeoLocationOverrideOptions =
Bidi.Emulation.SetGeolocationOverrideParameters;
/**
* @internal
*/
export class BrowsingContext extends EventEmitter<{
/** Emitted when this context is closed. */
closed: {
/** The reason the browsing context was closed */
reason: string;
};
/** 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 file dialog is opened occurs. */
filedialogopened: Bidi.Input.FileDialogInfo;
/** 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 history is updated. */
historyUpdated: void;
/** Emitted whenever the frame emits `DOMContentLoaded` */
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,
parent: BrowsingContext | undefined,
id: string,
url: string,
originalOpener: string | null,
): BrowsingContext {
const browsingContext = new BrowsingContext(
userContext,
parent,
id,
url,
originalOpener,
);
browsingContext.#initialize();
return browsingContext;
}
#navigation: Navigation | undefined;
#reason?: string;
#url: string;
readonly #children = new Map<string, BrowsingContext>();
readonly #disposables = new DisposableStack();
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;
readonly originalOpener: string | null;
readonly #emulationState: {
javaScriptEnabled: boolean;
} = {javaScriptEnabled: true};
private constructor(
context: UserContext,
parent: BrowsingContext | undefined,
id: string,
url: string,
originalOpener: string | null,
) {
super();
this.#url = url;
this.id = id;
this.parent = parent;
this.userContext = context;
this.originalOpener = originalOpener;
this.defaultRealm = this.#createWindowRealm();
}
#initialize() {
const userContextEmitter = this.#disposables.use(
new EventEmitter(this.userContext),
);
userContextEmitter.once('closed', ({reason}) => {
this.dispose(`Browsing context already closed: ${reason}`);
});
const sessionEmitter = this.#disposables.use(
new EventEmitter(this.#session),
);
sessionEmitter.on('input.fileDialogOpened', info => {
if (this.id !== info.context) {
return;
}
this.emit('filedialogopened', info);
});
sessionEmitter.on('browsingContext.contextCreated', info => {
if (info.parent !== this.id) {
return;
}
const browsingContext = BrowsingContext.from(
this.userContext,
this,
info.context,
info.url,
info.originalOpener,
);
this.#children.set(info.context, browsingContext);
const browsingContextEmitter = this.#disposables.use(
new EventEmitter(browsingContext),
);
browsingContextEmitter.once('closed', () => {
browsingContextEmitter.removeAllListeners();
this.#children.delete(browsingContext.id);
});
this.emit('browsingcontext', {browsingContext});
});
sessionEmitter.on('browsingContext.contextDestroyed', info => {
if (info.context !== this.id) {
return;
}
this.dispose('Browsing context already closed.');
});
sessionEmitter.on('browsingContext.historyUpdated', info => {
if (info.context !== this.id) {
return;
}
this.#url = info.url;
this.emit('historyUpdated', undefined);
});
sessionEmitter.on('browsingContext.domContentLoaded', info => {
if (info.context !== this.id) {
return;
}
this.#url = info.url;
this.emit('DOMContentLoaded', undefined);
});
sessionEmitter.on('browsingContext.load', info => {
if (info.context !== this.id) {
return;
}
this.#url = info.url;
this.emit('load', undefined);
});
sessionEmitter.on('browsingContext.navigationStarted', info => {
if (info.context !== this.id) {
return;
}
// Note: we should not update this.#url at this point since the context
// has not finished navigating to the info.url yet.
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);
const navigationEmitter = this.#disposables.use(
new EventEmitter(this.#navigation),
);
for (const eventName of ['fragment', 'failed', 'aborted'] as const) {
navigationEmitter.once(eventName, ({url}) => {
navigationEmitter[disposeSymbol]();
this.#url = url;
});
}
this.emit('navigation', {navigation: this.#navigation});
});
sessionEmitter.on('network.beforeRequestSent', event => {
if (event.context !== this.id) {
return;
}
if (this.#requests.has(event.request.request)) {
// Means the request is a redirect. This is handled in Request.
// Or an Auth event was issued
return;
}
const request = Request.from(this, event);
this.#requests.set(request.id, request);
this.emit('request', {request});
});
sessionEmitter.on('log.entryAdded', entry => {
if (entry.source.context !== this.id) {
return;
}
this.emit('log', {entry});
});
sessionEmitter.on('browsingContext.userPromptOpened', info => {
if (info.context !== this.id) {
return;
}
const userPrompt = UserPrompt.from(this, info);
this.emit('userprompt', {userPrompt});
});
}
get #session() {
return this.userContext.browser.session;
}
get children(): Iterable<BrowsingContext> {
return this.#children.values();
}
get closed(): boolean {
return this.#reason !== undefined;
}
get disposed(): boolean {
return this.closed;
}
get realms(): Iterable<WindowRealm> {
// 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;
for (let {parent} = context; parent; {parent} = context) {
context = parent;
}
return context;
}
get url(): string {
return this.#url;
}
#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;
for (const context of this.#children.values()) {
context.dispose('Parent browsing context was disposed');
}
this[disposeSymbol]();
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async activate(): Promise<void> {
await this.#session.send('browsingContext.activate', {
context: this.id,
});
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async captureScreenshot(
options: CaptureScreenshotOptions = {},
): Promise<string> {
const {
result: {data},
} = await this.#session.send('browsingContext.captureScreenshot', {
context: this.id,
...options,
});
return data;
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
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<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async traverseHistory(delta: number): Promise<void> {
await this.#session.send('browsingContext.traverseHistory', {
context: this.id,
delta,
});
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async navigate(
url: string,
wait?: Bidi.BrowsingContext.ReadinessState,
): Promise<void> {
await this.#session.send('browsingContext.navigate', {
context: this.id,
url,
wait,
});
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async reload(options: ReloadOptions = {}): Promise<void> {
await this.#session.send('browsingContext.reload', {
context: this.id,
...options,
});
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async setCacheBehavior(cacheBehavior: 'default' | 'bypass'): Promise<void> {
await this.#session.send('network.setCacheBehavior', {
contexts: [this.id],
cacheBehavior,
});
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async print(options: PrintOptions = {}): Promise<string> {
const {
result: {data},
} = await this.#session.send('browsingContext.print', {
context: this.id,
...options,
});
return data;
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async handleUserPrompt(options: HandleUserPromptOptions = {}): Promise<void> {
await this.#session.send('browsingContext.handleUserPrompt', {
context: this.id,
...options,
});
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async setViewport(options: SetViewportOptions = {}): Promise<void> {
await this.#session.send('browsingContext.setViewport', {
context: this.id,
...options,
});
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async performActions(actions: Bidi.Input.SourceActions[]): Promise<void> {
await this.#session.send('input.performActions', {
context: this.id,
actions,
});
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async releaseActions(): Promise<void> {
await this.#session.send('input.releaseActions', {
context: this.id,
});
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
createWindowRealm(sandbox: string): WindowRealm {
return this.#createWindowRealm(sandbox);
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async addPreloadScript(
functionDeclaration: string,
options: AddPreloadScriptOptions = {},
): Promise<string> {
return await this.userContext.browser.addPreloadScript(
functionDeclaration,
{
...options,
contexts: [this],
},
);
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async addIntercept(options: AddInterceptOptions): Promise<string> {
const {
result: {intercept},
} = await this.userContext.browser.session.send('network.addIntercept', {
...options,
contexts: [this.id],
});
return intercept;
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async removePreloadScript(script: string): Promise<void> {
await this.userContext.browser.removePreloadScript(script);
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async setGeolocationOverride(
options: SetGeoLocationOverrideOptions,
): Promise<void> {
if (!('coordinates' in options)) {
throw new Error('Missing coordinates');
}
await this.userContext.browser.session.send(
'emulation.setGeolocationOverride',
{
coordinates: options.coordinates,
contexts: [this.id],
},
);
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async setTimezoneOverride(timezoneId?: string): Promise<void> {
if (timezoneId?.startsWith('GMT')) {
// CDP requires `GMT` prefix before timezone offset, while BiDi does not. Remove the
// `GMT` for interop between CDP and BiDi.
timezoneId = timezoneId?.replace('GMT', '');
}
await this.userContext.browser.session.send(
'emulation.setTimezoneOverride',
{
timezone: timezoneId ?? null,
contexts: [this.id],
},
);
}
@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,
});
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async subscribe(events: [string, ...string[]]): Promise<void> {
await this.#session.subscribe(events, [this.id]);
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async addInterception(events: [string, ...string[]]): Promise<void> {
await this.#session.subscribe(events, [this.id]);
}
override [disposeSymbol](): void {
this.#reason ??=
'Browsing context already closed, probably because the user context closed.';
this.emit('closed', {reason: this.#reason});
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,
},
});
}),
);
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async locateNodes(
locator: Bidi.BrowsingContext.Locator,
startNodes: [Bidi.Script.SharedReference, ...Bidi.Script.SharedReference[]],
): Promise<Bidi.Script.NodeRemoteValue[]> {
// TODO: add other locateNodes options if needed.
const result = await this.#session.send('browsingContext.locateNodes', {
context: this.id,
locator,
startNodes: startNodes.length ? startNodes : undefined,
});
return result.result.nodes;
}
async setJavaScriptEnabled(enabled: boolean): Promise<void> {
await this.userContext.browser.session.send(
'emulation.setScriptingEnabled',
{
// Enabled `null` means `default`, `false` means `disabled`.
enabled: enabled ? null : false,
contexts: [this.id],
},
);
this.#emulationState.javaScriptEnabled = enabled;
}
isJavaScriptEnabled(): boolean {
return this.#emulationState.javaScriptEnabled;
}
}

View file

@ -0,0 +1,30 @@
/**
* @license
* Copyright 2024 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {Event} from 'webdriver-bidi-protocol';
import type {Commands} from 'webdriver-bidi-protocol';
import type {EventEmitter} from '../../common/EventEmitter.js';
export type {Commands};
/**
* @internal
*/
export type BidiEvents = {
[K in Event['method']]: Extract<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']}>;
}

173
node_modules/puppeteer-core/src/bidi/core/Navigation.ts generated vendored Normal file
View file

@ -0,0 +1,173 @@
/**
* @license
* Copyright 2024 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import {EventEmitter} from '../../common/EventEmitter.js';
import {inertIfDisposed} from '../../util/decorators.js';
import {DisposableStack, disposeSymbol} from '../../util/disposable.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<{
/** Emitted when navigation has a request associated with it. */
request: Request;
/** Emitted when fragment navigation occurred. */
fragment: NavigationInfo;
/** Emitted when navigation failed. */
failed: NavigationInfo;
/** Emitted when navigation was aborted. */
aborted: NavigationInfo;
}> {
static from(context: BrowsingContext): Navigation {
const navigation = new Navigation(context);
navigation.#initialize();
return navigation;
}
#request: Request | undefined;
#navigation: Navigation | undefined;
readonly #browsingContext: BrowsingContext;
readonly #disposables = new DisposableStack();
#id?: string | null;
private constructor(context: BrowsingContext) {
super();
this.#browsingContext = context;
}
#initialize() {
const browsingContextEmitter = this.#disposables.use(
new EventEmitter(this.#browsingContext),
);
browsingContextEmitter.once('closed', () => {
this.emit('failed', {
url: this.#browsingContext.url,
timestamp: new Date(),
});
this.dispose();
});
browsingContextEmitter.on('request', ({request}) => {
if (
request.navigation === 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 requestEmitter = this.#disposables.use(
new EventEmitter(this.#request),
);
requestEmitter.on('redirect', request => {
this.#request = request;
});
});
const sessionEmitter = this.#disposables.use(
new EventEmitter(this.#session),
);
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 ||
info.navigation === null ||
!this.#matches(info.navigation)
) {
return;
}
this.dispose();
});
}
for (const [eventName, event] of [
['browsingContext.fragmentNavigated', 'fragment'],
['browsingContext.navigationFailed', 'failed'],
['browsingContext.navigationAborted', 'aborted'],
] as const) {
sessionEmitter.on(eventName, info => {
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),
});
this.dispose();
});
}
}
#matches(navigation: string | null): boolean {
if (this.#navigation !== undefined && !this.#navigation.disposed) {
return false;
}
if (this.#id === undefined) {
this.#id = navigation;
return true;
}
return this.#id === navigation;
}
get #session() {
return this.#browsingContext.userContext.browser.session;
}
get disposed(): boolean {
return this.#disposables.disposed;
}
get request(): Request | undefined {
return this.#request;
}
get navigation(): Navigation | undefined {
return this.#navigation;
}
@inertIfDisposed
private dispose(): void {
this[disposeSymbol]();
}
override [disposeSymbol](): void {
this.#disposables.dispose();
super[disposeSymbol]();
}
}

52
node_modules/puppeteer-core/src/bidi/core/README.md generated vendored Normal file
View file

@ -0,0 +1,52 @@
# `bidi/core`
`bidi/core` is a low-level layer that sits above the WebDriver BiDi transport to
provide a structured API to WebDriver BiDi's flat API. In particular,
`bidi/core` provides object-oriented semantics around WebDriver BiDi resources
and automatically carries out the correct order of events in WebDriver BiDi through
the use of events.
## Tips
There are a few design decisions in this library that should be considered when
developing `bidi/core`:
- Required arguments are inlined as function arguments while optional arguments
are put into an options object.
- Function arguments are implicitly required in TypeScript, so by putting
required arguments as function arguments, the semantic is automatically
inherited.
- The session shall never be exposed on any public method/getter on any
object except the browser. Private getters are allowed.
- Passing around the session is dangerous as it obfuscates the origin of the
session. By only allowing it on the browser, the origin is well-defined.
- `bidi/core` implements WebDriver BiDi plus its surrounding specifications.
- A lot of WebDriver BiDi is not strictly written in WebDriver BiDi. Since WebDriver
BiDi interacts with several other specs, there are other considerations that
also influence the design of `bidi/core`. For example, for navigation,
WebDriver BiDi doesn't have a concept of "nested navigation", but in
practice this exists if a fragment navigation happens in a `beforeunload`
hook.
- `bidi/core` always follow the spec and never Puppeteer's needs.
- By ensuring `bidi/core` follows the spec rather than Puppeteer's needs, we
can identify the source of a bug precisely (i.e. whether the spec needs to
be updated or Puppeteer needs to work around it).
- `bidi/core` attempts to implement WebDriver BiDi comprehensively, but
minimally.
- Imagine the objects and events in WebDriver BiDi as a large
[graph](<https://en.wikipedia.org/wiki/Graph_(discrete_mathematics)>) where
objects are nodes and events are edges. In `bidi/core`, we always implement
all edges and nodes required by a feature without skipping nodes and events
(e.g. [fragment navigation -> navigation -> browsing context]; not [fragment
navigation -> browsing context]). We also never compose edges (e.g. both
[fragment navigation -> navigation -> browsing context] and [fragment
navigation -> browsing context] must not exist; i.e. a fragment navigation
event should not occur on the browsing context). This ensures that the
semantics of WebDriver BiDi is carried out correctly.
- This point reinforces `bidi/core` should not follow Puppeteer's needs since
Puppeteer typically composes a lot of events to satisfy its needs.

338
node_modules/puppeteer-core/src/bidi/core/Realm.ts generated vendored Normal file
View file

@ -0,0 +1,338 @@
/**
* @license
* Copyright 2024 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type * as Bidi from 'webdriver-bidi-protocol';
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';
/**
* @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 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. */
worker: DedicatedWorkerRealm;
/** Emitted when a shared worker is created in the realm. */
sharedworker: SharedWorkerRealm;
}> {
#reason?: string;
protected readonly disposables = new DisposableStack();
readonly id: string;
readonly origin: string;
protected executionContextId?: number;
protected constructor(id: string, origin: string) {
super();
this.id = id;
this.origin = origin;
}
get disposed(): boolean {
return this.#reason !== undefined;
}
protected abstract get session(): Session;
get target(): Bidi.Script.Target {
return {realm: this.id};
}
@inertIfDisposed
protected dispose(reason?: string): void {
this.#reason = reason;
this[disposeSymbol]();
}
@throwIfDisposed<Realm>(realm => {
// SAFETY: Disposal implies this exists.
return realm.#reason!;
})
async disown(handles: string[]): Promise<void> {
await this.session.send('script.disown', {
target: this.target,
handles,
});
}
@throwIfDisposed<Realm>(realm => {
// SAFETY: Disposal implies this exists.
return realm.#reason!;
})
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;
}
@throwIfDisposed<Realm>(realm => {
// SAFETY: Disposal implies this exists.
return realm.#reason!;
})
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;
}
@throwIfDisposed<Realm>(realm => {
// SAFETY: Disposal implies this exists.
return realm.#reason!;
})
async resolveExecutionContextId(): Promise<number> {
if (!this.executionContextId) {
const {result} = await (this.session.connection as BidiConnection).send(
'goog:cdp.resolveRealm',
{realm: this.id},
);
this.executionContextId = result.executionContextId;
}
return this.executionContextId;
}
override [disposeSymbol](): void {
this.#reason ??=
'Realm already destroyed, probably because all associated browsing contexts closed.';
this.emit('destroyed', {reason: this.#reason});
this.disposables.dispose();
super[disposeSymbol]();
}
}
/**
* @internal
*/
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 = new Map<string, DedicatedWorkerRealm>();
private constructor(context: BrowsingContext, sandbox?: string) {
super('', '');
this.browsingContext = context;
this.sandbox = sandbox;
}
#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' ||
info.context !== this.browsingContext.id ||
info.sandbox !== this.sandbox
) {
return;
}
(this as any).id = info.realm;
(this as any).origin = info.origin;
this.executionContextId = undefined;
this.emit('updated', this);
});
sessionEmitter.on('script.realmCreated', info => {
if (info.type !== 'dedicated-worker') {
return;
}
if (!info.owners.includes(this.id)) {
return;
}
const realm = DedicatedWorkerRealm.from(this, info.realm, info.origin);
this.#workers.set(realm.id, realm);
const realmEmitter = this.disposables.use(new EventEmitter(realm));
realmEmitter.once('destroyed', () => {
realmEmitter.removeAllListeners();
this.#workers.delete(realm.id);
});
this.emit('worker', 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};
}
}
/**
* @internal
*/
export type DedicatedWorkerOwnerRealm =
| DedicatedWorkerRealm
| SharedWorkerRealm
| WindowRealm;
/**
* @internal
*/
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 #workers = new Map<string, DedicatedWorkerRealm>();
readonly owners: Set<DedicatedWorkerOwnerRealm>;
private constructor(
owner: DedicatedWorkerOwnerRealm,
id: string,
origin: string,
) {
super(id, origin);
this.owners = new Set([owner]);
}
#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;
}
if (!info.owners.includes(this.id)) {
return;
}
const realm = DedicatedWorkerRealm.from(this, info.realm, info.origin);
this.#workers.set(realm.id, realm);
const realmEmitter = this.disposables.use(new EventEmitter(realm));
realmEmitter.once('destroyed', () => {
this.#workers.delete(realm.id);
});
this.emit('worker', realm);
});
}
override get session(): Session {
// SAFETY: At least one owner will exist.
return this.owners.values().next().value!.session;
}
}
/**
* @internal
*/
export class SharedWorkerRealm extends Realm {
static from(browser: Browser, id: string, origin: string): SharedWorkerRealm {
const realm = new SharedWorkerRealm(browser, id, origin);
realm.#initialize();
return realm;
}
readonly #workers = new Map<string, DedicatedWorkerRealm>();
readonly browser: Browser;
private constructor(browser: Browser, id: string, origin: string) {
super(id, origin);
this.browser = browser;
}
#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;
}
if (!info.owners.includes(this.id)) {
return;
}
const realm = DedicatedWorkerRealm.from(this, info.realm, info.origin);
this.#workers.set(realm.id, realm);
const realmEmitter = this.disposables.use(new EventEmitter(realm));
realmEmitter.once('destroyed', () => {
this.#workers.delete(realm.id);
});
this.emit('worker', realm);
});
}
override get session(): Session {
return this.browser.session;
}
}

304
node_modules/puppeteer-core/src/bidi/core/Request.ts generated vendored Normal file
View file

@ -0,0 +1,304 @@
/**
* @license
* Copyright 2024 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import * as Bidi from 'webdriver-bidi-protocol';
import {ProtocolError} from '../../common/Errors.js';
import {EventEmitter} from '../../common/EventEmitter.js';
import {inertIfDisposed} from '../../util/decorators.js';
import {DisposableStack, disposeSymbol} from '../../util/disposable.js';
import {stringToTypedArray} from '../../util/encoding.js';
import type {BrowsingContext} from './BrowsingContext.js';
/**
* @internal
*/
export class Request extends EventEmitter<{
/** Emitted when the request is redirected. */
redirect: Request;
/** Emitted when the request succeeds. */
authenticate: void;
/** Emitted when the request succeeds. */
success: Bidi.Network.ResponseData;
/** Emitted when the request fails. */
error: string;
}> {
static from(
browsingContext: BrowsingContext,
event: Bidi.Network.BeforeRequestSentParameters,
): Request {
const request = new Request(browsingContext, event);
request.#initialize();
return request;
}
#responseContentPromise: Promise<Uint8Array<ArrayBufferLike>> | null = null;
#error?: string;
#redirect?: Request;
#response?: Bidi.Network.ResponseData;
readonly #browsingContext: BrowsingContext;
readonly #disposables = new DisposableStack();
readonly #event: Bidi.Network.BeforeRequestSentParameters;
private constructor(
browsingContext: BrowsingContext,
event: Bidi.Network.BeforeRequestSentParameters,
) {
super();
this.#browsingContext = browsingContext;
this.#event = event;
}
#initialize() {
const browsingContextEmitter = this.#disposables.use(
new EventEmitter(this.#browsingContext),
);
browsingContextEmitter.once('closed', ({reason}) => {
this.#error = reason;
this.emit('error', this.#error);
this.dispose();
});
const sessionEmitter = this.#disposables.use(
new EventEmitter(this.#session),
);
sessionEmitter.on('network.beforeRequestSent', event => {
if (
event.context !== this.#browsingContext.id ||
event.request.request !== this.id
) {
return;
}
// This is a workaround to detect if a beforeRequestSent is for a request
// sent after continueWithAuth. Currently, only emitted in Firefox.
const previousRequestHasAuth = this.#event.request.headers.find(
header => {
return header.name.toLowerCase() === 'authorization';
},
);
const newRequestHasAuth = event.request.headers.find(header => {
return header.name.toLowerCase() === 'authorization';
});
const isAfterAuth = newRequestHasAuth && !previousRequestHasAuth;
if (
event.redirectCount !== this.#event.redirectCount + 1 &&
!isAfterAuth
) {
return;
}
this.#redirect = Request.from(this.#browsingContext, event);
this.emit('redirect', this.#redirect);
this.dispose();
});
sessionEmitter.on('network.authRequired', event => {
if (
event.context !== this.#browsingContext.id ||
event.request.request !== this.id ||
// Don't try to authenticate for events that are not blocked
!event.isBlocked
) {
return;
}
this.emit('authenticate', undefined);
});
sessionEmitter.on('network.fetchError', event => {
if (
event.context !== this.#browsingContext.id ||
event.request.request !== this.id ||
this.#event.redirectCount !== event.redirectCount
) {
return;
}
this.#error = event.errorText;
this.emit('error', this.#error);
this.dispose();
});
sessionEmitter.on('network.responseCompleted', event => {
if (
event.context !== this.#browsingContext.id ||
event.request.request !== this.id ||
this.#event.redirectCount !== event.redirectCount
) {
return;
}
this.#response = event.response;
this.#event.request.timings = event.request.timings;
this.emit('success', this.#response);
// In case this is a redirect.
if (this.#response.status >= 300 && this.#response.status < 400) {
return;
}
this.dispose();
});
}
get #session() {
return this.#browsingContext.userContext.browser.session;
}
get disposed(): boolean {
return this.#disposables.disposed;
}
get error(): string | undefined {
return this.#error;
}
get headers(): Bidi.Network.Header[] {
return this.#event.request.headers;
}
get id(): string {
return this.#event.request.request;
}
get initiator(): Bidi.Network.Initiator | undefined {
return this.#event.initiator;
}
get method(): string {
return this.#event.request.method;
}
get navigation(): string | undefined {
return this.#event.navigation ?? undefined;
}
get redirect(): Request | undefined {
return this.#redirect;
}
get lastRedirect(): Request | undefined {
let redirect = this.#redirect;
while (redirect) {
if (redirect && !redirect.#redirect) {
return redirect;
}
redirect = redirect.#redirect;
}
return redirect;
}
get response(): Bidi.Network.ResponseData | undefined {
return this.#response;
}
get url(): string {
return this.#event.request.url;
}
get isBlocked(): boolean {
return this.#event.isBlocked;
}
get resourceType(): string | undefined {
// @ts-expect-error non-standard attribute.
return this.#event.request['goog:resourceType'] ?? undefined;
}
get postData(): string | undefined {
// @ts-expect-error non-standard attribute.
return this.#event.request['goog:postData'] ?? undefined;
}
get hasPostData(): boolean {
// @ts-expect-error non-standard attribute.
return this.#event.request['goog:hasPostData'] ?? false;
}
async continueRequest({
url,
method,
headers,
cookies,
body,
}: Omit<Bidi.Network.ContinueRequestParameters, 'request'>): Promise<void> {
await this.#session.send('network.continueRequest', {
request: this.id,
url,
method,
headers,
body,
cookies,
});
}
async failRequest(): Promise<void> {
await this.#session.send('network.failRequest', {
request: this.id,
});
}
async provideResponse({
statusCode,
reasonPhrase,
headers,
body,
}: Omit<Bidi.Network.ProvideResponseParameters, 'request'>): Promise<void> {
await this.#session.send('network.provideResponse', {
request: this.id,
statusCode,
reasonPhrase,
headers,
body,
});
}
async getResponseContent(): Promise<Uint8Array> {
if (!this.#responseContentPromise) {
this.#responseContentPromise = (async () => {
try {
const data = await this.#session.send('network.getData', {
dataType: Bidi.Network.DataType.Response,
request: this.id,
});
return stringToTypedArray(
data.result.bytes.value,
data.result.bytes.type === 'base64',
);
} catch (error) {
if (
error instanceof ProtocolError &&
error.originalMessage.includes(
'No resource with given identifier found',
)
) {
throw new ProtocolError(
'Could not load body for this request. This might happen if the request is a preflight request.',
);
}
throw error;
}
})();
}
return await this.#responseContentPromise;
}
async continueWithAuth(
parameters:
| Bidi.Network.ContinueWithAuthCredentials
| Bidi.Network.ContinueWithAuthNoCredentials,
): Promise<void> {
if (parameters.action === 'provideCredentials') {
await this.#session.send('network.continueWithAuth', {
request: this.id,
action: parameters.action,
credentials: parameters.credentials,
});
} else {
await this.#session.send('network.continueWithAuth', {
request: this.id,
action: parameters.action,
});
}
}
@inertIfDisposed
private dispose(): void {
this[disposeSymbol]();
}
override [disposeSymbol](): void {
this.#disposables.dispose();
super[disposeSymbol]();
}
timing(): Bidi.Network.FetchTimingInfo {
return this.#event.request.timings;
}
}

162
node_modules/puppeteer-core/src/bidi/core/Session.ts generated vendored Normal file
View file

@ -0,0 +1,162 @@
/**
* @license
* Copyright 2024 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type * as Bidi from 'webdriver-bidi-protocol';
import {EventEmitter} from '../../common/EventEmitter.js';
import {
bubble,
inertIfDisposed,
throwIfDisposed,
} from '../../util/decorators.js';
import {DisposableStack, disposeSymbol} from '../../util/disposable.js';
import {Browser} from './Browser.js';
import type {BidiEvents, Commands, Connection} from './Connection.js';
/**
* @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> {
const {result} = await connection.send('session.new', {
capabilities,
});
const session = new Session(connection, result);
await session.#initialize();
return session;
}
#reason: string | undefined;
readonly #disposables = new DisposableStack();
readonly #info: Bidi.Session.NewResult;
readonly browser!: Browser;
@bubble()
accessor connection: Connection;
private constructor(connection: Connection, info: Bidi.Session.NewResult) {
super();
this.#info = info;
this.connection = connection;
}
async #initialize(): Promise<void> {
// SAFETY: We use `any` to allow assignment of the readonly property.
(this as any).browser = await Browser.from(this);
const browserEmitter = this.#disposables.use(this.browser);
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);
});
}
get capabilities(): Bidi.Session.NewResult['capabilities'] {
return this.#info.capabilities;
}
get disposed(): boolean {
return this.ended;
}
get ended(): boolean {
return this.#reason !== undefined;
}
get id(): string {
return this.#info.sessionId;
}
@inertIfDisposed
private dispose(reason?: string): void {
this.#reason = reason;
this[disposeSymbol]();
}
/**
* 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, ...string[]],
contexts?: [string, ...string[]],
): Promise<void> {
await this.send('session.subscribe', {
events,
contexts,
});
}
@throwIfDisposed<Session>(session => {
// SAFETY: By definition of `disposed`, `#reason` is defined.
return session.#reason!;
})
async addIntercepts(
events: [string, ...string[]],
contexts?: [string, ...string[]],
): Promise<void> {
await this.send('session.subscribe', {
events,
contexts,
});
}
@throwIfDisposed<Session>(session => {
// SAFETY: By definition of `disposed`, `#reason` is defined.
return session.#reason!;
})
async end(): Promise<void> {
try {
await this.send('session.end', {});
} finally {
this.dispose(`Session already ended.`);
}
}
override [disposeSymbol](): void {
this.#reason ??=
'Session already destroyed, probably because the connection broke.';
this.emit('ended', {reason: this.#reason});
this.#disposables.dispose();
super[disposeSymbol]();
}
}

View file

@ -0,0 +1,241 @@
/**
* @license
* Copyright 2024 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type * as Bidi from 'webdriver-bidi-protocol';
import {EventEmitter} from '../../common/EventEmitter.js';
import {assert} from '../../util/assert.js';
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';
/**
* @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;
};
/**
* Emitted when the user context is closed.
*/
closed: {
/** The reason the user context was closed. */
reason: string;
};
}> {
static DEFAULT = 'default' as const;
static create(browser: Browser, id: string): UserContext {
const context = new UserContext(browser, id);
context.#initialize();
return context;
}
#reason?: string;
// Note these are only top-level contexts.
readonly #browsingContexts = new Map<string, BrowsingContext>();
readonly #disposables = new DisposableStack();
readonly #id: string;
readonly browser: Browser;
private constructor(browser: Browser, id: string) {
super();
this.#id = id;
this.browser = browser;
}
#initialize() {
const browserEmitter = this.#disposables.use(
new EventEmitter(this.browser),
);
browserEmitter.once('closed', ({reason}) => {
this.dispose(`User context was closed: ${reason}`);
});
browserEmitter.once('disconnected', ({reason}) => {
this.dispose(`User context was closed: ${reason}`);
});
const sessionEmitter = this.#disposables.use(
new EventEmitter(this.#session),
);
sessionEmitter.on('browsingContext.contextCreated', info => {
if (info.parent) {
return;
}
if (info.userContext !== this.#id) {
return;
}
const browsingContext = BrowsingContext.from(
this,
undefined,
info.context,
info.url,
info.originalOpener,
);
this.#browsingContexts.set(browsingContext.id, browsingContext);
const browsingContextEmitter = this.#disposables.use(
new EventEmitter(browsingContext),
);
browsingContextEmitter.on('closed', () => {
browsingContextEmitter.removeAllListeners();
this.#browsingContexts.delete(browsingContext.id);
});
this.emit('browsingcontext', {browsingContext});
});
}
get #session() {
return this.browser.session;
}
get browsingContexts(): Iterable<BrowsingContext> {
return this.#browsingContexts.values();
}
get closed(): boolean {
return this.#reason !== undefined;
}
get disposed(): boolean {
return this.closed;
}
get id(): string {
return this.#id;
}
@inertIfDisposed
private dispose(reason?: string): void {
this.#reason = reason;
this[disposeSymbol]();
}
@throwIfDisposed<UserContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
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,
userContext: this.#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;
}
@throwIfDisposed<UserContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async remove(): Promise<void> {
try {
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,
userContext: this.#id,
});
}
override [disposeSymbol](): void {
this.#reason ??=
'User context already closed, probably because the browser disconnected/closed.';
this.emit('closed', {reason: this.#reason});
this.#disposables.dispose();
super[disposeSymbol]();
}
}

138
node_modules/puppeteer-core/src/bidi/core/UserPrompt.ts generated vendored Normal file
View file

@ -0,0 +1,138 @@
/**
* @license
* Copyright 2024 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import * as Bidi from 'webdriver-bidi-protocol';
import {EventEmitter} from '../../common/EventEmitter.js';
import {inertIfDisposed, throwIfDisposed} from '../../util/decorators.js';
import {DisposableStack, disposeSymbol} from '../../util/disposable.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<{
/** Emitted when the user prompt is handled. */
handled: UserPromptResult;
/** Emitted when the user prompt is closed. */
closed: {
/** The reason the user prompt was closed. */
reason: string;
};
}> {
static from(
browsingContext: BrowsingContext,
info: Bidi.BrowsingContext.UserPromptOpenedParameters,
): UserPrompt {
const userPrompt = new UserPrompt(browsingContext, info);
userPrompt.#initialize();
return userPrompt;
}
#reason?: string;
#result?: UserPromptResult;
readonly #disposables = new DisposableStack();
readonly browsingContext: BrowsingContext;
readonly info: Bidi.BrowsingContext.UserPromptOpenedParameters;
private constructor(
context: BrowsingContext,
info: Bidi.BrowsingContext.UserPromptOpenedParameters,
) {
super();
this.browsingContext = context;
this.info = info;
}
#initialize() {
const browserContextEmitter = this.#disposables.use(
new EventEmitter(this.browsingContext),
);
browserContextEmitter.once('closed', ({reason}) => {
this.dispose(`User prompt already closed: ${reason}`);
});
const sessionEmitter = this.#disposables.use(
new EventEmitter(this.#session),
);
sessionEmitter.on('browsingContext.userPromptClosed', parameters => {
if (parameters.context !== this.browsingContext.id) {
return;
}
this.#result = parameters;
this.emit('handled', parameters);
this.dispose('User prompt already handled.');
});
}
get #session() {
return this.browsingContext.userContext.browser.session;
}
get closed(): boolean {
return this.#reason !== undefined;
}
get disposed(): boolean {
return this.closed;
}
get handled(): boolean {
if (
this.info.handler === Bidi.Session.UserPromptHandlerType.Accept ||
this.info.handler === Bidi.Session.UserPromptHandlerType.Dismiss
) {
return true;
}
return this.#result !== undefined;
}
get result(): UserPromptResult | undefined {
return this.#result;
}
@inertIfDisposed
private dispose(reason?: string): void {
this.#reason = reason;
this[disposeSymbol]();
}
@throwIfDisposed<UserPrompt>(prompt => {
// SAFETY: Disposal implies this exists.
return prompt.#reason!;
})
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!;
}
override [disposeSymbol](): void {
this.#reason ??=
'User prompt already closed, probably because the associated browsing context was destroyed.';
this.emit('closed', {reason: this.#reason});
this.#disposables.dispose();
super[disposeSymbol]();
}
}

15
node_modules/puppeteer-core/src/bidi/core/core.ts generated vendored Normal file
View 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 type * 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';

76
node_modules/puppeteer-core/src/bidi/util.ts generated vendored Normal file
View file

@ -0,0 +1,76 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type * as Bidi from 'webdriver-bidi-protocol';
import {ProtocolError, TimeoutError} from '../common/Errors.js';
import {PuppeteerURL} from '../common/util.js';
import {BidiDeserializer} from './Deserializer.js';
/**
* @internal
*/
export function createEvaluationError(
details: Bidi.Script.ExceptionDetails,
): unknown {
if (details.exception.type !== 'error') {
return BidiDeserializer.deserialize(details.exception);
}
const [name = '', ...parts] = details.text.split(': ');
const message = parts.join(': ');
const error = new Error(message);
error.name = name;
// The first line is this function which we ignore.
const stackLines = [];
if (details.stackTrace && stackLines.length < Error.stackTraceLimit) {
for (const frame of details.stackTrace.callFrames.reverse()) {
if (
PuppeteerURL.isPuppeteerURL(frame.url) &&
frame.url !== PuppeteerURL.INTERNAL_URL
) {
const url = PuppeteerURL.parse(frame.url);
stackLines.unshift(
` at ${frame.functionName || url.functionName} (${
url.functionName
} at ${url.siteString}, <anonymous>:${frame.lineNumber}:${
frame.columnNumber
})`,
);
} else {
stackLines.push(
` at ${frame.functionName || '<anonymous>'} (${frame.url}:${
frame.lineNumber
}:${frame.columnNumber})`,
);
}
if (stackLines.length >= Error.stackTraceLimit) {
break;
}
}
}
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;
};
}