diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /remote/test/puppeteer/packages/puppeteer-core/src/cdp/ExecutionContext.ts | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'remote/test/puppeteer/packages/puppeteer-core/src/cdp/ExecutionContext.ts')
-rw-r--r-- | remote/test/puppeteer/packages/puppeteer-core/src/cdp/ExecutionContext.ts | 392 |
1 files changed, 392 insertions, 0 deletions
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/cdp/ExecutionContext.ts b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/ExecutionContext.ts new file mode 100644 index 0000000000..6efdf8ac76 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/cdp/ExecutionContext.ts @@ -0,0 +1,392 @@ +/** + * @license + * Copyright 2017 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {Protocol} from 'devtools-protocol'; + +import type {CDPSession} from '../api/CDPSession.js'; +import type {ElementHandle} from '../api/ElementHandle.js'; +import type {JSHandle} from '../api/JSHandle.js'; +import {LazyArg} from '../common/LazyArg.js'; +import {scriptInjector} from '../common/ScriptInjector.js'; +import type {EvaluateFunc, HandleFor} from '../common/types.js'; +import { + PuppeteerURL, + SOURCE_URL_REGEX, + getSourcePuppeteerURLIfAvailable, + getSourceUrlComment, + isString, +} from '../common/util.js'; +import type PuppeteerUtil from '../injected/injected.js'; +import {AsyncIterableUtil} from '../util/AsyncIterableUtil.js'; +import {stringifyFunction} from '../util/Function.js'; + +import {ARIAQueryHandler} from './AriaQueryHandler.js'; +import {Binding} from './Binding.js'; +import {CdpElementHandle} from './ElementHandle.js'; +import type {IsolatedWorld} from './IsolatedWorld.js'; +import {CdpJSHandle} from './JSHandle.js'; +import {createEvaluationError, valueFromRemoteObject} from './utils.js'; + +/** + * @internal + */ +export class ExecutionContext { + _client: CDPSession; + _world: IsolatedWorld; + _contextId: number; + _contextName?: string; + + constructor( + client: CDPSession, + contextPayload: Protocol.Runtime.ExecutionContextDescription, + world: IsolatedWorld + ) { + this._client = client; + this._world = world; + this._contextId = contextPayload.id; + if (contextPayload.name) { + this._contextName = contextPayload.name; + } + } + + #bindingsInstalled = false; + #puppeteerUtil?: Promise<JSHandle<PuppeteerUtil>>; + get puppeteerUtil(): Promise<JSHandle<PuppeteerUtil>> { + let promise = Promise.resolve() as Promise<unknown>; + if (!this.#bindingsInstalled) { + promise = Promise.all([ + this.#installGlobalBinding( + new Binding( + '__ariaQuerySelector', + ARIAQueryHandler.queryOne as (...args: unknown[]) => unknown + ) + ), + this.#installGlobalBinding( + new Binding('__ariaQuerySelectorAll', (async ( + element: ElementHandle<Node>, + selector: string + ): Promise<JSHandle<Node[]>> => { + const results = ARIAQueryHandler.queryAll(element, selector); + return await element.realm.evaluateHandle( + (...elements) => { + return elements; + }, + ...(await AsyncIterableUtil.collect(results)) + ); + }) as (...args: unknown[]) => unknown) + ), + ]); + this.#bindingsInstalled = true; + } + scriptInjector.inject(script => { + if (this.#puppeteerUtil) { + void this.#puppeteerUtil.then(handle => { + void handle.dispose(); + }); + } + this.#puppeteerUtil = promise.then(() => { + return this.evaluateHandle(script) as Promise<JSHandle<PuppeteerUtil>>; + }); + }, !this.#puppeteerUtil); + return this.#puppeteerUtil as Promise<JSHandle<PuppeteerUtil>>; + } + + async #installGlobalBinding(binding: Binding) { + try { + if (this._world) { + this._world._bindings.set(binding.name, binding); + await this._world._addBindingToContext(this, binding.name); + } + } catch { + // If the binding cannot be added, then either the browser doesn't support + // bindings (e.g. Firefox) or the context is broken. Either breakage is + // okay, so we ignore the error. + } + } + + /** + * Evaluates the given function. + * + * @example + * + * ```ts + * const executionContext = await page.mainFrame().executionContext(); + * const result = await executionContext.evaluate(() => Promise.resolve(8 * 7))* ; + * console.log(result); // prints "56" + * ``` + * + * @example + * A string can also be passed in instead of a function: + * + * ```ts + * console.log(await executionContext.evaluate('1 + 2')); // prints "3" + * ``` + * + * @example + * Handles can also be passed as `args`. They resolve to their referenced object: + * + * ```ts + * const oneHandle = await executionContext.evaluateHandle(() => 1); + * const twoHandle = await executionContext.evaluateHandle(() => 2); + * const result = await executionContext.evaluate( + * (a, b) => a + b, + * oneHandle, + * twoHandle + * ); + * await oneHandle.dispose(); + * await twoHandle.dispose(); + * console.log(result); // prints '3'. + * ``` + * + * @param pageFunction - The function to evaluate. + * @param args - Additional arguments to pass into the function. + * @returns The result of evaluating the function. If the result is an object, + * a vanilla object containing the serializable properties of the result is + * returned. + */ + 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); + } + + /** + * Evaluates the given function. + * + * Unlike {@link ExecutionContext.evaluate | evaluate}, this method returns a + * handle to the result of the function. + * + * This method may be better suited if the object cannot be serialized (e.g. + * `Map`) and requires further manipulation. + * + * @example + * + * ```ts + * const context = await page.mainFrame().executionContext(); + * const handle: JSHandle<typeof globalThis> = await context.evaluateHandle( + * () => Promise.resolve(self) + * ); + * ``` + * + * @example + * A string can also be passed in instead of a function. + * + * ```ts + * const handle: JSHandle<number> = await context.evaluateHandle('1 + 2'); + * ``` + * + * @example + * Handles can also be passed as `args`. They resolve to their referenced object: + * + * ```ts + * const bodyHandle: ElementHandle<HTMLBodyElement> = + * await context.evaluateHandle(() => { + * return document.body; + * }); + * const stringHandle: JSHandle<string> = await context.evaluateHandle( + * body => body.innerHTML, + * body + * ); + * console.log(await stringHandle.jsonValue()); // prints body's innerHTML + * // Always dispose your garbage! :) + * await bodyHandle.dispose(); + * await stringHandle.dispose(); + * ``` + * + * @param pageFunction - The function to evaluate. + * @param args - Additional arguments to pass into the function. + * @returns A {@link JSHandle | handle} to the result of evaluating the + * function. If the result is a `Node`, then this will return an + * {@link ElementHandle | element handle}. + */ + 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); + } + + 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 + ); + + if (isString(pageFunction)) { + const contextId = this._contextId; + const expression = pageFunction; + const expressionWithSourceUrl = SOURCE_URL_REGEX.test(expression) + ? expression + : `${expression}\n${sourceUrlComment}\n`; + + const {exceptionDetails, result: remoteObject} = await this._client + .send('Runtime.evaluate', { + expression: expressionWithSourceUrl, + contextId, + returnByValue, + awaitPromise: true, + userGesture: true, + }) + .catch(rewriteError); + + if (exceptionDetails) { + throw createEvaluationError(exceptionDetails); + } + + return returnByValue + ? valueFromRemoteObject(remoteObject) + : createCdpHandle(this._world, remoteObject); + } + + const functionDeclaration = stringifyFunction(pageFunction); + const functionDeclarationWithSourceUrl = SOURCE_URL_REGEX.test( + functionDeclaration + ) + ? functionDeclaration + : `${functionDeclaration}\n${sourceUrlComment}\n`; + let callFunctionOnPromise; + try { + callFunctionOnPromise = this._client.send('Runtime.callFunctionOn', { + functionDeclaration: functionDeclarationWithSourceUrl, + executionContextId: this._contextId, + arguments: args.length + ? await Promise.all(args.map(convertArgument.bind(this))) + : [], + returnByValue, + awaitPromise: true, + userGesture: true, + }); + } catch (error) { + if ( + error instanceof TypeError && + error.message.startsWith('Converting circular structure to JSON') + ) { + error.message += ' Recursive objects are not allowed.'; + } + throw error; + } + const {exceptionDetails, result: remoteObject} = + await callFunctionOnPromise.catch(rewriteError); + if (exceptionDetails) { + throw createEvaluationError(exceptionDetails); + } + return returnByValue + ? valueFromRemoteObject(remoteObject) + : createCdpHandle(this._world, remoteObject); + + async function convertArgument( + this: ExecutionContext, + arg: unknown + ): Promise<Protocol.Runtime.CallArgument> { + if (arg instanceof LazyArg) { + arg = await arg.get(this); + } + if (typeof arg === 'bigint') { + // eslint-disable-line valid-typeof + return {unserializableValue: `${arg.toString()}n`}; + } + if (Object.is(arg, -0)) { + return {unserializableValue: '-0'}; + } + if (Object.is(arg, Infinity)) { + return {unserializableValue: 'Infinity'}; + } + if (Object.is(arg, -Infinity)) { + return {unserializableValue: '-Infinity'}; + } + if (Object.is(arg, NaN)) { + return {unserializableValue: 'NaN'}; + } + const objectHandle = + arg && (arg instanceof CdpJSHandle || arg instanceof CdpElementHandle) + ? arg + : null; + if (objectHandle) { + if (objectHandle.realm !== this._world) { + throw new Error( + 'JSHandles can be evaluated only in the context they were created!' + ); + } + if (objectHandle.disposed) { + throw new Error('JSHandle is disposed!'); + } + if (objectHandle.remoteObject().unserializableValue) { + return { + unserializableValue: + objectHandle.remoteObject().unserializableValue, + }; + } + if (!objectHandle.remoteObject().objectId) { + return {value: objectHandle.remoteObject().value}; + } + return {objectId: objectHandle.remoteObject().objectId}; + } + return {value: arg}; + } + } +} + +const rewriteError = (error: Error): Protocol.Runtime.EvaluateResponse => { + if (error.message.includes('Object reference chain is too long')) { + return {result: {type: 'undefined'}}; + } + if (error.message.includes("Object couldn't be returned by value")) { + return {result: {type: 'undefined'}}; + } + + if ( + error.message.endsWith('Cannot find context with specified id') || + error.message.endsWith('Inspected target navigated or closed') + ) { + throw new Error( + 'Execution context was destroyed, most likely because of a navigation.' + ); + } + throw error; +}; + +/** + * @internal + */ +export function createCdpHandle( + realm: IsolatedWorld, + remoteObject: Protocol.Runtime.RemoteObject +): JSHandle | ElementHandle<Node> { + if (remoteObject.subtype === 'node') { + return new CdpElementHandle(realm, remoteObject); + } + return new CdpJSHandle(realm, remoteObject); +} |