diff options
Diffstat (limited to 'remote/test/puppeteer/packages/puppeteer-core/src/bidi/Realm.ts')
-rw-r--r-- | remote/test/puppeteer/packages/puppeteer-core/src/bidi/Realm.ts | 344 |
1 files changed, 252 insertions, 92 deletions
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Realm.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Realm.ts index 84f13bc703..1027941e2f 100644 --- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Realm.ts +++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/Realm.ts @@ -1,80 +1,63 @@ +/** + * @license + * Copyright 2024 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; -import {EventEmitter, type EventType} from '../common/EventEmitter.js'; +import type {JSHandle} from '../api/JSHandle.js'; +import {Realm} from '../api/Realm.js'; +import {ARIAQueryHandler} from '../cdp/AriaQueryHandler.js'; +import {LazyArg} from '../common/LazyArg.js'; import {scriptInjector} from '../common/ScriptInjector.js'; +import type {TimeoutSettings} from '../common/TimeoutSettings.js'; import type {EvaluateFunc, HandleFor} from '../common/types.js'; import { - PuppeteerURL, - SOURCE_URL_REGEX, + debugError, getSourcePuppeteerURLIfAvailable, getSourceUrlComment, isString, + PuppeteerURL, + SOURCE_URL_REGEX, } from '../common/util.js'; import type PuppeteerUtil from '../injected/injected.js'; -import {disposeSymbol} from '../util/disposable.js'; +import {AsyncIterableUtil} from '../util/AsyncIterableUtil.js'; import {stringifyFunction} from '../util/Function.js'; -import type {BidiConnection} from './Connection.js'; +import type { + Realm as BidiRealmCore, + DedicatedWorkerRealm, + SharedWorkerRealm, +} from './core/Realm.js'; +import type {WindowRealm} from './core/Realm.js'; import {BidiDeserializer} from './Deserializer.js'; import {BidiElementHandle} from './ElementHandle.js'; +import {ExposeableFunction} from './ExposedFunction.js'; +import type {BidiFrame} from './Frame.js'; import {BidiJSHandle} from './JSHandle.js'; -import type {Sandbox} from './Sandbox.js'; import {BidiSerializer} from './Serializer.js'; import {createEvaluationError} from './util.js'; +import type {BidiWebWorker} from './WebWorker.js'; /** * @internal */ -export class BidiRealm extends EventEmitter<Record<EventType, any>> { - readonly connection: BidiConnection; - - #id!: string; - #sandbox!: Sandbox; +export abstract class BidiRealm extends Realm { + readonly realm: BidiRealmCore; - constructor(connection: BidiConnection) { - super(); - this.connection = connection; + constructor(realm: BidiRealmCore, timeoutSettings: TimeoutSettings) { + super(timeoutSettings); + this.realm = realm; } - get target(): Bidi.Script.Target { - return { - context: this.#sandbox.environment._id, - sandbox: this.#sandbox.name, - }; - } - - handleRealmDestroyed = async ( - params: Bidi.Script.RealmDestroyed['params'] - ): Promise<void> => { - if (params.realm === this.#id) { - // Note: The Realm is destroyed, so in theory the handle should be as - // well. + protected initialize(): void { + this.realm.on('destroyed', ({reason}) => { + this.taskManager.terminateAll(new Error(reason)); + }); + this.realm.on('updated', () => { this.internalPuppeteerUtil = undefined; - this.#sandbox.environment.clearDocumentHandle(); - } - }; - - handleRealmCreated = (params: Bidi.Script.RealmCreated['params']): void => { - if ( - params.type === 'window' && - params.context === this.#sandbox.environment._id && - params.sandbox === this.#sandbox.name - ) { - this.#id = params.realm; - void this.#sandbox.taskManager.rerunAll(); - } - }; - - setSandbox(sandbox: Sandbox): void { - this.#sandbox = sandbox; - this.connection.on( - Bidi.ChromiumBidi.Script.EventNames.RealmCreated, - this.handleRealmCreated - ); - this.connection.on( - Bidi.ChromiumBidi.Script.EventNames.RealmDestroyed, - this.handleRealmDestroyed - ); + void this.taskManager.rerunAll(); + }); } protected internalPuppeteerUtil?: Promise<BidiJSHandle<PuppeteerUtil>>; @@ -95,7 +78,7 @@ export class BidiRealm extends EventEmitter<Record<EventType, any>> { return this.internalPuppeteerUtil as Promise<BidiJSHandle<PuppeteerUtil>>; } - async evaluateHandle< + override async evaluateHandle< Params extends unknown[], Func extends EvaluateFunc<Params> = EvaluateFunc<Params>, >( @@ -105,7 +88,7 @@ export class BidiRealm extends EventEmitter<Record<EventType, any>> { return await this.#evaluate(false, pageFunction, ...args); } - async evaluate< + override async evaluate< Params extends unknown[], Func extends EvaluateFunc<Params> = EvaluateFunc<Params>, >( @@ -144,8 +127,6 @@ export class BidiRealm extends EventEmitter<Record<EventType, any>> { PuppeteerURL.INTERNAL_URL ); - const sandbox = this.#sandbox; - let responsePromise; const resultOwnership = returnByValue ? Bidi.Script.ResultOwnership.None @@ -161,11 +142,8 @@ export class BidiRealm extends EventEmitter<Record<EventType, any>> { ? pageFunction : `${pageFunction}\n${sourceUrlComment}\n`; - responsePromise = this.connection.send('script.evaluate', { - expression, - target: this.target, + responsePromise = this.realm.evaluate(expression, true, { resultOwnership, - awaitPromise: true, userActivation: true, serializationOptions, }); @@ -174,24 +152,25 @@ export class BidiRealm extends EventEmitter<Record<EventType, any>> { functionDeclaration = SOURCE_URL_REGEX.test(functionDeclaration) ? functionDeclaration : `${functionDeclaration}\n${sourceUrlComment}\n`; - responsePromise = this.connection.send('script.callFunction', { + responsePromise = this.realm.callFunction( functionDeclaration, - arguments: args.length - ? await Promise.all( - args.map(arg => { - return BidiSerializer.serialize(sandbox, arg); - }) - ) - : [], - target: this.target, - resultOwnership, - awaitPromise: true, - userActivation: true, - serializationOptions, - }); + /* awaitPromise= */ true, + { + arguments: args.length + ? await Promise.all( + args.map(arg => { + return this.serialize(arg); + }) + ) + : [], + resultOwnership, + userActivation: true, + serializationOptions, + } + ); } - const {result} = await responsePromise; + const result = await responsePromise; if ('type' in result && result.type === 'exception') { throw createEvaluationError(result.exceptionDetails); @@ -199,30 +178,211 @@ export class BidiRealm extends EventEmitter<Record<EventType, any>> { return returnByValue ? BidiDeserializer.deserialize(result.result) - : createBidiHandle(sandbox, result.result); + : this.createHandle(result.result); } - [disposeSymbol](): void { - this.connection.off( - Bidi.ChromiumBidi.Script.EventNames.RealmCreated, - this.handleRealmCreated - ); - this.connection.off( - Bidi.ChromiumBidi.Script.EventNames.RealmDestroyed, - this.handleRealmDestroyed + createHandle( + result: Bidi.Script.RemoteValue + ): BidiJSHandle<unknown> | BidiElementHandle<Node> { + if ( + (result.type === 'node' || result.type === 'window') && + this instanceof BidiFrameRealm + ) { + return BidiElementHandle.from(result, this); + } + return BidiJSHandle.from(result, this); + } + + async serialize(arg: unknown): Promise<Bidi.Script.LocalValue> { + if (arg instanceof LazyArg) { + arg = await arg.get(this); + } + + if (arg instanceof BidiJSHandle || arg instanceof BidiElementHandle) { + if (arg.realm !== this) { + if ( + !(arg.realm instanceof BidiFrameRealm) || + !(this instanceof BidiFrameRealm) + ) { + throw new Error( + "Trying to evaluate JSHandle from different global types. Usually this means you're using a handle from a worker in a page or vice versa." + ); + } + if (arg.realm.environment !== this.environment) { + throw new Error( + "Trying to evaluate JSHandle from different frames. Usually this means you're using a handle from a page on a different page." + ); + } + } + if (arg.disposed) { + throw new Error('JSHandle is disposed!'); + } + return arg.remoteValue() as Bidi.Script.RemoteReference; + } + + return BidiSerializer.serialize(arg); + } + + async destroyHandles(handles: Array<BidiJSHandle<unknown>>): Promise<void> { + const handleIds = handles + .map(({id}) => { + return id; + }) + .filter((id): id is string => { + return id !== undefined; + }); + + if (handleIds.length === 0) { + return; + } + + await this.realm.disown(handleIds).catch(error => { + // Exceptions might happen in case of a page been navigated or closed. + // Swallow these since they are harmless and we don't leak anything in this case. + debugError(error); + }); + } + + override async adoptHandle<T extends JSHandle<Node>>(handle: T): Promise<T> { + return (await this.evaluateHandle(node => { + return node; + }, handle)) as unknown as T; + } + + override async transferHandle<T extends JSHandle<Node>>( + handle: T + ): Promise<T> { + if (handle.realm === this) { + return handle; + } + const transferredHandle = this.adoptHandle(handle); + await handle.dispose(); + return await transferredHandle; + } +} + +/** + * @internal + */ +export class BidiFrameRealm extends BidiRealm { + static from(realm: WindowRealm, frame: BidiFrame): BidiFrameRealm { + const frameRealm = new BidiFrameRealm(realm, frame); + frameRealm.#initialize(); + return frameRealm; + } + declare readonly realm: WindowRealm; + + readonly #frame: BidiFrame; + + private constructor(realm: WindowRealm, frame: BidiFrame) { + super(realm, frame.timeoutSettings); + this.#frame = frame; + } + + #initialize() { + super.initialize(); + + // This should run first. + this.realm.on('updated', () => { + this.environment.clearDocumentHandle(); + this.#bindingsInstalled = false; + }); + } + + #bindingsInstalled = false; + override get puppeteerUtil(): Promise<BidiJSHandle<PuppeteerUtil>> { + let promise = Promise.resolve() as Promise<unknown>; + if (!this.#bindingsInstalled) { + promise = Promise.all([ + ExposeableFunction.from( + this.environment as BidiFrame, + '__ariaQuerySelector', + ARIAQueryHandler.queryOne, + !!this.sandbox + ), + ExposeableFunction.from( + this.environment as BidiFrame, + '__ariaQuerySelectorAll', + async ( + element: BidiElementHandle<Node>, + selector: string + ): Promise<JSHandle<Node[]>> => { + const results = ARIAQueryHandler.queryAll(element, selector); + return await element.realm.evaluateHandle( + (...elements) => { + return elements; + }, + ...(await AsyncIterableUtil.collect(results)) + ); + }, + !!this.sandbox + ), + ]); + this.#bindingsInstalled = true; + } + return promise.then(() => { + return super.puppeteerUtil; + }); + } + + get sandbox(): string | undefined { + return this.realm.sandbox; + } + + override get environment(): BidiFrame { + return this.#frame; + } + + override async adoptBackendNode( + backendNodeId?: number | undefined + ): Promise<JSHandle<Node>> { + const {object} = await this.#frame.client.send('DOM.resolveNode', { + backendNodeId, + executionContextId: await this.realm.resolveExecutionContextId(), + }); + using handle = BidiElementHandle.from( + { + handle: object.objectId, + type: 'node', + }, + this ); + // We need the sharedId, so we perform the following to obtain it. + return await handle.evaluateHandle(element => { + return element; + }); } } /** * @internal */ -export function createBidiHandle( - sandbox: Sandbox, - result: Bidi.Script.RemoteValue -): BidiJSHandle<unknown> | BidiElementHandle<Node> { - if (result.type === 'node' || result.type === 'window') { - return new BidiElementHandle(sandbox, result); - } - return new BidiJSHandle(sandbox, result); +export class BidiWorkerRealm extends BidiRealm { + static from( + realm: DedicatedWorkerRealm | SharedWorkerRealm, + worker: BidiWebWorker + ): BidiWorkerRealm { + const workerRealm = new BidiWorkerRealm(realm, worker); + workerRealm.initialize(); + return workerRealm; + } + declare readonly realm: DedicatedWorkerRealm | SharedWorkerRealm; + + readonly #worker: BidiWebWorker; + + private constructor( + realm: DedicatedWorkerRealm | SharedWorkerRealm, + frame: BidiWebWorker + ) { + super(realm, frame.timeoutSettings); + this.#worker = frame; + } + + override get environment(): BidiWebWorker { + return this.#worker; + } + + override async adoptBackendNode(): Promise<JSHandle<Node>> { + throw new Error('Cannot adopt DOM nodes into a worker.'); + } } |