chore: refactor handles to use realms (#10830)

This commit is contained in:
jrandolf 2023-09-01 14:12:29 +02:00 committed by GitHub
parent 7e74439c51
commit f3c7499e67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 386 additions and 548 deletions

View File

@ -10,7 +10,7 @@ Evaluates the given function with the current handle as its first argument.
```typescript ```typescript
class JSHandle { class JSHandle {
abstract evaluate< evaluate<
Params extends unknown[], Params extends unknown[],
Func extends EvaluateFuncWith<T, Params> = EvaluateFuncWith<T, Params>, Func extends EvaluateFuncWith<T, Params> = EvaluateFuncWith<T, Params>,
>( >(

View File

@ -10,7 +10,7 @@ Evaluates the given function with the current handle as its first argument.
```typescript ```typescript
class JSHandle { class JSHandle {
abstract evaluateHandle< evaluateHandle<
Params extends unknown[], Params extends unknown[],
Func extends EvaluateFuncWith<T, Params> = EvaluateFuncWith<T, Params>, Func extends EvaluateFuncWith<T, Params> = EvaluateFuncWith<T, Params>,
>( >(

View File

@ -10,13 +10,13 @@ Gets a map of handles representing the properties of the current handle.
```typescript ```typescript
class JSHandle { class JSHandle {
abstract getProperties(): Promise<Map<string, JSHandle<unknown>>>; getProperties(): Promise<Map<string, JSHandle>>;
} }
``` ```
**Returns:** **Returns:**
Promise&lt;Map&lt;string, [JSHandle](./puppeteer.jshandle.md)&lt;unknown&gt;&gt;&gt; Promise&lt;Map&lt;string, [JSHandle](./puppeteer.jshandle.md)&gt;&gt;
## Example ## Example

View File

@ -10,7 +10,7 @@ Fetches a single property from the referenced object.
```typescript ```typescript
class JSHandle { class JSHandle {
abstract getProperty<K extends keyof T>( getProperty<K extends keyof T>(
propertyName: HandleOr<K> propertyName: HandleOr<K>
): Promise<HandleFor<T[K]>>; ): Promise<HandleFor<T[K]>>;
} }

View File

@ -8,7 +8,7 @@ sidebar_label: JSHandle.getProperty_1
```typescript ```typescript
class JSHandle { class JSHandle {
abstract getProperty(propertyName: string): Promise<JSHandle<unknown>>; getProperty(propertyName: string): Promise<JSHandle<unknown>>;
} }
``` ```

View File

@ -142,7 +142,7 @@ export abstract class ElementHandle<
/** /**
* @internal * @internal
*/ */
protected handle; protected readonly handle;
/** /**
* @internal * @internal
@ -208,7 +208,7 @@ export abstract class ElementHandle<
/** /**
* @internal * @internal
*/ */
override evaluateHandle< override async evaluateHandle<
Params extends unknown[], Params extends unknown[],
Func extends EvaluateFuncWith<ElementType, Params> = EvaluateFuncWith< Func extends EvaluateFuncWith<ElementType, Params> = EvaluateFuncWith<
ElementType, ElementType,
@ -218,7 +218,7 @@ export abstract class ElementHandle<
pageFunction: Func | string, pageFunction: Func | string,
...args: Params ...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>> { ): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
return this.handle.evaluateHandle(pageFunction, ...args); return await this.handle.evaluateHandle(pageFunction, ...args);
} }
/** /**
@ -1269,11 +1269,6 @@ export abstract class ElementHandle<
}); });
} }
/**
* @internal
*/
abstract assertElementHasWorld(): asserts this;
/** /**
* If the element is a form input, you can use {@link ElementHandle.autofill} * If the element is a form input, you can use {@link ElementHandle.autofill}
* to test if the form is compatible with the browser's autofill * to test if the form is compatible with the browser's autofill

View File

@ -0,0 +1,27 @@
/**
* Copyright 2023 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {CDPSession} from '../common/Connection.js';
import {Realm} from './Realm.js';
/**
* @internal
*/
export interface Environment {
get client(): CDPSession;
mainRealm(): Realm;
}

View File

@ -20,7 +20,6 @@ import {Page, WaitTimeoutOptions} from '../api/Page.js';
import {CDPSession} from '../common/Connection.js'; import {CDPSession} from '../common/Connection.js';
import {DeviceRequestPrompt} from '../common/DeviceRequestPrompt.js'; import {DeviceRequestPrompt} from '../common/DeviceRequestPrompt.js';
import {EventEmitter} from '../common/EventEmitter.js'; import {EventEmitter} from '../common/EventEmitter.js';
import {ExecutionContext} from '../common/ExecutionContext.js';
import {getQueryHandlerAndSelector} from '../common/GetQueryHandler.js'; import {getQueryHandlerAndSelector} from '../common/GetQueryHandler.js';
import {transposeIterableHandle} from '../common/HandleIterator.js'; import {transposeIterableHandle} from '../common/HandleIterator.js';
import { import {
@ -309,14 +308,7 @@ export abstract class Frame extends EventEmitter {
/** /**
* @internal * @internal
*/ */
abstract _client(): CDPSession; abstract get client(): CDPSession;
/**
* @internal
*/
executionContext(): Promise<ExecutionContext> {
throw new Error('Not implemented');
}
/** /**
* @internal * @internal
@ -475,7 +467,7 @@ export abstract class Frame extends EventEmitter {
* @returns A promise to the result of the function. * @returns A promise to the result of the function.
*/ */
@throwIfDetached @throwIfDetached
$eval< async $eval<
Selector extends string, Selector extends string,
Params extends unknown[], Params extends unknown[],
Func extends EvaluateFuncWith<NodeFor<Selector>, Params> = EvaluateFuncWith< Func extends EvaluateFuncWith<NodeFor<Selector>, Params> = EvaluateFuncWith<
@ -488,7 +480,7 @@ export abstract class Frame extends EventEmitter {
...args: Params ...args: Params
): Promise<Awaited<ReturnType<Func>>> { ): Promise<Awaited<ReturnType<Func>>> {
pageFunction = withSourcePuppeteerURLIfNone(this.$eval.name, pageFunction); pageFunction = withSourcePuppeteerURLIfNone(this.$eval.name, pageFunction);
return this.mainRealm().$eval(selector, pageFunction, ...args); return await this.mainRealm().$eval(selector, pageFunction, ...args);
} }
/** /**
@ -512,7 +504,7 @@ export abstract class Frame extends EventEmitter {
* @returns A promise to the result of the function. * @returns A promise to the result of the function.
*/ */
@throwIfDetached @throwIfDetached
$$eval< async $$eval<
Selector extends string, Selector extends string,
Params extends unknown[], Params extends unknown[],
Func extends EvaluateFuncWith< Func extends EvaluateFuncWith<
@ -525,7 +517,7 @@ export abstract class Frame extends EventEmitter {
...args: Params ...args: Params
): Promise<Awaited<ReturnType<Func>>> { ): Promise<Awaited<ReturnType<Func>>> {
pageFunction = withSourcePuppeteerURLIfNone(this.$$eval.name, pageFunction); pageFunction = withSourcePuppeteerURLIfNone(this.$$eval.name, pageFunction);
return this.mainRealm().$$eval(selector, pageFunction, ...args); return await this.mainRealm().$$eval(selector, pageFunction, ...args);
} }
/** /**
@ -539,8 +531,8 @@ export abstract class Frame extends EventEmitter {
* @param expression - the XPath expression to evaluate. * @param expression - the XPath expression to evaluate.
*/ */
@throwIfDetached @throwIfDetached
$x(expression: string): Promise<Array<ElementHandle<Node>>> { async $x(expression: string): Promise<Array<ElementHandle<Node>>> {
return this.mainRealm().$x(expression); return await this.mainRealm().$x(expression);
} }
/** /**
@ -659,7 +651,7 @@ export abstract class Frame extends EventEmitter {
* @returns the promise which resolve when the `pageFunction` returns a truthy value. * @returns the promise which resolve when the `pageFunction` returns a truthy value.
*/ */
@throwIfDetached @throwIfDetached
waitForFunction< async waitForFunction<
Params extends unknown[], Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>, Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>( >(
@ -667,18 +659,18 @@ export abstract class Frame extends EventEmitter {
options: FrameWaitForFunctionOptions = {}, options: FrameWaitForFunctionOptions = {},
...args: Params ...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>> { ): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
return this.mainRealm().waitForFunction( return await (this.mainRealm().waitForFunction(
pageFunction, pageFunction,
options, options,
...args ...args
) as Promise<HandleFor<Awaited<ReturnType<Func>>>>; ) as Promise<HandleFor<Awaited<ReturnType<Func>>>>);
} }
/** /**
* The full HTML contents of the frame, including the DOCTYPE. * The full HTML contents of the frame, including the DOCTYPE.
*/ */
@throwIfDetached @throwIfDetached
content(): Promise<string> { async content(): Promise<string> {
return this.isolatedRealm().content(); return await this.isolatedRealm().content();
} }
/** /**
@ -922,8 +914,11 @@ export abstract class Frame extends EventEmitter {
* @param selector - The selector to query for. * @param selector - The selector to query for.
*/ */
@throwIfDetached @throwIfDetached
click(selector: string, options: Readonly<ClickOptions> = {}): Promise<void> { async click(
return this.isolatedRealm().click(selector, options); selector: string,
options: Readonly<ClickOptions> = {}
): Promise<void> {
return await this.isolatedRealm().click(selector, options);
} }
/** /**
@ -933,8 +928,8 @@ export abstract class Frame extends EventEmitter {
* @throws Throws if there's no element matching `selector`. * @throws Throws if there's no element matching `selector`.
*/ */
@throwIfDetached @throwIfDetached
focus(selector: string): Promise<void> { async focus(selector: string): Promise<void> {
return this.isolatedRealm().focus(selector); return await this.isolatedRealm().focus(selector);
} }
/** /**
@ -945,8 +940,8 @@ export abstract class Frame extends EventEmitter {
* @throws Throws if there's no element matching `selector`. * @throws Throws if there's no element matching `selector`.
*/ */
@throwIfDetached @throwIfDetached
hover(selector: string): Promise<void> { async hover(selector: string): Promise<void> {
return this.isolatedRealm().hover(selector); return await this.isolatedRealm().hover(selector);
} }
/** /**
@ -968,8 +963,8 @@ export abstract class Frame extends EventEmitter {
* @throws Throws if there's no `<select>` matching `selector`. * @throws Throws if there's no `<select>` matching `selector`.
*/ */
@throwIfDetached @throwIfDetached
select(selector: string, ...values: string[]): Promise<string[]> { async select(selector: string, ...values: string[]): Promise<string[]> {
return this.isolatedRealm().select(selector, ...values); return await this.isolatedRealm().select(selector, ...values);
} }
/** /**
@ -979,8 +974,8 @@ export abstract class Frame extends EventEmitter {
* @throws Throws if there's no element matching `selector`. * @throws Throws if there's no element matching `selector`.
*/ */
@throwIfDetached @throwIfDetached
tap(selector: string): Promise<void> { async tap(selector: string): Promise<void> {
return this.isolatedRealm().tap(selector); return await this.isolatedRealm().tap(selector);
} }
/** /**
@ -1005,12 +1000,12 @@ export abstract class Frame extends EventEmitter {
* between key presses in milliseconds. Defaults to `0`. * between key presses in milliseconds. Defaults to `0`.
*/ */
@throwIfDetached @throwIfDetached
type( async type(
selector: string, selector: string,
text: string, text: string,
options?: Readonly<KeyboardTypeOptions> options?: Readonly<KeyboardTypeOptions>
): Promise<void> { ): Promise<void> {
return this.isolatedRealm().type(selector, text, options); return await this.isolatedRealm().type(selector, text, options);
} }
/** /**
@ -1033,8 +1028,8 @@ export abstract class Frame extends EventEmitter {
* *
* @param milliseconds - the number of milliseconds to wait. * @param milliseconds - the number of milliseconds to wait.
*/ */
waitForTimeout(milliseconds: number): Promise<void> { async waitForTimeout(milliseconds: number): Promise<void> {
return new Promise(resolve => { return await new Promise(resolve => {
setTimeout(resolve, milliseconds); setTimeout(resolve, milliseconds);
}); });
} }
@ -1043,8 +1038,8 @@ export abstract class Frame extends EventEmitter {
* The frame's title. * The frame's title.
*/ */
@throwIfDetached @throwIfDetached
title(): Promise<string> { async title(): Promise<string> {
return this.isolatedRealm().title(); return await this.isolatedRealm().title();
} }
/** /**

View File

@ -23,10 +23,11 @@ import {
HandleOr, HandleOr,
Moveable, Moveable,
} from '../common/types.js'; } from '../common/types.js';
import {debugError} from '../common/util.js'; import {debugError, withSourcePuppeteerURLIfNone} from '../common/util.js';
import {moveable} from '../util/decorators.js'; import {moveable} from '../util/decorators.js';
import {ElementHandle} from './ElementHandle.js'; import {ElementHandle} from './ElementHandle.js';
import {Realm} from './Realm.js';
/** /**
* Represents a reference to a JavaScript object. Instances can be created using * Represents a reference to a JavaScript object. Instances can be created using
@ -65,6 +66,11 @@ export abstract class JSHandle<T = unknown>
*/ */
constructor() {} constructor() {}
/**
* @internal
*/
abstract get realm(): Realm;
/** /**
* @internal * @internal
*/ */
@ -75,33 +81,56 @@ export abstract class JSHandle<T = unknown>
/** /**
* Evaluates the given function with the current handle as its first argument. * Evaluates the given function with the current handle as its first argument.
*/ */
abstract evaluate< async evaluate<
Params extends unknown[], Params extends unknown[],
Func extends EvaluateFuncWith<T, Params> = EvaluateFuncWith<T, Params>, Func extends EvaluateFuncWith<T, Params> = EvaluateFuncWith<T, Params>,
>( >(
pageFunction: Func | string, pageFunction: Func | string,
...args: Params ...args: Params
): Promise<Awaited<ReturnType<Func>>>; ): Promise<Awaited<ReturnType<Func>>> {
pageFunction = withSourcePuppeteerURLIfNone(
this.evaluate.name,
pageFunction
);
return await this.realm.evaluate(pageFunction, this, ...args);
}
/** /**
* Evaluates the given function with the current handle as its first argument. * Evaluates the given function with the current handle as its first argument.
* *
*/ */
abstract evaluateHandle< async evaluateHandle<
Params extends unknown[], Params extends unknown[],
Func extends EvaluateFuncWith<T, Params> = EvaluateFuncWith<T, Params>, Func extends EvaluateFuncWith<T, Params> = EvaluateFuncWith<T, Params>,
>( >(
pageFunction: Func | string, pageFunction: Func | string,
...args: Params ...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>>; ): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
pageFunction = withSourcePuppeteerURLIfNone(
this.evaluateHandle.name,
pageFunction
);
return await this.realm.evaluateHandle(pageFunction, this, ...args);
}
/** /**
* Fetches a single property from the referenced object. * Fetches a single property from the referenced object.
*/ */
abstract getProperty<K extends keyof T>( getProperty<K extends keyof T>(
propertyName: HandleOr<K> propertyName: HandleOr<K>
): Promise<HandleFor<T[K]>>; ): Promise<HandleFor<T[K]>>;
abstract getProperty(propertyName: string): Promise<JSHandle<unknown>>; getProperty(propertyName: string): Promise<JSHandle<unknown>>;
/**
* @internal
*/
async getProperty<K extends keyof T>(
propertyName: HandleOr<K>
): Promise<HandleFor<T[K]>> {
return await this.evaluateHandle((object, propertyName) => {
return object[propertyName as K];
}, propertyName);
}
/** /**
* Gets a map of handles representing the properties of the current handle. * Gets a map of handles representing the properties of the current handle.
@ -121,7 +150,31 @@ export abstract class JSHandle<T = unknown>
* children; // holds elementHandles to all children of document.body * children; // holds elementHandles to all children of document.body
* ``` * ```
*/ */
abstract getProperties(): Promise<Map<string, JSHandle<unknown>>>; async getProperties(): Promise<Map<string, JSHandle>> {
const propertyNames = await this.evaluate(object => {
const enumerableProperties = [];
const descriptors = Object.getOwnPropertyDescriptors(object);
for (const propertyName in descriptors) {
if (descriptors[propertyName]?.enumerable) {
enumerableProperties.push(propertyName);
}
}
return enumerableProperties;
});
const map = new Map<string, JSHandle>();
const results = await Promise.all(
propertyNames.map(key => {
return this.getProperty(key);
})
);
for (const [key, value] of Object.entries(propertyNames)) {
using handle = results[key as any];
if (handle) {
map.set(value, handle.move());
}
}
return map;
}
/** /**
* A vanilla object representing the serializable portions of the * A vanilla object representing the serializable portions of the

View File

@ -27,6 +27,7 @@ import {TaskManager, WaitTask} from '../common/WaitTask.js';
import {assert} from '../util/assert.js'; import {assert} from '../util/assert.js';
import {ClickOptions, ElementHandle} from './ElementHandle.js'; import {ClickOptions, ElementHandle} from './ElementHandle.js';
import {Environment} from './Environment.js';
import {KeyboardTypeOptions} from './Input.js'; import {KeyboardTypeOptions} from './Input.js';
import {JSHandle} from './JSHandle.js'; import {JSHandle} from './JSHandle.js';
@ -34,20 +35,14 @@ import {JSHandle} from './JSHandle.js';
* @internal * @internal
*/ */
export abstract class Realm implements Disposable { export abstract class Realm implements Disposable {
#timeoutSettings: TimeoutSettings; protected readonly timeoutSettings: TimeoutSettings;
#taskManager = new TaskManager(); readonly taskManager = new TaskManager();
constructor(timeoutSettings: TimeoutSettings) { constructor(timeoutSettings: TimeoutSettings) {
this.#timeoutSettings = timeoutSettings; this.timeoutSettings = timeoutSettings;
} }
protected get timeoutSettings(): TimeoutSettings { abstract get environment(): Environment;
return this.#timeoutSettings;
}
get taskManager(): TaskManager {
return this.#taskManager;
}
abstract adoptHandle<T extends JSHandle<Node>>(handle: T): Promise<T>; abstract adoptHandle<T extends JSHandle<Node>>(handle: T): Promise<T>;
abstract transferHandle<T extends JSHandle<Node>>(handle: T): Promise<T>; abstract transferHandle<T extends JSHandle<Node>>(handle: T): Promise<T>;
@ -136,7 +131,7 @@ export abstract class Realm implements Disposable {
}); });
} }
waitForFunction< async waitForFunction<
Params extends unknown[], Params extends unknown[],
Func extends EvaluateFunc<InnerLazyParams<Params>> = EvaluateFunc< Func extends EvaluateFunc<InnerLazyParams<Params>> = EvaluateFunc<
InnerLazyParams<Params> InnerLazyParams<Params>
@ -153,7 +148,7 @@ export abstract class Realm implements Disposable {
): Promise<HandleFor<Awaited<ReturnType<Func>>>> { ): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
const { const {
polling = 'raf', polling = 'raf',
timeout = this.#timeoutSettings.timeout(), timeout = this.timeoutSettings.timeout(),
root, root,
signal, signal,
} = options; } = options;
@ -173,7 +168,7 @@ export abstract class Realm implements Disposable {
| string, | string,
...args ...args
); );
return waitTask.result; return await waitTask.result;
} }
async click( async click(

View File

@ -21,7 +21,7 @@ import {assert} from '../util/assert.js';
import {AsyncIterableUtil} from '../util/AsyncIterableUtil.js'; import {AsyncIterableUtil} from '../util/AsyncIterableUtil.js';
import {CDPSession} from './Connection.js'; import {CDPSession} from './Connection.js';
import {CDPJSHandle} from './JSHandle.js'; import {IsolatedWorld} from './IsolatedWorld.js';
import {QueryHandler, QuerySelector} from './QueryHandler.js'; import {QueryHandler, QuerySelector} from './QueryHandler.js';
import {AwaitableIterable} from './types.js'; import {AwaitableIterable} from './types.js';
@ -106,16 +106,17 @@ export class ARIAQueryHandler extends QueryHandler {
element: ElementHandle<Node>, element: ElementHandle<Node>,
selector: string selector: string
): AwaitableIterable<ElementHandle<Node>> { ): AwaitableIterable<ElementHandle<Node>> {
const context = (
element as unknown as CDPJSHandle<Node>
).executionContext();
const {name, role} = parseARIASelector(selector); const {name, role} = parseARIASelector(selector);
const results = await queryAXTree(context._client, element, name, role); const results = await queryAXTree(
const world = context._world!; element.realm.environment.client,
element,
name,
role
);
yield* AsyncIterableUtil.map(results, node => { yield* AsyncIterableUtil.map(results, node => {
return world.adoptBackendNode(node.backendDOMNodeId) as Promise< return (element.realm as IsolatedWorld).adoptBackendNode(
ElementHandle<Node> node.backendDOMNodeId
>; ) as Promise<ElementHandle<Node>>;
}); });
} }

View File

@ -22,12 +22,10 @@ import {assert} from '../util/assert.js';
import {throwIfDisposed} from '../util/decorators.js'; import {throwIfDisposed} from '../util/decorators.js';
import {CDPSession} from './Connection.js'; import {CDPSession} from './Connection.js';
import {ExecutionContext} from './ExecutionContext.js';
import {CDPFrame} from './Frame.js'; import {CDPFrame} from './Frame.js';
import {FrameManager} from './FrameManager.js'; import {FrameManager} from './FrameManager.js';
import {WaitForSelectorOptions} from './IsolatedWorld.js'; import {IsolatedWorld} from './IsolatedWorld.js';
import {CDPJSHandle} from './JSHandle.js'; import {CDPJSHandle} from './JSHandle.js';
import {NodeFor} from './types.js';
import {debugError} from './util.js'; import {debugError} from './util.js';
/** /**
@ -40,20 +38,17 @@ import {debugError} from './util.js';
export class CDPElementHandle< export class CDPElementHandle<
ElementType extends Node = Element, ElementType extends Node = Element,
> extends ElementHandle<ElementType> { > extends ElementHandle<ElementType> {
#frame: CDPFrame; protected declare readonly handle: CDPJSHandle<ElementType>;
declare handle: CDPJSHandle<ElementType>;
constructor( constructor(
context: ExecutionContext, world: IsolatedWorld,
remoteObject: Protocol.Runtime.RemoteObject, remoteObject: Protocol.Runtime.RemoteObject
frame: CDPFrame
) { ) {
super(new CDPJSHandle(context, remoteObject)); super(new CDPJSHandle(world, remoteObject));
this.#frame = frame;
} }
executionContext(): ExecutionContext { override get realm(): IsolatedWorld {
return this.handle.executionContext(); return this.handle.realm;
} }
get client(): CDPSession { get client(): CDPSession {
@ -65,48 +60,21 @@ export class CDPElementHandle<
} }
get #frameManager(): FrameManager { get #frameManager(): FrameManager {
return this.#frame._frameManager; return this.frame._frameManager;
} }
get #page(): Page { get #page(): Page {
return this.#frame.page(); return this.frame.page();
} }
override get frame(): CDPFrame { override get frame(): CDPFrame {
return this.#frame; return this.realm.environment as CDPFrame;
}
@throwIfDisposed()
override async $<Selector extends string>(
selector: Selector
): Promise<CDPElementHandle<NodeFor<Selector>> | null> {
return await (super.$(selector) as Promise<CDPElementHandle<
NodeFor<Selector>
> | null>);
}
@throwIfDisposed()
override async $$<Selector extends string>(
selector: Selector
): Promise<Array<CDPElementHandle<NodeFor<Selector>>>> {
return await (super.$$(selector) as Promise<
Array<CDPElementHandle<NodeFor<Selector>>>
>);
}
@throwIfDisposed()
override async waitForSelector<Selector extends string>(
selector: Selector,
options?: WaitForSelectorOptions
): Promise<CDPElementHandle<NodeFor<Selector>> | null> {
return (await super.waitForSelector(selector, options)) as CDPElementHandle<
NodeFor<Selector>
> | null;
} }
override async contentFrame( override async contentFrame(
this: ElementHandle<HTMLIFrameElement> this: ElementHandle<HTMLIFrameElement>
): Promise<CDPFrame>; ): Promise<CDPFrame>;
@throwIfDisposed() @throwIfDisposed()
override async contentFrame(): Promise<CDPFrame | null> { override async contentFrame(): Promise<CDPFrame | null> {
const nodeInfo = await this.client.send('DOM.describeNode', { const nodeInfo = await this.client.send('DOM.describeNode', {
@ -320,15 +288,11 @@ export class CDPElementHandle<
objectId: this.handle.id, objectId: this.handle.id,
}); });
const fieldId = nodeInfo.node.backendNodeId; const fieldId = nodeInfo.node.backendNodeId;
const frameId = this.#frame._id; const frameId = this.frame._id;
await this.client.send('Autofill.trigger', { await this.client.send('Autofill.trigger', {
fieldId, fieldId,
frameId, frameId,
card: data.creditCard, card: data.creditCard,
}); });
} }
override assertElementHasWorld(): asserts this {
assert(this.executionContext()._world);
}
} }

View File

@ -34,7 +34,7 @@ import {EvaluateFunc, HandleFor} from './types.js';
import { import {
PuppeteerURL, PuppeteerURL,
createEvaluationError, createEvaluationError,
createJSHandle, createCdpHandle,
getSourcePuppeteerURLIfAvailable, getSourcePuppeteerURLIfAvailable,
isString, isString,
valueFromRemoteObject, valueFromRemoteObject,
@ -70,14 +70,14 @@ const getSourceUrlComment = (url: string) => {
*/ */
export class ExecutionContext { export class ExecutionContext {
_client: CDPSession; _client: CDPSession;
_world?: IsolatedWorld; _world: IsolatedWorld;
_contextId: number; _contextId: number;
_contextName?: string; _contextName?: string;
constructor( constructor(
client: CDPSession, client: CDPSession,
contextPayload: Protocol.Runtime.ExecutionContextDescription, contextPayload: Protocol.Runtime.ExecutionContextDescription,
world?: IsolatedWorld world: IsolatedWorld
) { ) {
this._client = client; this._client = client;
this._world = world; this._world = world;
@ -105,14 +105,12 @@ export class ExecutionContext {
selector: string selector: string
): Promise<JSHandle<Node[]>> => { ): Promise<JSHandle<Node[]>> => {
const results = ARIAQueryHandler.queryAll(element, selector); const results = ARIAQueryHandler.queryAll(element, selector);
return await (element as unknown as CDPJSHandle<Node>) return await element.realm.evaluateHandle(
.executionContext() (...elements) => {
.evaluateHandle( return elements;
(...elements) => { },
return elements; ...(await AsyncIterableUtil.collect(results))
}, );
...(await AsyncIterableUtil.collect(results))
);
}) as (...args: unknown[]) => unknown) }) as (...args: unknown[]) => unknown)
), ),
]); ]);
@ -305,7 +303,7 @@ export class ExecutionContext {
return returnByValue return returnByValue
? valueFromRemoteObject(remoteObject) ? valueFromRemoteObject(remoteObject)
: createJSHandle(this, remoteObject); : createCdpHandle(this._world, remoteObject);
} }
const functionDeclaration = stringifyFunction(pageFunction); const functionDeclaration = stringifyFunction(pageFunction);
@ -340,7 +338,7 @@ export class ExecutionContext {
} }
return returnByValue return returnByValue
? valueFromRemoteObject(remoteObject) ? valueFromRemoteObject(remoteObject)
: createJSHandle(this, remoteObject); : createCdpHandle(this._world, remoteObject);
async function convertArgument( async function convertArgument(
this: ExecutionContext, this: ExecutionContext,
@ -370,7 +368,7 @@ export class ExecutionContext {
? arg ? arg
: null; : null;
if (objectHandle) { if (objectHandle) {
if (objectHandle.executionContext() !== this) { if (objectHandle.realm !== this._world) {
throw new Error( throw new Error(
'JSHandles can be evaluated only in the context they were created!' 'JSHandles can be evaluated only in the context they were created!'
); );

View File

@ -28,7 +28,6 @@ import {
DeviceRequestPrompt, DeviceRequestPrompt,
DeviceRequestPromptManager, DeviceRequestPromptManager,
} from './DeviceRequestPrompt.js'; } from './DeviceRequestPrompt.js';
import {ExecutionContext} from './ExecutionContext.js';
import {FrameManager} from './FrameManager.js'; import {FrameManager} from './FrameManager.js';
import {IsolatedWorld} from './IsolatedWorld.js'; import {IsolatedWorld} from './IsolatedWorld.js';
import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js'; import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js';
@ -99,8 +98,14 @@ export class CDPFrame extends Frame {
this.#client = client; this.#client = client;
if (!keepWorlds) { if (!keepWorlds) {
this.worlds = { this.worlds = {
[MAIN_WORLD]: new IsolatedWorld(this), [MAIN_WORLD]: new IsolatedWorld(
[PUPPETEER_WORLD]: new IsolatedWorld(this), this,
this._frameManager.timeoutSettings
),
[PUPPETEER_WORLD]: new IsolatedWorld(
this,
this._frameManager.timeoutSettings
),
}; };
} else { } else {
this.worlds[MAIN_WORLD].frameUpdated(); this.worlds[MAIN_WORLD].frameUpdated();
@ -232,14 +237,10 @@ export class CDPFrame extends Frame {
} }
} }
override _client(): CDPSession { override get client(): CDPSession {
return this.#client; return this.#client;
} }
override executionContext(): Promise<ExecutionContext> {
return this.worlds[MAIN_WORLD].executionContext();
}
override mainRealm(): IsolatedWorld { override mainRealm(): IsolatedWorld {
return this.worlds[MAIN_WORLD]; return this.worlds[MAIN_WORLD];
} }
@ -281,10 +282,12 @@ export class CDPFrame extends Frame {
} }
@throwIfDetached @throwIfDetached
override waitForDevicePrompt( override async waitForDevicePrompt(
options: WaitTimeoutOptions = {} options: WaitTimeoutOptions = {}
): Promise<DeviceRequestPrompt> { ): Promise<DeviceRequestPrompt> {
return this.#deviceRequestPromptManager().waitForDevicePrompt(options); return await this.#deviceRequestPromptManager().waitForDevicePrompt(
options
);
} }
_navigated(framePayload: Protocol.Page.Frame): void { _navigated(framePayload: Protocol.Page.Frame): void {

View File

@ -431,7 +431,7 @@ export class FrameManager extends EventEmitter {
await Promise.all( await Promise.all(
this.frames() this.frames()
.filter(frame => { .filter(frame => {
return frame._client() === session; return frame.client === session;
}) })
.map(frame => { .map(frame => {
// Frames might be removed before we send this, so we don't want to // Frames might be removed before we send this, so we don't want to
@ -489,7 +489,7 @@ export class FrameManager extends EventEmitter {
let world: IsolatedWorld | undefined; let world: IsolatedWorld | undefined;
if (frame) { if (frame) {
// Only care about execution contexts created for the current session. // Only care about execution contexts created for the current session.
if (frame._client() !== session) { if (frame.client !== session) {
return; return;
} }
if (contextPayload.auxData && contextPayload.auxData['isDefault']) { if (contextPayload.auxData && contextPayload.auxData['isDefault']) {
@ -504,8 +504,12 @@ export class FrameManager extends EventEmitter {
world = frame.worlds[PUPPETEER_WORLD]; world = frame.worlds[PUPPETEER_WORLD];
} }
} }
// If there is no world, the context is not meant to be handled by us.
if (!world) {
return;
}
const context = new ExecutionContext( const context = new ExecutionContext(
frame?._client() || this.#client, frame?.client || this.#client,
contextPayload, contextPayload,
world world
); );

View File

@ -18,25 +18,26 @@ import {Protocol} from 'devtools-protocol';
import {JSHandle} from '../api/JSHandle.js'; import {JSHandle} from '../api/JSHandle.js';
import {Realm} from '../api/Realm.js'; import {Realm} from '../api/Realm.js';
import {assert} from '../util/assert.js';
import {Deferred} from '../util/Deferred.js'; import {Deferred} from '../util/Deferred.js';
import {Binding} from './Binding.js'; import {Binding} from './Binding.js';
import {CDPSession} from './Connection.js'; import {CDPSession} from './Connection.js';
import {ExecutionContext} from './ExecutionContext.js'; import {ExecutionContext} from './ExecutionContext.js';
import {CDPFrame} from './Frame.js'; import {CDPFrame} from './Frame.js';
import {FrameManager} from './FrameManager.js';
import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js'; import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js';
import {CDPJSHandle} from './JSHandle.js';
import {LifecycleWatcher, PuppeteerLifeCycleEvent} from './LifecycleWatcher.js'; import {LifecycleWatcher, PuppeteerLifeCycleEvent} from './LifecycleWatcher.js';
import {TimeoutSettings} from './TimeoutSettings.js';
import {BindingPayload, EvaluateFunc, HandleFor} from './types.js'; import {BindingPayload, EvaluateFunc, HandleFor} from './types.js';
import { import {
Mutex,
addPageBinding, addPageBinding,
createJSHandle, createCdpHandle,
debugError, debugError,
Mutex,
setPageContent, setPageContent,
withSourcePuppeteerURLIfNone, withSourcePuppeteerURLIfNone,
} from './util.js'; } from './util.js';
import {WebWorker} from './WebWorker.js';
/** /**
* @public * @public
@ -91,7 +92,6 @@ export interface IsolatedWorldChart {
* @internal * @internal
*/ */
export class IsolatedWorld extends Realm { export class IsolatedWorld extends Realm {
#frame: CDPFrame;
#context = Deferred.create<ExecutionContext>(); #context = Deferred.create<ExecutionContext>();
// Set of bindings that have been registered in the current context. // Set of bindings that have been registered in the current context.
@ -104,26 +104,32 @@ export class IsolatedWorld extends Realm {
return this.#bindings; return this.#bindings;
} }
constructor(frame: CDPFrame) { readonly #frameOrWorker: CDPFrame | WebWorker;
super(frame._frameManager.timeoutSettings);
this.#frame = frame; constructor(
frameOrWorker: CDPFrame | WebWorker,
timeoutSettings: TimeoutSettings
) {
super(timeoutSettings);
this.#frameOrWorker = frameOrWorker;
this.frameUpdated(); this.frameUpdated();
} }
get environment(): CDPFrame | WebWorker {
return this.#frameOrWorker;
}
frameUpdated(): void { frameUpdated(): void {
this.#client.on('Runtime.bindingCalled', this.#onBindingCalled); this.client.on('Runtime.bindingCalled', this.#onBindingCalled);
} }
get #client(): CDPSession { get client(): CDPSession {
return this.#frame._client(); return this.#frameOrWorker.client;
} }
get #frameManager(): FrameManager { get #frame(): CDPFrame {
return this.#frame._frameManager; assert(this.#frameOrWorker instanceof CDPFrame);
} return this.#frameOrWorker;
frame(): CDPFrame {
return this.#frame;
} }
clearContext(): void { clearContext(): void {
@ -140,10 +146,10 @@ export class IsolatedWorld extends Realm {
return this.#context.resolved(); return this.#context.resolved();
} }
executionContext(): Promise<ExecutionContext> { #executionContext(): Promise<ExecutionContext> {
if (this.disposed) { if (this.disposed) {
throw new Error( throw new Error(
`Execution context is not available in detached frame "${this.#frame.url()}" (are you trying to evaluate?)` `Execution context is not available in detached frame "${this.environment.url()}" (are you trying to evaluate?)`
); );
} }
if (this.#context === null) { if (this.#context === null) {
@ -163,7 +169,7 @@ export class IsolatedWorld extends Realm {
this.evaluateHandle.name, this.evaluateHandle.name,
pageFunction pageFunction
); );
const context = await this.executionContext(); const context = await this.#executionContext();
return await context.evaluateHandle(pageFunction, ...args); return await context.evaluateHandle(pageFunction, ...args);
} }
@ -178,7 +184,7 @@ export class IsolatedWorld extends Realm {
this.evaluate.name, this.evaluate.name,
pageFunction pageFunction
); );
const context = await this.executionContext(); const context = await this.#executionContext();
return await context.evaluate(pageFunction, ...args); return await context.evaluate(pageFunction, ...args);
} }
@ -197,7 +203,7 @@ export class IsolatedWorld extends Realm {
await setPageContent(this, html); await setPageContent(this, html);
const watcher = new LifecycleWatcher( const watcher = new LifecycleWatcher(
this.#frameManager.networkManager, this.#frame._frameManager.networkManager,
this.#frame, this.#frame,
waitUntil, waitUntil,
timeout timeout
@ -296,40 +302,33 @@ export class IsolatedWorld extends Realm {
async adoptBackendNode( async adoptBackendNode(
backendNodeId?: Protocol.DOM.BackendNodeId backendNodeId?: Protocol.DOM.BackendNodeId
): Promise<JSHandle<Node>> { ): Promise<JSHandle<Node>> {
const executionContext = await this.executionContext(); const executionContext = await this.#executionContext();
const {object} = await this.#client.send('DOM.resolveNode', { const {object} = await this.client.send('DOM.resolveNode', {
backendNodeId: backendNodeId, backendNodeId: backendNodeId,
executionContextId: executionContext._contextId, executionContextId: executionContext._contextId,
}); });
return createJSHandle(executionContext, object) as JSHandle<Node>; return createCdpHandle(this, object) as JSHandle<Node>;
} }
async adoptHandle<T extends JSHandle<Node>>(handle: T): Promise<T> { async adoptHandle<T extends JSHandle<Node>>(handle: T): Promise<T> {
const context = await this.executionContext(); if (handle.realm === this) {
if (
(handle as unknown as CDPJSHandle<Node>).executionContext() === context
) {
// If the context has already adopted this handle, clone it so downstream // If the context has already adopted this handle, clone it so downstream
// disposal doesn't become an issue. // disposal doesn't become an issue.
return (await handle.evaluateHandle(value => { return (await handle.evaluateHandle(value => {
return value; return value;
// SAFETY: We know the
})) as unknown as T; })) as unknown as T;
} }
const nodeInfo = await this.#client.send('DOM.describeNode', { const nodeInfo = await this.client.send('DOM.describeNode', {
objectId: handle.id, objectId: handle.id,
}); });
return (await this.adoptBackendNode(nodeInfo.node.backendNodeId)) as T; return (await this.adoptBackendNode(nodeInfo.node.backendNodeId)) as T;
} }
async transferHandle<T extends JSHandle<Node>>(handle: T): Promise<T> { async transferHandle<T extends JSHandle<Node>>(handle: T): Promise<T> {
const context = await this.executionContext(); if (handle.realm === this) {
if (
(handle as unknown as CDPJSHandle<Node>).executionContext() === context
) {
return handle; return handle;
} }
const info = await this.#client.send('DOM.describeNode', { const info = await this.client.send('DOM.describeNode', {
objectId: handle.remoteObject().objectId, objectId: handle.remoteObject().objectId,
}); });
const newHandle = (await this.adoptBackendNode( const newHandle = (await this.adoptBackendNode(
@ -341,6 +340,6 @@ export class IsolatedWorld extends Realm {
[Symbol.dispose](): void { [Symbol.dispose](): void {
super[Symbol.dispose](); super[Symbol.dispose]();
this.#client.off('Runtime.bindingCalled', this.#onBindingCalled); this.client.off('Runtime.bindingCalled', this.#onBindingCalled);
} }
} }

View File

@ -17,114 +17,39 @@
import {Protocol} from 'devtools-protocol'; import {Protocol} from 'devtools-protocol';
import {JSHandle} from '../api/JSHandle.js'; import {JSHandle} from '../api/JSHandle.js';
import {assert} from '../util/assert.js';
import {CDPSession} from './Connection.js'; import {CDPSession} from './Connection.js';
import type {CDPElementHandle} from './ElementHandle.js'; import type {CDPElementHandle} from './ElementHandle.js';
import {ExecutionContext} from './ExecutionContext.js'; import {IsolatedWorld} from './IsolatedWorld.js';
import {EvaluateFuncWith, HandleFor, HandleOr} from './types.js'; import {releaseObject, valueFromRemoteObject} from './util.js';
import {
createJSHandle,
releaseObject,
valueFromRemoteObject,
withSourcePuppeteerURLIfNone,
} from './util.js';
/** /**
* @internal * @internal
*/ */
export class CDPJSHandle<T = unknown> extends JSHandle<T> { export class CDPJSHandle<T = unknown> extends JSHandle<T> {
#disposed = false; #disposed = false;
#context: ExecutionContext; readonly #remoteObject: Protocol.Runtime.RemoteObject;
#remoteObject: Protocol.Runtime.RemoteObject; readonly #world: IsolatedWorld;
constructor(
world: IsolatedWorld,
remoteObject: Protocol.Runtime.RemoteObject
) {
super();
this.#world = world;
this.#remoteObject = remoteObject;
}
override get disposed(): boolean { override get disposed(): boolean {
return this.#disposed; return this.#disposed;
} }
constructor( override get realm(): IsolatedWorld {
context: ExecutionContext, return this.#world;
remoteObject: Protocol.Runtime.RemoteObject
) {
super();
this.#context = context;
this.#remoteObject = remoteObject;
}
executionContext(): ExecutionContext {
return this.#context;
} }
get client(): CDPSession { get client(): CDPSession {
return this.#context._client; return this.realm.environment.client;
}
/**
* @see {@link ExecutionContext.evaluate} for more details.
*/
override async evaluate<
Params extends unknown[],
Func extends EvaluateFuncWith<T, Params> = EvaluateFuncWith<T, Params>,
>(
pageFunction: Func | string,
...args: Params
): Promise<Awaited<ReturnType<Func>>> {
pageFunction = withSourcePuppeteerURLIfNone(
this.evaluate.name,
pageFunction
);
return await this.executionContext().evaluate(pageFunction, this, ...args);
}
/**
* @see {@link ExecutionContext.evaluateHandle} for more details.
*/
override async evaluateHandle<
Params extends unknown[],
Func extends EvaluateFuncWith<T, Params> = EvaluateFuncWith<T, Params>,
>(
pageFunction: Func | string,
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
pageFunction = withSourcePuppeteerURLIfNone(
this.evaluateHandle.name,
pageFunction
);
return await this.executionContext().evaluateHandle(
pageFunction,
this,
...args
);
}
override async getProperty<K extends keyof T>(
propertyName: HandleOr<K>
): Promise<HandleFor<T[K]>>;
override async getProperty(propertyName: string): Promise<JSHandle<unknown>>;
override async getProperty<K extends keyof T>(
propertyName: HandleOr<K>
): Promise<HandleFor<T[K]>> {
return await this.evaluateHandle((object, propertyName) => {
return object[propertyName as K];
}, propertyName);
}
override async getProperties(): Promise<Map<string, JSHandle>> {
assert(this.#remoteObject.objectId);
// We use Runtime.getProperties rather than iterative building because the
// iterative approach might create a distorted snapshot.
const response = await this.client.send('Runtime.getProperties', {
objectId: this.#remoteObject.objectId,
ownProperties: true,
});
const result = new Map<string, JSHandle>();
for (const property of response.result) {
if (!property.enumerable || !property.value) {
continue;
}
result.set(property.name, createJSHandle(this.#context, property.value));
}
return result;
} }
override async jsonValue(): Promise<T> { override async jsonValue(): Promise<T> {

View File

@ -57,6 +57,7 @@ import {EmulationManager} from './EmulationManager.js';
import {TargetCloseError} from './Errors.js'; import {TargetCloseError} from './Errors.js';
import {Handler} from './EventEmitter.js'; import {Handler} from './EventEmitter.js';
import {FileChooser} from './FileChooser.js'; import {FileChooser} from './FileChooser.js';
import {CDPFrame} from './Frame.js';
import {FrameManager, FrameManagerEmittedEvents} from './FrameManager.js'; import {FrameManager, FrameManagerEmittedEvents} from './FrameManager.js';
import {CDPKeyboard, CDPMouse, CDPTouchscreen} from './Input.js'; import {CDPKeyboard, CDPMouse, CDPTouchscreen} from './Input.js';
import {MAIN_WORLD} from './IsolatedWorlds.js'; import {MAIN_WORLD} from './IsolatedWorlds.js';
@ -74,8 +75,8 @@ import {TimeoutSettings} from './TimeoutSettings.js';
import {Tracing} from './Tracing.js'; import {Tracing} from './Tracing.js';
import {BindingPayload, EvaluateFunc, HandleFor} from './types.js'; import {BindingPayload, EvaluateFunc, HandleFor} from './types.js';
import { import {
createCdpHandle,
createClientError, createClientError,
createJSHandle,
debugError, debugError,
evaluationString, evaluationString,
getReadableAsBuffer, getReadableAsBuffer,
@ -389,7 +390,7 @@ export class CDPPage extends Page {
return this.#emulationManager.javascriptEnabled; return this.#emulationManager.javascriptEnabled;
} }
override waitForFileChooser( override async waitForFileChooser(
options: WaitTimeoutOptions = {} options: WaitTimeoutOptions = {}
): Promise<FileChooser> { ): Promise<FileChooser> {
const needsEnable = this.#fileChooserDeferreds.size === 0; const needsEnable = this.#fileChooserDeferreds.size === 0;
@ -405,14 +406,16 @@ export class CDPPage extends Page {
enabled: true, enabled: true,
}); });
} }
return Promise.all([deferred.valueOrThrow(), enablePromise]) try {
.then(([result]) => { const [result] = await Promise.all([
return result; deferred.valueOrThrow(),
}) enablePromise,
.catch(error => { ]);
this.#fileChooserDeferreds.delete(deferred); return result;
throw error; } catch (error) {
}); this.#fileChooserDeferreds.delete(deferred);
throw error;
}
} }
override async setGeolocation(options: GeolocationOptions): Promise<void> { override async setGeolocation(options: GeolocationOptions): Promise<void> {
@ -450,7 +453,7 @@ export class CDPPage extends Page {
} }
} }
override mainFrame(): Frame { override mainFrame(): CDPFrame {
return this.#frameManager.mainFrame(); return this.#frameManager.mainFrame();
} }
@ -498,14 +501,14 @@ export class CDPPage extends Page {
return await this.#client.send('Input.setInterceptDrags', {enabled}); return await this.#client.send('Input.setInterceptDrags', {enabled});
} }
override setOfflineMode(enabled: boolean): Promise<void> { override async setOfflineMode(enabled: boolean): Promise<void> {
return this.#frameManager.networkManager.setOfflineMode(enabled); return await this.#frameManager.networkManager.setOfflineMode(enabled);
} }
override emulateNetworkConditions( override async emulateNetworkConditions(
networkConditions: NetworkConditions | null networkConditions: NetworkConditions | null
): Promise<void> { ): Promise<void> {
return this.#frameManager.networkManager.emulateNetworkConditions( return await this.#frameManager.networkManager.emulateNetworkConditions(
networkConditions networkConditions
); );
} }
@ -533,23 +536,27 @@ export class CDPPage extends Page {
this.evaluateHandle.name, this.evaluateHandle.name,
pageFunction pageFunction
); );
const context = await this.mainFrame().executionContext(); return await this.mainFrame().evaluateHandle(pageFunction, ...args);
return await context.evaluateHandle(pageFunction, ...args);
} }
override async queryObjects<Prototype>( override async queryObjects<Prototype>(
prototypeHandle: JSHandle<Prototype> prototypeHandle: JSHandle<Prototype>
): Promise<JSHandle<Prototype[]>> { ): Promise<JSHandle<Prototype[]>> {
const context = await this.mainFrame().executionContext();
assert(!prototypeHandle.disposed, 'Prototype JSHandle is disposed!'); assert(!prototypeHandle.disposed, 'Prototype JSHandle is disposed!');
assert( assert(
prototypeHandle.id, prototypeHandle.id,
'Prototype JSHandle must not be referencing primitive value' 'Prototype JSHandle must not be referencing primitive value'
); );
const response = await context._client.send('Runtime.queryObjects', { const response = await this.mainFrame().client.send(
prototypeObjectId: prototypeHandle.id, 'Runtime.queryObjects',
}); {
return createJSHandle(context, response.objects) as HandleFor<Prototype[]>; prototypeObjectId: prototypeHandle.id,
}
);
return createCdpHandle(
this.mainFrame().mainRealm(),
response.objects
) as HandleFor<Prototype[]>;
} }
override async cookies( override async cookies(
@ -771,7 +778,7 @@ export class CDPPage extends Page {
return; return;
} }
const values = event.args.map(arg => { const values = event.args.map(arg => {
return createJSHandle(context, arg); return createCdpHandle(context._world, arg);
}); });
this.#addConsoleMessage(event.type, values, event.stackTrace); this.#addConsoleMessage(event.type, values, event.stackTrace);
} }
@ -1378,10 +1385,10 @@ export class CDPPage extends Page {
* ); * );
* ``` * ```
*/ */
override waitForDevicePrompt( override async waitForDevicePrompt(
options: WaitTimeoutOptions = {} options: WaitTimeoutOptions = {}
): Promise<DeviceRequestPrompt> { ): Promise<DeviceRequestPrompt> {
return this.mainFrame().waitForDevicePrompt(options); return await this.mainFrame().waitForDevicePrompt(options);
} }
} }

View File

@ -106,7 +106,6 @@ export class QueryHandler {
element: ElementHandle<Node>, element: ElementHandle<Node>,
selector: string selector: string
): AwaitableIterable<ElementHandle<Node>> { ): AwaitableIterable<ElementHandle<Node>> {
element.assertElementHasWorld();
using handle = await element.evaluateHandle( using handle = await element.evaluateHandle(
this._querySelectorAll, this._querySelectorAll,
selector, selector,
@ -126,7 +125,6 @@ export class QueryHandler {
element: ElementHandle<Node>, element: ElementHandle<Node>,
selector: string selector: string
): Promise<ElementHandle<Node> | null> { ): Promise<ElementHandle<Node> | null> {
element.assertElementHasWorld();
using result = await element.evaluateHandle( using result = await element.evaluateHandle(
this._querySelector, this._querySelector,
selector, selector,

View File

@ -15,13 +15,15 @@
*/ */
import {Protocol} from 'devtools-protocol'; import {Protocol} from 'devtools-protocol';
import {Deferred} from '../util/Deferred.js'; import {Realm} from '../api/Realm.js';
import {CDPSession} from './Connection.js'; import {CDPSession} from './Connection.js';
import {ConsoleMessageType} from './ConsoleMessage.js'; import {ConsoleMessageType} from './ConsoleMessage.js';
import {EventEmitter} from './EventEmitter.js'; import {EventEmitter} from './EventEmitter.js';
import {ExecutionContext} from './ExecutionContext.js'; import {ExecutionContext} from './ExecutionContext.js';
import {IsolatedWorld} from './IsolatedWorld.js';
import {CDPJSHandle} from './JSHandle.js'; import {CDPJSHandle} from './JSHandle.js';
import {TimeoutSettings} from './TimeoutSettings.js';
import {EvaluateFunc, HandleFor} from './types.js'; import {EvaluateFunc, HandleFor} from './types.js';
import {debugError, withSourcePuppeteerURLIfNone} from './util.js'; import {debugError, withSourcePuppeteerURLIfNone} from './util.js';
@ -68,7 +70,12 @@ export type ExceptionThrownCallback = (
* @public * @public
*/ */
export class WebWorker extends EventEmitter { export class WebWorker extends EventEmitter {
#executionContext = Deferred.create<ExecutionContext>(); /**
* @internal
*/
readonly timeoutSettings = new TimeoutSettings();
#world: IsolatedWorld;
#client: CDPSession; #client: CDPSession;
#url: string; #url: string;
@ -84,18 +91,19 @@ export class WebWorker extends EventEmitter {
super(); super();
this.#client = client; this.#client = client;
this.#url = url; this.#url = url;
this.#world = new IsolatedWorld(this, new TimeoutSettings());
this.#client.once('Runtime.executionContextCreated', async event => { this.#client.once('Runtime.executionContextCreated', async event => {
const context = new ExecutionContext(client, event.context); this.#world.setContext(
this.#executionContext.resolve(context); new ExecutionContext(client, event.context, this.#world)
);
}); });
this.#client.on('Runtime.consoleAPICalled', async event => { this.#client.on('Runtime.consoleAPICalled', async event => {
try { try {
const context = await this.#executionContext.valueOrThrow();
return consoleAPICalled( return consoleAPICalled(
event.type, event.type,
event.args.map((object: Protocol.Runtime.RemoteObject) => { event.args.map((object: Protocol.Runtime.RemoteObject) => {
return new CDPJSHandle(context, object); return new CDPJSHandle(this.#world, object);
}), }),
event.stackTrace event.stackTrace
); );
@ -112,8 +120,8 @@ export class WebWorker extends EventEmitter {
/** /**
* @internal * @internal
*/ */
async executionContext(): Promise<ExecutionContext> { mainRealm(): Realm {
return await this.#executionContext.valueOrThrow(); return this.#world;
} }
/** /**
@ -155,8 +163,7 @@ export class WebWorker extends EventEmitter {
this.evaluate.name, this.evaluate.name,
pageFunction pageFunction
); );
const context = await this.#executionContext.valueOrThrow(); return await this.mainRealm().evaluate(pageFunction, ...args);
return await context.evaluate(pageFunction, ...args);
} }
/** /**
@ -182,7 +189,6 @@ export class WebWorker extends EventEmitter {
this.evaluateHandle.name, this.evaluateHandle.name,
pageFunction pageFunction
); );
const context = await this.#executionContext.valueOrThrow(); return await this.mainRealm().evaluateHandle(pageFunction, ...args);
return await context.evaluateHandle(pageFunction, ...args);
} }
} }

View File

@ -148,7 +148,6 @@ export class BrowsingContext extends Realm {
browserName: string browserName: string
) { ) {
super(connection, info.context); super(connection, info.context);
this.connection = connection;
this.#id = info.context; this.#id = info.context;
this.#url = info.url; this.#url = info.url;
this.#parent = info.parent; this.#parent = info.parent;
@ -167,8 +166,8 @@ export class BrowsingContext extends Realm {
this.url = info.url; this.url = info.url;
} }
createSandboxRealm(sandbox: string): Realm { createRealmForSandbox(): Realm {
return new Realm(this.connection, this.#id, sandbox); return new Realm(this.connection, this.#id);
} }
get url(): string { get url(): string {

View File

@ -21,6 +21,7 @@ import {AutofillData, ElementHandle} from '../../api/ElementHandle.js';
import {BidiFrame} from './Frame.js'; import {BidiFrame} from './Frame.js';
import {BidiJSHandle} from './JSHandle.js'; import {BidiJSHandle} from './JSHandle.js';
import {Realm} from './Realm.js'; import {Realm} from './Realm.js';
import {Sandbox} from './Sandbox.js';
/** /**
* @internal * @internal
@ -29,19 +30,17 @@ export class BidiElementHandle<
ElementType extends Node = Element, ElementType extends Node = Element,
> extends ElementHandle<ElementType> { > extends ElementHandle<ElementType> {
declare handle: BidiJSHandle<ElementType>; declare handle: BidiJSHandle<ElementType>;
#frame: BidiFrame;
constructor( constructor(sandbox: Sandbox, remoteValue: Bidi.Script.RemoteValue) {
realm: Realm, super(new BidiJSHandle(sandbox, remoteValue));
remoteValue: Bidi.Script.RemoteValue, }
frame: BidiFrame
) { override get realm(): Sandbox {
super(new BidiJSHandle(realm, remoteValue)); return this.handle.realm;
this.#frame = frame;
} }
override get frame(): BidiFrame { override get frame(): BidiFrame {
return this.#frame; return this.realm.environment;
} }
context(): Realm { context(): Realm {
@ -62,12 +61,12 @@ export class BidiElementHandle<
} }
override async autofill(data: AutofillData): Promise<void> { override async autofill(data: AutofillData): Promise<void> {
const client = this.#frame.context().cdpSession; const client = this.frame.client;
const nodeInfo = await client.send('DOM.describeNode', { const nodeInfo = await client.send('DOM.describeNode', {
objectId: this.handle.id, objectId: this.handle.id,
}); });
const fieldId = nodeInfo.node.backendNodeId; const fieldId = nodeInfo.node.backendNodeId;
const frameId = this.#frame._id; const frameId = this.frame._id;
await client.send('Autofill.trigger', { await client.send('Autofill.trigger', {
fieldId, fieldId,
frameId, frameId,

View File

@ -64,17 +64,18 @@ export class BidiFrame extends Frame {
this._id = this.#context.id; this._id = this.#context.id;
this._parentId = parentId ?? undefined; this._parentId = parentId ?? undefined;
const puppeteerRealm = context.createSandboxRealm(UTILITY_WORLD_NAME);
this.sandboxes = { this.sandboxes = {
[MAIN_SANDBOX]: new Sandbox(context, timeoutSettings), [MAIN_SANDBOX]: new Sandbox(undefined, this, context, timeoutSettings),
[PUPPETEER_SANDBOX]: new Sandbox(puppeteerRealm, timeoutSettings), [PUPPETEER_SANDBOX]: new Sandbox(
UTILITY_WORLD_NAME,
this,
context.createRealmForSandbox(),
timeoutSettings
),
}; };
puppeteerRealm.setFrame(this);
context.setFrame(this);
} }
override _client(): CDPSession { override get client(): CDPSession {
return this.context().cdpSession; return this.context().cdpSession;
} }

View File

@ -19,102 +19,35 @@ import Protocol from 'devtools-protocol';
import {ElementHandle} from '../../api/ElementHandle.js'; import {ElementHandle} from '../../api/ElementHandle.js';
import {JSHandle} from '../../api/JSHandle.js'; import {JSHandle} from '../../api/JSHandle.js';
import {EvaluateFuncWith, HandleFor, HandleOr} from '../../common/types.js';
import {withSourcePuppeteerURLIfNone} from '../util.js';
import {Realm} from './Realm.js'; import {Realm} from './Realm.js';
import {Sandbox} from './Sandbox.js';
import {BidiSerializer} from './Serializer.js'; import {BidiSerializer} from './Serializer.js';
import {releaseReference} from './utils.js'; import {releaseReference} from './utils.js';
export class BidiJSHandle<T = unknown> extends JSHandle<T> { export class BidiJSHandle<T = unknown> extends JSHandle<T> {
#disposed = false; #disposed = false;
#realm: Realm; readonly #sandbox: Sandbox;
#remoteValue: Bidi.Script.RemoteValue; readonly #remoteValue: Bidi.Script.RemoteValue;
constructor(realm: Realm, remoteValue: Bidi.Script.RemoteValue) { constructor(sandbox: Sandbox, remoteValue: Bidi.Script.RemoteValue) {
super(); super();
this.#realm = realm; this.#sandbox = sandbox;
this.#remoteValue = remoteValue; this.#remoteValue = remoteValue;
} }
context(): Realm { context(): Realm {
return this.#realm; return this.realm.environment.context();
}
override get realm(): Sandbox {
return this.#sandbox;
} }
override get disposed(): boolean { override get disposed(): boolean {
return this.#disposed; return this.#disposed;
} }
override async evaluate<
Params extends unknown[],
Func extends EvaluateFuncWith<T, Params> = EvaluateFuncWith<T, Params>,
>(
pageFunction: Func | string,
...args: Params
): Promise<Awaited<ReturnType<Func>>> {
pageFunction = withSourcePuppeteerURLIfNone(
this.evaluate.name,
pageFunction
);
return await this.context().evaluate(pageFunction, this, ...args);
}
override async evaluateHandle<
Params extends unknown[],
Func extends EvaluateFuncWith<T, Params> = EvaluateFuncWith<T, Params>,
>(
pageFunction: Func | string,
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
pageFunction = withSourcePuppeteerURLIfNone(
this.evaluateHandle.name,
pageFunction
);
return await this.context().evaluateHandle(pageFunction, this, ...args);
}
override async getProperty<K extends keyof T>(
propertyName: HandleOr<K>
): Promise<HandleFor<T[K]>>;
override async getProperty(propertyName: string): Promise<HandleFor<unknown>>;
override async getProperty<K extends keyof T>(
propertyName: HandleOr<K>
): Promise<HandleFor<T[K]>> {
return await this.evaluateHandle((object, propertyName) => {
return object[propertyName as K];
}, propertyName);
}
override async getProperties(): Promise<Map<string, BidiJSHandle>> {
// TODO(lightning00blade): Either include return of depth Handles in RemoteValue
// or new BiDi command that returns array of remote value
const keys = await this.evaluate(object => {
const enumerableKeys = [];
const descriptors = Object.getOwnPropertyDescriptors(object);
for (const key in descriptors) {
if (descriptors[key]?.enumerable) {
enumerableKeys.push(key);
}
}
return enumerableKeys;
});
const map = new Map<string, BidiJSHandle>();
const results = await Promise.all(
keys.map(key => {
return this.getProperty(key);
})
);
for (const [key, value] of Object.entries(keys)) {
using handle = results[key as any];
if (handle) {
map.set(value, handle.move() as BidiJSHandle);
}
}
return map;
}
override async jsonValue(): Promise<T> { override async jsonValue(): Promise<T> {
return await this.evaluate(value => { return await this.evaluate(value => {
return value; return value;
@ -132,7 +65,7 @@ export class BidiJSHandle<T = unknown> extends JSHandle<T> {
this.#disposed = true; this.#disposed = true;
if ('handle' in this.#remoteValue) { if ('handle' in this.#remoteValue) {
await releaseReference( await releaseReference(
this.#realm, this.context(),
this.#remoteValue as Bidi.Script.RemoteReference this.#remoteValue as Bidi.Script.RemoteReference
); );
} }

View File

@ -69,7 +69,7 @@ import {HTTPRequest} from './HTTPRequest.js';
import {HTTPResponse} from './HTTPResponse.js'; import {HTTPResponse} from './HTTPResponse.js';
import {Keyboard, Mouse, Touchscreen} from './Input.js'; import {Keyboard, Mouse, Touchscreen} from './Input.js';
import {NetworkManager} from './NetworkManager.js'; import {NetworkManager} from './NetworkManager.js';
import {getBidiHandle} from './Realm.js'; import {createBidiHandle} from './Realm.js';
import {BidiSerializer} from './Serializer.js'; import {BidiSerializer} from './Serializer.js';
/** /**
@ -343,7 +343,7 @@ export class BidiPage extends Page {
} }
if (isConsoleLogEntry(event)) { if (isConsoleLogEntry(event)) {
const args = event.args.map(arg => { const args = event.args.map(arg => {
return getBidiHandle(frame.context(), arg, frame); return createBidiHandle(frame.mainRealm(), arg);
}); });
const text = args const text = args
@ -662,12 +662,12 @@ export class BidiPage extends Page {
return buffer; return buffer;
} }
override waitForRequest( override async waitForRequest(
urlOrPredicate: string | ((req: HTTPRequest) => boolean | Promise<boolean>), urlOrPredicate: string | ((req: HTTPRequest) => boolean | Promise<boolean>),
options: {timeout?: number} = {} options: {timeout?: number} = {}
): Promise<HTTPRequest> { ): Promise<HTTPRequest> {
const {timeout = this.#timeoutSettings.timeout()} = options; const {timeout = this.#timeoutSettings.timeout()} = options;
return waitForEvent( return await waitForEvent(
this.#networkManager, this.#networkManager,
NetworkManagerEmittedEvents.Request, NetworkManagerEmittedEvents.Request,
async request => { async request => {
@ -684,14 +684,14 @@ export class BidiPage extends Page {
); );
} }
override waitForResponse( override async waitForResponse(
urlOrPredicate: urlOrPredicate:
| string | string
| ((res: HTTPResponse) => boolean | Promise<boolean>), | ((res: HTTPResponse) => boolean | Promise<boolean>),
options: {timeout?: number} = {} options: {timeout?: number} = {}
): Promise<HTTPResponse> { ): Promise<HTTPResponse> {
const {timeout = this.#timeoutSettings.timeout()} = options; const {timeout = this.#timeoutSettings.timeout()} = options;
return waitForEvent( return await waitForEvent(
this.#networkManager, this.#networkManager,
NetworkManagerEmittedEvents.Response, NetworkManagerEmittedEvents.Response,
async response => { async response => {

View File

@ -14,8 +14,8 @@ import {
import {Connection} from './Connection.js'; import {Connection} from './Connection.js';
import {BidiElementHandle} from './ElementHandle.js'; import {BidiElementHandle} from './ElementHandle.js';
import {BidiFrame} from './Frame.js';
import {BidiJSHandle} from './JSHandle.js'; import {BidiJSHandle} from './JSHandle.js';
import {Sandbox} from './Sandbox.js';
import {BidiSerializer} from './Serializer.js'; import {BidiSerializer} from './Serializer.js';
import {createEvaluationError} from './utils.js'; import {createEvaluationError} from './utils.js';
@ -26,28 +26,31 @@ export const getSourceUrlComment = (url: string): string => {
}; };
export class Realm extends EventEmitter { export class Realm extends EventEmitter {
connection: Connection; readonly connection: Connection;
#frame!: BidiFrame; readonly #id: string;
#id: string;
#sandbox?: string;
constructor(connection: Connection, id: string, sandbox?: string) { #sandbox!: Sandbox;
constructor(connection: Connection, id: string) {
super(); super();
this.connection = connection; this.connection = connection;
this.#id = id; this.#id = id;
this.#sandbox = sandbox;
} }
get target(): Bidi.Script.Target { get target(): Bidi.Script.Target {
return { return {
context: this.#id, context: this.#id,
sandbox: this.#sandbox, sandbox: this.#sandbox.name,
}; };
} }
setFrame(frame: BidiFrame): void { setSandbox(sandbox: Sandbox): void {
this.#frame = frame; this.#sandbox = sandbox;
// TODO: Tack correct realm similar to BrowsingContexts
this.connection.on(Bidi.ChromiumBidi.Script.EventNames.RealmCreated, () => {
void this.#sandbox.taskManager.rerunAll();
});
// TODO(jrandolf): We should try to find a less brute-force way of doing // TODO(jrandolf): We should try to find a less brute-force way of doing
// this. // this.
this.connection.on( this.connection.on(
@ -131,6 +134,8 @@ export class Realm extends EventEmitter {
PuppeteerURL.INTERNAL_URL PuppeteerURL.INTERNAL_URL
); );
const sandbox = this.#sandbox;
let responsePromise; let responsePromise;
const resultOwnership = returnByValue const resultOwnership = returnByValue
? Bidi.Script.ResultOwnership.None ? Bidi.Script.ResultOwnership.None
@ -156,7 +161,7 @@ export class Realm extends EventEmitter {
functionDeclaration, functionDeclaration,
arguments: await Promise.all( arguments: await Promise.all(
args.map(arg => { args.map(arg => {
return BidiSerializer.serialize(arg, this as any); return BidiSerializer.serialize(sandbox, arg);
}) })
), ),
target: this.target, target: this.target,
@ -174,20 +179,19 @@ export class Realm extends EventEmitter {
return returnByValue return returnByValue
? BidiSerializer.deserialize(result.result) ? BidiSerializer.deserialize(result.result)
: getBidiHandle(this as any, result.result, this.#frame); : createBidiHandle(sandbox, result.result);
} }
} }
/** /**
* @internal * @internal
*/ */
export function getBidiHandle( export function createBidiHandle(
realmOrContext: Realm, sandbox: Sandbox,
result: Bidi.Script.RemoteValue, result: Bidi.Script.RemoteValue
frame: BidiFrame ): BidiJSHandle<unknown> | BidiElementHandle<Node> {
): BidiJSHandle | BidiElementHandle<Node> {
if (result.type === 'node' || result.type === 'window') { if (result.type === 'node' || result.type === 'window') {
return new BidiElementHandle(realmOrContext, result, frame); return new BidiElementHandle(sandbox, result);
} }
return new BidiJSHandle(realmOrContext, result); return new BidiJSHandle(sandbox, result);
} }

View File

@ -14,8 +14,6 @@
* limitations under the License. * limitations under the License.
*/ */
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import {JSHandle} from '../../api/JSHandle.js'; import {JSHandle} from '../../api/JSHandle.js';
import {Realm} from '../../api/Realm.js'; import {Realm} from '../../api/Realm.js';
import {TimeoutSettings} from '../TimeoutSettings.js'; import {TimeoutSettings} from '../TimeoutSettings.js';
@ -23,7 +21,7 @@ import {EvaluateFunc, HandleFor} from '../types.js';
import {withSourcePuppeteerURLIfNone} from '../util.js'; import {withSourcePuppeteerURLIfNone} from '../util.js';
import {BrowsingContext} from './BrowsingContext.js'; import {BrowsingContext} from './BrowsingContext.js';
import {BidiJSHandle} from './JSHandle.js'; import {BidiFrame} from './Frame.js';
import {Realm as BidiRealm} from './Realm.js'; import {Realm as BidiRealm} from './Realm.js';
/** /**
* A unique key for {@link SandboxChart} to denote the default world. * A unique key for {@link SandboxChart} to denote the default world.
@ -53,23 +51,26 @@ export interface SandboxChart {
* @internal * @internal
*/ */
export class Sandbox extends Realm { export class Sandbox extends Realm {
#realm: BidiRealm; readonly name: string | undefined;
readonly realm: BidiRealm;
#frame: BidiFrame;
constructor( constructor(
name: string | undefined,
frame: BidiFrame,
// TODO: We should split the Realm and BrowsingContext // TODO: We should split the Realm and BrowsingContext
realm: BidiRealm | BrowsingContext, realm: BidiRealm | BrowsingContext,
timeoutSettings: TimeoutSettings timeoutSettings: TimeoutSettings
) { ) {
super(timeoutSettings); super(timeoutSettings);
this.#realm = realm; this.name = name;
this.realm = realm;
this.#frame = frame;
this.realm.setSandbox(this);
}
// TODO: Tack correct realm similar to BrowsingContexts override get environment(): BidiFrame {
this.#realm.connection.on( return this.#frame;
Bidi.ChromiumBidi.Script.EventNames.RealmCreated,
() => {
void this.taskManager.rerunAll();
}
);
} }
async evaluateHandle< async evaluateHandle<
@ -83,7 +84,7 @@ export class Sandbox extends Realm {
this.evaluateHandle.name, this.evaluateHandle.name,
pageFunction pageFunction
); );
return await this.#realm.evaluateHandle(pageFunction, ...args); return await this.realm.evaluateHandle(pageFunction, ...args);
} }
async evaluate< async evaluate<
@ -97,7 +98,7 @@ export class Sandbox extends Realm {
this.evaluate.name, this.evaluate.name,
pageFunction pageFunction
); );
return await this.#realm.evaluate(pageFunction, ...args); return await this.realm.evaluate(pageFunction, ...args);
} }
async adoptHandle<T extends JSHandle<Node>>(handle: T): Promise<T> { async adoptHandle<T extends JSHandle<Node>>(handle: T): Promise<T> {
@ -107,7 +108,7 @@ export class Sandbox extends Realm {
} }
async transferHandle<T extends JSHandle<Node>>(handle: T): Promise<T> { async transferHandle<T extends JSHandle<Node>>(handle: T): Promise<T> {
if ((handle as unknown as BidiJSHandle).context() === this.#realm) { if (handle.realm === this) {
return handle; return handle;
} }
const transferredHandle = await this.evaluateHandle(node => { const transferredHandle = await this.evaluateHandle(node => {

View File

@ -19,9 +19,9 @@ import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import {LazyArg} from '../LazyArg.js'; import {LazyArg} from '../LazyArg.js';
import {debugError, isDate, isPlainObject, isRegExp} from '../util.js'; import {debugError, isDate, isPlainObject, isRegExp} from '../util.js';
import {BrowsingContext} from './BrowsingContext.js';
import {BidiElementHandle} from './ElementHandle.js'; import {BidiElementHandle} from './ElementHandle.js';
import {BidiJSHandle} from './JSHandle.js'; import {BidiJSHandle} from './JSHandle.js';
import {Sandbox} from './Sandbox.js';
/** /**
* @internal * @internal
@ -58,7 +58,7 @@ export class BidiSerializer {
}; };
} else if (Array.isArray(arg)) { } else if (Array.isArray(arg)) {
const parsedArray = arg.map(subArg => { const parsedArray = arg.map(subArg => {
return BidiSerializer.serializeRemoveValue(subArg); return BidiSerializer.serializeRemoteValue(subArg);
}); });
return { return {
@ -81,8 +81,8 @@ export class BidiSerializer {
const parsedObject: Bidi.Script.MappingLocalValue = []; const parsedObject: Bidi.Script.MappingLocalValue = [];
for (const key in arg) { for (const key in arg) {
parsedObject.push([ parsedObject.push([
BidiSerializer.serializeRemoveValue(key), BidiSerializer.serializeRemoteValue(key),
BidiSerializer.serializeRemoveValue(arg[key]), BidiSerializer.serializeRemoteValue(arg[key]),
]); ]);
} }
@ -110,7 +110,7 @@ export class BidiSerializer {
); );
} }
static serializeRemoveValue(arg: unknown): Bidi.Script.LocalValue { static serializeRemoteValue(arg: unknown): Bidi.Script.LocalValue {
switch (typeof arg) { switch (typeof arg) {
case 'symbol': case 'symbol':
case 'function': case 'function':
@ -143,11 +143,11 @@ export class BidiSerializer {
} }
static async serialize( static async serialize(
arg: unknown, sandbox: Sandbox,
context: BrowsingContext arg: unknown
): Promise<Bidi.Script.LocalValue> { ): Promise<Bidi.Script.LocalValue> {
if (arg instanceof LazyArg) { if (arg instanceof LazyArg) {
arg = await arg.get(context); arg = await arg.get(sandbox.realm);
} }
// eslint-disable-next-line rulesdir/use-using -- We want this to continue living. // eslint-disable-next-line rulesdir/use-using -- We want this to continue living.
const objectHandle = const objectHandle =
@ -156,7 +156,7 @@ export class BidiSerializer {
: null; : null;
if (objectHandle) { if (objectHandle) {
if ( if (
objectHandle.context() !== context && objectHandle.realm !== sandbox &&
!('sharedId' in objectHandle.remoteValue()) !('sharedId' in objectHandle.remoteValue())
) { ) {
throw new Error( throw new Error(
@ -169,7 +169,7 @@ export class BidiSerializer {
return objectHandle.remoteValue() as Bidi.Script.RemoteReference; return objectHandle.remoteValue() as Bidi.Script.RemoteReference;
} }
return BidiSerializer.serializeRemoveValue(arg); return BidiSerializer.serializeRemoteValue(arg);
} }
static deserializeNumber(value: Bidi.Script.SpecialNumber | number): number { static deserializeNumber(value: Bidi.Script.SpecialNumber | number): number {

View File

@ -30,7 +30,7 @@ import type {CDPSession} from './Connection.js';
import {debug} from './Debug.js'; import {debug} from './Debug.js';
import {CDPElementHandle} from './ElementHandle.js'; import {CDPElementHandle} from './ElementHandle.js';
import type {CommonEventEmitter} from './EventEmitter.js'; import type {CommonEventEmitter} from './EventEmitter.js';
import type {ExecutionContext} from './ExecutionContext.js'; import {IsolatedWorld} from './IsolatedWorld.js';
import {CDPJSHandle} from './JSHandle.js'; import {CDPJSHandle} from './JSHandle.js';
import {Awaitable} from './types.js'; import {Awaitable} from './types.js';
@ -413,14 +413,14 @@ export async function waitForEvent<T>(
/** /**
* @internal * @internal
*/ */
export function createJSHandle( export function createCdpHandle(
context: ExecutionContext, realm: IsolatedWorld,
remoteObject: Protocol.Runtime.RemoteObject remoteObject: Protocol.Runtime.RemoteObject
): JSHandle | ElementHandle<Node> { ): JSHandle | ElementHandle<Node> {
if (remoteObject.subtype === 'node' && context._world) { if (remoteObject.subtype === 'node') {
return new CDPElementHandle(context, remoteObject, context._world.frame()); return new CDPElementHandle(realm, remoteObject);
} }
return new CDPJSHandle(context, remoteObject); return new CDPJSHandle(realm, remoteObject);
} }
/** /**

View File

@ -755,12 +755,6 @@
"parameters": ["webDriverBiDi"], "parameters": ["webDriverBiDi"],
"expectations": ["PASS"] "expectations": ["PASS"]
}, },
{
"testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should throw a nice error after a navigation",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["FAIL"]
},
{ {
"testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should throw if elementHandles are from other frames", "testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should throw if elementHandles are from other frames",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
@ -2267,12 +2261,6 @@
"parameters": ["cdp", "firefox"], "parameters": ["cdp", "firefox"],
"expectations": ["SKIP"] "expectations": ["SKIP"]
}, },
{
"testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should throw a nice error after a navigation",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["cdp", "firefox"],
"expectations": ["FAIL", "TIMEOUT"]
},
{ {
"testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should throw when evaluation triggers reload", "testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should throw when evaluation triggers reload",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],

View File

@ -419,26 +419,6 @@ describe('Evaluation specs', function () {
}); });
expect(result).toBe(true); expect(result).toBe(true);
}); });
it('should throw a nice error after a navigation', async () => {
const {page} = await getTestState();
const executionContext = await page.mainFrame().executionContext();
await Promise.all([
page.waitForNavigation(),
executionContext.evaluate(() => {
return window.location.reload();
}),
]);
const error = await executionContext
.evaluate(() => {
return null;
})
.catch(error_ => {
return error_;
});
expect((error as Error).message).toContain('navigation');
});
it('should not throw an error when evaluation does a navigation', async () => { it('should not throw an error when evaluation does a navigation', async () => {
const {page, server} = await getTestState(); const {page, server} = await getTestState();

View File

@ -30,43 +30,6 @@ import {
describe('Frame specs', function () { describe('Frame specs', function () {
setupTestBrowserHooks(); setupTestBrowserHooks();
describe('Frame.executionContext', function () {
it('should work', async () => {
const {page, server} = await getTestState();
await page.goto(server.EMPTY_PAGE);
await attachFrame(page, 'frame1', server.EMPTY_PAGE);
expect(page.frames()).toHaveLength(2);
const [frame1, frame2] = page.frames();
const context1 = await frame1!.executionContext();
const context2 = await frame2!.executionContext();
expect(context1).toBeTruthy();
expect(context2).toBeTruthy();
expect(context1 !== context2).toBeTruthy();
expect(context1._world?.frame()).toBe(frame1);
expect(context2._world?.frame()).toBe(frame2);
await Promise.all([
context1.evaluate(() => {
return ((globalThis as any).a = 1);
}),
context2.evaluate(() => {
return ((globalThis as any).a = 2);
}),
]);
const [a1, a2] = await Promise.all([
context1.evaluate(() => {
return (globalThis as any).a;
}),
context2.evaluate(() => {
return (globalThis as any).a;
}),
]);
expect(a1).toBe(1);
expect(a2).toBe(2);
});
});
describe('Frame.evaluateHandle', function () { describe('Frame.evaluateHandle', function () {
it('should work', async () => { it('should work', async () => {
const {page, server} = await getTestState(); const {page, server} = await getTestState();
@ -338,7 +301,7 @@ describe('Frame specs', function () {
describe('Frame.client', function () { describe('Frame.client', function () {
it('should return the client instance', async () => { it('should return the client instance', async () => {
const {page} = await getTestState(); const {page} = await getTestState();
expect(page.mainFrame()._client()).toBeInstanceOf(CDPSession); expect(page.mainFrame().client).toBeInstanceOf(CDPSession);
}); });
}); });
}); });

View File

@ -102,7 +102,7 @@ describe('Workers', function () {
return new Worker(`data:text/javascript,console.log(1)`); return new Worker(`data:text/javascript,console.log(1)`);
}); });
const worker = await workerCreatedPromise; const worker = await workerCreatedPromise;
expect(await (await worker.executionContext()).evaluate('1+1')).toBe(2); expect(await worker.evaluate('1+1')).toBe(2);
}); });
it('should report errors', async () => { it('should report errors', async () => {
const {page} = await getTestState(); const {page} = await getTestState();