ElementHandle.ts (5967B)
1 /** 2 * @license 3 * Copyright 2019 Google Inc. 4 * SPDX-License-Identifier: Apache-2.0 5 */ 6 7 import type {Protocol} from 'devtools-protocol'; 8 9 import type {CDPSession} from '../api/CDPSession.js'; 10 import { 11 bindIsolatedHandle, 12 ElementHandle, 13 type AutofillData, 14 } from '../api/ElementHandle.js'; 15 import type {AwaitableIterable} from '../common/types.js'; 16 import {debugError} from '../common/util.js'; 17 import {environment} from '../environment.js'; 18 import {assert} from '../util/assert.js'; 19 import {AsyncIterableUtil} from '../util/AsyncIterableUtil.js'; 20 import {throwIfDisposed} from '../util/decorators.js'; 21 22 import type {CdpFrame} from './Frame.js'; 23 import type {FrameManager} from './FrameManager.js'; 24 import type {IsolatedWorld} from './IsolatedWorld.js'; 25 import {CdpJSHandle} from './JSHandle.js'; 26 27 const NON_ELEMENT_NODE_ROLES = new Set(['StaticText', 'InlineTextBox']); 28 29 /** 30 * The CdpElementHandle extends ElementHandle now to keep compatibility 31 * with `instanceof` because of that we need to have methods for 32 * CdpJSHandle to in this implementation as well. 33 * 34 * @internal 35 */ 36 export class CdpElementHandle< 37 ElementType extends Node = Element, 38 > extends ElementHandle<ElementType> { 39 declare protected readonly handle: CdpJSHandle<ElementType>; 40 #backendNodeId?: number; 41 42 constructor( 43 world: IsolatedWorld, 44 remoteObject: Protocol.Runtime.RemoteObject, 45 ) { 46 super(new CdpJSHandle(world, remoteObject)); 47 } 48 49 override get realm(): IsolatedWorld { 50 return this.handle.realm; 51 } 52 53 get client(): CDPSession { 54 return this.handle.client; 55 } 56 57 override remoteObject(): Protocol.Runtime.RemoteObject { 58 return this.handle.remoteObject(); 59 } 60 61 get #frameManager(): FrameManager { 62 return this.frame._frameManager; 63 } 64 65 override get frame(): CdpFrame { 66 return this.realm.environment as CdpFrame; 67 } 68 69 override async contentFrame( 70 this: ElementHandle<HTMLIFrameElement>, 71 ): Promise<CdpFrame>; 72 73 @throwIfDisposed() 74 override async contentFrame(): Promise<CdpFrame | null> { 75 const nodeInfo = await this.client.send('DOM.describeNode', { 76 objectId: this.id, 77 }); 78 if (typeof nodeInfo.node.frameId !== 'string') { 79 return null; 80 } 81 return this.#frameManager.frame(nodeInfo.node.frameId); 82 } 83 84 @throwIfDisposed() 85 @bindIsolatedHandle 86 override async scrollIntoView( 87 this: CdpElementHandle<Element>, 88 ): Promise<void> { 89 await this.assertConnectedElement(); 90 try { 91 await this.client.send('DOM.scrollIntoViewIfNeeded', { 92 objectId: this.id, 93 }); 94 } catch (error) { 95 debugError(error); 96 // Fallback to Element.scrollIntoView if DOM.scrollIntoViewIfNeeded is not supported 97 await super.scrollIntoView(); 98 } 99 } 100 101 @throwIfDisposed() 102 @bindIsolatedHandle 103 override async uploadFile( 104 this: CdpElementHandle<HTMLInputElement>, 105 ...files: string[] 106 ): Promise<void> { 107 const isMultiple = await this.evaluate(element => { 108 return element.multiple; 109 }); 110 assert( 111 files.length <= 1 || isMultiple, 112 'Multiple file uploads only work with <input type=file multiple>', 113 ); 114 115 // Locate all files and confirm that they exist. 116 const path = environment.value.path; 117 if (path) { 118 files = files.map(filePath => { 119 if ( 120 path.win32.isAbsolute(filePath) || 121 path.posix.isAbsolute(filePath) 122 ) { 123 return filePath; 124 } else { 125 return path.resolve(filePath); 126 } 127 }); 128 } 129 130 /** 131 * The zero-length array is a special case, it seems that 132 * DOM.setFileInputFiles does not actually update the files in that case, so 133 * the solution is to eval the element value to a new FileList directly. 134 */ 135 if (files.length === 0) { 136 // XXX: These events should converted to trusted events. Perhaps do this 137 // in `DOM.setFileInputFiles`? 138 await this.evaluate(element => { 139 element.files = new DataTransfer().files; 140 141 // Dispatch events for this case because it should behave akin to a user action. 142 element.dispatchEvent( 143 new Event('input', {bubbles: true, composed: true}), 144 ); 145 element.dispatchEvent(new Event('change', {bubbles: true})); 146 }); 147 return; 148 } 149 150 const { 151 node: {backendNodeId}, 152 } = await this.client.send('DOM.describeNode', { 153 objectId: this.id, 154 }); 155 await this.client.send('DOM.setFileInputFiles', { 156 objectId: this.id, 157 files, 158 backendNodeId, 159 }); 160 } 161 162 @throwIfDisposed() 163 override async autofill(data: AutofillData): Promise<void> { 164 const nodeInfo = await this.client.send('DOM.describeNode', { 165 objectId: this.handle.id, 166 }); 167 const fieldId = nodeInfo.node.backendNodeId; 168 const frameId = this.frame._id; 169 await this.client.send('Autofill.trigger', { 170 fieldId, 171 frameId, 172 card: data.creditCard, 173 }); 174 } 175 176 override async *queryAXTree( 177 name?: string | undefined, 178 role?: string | undefined, 179 ): AwaitableIterable<ElementHandle<Node>> { 180 const {nodes} = await this.client.send('Accessibility.queryAXTree', { 181 objectId: this.id, 182 accessibleName: name, 183 role, 184 }); 185 186 const results = nodes.filter(node => { 187 if (node.ignored) { 188 return false; 189 } 190 if (!node.role) { 191 return false; 192 } 193 if (NON_ELEMENT_NODE_ROLES.has(node.role.value)) { 194 return false; 195 } 196 return true; 197 }); 198 199 return yield* AsyncIterableUtil.map(results, node => { 200 return this.realm.adoptBackendNode(node.backendDOMNodeId) as Promise< 201 ElementHandle<Node> 202 >; 203 }); 204 } 205 206 override async backendNodeId(): Promise<number> { 207 if (this.#backendNodeId) { 208 return this.#backendNodeId; 209 } 210 const {node} = await this.client.send('DOM.describeNode', { 211 objectId: this.handle.id, 212 }); 213 this.#backendNodeId = node.backendNodeId; 214 return this.#backendNodeId; 215 } 216 }