Initial commit: Torrent search and download application
This commit is contained in:
commit
e38be704ff
4313 changed files with 791544 additions and 0 deletions
207
node_modules/puppeteer-core/src/bidi/BidiOverCdp.ts
generated
vendored
Normal file
207
node_modules/puppeteer-core/src/bidi/BidiOverCdp.ts
generated
vendored
Normal 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
315
node_modules/puppeteer-core/src/bidi/Browser.ts
generated
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
123
node_modules/puppeteer-core/src/bidi/BrowserConnector.ts
generated
vendored
Normal file
123
node_modules/puppeteer-core/src/bidi/BrowserConnector.ts
generated
vendored
Normal 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
331
node_modules/puppeteer-core/src/bidi/BrowserContext.ts
generated
vendored
Normal 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
124
node_modules/puppeteer-core/src/bidi/CDPSession.ts
generated
vendored
Normal 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
213
node_modules/puppeteer-core/src/bidi/Connection.ts
generated
vendored
Normal 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
92
node_modules/puppeteer-core/src/bidi/Deserializer.ts
generated
vendored
Normal 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
32
node_modules/puppeteer-core/src/bidi/Dialog.ts
generated
vendored
Normal 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
152
node_modules/puppeteer-core/src/bidi/ElementHandle.ts
generated
vendored
Normal 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
256
node_modules/puppeteer-core/src/bidi/ExposedFunction.ts
generated
vendored
Normal 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
643
node_modules/puppeteer-core/src/bidi/Frame.ts
generated
vendored
Normal 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
384
node_modules/puppeteer-core/src/bidi/HTTPRequest.ts
generated
vendored
Normal 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
152
node_modules/puppeteer-core/src/bidi/HTTPResponse.ts
generated
vendored
Normal 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
742
node_modules/puppeteer-core/src/bidi/Input.ts
generated
vendored
Normal 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
95
node_modules/puppeteer-core/src/bidi/JSHandle.ts
generated
vendored
Normal 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
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
410
node_modules/puppeteer-core/src/bidi/Realm.ts
generated
vendored
Normal 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
126
node_modules/puppeteer-core/src/bidi/Serializer.ts
generated
vendored
Normal 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
170
node_modules/puppeteer-core/src/bidi/Target.ts
generated
vendored
Normal 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
48
node_modules/puppeteer-core/src/bidi/WebWorker.ts
generated
vendored
Normal 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
18
node_modules/puppeteer-core/src/bidi/bidi.ts
generated
vendored
Normal 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
300
node_modules/puppeteer-core/src/bidi/core/Browser.ts
generated
vendored
Normal 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]();
|
||||
}
|
||||
}
|
||||
729
node_modules/puppeteer-core/src/bidi/core/BrowsingContext.ts
generated
vendored
Normal file
729
node_modules/puppeteer-core/src/bidi/core/BrowsingContext.ts
generated
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
30
node_modules/puppeteer-core/src/bidi/core/Connection.ts
generated
vendored
Normal file
30
node_modules/puppeteer-core/src/bidi/core/Connection.ts
generated
vendored
Normal 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
173
node_modules/puppeteer-core/src/bidi/core/Navigation.ts
generated
vendored
Normal 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
52
node_modules/puppeteer-core/src/bidi/core/README.md
generated
vendored
Normal 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
338
node_modules/puppeteer-core/src/bidi/core/Realm.ts
generated
vendored
Normal 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
304
node_modules/puppeteer-core/src/bidi/core/Request.ts
generated
vendored
Normal 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
162
node_modules/puppeteer-core/src/bidi/core/Session.ts
generated
vendored
Normal 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]();
|
||||
}
|
||||
}
|
||||
241
node_modules/puppeteer-core/src/bidi/core/UserContext.ts
generated
vendored
Normal file
241
node_modules/puppeteer-core/src/bidi/core/UserContext.ts
generated
vendored
Normal 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
138
node_modules/puppeteer-core/src/bidi/core/UserPrompt.ts
generated
vendored
Normal 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
15
node_modules/puppeteer-core/src/bidi/core/core.ts
generated
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2024 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
export * from './Browser.js';
|
||||
export * from './BrowsingContext.js';
|
||||
export 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
76
node_modules/puppeteer-core/src/bidi/util.ts
generated
vendored
Normal 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;
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue