| /** |
| * @license |
| * Copyright 2024 Google Inc. |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| import * as Bidi from 'webdriver-bidi-protocol'; |
| |
| import type {JSHandle} from '../api/JSHandle.js'; |
| import {Realm} from '../api/Realm.js'; |
| import {ARIAQueryHandler} from '../common/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 { |
| debugError, |
| getSourcePuppeteerURLIfAvailable, |
| getSourceUrlComment, |
| isString, |
| PuppeteerURL, |
| SOURCE_URL_REGEX, |
| } from '../common/util.js'; |
| import type {PuppeteerInjectedUtil} from '../injected/injected.js'; |
| import {AsyncIterableUtil} from '../util/AsyncIterableUtil.js'; |
| import {stringifyFunction} from '../util/Function.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 {ExposableFunction} from './ExposedFunction.js'; |
| import type {BidiFrame} from './Frame.js'; |
| import {BidiJSHandle} from './JSHandle.js'; |
| import {BidiSerializer} from './Serializer.js'; |
| import {createEvaluationError} from './util.js'; |
| import type {BidiWebWorker} from './WebWorker.js'; |
| |
| /** |
| * @internal |
| */ |
| export abstract class BidiRealm extends Realm { |
| readonly realm: BidiRealmCore; |
| |
| constructor(realm: BidiRealmCore, timeoutSettings: TimeoutSettings) { |
| super(timeoutSettings); |
| this.realm = realm; |
| } |
| |
| protected initialize(): void { |
| this.realm.on('destroyed', ({reason}) => { |
| this.taskManager.terminateAll(new Error(reason)); |
| this.dispose(); |
| }); |
| this.realm.on('updated', () => { |
| this.internalPuppeteerUtil = undefined; |
| void this.taskManager.rerunAll(); |
| }); |
| } |
| |
| protected internalPuppeteerUtil?: Promise< |
| BidiJSHandle<PuppeteerInjectedUtil> |
| >; |
| get puppeteerUtil(): Promise<BidiJSHandle<PuppeteerInjectedUtil>> { |
| const promise = Promise.resolve() as Promise<unknown>; |
| scriptInjector.inject(script => { |
| if (this.internalPuppeteerUtil) { |
| void this.internalPuppeteerUtil.then(handle => { |
| void handle.dispose(); |
| }); |
| } |
| this.internalPuppeteerUtil = promise.then(() => { |
| return this.evaluateHandle(script) as Promise< |
| BidiJSHandle<PuppeteerInjectedUtil> |
| >; |
| }); |
| }, !this.internalPuppeteerUtil); |
| return this.internalPuppeteerUtil as Promise< |
| BidiJSHandle<PuppeteerInjectedUtil> |
| >; |
| } |
| |
| override async evaluateHandle< |
| Params extends unknown[], |
| Func extends EvaluateFunc<Params> = EvaluateFunc<Params>, |
| >( |
| pageFunction: Func | string, |
| ...args: Params |
| ): Promise<HandleFor<Awaited<ReturnType<Func>>>> { |
| return await this.#evaluate(false, pageFunction, ...args); |
| } |
| |
| override async evaluate< |
| Params extends unknown[], |
| Func extends EvaluateFunc<Params> = EvaluateFunc<Params>, |
| >( |
| pageFunction: Func | string, |
| ...args: Params |
| ): Promise<Awaited<ReturnType<Func>>> { |
| return await this.#evaluate(true, pageFunction, ...args); |
| } |
| |
| async #evaluate< |
| Params extends unknown[], |
| Func extends EvaluateFunc<Params> = EvaluateFunc<Params>, |
| >( |
| returnByValue: true, |
| pageFunction: Func | string, |
| ...args: Params |
| ): Promise<Awaited<ReturnType<Func>>>; |
| async #evaluate< |
| Params extends unknown[], |
| Func extends EvaluateFunc<Params> = EvaluateFunc<Params>, |
| >( |
| returnByValue: false, |
| pageFunction: Func | string, |
| ...args: Params |
| ): Promise<HandleFor<Awaited<ReturnType<Func>>>>; |
| async #evaluate< |
| Params extends unknown[], |
| Func extends EvaluateFunc<Params> = EvaluateFunc<Params>, |
| >( |
| returnByValue: boolean, |
| pageFunction: Func | string, |
| ...args: Params |
| ): Promise<HandleFor<Awaited<ReturnType<Func>>> | Awaited<ReturnType<Func>>> { |
| const sourceUrlComment = getSourceUrlComment( |
| getSourcePuppeteerURLIfAvailable(pageFunction)?.toString() ?? |
| PuppeteerURL.INTERNAL_URL, |
| ); |
| |
| let responsePromise; |
| const resultOwnership = returnByValue |
| ? Bidi.Script.ResultOwnership.None |
| : Bidi.Script.ResultOwnership.Root; |
| const serializationOptions: Bidi.Script.SerializationOptions = returnByValue |
| ? {} |
| : { |
| maxObjectDepth: 0, |
| maxDomDepth: 0, |
| }; |
| if (isString(pageFunction)) { |
| const expression = SOURCE_URL_REGEX.test(pageFunction) |
| ? pageFunction |
| : `${pageFunction}\n${sourceUrlComment}\n`; |
| |
| responsePromise = this.realm.evaluate(expression, true, { |
| resultOwnership, |
| userActivation: true, |
| serializationOptions, |
| }); |
| } else { |
| let functionDeclaration = stringifyFunction(pageFunction); |
| functionDeclaration = SOURCE_URL_REGEX.test(functionDeclaration) |
| ? functionDeclaration |
| : `${functionDeclaration}\n${sourceUrlComment}\n`; |
| responsePromise = this.realm.callFunction( |
| functionDeclaration, |
| /* awaitPromise= */ true, |
| { |
| // LazyArgs are used only internally and should not affect the order |
| // evaluate calls for the public APIs. |
| arguments: args.some(arg => { |
| return arg instanceof LazyArg; |
| }) |
| ? await Promise.all( |
| args.map(arg => { |
| return this.serializeAsync(arg); |
| }), |
| ) |
| : args.map(arg => { |
| return this.serialize(arg); |
| }), |
| resultOwnership, |
| userActivation: true, |
| serializationOptions, |
| }, |
| ); |
| } |
| |
| const result = await responsePromise; |
| |
| if ('type' in result && result.type === 'exception') { |
| throw createEvaluationError(result.exceptionDetails); |
| } |
| |
| if (returnByValue) { |
| return BidiDeserializer.deserialize(result.result); |
| } |
| |
| return this.createHandle(result.result) as unknown as HandleFor< |
| Awaited<ReturnType<Func>> |
| >; |
| } |
| |
| 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 serializeAsync(arg: unknown): Promise<Bidi.Script.LocalValue> { |
| if (arg instanceof LazyArg) { |
| arg = await arg.get(this); |
| } |
| return this.serialize(arg); |
| } |
| |
| serialize(arg: unknown): Bidi.Script.LocalValue { |
| 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> { |
| if (this.disposed) { |
| return; |
| } |
| |
| 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<PuppeteerInjectedUtil>> { |
| let promise = Promise.resolve() as Promise<unknown>; |
| if (!this.#bindingsInstalled) { |
| promise = Promise.all([ |
| ExposableFunction.from( |
| this.environment, |
| '__ariaQuerySelector', |
| ARIAQueryHandler.queryOne, |
| !!this.sandbox, |
| ), |
| ExposableFunction.from( |
| this.environment, |
| '__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 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.'); |
| } |
| } |