UNPKG

5.73 kBPlain TextView Raw
1/**
2 * @license
3 * Copyright 2023 Google Inc.
4 * SPDX-License-Identifier: Apache-2.0
5 */
6
7import type Protocol from 'devtools-protocol';
8
9import type {EvaluateFuncWith, HandleFor, HandleOr} from '../common/types.js';
10import {debugError, withSourcePuppeteerURLIfNone} from '../common/util.js';
11import {moveable, throwIfDisposed} from '../util/decorators.js';
12import {disposeSymbol, asyncDisposeSymbol} from '../util/disposable.js';
13
14import type {ElementHandle} from './ElementHandle.js';
15import type {Realm} from './Realm.js';
16
17/**
18 * Represents a reference to a JavaScript object. Instances can be created using
19 * {@link Page.evaluateHandle}.
20 *
21 * Handles prevent the referenced JavaScript object from being garbage-collected
22 * unless the handle is purposely {@link JSHandle.dispose | disposed}. JSHandles
23 * are auto-disposed when their associated frame is navigated away or the parent
24 * context gets destroyed.
25 *
26 * Handles can be used as arguments for any evaluation function such as
27 * {@link Page.$eval}, {@link Page.evaluate}, and {@link Page.evaluateHandle}.
28 * They are resolved to their referenced object.
29 *
30 * @example
31 *
32 * ```ts
33 * const windowHandle = await page.evaluateHandle(() => window);
34 * ```
35 *
36 * @public
37 */
38@moveable
39export abstract class JSHandle<T = unknown> {
40 declare move: () => this;
41
42 /**
43 * Used for nominally typing {@link JSHandle}.
44 */
45 declare _?: T;
46
47 /**
48 * @internal
49 */
50 constructor() {}
51
52 /**
53 * @internal
54 */
55 abstract get realm(): Realm;
56
57 /**
58 * @internal
59 */
60 abstract get disposed(): boolean;
61
62 /**
63 * Evaluates the given function with the current handle as its first argument.
64 */
65 async evaluate<
66 Params extends unknown[],
67 Func extends EvaluateFuncWith<T, Params> = EvaluateFuncWith<T, Params>,
68 >(
69 pageFunction: Func | string,
70 ...args: Params
71 ): Promise<Awaited<ReturnType<Func>>> {
72 pageFunction = withSourcePuppeteerURLIfNone(
73 this.evaluate.name,
74 pageFunction,
75 );
76 return await this.realm.evaluate(pageFunction, this, ...args);
77 }
78
79 /**
80 * Evaluates the given function with the current handle as its first argument.
81 *
82 */
83 async evaluateHandle<
84 Params extends unknown[],
85 Func extends EvaluateFuncWith<T, Params> = EvaluateFuncWith<T, Params>,
86 >(
87 pageFunction: Func | string,
88 ...args: Params
89 ): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
90 pageFunction = withSourcePuppeteerURLIfNone(
91 this.evaluateHandle.name,
92 pageFunction,
93 );
94 return await this.realm.evaluateHandle(pageFunction, this, ...args);
95 }
96
97 /**
98 * Fetches a single property from the referenced object.
99 */
100 getProperty<K extends keyof T>(
101 propertyName: HandleOr<K>,
102 ): Promise<HandleFor<T[K]>>;
103 getProperty(propertyName: string): Promise<JSHandle<unknown>>;
104
105 /**
106 * @internal
107 */
108 @throwIfDisposed()
109 async getProperty<K extends keyof T>(
110 propertyName: HandleOr<K>,
111 ): Promise<HandleFor<T[K]>> {
112 return await this.evaluateHandle((object, propertyName) => {
113 return object[propertyName as K];
114 }, propertyName);
115 }
116
117 /**
118 * Gets a map of handles representing the properties of the current handle.
119 *
120 * @example
121 *
122 * ```ts
123 * const listHandle = await page.evaluateHandle(() => document.body.children);
124 * const properties = await listHandle.getProperties();
125 * const children = [];
126 * for (const property of properties.values()) {
127 * const element = property.asElement();
128 * if (element) {
129 * children.push(element);
130 * }
131 * }
132 * children; // holds elementHandles to all children of document.body
133 * ```
134 */
135 @throwIfDisposed()
136 async getProperties(): Promise<Map<string, JSHandle>> {
137 const propertyNames = await this.evaluate(object => {
138 const enumerableProperties = [];
139 const descriptors = Object.getOwnPropertyDescriptors(object);
140 for (const propertyName in descriptors) {
141 if (descriptors[propertyName]?.enumerable) {
142 enumerableProperties.push(propertyName);
143 }
144 }
145 return enumerableProperties;
146 });
147 const map = new Map<string, JSHandle>();
148 const results = await Promise.all(
149 propertyNames.map(key => {
150 return this.getProperty(key);
151 }),
152 );
153 for (const [key, value] of Object.entries(propertyNames)) {
154 using handle = results[key as any];
155 if (handle) {
156 map.set(value, handle.move());
157 }
158 }
159 return map;
160 }
161
162 /**
163 * A vanilla object representing the serializable portions of the
164 * referenced object.
165 * @throws Throws if the object cannot be serialized due to circularity.
166 *
167 * @remarks
168 * If the object has a `toJSON` function, it **will not** be called.
169 */
170 abstract jsonValue(): Promise<T>;
171
172 /**
173 * Either `null` or the handle itself if the handle is an
174 * instance of {@link ElementHandle}.
175 */
176 abstract asElement(): ElementHandle<Node> | null;
177
178 /**
179 * Releases the object referenced by the handle for garbage collection.
180 */
181 abstract dispose(): Promise<void>;
182
183 /**
184 * Returns a string representation of the JSHandle.
185 *
186 * @remarks
187 * Useful during debugging.
188 */
189 abstract toString(): string;
190
191 /**
192 * @internal
193 */
194 abstract get id(): string | undefined;
195
196 /**
197 * Provides access to the
198 * {@link https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#type-RemoteObject | Protocol.Runtime.RemoteObject}
199 * backing this handle.
200 */
201 abstract remoteObject(): Protocol.Runtime.RemoteObject;
202
203 /** @internal */
204 [disposeSymbol](): void {
205 return void this.dispose().catch(debugError);
206 }
207
208 /** @internal */
209 [asyncDisposeSymbol](): Promise<void> {
210 return this.dispose();
211 }
212}