Initial commit
This commit is contained in:
+913
@@ -0,0 +1,913 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2022 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {Readable} from 'stream';
|
||||
|
||||
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
||||
import type Protocol from 'devtools-protocol';
|
||||
|
||||
import {
|
||||
firstValueFrom,
|
||||
from,
|
||||
map,
|
||||
raceWith,
|
||||
zip,
|
||||
} from '../../third_party/rxjs/rxjs.js';
|
||||
import type {CDPSession} from '../api/CDPSession.js';
|
||||
import type {BoundingBox} from '../api/ElementHandle.js';
|
||||
import type {WaitForOptions} from '../api/Frame.js';
|
||||
import type {HTTPResponse} from '../api/HTTPResponse.js';
|
||||
import {
|
||||
Page,
|
||||
PageEvent,
|
||||
type GeolocationOptions,
|
||||
type MediaFeature,
|
||||
type NewDocumentScriptEvaluation,
|
||||
type ScreenshotOptions,
|
||||
} from '../api/Page.js';
|
||||
import {Accessibility} from '../cdp/Accessibility.js';
|
||||
import {Coverage} from '../cdp/Coverage.js';
|
||||
import {EmulationManager as CdpEmulationManager} from '../cdp/EmulationManager.js';
|
||||
import {FrameTree} from '../cdp/FrameTree.js';
|
||||
import {Tracing} from '../cdp/Tracing.js';
|
||||
import {
|
||||
ConsoleMessage,
|
||||
type ConsoleMessageLocation,
|
||||
} from '../common/ConsoleMessage.js';
|
||||
import {TargetCloseError, UnsupportedOperation} from '../common/Errors.js';
|
||||
import type {Handler} from '../common/EventEmitter.js';
|
||||
import {NetworkManagerEvent} from '../common/NetworkManagerEvents.js';
|
||||
import type {PDFOptions} from '../common/PDFOptions.js';
|
||||
import type {Awaitable} from '../common/types.js';
|
||||
import {
|
||||
debugError,
|
||||
evaluationString,
|
||||
NETWORK_IDLE_TIME,
|
||||
parsePDFOptions,
|
||||
timeout,
|
||||
validateDialogType,
|
||||
} from '../common/util.js';
|
||||
import type {Viewport} from '../common/Viewport.js';
|
||||
import {assert} from '../util/assert.js';
|
||||
import {Deferred} from '../util/Deferred.js';
|
||||
import {disposeSymbol} from '../util/disposable.js';
|
||||
import {isErrorLike} from '../util/ErrorLike.js';
|
||||
|
||||
import type {BidiBrowser} from './Browser.js';
|
||||
import type {BidiBrowserContext} from './BrowserContext.js';
|
||||
import {
|
||||
BrowsingContextEvent,
|
||||
CdpSessionWrapper,
|
||||
type BrowsingContext,
|
||||
} from './BrowsingContext.js';
|
||||
import type {BidiConnection} from './Connection.js';
|
||||
import {BidiDeserializer} from './Deserializer.js';
|
||||
import {BidiDialog} from './Dialog.js';
|
||||
import {BidiElementHandle} from './ElementHandle.js';
|
||||
import {EmulationManager} from './EmulationManager.js';
|
||||
import {BidiFrame} from './Frame.js';
|
||||
import type {BidiHTTPRequest} from './HTTPRequest.js';
|
||||
import type {BidiHTTPResponse} from './HTTPResponse.js';
|
||||
import {BidiKeyboard, BidiMouse, BidiTouchscreen} from './Input.js';
|
||||
import type {BidiJSHandle} from './JSHandle.js';
|
||||
import {getBiDiReadinessState, rewriteNavigationError} from './lifecycle.js';
|
||||
import {BidiNetworkManager} from './NetworkManager.js';
|
||||
import {createBidiHandle} from './Realm.js';
|
||||
import type {BiDiPageTarget} from './Target.js';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class BidiPage extends Page {
|
||||
#accessibility: Accessibility;
|
||||
#connection: BidiConnection;
|
||||
#frameTree = new FrameTree<BidiFrame>();
|
||||
#networkManager: BidiNetworkManager;
|
||||
#viewport: Viewport | null = null;
|
||||
#closedDeferred = Deferred.create<never, TargetCloseError>();
|
||||
#subscribedEvents = new Map<Bidi.Event['method'], Handler<any>>([
|
||||
['log.entryAdded', this.#onLogEntryAdded.bind(this)],
|
||||
['browsingContext.load', this.#onFrameLoaded.bind(this)],
|
||||
[
|
||||
'browsingContext.fragmentNavigated',
|
||||
this.#onFrameFragmentNavigated.bind(this),
|
||||
],
|
||||
[
|
||||
'browsingContext.domContentLoaded',
|
||||
this.#onFrameDOMContentLoaded.bind(this),
|
||||
],
|
||||
['browsingContext.userPromptOpened', this.#onDialog.bind(this)],
|
||||
]);
|
||||
readonly #networkManagerEvents = [
|
||||
[
|
||||
NetworkManagerEvent.Request,
|
||||
(request: BidiHTTPRequest) => {
|
||||
this.emit(PageEvent.Request, request);
|
||||
},
|
||||
],
|
||||
[
|
||||
NetworkManagerEvent.RequestServedFromCache,
|
||||
(request: BidiHTTPRequest) => {
|
||||
this.emit(PageEvent.RequestServedFromCache, request);
|
||||
},
|
||||
],
|
||||
[
|
||||
NetworkManagerEvent.RequestFailed,
|
||||
(request: BidiHTTPRequest) => {
|
||||
this.emit(PageEvent.RequestFailed, request);
|
||||
},
|
||||
],
|
||||
[
|
||||
NetworkManagerEvent.RequestFinished,
|
||||
(request: BidiHTTPRequest) => {
|
||||
this.emit(PageEvent.RequestFinished, request);
|
||||
},
|
||||
],
|
||||
[
|
||||
NetworkManagerEvent.Response,
|
||||
(response: BidiHTTPResponse) => {
|
||||
this.emit(PageEvent.Response, response);
|
||||
},
|
||||
],
|
||||
] as const;
|
||||
|
||||
readonly #browsingContextEvents = new Map<symbol, Handler<any>>([
|
||||
[BrowsingContextEvent.Created, this.#onContextCreated.bind(this)],
|
||||
[BrowsingContextEvent.Destroyed, this.#onContextDestroyed.bind(this)],
|
||||
]);
|
||||
#tracing: Tracing;
|
||||
#coverage: Coverage;
|
||||
#cdpEmulationManager: CdpEmulationManager;
|
||||
#emulationManager: EmulationManager;
|
||||
#mouse: BidiMouse;
|
||||
#touchscreen: BidiTouchscreen;
|
||||
#keyboard: BidiKeyboard;
|
||||
#browsingContext: BrowsingContext;
|
||||
#browserContext: BidiBrowserContext;
|
||||
#target: BiDiPageTarget;
|
||||
|
||||
_client(): CDPSession {
|
||||
return this.mainFrame().context().cdpSession;
|
||||
}
|
||||
|
||||
constructor(
|
||||
browsingContext: BrowsingContext,
|
||||
browserContext: BidiBrowserContext,
|
||||
target: BiDiPageTarget
|
||||
) {
|
||||
super();
|
||||
this.#browsingContext = browsingContext;
|
||||
this.#browserContext = browserContext;
|
||||
this.#target = target;
|
||||
this.#connection = browsingContext.connection;
|
||||
|
||||
for (const [event, subscriber] of this.#browsingContextEvents) {
|
||||
this.#browsingContext.on(event, subscriber);
|
||||
}
|
||||
|
||||
this.#networkManager = new BidiNetworkManager(this.#connection, this);
|
||||
|
||||
for (const [event, subscriber] of this.#subscribedEvents) {
|
||||
this.#connection.on(event, subscriber);
|
||||
}
|
||||
|
||||
for (const [event, subscriber] of this.#networkManagerEvents) {
|
||||
// TODO: remove any
|
||||
this.#networkManager.on(event, subscriber as any);
|
||||
}
|
||||
|
||||
const frame = new BidiFrame(
|
||||
this,
|
||||
this.#browsingContext,
|
||||
this._timeoutSettings,
|
||||
this.#browsingContext.parent
|
||||
);
|
||||
this.#frameTree.addFrame(frame);
|
||||
this.emit(PageEvent.FrameAttached, frame);
|
||||
|
||||
// TODO: https://github.com/w3c/webdriver-bidi/issues/443
|
||||
this.#accessibility = new Accessibility(
|
||||
this.mainFrame().context().cdpSession
|
||||
);
|
||||
this.#tracing = new Tracing(this.mainFrame().context().cdpSession);
|
||||
this.#coverage = new Coverage(this.mainFrame().context().cdpSession);
|
||||
this.#cdpEmulationManager = new CdpEmulationManager(
|
||||
this.mainFrame().context().cdpSession
|
||||
);
|
||||
this.#emulationManager = new EmulationManager(browsingContext);
|
||||
this.#mouse = new BidiMouse(this.mainFrame().context());
|
||||
this.#touchscreen = new BidiTouchscreen(this.mainFrame().context());
|
||||
this.#keyboard = new BidiKeyboard(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
get connection(): BidiConnection {
|
||||
return this.#connection;
|
||||
}
|
||||
|
||||
override async setUserAgent(
|
||||
userAgent: string,
|
||||
userAgentMetadata?: Protocol.Emulation.UserAgentMetadata | undefined
|
||||
): Promise<void> {
|
||||
// TODO: handle CDP-specific cases such as mprach.
|
||||
await this._client().send('Network.setUserAgentOverride', {
|
||||
userAgent: userAgent,
|
||||
userAgentMetadata: userAgentMetadata,
|
||||
});
|
||||
}
|
||||
|
||||
override async setBypassCSP(enabled: boolean): Promise<void> {
|
||||
// TODO: handle CDP-specific cases such as mprach.
|
||||
await this._client().send('Page.setBypassCSP', {enabled});
|
||||
}
|
||||
|
||||
override async queryObjects<Prototype>(
|
||||
prototypeHandle: BidiJSHandle<Prototype>
|
||||
): Promise<BidiJSHandle<Prototype[]>> {
|
||||
assert(!prototypeHandle.disposed, 'Prototype JSHandle is disposed!');
|
||||
assert(
|
||||
prototypeHandle.id,
|
||||
'Prototype JSHandle must not be referencing primitive value'
|
||||
);
|
||||
const response = await this.mainFrame().client.send(
|
||||
'Runtime.queryObjects',
|
||||
{
|
||||
prototypeObjectId: prototypeHandle.id,
|
||||
}
|
||||
);
|
||||
return createBidiHandle(this.mainFrame().mainRealm(), {
|
||||
type: 'array',
|
||||
handle: response.objects.objectId,
|
||||
}) as BidiJSHandle<Prototype[]>;
|
||||
}
|
||||
|
||||
_setBrowserContext(browserContext: BidiBrowserContext): void {
|
||||
this.#browserContext = browserContext;
|
||||
}
|
||||
|
||||
override get accessibility(): Accessibility {
|
||||
return this.#accessibility;
|
||||
}
|
||||
|
||||
override get tracing(): Tracing {
|
||||
return this.#tracing;
|
||||
}
|
||||
|
||||
override get coverage(): Coverage {
|
||||
return this.#coverage;
|
||||
}
|
||||
|
||||
override get mouse(): BidiMouse {
|
||||
return this.#mouse;
|
||||
}
|
||||
|
||||
override get touchscreen(): BidiTouchscreen {
|
||||
return this.#touchscreen;
|
||||
}
|
||||
|
||||
override get keyboard(): BidiKeyboard {
|
||||
return this.#keyboard;
|
||||
}
|
||||
|
||||
override browser(): BidiBrowser {
|
||||
return this.browserContext().browser();
|
||||
}
|
||||
|
||||
override browserContext(): BidiBrowserContext {
|
||||
return this.#browserContext;
|
||||
}
|
||||
|
||||
override mainFrame(): BidiFrame {
|
||||
const mainFrame = this.#frameTree.getMainFrame();
|
||||
assert(mainFrame, 'Requesting main frame too early!');
|
||||
return mainFrame;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async focusedFrame(): Promise<BidiFrame> {
|
||||
using frame = await this.mainFrame()
|
||||
.isolatedRealm()
|
||||
.evaluateHandle(() => {
|
||||
let frame: HTMLIFrameElement | undefined;
|
||||
let win: Window | null = window;
|
||||
while (win?.document.activeElement instanceof HTMLIFrameElement) {
|
||||
frame = win.document.activeElement;
|
||||
win = frame.contentWindow;
|
||||
}
|
||||
return frame;
|
||||
});
|
||||
if (!(frame instanceof BidiElementHandle)) {
|
||||
return this.mainFrame();
|
||||
}
|
||||
return await frame.contentFrame();
|
||||
}
|
||||
|
||||
override frames(): BidiFrame[] {
|
||||
return Array.from(this.#frameTree.frames());
|
||||
}
|
||||
|
||||
frame(frameId?: string): BidiFrame | null {
|
||||
return this.#frameTree.getById(frameId ?? '') || null;
|
||||
}
|
||||
|
||||
childFrames(frameId: string): BidiFrame[] {
|
||||
return this.#frameTree.childFrames(frameId);
|
||||
}
|
||||
|
||||
#onFrameLoaded(info: Bidi.BrowsingContext.NavigationInfo): void {
|
||||
const frame = this.frame(info.context);
|
||||
if (frame && this.mainFrame() === frame) {
|
||||
this.emit(PageEvent.Load, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
#onFrameFragmentNavigated(info: Bidi.BrowsingContext.NavigationInfo): void {
|
||||
const frame = this.frame(info.context);
|
||||
if (frame) {
|
||||
this.emit(PageEvent.FrameNavigated, frame);
|
||||
}
|
||||
}
|
||||
|
||||
#onFrameDOMContentLoaded(info: Bidi.BrowsingContext.NavigationInfo): void {
|
||||
const frame = this.frame(info.context);
|
||||
if (frame) {
|
||||
frame._hasStartedLoading = true;
|
||||
if (this.mainFrame() === frame) {
|
||||
this.emit(PageEvent.DOMContentLoaded, undefined);
|
||||
}
|
||||
this.emit(PageEvent.FrameNavigated, frame);
|
||||
}
|
||||
}
|
||||
|
||||
#onContextCreated(context: BrowsingContext): void {
|
||||
if (
|
||||
!this.frame(context.id) &&
|
||||
(this.frame(context.parent ?? '') || !this.#frameTree.getMainFrame())
|
||||
) {
|
||||
const frame = new BidiFrame(
|
||||
this,
|
||||
context,
|
||||
this._timeoutSettings,
|
||||
context.parent
|
||||
);
|
||||
this.#frameTree.addFrame(frame);
|
||||
if (frame !== this.mainFrame()) {
|
||||
this.emit(PageEvent.FrameAttached, frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#onContextDestroyed(context: BrowsingContext): void {
|
||||
const frame = this.frame(context.id);
|
||||
|
||||
if (frame) {
|
||||
if (frame === this.mainFrame()) {
|
||||
this.emit(PageEvent.Close, undefined);
|
||||
}
|
||||
this.#removeFramesRecursively(frame);
|
||||
}
|
||||
}
|
||||
|
||||
#removeFramesRecursively(frame: BidiFrame): void {
|
||||
for (const child of frame.childFrames()) {
|
||||
this.#removeFramesRecursively(child);
|
||||
}
|
||||
frame[disposeSymbol]();
|
||||
this.#networkManager.clearMapAfterFrameDispose(frame);
|
||||
this.#frameTree.removeFrame(frame);
|
||||
this.emit(PageEvent.FrameDetached, frame);
|
||||
}
|
||||
|
||||
#onLogEntryAdded(event: Bidi.Log.Entry): void {
|
||||
const frame = this.frame(event.source.context);
|
||||
if (!frame) {
|
||||
return;
|
||||
}
|
||||
if (isConsoleLogEntry(event)) {
|
||||
const args = event.args.map(arg => {
|
||||
return createBidiHandle(frame.mainRealm(), arg);
|
||||
});
|
||||
|
||||
const text = args
|
||||
.reduce((value, arg) => {
|
||||
const parsedValue = arg.isPrimitiveValue
|
||||
? BidiDeserializer.deserialize(arg.remoteValue())
|
||||
: arg.toString();
|
||||
return `${value} ${parsedValue}`;
|
||||
}, '')
|
||||
.slice(1);
|
||||
|
||||
this.emit(
|
||||
PageEvent.Console,
|
||||
new ConsoleMessage(
|
||||
event.method as any,
|
||||
text,
|
||||
args,
|
||||
getStackTraceLocations(event.stackTrace)
|
||||
)
|
||||
);
|
||||
} else if (isJavaScriptLogEntry(event)) {
|
||||
const error = new Error(event.text ?? '');
|
||||
|
||||
const messageHeight = error.message.split('\n').length;
|
||||
const messageLines = error.stack!.split('\n').splice(0, messageHeight);
|
||||
|
||||
const stackLines = [];
|
||||
if (event.stackTrace) {
|
||||
for (const frame of event.stackTrace.callFrames) {
|
||||
// Note we need to add `1` because the values are 0-indexed.
|
||||
stackLines.push(
|
||||
` at ${frame.functionName || '<anonymous>'} (${frame.url}:${
|
||||
frame.lineNumber + 1
|
||||
}:${frame.columnNumber + 1})`
|
||||
);
|
||||
if (stackLines.length >= Error.stackTraceLimit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
error.stack = [...messageLines, ...stackLines].join('\n');
|
||||
this.emit(PageEvent.PageError, error);
|
||||
} else {
|
||||
debugError(
|
||||
`Unhandled LogEntry with type "${event.type}", text "${event.text}" and level "${event.level}"`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#onDialog(event: Bidi.BrowsingContext.UserPromptOpenedParameters): void {
|
||||
const frame = this.frame(event.context);
|
||||
if (!frame) {
|
||||
return;
|
||||
}
|
||||
const type = validateDialogType(event.type);
|
||||
|
||||
const dialog = new BidiDialog(
|
||||
frame.context(),
|
||||
type,
|
||||
event.message,
|
||||
event.defaultValue
|
||||
);
|
||||
this.emit(PageEvent.Dialog, dialog);
|
||||
}
|
||||
|
||||
getNavigationResponse(id?: string | null): BidiHTTPResponse | null {
|
||||
return this.#networkManager.getNavigationResponse(id);
|
||||
}
|
||||
|
||||
override isClosed(): boolean {
|
||||
return this.#closedDeferred.finished();
|
||||
}
|
||||
|
||||
override async close(options?: {runBeforeUnload?: boolean}): Promise<void> {
|
||||
if (this.#closedDeferred.finished()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#closedDeferred.reject(new TargetCloseError('Page closed!'));
|
||||
this.#networkManager.dispose();
|
||||
|
||||
await this.#connection.send('browsingContext.close', {
|
||||
context: this.mainFrame()._id,
|
||||
promptUnload: options?.runBeforeUnload ?? false,
|
||||
});
|
||||
|
||||
this.emit(PageEvent.Close, undefined);
|
||||
this.removeAllListeners();
|
||||
}
|
||||
|
||||
override async reload(
|
||||
options: WaitForOptions = {}
|
||||
): Promise<BidiHTTPResponse | null> {
|
||||
const {
|
||||
waitUntil = 'load',
|
||||
timeout: ms = this._timeoutSettings.navigationTimeout(),
|
||||
} = options;
|
||||
|
||||
const [readiness, networkIdle] = getBiDiReadinessState(waitUntil);
|
||||
|
||||
const result$ = zip(
|
||||
from(
|
||||
this.#connection.send('browsingContext.reload', {
|
||||
context: this.mainFrame()._id,
|
||||
wait: readiness,
|
||||
})
|
||||
),
|
||||
...(networkIdle !== null
|
||||
? [
|
||||
this.waitForNetworkIdle$({
|
||||
timeout: ms,
|
||||
concurrency: networkIdle === 'networkidle2' ? 2 : 0,
|
||||
idleTime: NETWORK_IDLE_TIME,
|
||||
}),
|
||||
]
|
||||
: [])
|
||||
).pipe(
|
||||
map(([{result}]) => {
|
||||
return result;
|
||||
}),
|
||||
raceWith(timeout(ms), from(this.#closedDeferred.valueOrThrow())),
|
||||
rewriteNavigationError(this.url(), ms)
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(result$);
|
||||
return this.getNavigationResponse(result.navigation);
|
||||
}
|
||||
|
||||
override setDefaultNavigationTimeout(timeout: number): void {
|
||||
this._timeoutSettings.setDefaultNavigationTimeout(timeout);
|
||||
}
|
||||
|
||||
override setDefaultTimeout(timeout: number): void {
|
||||
this._timeoutSettings.setDefaultTimeout(timeout);
|
||||
}
|
||||
|
||||
override getDefaultTimeout(): number {
|
||||
return this._timeoutSettings.timeout();
|
||||
}
|
||||
|
||||
override isJavaScriptEnabled(): boolean {
|
||||
return this.#cdpEmulationManager.javascriptEnabled;
|
||||
}
|
||||
|
||||
override async setGeolocation(options: GeolocationOptions): Promise<void> {
|
||||
return await this.#cdpEmulationManager.setGeolocation(options);
|
||||
}
|
||||
|
||||
override async setJavaScriptEnabled(enabled: boolean): Promise<void> {
|
||||
return await this.#cdpEmulationManager.setJavaScriptEnabled(enabled);
|
||||
}
|
||||
|
||||
override async emulateMediaType(type?: string): Promise<void> {
|
||||
return await this.#cdpEmulationManager.emulateMediaType(type);
|
||||
}
|
||||
|
||||
override async emulateCPUThrottling(factor: number | null): Promise<void> {
|
||||
return await this.#cdpEmulationManager.emulateCPUThrottling(factor);
|
||||
}
|
||||
|
||||
override async emulateMediaFeatures(
|
||||
features?: MediaFeature[]
|
||||
): Promise<void> {
|
||||
return await this.#cdpEmulationManager.emulateMediaFeatures(features);
|
||||
}
|
||||
|
||||
override async emulateTimezone(timezoneId?: string): Promise<void> {
|
||||
return await this.#cdpEmulationManager.emulateTimezone(timezoneId);
|
||||
}
|
||||
|
||||
override async emulateIdleState(overrides?: {
|
||||
isUserActive: boolean;
|
||||
isScreenUnlocked: boolean;
|
||||
}): Promise<void> {
|
||||
return await this.#cdpEmulationManager.emulateIdleState(overrides);
|
||||
}
|
||||
|
||||
override async emulateVisionDeficiency(
|
||||
type?: Protocol.Emulation.SetEmulatedVisionDeficiencyRequest['type']
|
||||
): Promise<void> {
|
||||
return await this.#cdpEmulationManager.emulateVisionDeficiency(type);
|
||||
}
|
||||
|
||||
override async setViewport(viewport: Viewport): Promise<void> {
|
||||
if (!this.#browsingContext.supportsCdp()) {
|
||||
await this.#emulationManager.emulateViewport(viewport);
|
||||
this.#viewport = viewport;
|
||||
return;
|
||||
}
|
||||
const needsReload =
|
||||
await this.#cdpEmulationManager.emulateViewport(viewport);
|
||||
this.#viewport = viewport;
|
||||
if (needsReload) {
|
||||
await this.reload();
|
||||
}
|
||||
}
|
||||
|
||||
override viewport(): Viewport | null {
|
||||
return this.#viewport;
|
||||
}
|
||||
|
||||
override async pdf(options: PDFOptions = {}): Promise<Buffer> {
|
||||
const {timeout: ms = this._timeoutSettings.timeout(), path = undefined} =
|
||||
options;
|
||||
const {
|
||||
printBackground: background,
|
||||
margin,
|
||||
landscape,
|
||||
width,
|
||||
height,
|
||||
pageRanges: ranges,
|
||||
scale,
|
||||
preferCSSPageSize,
|
||||
} = parsePDFOptions(options, 'cm');
|
||||
const pageRanges = ranges ? ranges.split(', ') : [];
|
||||
const {result} = await firstValueFrom(
|
||||
from(
|
||||
this.#connection.send('browsingContext.print', {
|
||||
context: this.mainFrame()._id,
|
||||
background,
|
||||
margin,
|
||||
orientation: landscape ? 'landscape' : 'portrait',
|
||||
page: {
|
||||
width,
|
||||
height,
|
||||
},
|
||||
pageRanges,
|
||||
scale,
|
||||
shrinkToFit: !preferCSSPageSize,
|
||||
})
|
||||
).pipe(raceWith(timeout(ms)))
|
||||
);
|
||||
|
||||
const buffer = Buffer.from(result.data, 'base64');
|
||||
|
||||
await this._maybeWriteBufferToFile(path, buffer);
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
override async createPDFStream(
|
||||
options?: PDFOptions | undefined
|
||||
): Promise<Readable> {
|
||||
const buffer = await this.pdf(options);
|
||||
try {
|
||||
const {Readable} = await import('stream');
|
||||
return Readable.from(buffer);
|
||||
} catch (error) {
|
||||
if (error instanceof TypeError) {
|
||||
throw new Error(
|
||||
'Can only pass a file path in a Node-like environment.'
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
override async _screenshot(
|
||||
options: Readonly<ScreenshotOptions>
|
||||
): Promise<string> {
|
||||
const {clip, type, captureBeyondViewport, quality} = options;
|
||||
if (options.omitBackground !== undefined && options.omitBackground) {
|
||||
throw new UnsupportedOperation(`BiDi does not support 'omitBackground'.`);
|
||||
}
|
||||
if (options.optimizeForSpeed !== undefined && options.optimizeForSpeed) {
|
||||
throw new UnsupportedOperation(
|
||||
`BiDi does not support 'optimizeForSpeed'.`
|
||||
);
|
||||
}
|
||||
if (options.fromSurface !== undefined && !options.fromSurface) {
|
||||
throw new UnsupportedOperation(`BiDi does not support 'fromSurface'.`);
|
||||
}
|
||||
if (clip !== undefined && clip.scale !== undefined && clip.scale !== 1) {
|
||||
throw new UnsupportedOperation(
|
||||
`BiDi does not support 'scale' in 'clip'.`
|
||||
);
|
||||
}
|
||||
|
||||
let box: BoundingBox | undefined;
|
||||
if (clip) {
|
||||
if (captureBeyondViewport) {
|
||||
box = clip;
|
||||
} else {
|
||||
// The clip is always with respect to the document coordinates, so we
|
||||
// need to convert this to viewport coordinates when we aren't capturing
|
||||
// beyond the viewport.
|
||||
const [pageLeft, pageTop] = await this.evaluate(() => {
|
||||
if (!window.visualViewport) {
|
||||
throw new Error('window.visualViewport is not supported.');
|
||||
}
|
||||
return [
|
||||
window.visualViewport.pageLeft,
|
||||
window.visualViewport.pageTop,
|
||||
] as const;
|
||||
});
|
||||
box = {
|
||||
...clip,
|
||||
x: clip.x - pageLeft,
|
||||
y: clip.y - pageTop,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
result: {data},
|
||||
} = await this.#connection.send('browsingContext.captureScreenshot', {
|
||||
context: this.mainFrame()._id,
|
||||
origin: captureBeyondViewport ? 'document' : 'viewport',
|
||||
format: {
|
||||
type: `image/${type}`,
|
||||
...(quality !== undefined ? {quality: quality / 100} : {}),
|
||||
},
|
||||
...(box ? {clip: {type: 'box', ...box}} : {}),
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
override async createCDPSession(): Promise<CDPSession> {
|
||||
const {sessionId} = await this.mainFrame()
|
||||
.context()
|
||||
.cdpSession.send('Target.attachToTarget', {
|
||||
targetId: this.mainFrame()._id,
|
||||
flatten: true,
|
||||
});
|
||||
return new CdpSessionWrapper(this.mainFrame().context(), sessionId);
|
||||
}
|
||||
|
||||
override async bringToFront(): Promise<void> {
|
||||
await this.#connection.send('browsingContext.activate', {
|
||||
context: this.mainFrame()._id,
|
||||
});
|
||||
}
|
||||
|
||||
override async evaluateOnNewDocument<
|
||||
Params extends unknown[],
|
||||
Func extends (...args: Params) => unknown = (...args: Params) => unknown,
|
||||
>(
|
||||
pageFunction: Func | string,
|
||||
...args: Params
|
||||
): Promise<NewDocumentScriptEvaluation> {
|
||||
const expression = evaluationExpression(pageFunction, ...args);
|
||||
const {result} = await this.#connection.send('script.addPreloadScript', {
|
||||
functionDeclaration: expression,
|
||||
contexts: [this.mainFrame()._id],
|
||||
});
|
||||
|
||||
return {identifier: result.script};
|
||||
}
|
||||
|
||||
override async removeScriptToEvaluateOnNewDocument(
|
||||
id: string
|
||||
): Promise<void> {
|
||||
await this.#connection.send('script.removePreloadScript', {
|
||||
script: id,
|
||||
});
|
||||
}
|
||||
|
||||
override async exposeFunction<Args extends unknown[], Ret>(
|
||||
name: string,
|
||||
pptrFunction:
|
||||
| ((...args: Args) => Awaitable<Ret>)
|
||||
| {default: (...args: Args) => Awaitable<Ret>}
|
||||
): Promise<void> {
|
||||
return await this.mainFrame().exposeFunction(
|
||||
name,
|
||||
'default' in pptrFunction ? pptrFunction.default : pptrFunction
|
||||
);
|
||||
}
|
||||
|
||||
override isDragInterceptionEnabled(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
override async setCacheEnabled(enabled?: boolean): Promise<void> {
|
||||
// TODO: handle CDP-specific cases such as mprach.
|
||||
await this._client().send('Network.setCacheDisabled', {
|
||||
cacheDisabled: !enabled,
|
||||
});
|
||||
}
|
||||
|
||||
override isServiceWorkerBypassed(): never {
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
|
||||
override target(): BiDiPageTarget {
|
||||
return this.#target;
|
||||
}
|
||||
|
||||
override waitForFileChooser(): never {
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
|
||||
override workers(): never {
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
|
||||
override setRequestInterception(): never {
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
|
||||
override setDragInterception(): never {
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
|
||||
override setBypassServiceWorker(): never {
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
|
||||
override setOfflineMode(): never {
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
|
||||
override emulateNetworkConditions(): never {
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
|
||||
override cookies(): never {
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
|
||||
override setCookie(): never {
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
|
||||
override deleteCookie(): never {
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
|
||||
override removeExposedFunction(): never {
|
||||
// TODO: Quick win?
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
|
||||
override authenticate(): never {
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
|
||||
override setExtraHTTPHeaders(): never {
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
|
||||
override metrics(): never {
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
|
||||
override async goBack(
|
||||
options: WaitForOptions = {}
|
||||
): Promise<HTTPResponse | null> {
|
||||
return await this.#go(-1, options);
|
||||
}
|
||||
|
||||
override async goForward(
|
||||
options: WaitForOptions = {}
|
||||
): Promise<HTTPResponse | null> {
|
||||
return await this.#go(+1, options);
|
||||
}
|
||||
|
||||
async #go(
|
||||
delta: number,
|
||||
options: WaitForOptions
|
||||
): Promise<HTTPResponse | null> {
|
||||
try {
|
||||
const result = await Promise.all([
|
||||
this.waitForNavigation(options),
|
||||
this.#connection.send('browsingContext.traverseHistory', {
|
||||
delta,
|
||||
context: this.mainFrame()._id,
|
||||
}),
|
||||
]);
|
||||
return result[0];
|
||||
} catch (err) {
|
||||
// TODO: waitForNavigation should be cancelled if an error happens.
|
||||
if (isErrorLike(err)) {
|
||||
if (err.message.includes('no such history entry')) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
override waitForDevicePrompt(): never {
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function evaluationExpression(fun: Function | string, ...args: unknown[]) {
|
||||
return `() => {${evaluationString(fun, ...args)}}`;
|
||||
}
|
||||
Reference in New Issue
Block a user