2022-09-21 06:10:50 +00:00
|
|
|
/**
|
|
|
|
* Copyright 2022 Google Inc. All rights reserved.
|
|
|
|
*
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
* limitations under the License.
|
|
|
|
*/
|
|
|
|
|
2023-03-27 09:39:40 +00:00
|
|
|
import type {Readable} from 'stream';
|
|
|
|
|
2023-02-20 12:00:29 +00:00
|
|
|
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
|
|
|
|
2023-03-15 16:51:34 +00:00
|
|
|
import {HTTPResponse} from '../../api/HTTPResponse.js';
|
2023-03-10 15:59:02 +00:00
|
|
|
import {
|
|
|
|
Page as PageBase,
|
|
|
|
PageEmittedEvents,
|
2023-03-28 11:02:59 +00:00
|
|
|
ScreenshotOptions,
|
2023-03-10 15:59:02 +00:00
|
|
|
WaitForOptions,
|
|
|
|
} from '../../api/Page.js';
|
2023-03-24 08:35:45 +00:00
|
|
|
import {isErrorLike} from '../../util/ErrorLike.js';
|
2023-02-20 12:00:29 +00:00
|
|
|
import {ConsoleMessage, ConsoleMessageLocation} from '../ConsoleMessage.js';
|
2023-03-20 13:00:13 +00:00
|
|
|
import {Handler} from '../EventEmitter.js';
|
2023-03-27 09:39:40 +00:00
|
|
|
import {PDFOptions} from '../PDFOptions.js';
|
2023-02-28 10:10:14 +00:00
|
|
|
import {EvaluateFunc, HandleFor} from '../types.js';
|
2023-03-28 11:02:59 +00:00
|
|
|
import {debugError, waitWithTimeout} from '../util.js';
|
2023-02-15 23:09:31 +00:00
|
|
|
|
2023-02-28 10:10:14 +00:00
|
|
|
import {Context, getBidiHandle} from './Context.js';
|
2023-02-15 23:09:31 +00:00
|
|
|
import {BidiSerializer} from './Serializer.js';
|
2023-02-15 10:29:18 +00:00
|
|
|
|
2022-09-21 06:10:50 +00:00
|
|
|
/**
|
|
|
|
* @internal
|
|
|
|
*/
|
|
|
|
export class Page extends PageBase {
|
2023-02-28 10:10:14 +00:00
|
|
|
#context: Context;
|
2023-03-20 13:00:13 +00:00
|
|
|
#subscribedEvents = new Map<string, Handler<any>>([
|
|
|
|
['log.entryAdded', this.#onLogEntryAdded.bind(this)],
|
|
|
|
['browsingContext.load', this.#onLoad.bind(this)],
|
|
|
|
['browsingContext.domContentLoaded', this.#onDOMLoad.bind(this)],
|
|
|
|
]) as Map<Bidi.Session.SubscribeParametersEvent, Handler>;
|
2022-09-21 06:10:50 +00:00
|
|
|
|
2023-02-28 10:10:14 +00:00
|
|
|
constructor(context: Context) {
|
2022-09-21 06:10:50 +00:00
|
|
|
super();
|
2023-02-28 10:10:14 +00:00
|
|
|
this.#context = context;
|
2023-02-20 12:00:29 +00:00
|
|
|
|
2023-03-24 08:35:45 +00:00
|
|
|
this.#context.connection
|
|
|
|
.send('session.subscribe', {
|
|
|
|
events: [
|
|
|
|
...this.#subscribedEvents.keys(),
|
|
|
|
] as Bidi.Session.SubscribeParameters['events'],
|
|
|
|
contexts: [this.#context.id],
|
|
|
|
})
|
|
|
|
.catch(error => {
|
|
|
|
if (isErrorLike(error) && !error.message.includes('Target closed')) {
|
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
});
|
2023-02-20 12:00:29 +00:00
|
|
|
|
2023-03-20 13:00:13 +00:00
|
|
|
for (const [event, subscriber] of this.#subscribedEvents) {
|
|
|
|
this.#context.on(event, subscriber);
|
|
|
|
}
|
2023-02-20 12:00:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#onLogEntryAdded(event: Bidi.Log.LogEntry): void {
|
|
|
|
if (isConsoleLogEntry(event)) {
|
|
|
|
const args = event.args.map(arg => {
|
2023-02-28 10:10:14 +00:00
|
|
|
return getBidiHandle(this.#context, arg);
|
2023-02-20 12:00:29 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
const text = args
|
|
|
|
.reduce((value, arg) => {
|
|
|
|
const parsedValue = arg.isPrimitiveValue
|
2023-02-28 10:10:14 +00:00
|
|
|
? BidiSerializer.deserialize(arg.remoteValue())
|
2023-02-20 12:00:29 +00:00
|
|
|
: arg.toString();
|
|
|
|
return `${value} ${parsedValue}`;
|
|
|
|
}, '')
|
|
|
|
.slice(1);
|
|
|
|
|
|
|
|
this.emit(
|
|
|
|
PageEmittedEvents.Console,
|
|
|
|
new ConsoleMessage(
|
|
|
|
event.method as any,
|
|
|
|
text,
|
|
|
|
args,
|
|
|
|
getStackTraceLocations(event.stackTrace)
|
|
|
|
)
|
|
|
|
);
|
|
|
|
} else if (isJavaScriptLogEntry(event)) {
|
2023-03-21 09:21:48 +00:00
|
|
|
let message = event.text ?? '';
|
|
|
|
|
|
|
|
if (event.stackTrace) {
|
|
|
|
for (const callFrame of event.stackTrace.callFrames) {
|
|
|
|
const location =
|
|
|
|
callFrame.url +
|
|
|
|
':' +
|
|
|
|
callFrame.lineNumber +
|
|
|
|
':' +
|
|
|
|
callFrame.columnNumber;
|
|
|
|
const functionName = callFrame.functionName || '<anonymous>';
|
|
|
|
message += `\n at ${functionName} (${location})`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const error = new Error(message);
|
|
|
|
error.stack = ''; // Don't capture Puppeteer stacktrace.
|
|
|
|
|
|
|
|
this.emit(PageEmittedEvents.PageError, error);
|
|
|
|
} else {
|
|
|
|
debugError(
|
|
|
|
`Unhandled LogEntry with type "${event.type}", text "${event.text}" and level "${event.level}"`
|
2023-02-20 12:00:29 +00:00
|
|
|
);
|
|
|
|
}
|
2022-09-21 06:10:50 +00:00
|
|
|
}
|
|
|
|
|
2023-03-10 15:59:02 +00:00
|
|
|
#onLoad(_event: Bidi.BrowsingContext.NavigationInfo): void {
|
|
|
|
this.emit(PageEmittedEvents.Load);
|
|
|
|
}
|
|
|
|
|
2023-03-20 13:00:13 +00:00
|
|
|
#onDOMLoad(_event: Bidi.BrowsingContext.NavigationInfo): void {
|
|
|
|
this.emit(PageEmittedEvents.DOMContentLoaded);
|
|
|
|
}
|
|
|
|
|
2022-09-21 06:10:50 +00:00
|
|
|
override async close(): Promise<void> {
|
2023-03-01 10:09:17 +00:00
|
|
|
await this.#context.connection.send('session.unsubscribe', {
|
2023-03-20 13:00:13 +00:00
|
|
|
events: [...this.#subscribedEvents.keys()],
|
2023-03-01 10:09:17 +00:00
|
|
|
contexts: [this.#context.id],
|
2022-09-21 06:10:50 +00:00
|
|
|
});
|
2023-02-20 12:00:29 +00:00
|
|
|
|
2023-03-01 10:09:17 +00:00
|
|
|
await this.#context.connection.send('browsingContext.close', {
|
|
|
|
context: this.#context.id,
|
2023-02-20 12:00:29 +00:00
|
|
|
});
|
|
|
|
|
2023-03-20 13:00:13 +00:00
|
|
|
for (const [event, subscriber] of this.#subscribedEvents) {
|
|
|
|
this.#context.off(event, subscriber);
|
|
|
|
}
|
2022-09-21 06:10:50 +00:00
|
|
|
}
|
|
|
|
|
2023-02-15 10:29:18 +00:00
|
|
|
override async evaluateHandle<
|
|
|
|
Params extends unknown[],
|
|
|
|
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
|
|
|
|
>(
|
|
|
|
pageFunction: Func | string,
|
|
|
|
...args: Params
|
|
|
|
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
|
2023-02-28 10:10:14 +00:00
|
|
|
return this.#context.evaluateHandle(pageFunction, ...args);
|
2023-02-15 10:29:18 +00:00
|
|
|
}
|
|
|
|
|
2022-09-21 06:10:50 +00:00
|
|
|
override async evaluate<
|
|
|
|
Params extends unknown[],
|
|
|
|
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
|
|
|
|
>(
|
|
|
|
pageFunction: Func | string,
|
2023-02-02 14:14:28 +00:00
|
|
|
...args: Params
|
2022-09-21 06:10:50 +00:00
|
|
|
): Promise<Awaited<ReturnType<Func>>> {
|
2023-02-28 10:10:14 +00:00
|
|
|
return this.#context.evaluate(pageFunction, ...args);
|
2022-09-21 06:10:50 +00:00
|
|
|
}
|
2023-03-10 15:59:02 +00:00
|
|
|
|
|
|
|
override async goto(
|
|
|
|
url: string,
|
|
|
|
options?: WaitForOptions & {
|
|
|
|
referer?: string | undefined;
|
|
|
|
referrerPolicy?: string | undefined;
|
|
|
|
}
|
|
|
|
): Promise<HTTPResponse | null> {
|
|
|
|
return this.#context.goto(url, options);
|
|
|
|
}
|
|
|
|
|
|
|
|
override url(): string {
|
|
|
|
return this.#context.url();
|
|
|
|
}
|
|
|
|
|
|
|
|
override setDefaultNavigationTimeout(timeout: number): void {
|
|
|
|
this.#context._timeoutSettings.setDefaultNavigationTimeout(timeout);
|
|
|
|
}
|
|
|
|
|
|
|
|
override setDefaultTimeout(timeout: number): void {
|
|
|
|
this.#context._timeoutSettings.setDefaultTimeout(timeout);
|
|
|
|
}
|
2023-03-20 13:00:13 +00:00
|
|
|
|
|
|
|
override async setContent(
|
|
|
|
html: string,
|
|
|
|
options: WaitForOptions = {}
|
|
|
|
): Promise<void> {
|
|
|
|
await this.#context.setContent(html, options);
|
|
|
|
}
|
|
|
|
|
|
|
|
override async content(): Promise<string> {
|
|
|
|
return await this.evaluate(() => {
|
|
|
|
let retVal = '';
|
|
|
|
if (document.doctype) {
|
|
|
|
retVal = new XMLSerializer().serializeToString(document.doctype);
|
|
|
|
}
|
|
|
|
if (document.documentElement) {
|
|
|
|
retVal += document.documentElement.outerHTML;
|
|
|
|
}
|
|
|
|
return retVal;
|
|
|
|
});
|
|
|
|
}
|
2023-03-27 09:39:40 +00:00
|
|
|
|
|
|
|
override async pdf(options: PDFOptions = {}): Promise<Buffer> {
|
|
|
|
const {path = undefined} = options;
|
|
|
|
const params = this._getPDFOptions(options);
|
|
|
|
const {result} = await waitWithTimeout(
|
|
|
|
this.#context.connection.send('browsingContext.print', {
|
|
|
|
context: this.#context._contextId,
|
|
|
|
background: params.printBackground,
|
|
|
|
margin: params.margin,
|
|
|
|
orientation: params.landscape ? 'landscape' : 'portrait',
|
|
|
|
page: {
|
|
|
|
width: params.width,
|
|
|
|
height: params.height,
|
|
|
|
},
|
|
|
|
pageRanges: params.pageRanges.split(', '),
|
|
|
|
scale: params.scale,
|
|
|
|
shrinkToFit: !params.preferCSSPageSize,
|
|
|
|
}),
|
|
|
|
'browsingContext.print',
|
|
|
|
params.timeout
|
|
|
|
);
|
|
|
|
|
|
|
|
const buffer = Buffer.from(result.data, 'base64');
|
|
|
|
|
2023-03-28 11:02:59 +00:00
|
|
|
await this._maybeWriteBufferToFile(path, buffer);
|
2023-03-27 09:39:40 +00:00
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
2023-03-28 11:02:59 +00:00
|
|
|
|
|
|
|
override screenshot(
|
|
|
|
options: ScreenshotOptions & {encoding: 'base64'}
|
|
|
|
): Promise<string>;
|
|
|
|
override screenshot(
|
|
|
|
options?: ScreenshotOptions & {encoding?: 'binary'}
|
|
|
|
): never;
|
|
|
|
override async screenshot(
|
|
|
|
options: ScreenshotOptions = {}
|
|
|
|
): Promise<Buffer | string> {
|
|
|
|
const {path = undefined, encoding, ...args} = options;
|
|
|
|
if (Object.keys(args).length >= 1) {
|
|
|
|
throw new Error('BiDi only supports "encoding" and "path" options');
|
|
|
|
}
|
|
|
|
|
|
|
|
const {result} = await this.#context.connection.send(
|
|
|
|
'browsingContext.captureScreenshot',
|
|
|
|
{
|
|
|
|
context: this.#context._contextId,
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
if (encoding === 'base64') {
|
|
|
|
return result.data;
|
|
|
|
}
|
|
|
|
|
|
|
|
const buffer = Buffer.from(result.data, 'base64');
|
|
|
|
await this._maybeWriteBufferToFile(path, buffer);
|
|
|
|
|
|
|
|
return buffer;
|
|
|
|
}
|
2022-09-21 06:10:50 +00:00
|
|
|
}
|
2023-02-20 12:00:29 +00:00
|
|
|
|
|
|
|
function isConsoleLogEntry(
|
|
|
|
event: Bidi.Log.LogEntry
|
|
|
|
): event is Bidi.Log.ConsoleLogEntry {
|
|
|
|
return event.type === 'console';
|
|
|
|
}
|
|
|
|
|
|
|
|
function isJavaScriptLogEntry(
|
|
|
|
event: Bidi.Log.LogEntry
|
|
|
|
): event is Bidi.Log.JavascriptLogEntry {
|
|
|
|
return event.type === 'javascript';
|
|
|
|
}
|
|
|
|
|
2023-03-21 09:21:48 +00:00
|
|
|
function getStackTraceLocations(
|
|
|
|
stackTrace?: Bidi.Script.StackTrace
|
|
|
|
): ConsoleMessageLocation[] {
|
2023-02-20 12:00:29 +00:00
|
|
|
const stackTraceLocations: ConsoleMessageLocation[] = [];
|
|
|
|
if (stackTrace) {
|
|
|
|
for (const callFrame of stackTrace.callFrames) {
|
|
|
|
stackTraceLocations.push({
|
|
|
|
url: callFrame.url,
|
|
|
|
lineNumber: callFrame.lineNumber,
|
|
|
|
columnNumber: callFrame.columnNumber,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return stackTraceLocations;
|
|
|
|
}
|