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
class JSHandle {
abstract evaluate<
evaluate<
Params extends unknown[],
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
class JSHandle {
abstract evaluateHandle<
evaluateHandle<
Params extends unknown[],
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
class JSHandle {
abstract getProperties(): Promise<Map<string, JSHandle<unknown>>>;
getProperties(): Promise<Map<string, JSHandle>>;
}
```
**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

View File

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

View File

@ -8,7 +8,7 @@ sidebar_label: JSHandle.getProperty_1
```typescript
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
*/
protected handle;
protected readonly handle;
/**
* @internal
@ -208,7 +208,7 @@ export abstract class ElementHandle<
/**
* @internal
*/
override evaluateHandle<
override async evaluateHandle<
Params extends unknown[],
Func extends EvaluateFuncWith<ElementType, Params> = EvaluateFuncWith<
ElementType,
@ -218,7 +218,7 @@ export abstract class ElementHandle<
pageFunction: Func | string,
...args: Params
): 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}
* 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 {DeviceRequestPrompt} from '../common/DeviceRequestPrompt.js';
import {EventEmitter} from '../common/EventEmitter.js';
import {ExecutionContext} from '../common/ExecutionContext.js';
import {getQueryHandlerAndSelector} from '../common/GetQueryHandler.js';
import {transposeIterableHandle} from '../common/HandleIterator.js';
import {
@ -309,14 +308,7 @@ export abstract class Frame extends EventEmitter {
/**
* @internal
*/
abstract _client(): CDPSession;
/**
* @internal
*/
executionContext(): Promise<ExecutionContext> {
throw new Error('Not implemented');
}
abstract get client(): CDPSession;
/**
* @internal
@ -475,7 +467,7 @@ export abstract class Frame extends EventEmitter {
* @returns A promise to the result of the function.
*/
@throwIfDetached
$eval<
async $eval<
Selector extends string,
Params extends unknown[],
Func extends EvaluateFuncWith<NodeFor<Selector>, Params> = EvaluateFuncWith<
@ -488,7 +480,7 @@ export abstract class Frame extends EventEmitter {
...args: Params
): Promise<Awaited<ReturnType<Func>>> {
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.
*/
@throwIfDetached
$$eval<
async $$eval<
Selector extends string,
Params extends unknown[],
Func extends EvaluateFuncWith<
@ -525,7 +517,7 @@ export abstract class Frame extends EventEmitter {
...args: Params
): Promise<Awaited<ReturnType<Func>>> {
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.
*/
@throwIfDetached
$x(expression: string): Promise<Array<ElementHandle<Node>>> {
return this.mainRealm().$x(expression);
async $x(expression: string): Promise<Array<ElementHandle<Node>>> {
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.
*/
@throwIfDetached
waitForFunction<
async waitForFunction<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(
@ -667,18 +659,18 @@ export abstract class Frame extends EventEmitter {
options: FrameWaitForFunctionOptions = {},
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
return this.mainRealm().waitForFunction(
return await (this.mainRealm().waitForFunction(
pageFunction,
options,
...args
) as Promise<HandleFor<Awaited<ReturnType<Func>>>>;
) as Promise<HandleFor<Awaited<ReturnType<Func>>>>);
}
/**
* The full HTML contents of the frame, including the DOCTYPE.
*/
@throwIfDetached
content(): Promise<string> {
return this.isolatedRealm().content();
async content(): Promise<string> {
return await this.isolatedRealm().content();
}
/**
@ -922,8 +914,11 @@ export abstract class Frame extends EventEmitter {
* @param selector - The selector to query for.
*/
@throwIfDetached
click(selector: string, options: Readonly<ClickOptions> = {}): Promise<void> {
return this.isolatedRealm().click(selector, options);
async click(
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`.
*/
@throwIfDetached
focus(selector: string): Promise<void> {
return this.isolatedRealm().focus(selector);
async focus(selector: string): Promise<void> {
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`.
*/
@throwIfDetached
hover(selector: string): Promise<void> {
return this.isolatedRealm().hover(selector);
async hover(selector: string): Promise<void> {
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`.
*/
@throwIfDetached
select(selector: string, ...values: string[]): Promise<string[]> {
return this.isolatedRealm().select(selector, ...values);
async select(selector: string, ...values: string[]): Promise<string[]> {
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`.
*/
@throwIfDetached
tap(selector: string): Promise<void> {
return this.isolatedRealm().tap(selector);
async tap(selector: string): Promise<void> {
return await this.isolatedRealm().tap(selector);
}
/**
@ -1005,12 +1000,12 @@ export abstract class Frame extends EventEmitter {
* between key presses in milliseconds. Defaults to `0`.
*/
@throwIfDetached
type(
async type(
selector: string,
text: string,
options?: Readonly<KeyboardTypeOptions>
): 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.
*/
waitForTimeout(milliseconds: number): Promise<void> {
return new Promise(resolve => {
async waitForTimeout(milliseconds: number): Promise<void> {
return await new Promise(resolve => {
setTimeout(resolve, milliseconds);
});
}
@ -1043,8 +1038,8 @@ export abstract class Frame extends EventEmitter {
* The frame's title.
*/
@throwIfDetached
title(): Promise<string> {
return this.isolatedRealm().title();
async title(): Promise<string> {
return await this.isolatedRealm().title();
}
/**

View File

@ -23,10 +23,11 @@ import {
HandleOr,
Moveable,
} from '../common/types.js';
import {debugError} from '../common/util.js';
import {debugError, withSourcePuppeteerURLIfNone} from '../common/util.js';
import {moveable} from '../util/decorators.js';
import {ElementHandle} from './ElementHandle.js';
import {Realm} from './Realm.js';
/**
* Represents a reference to a JavaScript object. Instances can be created using
@ -65,6 +66,11 @@ export abstract class JSHandle<T = unknown>
*/
constructor() {}
/**
* @internal
*/
abstract get realm(): Realm;
/**
* @internal
*/
@ -75,33 +81,56 @@ export abstract class JSHandle<T = unknown>
/**
* Evaluates the given function with the current handle as its first argument.
*/
abstract evaluate<
async evaluate<
Params extends unknown[],
Func extends EvaluateFuncWith<T, Params> = EvaluateFuncWith<T, Params>,
>(
pageFunction: Func | string,
...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.
*
*/
abstract evaluateHandle<
async evaluateHandle<
Params extends unknown[],
Func extends EvaluateFuncWith<T, Params> = EvaluateFuncWith<T, Params>,
>(
pageFunction: Func | string,
...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.
*/
abstract getProperty<K extends keyof T>(
getProperty<K extends keyof T>(
propertyName: HandleOr<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.
@ -121,7 +150,31 @@ export abstract class JSHandle<T = unknown>
* 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

View File

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

View File

@ -21,7 +21,7 @@ import {assert} from '../util/assert.js';
import {AsyncIterableUtil} from '../util/AsyncIterableUtil.js';
import {CDPSession} from './Connection.js';
import {CDPJSHandle} from './JSHandle.js';
import {IsolatedWorld} from './IsolatedWorld.js';
import {QueryHandler, QuerySelector} from './QueryHandler.js';
import {AwaitableIterable} from './types.js';
@ -106,16 +106,17 @@ export class ARIAQueryHandler extends QueryHandler {
element: ElementHandle<Node>,
selector: string
): AwaitableIterable<ElementHandle<Node>> {
const context = (
element as unknown as CDPJSHandle<Node>
).executionContext();
const {name, role} = parseARIASelector(selector);
const results = await queryAXTree(context._client, element, name, role);
const world = context._world!;
const results = await queryAXTree(
element.realm.environment.client,
element,
name,
role
);
yield* AsyncIterableUtil.map(results, node => {
return world.adoptBackendNode(node.backendDOMNodeId) as Promise<
ElementHandle<Node>
>;
return (element.realm as IsolatedWorld).adoptBackendNode(
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 {CDPSession} from './Connection.js';
import {ExecutionContext} from './ExecutionContext.js';
import {CDPFrame} from './Frame.js';
import {FrameManager} from './FrameManager.js';
import {WaitForSelectorOptions} from './IsolatedWorld.js';
import {IsolatedWorld} from './IsolatedWorld.js';
import {CDPJSHandle} from './JSHandle.js';
import {NodeFor} from './types.js';
import {debugError} from './util.js';
/**
@ -40,20 +38,17 @@ import {debugError} from './util.js';
export class CDPElementHandle<
ElementType extends Node = Element,
> extends ElementHandle<ElementType> {
#frame: CDPFrame;
declare handle: CDPJSHandle<ElementType>;
protected declare readonly handle: CDPJSHandle<ElementType>;
constructor(
context: ExecutionContext,
remoteObject: Protocol.Runtime.RemoteObject,
frame: CDPFrame
world: IsolatedWorld,
remoteObject: Protocol.Runtime.RemoteObject
) {
super(new CDPJSHandle(context, remoteObject));
this.#frame = frame;
super(new CDPJSHandle(world, remoteObject));
}
executionContext(): ExecutionContext {
return this.handle.executionContext();
override get realm(): IsolatedWorld {
return this.handle.realm;
}
get client(): CDPSession {
@ -65,48 +60,21 @@ export class CDPElementHandle<
}
get #frameManager(): FrameManager {
return this.#frame._frameManager;
return this.frame._frameManager;
}
get #page(): Page {
return this.#frame.page();
return this.frame.page();
}
override get frame(): CDPFrame {
return this.#frame;
}
@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;
return this.realm.environment as CDPFrame;
}
override async contentFrame(
this: ElementHandle<HTMLIFrameElement>
): Promise<CDPFrame>;
@throwIfDisposed()
override async contentFrame(): Promise<CDPFrame | null> {
const nodeInfo = await this.client.send('DOM.describeNode', {
@ -320,15 +288,11 @@ export class CDPElementHandle<
objectId: this.handle.id,
});
const fieldId = nodeInfo.node.backendNodeId;
const frameId = this.#frame._id;
const frameId = this.frame._id;
await this.client.send('Autofill.trigger', {
fieldId,
frameId,
card: data.creditCard,
});
}
override assertElementHasWorld(): asserts this {
assert(this.executionContext()._world);
}
}

View File

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

View File

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

View File

@ -431,7 +431,7 @@ export class FrameManager extends EventEmitter {
await Promise.all(
this.frames()
.filter(frame => {
return frame._client() === session;
return frame.client === session;
})
.map(frame => {
// 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;
if (frame) {
// Only care about execution contexts created for the current session.
if (frame._client() !== session) {
if (frame.client !== session) {
return;
}
if (contextPayload.auxData && contextPayload.auxData['isDefault']) {
@ -504,8 +504,12 @@ export class FrameManager extends EventEmitter {
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(
frame?._client() || this.#client,
frame?.client || this.#client,
contextPayload,
world
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -64,17 +64,18 @@ export class BidiFrame extends Frame {
this._id = this.#context.id;
this._parentId = parentId ?? undefined;
const puppeteerRealm = context.createSandboxRealm(UTILITY_WORLD_NAME);
this.sandboxes = {
[MAIN_SANDBOX]: new Sandbox(context, timeoutSettings),
[PUPPETEER_SANDBOX]: new Sandbox(puppeteerRealm, timeoutSettings),
[MAIN_SANDBOX]: new Sandbox(undefined, this, context, 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;
}

View File

@ -19,102 +19,35 @@ import Protocol from 'devtools-protocol';
import {ElementHandle} from '../../api/ElementHandle.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 {Sandbox} from './Sandbox.js';
import {BidiSerializer} from './Serializer.js';
import {releaseReference} from './utils.js';
export class BidiJSHandle<T = unknown> extends JSHandle<T> {
#disposed = false;
#realm: Realm;
#remoteValue: Bidi.Script.RemoteValue;
readonly #sandbox: Sandbox;
readonly #remoteValue: Bidi.Script.RemoteValue;
constructor(realm: Realm, remoteValue: Bidi.Script.RemoteValue) {
constructor(sandbox: Sandbox, remoteValue: Bidi.Script.RemoteValue) {
super();
this.#realm = realm;
this.#sandbox = sandbox;
this.#remoteValue = remoteValue;
}
context(): Realm {
return this.#realm;
return this.realm.environment.context();
}
override get realm(): Sandbox {
return this.#sandbox;
}
override get disposed(): boolean {
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> {
return await this.evaluate(value => {
return value;
@ -132,7 +65,7 @@ export class BidiJSHandle<T = unknown> extends JSHandle<T> {
this.#disposed = true;
if ('handle' in this.#remoteValue) {
await releaseReference(
this.#realm,
this.context(),
this.#remoteValue as Bidi.Script.RemoteReference
);
}

View File

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

View File

@ -14,8 +14,8 @@ import {
import {Connection} from './Connection.js';
import {BidiElementHandle} from './ElementHandle.js';
import {BidiFrame} from './Frame.js';
import {BidiJSHandle} from './JSHandle.js';
import {Sandbox} from './Sandbox.js';
import {BidiSerializer} from './Serializer.js';
import {createEvaluationError} from './utils.js';
@ -26,28 +26,31 @@ export const getSourceUrlComment = (url: string): string => {
};
export class Realm extends EventEmitter {
connection: Connection;
#frame!: BidiFrame;
#id: string;
#sandbox?: string;
readonly connection: Connection;
readonly #id: string;
constructor(connection: Connection, id: string, sandbox?: string) {
#sandbox!: Sandbox;
constructor(connection: Connection, id: string) {
super();
this.connection = connection;
this.#id = id;
this.#sandbox = sandbox;
}
get target(): Bidi.Script.Target {
return {
context: this.#id,
sandbox: this.#sandbox,
sandbox: this.#sandbox.name,
};
}
setFrame(frame: BidiFrame): void {
this.#frame = frame;
setSandbox(sandbox: Sandbox): void {
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
// this.
this.connection.on(
@ -131,6 +134,8 @@ export class Realm extends EventEmitter {
PuppeteerURL.INTERNAL_URL
);
const sandbox = this.#sandbox;
let responsePromise;
const resultOwnership = returnByValue
? Bidi.Script.ResultOwnership.None
@ -156,7 +161,7 @@ export class Realm extends EventEmitter {
functionDeclaration,
arguments: await Promise.all(
args.map(arg => {
return BidiSerializer.serialize(arg, this as any);
return BidiSerializer.serialize(sandbox, arg);
})
),
target: this.target,
@ -174,20 +179,19 @@ export class Realm extends EventEmitter {
return returnByValue
? BidiSerializer.deserialize(result.result)
: getBidiHandle(this as any, result.result, this.#frame);
: createBidiHandle(sandbox, result.result);
}
}
/**
* @internal
*/
export function getBidiHandle(
realmOrContext: Realm,
result: Bidi.Script.RemoteValue,
frame: BidiFrame
): BidiJSHandle | BidiElementHandle<Node> {
export function createBidiHandle(
sandbox: Sandbox,
result: Bidi.Script.RemoteValue
): BidiJSHandle<unknown> | BidiElementHandle<Node> {
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.
*/
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import {JSHandle} from '../../api/JSHandle.js';
import {Realm} from '../../api/Realm.js';
import {TimeoutSettings} from '../TimeoutSettings.js';
@ -23,7 +21,7 @@ import {EvaluateFunc, HandleFor} from '../types.js';
import {withSourcePuppeteerURLIfNone} from '../util.js';
import {BrowsingContext} from './BrowsingContext.js';
import {BidiJSHandle} from './JSHandle.js';
import {BidiFrame} from './Frame.js';
import {Realm as BidiRealm} from './Realm.js';
/**
* A unique key for {@link SandboxChart} to denote the default world.
@ -53,23 +51,26 @@ export interface SandboxChart {
* @internal
*/
export class Sandbox extends Realm {
#realm: BidiRealm;
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.#realm = realm;
this.name = name;
this.realm = realm;
this.#frame = frame;
this.realm.setSandbox(this);
}
// TODO: Tack correct realm similar to BrowsingContexts
this.#realm.connection.on(
Bidi.ChromiumBidi.Script.EventNames.RealmCreated,
() => {
void this.taskManager.rerunAll();
}
);
override get environment(): BidiFrame {
return this.#frame;
}
async evaluateHandle<
@ -83,7 +84,7 @@ export class Sandbox extends Realm {
this.evaluateHandle.name,
pageFunction
);
return await this.#realm.evaluateHandle(pageFunction, ...args);
return await this.realm.evaluateHandle(pageFunction, ...args);
}
async evaluate<
@ -97,7 +98,7 @@ export class Sandbox extends Realm {
this.evaluate.name,
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> {
@ -107,7 +108,7 @@ export class Sandbox extends Realm {
}
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;
}
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 {debugError, isDate, isPlainObject, isRegExp} from '../util.js';
import {BrowsingContext} from './BrowsingContext.js';
import {BidiElementHandle} from './ElementHandle.js';
import {BidiJSHandle} from './JSHandle.js';
import {Sandbox} from './Sandbox.js';
/**
* @internal
@ -58,7 +58,7 @@ export class BidiSerializer {
};
} else if (Array.isArray(arg)) {
const parsedArray = arg.map(subArg => {
return BidiSerializer.serializeRemoveValue(subArg);
return BidiSerializer.serializeRemoteValue(subArg);
});
return {
@ -81,8 +81,8 @@ export class BidiSerializer {
const parsedObject: Bidi.Script.MappingLocalValue = [];
for (const key in arg) {
parsedObject.push([
BidiSerializer.serializeRemoveValue(key),
BidiSerializer.serializeRemoveValue(arg[key]),
BidiSerializer.serializeRemoteValue(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) {
case 'symbol':
case 'function':
@ -143,11 +143,11 @@ export class BidiSerializer {
}
static async serialize(
arg: unknown,
context: BrowsingContext
sandbox: Sandbox,
arg: unknown
): Promise<Bidi.Script.LocalValue> {
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.
const objectHandle =
@ -156,7 +156,7 @@ export class BidiSerializer {
: null;
if (objectHandle) {
if (
objectHandle.context() !== context &&
objectHandle.realm !== sandbox &&
!('sharedId' in objectHandle.remoteValue())
) {
throw new Error(
@ -169,7 +169,7 @@ export class BidiSerializer {
return objectHandle.remoteValue() as Bidi.Script.RemoteReference;
}
return BidiSerializer.serializeRemoveValue(arg);
return BidiSerializer.serializeRemoteValue(arg);
}
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 {CDPElementHandle} from './ElementHandle.js';
import type {CommonEventEmitter} from './EventEmitter.js';
import type {ExecutionContext} from './ExecutionContext.js';
import {IsolatedWorld} from './IsolatedWorld.js';
import {CDPJSHandle} from './JSHandle.js';
import {Awaitable} from './types.js';
@ -413,14 +413,14 @@ export async function waitForEvent<T>(
/**
* @internal
*/
export function createJSHandle(
context: ExecutionContext,
export function createCdpHandle(
realm: IsolatedWorld,
remoteObject: Protocol.Runtime.RemoteObject
): JSHandle | ElementHandle<Node> {
if (remoteObject.subtype === 'node' && context._world) {
return new CDPElementHandle(context, remoteObject, context._world.frame());
if (remoteObject.subtype === 'node') {
return new CDPElementHandle(realm, remoteObject);
}
return new CDPJSHandle(context, remoteObject);
return new CDPJSHandle(realm, remoteObject);
}
/**

View File

@ -755,12 +755,6 @@
"parameters": ["webDriverBiDi"],
"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",
"platforms": ["darwin", "linux", "win32"],
@ -2267,12 +2261,6 @@
"parameters": ["cdp", "firefox"],
"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",
"platforms": ["darwin", "linux", "win32"],

View File

@ -419,26 +419,6 @@ describe('Evaluation specs', function () {
});
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 () => {
const {page, server} = await getTestState();

View File

@ -30,43 +30,6 @@ import {
describe('Frame specs', function () {
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 () {
it('should work', async () => {
const {page, server} = await getTestState();
@ -338,7 +301,7 @@ describe('Frame specs', function () {
describe('Frame.client', function () {
it('should return the client instance', async () => {
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)`);
});
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 () => {
const {page} = await getTestState();