summaryrefslogtreecommitdiffstats
path: root/remote/test/puppeteer/packages/puppeteer-core/src/common/ExecutionContext.ts
diff options
context:
space:
mode:
Diffstat (limited to 'remote/test/puppeteer/packages/puppeteer-core/src/common/ExecutionContext.ts')
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/common/ExecutionContext.ts402
1 files changed, 402 insertions, 0 deletions
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/ExecutionContext.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/ExecutionContext.ts
new file mode 100644
index 0000000000..e00c45b2dd
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/ExecutionContext.ts
@@ -0,0 +1,402 @@
+/**
+ * Copyright 2017 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 {Protocol} from 'devtools-protocol';
+
+import type {ElementHandle} from '../api/ElementHandle.js';
+import {JSHandle} from '../api/JSHandle.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 {CDPSession} from './Connection.js';
+import {CDPElementHandle} from './ElementHandle.js';
+import {IsolatedWorld} from './IsolatedWorld.js';
+import {CDPJSHandle} from './JSHandle.js';
+import {LazyArg} from './LazyArg.js';
+import {scriptInjector} from './ScriptInjector.js';
+import {EvaluateFunc, HandleFor} from './types.js';
+import {
+ createJSHandle,
+ getExceptionMessage,
+ isString,
+ valueFromRemoteObject,
+} from './util.js';
+
+/**
+ * @public
+ */
+export const EVALUATION_SCRIPT_URL = 'pptr://__puppeteer_evaluation_script__';
+const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m;
+
+/**
+ * Represents a context for JavaScript execution.
+ *
+ * @example
+ * A {@link Page} can have several execution contexts:
+ *
+ * - Each {@link Frame} of a {@link Page | page} has a "default" execution
+ * context that is always created after frame is attached to DOM. This context
+ * is returned by the {@link Frame.executionContext} method.
+ * - Each {@link https://developer.chrome.com/extensions | Chrome extensions}
+ * creates additional execution contexts to isolate their code.
+ *
+ * @remarks
+ * By definition, each context is isolated from one another, however they are
+ * all able to manipulate non-JavaScript resources (such as DOM).
+ *
+ * @remarks
+ * Besides pages, execution contexts can be found in
+ * {@link WebWorker | workers}.
+ *
+ * @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 element.executionContext().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 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 suffix = `//# sourceURL=${EVALUATION_SCRIPT_URL}`;
+
+ if (isString(pageFunction)) {
+ const contextId = this._contextId;
+ const expression = pageFunction;
+ const expressionWithSourceUrl = SOURCE_URL_REGEX.test(expression)
+ ? expression
+ : expression + '\n' + suffix;
+
+ const {exceptionDetails, result: remoteObject} = await this._client
+ .send('Runtime.evaluate', {
+ expression: expressionWithSourceUrl,
+ contextId,
+ returnByValue,
+ awaitPromise: true,
+ userGesture: true,
+ })
+ .catch(rewriteError);
+
+ if (exceptionDetails) {
+ throw new Error(
+ 'Evaluation failed: ' + getExceptionMessage(exceptionDetails)
+ );
+ }
+
+ return returnByValue
+ ? valueFromRemoteObject(remoteObject)
+ : createJSHandle(this, remoteObject);
+ }
+
+ let callFunctionOnPromise;
+ try {
+ callFunctionOnPromise = this._client.send('Runtime.callFunctionOn', {
+ functionDeclaration: `${stringifyFunction(pageFunction)}\n${suffix}\n`,
+ executionContextId: this._contextId,
+ arguments: 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 new Error(
+ 'Evaluation failed: ' + getExceptionMessage(exceptionDetails)
+ );
+ }
+ return returnByValue
+ ? valueFromRemoteObject(remoteObject)
+ : createJSHandle(this, 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.executionContext() !== this) {
+ 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;
+};