mirror of
https://github.com/puppeteer/puppeteer
synced 2024-06-14 14:02:48 +00:00
refactor: adopt the rest of bidi/core
(#11836)
This commit is contained in:
parent
d3f00bf032
commit
e9f9f4c356
@ -19,22 +19,15 @@ import {BrowserContextEvent} from '../api/BrowserContext.js';
|
||||
import type {Page} from '../api/Page.js';
|
||||
import type {Target} from '../api/Target.js';
|
||||
import {UnsupportedOperation} from '../common/Errors.js';
|
||||
import type {Handler} from '../common/EventEmitter.js';
|
||||
import {debugError} from '../common/util.js';
|
||||
import type {Viewport} from '../common/Viewport.js';
|
||||
|
||||
import {BidiBrowserContext} from './BrowserContext.js';
|
||||
import {BrowsingContext, BrowsingContextEvent} from './BrowsingContext.js';
|
||||
import type {BidiConnection} from './Connection.js';
|
||||
import type {Browser as BrowserCore} from './core/Browser.js';
|
||||
import {Session} from './core/Session.js';
|
||||
import {UserContext} from './core/UserContext.js';
|
||||
import {
|
||||
BiDiBrowserTarget,
|
||||
BiDiBrowsingContextTarget,
|
||||
BiDiPageTarget,
|
||||
type BidiTarget,
|
||||
} from './Target.js';
|
||||
import type {UserContext} from './core/UserContext.js';
|
||||
import {BidiBrowserTarget} from './Target.js';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -89,7 +82,6 @@ export class BidiBrowser extends Browser {
|
||||
|
||||
const browser = new BidiBrowser(session.browser, opts);
|
||||
browser.#initialize();
|
||||
await browser.#getTree();
|
||||
return browser;
|
||||
}
|
||||
|
||||
@ -97,20 +89,8 @@ export class BidiBrowser extends Browser {
|
||||
#closeCallback?: BrowserCloseCallback;
|
||||
#browserCore: BrowserCore;
|
||||
#defaultViewport: Viewport | null;
|
||||
#targets = new Map<string, BidiTarget>();
|
||||
#browserContexts = new WeakMap<UserContext, BidiBrowserContext>();
|
||||
#browserTarget: BiDiBrowserTarget;
|
||||
|
||||
#connectionEventHandlers = new Map<
|
||||
Bidi.BrowsingContextEvent['method'],
|
||||
Handler<any>
|
||||
>([
|
||||
['browsingContext.contextCreated', this.#onContextCreated.bind(this)],
|
||||
['browsingContext.contextDestroyed', this.#onContextDestroyed.bind(this)],
|
||||
['browsingContext.domContentLoaded', this.#onContextDomLoaded.bind(this)],
|
||||
['browsingContext.fragmentNavigated', this.#onContextNavigation.bind(this)],
|
||||
['browsingContext.navigationStarted', this.#onContextNavigation.bind(this)],
|
||||
]);
|
||||
#target = new BidiBrowserTarget(this);
|
||||
|
||||
private constructor(browserCore: BrowserCore, opts: BidiBrowserOptions) {
|
||||
super();
|
||||
@ -118,13 +98,14 @@ export class BidiBrowser extends Browser {
|
||||
this.#closeCallback = opts.closeCallback;
|
||||
this.#browserCore = browserCore;
|
||||
this.#defaultViewport = opts.defaultViewport;
|
||||
this.#browserTarget = new BiDiBrowserTarget(this);
|
||||
for (const context of this.#browserCore.userContexts) {
|
||||
this.#createBrowserContext(context);
|
||||
}
|
||||
}
|
||||
|
||||
#initialize() {
|
||||
// Initializing existing contexts.
|
||||
for (const userContext of this.#browserCore.userContexts) {
|
||||
this.#createBrowserContext(userContext);
|
||||
}
|
||||
|
||||
this.#browserCore.once('disconnected', () => {
|
||||
this.emit(BrowserEvent.Disconnected, undefined);
|
||||
});
|
||||
@ -132,10 +113,6 @@ export class BidiBrowser extends Browser {
|
||||
this.#browserCore.dispose('Browser process exited.', true);
|
||||
this.connection.dispose();
|
||||
});
|
||||
|
||||
for (const [eventName, handler] of this.#connectionEventHandlers) {
|
||||
this.connection.on(eventName, handler);
|
||||
}
|
||||
}
|
||||
|
||||
get #browserName() {
|
||||
@ -145,85 +122,31 @@ export class BidiBrowser extends Browser {
|
||||
return this.#browserCore.session.capabilities.browserVersion;
|
||||
}
|
||||
|
||||
get cdpSupported(): boolean {
|
||||
return !this.#browserName.toLocaleLowerCase().includes('firefox');
|
||||
}
|
||||
|
||||
override userAgent(): never {
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
|
||||
#createBrowserContext(userContext: UserContext) {
|
||||
const browserContext = new BidiBrowserContext(this, userContext, {
|
||||
const browserContext = BidiBrowserContext.from(this, userContext, {
|
||||
defaultViewport: this.#defaultViewport,
|
||||
});
|
||||
this.#browserContexts.set(userContext, browserContext);
|
||||
return browserContext;
|
||||
}
|
||||
|
||||
#onContextDomLoaded(event: Bidi.BrowsingContext.Info) {
|
||||
const target = this.#targets.get(event.context);
|
||||
if (target) {
|
||||
browserContext.on(BrowserContextEvent.TargetCreated, target => {
|
||||
this.emit(BrowserEvent.TargetCreated, target);
|
||||
});
|
||||
browserContext.on(BrowserContextEvent.TargetChanged, target => {
|
||||
this.emit(BrowserEvent.TargetChanged, target);
|
||||
target.browserContext().emit(BrowserContextEvent.TargetChanged, target);
|
||||
}
|
||||
}
|
||||
|
||||
#onContextNavigation(event: Bidi.BrowsingContext.NavigationInfo) {
|
||||
const target = this.#targets.get(event.context);
|
||||
if (target) {
|
||||
this.emit(BrowserEvent.TargetChanged, target);
|
||||
target.browserContext().emit(BrowserContextEvent.TargetChanged, target);
|
||||
}
|
||||
}
|
||||
|
||||
#onContextCreated(event: Bidi.BrowsingContext.ContextCreated['params']) {
|
||||
const context = new BrowsingContext(
|
||||
this.connection,
|
||||
event,
|
||||
this.#browserName
|
||||
);
|
||||
this.connection.registerBrowsingContexts(context);
|
||||
const browserContext =
|
||||
event.userContext === UserContext.DEFAULT
|
||||
? this.defaultBrowserContext()
|
||||
: this.browserContexts().find(browserContext => {
|
||||
return browserContext.id === event.userContext;
|
||||
});
|
||||
if (!browserContext) {
|
||||
throw new Error('Missing browser contexts');
|
||||
}
|
||||
const target = !context.parent
|
||||
? new BiDiPageTarget(browserContext, context)
|
||||
: new BiDiBrowsingContextTarget(browserContext, context);
|
||||
this.#targets.set(event.context, target);
|
||||
|
||||
this.emit(BrowserEvent.TargetCreated, target);
|
||||
target.browserContext().emit(BrowserContextEvent.TargetCreated, target);
|
||||
|
||||
if (context.parent) {
|
||||
const topLevel = this.connection.getTopLevelContext(context.parent);
|
||||
topLevel.emit(BrowsingContextEvent.Created, context);
|
||||
}
|
||||
}
|
||||
|
||||
async #getTree(): Promise<void> {
|
||||
const {result} = await this.connection.send('browsingContext.getTree', {});
|
||||
for (const context of result.contexts) {
|
||||
this.#onContextCreated(context);
|
||||
}
|
||||
}
|
||||
|
||||
async #onContextDestroyed(
|
||||
event: Bidi.BrowsingContext.ContextDestroyed['params']
|
||||
) {
|
||||
const context = this.connection.getBrowsingContext(event.context);
|
||||
const topLevelContext = this.connection.getTopLevelContext(event.context);
|
||||
topLevelContext.emit(BrowsingContextEvent.Destroyed, context);
|
||||
const target = this.#targets.get(event.context);
|
||||
const page = await target?.page();
|
||||
await page?.close().catch(debugError);
|
||||
this.#targets.delete(event.context);
|
||||
if (target) {
|
||||
});
|
||||
browserContext.on(BrowserContextEvent.TargetDestroyed, target => {
|
||||
this.emit(BrowserEvent.TargetDestroyed, target);
|
||||
target.browserContext().emit(BrowserContextEvent.TargetDestroyed, target);
|
||||
}
|
||||
});
|
||||
|
||||
return browserContext;
|
||||
}
|
||||
|
||||
get connection(): BidiConnection {
|
||||
@ -236,9 +159,6 @@ export class BidiBrowser extends Browser {
|
||||
}
|
||||
|
||||
override async close(): Promise<void> {
|
||||
for (const [eventName, handler] of this.#connectionEventHandlers) {
|
||||
this.connection.off(eventName, handler);
|
||||
}
|
||||
if (this.connection.closed) {
|
||||
return;
|
||||
}
|
||||
@ -255,7 +175,7 @@ export class BidiBrowser extends Browser {
|
||||
}
|
||||
|
||||
override get connected(): boolean {
|
||||
return !this.#browserCore.disposed;
|
||||
return !this.#browserCore.disconnected;
|
||||
}
|
||||
|
||||
override process(): ChildProcess | null {
|
||||
@ -288,19 +208,16 @@ export class BidiBrowser extends Browser {
|
||||
}
|
||||
|
||||
override targets(): Target[] {
|
||||
return [this.#browserTarget, ...Array.from(this.#targets.values())];
|
||||
return [
|
||||
this.#target,
|
||||
...this.browserContexts().flatMap(context => {
|
||||
return context.targets();
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
_getTargetById(id: string): BidiTarget {
|
||||
const target = this.#targets.get(id);
|
||||
if (!target) {
|
||||
throw new Error('Target not found');
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
override target(): Target {
|
||||
return this.#browserTarget;
|
||||
override target(): BidiBrowserTarget {
|
||||
return this.#target;
|
||||
}
|
||||
|
||||
override async disconnect(): Promise<void> {
|
||||
|
@ -6,17 +6,20 @@
|
||||
|
||||
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
||||
|
||||
import {BrowserContext} from '../api/BrowserContext.js';
|
||||
import type {Page} from '../api/Page.js';
|
||||
import {BrowserContext, BrowserContextEvent} from '../api/BrowserContext.js';
|
||||
import {PageEvent, type Page} from '../api/Page.js';
|
||||
import type {Target} from '../api/Target.js';
|
||||
import {UnsupportedOperation} from '../common/Errors.js';
|
||||
import {debugError} from '../common/util.js';
|
||||
import type {Viewport} from '../common/Viewport.js';
|
||||
|
||||
import type {BidiBrowser} from './Browser.js';
|
||||
import type {BidiConnection} from './Connection.js';
|
||||
import type {BrowsingContext} from './core/BrowsingContext.js';
|
||||
import {UserContext} from './core/UserContext.js';
|
||||
import type {BidiPage} from './Page.js';
|
||||
import type {BidiFrame} from './Frame.js';
|
||||
import {BidiPage} from './Page.js';
|
||||
import {BidiPageTarget} from './Target.js';
|
||||
import {BidiFrameTarget} from './Target.js';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -29,10 +32,25 @@ export interface BidiBrowserContextOptions {
|
||||
* @internal
|
||||
*/
|
||||
export class BidiBrowserContext extends BrowserContext {
|
||||
#browser: BidiBrowser;
|
||||
#connection: BidiConnection;
|
||||
#defaultViewport: Viewport | null;
|
||||
#userContext: UserContext;
|
||||
static from(
|
||||
browser: BidiBrowser,
|
||||
userContext: UserContext,
|
||||
options: BidiBrowserContextOptions
|
||||
): BidiBrowserContext {
|
||||
const context = new BidiBrowserContext(browser, userContext, options);
|
||||
context.#initialize();
|
||||
return context;
|
||||
}
|
||||
|
||||
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, BidiFrameTarget>]
|
||||
>();
|
||||
|
||||
constructor(
|
||||
browser: BidiBrowser,
|
||||
@ -41,36 +59,78 @@ export class BidiBrowserContext extends BrowserContext {
|
||||
) {
|
||||
super();
|
||||
this.#browser = browser;
|
||||
this.#userContext = userContext;
|
||||
this.#connection = this.#browser.connection;
|
||||
this.userContext = userContext;
|
||||
this.#defaultViewport = options.defaultViewport;
|
||||
}
|
||||
|
||||
override targets(): Target[] {
|
||||
return this.#browser.targets().filter(target => {
|
||||
return target.browserContext() === this;
|
||||
#initialize() {
|
||||
// Create targets for existing browsing contexts.
|
||||
for (const browsingContext of this.userContext.browsingContexts) {
|
||||
this.#createPage(browsingContext);
|
||||
}
|
||||
|
||||
this.userContext.on('browsingcontext', ({browsingContext}) => {
|
||||
this.#createPage(browsingContext);
|
||||
});
|
||||
}
|
||||
|
||||
get connection(): BidiConnection {
|
||||
return this.#connection;
|
||||
#createPage(browsingContext: BrowsingContext): BidiPage {
|
||||
const page = BidiPage.from(this, browsingContext);
|
||||
this.#pages.set(browsingContext, page);
|
||||
page.on(PageEvent.Close, () => {
|
||||
this.#pages.delete(browsingContext);
|
||||
});
|
||||
|
||||
// -- Target stuff starts here --
|
||||
const pageTarget = new BidiPageTarget(page);
|
||||
const frameTargets = new Map();
|
||||
this.#targets.set(page, [pageTarget, frameTargets]);
|
||||
page.on(PageEvent.FrameAttached, frame => {
|
||||
const bidiFrame = frame as BidiFrame;
|
||||
const target = new BidiFrameTarget(bidiFrame);
|
||||
frameTargets.set(bidiFrame, target);
|
||||
this.emit(BrowserContextEvent.TargetCreated, target);
|
||||
});
|
||||
page.on(PageEvent.FrameNavigated, frame => {
|
||||
const bidiFrame = frame as BidiFrame;
|
||||
const target = frameTargets.get(bidiFrame);
|
||||
// If there is no target, then this is the page's frame.
|
||||
if (target === undefined) {
|
||||
this.emit(BrowserContextEvent.TargetChanged, pageTarget);
|
||||
} else {
|
||||
this.emit(BrowserContextEvent.TargetChanged, target);
|
||||
}
|
||||
});
|
||||
page.on(PageEvent.FrameDetached, frame => {
|
||||
const bidiFrame = frame as BidiFrame;
|
||||
const target = frameTargets.get(bidiFrame);
|
||||
if (target === undefined) {
|
||||
return;
|
||||
}
|
||||
frameTargets.delete(bidiFrame);
|
||||
this.emit(BrowserContextEvent.TargetDestroyed, target);
|
||||
});
|
||||
page.on(PageEvent.Close, () => {
|
||||
this.#targets.delete(page);
|
||||
this.emit(BrowserContextEvent.TargetDestroyed, pageTarget);
|
||||
});
|
||||
this.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> {
|
||||
const {result} = await this.#connection.send('browsingContext.create', {
|
||||
type: Bidi.BrowsingContext.CreateType.Tab,
|
||||
userContext: this.#userContext.id,
|
||||
});
|
||||
const target = this.#browser._getTargetById(result.context);
|
||||
|
||||
// TODO: once BiDi has some concept matching BrowserContext, the newly
|
||||
// created contexts should get automatically assigned to the right
|
||||
// BrowserContext. For now, we assume that only explicitly created pages go
|
||||
// to the current BrowserContext. Otherwise, the contexts get assigned to
|
||||
// the default BrowserContext by the Browser.
|
||||
target._setBrowserContext(this);
|
||||
|
||||
const page = await target.page();
|
||||
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');
|
||||
}
|
||||
@ -91,7 +151,7 @@ export class BidiBrowserContext extends BrowserContext {
|
||||
}
|
||||
|
||||
try {
|
||||
await this.#userContext.remove();
|
||||
await this.userContext.remove();
|
||||
} catch (error) {
|
||||
debugError(error);
|
||||
}
|
||||
@ -102,18 +162,13 @@ export class BidiBrowserContext extends BrowserContext {
|
||||
}
|
||||
|
||||
override async pages(): Promise<BidiPage[]> {
|
||||
const results = await Promise.all(
|
||||
[...this.targets()].map(t => {
|
||||
return t.page();
|
||||
})
|
||||
);
|
||||
return results.filter((p): p is BidiPage => {
|
||||
return p !== null;
|
||||
return [...this.userContext.browsingContexts].map(context => {
|
||||
return this.#pages.get(context)!;
|
||||
});
|
||||
}
|
||||
|
||||
override isIncognito(): boolean {
|
||||
return this.#userContext.id !== UserContext.DEFAULT;
|
||||
return this.userContext.id !== UserContext.DEFAULT;
|
||||
}
|
||||
|
||||
override overridePermissions(): never {
|
||||
@ -125,9 +180,9 @@ export class BidiBrowserContext extends BrowserContext {
|
||||
}
|
||||
|
||||
override get id(): string | undefined {
|
||||
if (this.#userContext.id === UserContext.DEFAULT) {
|
||||
if (this.userContext.id === UserContext.DEFAULT) {
|
||||
return undefined;
|
||||
}
|
||||
return this.#userContext.id;
|
||||
return this.userContext.id;
|
||||
}
|
||||
}
|
||||
|
@ -1,110 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2024 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
||||
import type ProtocolMapping from 'devtools-protocol/types/protocol-mapping.js';
|
||||
|
||||
import type {CDPSession} from '../api/CDPSession.js';
|
||||
import type {EventType} from '../common/EventEmitter.js';
|
||||
import {debugError} from '../common/util.js';
|
||||
|
||||
import {BidiCdpSession} from './CDPSession.js';
|
||||
import type {BidiConnection} from './Connection.js';
|
||||
import {BidiRealm} from './Realm.js';
|
||||
|
||||
/**
|
||||
* Internal events that the BrowsingContext class emits.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace BrowsingContextEvent {
|
||||
/**
|
||||
* Emitted on the top-level context, when a descendant context is created.
|
||||
*/
|
||||
export const Created = Symbol('BrowsingContext.created');
|
||||
/**
|
||||
* Emitted on the top-level context, when a descendant context or the
|
||||
* top-level context itself is destroyed.
|
||||
*/
|
||||
export const Destroyed = Symbol('BrowsingContext.destroyed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export interface BrowsingContextEvents extends Record<EventType, unknown> {
|
||||
[BrowsingContextEvent.Created]: BrowsingContext;
|
||||
[BrowsingContextEvent.Destroyed]: BrowsingContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class BrowsingContext extends BidiRealm {
|
||||
#id: string;
|
||||
#url: string;
|
||||
#cdpSession: CDPSession;
|
||||
#parent?: string | null;
|
||||
#browserName = '';
|
||||
|
||||
constructor(
|
||||
connection: BidiConnection,
|
||||
info: Bidi.BrowsingContext.Info,
|
||||
browserName: string
|
||||
) {
|
||||
super(connection);
|
||||
this.#id = info.context;
|
||||
this.#url = info.url;
|
||||
this.#parent = info.parent;
|
||||
this.#browserName = browserName;
|
||||
this.#cdpSession = new BidiCdpSession(this, undefined);
|
||||
|
||||
this.on('browsingContext.domContentLoaded', this.#updateUrl.bind(this));
|
||||
this.on('browsingContext.fragmentNavigated', this.#updateUrl.bind(this));
|
||||
this.on('browsingContext.load', this.#updateUrl.bind(this));
|
||||
}
|
||||
|
||||
supportsCdp(): boolean {
|
||||
return !this.#browserName.toLowerCase().includes('firefox');
|
||||
}
|
||||
|
||||
#updateUrl(info: Bidi.BrowsingContext.NavigationInfo) {
|
||||
this.#url = info.url;
|
||||
}
|
||||
|
||||
createRealmForSandbox(): BidiRealm {
|
||||
return new BidiRealm(this.connection);
|
||||
}
|
||||
|
||||
get url(): string {
|
||||
return this.#url;
|
||||
}
|
||||
|
||||
get id(): string {
|
||||
return this.#id;
|
||||
}
|
||||
|
||||
get parent(): string | undefined | null {
|
||||
return this.#parent;
|
||||
}
|
||||
|
||||
get cdpSession(): CDPSession {
|
||||
return this.#cdpSession;
|
||||
}
|
||||
|
||||
async sendCdpCommand<T extends keyof ProtocolMapping.Commands>(
|
||||
method: T,
|
||||
...paramArgs: ProtocolMapping.Commands[T]['paramsType']
|
||||
): Promise<ProtocolMapping.Commands[T]['returnType']> {
|
||||
return await this.#cdpSession.send(method, ...paramArgs);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.removeAllListeners();
|
||||
this.connection.unregisterBrowsingContexts(this.#id);
|
||||
void this.#cdpSession.detach().catch(debugError);
|
||||
}
|
||||
}
|
@ -3,7 +3,6 @@
|
||||
* Copyright 2024 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type ProtocolMapping from 'devtools-protocol/types/protocol-mapping.js';
|
||||
|
||||
import {CDPSession} from '../api/CDPSession.js';
|
||||
@ -11,44 +10,49 @@ import type {Connection as CdpConnection} from '../cdp/Connection.js';
|
||||
import {TargetCloseError, UnsupportedOperation} from '../common/Errors.js';
|
||||
import {Deferred} from '../util/Deferred.js';
|
||||
|
||||
import type {BrowsingContext} from './BrowsingContext.js';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
|
||||
export const cdpSessions = new Map<string, BidiCdpSession>();
|
||||
import type {BidiConnection} from './Connection.js';
|
||||
import type {BidiFrame} from './Frame.js';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class BidiCdpSession extends CDPSession {
|
||||
#context: BrowsingContext;
|
||||
#sessionId = Deferred.create<string>();
|
||||
#detached = false;
|
||||
static sessions = new Map<string, BidiCdpSession>();
|
||||
|
||||
constructor(context: BrowsingContext, sessionId?: string) {
|
||||
#detached = false;
|
||||
readonly #connection: BidiConnection | undefined = undefined;
|
||||
readonly #sessionId = Deferred.create<string>();
|
||||
readonly frame: BidiFrame;
|
||||
|
||||
constructor(frame: BidiFrame, sessionId?: string) {
|
||||
super();
|
||||
this.#context = context;
|
||||
if (!this.#context.supportsCdp()) {
|
||||
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);
|
||||
cdpSessions.set(sessionId, this);
|
||||
BidiCdpSession.sessions.set(sessionId, this);
|
||||
} else {
|
||||
context.connection
|
||||
.send('cdp.getSession', {
|
||||
context: context.id,
|
||||
})
|
||||
.then(session => {
|
||||
(async () => {
|
||||
try {
|
||||
const session = await connection.send('cdp.getSession', {
|
||||
context: frame._id,
|
||||
});
|
||||
this.#sessionId.resolve(session.result.session!);
|
||||
cdpSessions.set(session.result.session!, this);
|
||||
})
|
||||
.catch(err => {
|
||||
this.#sessionId.reject(err);
|
||||
});
|
||||
BidiCdpSession.sessions.set(session.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 {
|
||||
@ -59,7 +63,7 @@ export class BidiCdpSession extends CDPSession {
|
||||
method: T,
|
||||
params?: ProtocolMapping.Commands[T]['paramsType'][0]
|
||||
): Promise<ProtocolMapping.Commands[T]['returnType']> {
|
||||
if (!this.#context.supportsCdp()) {
|
||||
if (this.#connection === undefined) {
|
||||
throw new UnsupportedOperation(
|
||||
'CDP support is required for this feature. The current browser does not support CDP.'
|
||||
);
|
||||
@ -70,7 +74,7 @@ export class BidiCdpSession extends CDPSession {
|
||||
);
|
||||
}
|
||||
const session = await this.#sessionId.valueOrThrow();
|
||||
const {result} = await this.#context.connection.send('cdp.sendCommand', {
|
||||
const {result} = await this.#connection.send('cdp.sendCommand', {
|
||||
method: method,
|
||||
params: params,
|
||||
session,
|
||||
@ -79,17 +83,21 @@ export class BidiCdpSession extends CDPSession {
|
||||
}
|
||||
|
||||
override async detach(): Promise<void> {
|
||||
cdpSessions.delete(this.id());
|
||||
if (!this.#detached && this.#context.supportsCdp()) {
|
||||
await this.#context.cdpSession.send('Target.detachFromTarget', {
|
||||
if (this.#connection === undefined || this.#detached) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.frame.client.send('Target.detachFromTarget', {
|
||||
sessionId: this.id(),
|
||||
});
|
||||
} finally {
|
||||
BidiCdpSession.sessions.delete(this.id());
|
||||
this.#detached = true;
|
||||
}
|
||||
this.#detached = true;
|
||||
}
|
||||
|
||||
override id(): string {
|
||||
const val = this.#sessionId.value();
|
||||
return val instanceof Error || val === undefined ? '' : val;
|
||||
const value = this.#sessionId.value();
|
||||
return typeof value === 'string' ? value : '';
|
||||
}
|
||||
}
|
||||
|
@ -14,11 +14,10 @@ import {EventEmitter} from '../common/EventEmitter.js';
|
||||
import {debugError} from '../common/util.js';
|
||||
import {assert} from '../util/assert.js';
|
||||
|
||||
import type {BrowsingContext} from './BrowsingContext.js';
|
||||
import {cdpSessions} from './CDPSession.js';
|
||||
import {BidiCdpSession} from './CDPSession.js';
|
||||
import type {
|
||||
BidiEvents,
|
||||
Commands as BidiCommands,
|
||||
BidiEvents,
|
||||
Connection,
|
||||
} from './core/Connection.js';
|
||||
|
||||
@ -52,7 +51,6 @@ export class BidiConnection
|
||||
#timeout? = 0;
|
||||
#closed = false;
|
||||
#callbacks = new CallbackRegistry();
|
||||
#browsingContexts = new Map<string, BrowsingContext>();
|
||||
#emitters: Array<EventEmitter<any>> = [];
|
||||
|
||||
constructor(
|
||||
@ -138,12 +136,11 @@ export class BidiConnection
|
||||
return;
|
||||
case 'event':
|
||||
if (isCdpEvent(object)) {
|
||||
cdpSessions
|
||||
BidiCdpSession.sessions
|
||||
.get(object.params.session)
|
||||
?.emit(object.params.event, object.params.params);
|
||||
return;
|
||||
}
|
||||
this.#maybeEmitOnContext(object);
|
||||
// SAFETY: We know the method and parameter still match here.
|
||||
this.emit(
|
||||
object.method,
|
||||
@ -164,52 +161,6 @@ export class BidiConnection
|
||||
debugError(object);
|
||||
}
|
||||
|
||||
#maybeEmitOnContext(event: Bidi.ChromiumBidi.Event) {
|
||||
let context: BrowsingContext | undefined;
|
||||
// Context specific events
|
||||
if ('context' in event.params && event.params.context !== null) {
|
||||
context = this.#browsingContexts.get(event.params.context);
|
||||
// `log.entryAdded` specific context
|
||||
} else if (
|
||||
'source' in event.params &&
|
||||
event.params.source.context !== undefined
|
||||
) {
|
||||
context = this.#browsingContexts.get(event.params.source.context);
|
||||
}
|
||||
context?.emit(event.method, event.params);
|
||||
}
|
||||
|
||||
registerBrowsingContexts(context: BrowsingContext): void {
|
||||
this.#browsingContexts.set(context.id, context);
|
||||
}
|
||||
|
||||
getBrowsingContext(contextId: string): BrowsingContext {
|
||||
const currentContext = this.#browsingContexts.get(contextId);
|
||||
if (!currentContext) {
|
||||
throw new Error(`BrowsingContext ${contextId} does not exist.`);
|
||||
}
|
||||
return currentContext;
|
||||
}
|
||||
|
||||
getTopLevelContext(contextId: string): BrowsingContext {
|
||||
let currentContext = this.#browsingContexts.get(contextId);
|
||||
if (!currentContext) {
|
||||
throw new Error(`BrowsingContext ${contextId} does not exist.`);
|
||||
}
|
||||
while (currentContext.parent) {
|
||||
contextId = currentContext.parent;
|
||||
currentContext = this.#browsingContexts.get(contextId);
|
||||
if (!currentContext) {
|
||||
throw new Error(`BrowsingContext ${contextId} does not exist.`);
|
||||
}
|
||||
}
|
||||
return currentContext;
|
||||
}
|
||||
|
||||
unregisterBrowsingContexts(id: string): void {
|
||||
this.#browsingContexts.delete(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unbinds the connection, but keeps the transport open. Useful when the transport will
|
||||
* be reused by other connection e.g. with different protocol.
|
||||
@ -224,7 +175,6 @@ export class BidiConnection
|
||||
this.#transport.onmessage = () => {};
|
||||
this.#transport.onclose = () => {};
|
||||
|
||||
this.#browsingContexts.clear();
|
||||
this.#callbacks.clear();
|
||||
}
|
||||
|
||||
|
@ -4,40 +4,26 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
||||
|
||||
import {Dialog} from '../api/Dialog.js';
|
||||
|
||||
import type {BrowsingContext} from './BrowsingContext.js';
|
||||
import type {UserPrompt} from './core/UserPrompt.js';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class BidiDialog extends Dialog {
|
||||
#context: BrowsingContext;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
constructor(
|
||||
context: BrowsingContext,
|
||||
type: Bidi.BrowsingContext.UserPromptOpenedParameters['type'],
|
||||
message: string,
|
||||
defaultValue?: string
|
||||
) {
|
||||
super(type, message, defaultValue);
|
||||
this.#context = context;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
override async handle(options: {
|
||||
accept: boolean;
|
||||
text?: string;
|
||||
}): Promise<void> {
|
||||
await this.#context.connection.send('browsingContext.handleUserPrompt', {
|
||||
context: this.#context.id,
|
||||
await this.#prompt.handle({
|
||||
accept: options.accept,
|
||||
userText: options.text,
|
||||
});
|
||||
|
@ -6,14 +6,13 @@
|
||||
|
||||
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
||||
|
||||
import {type AutofillData, ElementHandle} from '../api/ElementHandle.js';
|
||||
import {ElementHandle, type AutofillData} from '../api/ElementHandle.js';
|
||||
import {UnsupportedOperation} from '../common/Errors.js';
|
||||
import {throwIfDisposed} from '../util/decorators.js';
|
||||
|
||||
import type {BidiFrame} from './Frame.js';
|
||||
import {BidiJSHandle} from './JSHandle.js';
|
||||
import type {BidiRealm} from './Realm.js';
|
||||
import type {Sandbox} from './Sandbox.js';
|
||||
import type {BidiFrameRealm} from './Realm.js';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -21,28 +20,28 @@ import type {Sandbox} from './Sandbox.js';
|
||||
export class BidiElementHandle<
|
||||
ElementType extends Node = Element,
|
||||
> extends ElementHandle<ElementType> {
|
||||
declare handle: BidiJSHandle<ElementType>;
|
||||
|
||||
constructor(sandbox: Sandbox, remoteValue: Bidi.Script.RemoteValue) {
|
||||
super(new BidiJSHandle(sandbox, remoteValue));
|
||||
static from<ElementType extends Node = Element>(
|
||||
value: Bidi.Script.RemoteValue,
|
||||
realm: BidiFrameRealm
|
||||
): BidiElementHandle<ElementType> {
|
||||
return new BidiElementHandle(value, realm);
|
||||
}
|
||||
|
||||
override get realm(): Sandbox {
|
||||
return this.handle.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;
|
||||
}
|
||||
|
||||
context(): BidiRealm {
|
||||
return this.handle.context();
|
||||
}
|
||||
|
||||
get isPrimitiveValue(): boolean {
|
||||
return this.handle.isPrimitiveValue;
|
||||
}
|
||||
|
||||
remoteValue(): Bidi.Script.RemoteValue {
|
||||
return this.handle.remoteValue();
|
||||
}
|
||||
@ -76,7 +75,14 @@ export class BidiElementHandle<
|
||||
})) as BidiJSHandle;
|
||||
const value = handle.remoteValue();
|
||||
if (value.type === 'window') {
|
||||
return this.frame.page().frame(value.value.context);
|
||||
return (
|
||||
this.frame
|
||||
.page()
|
||||
.frames()
|
||||
.find(frame => {
|
||||
return frame._id === value.value.context;
|
||||
}) ?? null
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -1,35 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
import type {Viewport} from '../common/Viewport.js';
|
||||
|
||||
import type {BrowsingContext} from './BrowsingContext.js';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class EmulationManager {
|
||||
#browsingContext: BrowsingContext;
|
||||
|
||||
constructor(browsingContext: BrowsingContext) {
|
||||
this.#browsingContext = browsingContext;
|
||||
}
|
||||
|
||||
async emulateViewport(viewport: Viewport): Promise<void> {
|
||||
await this.#browsingContext.connection.send('browsingContext.setViewport', {
|
||||
context: this.#browsingContext.id,
|
||||
viewport:
|
||||
viewport.width && viewport.height
|
||||
? {
|
||||
width: viewport.width,
|
||||
height: viewport.height,
|
||||
}
|
||||
: null,
|
||||
devicePixelRatio: viewport.deviceScaleFactor
|
||||
? viewport.deviceScaleFactor
|
||||
: null,
|
||||
});
|
||||
}
|
||||
}
|
@ -12,7 +12,7 @@ import {assert} from '../util/assert.js';
|
||||
import {Deferred} from '../util/Deferred.js';
|
||||
import {interpolateFunction, stringifyFunction} from '../util/Function.js';
|
||||
|
||||
import type {BidiConnection} from './Connection.js';
|
||||
import type {Connection} from './core/Connection.js';
|
||||
import {BidiDeserializer} from './Deserializer.js';
|
||||
import type {BidiFrame} from './Frame.js';
|
||||
import {BidiSerializer} from './Serializer.js';
|
||||
@ -207,8 +207,8 @@ export class ExposeableFunction<Args extends unknown[], Ret> {
|
||||
}
|
||||
};
|
||||
|
||||
get #connection(): BidiConnection {
|
||||
return this.#frame.context().connection;
|
||||
get #connection(): Connection {
|
||||
return this.#frame.page().browser().connection;
|
||||
}
|
||||
|
||||
get #channelArguments() {
|
||||
|
@ -4,17 +4,17 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
||||
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
||||
|
||||
import type {Observable} from '../../third_party/rxjs/rxjs.js';
|
||||
import {
|
||||
combineLatest,
|
||||
first,
|
||||
firstValueFrom,
|
||||
forkJoin,
|
||||
from,
|
||||
map,
|
||||
merge,
|
||||
of,
|
||||
raceWith,
|
||||
zip,
|
||||
switchMap,
|
||||
} from '../../third_party/rxjs/rxjs.js';
|
||||
import type {CDPSession} from '../api/CDPSession.js';
|
||||
import type {ElementHandle} from '../api/ElementHandle.js';
|
||||
@ -25,85 +25,205 @@ import {
|
||||
type WaitForOptions,
|
||||
} from '../api/Frame.js';
|
||||
import type {WaitForSelectorOptions} from '../api/Page.js';
|
||||
import {UnsupportedOperation} from '../common/Errors.js';
|
||||
import {PageEvent} from '../api/Page.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, NodeFor} from '../common/types.js';
|
||||
import {
|
||||
fromEmitterEvent,
|
||||
NETWORK_IDLE_TIME,
|
||||
timeout,
|
||||
UTILITY_WORLD_NAME,
|
||||
} from '../common/util.js';
|
||||
import {Deferred} from '../util/Deferred.js';
|
||||
import {disposeSymbol} from '../util/disposable.js';
|
||||
import {debugError, fromEmitterEvent, timeout} from '../common/util.js';
|
||||
|
||||
import type {BrowsingContext} from './BrowsingContext.js';
|
||||
import {BidiCdpSession} from './CDPSession.js';
|
||||
import type {BrowsingContext} from './core/BrowsingContext.js';
|
||||
import {BidiDeserializer} from './Deserializer.js';
|
||||
import {BidiDialog} from './Dialog.js';
|
||||
import {ExposeableFunction} from './ExposedFunction.js';
|
||||
import {BidiHTTPRequest, requests} from './HTTPRequest.js';
|
||||
import type {BidiHTTPResponse} from './HTTPResponse.js';
|
||||
import {
|
||||
getBiDiLifecycleEvent,
|
||||
getBiDiReadinessState,
|
||||
rewriteNavigationError,
|
||||
} from './lifecycle.js';
|
||||
import {BidiJSHandle} from './JSHandle.js';
|
||||
import type {BidiPage} from './Page.js';
|
||||
import {
|
||||
MAIN_SANDBOX,
|
||||
PUPPETEER_SANDBOX,
|
||||
Sandbox,
|
||||
type SandboxChart,
|
||||
} from './Sandbox.js';
|
||||
import type {BidiRealm} from './Realm.js';
|
||||
import {BidiFrameRealm} from './Realm.js';
|
||||
import {rewriteNavigationError} from './util.js';
|
||||
|
||||
/**
|
||||
* Puppeteer's Frame class could be viewed as a BiDi BrowsingContext implementation
|
||||
* @internal
|
||||
*/
|
||||
export class BidiFrame extends Frame {
|
||||
#page: BidiPage;
|
||||
#context: BrowsingContext;
|
||||
#timeoutSettings: TimeoutSettings;
|
||||
#abortDeferred = Deferred.create<never>();
|
||||
#disposed = false;
|
||||
sandboxes: SandboxChart;
|
||||
override _id: string;
|
||||
static from(
|
||||
parent: BidiPage | BidiFrame,
|
||||
browsingContext: BrowsingContext
|
||||
): BidiFrame {
|
||||
const frame = new BidiFrame(parent, browsingContext);
|
||||
frame.#initialize();
|
||||
return frame;
|
||||
}
|
||||
|
||||
constructor(
|
||||
page: BidiPage,
|
||||
context: BrowsingContext,
|
||||
timeoutSettings: TimeoutSettings,
|
||||
parentId?: string | null
|
||||
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;
|
||||
|
||||
private constructor(
|
||||
parent: BidiPage | BidiFrame,
|
||||
browsingContext: BrowsingContext
|
||||
) {
|
||||
super();
|
||||
this.#page = page;
|
||||
this.#context = context;
|
||||
this.#timeoutSettings = timeoutSettings;
|
||||
this._id = this.#context.id;
|
||||
this._parentId = parentId ?? undefined;
|
||||
this.#parent = parent;
|
||||
this.browsingContext = browsingContext;
|
||||
|
||||
this.sandboxes = {
|
||||
[MAIN_SANDBOX]: new Sandbox(undefined, this, context, timeoutSettings),
|
||||
[PUPPETEER_SANDBOX]: new Sandbox(
|
||||
UTILITY_WORLD_NAME,
|
||||
this,
|
||||
context.createRealmForSandbox(),
|
||||
timeoutSettings
|
||||
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
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
override get client(): CDPSession {
|
||||
return this.context().cdpSession;
|
||||
#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) {
|
||||
void session.detach().catch(debugError);
|
||||
}
|
||||
}
|
||||
this.page().emit(PageEvent.FrameDetached, this);
|
||||
this.removeAllListeners();
|
||||
});
|
||||
|
||||
this.browsingContext.on('request', ({request}) => {
|
||||
const httpRequest = BidiHTTPRequest.from(request, this);
|
||||
request.once('success', () => {
|
||||
// SAFETY: BidiHTTPRequest will create this before here.
|
||||
this.page().emit(PageEvent.RequestFinished, httpRequest);
|
||||
});
|
||||
|
||||
request.once('error', () => {
|
||||
this.page().emit(PageEvent.RequestFailed, httpRequest);
|
||||
});
|
||||
});
|
||||
|
||||
this.browsingContext.on('navigation', ({navigation}) => {
|
||||
navigation.once('fragment', () => {
|
||||
this.page().emit(PageEvent.FrameNavigated, this);
|
||||
});
|
||||
});
|
||||
this.browsingContext.on('load', () => {
|
||||
this.page().emit(PageEvent.Load, undefined);
|
||||
});
|
||||
this.browsingContext.on('DOMContentLoaded', () => {
|
||||
this._hasStartedLoading = true;
|
||||
this.page().emit(PageEvent.DOMContentLoaded, undefined);
|
||||
this.page().emit(PageEvent.FrameNavigated, this);
|
||||
});
|
||||
|
||||
this.browsingContext.on('userprompt', ({userPrompt}) => {
|
||||
this.page().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().emit(
|
||||
PageEvent.Console,
|
||||
new ConsoleMessage(
|
||||
entry.method as any,
|
||||
text,
|
||||
args,
|
||||
getStackTraceLocations(entry.stackTrace)
|
||||
)
|
||||
);
|
||||
} 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().emit(PageEvent.PageError, error);
|
||||
} else {
|
||||
debugError(
|
||||
`Unhandled LogEntry with type "${entry.type}", text "${entry.text}" and level "${entry.level}"`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
override mainRealm(): Sandbox {
|
||||
return this.sandboxes[MAIN_SANDBOX];
|
||||
#createFrameTarget(browsingContext: BrowsingContext) {
|
||||
const frame = BidiFrame.from(this, browsingContext);
|
||||
this.#frames.set(browsingContext, frame);
|
||||
this.page().emit(PageEvent.FrameAttached, frame);
|
||||
|
||||
browsingContext.on('closed', () => {
|
||||
this.#frames.delete(browsingContext);
|
||||
});
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
override isolatedRealm(): Sandbox {
|
||||
return this.sandboxes[PUPPETEER_SANDBOX];
|
||||
get timeoutSettings(): TimeoutSettings {
|
||||
return this.page()._timeoutSettings;
|
||||
}
|
||||
|
||||
override mainRealm(): BidiRealm {
|
||||
return this.realms.default;
|
||||
}
|
||||
|
||||
override isolatedRealm(): BidiRealm {
|
||||
return this.realms.internal;
|
||||
}
|
||||
|
||||
override page(): BidiPage {
|
||||
return this.#page;
|
||||
let parent = this.#parent;
|
||||
while (parent instanceof BidiFrame) {
|
||||
parent = parent.#parent;
|
||||
}
|
||||
return parent;
|
||||
}
|
||||
|
||||
override isOOPFrame(): never {
|
||||
@ -111,15 +231,20 @@ export class BidiFrame extends Frame {
|
||||
}
|
||||
|
||||
override url(): string {
|
||||
return this.#context.url;
|
||||
return this.browsingContext.url;
|
||||
}
|
||||
|
||||
override parentFrame(): BidiFrame | null {
|
||||
return this.#page.frame(this._parentId ?? '');
|
||||
if (this.#parent instanceof BidiFrame) {
|
||||
return this.#parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
override childFrames(): BidiFrame[] {
|
||||
return this.#page.childFrames(this.#context.id);
|
||||
return [...this.browsingContext.children].map(child => {
|
||||
return this.#frames.get(child)!;
|
||||
});
|
||||
}
|
||||
|
||||
@throwIfDetached
|
||||
@ -127,40 +252,16 @@ export class BidiFrame extends Frame {
|
||||
url: string,
|
||||
options: GoToOptions = {}
|
||||
): Promise<BidiHTTPResponse | null> {
|
||||
const {
|
||||
waitUntil = 'load',
|
||||
timeout: ms = this.#timeoutSettings.navigationTimeout(),
|
||||
} = options;
|
||||
|
||||
const [readiness, networkIdle] = getBiDiReadinessState(waitUntil);
|
||||
|
||||
const result$ = zip(
|
||||
from(
|
||||
this.#context.connection.send('browsingContext.navigate', {
|
||||
context: this.#context.id,
|
||||
url,
|
||||
wait: readiness,
|
||||
})
|
||||
),
|
||||
...(networkIdle !== null
|
||||
? [
|
||||
this.#page.waitForNetworkIdle$({
|
||||
timeout: ms,
|
||||
concurrency: networkIdle === 'networkidle2' ? 2 : 0,
|
||||
idleTime: NETWORK_IDLE_TIME,
|
||||
}),
|
||||
]
|
||||
: [])
|
||||
).pipe(
|
||||
map(([{result}]) => {
|
||||
return result;
|
||||
}),
|
||||
raceWith(timeout(ms), from(this.#abortDeferred.valueOrThrow())),
|
||||
rewriteNavigationError(url, ms)
|
||||
const [response] = await Promise.all([
|
||||
this.waitForNavigation(options),
|
||||
this.browsingContext.navigate(url),
|
||||
]).catch(
|
||||
rewriteNavigationError(
|
||||
url,
|
||||
options.timeout ?? this.timeoutSettings.navigationTimeout()
|
||||
)
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(result$);
|
||||
return this.#page.getNavigationResponse(result.navigation);
|
||||
return response;
|
||||
}
|
||||
|
||||
@throwIfDetached
|
||||
@ -168,95 +269,58 @@ export class BidiFrame extends Frame {
|
||||
html: string,
|
||||
options: WaitForOptions = {}
|
||||
): Promise<void> {
|
||||
const {
|
||||
waitUntil = 'load',
|
||||
timeout: ms = this.#timeoutSettings.navigationTimeout(),
|
||||
} = options;
|
||||
|
||||
const [waitEvent, networkIdle] = getBiDiLifecycleEvent(waitUntil);
|
||||
|
||||
const result$ = zip(
|
||||
forkJoin([
|
||||
fromEmitterEvent(this.#context, waitEvent).pipe(first()),
|
||||
from(this.setFrameContent(html)),
|
||||
]).pipe(
|
||||
map(() => {
|
||||
return null;
|
||||
})
|
||||
await Promise.all([
|
||||
this.setFrameContent(html),
|
||||
firstValueFrom(
|
||||
combineLatest([
|
||||
this.#waitForLoad$(options),
|
||||
this.#waitForNetworkIdle$(options),
|
||||
])
|
||||
),
|
||||
...(networkIdle !== null
|
||||
? [
|
||||
this.#page.waitForNetworkIdle$({
|
||||
timeout: ms,
|
||||
concurrency: networkIdle === 'networkidle2' ? 2 : 0,
|
||||
idleTime: NETWORK_IDLE_TIME,
|
||||
}),
|
||||
]
|
||||
: [])
|
||||
).pipe(
|
||||
raceWith(timeout(ms), from(this.#abortDeferred.valueOrThrow())),
|
||||
rewriteNavigationError('setContent', ms)
|
||||
);
|
||||
|
||||
await firstValueFrom(result$);
|
||||
}
|
||||
|
||||
context(): BrowsingContext {
|
||||
return this.#context;
|
||||
]);
|
||||
}
|
||||
|
||||
@throwIfDetached
|
||||
override async waitForNavigation(
|
||||
options: WaitForOptions = {}
|
||||
): Promise<BidiHTTPResponse | null> {
|
||||
const {
|
||||
waitUntil = 'load',
|
||||
timeout: ms = this.#timeoutSettings.navigationTimeout(),
|
||||
} = options;
|
||||
const {timeout: ms = this.timeoutSettings.navigationTimeout()} = options;
|
||||
|
||||
const [waitUntilEvent, networkIdle] = getBiDiLifecycleEvent(waitUntil);
|
||||
|
||||
const navigation$ = merge(
|
||||
forkJoin([
|
||||
fromEmitterEvent(
|
||||
this.#context,
|
||||
Bidi.ChromiumBidi.BrowsingContext.EventNames.NavigationStarted
|
||||
).pipe(first()),
|
||||
fromEmitterEvent(this.#context, waitUntilEvent).pipe(first()),
|
||||
]),
|
||||
fromEmitterEvent(
|
||||
this.#context,
|
||||
Bidi.ChromiumBidi.BrowsingContext.EventNames.FragmentNavigated
|
||||
return await firstValueFrom(
|
||||
combineLatest([
|
||||
fromEmitterEvent(this.browsingContext, 'navigation').pipe(
|
||||
switchMap(({navigation}) => {
|
||||
return this.#waitForLoad$(options).pipe(
|
||||
raceWith(fromEmitterEvent(navigation, 'fragment')),
|
||||
map(() => {
|
||||
return navigation;
|
||||
})
|
||||
);
|
||||
})
|
||||
),
|
||||
this.#waitForNetworkIdle$(options),
|
||||
]).pipe(
|
||||
map(([navigation]) => {
|
||||
const request = navigation.request;
|
||||
if (!request) {
|
||||
return null;
|
||||
}
|
||||
const httpRequest = requests.get(request)!;
|
||||
const lastRedirect = httpRequest.redirectChain().at(-1);
|
||||
return (
|
||||
lastRedirect !== undefined ? lastRedirect : httpRequest
|
||||
).response();
|
||||
}),
|
||||
raceWith(
|
||||
timeout(ms),
|
||||
fromEmitterEvent(this.browsingContext, 'closed').pipe(
|
||||
map(() => {
|
||||
throw new TargetCloseError('Frame detached.');
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
).pipe(
|
||||
map(result => {
|
||||
if (Array.isArray(result)) {
|
||||
return {result: result[1]};
|
||||
}
|
||||
return {result};
|
||||
})
|
||||
);
|
||||
|
||||
const result$ = zip(
|
||||
navigation$,
|
||||
...(networkIdle !== null
|
||||
? [
|
||||
this.#page.waitForNetworkIdle$({
|
||||
timeout: ms,
|
||||
concurrency: networkIdle === 'networkidle2' ? 2 : 0,
|
||||
idleTime: NETWORK_IDLE_TIME,
|
||||
}),
|
||||
]
|
||||
: [])
|
||||
).pipe(
|
||||
map(([{result}]) => {
|
||||
return result;
|
||||
}),
|
||||
raceWith(timeout(ms), from(this.#abortDeferred.valueOrThrow()))
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(result$);
|
||||
return this.#page.getNavigationResponse(result.navigation);
|
||||
}
|
||||
|
||||
override waitForDevicePrompt(): never {
|
||||
@ -264,18 +328,7 @@ export class BidiFrame extends Frame {
|
||||
}
|
||||
|
||||
override get detached(): boolean {
|
||||
return this.#disposed;
|
||||
}
|
||||
|
||||
[disposeSymbol](): void {
|
||||
if (this.#disposed) {
|
||||
return;
|
||||
}
|
||||
this.#disposed = true;
|
||||
this.#abortDeferred.reject(new Error('Frame detached'));
|
||||
this.#context.dispose();
|
||||
this.sandboxes[MAIN_SANDBOX][disposeSymbol]();
|
||||
this.sandboxes[PUPPETEER_SANDBOX][disposeSymbol]();
|
||||
return this.browsingContext.closed;
|
||||
}
|
||||
|
||||
#exposedFunctions = new Map<string, ExposeableFunction<never[], unknown>>();
|
||||
@ -310,4 +363,115 @@ export class BidiFrame extends Frame {
|
||||
|
||||
return super.waitForSelector(selector, options);
|
||||
}
|
||||
|
||||
async createCDPSession(): Promise<CDPSession> {
|
||||
const {sessionId} = await this.client.send('Target.attachToTarget', {
|
||||
targetId: this._id,
|
||||
flatten: true,
|
||||
});
|
||||
return new BidiCdpSession(this, sessionId);
|
||||
}
|
||||
|
||||
@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),
|
||||
fromEmitterEvent(this.browsingContext, 'closed').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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
@ -5,107 +5,126 @@
|
||||
*/
|
||||
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
||||
|
||||
import type {Frame} from '../api/Frame.js';
|
||||
import type {CDPSession} from '../api/CDPSession.js';
|
||||
import type {
|
||||
ContinueRequestOverrides,
|
||||
ResponseForRequest,
|
||||
} from '../api/HTTPRequest.js';
|
||||
import {HTTPRequest, type ResourceType} from '../api/HTTPRequest.js';
|
||||
import {PageEvent} from '../api/Page.js';
|
||||
import {UnsupportedOperation} from '../common/Errors.js';
|
||||
|
||||
import type {BidiHTTPResponse} from './HTTPResponse.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 {
|
||||
override id: string;
|
||||
override _response: BidiHTTPResponse | null = null;
|
||||
override _redirectChain: BidiHTTPRequest[];
|
||||
_navigationId: string | null;
|
||||
|
||||
#url: string;
|
||||
#resourceType: ResourceType;
|
||||
|
||||
#method: string;
|
||||
#postData?: string;
|
||||
#headers: Record<string, string> = {};
|
||||
#initiator: Bidi.Network.Initiator;
|
||||
#frame: Frame | null;
|
||||
|
||||
constructor(
|
||||
event: Bidi.Network.BeforeRequestSentParameters,
|
||||
frame: Frame | null,
|
||||
redirectChain: BidiHTTPRequest[] = []
|
||||
) {
|
||||
super();
|
||||
|
||||
this.#url = event.request.url;
|
||||
this.#resourceType = event.initiator.type.toLowerCase() as ResourceType;
|
||||
this.#method = event.request.method;
|
||||
this.#postData = undefined;
|
||||
this.#initiator = event.initiator;
|
||||
this.#frame = frame;
|
||||
|
||||
this.id = event.request.request;
|
||||
this._redirectChain = redirectChain;
|
||||
this._navigationId = event.navigation;
|
||||
|
||||
for (const header of event.request.headers) {
|
||||
// TODO: How to handle Binary Headers
|
||||
// https://w3c.github.io/webdriver-bidi/#type-network-Header
|
||||
if (header.value.type === 'string') {
|
||||
this.#headers[header.name.toLowerCase()] = header.value.value;
|
||||
}
|
||||
}
|
||||
static from(
|
||||
bidiRequest: Request,
|
||||
frame: BidiFrame | undefined
|
||||
): BidiHTTPRequest {
|
||||
const request = new BidiHTTPRequest(bidiRequest, frame);
|
||||
request.#initialize();
|
||||
return request;
|
||||
}
|
||||
|
||||
override get client(): never {
|
||||
#redirect: BidiHTTPRequest | undefined;
|
||||
#response: BidiHTTPResponse | null = null;
|
||||
override readonly id: string;
|
||||
readonly #frame: BidiFrame | undefined;
|
||||
readonly #request: Request;
|
||||
|
||||
private constructor(request: Request, frame: BidiFrame | undefined) {
|
||||
super();
|
||||
requests.set(request, this);
|
||||
|
||||
this.#request = request;
|
||||
this.#frame = frame;
|
||||
this.id = request.id;
|
||||
}
|
||||
|
||||
override get client(): CDPSession {
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
|
||||
#initialize() {
|
||||
this.#request.on('redirect', request => {
|
||||
this.#redirect = BidiHTTPRequest.from(request, this.#frame);
|
||||
});
|
||||
this.#request.once('success', data => {
|
||||
this.#response = BidiHTTPResponse.from(data, this);
|
||||
});
|
||||
|
||||
this.#frame?.page().emit(PageEvent.Request, this);
|
||||
}
|
||||
|
||||
override url(): string {
|
||||
return this.#url;
|
||||
return this.#request.url;
|
||||
}
|
||||
|
||||
override resourceType(): ResourceType {
|
||||
return this.#resourceType;
|
||||
return this.initiator().type.toLowerCase() as ResourceType;
|
||||
}
|
||||
|
||||
override method(): string {
|
||||
return this.#method;
|
||||
return this.#request.method;
|
||||
}
|
||||
|
||||
override postData(): string | undefined {
|
||||
return this.#postData;
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
|
||||
override hasPostData(): boolean {
|
||||
return this.#postData !== undefined;
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
|
||||
override async fetchPostData(): Promise<string | undefined> {
|
||||
return this.#postData;
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
|
||||
override headers(): Record<string, string> {
|
||||
return this.#headers;
|
||||
const headers: Record<string, string> = {};
|
||||
for (const header of this.#request.headers) {
|
||||
headers[header.name.toLowerCase()] = header.value.value;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
override response(): BidiHTTPResponse | null {
|
||||
return this._response;
|
||||
return this.#response;
|
||||
}
|
||||
|
||||
override failure(): {errorText: string} | null {
|
||||
if (this.#request.error === undefined) {
|
||||
return null;
|
||||
}
|
||||
return {errorText: this.#request.error};
|
||||
}
|
||||
|
||||
override isNavigationRequest(): boolean {
|
||||
return Boolean(this._navigationId);
|
||||
return this.#request.navigation !== undefined;
|
||||
}
|
||||
|
||||
override initiator(): Bidi.Network.Initiator {
|
||||
return this.#initiator;
|
||||
return this.#request.initiator;
|
||||
}
|
||||
|
||||
override redirectChain(): BidiHTTPRequest[] {
|
||||
return this._redirectChain.slice();
|
||||
if (this.#redirect === undefined) {
|
||||
return [];
|
||||
}
|
||||
const redirects = [this.#redirect];
|
||||
for (const redirect of redirects) {
|
||||
if (redirect.#redirect !== undefined) {
|
||||
redirects.push(redirect.#redirect);
|
||||
}
|
||||
}
|
||||
return redirects;
|
||||
}
|
||||
|
||||
override enqueueInterceptAction(
|
||||
@ -115,8 +134,8 @@ export class BidiHTTPRequest extends HTTPRequest {
|
||||
void pendingHandler();
|
||||
}
|
||||
|
||||
override frame(): Frame | null {
|
||||
return this.#frame;
|
||||
override frame(): BidiFrame | null {
|
||||
return this.#frame ?? null;
|
||||
}
|
||||
|
||||
override continueRequestOverrides(): never {
|
||||
@ -157,8 +176,4 @@ export class BidiHTTPRequest extends HTTPRequest {
|
||||
): never {
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
|
||||
override failure(): never {
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
}
|
||||
|
@ -7,11 +7,10 @@ import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
||||
import type Protocol from 'devtools-protocol';
|
||||
|
||||
import type {Frame} from '../api/Frame.js';
|
||||
import {
|
||||
HTTPResponse as HTTPResponse,
|
||||
type RemoteAddress,
|
||||
} from '../api/HTTPResponse.js';
|
||||
import {HTTPResponse, type RemoteAddress} from '../api/HTTPResponse.js';
|
||||
import {PageEvent} from '../api/Page.js';
|
||||
import {UnsupportedOperation} from '../common/Errors.js';
|
||||
import {invokeAtMostOnceForArguments} from '../util/decorators.js';
|
||||
|
||||
import type {BidiHTTPRequest} from './HTTPRequest.js';
|
||||
|
||||
@ -19,62 +18,62 @@ import type {BidiHTTPRequest} from './HTTPRequest.js';
|
||||
* @internal
|
||||
*/
|
||||
export class BidiHTTPResponse extends HTTPResponse {
|
||||
#request: BidiHTTPRequest;
|
||||
#remoteAddress: RemoteAddress;
|
||||
#status: number;
|
||||
#statusText: string;
|
||||
#url: string;
|
||||
#fromCache: boolean;
|
||||
#headers: Record<string, string> = {};
|
||||
#timings: Record<string, string> | null;
|
||||
static from(
|
||||
data: Bidi.Network.ResponseData,
|
||||
request: BidiHTTPRequest
|
||||
): BidiHTTPResponse {
|
||||
const response = new BidiHTTPResponse(data, request);
|
||||
response.#initialize();
|
||||
return response;
|
||||
}
|
||||
|
||||
constructor(
|
||||
request: BidiHTTPRequest,
|
||||
{response}: Bidi.Network.ResponseCompletedParameters
|
||||
#data: Bidi.Network.ResponseData;
|
||||
#request: BidiHTTPRequest;
|
||||
|
||||
private constructor(
|
||||
data: Bidi.Network.ResponseData,
|
||||
request: BidiHTTPRequest
|
||||
) {
|
||||
super();
|
||||
this.#data = data;
|
||||
this.#request = request;
|
||||
}
|
||||
|
||||
this.#remoteAddress = {
|
||||
#initialize() {
|
||||
this.#request.frame()?.page().emit(PageEvent.Response, this);
|
||||
}
|
||||
|
||||
@invokeAtMostOnceForArguments
|
||||
override remoteAddress(): RemoteAddress {
|
||||
return {
|
||||
ip: '',
|
||||
port: -1,
|
||||
};
|
||||
|
||||
this.#url = response.url;
|
||||
this.#fromCache = response.fromCache;
|
||||
this.#status = response.status;
|
||||
this.#statusText = response.statusText;
|
||||
// TODO: File and issue with BiDi spec
|
||||
this.#timings = null;
|
||||
|
||||
// TODO: Removed once the Firefox implementation is compliant with https://w3c.github.io/webdriver-bidi/#get-the-response-data.
|
||||
for (const header of response.headers || []) {
|
||||
// TODO: How to handle Binary Headers
|
||||
// https://w3c.github.io/webdriver-bidi/#type-network-Header
|
||||
if (header.value.type === 'string') {
|
||||
this.#headers[header.name.toLowerCase()] = header.value.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override remoteAddress(): RemoteAddress {
|
||||
return this.#remoteAddress;
|
||||
}
|
||||
|
||||
override url(): string {
|
||||
return this.#url;
|
||||
return this.#data.url;
|
||||
}
|
||||
|
||||
override status(): number {
|
||||
return this.#status;
|
||||
return this.#data.status;
|
||||
}
|
||||
|
||||
override statusText(): string {
|
||||
return this.#statusText;
|
||||
return this.#data.statusText;
|
||||
}
|
||||
|
||||
override headers(): Record<string, string> {
|
||||
return this.#headers;
|
||||
const headers: Record<string, string> = {};
|
||||
// TODO: Remove once the Firefox implementation is compliant with https://w3c.github.io/webdriver-bidi/#get-the-response-data.
|
||||
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 {
|
||||
@ -82,11 +81,12 @@ export class BidiHTTPResponse extends HTTPResponse {
|
||||
}
|
||||
|
||||
override fromCache(): boolean {
|
||||
return this.#fromCache;
|
||||
return this.#data.fromCache;
|
||||
}
|
||||
|
||||
override timing(): Protocol.Network.ResourceTiming | null {
|
||||
return this.#timings as any;
|
||||
// TODO: File and issue with BiDi spec
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
|
||||
override frame(): Frame | null {
|
||||
|
@ -12,9 +12,9 @@ import {
|
||||
Mouse,
|
||||
MouseButton,
|
||||
Touchscreen,
|
||||
type KeyboardTypeOptions,
|
||||
type KeyDownOptions,
|
||||
type KeyPressOptions,
|
||||
type KeyboardTypeOptions,
|
||||
type MouseClickOptions,
|
||||
type MouseMoveOptions,
|
||||
type MouseOptions,
|
||||
@ -23,7 +23,6 @@ import {
|
||||
import {UnsupportedOperation} from '../common/Errors.js';
|
||||
import type {KeyInput} from '../common/USKeyboardLayout.js';
|
||||
|
||||
import type {BrowsingContext} from './BrowsingContext.js';
|
||||
import type {BidiPage} from './Page.js';
|
||||
|
||||
const enum InputId {
|
||||
@ -288,39 +287,33 @@ export class BidiKeyboard extends Keyboard {
|
||||
key: KeyInput,
|
||||
_options?: Readonly<KeyDownOptions>
|
||||
): Promise<void> {
|
||||
await this.#page.connection.send('input.performActions', {
|
||||
context: this.#page.mainFrame()._id,
|
||||
actions: [
|
||||
{
|
||||
type: SourceActionsType.Key,
|
||||
id: InputId.Keyboard,
|
||||
actions: [
|
||||
{
|
||||
type: ActionType.KeyDown,
|
||||
value: getBidiKeyValue(key),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
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.connection.send('input.performActions', {
|
||||
context: this.#page.mainFrame()._id,
|
||||
actions: [
|
||||
{
|
||||
type: SourceActionsType.Key,
|
||||
id: InputId.Keyboard,
|
||||
actions: [
|
||||
{
|
||||
type: ActionType.KeyUp,
|
||||
value: getBidiKeyValue(key),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
await this.#page.mainFrame().browsingContext.performActions([
|
||||
{
|
||||
type: SourceActionsType.Key,
|
||||
id: InputId.Keyboard,
|
||||
actions: [
|
||||
{
|
||||
type: ActionType.KeyUp,
|
||||
value: getBidiKeyValue(key),
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
override async press(
|
||||
@ -344,16 +337,13 @@ export class BidiKeyboard extends Keyboard {
|
||||
type: ActionType.KeyUp,
|
||||
value: getBidiKeyValue(key),
|
||||
});
|
||||
await this.#page.connection.send('input.performActions', {
|
||||
context: this.#page.mainFrame()._id,
|
||||
actions: [
|
||||
{
|
||||
type: SourceActionsType.Key,
|
||||
id: InputId.Keyboard,
|
||||
actions,
|
||||
},
|
||||
],
|
||||
});
|
||||
await this.#page.mainFrame().browsingContext.performActions([
|
||||
{
|
||||
type: SourceActionsType.Key,
|
||||
id: InputId.Keyboard,
|
||||
actions,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
override async type(
|
||||
@ -396,16 +386,13 @@ export class BidiKeyboard extends Keyboard {
|
||||
);
|
||||
}
|
||||
}
|
||||
await this.#page.connection.send('input.performActions', {
|
||||
context: this.#page.mainFrame()._id,
|
||||
actions: [
|
||||
{
|
||||
type: SourceActionsType.Key,
|
||||
id: InputId.Keyboard,
|
||||
actions,
|
||||
},
|
||||
],
|
||||
});
|
||||
await this.#page.mainFrame().browsingContext.performActions([
|
||||
{
|
||||
type: SourceActionsType.Key,
|
||||
id: InputId.Keyboard,
|
||||
actions,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
override async sendCharacter(char: string): Promise<void> {
|
||||
@ -460,19 +447,17 @@ const getBidiButton = (button: MouseButton) => {
|
||||
* @internal
|
||||
*/
|
||||
export class BidiMouse extends Mouse {
|
||||
#context: BrowsingContext;
|
||||
#page: BidiPage;
|
||||
#lastMovePoint: Point = {x: 0, y: 0};
|
||||
|
||||
constructor(context: BrowsingContext) {
|
||||
constructor(page: BidiPage) {
|
||||
super();
|
||||
this.#context = context;
|
||||
this.#page = page;
|
||||
}
|
||||
|
||||
override async reset(): Promise<void> {
|
||||
this.#lastMovePoint = {x: 0, y: 0};
|
||||
await this.#context.connection.send('input.releaseActions', {
|
||||
context: this.#context.id,
|
||||
});
|
||||
await this.#page.mainFrame().browsingContext.releaseActions();
|
||||
}
|
||||
|
||||
override async move(
|
||||
@ -502,52 +487,43 @@ export class BidiMouse extends Mouse {
|
||||
});
|
||||
// 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.#context.connection.send('input.performActions', {
|
||||
context: this.#context.id,
|
||||
actions: [
|
||||
{
|
||||
type: SourceActionsType.Pointer,
|
||||
id: InputId.Mouse,
|
||||
actions,
|
||||
},
|
||||
],
|
||||
});
|
||||
await this.#page.mainFrame().browsingContext.performActions([
|
||||
{
|
||||
type: SourceActionsType.Pointer,
|
||||
id: InputId.Mouse,
|
||||
actions,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
override async down(options: Readonly<MouseOptions> = {}): Promise<void> {
|
||||
await this.#context.connection.send('input.performActions', {
|
||||
context: this.#context.id,
|
||||
actions: [
|
||||
{
|
||||
type: SourceActionsType.Pointer,
|
||||
id: InputId.Mouse,
|
||||
actions: [
|
||||
{
|
||||
type: ActionType.PointerDown,
|
||||
button: getBidiButton(options.button ?? MouseButton.Left),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
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.#context.connection.send('input.performActions', {
|
||||
context: this.#context.id,
|
||||
actions: [
|
||||
{
|
||||
type: SourceActionsType.Pointer,
|
||||
id: InputId.Mouse,
|
||||
actions: [
|
||||
{
|
||||
type: ActionType.PointerUp,
|
||||
button: getBidiButton(options.button ?? MouseButton.Left),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
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(
|
||||
@ -582,41 +558,35 @@ export class BidiMouse extends Mouse {
|
||||
});
|
||||
}
|
||||
actions.push(pointerUpAction);
|
||||
await this.#context.connection.send('input.performActions', {
|
||||
context: this.#context.id,
|
||||
actions: [
|
||||
{
|
||||
type: SourceActionsType.Pointer,
|
||||
id: InputId.Mouse,
|
||||
actions,
|
||||
},
|
||||
],
|
||||
});
|
||||
await this.#page.mainFrame().browsingContext.performActions([
|
||||
{
|
||||
type: SourceActionsType.Pointer,
|
||||
id: InputId.Mouse,
|
||||
actions,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
override async wheel(
|
||||
options: Readonly<MouseWheelOptions> = {}
|
||||
): Promise<void> {
|
||||
await this.#context.connection.send('input.performActions', {
|
||||
context: this.#context.id,
|
||||
actions: [
|
||||
{
|
||||
type: SourceActionsType.Wheel,
|
||||
id: InputId.Wheel,
|
||||
actions: [
|
||||
{
|
||||
type: ActionType.Scroll,
|
||||
...(this.#lastMovePoint ?? {
|
||||
x: 0,
|
||||
y: 0,
|
||||
}),
|
||||
deltaX: options.deltaX ?? 0,
|
||||
deltaY: options.deltaY ?? 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
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 {
|
||||
@ -644,11 +614,11 @@ export class BidiMouse extends Mouse {
|
||||
* @internal
|
||||
*/
|
||||
export class BidiTouchscreen extends Touchscreen {
|
||||
#context: BrowsingContext;
|
||||
#page: BidiPage;
|
||||
|
||||
constructor(context: BrowsingContext) {
|
||||
constructor(page: BidiPage) {
|
||||
super();
|
||||
this.#context = context;
|
||||
this.#page = page;
|
||||
}
|
||||
|
||||
override async touchStart(
|
||||
@ -656,30 +626,27 @@ export class BidiTouchscreen extends Touchscreen {
|
||||
y: number,
|
||||
options: BidiTouchMoveOptions = {}
|
||||
): Promise<void> {
|
||||
await this.#context.connection.send('input.performActions', {
|
||||
context: this.#context.id,
|
||||
actions: [
|
||||
{
|
||||
type: SourceActionsType.Pointer,
|
||||
id: InputId.Finger,
|
||||
parameters: {
|
||||
pointerType: Bidi.Input.PointerType.Touch,
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
type: ActionType.PointerMove,
|
||||
x: Math.round(x),
|
||||
y: Math.round(y),
|
||||
origin: options.origin,
|
||||
},
|
||||
{
|
||||
type: ActionType.PointerDown,
|
||||
button: 0,
|
||||
},
|
||||
],
|
||||
await this.#page.mainFrame().browsingContext.performActions([
|
||||
{
|
||||
type: SourceActionsType.Pointer,
|
||||
id: InputId.Finger,
|
||||
parameters: {
|
||||
pointerType: Bidi.Input.PointerType.Touch,
|
||||
},
|
||||
],
|
||||
});
|
||||
actions: [
|
||||
{
|
||||
type: ActionType.PointerMove,
|
||||
x: Math.round(x),
|
||||
y: Math.round(y),
|
||||
origin: options.origin,
|
||||
},
|
||||
{
|
||||
type: ActionType.PointerDown,
|
||||
button: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
override async touchMove(
|
||||
@ -687,46 +654,40 @@ export class BidiTouchscreen extends Touchscreen {
|
||||
y: number,
|
||||
options: BidiTouchMoveOptions = {}
|
||||
): Promise<void> {
|
||||
await this.#context.connection.send('input.performActions', {
|
||||
context: this.#context.id,
|
||||
actions: [
|
||||
{
|
||||
type: SourceActionsType.Pointer,
|
||||
id: InputId.Finger,
|
||||
parameters: {
|
||||
pointerType: Bidi.Input.PointerType.Touch,
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
type: ActionType.PointerMove,
|
||||
x: Math.round(x),
|
||||
y: Math.round(y),
|
||||
origin: options.origin,
|
||||
},
|
||||
],
|
||||
await this.#page.mainFrame().browsingContext.performActions([
|
||||
{
|
||||
type: SourceActionsType.Pointer,
|
||||
id: InputId.Finger,
|
||||
parameters: {
|
||||
pointerType: Bidi.Input.PointerType.Touch,
|
||||
},
|
||||
],
|
||||
});
|
||||
actions: [
|
||||
{
|
||||
type: ActionType.PointerMove,
|
||||
x: Math.round(x),
|
||||
y: Math.round(y),
|
||||
origin: options.origin,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
override async touchEnd(): Promise<void> {
|
||||
await this.#context.connection.send('input.performActions', {
|
||||
context: this.#context.id,
|
||||
actions: [
|
||||
{
|
||||
type: SourceActionsType.Pointer,
|
||||
id: InputId.Finger,
|
||||
parameters: {
|
||||
pointerType: Bidi.Input.PointerType.Touch,
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
type: ActionType.PointerUp,
|
||||
button: 0,
|
||||
},
|
||||
],
|
||||
await this.#page.mainFrame().browsingContext.performActions([
|
||||
{
|
||||
type: SourceActionsType.Pointer,
|
||||
id: InputId.Finger,
|
||||
parameters: {
|
||||
pointerType: Bidi.Input.PointerType.Touch,
|
||||
},
|
||||
],
|
||||
});
|
||||
actions: [
|
||||
{
|
||||
type: ActionType.PointerUp,
|
||||
button: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -12,28 +12,28 @@ import {UnsupportedOperation} from '../common/Errors.js';
|
||||
|
||||
import {BidiDeserializer} from './Deserializer.js';
|
||||
import type {BidiRealm} from './Realm.js';
|
||||
import type {Sandbox} from './Sandbox.js';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class BidiJSHandle<T = unknown> extends JSHandle<T> {
|
||||
#disposed = false;
|
||||
readonly #sandbox: Sandbox;
|
||||
static from<T>(
|
||||
value: Bidi.Script.RemoteValue,
|
||||
realm: BidiRealm
|
||||
): BidiJSHandle<T> {
|
||||
return new BidiJSHandle(value, realm);
|
||||
}
|
||||
|
||||
readonly #remoteValue: Bidi.Script.RemoteValue;
|
||||
|
||||
constructor(sandbox: Sandbox, remoteValue: Bidi.Script.RemoteValue) {
|
||||
override readonly realm: BidiRealm;
|
||||
|
||||
#disposed = false;
|
||||
|
||||
constructor(value: Bidi.Script.RemoteValue, realm: BidiRealm) {
|
||||
super();
|
||||
this.#sandbox = sandbox;
|
||||
this.#remoteValue = remoteValue;
|
||||
}
|
||||
|
||||
context(): BidiRealm {
|
||||
return this.realm.environment.context();
|
||||
}
|
||||
|
||||
override get realm(): Sandbox {
|
||||
return this.#sandbox;
|
||||
this.#remoteValue = value;
|
||||
this.realm = realm;
|
||||
}
|
||||
|
||||
override get disposed(): boolean {
|
||||
@ -55,7 +55,7 @@ export class BidiJSHandle<T = unknown> extends JSHandle<T> {
|
||||
return;
|
||||
}
|
||||
this.#disposed = true;
|
||||
await this.context().destroyHandles([this]);
|
||||
await this.realm.destroyHandles([this]);
|
||||
}
|
||||
|
||||
get isPrimitiveValue(): boolean {
|
||||
|
@ -1,155 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
||||
|
||||
import {EventEmitter, EventSubscription} from '../common/EventEmitter.js';
|
||||
import {
|
||||
NetworkManagerEvent,
|
||||
type NetworkManagerEvents,
|
||||
} from '../common/NetworkManagerEvents.js';
|
||||
import {DisposableStack} from '../util/disposable.js';
|
||||
|
||||
import type {BidiConnection} from './Connection.js';
|
||||
import type {BidiFrame} from './Frame.js';
|
||||
import {BidiHTTPRequest} from './HTTPRequest.js';
|
||||
import {BidiHTTPResponse} from './HTTPResponse.js';
|
||||
import type {BidiPage} from './Page.js';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class BidiNetworkManager extends EventEmitter<NetworkManagerEvents> {
|
||||
#connection: BidiConnection;
|
||||
#page: BidiPage;
|
||||
#subscriptions = new DisposableStack();
|
||||
|
||||
#requestMap = new Map<string, BidiHTTPRequest>();
|
||||
#navigationMap = new Map<string, BidiHTTPResponse>();
|
||||
|
||||
constructor(connection: BidiConnection, page: BidiPage) {
|
||||
super();
|
||||
this.#connection = connection;
|
||||
this.#page = page;
|
||||
|
||||
// TODO: Subscribe to the Frame individually
|
||||
this.#subscriptions.use(
|
||||
new EventSubscription(
|
||||
this.#connection,
|
||||
'network.beforeRequestSent',
|
||||
this.#onBeforeRequestSent.bind(this)
|
||||
)
|
||||
);
|
||||
this.#subscriptions.use(
|
||||
new EventSubscription(
|
||||
this.#connection,
|
||||
'network.responseStarted',
|
||||
this.#onResponseStarted.bind(this)
|
||||
)
|
||||
);
|
||||
this.#subscriptions.use(
|
||||
new EventSubscription(
|
||||
this.#connection,
|
||||
'network.responseCompleted',
|
||||
this.#onResponseCompleted.bind(this)
|
||||
)
|
||||
);
|
||||
this.#subscriptions.use(
|
||||
new EventSubscription(
|
||||
this.#connection,
|
||||
'network.fetchError',
|
||||
this.#onFetchError.bind(this)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#onBeforeRequestSent(event: Bidi.Network.BeforeRequestSentParameters): void {
|
||||
const frame = this.#page.frame(event.context ?? '');
|
||||
if (!frame) {
|
||||
return;
|
||||
}
|
||||
const request = this.#requestMap.get(event.request.request);
|
||||
let upsertRequest: BidiHTTPRequest;
|
||||
if (request) {
|
||||
request._redirectChain.push(request);
|
||||
upsertRequest = new BidiHTTPRequest(event, frame, request._redirectChain);
|
||||
} else {
|
||||
upsertRequest = new BidiHTTPRequest(event, frame, []);
|
||||
}
|
||||
this.#requestMap.set(event.request.request, upsertRequest);
|
||||
this.emit(NetworkManagerEvent.Request, upsertRequest);
|
||||
}
|
||||
|
||||
#onResponseStarted(_event: Bidi.Network.ResponseStartedParameters) {}
|
||||
|
||||
#onResponseCompleted(event: Bidi.Network.ResponseCompletedParameters): void {
|
||||
const request = this.#requestMap.get(event.request.request);
|
||||
if (!request) {
|
||||
return;
|
||||
}
|
||||
const response = new BidiHTTPResponse(request, event);
|
||||
request._response = response;
|
||||
if (event.navigation) {
|
||||
this.#navigationMap.set(event.navigation, response);
|
||||
}
|
||||
if (response.fromCache()) {
|
||||
this.emit(NetworkManagerEvent.RequestServedFromCache, request);
|
||||
}
|
||||
this.emit(NetworkManagerEvent.Response, response);
|
||||
this.emit(NetworkManagerEvent.RequestFinished, request);
|
||||
}
|
||||
|
||||
#onFetchError(event: Bidi.Network.FetchErrorParameters) {
|
||||
const request = this.#requestMap.get(event.request.request);
|
||||
if (!request) {
|
||||
return;
|
||||
}
|
||||
request._failureText = event.errorText;
|
||||
this.emit(NetworkManagerEvent.RequestFailed, request);
|
||||
this.#requestMap.delete(event.request.request);
|
||||
}
|
||||
|
||||
getNavigationResponse(navigationId?: string | null): BidiHTTPResponse | null {
|
||||
if (!navigationId) {
|
||||
return null;
|
||||
}
|
||||
const response = this.#navigationMap.get(navigationId);
|
||||
|
||||
return response ?? null;
|
||||
}
|
||||
|
||||
inFlightRequestsCount(): number {
|
||||
let inFlightRequestCounter = 0;
|
||||
for (const request of this.#requestMap.values()) {
|
||||
if (!request.response() || request._failureText) {
|
||||
inFlightRequestCounter++;
|
||||
}
|
||||
}
|
||||
|
||||
return inFlightRequestCounter;
|
||||
}
|
||||
|
||||
clearMapAfterFrameDispose(frame: BidiFrame): void {
|
||||
for (const [id, request] of this.#requestMap.entries()) {
|
||||
if (request.frame() === frame) {
|
||||
this.#requestMap.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [id, response] of this.#navigationMap.entries()) {
|
||||
if (response.frame() === frame) {
|
||||
this.#navigationMap.delete(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.removeAllListeners();
|
||||
this.#requestMap.clear();
|
||||
this.#navigationMap.clear();
|
||||
this.#subscriptions.dispose();
|
||||
}
|
||||
}
|
@ -7,201 +7,93 @@
|
||||
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} 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 type {ConsoleMessageType} from '../common/ConsoleMessage.js';
|
||||
import {
|
||||
ConsoleMessage,
|
||||
type ConsoleMessageLocation,
|
||||
} from '../common/ConsoleMessage.js';
|
||||
import type {Cookie, CookieSameSite, CookieParam} from '../common/Cookie.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 {UnsupportedOperation} from '../common/Errors.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 {isErrorLike} from '../util/ErrorLike.js';
|
||||
|
||||
import type {BidiBrowser} from './Browser.js';
|
||||
import type {BidiBrowserContext} from './BrowserContext.js';
|
||||
import {BrowsingContextEvent, type BrowsingContext} from './BrowsingContext.js';
|
||||
import {BidiCdpSession} from './CDPSession.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';
|
||||
|
||||
/**
|
||||
* @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;
|
||||
|
||||
_client(): CDPSession {
|
||||
return this.mainFrame().context().cdpSession;
|
||||
static from(
|
||||
browserContext: BidiBrowserContext,
|
||||
browsingContext: BrowsingContext
|
||||
): BidiPage {
|
||||
const page = new BidiPage(browserContext, browsingContext);
|
||||
page.#initialize();
|
||||
return page;
|
||||
}
|
||||
|
||||
constructor(
|
||||
browsingContext: BrowsingContext,
|
||||
browserContext: BidiBrowserContext
|
||||
readonly #browserContext: BidiBrowserContext;
|
||||
readonly #frame: BidiFrame;
|
||||
#viewport: Viewport | null = null;
|
||||
|
||||
readonly keyboard: BidiKeyboard;
|
||||
readonly mouse: BidiMouse;
|
||||
readonly touchscreen: BidiTouchscreen;
|
||||
readonly accessibility: Accessibility;
|
||||
readonly tracing: Tracing;
|
||||
readonly coverage: Coverage;
|
||||
readonly #cdpEmulationManager: EmulationManager;
|
||||
|
||||
_client(): BidiCdpSession {
|
||||
return this.#frame.client;
|
||||
}
|
||||
|
||||
private constructor(
|
||||
browserContext: BidiBrowserContext,
|
||||
browsingContext: BrowsingContext
|
||||
) {
|
||||
super();
|
||||
this.#browsingContext = browsingContext;
|
||||
this.#browserContext = browserContext;
|
||||
this.#connection = browsingContext.connection;
|
||||
this.#frame = BidiFrame.from(this, browsingContext);
|
||||
|
||||
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);
|
||||
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.emit(PageEvent.Close, undefined);
|
||||
this.removeAllListeners();
|
||||
});
|
||||
}
|
||||
|
||||
override async setUserAgent(
|
||||
@ -228,46 +120,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();
|
||||
}
|
||||
@ -277,14 +138,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()
|
||||
@ -304,216 +160,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);
|
||||
const frames = [this.#frame];
|
||||
for (const frame of frames) {
|
||||
frames.push(...frame.childFrames());
|
||||
}
|
||||
}
|
||||
|
||||
#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 ConsoleMessageType,
|
||||
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);
|
||||
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 {
|
||||
@ -572,8 +250,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;
|
||||
}
|
||||
@ -603,10 +292,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',
|
||||
@ -621,7 +309,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);
|
||||
|
||||
@ -687,10 +375,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}`,
|
||||
@ -702,19 +387,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 BidiCdpSession(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<
|
||||
@ -725,20 +402,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>(
|
||||
@ -769,13 +442,8 @@ export class BidiPage extends Page {
|
||||
return new URL(url);
|
||||
});
|
||||
|
||||
const bidiCookies = await this.#connection.send('storage.getCookies', {
|
||||
partition: {
|
||||
type: 'context',
|
||||
context: this.mainFrame()._id,
|
||||
},
|
||||
});
|
||||
return bidiCookies.result.cookies
|
||||
const cookies = await this.#frame.browsingContext.getCookies();
|
||||
return cookies
|
||||
.map(cookie => {
|
||||
return bidiToPuppeteerCookie(cookie);
|
||||
})
|
||||
@ -790,7 +458,7 @@ export class BidiPage extends Page {
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
|
||||
override target(): BiDiPageTarget {
|
||||
override target(): never {
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
|
||||
@ -876,22 +544,14 @@ export class BidiPage extends Page {
|
||||
// TODO: delete cookie before setting them.
|
||||
// await this.deleteCookie(bidiCookie);
|
||||
|
||||
const partition: Bidi.Storage.PartitionDescriptor =
|
||||
cookie.partitionKey !== undefined
|
||||
? {
|
||||
type: 'storageKey',
|
||||
sourceOrigin: cookie.partitionKey,
|
||||
userContext: this.#browserContext.id,
|
||||
}
|
||||
: {
|
||||
type: 'context',
|
||||
context: this.mainFrame()._id,
|
||||
};
|
||||
|
||||
await this.#connection.send('storage.setCookie', {
|
||||
cookie: bidiCookie,
|
||||
partition,
|
||||
});
|
||||
if (cookie.partitionKey !== undefined) {
|
||||
await this.browserContext().userContext.setCookie(
|
||||
bidiCookie,
|
||||
cookie.partitionKey
|
||||
);
|
||||
} else {
|
||||
await this.#frame.browsingContext.setCookie(bidiCookie);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -925,7 +585,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(
|
||||
@ -933,22 +593,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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -957,34 +614,6 @@ export class BidiPage extends Page {
|
||||
}
|
||||
}
|
||||
|
||||
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)}}`;
|
||||
}
|
||||
|
@ -5,8 +5,11 @@
|
||||
*/
|
||||
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
||||
|
||||
import {EventEmitter, type EventType} from '../common/EventEmitter.js';
|
||||
import type {JSHandle} from '../api/JSHandle.js';
|
||||
import {Realm} from '../api/Realm.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,
|
||||
@ -17,69 +20,33 @@ import {
|
||||
SOURCE_URL_REGEX,
|
||||
} from '../common/util.js';
|
||||
import type PuppeteerUtil from '../injected/injected.js';
|
||||
import {disposeSymbol} from '../util/disposable.js';
|
||||
import {stringifyFunction} from '../util/Function.js';
|
||||
|
||||
import type {BidiConnection} from './Connection.js';
|
||||
import type {Realm as BidiRealmCore} from './core/Realm.js';
|
||||
import type {WindowRealm} from './core/Realm.js';
|
||||
import {BidiDeserializer} from './Deserializer.js';
|
||||
import {BidiElementHandle} from './ElementHandle.js';
|
||||
import type {BidiFrame} from './Frame.js';
|
||||
import {BidiJSHandle} from './JSHandle.js';
|
||||
import type {Sandbox} from './Sandbox.js';
|
||||
import {BidiSerializer} from './Serializer.js';
|
||||
import {createEvaluationError} from './util.js';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class BidiRealm extends EventEmitter<Record<EventType, any>> {
|
||||
readonly connection: BidiConnection;
|
||||
export abstract class BidiRealm extends Realm {
|
||||
realm: BidiRealmCore;
|
||||
|
||||
#id!: string;
|
||||
#sandbox!: Sandbox;
|
||||
|
||||
constructor(connection: BidiConnection) {
|
||||
super();
|
||||
this.connection = connection;
|
||||
constructor(realm: BidiRealmCore, timeoutSettings: TimeoutSettings) {
|
||||
super(timeoutSettings);
|
||||
this.realm = realm;
|
||||
}
|
||||
|
||||
get target(): Bidi.Script.Target {
|
||||
return {
|
||||
context: this.#sandbox.environment._id,
|
||||
sandbox: this.#sandbox.name,
|
||||
};
|
||||
}
|
||||
|
||||
handleRealmDestroyed = async (
|
||||
params: Bidi.Script.RealmDestroyed['params']
|
||||
): Promise<void> => {
|
||||
if (params.realm === this.#id) {
|
||||
// Note: The Realm is destroyed, so in theory the handle should be as
|
||||
// well.
|
||||
protected initialize(): void {
|
||||
this.realm.on('destroyed', ({reason}) => {
|
||||
this.taskManager.terminateAll(new Error(reason));
|
||||
});
|
||||
this.realm.on('updated', () => {
|
||||
this.internalPuppeteerUtil = undefined;
|
||||
this.#sandbox.environment.clearDocumentHandle();
|
||||
}
|
||||
};
|
||||
|
||||
handleRealmCreated = (params: Bidi.Script.RealmCreated['params']): void => {
|
||||
if (
|
||||
params.type === 'window' &&
|
||||
params.context === this.#sandbox.environment._id &&
|
||||
params.sandbox === this.#sandbox.name
|
||||
) {
|
||||
this.#id = params.realm;
|
||||
void this.#sandbox.taskManager.rerunAll();
|
||||
}
|
||||
};
|
||||
|
||||
setSandbox(sandbox: Sandbox): void {
|
||||
this.#sandbox = sandbox;
|
||||
this.connection.on(
|
||||
Bidi.ChromiumBidi.Script.EventNames.RealmCreated,
|
||||
this.handleRealmCreated
|
||||
);
|
||||
this.connection.on(
|
||||
Bidi.ChromiumBidi.Script.EventNames.RealmDestroyed,
|
||||
this.handleRealmDestroyed
|
||||
);
|
||||
void this.taskManager.rerunAll();
|
||||
});
|
||||
}
|
||||
|
||||
protected internalPuppeteerUtil?: Promise<BidiJSHandle<PuppeteerUtil>>;
|
||||
@ -100,7 +67,7 @@ export class BidiRealm extends EventEmitter<Record<EventType, any>> {
|
||||
return this.internalPuppeteerUtil as Promise<BidiJSHandle<PuppeteerUtil>>;
|
||||
}
|
||||
|
||||
async evaluateHandle<
|
||||
override async evaluateHandle<
|
||||
Params extends unknown[],
|
||||
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
|
||||
>(
|
||||
@ -110,7 +77,7 @@ export class BidiRealm extends EventEmitter<Record<EventType, any>> {
|
||||
return await this.#evaluate(false, pageFunction, ...args);
|
||||
}
|
||||
|
||||
async evaluate<
|
||||
override async evaluate<
|
||||
Params extends unknown[],
|
||||
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
|
||||
>(
|
||||
@ -149,8 +116,6 @@ export class BidiRealm extends EventEmitter<Record<EventType, any>> {
|
||||
PuppeteerURL.INTERNAL_URL
|
||||
);
|
||||
|
||||
const sandbox = this.#sandbox;
|
||||
|
||||
let responsePromise;
|
||||
const resultOwnership = returnByValue
|
||||
? Bidi.Script.ResultOwnership.None
|
||||
@ -166,11 +131,8 @@ export class BidiRealm extends EventEmitter<Record<EventType, any>> {
|
||||
? pageFunction
|
||||
: `${pageFunction}\n${sourceUrlComment}\n`;
|
||||
|
||||
responsePromise = this.connection.send('script.evaluate', {
|
||||
expression,
|
||||
target: this.target,
|
||||
responsePromise = this.realm.evaluate(expression, true, {
|
||||
resultOwnership,
|
||||
awaitPromise: true,
|
||||
userActivation: true,
|
||||
serializationOptions,
|
||||
});
|
||||
@ -179,24 +141,25 @@ export class BidiRealm extends EventEmitter<Record<EventType, any>> {
|
||||
functionDeclaration = SOURCE_URL_REGEX.test(functionDeclaration)
|
||||
? functionDeclaration
|
||||
: `${functionDeclaration}\n${sourceUrlComment}\n`;
|
||||
responsePromise = this.connection.send('script.callFunction', {
|
||||
responsePromise = this.realm.callFunction(
|
||||
functionDeclaration,
|
||||
arguments: args.length
|
||||
? await Promise.all(
|
||||
args.map(arg => {
|
||||
return sandbox.serialize(arg);
|
||||
})
|
||||
)
|
||||
: [],
|
||||
target: this.target,
|
||||
resultOwnership,
|
||||
awaitPromise: true,
|
||||
userActivation: true,
|
||||
serializationOptions,
|
||||
});
|
||||
/* awaitPromise= */ true,
|
||||
{
|
||||
arguments: args.length
|
||||
? await Promise.all(
|
||||
args.map(arg => {
|
||||
return this.serialize(arg);
|
||||
})
|
||||
)
|
||||
: [],
|
||||
resultOwnership,
|
||||
userActivation: true,
|
||||
serializationOptions,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const {result} = await responsePromise;
|
||||
const result = await responsePromise;
|
||||
|
||||
if ('type' in result && result.type === 'exception') {
|
||||
throw createEvaluationError(result.exceptionDetails);
|
||||
@ -204,7 +167,49 @@ export class BidiRealm extends EventEmitter<Record<EventType, any>> {
|
||||
|
||||
return returnByValue
|
||||
? BidiDeserializer.deserialize(result.result)
|
||||
: createBidiHandle(sandbox, result.result);
|
||||
: this.createHandle(result.result);
|
||||
}
|
||||
|
||||
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 serialize(arg: unknown): Promise<Bidi.Script.LocalValue> {
|
||||
if (arg instanceof LazyArg) {
|
||||
arg = await arg.get(this);
|
||||
}
|
||||
|
||||
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> {
|
||||
@ -215,43 +220,80 @@ export class BidiRealm extends EventEmitter<Record<EventType, any>> {
|
||||
.filter((id): id is string => {
|
||||
return id !== undefined;
|
||||
});
|
||||
|
||||
if (handleIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.connection
|
||||
.send('script.disown', {
|
||||
target: this.target,
|
||||
handles: 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);
|
||||
});
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
[disposeSymbol](): void {
|
||||
this.connection.off(
|
||||
Bidi.ChromiumBidi.Script.EventNames.RealmCreated,
|
||||
this.handleRealmCreated
|
||||
);
|
||||
this.connection.off(
|
||||
Bidi.ChromiumBidi.Script.EventNames.RealmDestroyed,
|
||||
this.handleRealmDestroyed
|
||||
);
|
||||
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 function createBidiHandle(
|
||||
sandbox: Sandbox,
|
||||
result: Bidi.Script.RemoteValue
|
||||
): BidiJSHandle<unknown> | BidiElementHandle<Node> {
|
||||
if (result.type === 'node' || result.type === 'window') {
|
||||
return new BidiElementHandle(sandbox, result);
|
||||
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() {
|
||||
// This should run first.
|
||||
this.realm.on('updated', () => {
|
||||
this.environment.clearDocumentHandle();
|
||||
});
|
||||
|
||||
super.initialize();
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
return BidiElementHandle.from(
|
||||
{
|
||||
handle: object.objectId,
|
||||
type: 'node',
|
||||
},
|
||||
this
|
||||
);
|
||||
}
|
||||
return new BidiJSHandle(sandbox, result);
|
||||
}
|
||||
|
@ -1,154 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
||||
|
||||
import type {JSHandle} from '../api/JSHandle.js';
|
||||
import {Realm} from '../api/Realm.js';
|
||||
import {LazyArg} from '../common/LazyArg.js';
|
||||
import type {TimeoutSettings} from '../common/TimeoutSettings.js';
|
||||
import type {EvaluateFunc, HandleFor} from '../common/types.js';
|
||||
import {withSourcePuppeteerURLIfNone} from '../common/util.js';
|
||||
|
||||
import type {BrowsingContext} from './BrowsingContext.js';
|
||||
import {BidiElementHandle} from './ElementHandle.js';
|
||||
import type {BidiFrame} from './Frame.js';
|
||||
import {BidiJSHandle} from './JSHandle.js';
|
||||
import type {BidiRealm as BidiRealm} from './Realm.js';
|
||||
import {BidiSerializer} from './Serializer.js';
|
||||
/**
|
||||
* A unique key for {@link SandboxChart} to denote the default world.
|
||||
* Realms are automatically created in the default sandbox.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export const MAIN_SANDBOX = Symbol('mainSandbox');
|
||||
/**
|
||||
* A unique key for {@link SandboxChart} to denote the puppeteer sandbox.
|
||||
* This world contains all puppeteer-internal bindings/code.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export const PUPPETEER_SANDBOX = Symbol('puppeteerSandbox');
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export interface SandboxChart {
|
||||
[key: string]: Sandbox;
|
||||
[MAIN_SANDBOX]: Sandbox;
|
||||
[PUPPETEER_SANDBOX]: Sandbox;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class Sandbox extends Realm {
|
||||
readonly name: string | undefined;
|
||||
readonly realm: BidiRealm;
|
||||
#frame: BidiFrame;
|
||||
|
||||
constructor(
|
||||
name: string | undefined,
|
||||
frame: BidiFrame,
|
||||
// TODO: We should split the Realm and BrowsingContext
|
||||
realm: BidiRealm | BrowsingContext,
|
||||
timeoutSettings: TimeoutSettings
|
||||
) {
|
||||
super(timeoutSettings);
|
||||
this.name = name;
|
||||
this.realm = realm;
|
||||
this.#frame = frame;
|
||||
this.realm.setSandbox(this);
|
||||
}
|
||||
|
||||
override get environment(): BidiFrame {
|
||||
return this.#frame;
|
||||
}
|
||||
|
||||
async evaluateHandle<
|
||||
Params extends unknown[],
|
||||
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
|
||||
>(
|
||||
pageFunction: Func | string,
|
||||
...args: Params
|
||||
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
|
||||
pageFunction = withSourcePuppeteerURLIfNone(
|
||||
this.evaluateHandle.name,
|
||||
pageFunction
|
||||
);
|
||||
return await this.realm.evaluateHandle(pageFunction, ...args);
|
||||
}
|
||||
|
||||
async evaluate<
|
||||
Params extends unknown[],
|
||||
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
|
||||
>(
|
||||
pageFunction: Func | string,
|
||||
...args: Params
|
||||
): Promise<Awaited<ReturnType<Func>>> {
|
||||
pageFunction = withSourcePuppeteerURLIfNone(
|
||||
this.evaluate.name,
|
||||
pageFunction
|
||||
);
|
||||
return await this.realm.evaluate(pageFunction, ...args);
|
||||
}
|
||||
|
||||
async adoptHandle<T extends JSHandle<Node>>(handle: T): Promise<T> {
|
||||
return (await this.evaluateHandle(node => {
|
||||
return node;
|
||||
}, handle)) as unknown as T;
|
||||
}
|
||||
|
||||
async transferHandle<T extends JSHandle<Node>>(handle: T): Promise<T> {
|
||||
if (handle.realm === this) {
|
||||
return handle;
|
||||
}
|
||||
const transferredHandle = await this.evaluateHandle(node => {
|
||||
return node;
|
||||
}, handle);
|
||||
await handle.dispose();
|
||||
return transferredHandle as unknown as T;
|
||||
}
|
||||
|
||||
override async adoptBackendNode(
|
||||
backendNodeId?: number
|
||||
): Promise<JSHandle<Node>> {
|
||||
const {object} = await this.environment.client.send('DOM.resolveNode', {
|
||||
backendNodeId: backendNodeId,
|
||||
});
|
||||
return new BidiElementHandle(this, {
|
||||
handle: object.objectId,
|
||||
type: 'node',
|
||||
});
|
||||
}
|
||||
|
||||
async serialize(arg: unknown): Promise<Bidi.Script.LocalValue> {
|
||||
if (arg instanceof LazyArg) {
|
||||
arg = await arg.get(this.realm);
|
||||
}
|
||||
// eslint-disable-next-line rulesdir/use-using -- We want this to continue living.
|
||||
const objectHandle =
|
||||
arg && (arg instanceof BidiJSHandle || arg instanceof BidiElementHandle)
|
||||
? arg
|
||||
: null;
|
||||
if (objectHandle) {
|
||||
if (
|
||||
objectHandle.realm.environment.context() !== this.environment.context()
|
||||
) {
|
||||
throw new Error(
|
||||
'JSHandles can be evaluated only in the context they were created!'
|
||||
);
|
||||
}
|
||||
if (objectHandle.disposed) {
|
||||
throw new Error('JSHandle is disposed!');
|
||||
}
|
||||
return objectHandle.remoteValue() as Bidi.Script.RemoteReference;
|
||||
}
|
||||
|
||||
return BidiSerializer.serialize(arg);
|
||||
}
|
||||
}
|
@ -4,57 +4,19 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {CDPSession} from '../api/CDPSession.js';
|
||||
import type {Page} from '../api/Page.js';
|
||||
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 {BrowsingContext} from './BrowsingContext.js';
|
||||
import {BidiCdpSession} from './CDPSession.js';
|
||||
import type {BidiFrame} from './Frame.js';
|
||||
import {BidiPage} from './Page.js';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export abstract class BidiTarget extends Target {
|
||||
protected _browserContext: BidiBrowserContext;
|
||||
|
||||
constructor(browserContext: BidiBrowserContext) {
|
||||
super();
|
||||
this._browserContext = browserContext;
|
||||
}
|
||||
|
||||
_setBrowserContext(browserContext: BidiBrowserContext): void {
|
||||
this._browserContext = browserContext;
|
||||
}
|
||||
|
||||
override asPage(): Promise<Page> {
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
|
||||
override browser(): BidiBrowser {
|
||||
return this._browserContext.browser();
|
||||
}
|
||||
|
||||
override browserContext(): BidiBrowserContext {
|
||||
return this._browserContext;
|
||||
}
|
||||
|
||||
override opener(): never {
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
|
||||
override createCDPSession(): Promise<CDPSession> {
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class BiDiBrowserTarget extends Target {
|
||||
export class BidiBrowserTarget extends Target {
|
||||
#browser: BidiBrowser;
|
||||
|
||||
constructor(browser: BidiBrowser) {
|
||||
@ -62,91 +24,109 @@ export class BiDiBrowserTarget extends Target {
|
||||
this.#browser = browser;
|
||||
}
|
||||
|
||||
override asPage(): Promise<BidiPage> {
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
override url(): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
override type(): TargetType {
|
||||
return TargetType.BROWSER;
|
||||
}
|
||||
|
||||
override asPage(): Promise<Page> {
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
|
||||
override browser(): BidiBrowser {
|
||||
return this.#browser;
|
||||
}
|
||||
|
||||
override browserContext(): BidiBrowserContext {
|
||||
return this.#browser.defaultBrowserContext();
|
||||
}
|
||||
|
||||
override opener(): never {
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
|
||||
override createCDPSession(): Promise<CDPSession> {
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class BiDiBrowsingContextTarget extends BidiTarget {
|
||||
protected _browsingContext: BrowsingContext;
|
||||
|
||||
constructor(
|
||||
browserContext: BidiBrowserContext,
|
||||
browsingContext: BrowsingContext
|
||||
) {
|
||||
super(browserContext);
|
||||
|
||||
this._browsingContext = browsingContext;
|
||||
}
|
||||
|
||||
override url(): string {
|
||||
return this._browsingContext.url;
|
||||
}
|
||||
|
||||
override async createCDPSession(): Promise<CDPSession> {
|
||||
const {sessionId} = await this._browsingContext.cdpSession.send(
|
||||
'Target.attachToTarget',
|
||||
{
|
||||
targetId: this._browsingContext.id,
|
||||
flatten: true,
|
||||
}
|
||||
);
|
||||
return new BidiCdpSession(this._browsingContext, sessionId);
|
||||
}
|
||||
|
||||
override type(): TargetType {
|
||||
return TargetType.PAGE;
|
||||
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 BiDiBrowsingContextTarget {
|
||||
export class BidiPageTarget extends Target {
|
||||
#page: BidiPage;
|
||||
|
||||
constructor(
|
||||
browserContext: BidiBrowserContext,
|
||||
browsingContext: BrowsingContext
|
||||
) {
|
||||
super(browserContext, browsingContext);
|
||||
|
||||
this.#page = new BidiPage(browsingContext, browserContext);
|
||||
constructor(page: BidiPage) {
|
||||
super();
|
||||
this.#page = page;
|
||||
}
|
||||
|
||||
override async page(): Promise<BidiPage> {
|
||||
return this.#page;
|
||||
}
|
||||
|
||||
override _setBrowserContext(browserContext: BidiBrowserContext): void {
|
||||
super._setBrowserContext(browserContext);
|
||||
this.#page._setBrowserContext(browserContext);
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,6 @@
|
||||
export * from './BidiOverCdp.js';
|
||||
export * from './Browser.js';
|
||||
export * from './BrowserContext.js';
|
||||
export * from './BrowsingContext.js';
|
||||
export * from './Connection.js';
|
||||
export * from './ElementHandle.js';
|
||||
export * from './Frame.js';
|
||||
@ -15,8 +14,5 @@ export * from './HTTPRequest.js';
|
||||
export * from './HTTPResponse.js';
|
||||
export * from './Input.js';
|
||||
export * from './JSHandle.js';
|
||||
export * from './NetworkManager.js';
|
||||
export * from './Page.js';
|
||||
export * from './Realm.js';
|
||||
export * from './Sandbox.js';
|
||||
export * from './Target.js';
|
||||
|
@ -1,119 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
||||
|
||||
import type {
|
||||
ObservableInput,
|
||||
ObservedValueOf,
|
||||
OperatorFunction,
|
||||
} from '../../third_party/rxjs/rxjs.js';
|
||||
import {catchError} from '../../third_party/rxjs/rxjs.js';
|
||||
import type {PuppeteerLifeCycleEvent} from '../cdp/LifecycleWatcher.js';
|
||||
import {ProtocolError, TimeoutError} from '../common/Errors.js';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export type BiDiNetworkIdle = Extract<
|
||||
PuppeteerLifeCycleEvent,
|
||||
'networkidle0' | 'networkidle2'
|
||||
> | null;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function getBiDiLifeCycles(
|
||||
event: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]
|
||||
): [
|
||||
Extract<PuppeteerLifeCycleEvent, 'load' | 'domcontentloaded'>,
|
||||
BiDiNetworkIdle,
|
||||
] {
|
||||
if (Array.isArray(event)) {
|
||||
const pageLifeCycle = event.some(lifeCycle => {
|
||||
return lifeCycle !== 'domcontentloaded';
|
||||
})
|
||||
? 'load'
|
||||
: 'domcontentloaded';
|
||||
|
||||
const networkLifeCycle = event.reduce((acc, lifeCycle) => {
|
||||
if (lifeCycle === 'networkidle0') {
|
||||
return lifeCycle;
|
||||
} else if (acc !== 'networkidle0' && lifeCycle === 'networkidle2') {
|
||||
return lifeCycle;
|
||||
}
|
||||
return acc;
|
||||
}, null as BiDiNetworkIdle);
|
||||
|
||||
return [pageLifeCycle, networkLifeCycle];
|
||||
}
|
||||
|
||||
if (event === 'networkidle0' || event === 'networkidle2') {
|
||||
return ['load', event];
|
||||
}
|
||||
|
||||
return [event, null];
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export const lifeCycleToReadinessState = new Map<
|
||||
PuppeteerLifeCycleEvent,
|
||||
Bidi.BrowsingContext.ReadinessState
|
||||
>([
|
||||
['load', Bidi.BrowsingContext.ReadinessState.Complete],
|
||||
['domcontentloaded', Bidi.BrowsingContext.ReadinessState.Interactive],
|
||||
]);
|
||||
|
||||
export function getBiDiReadinessState(
|
||||
event: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]
|
||||
): [Bidi.BrowsingContext.ReadinessState, BiDiNetworkIdle] {
|
||||
const lifeCycles = getBiDiLifeCycles(event);
|
||||
const readiness = lifeCycleToReadinessState.get(lifeCycles[0])!;
|
||||
return [readiness, lifeCycles[1]];
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export const lifeCycleToSubscribedEvent = new Map<
|
||||
PuppeteerLifeCycleEvent,
|
||||
'browsingContext.load' | 'browsingContext.domContentLoaded'
|
||||
>([
|
||||
['load', 'browsingContext.load'],
|
||||
['domcontentloaded', 'browsingContext.domContentLoaded'],
|
||||
]);
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function getBiDiLifecycleEvent(
|
||||
event: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]
|
||||
): [
|
||||
'browsingContext.load' | 'browsingContext.domContentLoaded',
|
||||
BiDiNetworkIdle,
|
||||
] {
|
||||
const lifeCycles = getBiDiLifeCycles(event);
|
||||
const bidiEvent = lifeCycleToSubscribedEvent.get(lifeCycles[0])!;
|
||||
return [bidiEvent, lifeCycles[1]];
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function rewriteNavigationError<T, R extends ObservableInput<T>>(
|
||||
message: string,
|
||||
ms: number
|
||||
): OperatorFunction<T, T | ObservedValueOf<R>> {
|
||||
return catchError<T, R>(error => {
|
||||
if (error instanceof ProtocolError) {
|
||||
error.message += ` at ${message}`;
|
||||
} else if (error instanceof TimeoutError) {
|
||||
error.message = `Navigation timeout of ${ms} ms exceeded`;
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
}
|
@ -6,6 +6,7 @@
|
||||
|
||||
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
||||
|
||||
import {ProtocolError, TimeoutError} from '../common/Errors.js';
|
||||
import {PuppeteerURL} from '../common/util.js';
|
||||
|
||||
import {BidiDeserializer} from './Deserializer.js';
|
||||
@ -56,3 +57,20 @@ export function createEvaluationError(
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
@ -6,8 +6,10 @@
|
||||
export {
|
||||
bufferCount,
|
||||
catchError,
|
||||
combineLatest,
|
||||
concat,
|
||||
concatMap,
|
||||
debounceTime,
|
||||
defaultIfEmpty,
|
||||
defer,
|
||||
delay,
|
||||
@ -22,7 +24,6 @@ export {
|
||||
ignoreElements,
|
||||
lastValueFrom,
|
||||
map,
|
||||
ReplaySubject,
|
||||
merge,
|
||||
mergeMap,
|
||||
mergeScan,
|
||||
@ -33,6 +34,7 @@ export {
|
||||
pipe,
|
||||
race,
|
||||
raceWith,
|
||||
ReplaySubject,
|
||||
retry,
|
||||
startWith,
|
||||
switchMap,
|
||||
|
@ -537,7 +537,7 @@
|
||||
"testIdPattern": "[coverage.spec] Coverage specs JSCoverage should ignore pptr internal scripts if reportAnonymousScripts is true",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["webDriverBiDi"],
|
||||
"expectations": ["FAIL", "PASS"]
|
||||
"expectations": ["FAIL"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[devtools.spec] DevTools should expose DevTools as a page",
|
||||
@ -579,7 +579,7 @@
|
||||
"testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.click should return Point data",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["webDriverBiDi"],
|
||||
"expectations": ["FAIL", "PASS"]
|
||||
"expectations": ["PASS"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.clickablePoint should not work if the click box is not visible due to the iframe",
|
||||
@ -670,7 +670,7 @@
|
||||
"testIdPattern": "[frame.spec] Frame specs Frame Management should support lazy frames",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["chrome"],
|
||||
"expectations": ["FAIL", "PASS"]
|
||||
"expectations": ["PASS"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[headful.spec] *",
|
||||
@ -712,7 +712,7 @@
|
||||
"testIdPattern": "[jshandle.spec] JSHandle Page.evaluateHandle should use the same JS wrappers",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["webDriverBiDi"],
|
||||
"expectations": ["FAIL", "PASS"]
|
||||
"expectations": ["PASS"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[keyboard.spec] Keyboard should send a character with sendCharacter in iframe",
|
||||
@ -820,7 +820,8 @@
|
||||
"testIdPattern": "[navigation.spec] navigation Page.goto should fail when navigating to bad SSL",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["firefox"],
|
||||
"expectations": ["FAIL"]
|
||||
"expectations": ["SKIP"],
|
||||
"comment": "https://github.com/w3c/webdriver-bidi/issues/657"
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[navigation.spec] navigation Page.goto should send referer",
|
||||
@ -846,12 +847,6 @@
|
||||
"parameters": ["firefox", "webDriverBiDi"],
|
||||
"expectations": ["SKIP"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[network.spec] network raw network headers Cross-origin set-cookie",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["webDriverBiDi"],
|
||||
"expectations": ["FAIL", "PASS"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[network.spec] network Request.initiator should return the initiator",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
@ -870,6 +865,13 @@
|
||||
"parameters": ["webDriverBiDi"],
|
||||
"expectations": ["PASS"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[network.spec] network Request.postData should be |undefined| when there is no post data",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["webDriverBiDi"],
|
||||
"expectations": ["FAIL"],
|
||||
"comment": "Unsupported"
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[network.spec] network Request.postData should work",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
@ -1218,7 +1220,7 @@
|
||||
"testIdPattern": "[target.spec] Target Browser.waitForTarget should timeout waiting for a non-existent target",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["webDriverBiDi"],
|
||||
"expectations": ["FAIL", "PASS"]
|
||||
"expectations": ["PASS"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[target.spec] Target should be able to use async waitForTarget",
|
||||
@ -1609,7 +1611,7 @@
|
||||
"testIdPattern": "[click.spec] Page.click should click on checkbox input and toggle",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["firefox", "webDriverBiDi"],
|
||||
"expectations": ["FAIL", "PASS"]
|
||||
"expectations": ["PASS"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[click.spec] Page.click should click on checkbox label and toggle",
|
||||
@ -1783,13 +1785,13 @@
|
||||
"testIdPattern": "[coverage.spec] Coverage specs CSSCoverage should work with complicated usecases",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["chrome", "webDriverBiDi"],
|
||||
"expectations": ["FAIL", "PASS"]
|
||||
"expectations": ["PASS"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[coverage.spec] Coverage specs JSCoverage should not ignore eval() scripts if reportAnonymousScripts is true",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["chrome", "webDriverBiDi"],
|
||||
"expectations": ["FAIL", "PASS"]
|
||||
"expectations": ["PASS"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[debugInfo.spec] DebugInfo Browser.debugInfo should work",
|
||||
@ -2125,7 +2127,7 @@
|
||||
"testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should await promise",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["firefox", "webDriverBiDi"],
|
||||
"expectations": ["FAIL", "PASS"]
|
||||
"expectations": ["PASS"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should simulate a user gesture",
|
||||
@ -2198,7 +2200,8 @@
|
||||
"testIdPattern": "[frame.spec] Frame specs Frame Management should detach child frames on navigation",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["firefox", "webDriverBiDi"],
|
||||
"expectations": ["PASS"]
|
||||
"expectations": ["FAIL"],
|
||||
"comment": "https://github.com/w3c/webdriver-bidi/issues/659"
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[frame.spec] Frame specs Frame Management should report different frame instance when frame re-attaches",
|
||||
@ -2236,6 +2239,13 @@
|
||||
"parameters": ["cdp", "firefox"],
|
||||
"expectations": ["SKIP"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[frame.spec] Frame specs Frame Management should send \"framenavigated\" when navigating on anchor URLs",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["firefox", "webDriverBiDi"],
|
||||
"expectations": ["SKIP"],
|
||||
"comment": "https://github.com/w3c/webdriver-bidi/issues/657"
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[frame.spec] Frame specs Frame Management should send events when frames are manipulated dynamically",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
@ -2252,7 +2262,8 @@
|
||||
"testIdPattern": "[frame.spec] Frame specs Frame Management should support framesets",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["firefox", "webDriverBiDi"],
|
||||
"expectations": ["PASS"]
|
||||
"expectations": ["FAIL"],
|
||||
"comment": "https://github.com/w3c/webdriver-bidi/issues/659"
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[frame.spec] Frame specs Frame Management should support lazy frames",
|
||||
@ -3015,7 +3026,7 @@
|
||||
"testIdPattern": "[navigation.spec] navigation Frame.goto should navigate subframes",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["firefox", "webDriverBiDi"],
|
||||
"expectations": ["FAIL", "PASS"]
|
||||
"expectations": ["PASS"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[navigation.spec] navigation Frame.goto should reject when frame detaches",
|
||||
@ -3059,11 +3070,33 @@
|
||||
"parameters": ["cdp", "firefox"],
|
||||
"expectations": ["FAIL"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[navigation.spec] navigation Page.goBack should work with HistoryAPI",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["firefox", "webDriverBiDi"],
|
||||
"expectations": ["SKIP"],
|
||||
"comment": "browsingContext.navigationStarted event not emitted for fragment navigation"
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[navigation.spec] navigation Page.goto should fail when main resources failed to load",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["firefox", "webDriverBiDi"],
|
||||
"expectations": ["FAIL", "PASS"]
|
||||
"expectations": ["SKIP"],
|
||||
"comment": "https://github.com/w3c/webdriver-bidi/issues/657"
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[navigation.spec] navigation Page.goto should fail when navigating and show the url at the error message",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["firefox", "webDriverBiDi"],
|
||||
"expectations": ["SKIP"],
|
||||
"comment": "https://github.com/w3c/webdriver-bidi/issues/657"
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[navigation.spec] navigation Page.goto should fail when navigating to bad SSL after redirects",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["firefox", "webDriverBiDi"],
|
||||
"expectations": ["SKIP"],
|
||||
"comment": "https://github.com/w3c/webdriver-bidi/issues/657"
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[navigation.spec] navigation Page.goto should fail when server returns 204",
|
||||
@ -3071,6 +3104,13 @@
|
||||
"parameters": ["cdp", "firefox"],
|
||||
"expectations": ["SKIP"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[navigation.spec] navigation Page.goto should fail when server returns 204",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["firefox", "webDriverBiDi"],
|
||||
"expectations": ["SKIP"],
|
||||
"comment": "https://github.com/w3c/webdriver-bidi/issues/657"
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[navigation.spec] navigation Page.goto should navigate to dataURL and fire dataURL requests",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
@ -3087,7 +3127,7 @@
|
||||
"testIdPattern": "[navigation.spec] navigation Page.goto should navigate to dataURL and fire dataURL requests",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["chrome", "webDriverBiDi"],
|
||||
"expectations": ["FAIL", "PASS"]
|
||||
"expectations": ["PASS"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[navigation.spec] navigation Page.goto should navigate to empty page with networkidle0",
|
||||
@ -3131,11 +3171,17 @@
|
||||
"parameters": ["chrome", "webDriverBiDi"],
|
||||
"expectations": ["PASS"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[navigation.spec] navigation Page.goto should return last response in redirect chain",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["firefox", "webDriverBiDi"],
|
||||
"expectations": ["PASS"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[navigation.spec] navigation Page.goto should return response when page changes its URL after load",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["firefox", "webDriverBiDi"],
|
||||
"expectations": ["FAIL", "PASS"]
|
||||
"expectations": ["PASS"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[navigation.spec] navigation Page.goto should send referer",
|
||||
@ -3161,18 +3207,19 @@
|
||||
"parameters": ["cdp", "firefox"],
|
||||
"expectations": ["FAIL"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[navigation.spec] navigation Page.goto should work when page calls history API in beforeunload",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["firefox", "webDriverBiDi"],
|
||||
"expectations": ["FAIL"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[navigation.spec] navigation Page.goto should work with anchor navigation",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["chrome", "chrome-headless-shell"],
|
||||
"expectations": ["SKIP"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[navigation.spec] navigation Page.goto should work with anchor navigation",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["firefox", "webDriverBiDi"],
|
||||
"expectations": ["SKIP"],
|
||||
"comment": "browsingContext.navigationStarted event not emitted for fragment navigation"
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[navigation.spec] navigation Page.goto should work with subframes return 204",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
@ -3209,6 +3256,13 @@
|
||||
"parameters": ["cdp", "firefox"],
|
||||
"expectations": ["FAIL"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[navigation.spec] navigation Page.waitForNavigation should work with DOM history.back()/history.forward()",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["firefox", "webDriverBiDi"],
|
||||
"expectations": ["SKIP"],
|
||||
"comment": "browsingContext.navigationStarted event not emitted for fragment navigation"
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[navigation.spec] navigation Page.waitForNavigation should work with history.pushState()",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
@ -3217,9 +3271,10 @@
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[navigation.spec] navigation Page.waitForNavigation should work with history.pushState()",
|
||||
"platforms": ["linux"],
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["firefox", "webDriverBiDi"],
|
||||
"expectations": ["SKIP"]
|
||||
"expectations": ["SKIP"],
|
||||
"comment": "browsingContext.navigationStarted event not emitted for fragment navigation"
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[navigation.spec] navigation Page.waitForNavigation should work with history.replaceState()",
|
||||
@ -3268,7 +3323,7 @@
|
||||
"testIdPattern": "[network.spec] network Network Events Page.Events.RequestServedFromCache",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["chrome", "webDriverBiDi"],
|
||||
"expectations": ["FAIL", "PASS"]
|
||||
"expectations": ["FAIL"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[network.spec] network Network Events Page.Events.Response",
|
||||
@ -3342,6 +3397,12 @@
|
||||
"parameters": ["cdp", "firefox"],
|
||||
"expectations": ["FAIL"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[network.spec] network raw network headers Cross-origin set-cookie",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["chrome", "webDriverBiDi"],
|
||||
"expectations": ["FAIL", "PASS"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[network.spec] network raw network headers Same-origin set-cookie navigation",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
@ -3388,7 +3449,7 @@
|
||||
"testIdPattern": "[network.spec] network Request.frame should work for subframe navigation request",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["firefox", "webDriverBiDi"],
|
||||
"expectations": ["FAIL", "PASS"]
|
||||
"expectations": ["PASS"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[network.spec] network Request.initiator should return the initiator",
|
||||
@ -3455,7 +3516,7 @@
|
||||
"testIdPattern": "[network.spec] network Response.fromCache should work",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["firefox", "webDriverBiDi"],
|
||||
"expectations": ["FAIL", "PASS"]
|
||||
"expectations": ["FAIL"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[network.spec] network Response.fromCache should work",
|
||||
@ -3529,12 +3590,6 @@
|
||||
"parameters": ["firefox", "webDriverBiDi"],
|
||||
"expectations": ["PASS"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[oopif.spec] OOPIF should detect existing OOPIFs when Puppeteer connects to an existing page",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["chrome", "webDriverBiDi"],
|
||||
"expectations": ["FAIL"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[oopif.spec] OOPIF should detect existing OOPIFs when Puppeteer connects to an existing page",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
@ -3597,28 +3652,18 @@
|
||||
"expectations": ["FAIL"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[oopif.spec] OOPIF should report oopif frames",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["chrome", "webDriverBiDi"],
|
||||
"expectations": ["FAIL"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[oopif.spec] OOPIF should report oopif frames",
|
||||
"testIdPattern": "[oopif.spec] OOPIF should support frames within OOP iframes",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["firefox", "webDriverBiDi"],
|
||||
"expectations": ["FAIL"]
|
||||
"expectations": ["SKIP"],
|
||||
"comment": "Fetch error"
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[oopif.spec] OOPIF should support lazy OOP frames",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["firefox", "webDriverBiDi"],
|
||||
"expectations": ["FAIL", "PASS"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[oopif.spec] OOPIF should support lazy OOP frames",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["chrome", "webDriverBiDi"],
|
||||
"expectations": ["PASS"]
|
||||
"expectations": ["FAIL"],
|
||||
"comment": "https://bugzilla.mozilla.org/show_bug.cgi?id=187816"
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[oopif.spec] OOPIF should support lazy OOP frames",
|
||||
@ -3678,7 +3723,8 @@
|
||||
"testIdPattern": "[oopif.spec] OOPIF should wait for inner OOPIFs",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["firefox", "webDriverBiDi"],
|
||||
"expectations": ["FAIL"]
|
||||
"expectations": ["SKIP"],
|
||||
"comment": "Fetch error"
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[page.spec] Page Page.addScriptTag should throw when added with content to the CSP page",
|
||||
@ -4080,7 +4126,7 @@
|
||||
"testIdPattern": "[proxy.spec] request proxy should respect proxy bypass list",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["firefox", "webDriverBiDi"],
|
||||
"expectations": ["FAIL", "PASS"]
|
||||
"expectations": ["PASS"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[proxy.spec] request proxy should respect proxy bypass list",
|
||||
@ -4224,7 +4270,7 @@
|
||||
"testIdPattern": "[target.spec] Target Browser.waitForTarget should wait for a target",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["chrome", "webDriverBiDi"],
|
||||
"expectations": ["FAIL", "PASS"]
|
||||
"expectations": ["PASS"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[target.spec] Target Browser.waitForTarget should wait for a target",
|
||||
@ -4232,12 +4278,6 @@
|
||||
"parameters": ["cdp", "firefox"],
|
||||
"expectations": ["SKIP"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[target.spec] Target Browser.waitForTarget should wait for a target",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["firefox", "webDriverBiDi"],
|
||||
"expectations": ["FAIL"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[target.spec] Target should be able to use async waitForTarget",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
@ -4332,7 +4372,7 @@
|
||||
"testIdPattern": "[target.spec] Target should not crash while redirecting if original request was missed",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["chrome", "webDriverBiDi"],
|
||||
"expectations": ["FAIL", "PASS"]
|
||||
"expectations": ["PASS"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[target.spec] Target should not report uninitialized pages",
|
||||
|
@ -370,9 +370,10 @@ describe('AriaQueryHandler', () => {
|
||||
await detachFrame(page, 'frame1');
|
||||
await waitPromise;
|
||||
expect(waitError).toBeTruthy();
|
||||
expect(waitError.message).toContain(
|
||||
'waitForFunction failed: frame got detached.'
|
||||
);
|
||||
expect(waitError.message).atLeastOneToContain([
|
||||
'waitForFunction failed: frame got detached.',
|
||||
'Browsing context already closed.',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should survive cross-process navigation', async () => {
|
||||
|
@ -408,9 +408,10 @@ describe('Evaluation specs', function () {
|
||||
return (error = error_);
|
||||
});
|
||||
expect(error).toBeTruthy();
|
||||
expect(error.message).toContain(
|
||||
'JSHandles can be evaluated only in the context they were created'
|
||||
);
|
||||
expect(error.message).atLeastOneToContain([
|
||||
'JSHandles can be evaluated only in the context they were created',
|
||||
"Trying to evaluate JSHandle from different frames. Usually this means you're using a handle from a page on a different page.",
|
||||
]);
|
||||
});
|
||||
it('should simulate a user gesture', async () => {
|
||||
const {page} = await getTestState();
|
||||
|
@ -102,6 +102,7 @@ describe('Launcher specs', function () {
|
||||
expect(message).atLeastOneToContain([
|
||||
'Target closed',
|
||||
'Page closed!',
|
||||
'Browser already closed',
|
||||
]);
|
||||
expect(message).not.toContain('Timeout');
|
||||
}
|
||||
|
@ -5,10 +5,9 @@
|
||||
*/
|
||||
|
||||
import expect from 'expect';
|
||||
import type {BrowserContext} from 'puppeteer-core/internal/api/BrowserContext.js';
|
||||
import type {CDPSession} from 'puppeteer-core/internal/api/CDPSession.js';
|
||||
import {CDPSessionEvent} from 'puppeteer-core/internal/api/CDPSession.js';
|
||||
import type {CdpTarget} from 'puppeteer-core/internal/cdp/Target.js';
|
||||
import type {Page} from 'puppeteer-core/internal/api/Page.js';
|
||||
|
||||
import {getTestState, launch} from './mocha-utils.js';
|
||||
import {attachFrame, detachFrame, navigateFrame} from './utils.js';
|
||||
@ -266,24 +265,24 @@ describe('OOPIF', function () {
|
||||
await frame.waitForSelector('#clicked');
|
||||
});
|
||||
it('should report oopif frames', async () => {
|
||||
const {server, page, context} = state;
|
||||
const {server, page} = state;
|
||||
|
||||
const frame = page.waitForFrame(frame => {
|
||||
return frame.url().endsWith('/oopif.html');
|
||||
});
|
||||
await page.goto(server.PREFIX + '/dynamic-oopif.html');
|
||||
await frame;
|
||||
expect(oopifs(context)).toHaveLength(1);
|
||||
expect(await iframes(page)).toHaveLength(1);
|
||||
expect(page.frames()).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should wait for inner OOPIFs', async () => {
|
||||
const {server, page, context} = state;
|
||||
const {server, page} = state;
|
||||
await page.goto(`http://mainframe:${server.PORT}/main-frame.html`);
|
||||
const frame2 = await page.waitForFrame(frame => {
|
||||
return frame.url().endsWith('inner-frame2.html');
|
||||
});
|
||||
expect(oopifs(context)).toHaveLength(2);
|
||||
expect(await iframes(page)).toHaveLength(2);
|
||||
expect(
|
||||
page.frames().filter(frame => {
|
||||
return frame.isOOPFrame();
|
||||
@ -297,7 +296,7 @@ describe('OOPIF', function () {
|
||||
});
|
||||
|
||||
it('should load oopif iframes with subresources and request interception', async () => {
|
||||
const {server, page, context} = state;
|
||||
const {server, page} = state;
|
||||
|
||||
const framePromise = page.waitForFrame(frame => {
|
||||
return frame.url().endsWith('/oopif.html');
|
||||
@ -312,7 +311,7 @@ describe('OOPIF', function () {
|
||||
await page.goto(server.PREFIX + '/dynamic-oopif.html');
|
||||
const frame = await framePromise;
|
||||
const request = await requestPromise;
|
||||
expect(oopifs(context)).toHaveLength(1);
|
||||
expect(await iframes(page)).toHaveLength(1);
|
||||
expect(request.frame()).toBe(frame);
|
||||
});
|
||||
|
||||
@ -394,14 +393,14 @@ describe('OOPIF', function () {
|
||||
});
|
||||
|
||||
it('should detect existing OOPIFs when Puppeteer connects to an existing page', async () => {
|
||||
const {server, puppeteer, page, context} = state;
|
||||
const {server, puppeteer, page} = state;
|
||||
|
||||
const frame = page.waitForFrame(frame => {
|
||||
return frame.url().endsWith('/oopif.html');
|
||||
});
|
||||
await page.goto(server.PREFIX + '/dynamic-oopif.html');
|
||||
await frame;
|
||||
expect(oopifs(context)).toHaveLength(1);
|
||||
expect(await iframes(page)).toHaveLength(1);
|
||||
expect(page.frames()).toHaveLength(2);
|
||||
|
||||
const browserURL = 'http://127.0.0.1:21222';
|
||||
@ -520,8 +519,13 @@ describe('OOPIF', function () {
|
||||
});
|
||||
});
|
||||
|
||||
function oopifs(context: BrowserContext) {
|
||||
return context.targets().filter(target => {
|
||||
return (target as CdpTarget)._getTargetInfo().type === 'iframe';
|
||||
async function iframes(page: Page) {
|
||||
const iframes = await Promise.all(
|
||||
page.frames().map(async frame => {
|
||||
return await frame.frameElement();
|
||||
})
|
||||
);
|
||||
return iframes.filter(frame => {
|
||||
return frame !== null;
|
||||
});
|
||||
}
|
||||
|
@ -102,7 +102,11 @@ describe('Page', function () {
|
||||
]);
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const message = results[i].message;
|
||||
expect(message).atLeastOneToContain(['Target closed', 'Page closed!']);
|
||||
expect(message).atLeastOneToContain([
|
||||
'Target closed',
|
||||
'Page closed!',
|
||||
'Frame detached',
|
||||
]);
|
||||
expect(message).not.toContain('Timeout');
|
||||
}
|
||||
});
|
||||
|
@ -446,9 +446,10 @@ describe('waittask specs', function () {
|
||||
await detachFrame(page, 'frame1');
|
||||
await waitPromise;
|
||||
expect(waitError).toBeTruthy();
|
||||
expect(waitError?.message).toContain(
|
||||
'waitForFunction failed: frame got detached.'
|
||||
);
|
||||
expect(waitError?.message).atLeastOneToContain([
|
||||
'waitForFunction failed: frame got detached.',
|
||||
'Browsing context already closed.',
|
||||
]);
|
||||
});
|
||||
it('should survive cross-process navigation', async () => {
|
||||
const {page, server} = await getTestState();
|
||||
@ -754,9 +755,10 @@ describe('waittask specs', function () {
|
||||
await detachFrame(page, 'frame1');
|
||||
await waitPromise;
|
||||
expect(waitError).toBeTruthy();
|
||||
expect(waitError?.message).toContain(
|
||||
'waitForFunction failed: frame got detached.'
|
||||
);
|
||||
expect(waitError?.message).atLeastOneToContain([
|
||||
'waitForFunction failed: frame got detached.',
|
||||
'Browsing context already closed.',
|
||||
]);
|
||||
});
|
||||
it('hidden should wait for display: none', async () => {
|
||||
const {page} = await getTestState();
|
||||
|
Loading…
Reference in New Issue
Block a user