/** * @license * Copyright 2023 Google Inc. * SPDX-License-Identifier: Apache-2.0 */ import type Protocol from 'devtools-protocol'; import type {EvaluateFuncWith, HandleFor, HandleOr} from '../common/types.js'; import {debugError, withSourcePuppeteerURLIfNone} from '../common/util.js'; import {moveable, throwIfDisposed} from '../util/decorators.js'; import {disposeSymbol, asyncDisposeSymbol} from '../util/disposable.js'; import type {ElementHandle} from './ElementHandle.js'; import type {Realm} from './Realm.js'; /** * Represents a reference to a JavaScript object. Instances can be created using * {@link Page.evaluateHandle}. * * Handles prevent the referenced JavaScript object from being garbage-collected * unless the handle is purposely {@link JSHandle.dispose | disposed}. JSHandles * are auto-disposed when their associated frame is navigated away or the parent * context gets destroyed. * * Handles can be used as arguments for any evaluation function such as * {@link Page.$eval}, {@link Page.evaluate}, and {@link Page.evaluateHandle}. * They are resolved to their referenced object. * * @example * * ```ts * const windowHandle = await page.evaluateHandle(() => window); * ``` * * @public */ @moveable export abstract class JSHandle<T = unknown> { declare move: () => this; /** * Used for nominally typing {@link JSHandle}. */ declare _?: T; /** * @internal */ constructor() {} /** * @internal */ abstract get realm(): Realm; /** * @internal */ abstract get disposed(): boolean; /** * Evaluates the given function with the current handle as its first argument. */ 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.realm.evaluate(pageFunction, this, ...args); } /** * Evaluates the given function with the current handle as its first argument. * */ 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.realm.evaluateHandle(pageFunction, this, ...args); } /** * Fetches a single property from the referenced object. */ getProperty<K extends keyof T>( propertyName: HandleOr<K> ): Promise<HandleFor<T[K]>>; getProperty(propertyName: string): Promise<JSHandle<unknown>>; /** * @internal */ @throwIfDisposed() 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. * * @example * * ```ts * const listHandle = await page.evaluateHandle(() => document.body.children); * const properties = await listHandle.getProperties(); * const children = []; * for (const property of properties.values()) { * const element = property.asElement(); * if (element) { * children.push(element); * } * } * children; // holds elementHandles to all children of document.body * ``` */ @throwIfDisposed() 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 * referenced object. * @throws Throws if the object cannot be serialized due to circularity. * * @remarks * If the object has a `toJSON` function, it **will not** be called. */ abstract jsonValue(): Promise<T>; /** * Either `null` or the handle itself if the handle is an * instance of {@link ElementHandle}. */ abstract asElement(): ElementHandle<Node> | null; /** * Releases the object referenced by the handle for garbage collection. */ abstract dispose(): Promise<void>; /** * Returns a string representation of the JSHandle. * * @remarks * Useful during debugging. */ abstract toString(): string; /** * @internal */ abstract get id(): string | undefined; /** * Provides access to the * {@link https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#type-RemoteObject | Protocol.Runtime.RemoteObject} * backing this handle. */ abstract remoteObject(): Protocol.Runtime.RemoteObject; /** @internal */ [disposeSymbol](): void { return void this.dispose().catch(debugError); } /** @internal */ [asyncDisposeSymbol](): Promise<void> { return this.dispose(); } }