summaryrefslogtreecommitdiffstats
path: root/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Page.ts
diff options
context:
space:
mode:
Diffstat (limited to 'remote/test/puppeteer/packages/puppeteer-core/src/bidi/Page.ts')
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/Page.ts846
1 files changed, 361 insertions, 485 deletions
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Page.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Page.ts
index 053d23b63a..c662496a18 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Page.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Page.ts
@@ -4,210 +4,115 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import type {Readable} from 'stream';
-
-import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+import * 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 {firstValueFrom, from, raceWith} 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 type {
+ MediaFeature,
+ GeolocationOptions,
+ PageEvents,
+} from '../api/Page.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 {EmulationManager} from '../cdp/EmulationManager.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 {Cookie, CookieParam, CookieSameSite} from '../common/Cookie.js';
+import type {DeleteCookiesRequest} from '../common/Cookie.js';
+import {UnsupportedOperation} from '../common/Errors.js';
+import {EventEmitter} from '../common/EventEmitter.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 {evaluationString, parsePDFOptions, timeout} 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 {bubble} from '../util/decorators.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 type {BidiCdpSession} from './CDPSession.js';
+import type {BrowsingContext} from './core/BrowsingContext.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';
+import {rewriteNavigationError} from './util.js';
+import type {BidiWebWorker} from './WebWorker.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,
+ static from(
browserContext: BidiBrowserContext,
- target: BiDiPageTarget
- ) {
- super();
- this.#browsingContext = browsingContext;
- this.#browserContext = browserContext;
- this.#target = target;
- this.#connection = browsingContext.connection;
+ browsingContext: BrowsingContext
+ ): BidiPage {
+ const page = new BidiPage(browserContext, browsingContext);
+ page.#initialize();
+ return page;
+ }
- for (const [event, subscriber] of this.#browsingContextEvents) {
- this.#browsingContext.on(event, subscriber);
- }
+ @bubble()
+ accessor trustedEmitter = new EventEmitter<PageEvents>();
- this.#networkManager = new BidiNetworkManager(this.#connection, this);
+ readonly #browserContext: BidiBrowserContext;
+ readonly #frame: BidiFrame;
+ #viewport: Viewport | null = null;
+ readonly #workers = new Set<BidiWebWorker>();
- for (const [event, subscriber] of this.#subscribedEvents) {
- this.#connection.on(event, subscriber);
- }
+ readonly keyboard: BidiKeyboard;
+ readonly mouse: BidiMouse;
+ readonly touchscreen: BidiTouchscreen;
+ readonly accessibility: Accessibility;
+ readonly tracing: Tracing;
+ readonly coverage: Coverage;
+ readonly #cdpEmulationManager: EmulationManager;
- for (const [event, subscriber] of this.#networkManagerEvents) {
- // TODO: remove any
- this.#networkManager.on(event, subscriber as any);
- }
+ _client(): BidiCdpSession {
+ return this.#frame.client;
+ }
- const frame = new BidiFrame(
- this,
- this.#browsingContext,
- this._timeoutSettings,
- this.#browsingContext.parent
- );
- this.#frameTree.addFrame(frame);
- this.emit(PageEvent.FrameAttached, frame);
+ private constructor(
+ browserContext: BidiBrowserContext,
+ browsingContext: BrowsingContext
+ ) {
+ super();
+ this.#browserContext = browserContext;
+ this.#frame = BidiFrame.from(this, browsingContext);
- // 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);
+ this.#cdpEmulationManager = new EmulationManager(this.#frame.client);
+ this.accessibility = new Accessibility(this.#frame.client);
+ this.tracing = new Tracing(this.#frame.client);
+ this.coverage = new Coverage(this.#frame.client);
+ this.keyboard = new BidiKeyboard(this);
+ this.mouse = new BidiMouse(this);
+ this.touchscreen = new BidiTouchscreen(this);
}
- /**
- * @internal
- */
- get connection(): BidiConnection {
- return this.#connection;
+ #initialize() {
+ this.#frame.browsingContext.on('closed', () => {
+ this.trustedEmitter.emit(PageEvent.Close, undefined);
+ this.trustedEmitter.removeAllListeners();
+ });
+
+ this.trustedEmitter.on(PageEvent.WorkerCreated, worker => {
+ this.#workers.add(worker as BidiWebWorker);
+ });
+ this.trustedEmitter.on(PageEvent.WorkerDestroyed, worker => {
+ this.#workers.delete(worker as BidiWebWorker);
+ });
}
override async setUserAgent(
@@ -234,46 +139,15 @@ export class BidiPage extends Page {
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(), {
+ const response = await this.#frame.client.send('Runtime.queryObjects', {
+ prototypeObjectId: prototypeHandle.id,
+ });
+ return this.#frame.mainRealm().createHandle({
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();
}
@@ -283,14 +157,9 @@ export class BidiPage extends Page {
}
override mainFrame(): BidiFrame {
- const mainFrame = this.#frameTree.getMainFrame();
- assert(mainFrame, 'Requesting main frame too early!');
- return mainFrame;
+ return this.#frame;
}
- /**
- * @internal
- */
async focusedFrame(): Promise<BidiFrame> {
using frame = await this.mainFrame()
.isolatedRealm()
@@ -310,216 +179,38 @@ export class BidiPage extends Page {
}
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 frames = [this.#frame];
+ for (const frame of frames) {
+ frames.push(...frame.childFrames());
}
- 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);
+ return frames;
}
override isClosed(): boolean {
- return this.#closedDeferred.finished();
+ return this.#frame.detached;
}
override async close(options?: {runBeforeUnload?: boolean}): Promise<void> {
- if (this.#closedDeferred.finished()) {
+ try {
+ await this.#frame.browsingContext.close(options?.runBeforeUnload);
+ } catch {
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 [response] = await Promise.all([
+ this.#frame.waitForNavigation(options),
+ this.#frame.browsingContext.reload(),
+ ]).catch(
+ rewriteNavigationError(
+ this.url(),
+ options.timeout ?? this._timeoutSettings.navigationTimeout()
+ )
);
-
- const result = await firstValueFrom(result$);
- return this.getNavigationResponse(result.navigation);
+ return response;
}
override setDefaultNavigationTimeout(timeout: number): void {
@@ -578,8 +269,19 @@ export class BidiPage extends Page {
}
override async setViewport(viewport: Viewport): Promise<void> {
- if (!this.#browsingContext.supportsCdp()) {
- await this.#emulationManager.emulateViewport(viewport);
+ if (!this.browser().cdpSupported) {
+ await this.#frame.browsingContext.setViewport({
+ viewport:
+ viewport.width && viewport.height
+ ? {
+ width: viewport.width,
+ height: viewport.height,
+ }
+ : null,
+ devicePixelRatio: viewport.deviceScaleFactor
+ ? viewport.deviceScaleFactor
+ : null,
+ });
this.#viewport = viewport;
return;
}
@@ -609,10 +311,9 @@ export class BidiPage extends Page {
preferCSSPageSize,
} = parsePDFOptions(options, 'cm');
const pageRanges = ranges ? ranges.split(', ') : [];
- const {result} = await firstValueFrom(
+ const data = await firstValueFrom(
from(
- this.#connection.send('browsingContext.print', {
- context: this.mainFrame()._id,
+ this.#frame.browsingContext.print({
background,
margin,
orientation: landscape ? 'landscape' : 'portrait',
@@ -627,7 +328,7 @@ export class BidiPage extends Page {
).pipe(raceWith(timeout(ms)))
);
- const buffer = Buffer.from(result.data, 'base64');
+ const buffer = Buffer.from(data, 'base64');
await this._maybeWriteBufferToFile(path, buffer);
@@ -636,19 +337,15 @@ export class BidiPage extends Page {
override async createPDFStream(
options?: PDFOptions | undefined
- ): Promise<Readable> {
+ ): Promise<ReadableStream<Uint8Array>> {
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;
- }
+
+ return new ReadableStream({
+ start(controller) {
+ controller.enqueue(buffer);
+ controller.close();
+ },
+ });
}
override async _screenshot(
@@ -697,10 +394,7 @@ export class BidiPage extends Page {
}
}
- const {
- result: {data},
- } = await this.#connection.send('browsingContext.captureScreenshot', {
- context: this.mainFrame()._id,
+ const data = await this.#frame.browsingContext.captureScreenshot({
origin: captureBeyondViewport ? 'document' : 'viewport',
format: {
type: `image/${type}`,
@@ -712,19 +406,11 @@ export class BidiPage extends Page {
}
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);
+ return await this.#frame.createCDPSession();
}
override async bringToFront(): Promise<void> {
- await this.#connection.send('browsingContext.activate', {
- context: this.mainFrame()._id,
- });
+ await this.#frame.browsingContext.activate();
}
override async evaluateOnNewDocument<
@@ -735,20 +421,16 @@ export class BidiPage extends Page {
...args: Params
): Promise<NewDocumentScriptEvaluation> {
const expression = evaluationExpression(pageFunction, ...args);
- const {result} = await this.#connection.send('script.addPreloadScript', {
- functionDeclaration: expression,
- contexts: [this.mainFrame()._id],
- });
+ const script =
+ await this.#frame.browsingContext.addPreloadScript(expression);
- return {identifier: result.script};
+ return {identifier: script};
}
override async removeScriptToEvaluateOnNewDocument(
id: string
): Promise<void> {
- await this.#connection.send('script.removePreloadScript', {
- script: id,
- });
+ await this.#frame.browsingContext.removePreloadScript(id);
}
override async exposeFunction<Args extends unknown[], Ret>(
@@ -774,20 +456,37 @@ export class BidiPage extends Page {
});
}
+ override async cookies(...urls: string[]): Promise<Cookie[]> {
+ const normalizedUrls = (urls.length ? urls : [this.url()]).map(url => {
+ return new URL(url);
+ });
+
+ const cookies = await this.#frame.browsingContext.getCookies();
+ return cookies
+ .map(cookie => {
+ return bidiToPuppeteerCookie(cookie);
+ })
+ .filter(cookie => {
+ return normalizedUrls.some(url => {
+ return testUrlMatchCookie(cookie, url);
+ });
+ });
+ }
+
override isServiceWorkerBypassed(): never {
throw new UnsupportedOperation();
}
- override target(): BiDiPageTarget {
- return this.#target;
+ override target(): never {
+ throw new UnsupportedOperation();
}
override waitForFileChooser(): never {
throw new UnsupportedOperation();
}
- override workers(): never {
- throw new UnsupportedOperation();
+ override workers(): BidiWebWorker[] {
+ return [...this.#workers];
}
override setRequestInterception(): never {
@@ -810,21 +509,98 @@ export class BidiPage extends Page {
throw new UnsupportedOperation();
}
- override cookies(): never {
- throw new UnsupportedOperation();
- }
+ override async setCookie(...cookies: CookieParam[]): Promise<void> {
+ const pageURL = this.url();
+ const pageUrlStartsWithHTTP = pageURL.startsWith('http');
+ for (const cookie of cookies) {
+ let cookieUrl = cookie.url || '';
+ if (!cookieUrl && pageUrlStartsWithHTTP) {
+ cookieUrl = pageURL;
+ }
+ assert(
+ cookieUrl !== 'about:blank',
+ `Blank page can not have cookie "${cookie.name}"`
+ );
+ assert(
+ !String.prototype.startsWith.call(cookieUrl || '', 'data:'),
+ `Data URL page can not have cookie "${cookie.name}"`
+ );
- override setCookie(): never {
- throw new UnsupportedOperation();
+ const normalizedUrl = URL.canParse(cookieUrl)
+ ? new URL(cookieUrl)
+ : undefined;
+
+ const domain = cookie.domain ?? normalizedUrl?.hostname;
+ assert(
+ domain !== undefined,
+ `At least one of the url and domain needs to be specified`
+ );
+
+ const bidiCookie: Bidi.Storage.PartialCookie = {
+ domain: 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)}
+ : {}),
+ ...(cookie.expires !== undefined ? {expiry: cookie.expires} : {}),
+ // Chrome-specific properties.
+ ...cdpSpecificCookiePropertiesFromPuppeteerToBidi(
+ cookie,
+ 'sameParty',
+ 'sourceScheme',
+ 'priority',
+ 'url'
+ ),
+ };
+
+ if (cookie.partitionKey !== undefined) {
+ await this.browserContext().userContext.setCookie(
+ bidiCookie,
+ cookie.partitionKey
+ );
+ } else {
+ await this.#frame.browsingContext.setCookie(bidiCookie);
+ }
+ }
}
- override deleteCookie(): never {
- throw new UnsupportedOperation();
+ override async deleteCookie(
+ ...cookies: DeleteCookiesRequest[]
+ ): Promise<void> {
+ await Promise.all(
+ cookies.map(async deleteCookieRequest => {
+ const cookieUrl = deleteCookieRequest.url ?? this.url();
+ const normalizedUrl = URL.canParse(cookieUrl)
+ ? new URL(cookieUrl)
+ : undefined;
+
+ const domain = deleteCookieRequest.domain ?? normalizedUrl?.hostname;
+ assert(
+ domain !== undefined,
+ `At least one of the url and domain needs to be specified`
+ );
+
+ const filter = {
+ domain: domain,
+ name: deleteCookieRequest.name,
+ ...(deleteCookieRequest.path !== undefined
+ ? {path: deleteCookieRequest.path}
+ : {}),
+ };
+ await this.#frame.browsingContext.deleteCookie(filter);
+ })
+ );
}
- override removeExposedFunction(): never {
- // TODO: Quick win?
- throw new UnsupportedOperation();
+ override async removeExposedFunction(name: string): Promise<void> {
+ await this.#frame.removeExposedFunction(name);
}
override authenticate(): never {
@@ -848,7 +624,7 @@ export class BidiPage extends Page {
override async goForward(
options: WaitForOptions = {}
): Promise<HTTPResponse | null> {
- return await this.#go(+1, options);
+ return await this.#go(1, options);
}
async #go(
@@ -856,22 +632,19 @@ export class BidiPage extends Page {
options: WaitForOptions
): Promise<HTTPResponse | null> {
try {
- const result = await Promise.all([
+ const [response] = await Promise.all([
this.waitForNavigation(options),
- this.#connection.send('browsingContext.traverseHistory', {
- delta,
- context: this.mainFrame()._id,
- }),
+ this.#frame.browsingContext.traverseHistory(delta),
]);
- return result[0];
- } catch (err) {
+ return response;
+ } catch (error) {
// TODO: waitForNavigation should be cancelled if an error happens.
- if (isErrorLike(err)) {
- if (err.message.includes('no such history entry')) {
+ if (isErrorLike(error)) {
+ if (error.message.includes('no such history entry')) {
return null;
}
}
- throw err;
+ throw error;
}
}
@@ -880,34 +653,137 @@ export class BidiPage extends Page {
}
}
-function isConsoleLogEntry(
- event: Bidi.Log.Entry
-): event is Bidi.Log.ConsoleLogEntry {
- return event.type === 'console';
+function evaluationExpression(fun: Function | string, ...args: unknown[]) {
+ return `() => {${evaluationString(fun, ...args)}}`;
}
-function isJavaScriptLogEntry(
- event: Bidi.Log.Entry
-): event is Bidi.Log.JavascriptLogEntry {
- return event.type === 'javascript';
+/**
+ * Check domains match.
+ * According to cookies spec, this check should match subdomains as well, but CDP
+ * implementation does not do that, so this method matches only the exact domains, not
+ * what is written in the spec:
+ * https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.3
+ */
+function testUrlMatchCookieHostname(
+ cookie: Cookie,
+ normalizedUrl: URL
+): boolean {
+ const cookieDomain = cookie.domain.toLowerCase();
+ const urlHostname = normalizedUrl.hostname.toLowerCase();
+ return cookieDomain === urlHostname;
}
-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,
- });
+/**
+ * Check paths match.
+ * Spec: https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.4
+ */
+function testUrlMatchCookiePath(cookie: Cookie, normalizedUrl: URL): boolean {
+ const uriPath = normalizedUrl.pathname;
+ const cookiePath = cookie.path;
+
+ if (uriPath === cookiePath) {
+ // The cookie-path and the request-path are identical.
+ return true;
+ }
+ if (uriPath.startsWith(cookiePath)) {
+ // The cookie-path is a prefix of the request-path.
+ if (cookiePath.endsWith('/')) {
+ // The last character of the cookie-path is %x2F ("/").
+ return true;
+ }
+ if (uriPath[cookiePath.length] === '/') {
+ // The first character of the request-path that is not included in the cookie-path
+ // is a %x2F ("/") character.
+ return true;
}
}
- return stackTraceLocations;
+ return false;
}
-function evaluationExpression(fun: Function | string, ...args: unknown[]) {
- return `() => {${evaluationString(fun, ...args)}}`;
+/**
+ * Checks the cookie matches the URL according to the spec:
+ */
+function testUrlMatchCookie(cookie: Cookie, url: URL): boolean {
+ const normalizedUrl = new URL(url);
+ assert(cookie !== undefined);
+ if (!testUrlMatchCookieHostname(cookie, normalizedUrl)) {
+ return false;
+ }
+ return testUrlMatchCookiePath(cookie, normalizedUrl);
+}
+
+function bidiToPuppeteerCookie(bidiCookie: Bidi.Network.Cookie): Cookie {
+ return {
+ name: bidiCookie.name,
+ // Presents binary value as base64 string.
+ value: bidiCookie.value.value,
+ domain: bidiCookie.domain,
+ path: bidiCookie.path,
+ size: bidiCookie.size,
+ httpOnly: bidiCookie.httpOnly,
+ secure: bidiCookie.secure,
+ sameSite: convertCookiesSameSiteBiDiToCdp(bidiCookie.sameSite),
+ expires: bidiCookie.expiry ?? -1,
+ session: bidiCookie.expiry === undefined || bidiCookie.expiry <= 0,
+ // Extending with CDP-specific properties with `goog:` prefix.
+ ...cdpSpecificCookiePropertiesFromBidiToPuppeteer(
+ bidiCookie,
+ 'sameParty',
+ 'sourceScheme',
+ 'partitionKey',
+ 'partitionKeyOpaque',
+ 'priority'
+ ),
+ };
+}
+
+const CDP_SPECIFIC_PREFIX = 'goog:';
+
+/**
+ * Gets CDP-specific properties from the BiDi cookie and returns them as a new object.
+ */
+function cdpSpecificCookiePropertiesFromBidiToPuppeteer(
+ bidiCookie: Bidi.Network.Cookie,
+ ...propertyNames: Array<keyof Cookie>
+): Partial<Cookie> {
+ const result: Partial<Cookie> = {};
+ for (const property of propertyNames) {
+ if (bidiCookie[CDP_SPECIFIC_PREFIX + property] !== undefined) {
+ result[property] = bidiCookie[CDP_SPECIFIC_PREFIX + property];
+ }
+ }
+ return result;
+}
+
+/**
+ * Gets CDP-specific properties from the cookie, adds CDP-specific prefixes and returns
+ * them as a new object which can be used in BiDi.
+ */
+function cdpSpecificCookiePropertiesFromPuppeteerToBidi(
+ cookieParam: CookieParam,
+ ...propertyNames: Array<keyof CookieParam>
+): Record<string, unknown> {
+ const result: Record<string, unknown> = {};
+ for (const property of propertyNames) {
+ if (cookieParam[property] !== undefined) {
+ result[CDP_SPECIFIC_PREFIX + property] = cookieParam[property];
+ }
+ }
+ return result;
+}
+
+function convertCookiesSameSiteBiDiToCdp(
+ sameSite: Bidi.Network.SameSite | undefined
+): CookieSameSite {
+ return sameSite === 'strict' ? 'Strict' : sameSite === 'lax' ? 'Lax' : 'None';
+}
+
+function convertCookiesSameSiteCdpToBiDi(
+ sameSite: CookieSameSite | undefined
+): Bidi.Network.SameSite {
+ return sameSite === 'Strict'
+ ? Bidi.Network.SameSite.Strict
+ : sameSite === 'Lax'
+ ? Bidi.Network.SameSite.Lax
+ : Bidi.Network.SameSite.None;
}