JSHandle.ts (5729B)
1 /** 2 * @license 3 * Copyright 2023 Google Inc. 4 * SPDX-License-Identifier: Apache-2.0 5 */ 6 7 import type Protocol from 'devtools-protocol'; 8 9 import type {EvaluateFuncWith, HandleFor, HandleOr} from '../common/types.js'; 10 import {debugError, withSourcePuppeteerURLIfNone} from '../common/util.js'; 11 import {moveable, throwIfDisposed} from '../util/decorators.js'; 12 import {disposeSymbol, asyncDisposeSymbol} from '../util/disposable.js'; 13 14 import type {ElementHandle} from './ElementHandle.js'; 15 import 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 39 export 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 }