input.sys.mjs (12352B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { RootBiDiModule } from "chrome://remote/content/webdriver-bidi/modules/RootBiDiModule.sys.mjs"; 6 7 const lazy = {}; 8 9 ChromeUtils.defineESModuleGetters(lazy, { 10 actions: "chrome://remote/content/shared/webdriver/Actions.sys.mjs", 11 assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs", 12 error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs", 13 event: "chrome://remote/content/shared/webdriver/Event.sys.mjs", 14 NavigableManager: "chrome://remote/content/shared/NavigableManager.sys.mjs", 15 pprint: "chrome://remote/content/shared/Format.sys.mjs", 16 }); 17 18 class InputModule extends RootBiDiModule { 19 #actionsOptions; 20 #inputStates; 21 22 constructor(messageHandler) { 23 super(messageHandler); 24 25 // Browsing context => input state. 26 // Bug 1821460: Move to WebDriver Session and share with Marionette. 27 this.#inputStates = new WeakMap(); 28 29 // Options for actions to pass through performActions and releaseActions. 30 this.#actionsOptions = { 31 // Callbacks as defined in the WebDriver specification. 32 getElementOrigin: this.#getElementOrigin.bind(this), 33 isElementOrigin: this.#isElementOrigin.bind(this), 34 35 // Custom callbacks. 36 assertInViewPort: this.#assertInViewPort.bind(this), 37 dispatchEvent: this.#dispatchEvent.bind(this), 38 getClientRects: this.#getClientRects.bind(this), 39 getInViewCentrePoint: this.#getInViewCentrePoint.bind(this), 40 toBrowserWindowCoordinates: this.#toBrowserWindowCoordinates.bind(this), 41 }; 42 } 43 44 destroy() {} 45 46 /** 47 * Assert that the target coordinates are within the visible viewport. 48 * 49 * @param {Array.<number>} target 50 * Coordinates [x, y] of the target relative to the viewport. 51 * @param {BrowsingContext} context 52 * The browsing context to dispatch the event to. 53 * 54 * @returns {Promise<undefined>} 55 * Promise that rejects, if the coordinates are not within 56 * the visible viewport. 57 * 58 * @throws {MoveTargetOutOfBoundsError} 59 * If target is outside the viewport. 60 */ 61 #assertInViewPort(target, context) { 62 return this._forwardToWindowGlobal("_assertInViewPort", context.id, { 63 target, 64 }); 65 } 66 67 /** 68 * Dispatch an event. 69 * 70 * @param {string} eventName 71 * Name of the event to be dispatched. 72 * @param {BrowsingContext} context 73 * The browsing context to dispatch the event to. 74 * @param {object} details 75 * Details of the event to be dispatched. 76 * 77 * @returns {Promise} 78 * Promise that resolves once the event is dispatched. 79 */ 80 async #dispatchEvent(eventName, context, details) { 81 details.eventData.asyncEnabled = 82 (eventName === "synthesizeWheelAtPoint" && 83 lazy.actions.useAsyncWheelEvents) || 84 (eventName == "synthesizeMouseAtPoint" && 85 lazy.actions.useAsyncMouseEvents); 86 87 // TODO: Call the _dispatchEvent method of the windowglobal module once 88 // chrome support was added for the message handler. 89 if (details.eventData.asyncEnabled) { 90 if (!context || context.isDiscarded) { 91 const id = lazy.NavigableManager.getIdForBrowsingContext(context); 92 throw new lazy.error.NoSuchFrameError( 93 `Browsing Context with id ${id} not found` 94 ); 95 } 96 97 switch (eventName) { 98 case "synthesizeMouseAtPoint": 99 await lazy.event.synthesizeMouseAtPoint( 100 details.x, 101 details.y, 102 details.eventData, 103 context.topChromeWindow 104 ); 105 break; 106 case "synthesizeWheelAtPoint": 107 await lazy.event.synthesizeWheelAtPoint( 108 details.x, 109 details.y, 110 details.eventData, 111 context.topChromeWindow 112 ); 113 break; 114 default: 115 throw new Error( 116 `${eventName} is not a supported type for dispatching` 117 ); 118 } 119 } else { 120 await this._forwardToWindowGlobal("_dispatchEvent", context.id, { 121 eventName, 122 details, 123 }); 124 } 125 } 126 127 /** 128 * Finalize an action command. 129 * 130 * @param {BrowsingContext} context 131 * The browsing context to forward the command to. 132 */ 133 async #finalizeAction(context) { 134 try { 135 await this._forwardToWindowGlobal("_finalizeAction", context.id); 136 } catch (e) { 137 // Ignore the error if the underlying browsing context is already gone. 138 if (e.name !== "DiscardedBrowsingContextError") { 139 throw e; 140 } 141 } 142 } 143 144 /** 145 * Retrieve the list of client rects for the element. 146 * 147 * @param {Node} node 148 * The web element reference to retrieve the rects from. 149 * @param {BrowsingContext} context 150 * The browsing context to dispatch the event to. 151 * 152 * @returns {Promise<Array<Map.<string, number>>>} 153 * Promise that resolves to a list of DOMRect-like objects. 154 */ 155 #getClientRects(node, context) { 156 return this._forwardToWindowGlobal("_getClientRects", context.id, { 157 element: node, 158 }); 159 } 160 161 /** 162 * Retrieves the Node reference of the origin. 163 * 164 * @param {ElementOrigin} origin 165 * Reference to the element origin of the action. 166 * @param {BrowsingContext} context 167 * The browsing context to dispatch the event to. 168 * 169 * @returns {Promise<SharedReference>} 170 * Promise that resolves to the shared reference. 171 */ 172 #getElementOrigin(origin, context) { 173 return this._forwardToWindowGlobal("_getElementOrigin", context.id, { 174 origin, 175 }); 176 } 177 178 /** 179 * Retrieves the action's input state. 180 * 181 * @param {BrowsingContext} context 182 * The Browsing Context to retrieve the input state for. 183 * 184 * @returns {Actions.InputState} 185 * The action's input state. 186 */ 187 #getInputState(context) { 188 // Bug 1821460: Fetch top-level browsing context. 189 let inputState = this.#inputStates.get(context); 190 191 if (inputState === undefined) { 192 inputState = new lazy.actions.State(); 193 this.#inputStates.set(context, inputState); 194 } 195 196 return inputState; 197 } 198 199 /** 200 * Retrieve the in-view center point for the rect and visible viewport. 201 * 202 * @param {DOMRect} rect 203 * Size and position of the rectangle to check. 204 * @param {BrowsingContext} context 205 * The browsing context to dispatch the event to. 206 * 207 * @returns {Promise<Map.<string, number>>} 208 * X and Y coordinates that denotes the in-view centre point of 209 * `rect`. 210 */ 211 #getInViewCentrePoint(rect, context) { 212 return this._forwardToWindowGlobal("_getInViewCentrePoint", context.id, { 213 rect, 214 }); 215 } 216 217 /** 218 * Checks if the given object is a valid element origin. 219 * 220 * @param {object} origin 221 * The object to check. 222 * 223 * @returns {boolean} 224 * True, if the object references a shared reference. 225 */ 226 #isElementOrigin(origin) { 227 return ( 228 origin?.type === "element" && typeof origin.element?.sharedId === "string" 229 ); 230 } 231 232 /** 233 * Resets the action's input state. 234 * 235 * @param {BrowsingContext} context 236 * The Browsing Context to reset the input state for. 237 */ 238 #resetInputState(context) { 239 // Bug 1821460: Fetch top-level browsing context. 240 if (this.#inputStates.has(context)) { 241 this.#inputStates.delete(context); 242 } 243 } 244 245 /** 246 * Convert a position or rect in browser coordinates of CSS units. 247 * 248 * @param {object} position - Object with the coordinates to convert. 249 * @param {number} position.x - X coordinate. 250 * @param {number} position.y - Y coordinate. 251 * @param {BrowsingContext} context - The Browsing Context to convert the 252 * coordinates for. 253 */ 254 #toBrowserWindowCoordinates(position, context) { 255 return this._forwardToWindowGlobal( 256 "_toBrowserWindowCoordinates", 257 context.id, 258 { position } 259 ); 260 } 261 262 /** 263 * Perform a series of grouped actions at the specified points in time. 264 * 265 * @param {object} options 266 * @param {Array<?>} options.actions 267 * Array of objects that each represent an action sequence. 268 * @param {string} options.context 269 * Id of the browsing context to reset the input state. 270 * 271 * @throws {InvalidArgumentError} 272 * If <var>context</var> is not valid type. 273 * @throws {MoveTargetOutOfBoundsError} 274 * If target is outside the viewport. 275 * @throws {NoSuchFrameError} 276 * If the browsing context cannot be found. 277 * @throws {NoSuchElementError} 278 * If an element that is used as part of the action chain is unknown. 279 */ 280 async performActions(options = {}) { 281 const { actions, context: contextId } = options; 282 283 lazy.assert.string( 284 contextId, 285 lazy.pprint`Expected "context" to be a string, got ${contextId}` 286 ); 287 288 const context = this._getNavigable(contextId); 289 290 // Bug 1821460: Fetch top-level browsing context. 291 const inputState = this.#getInputState(context); 292 const actionsOptions = { ...this.#actionsOptions, context }; 293 294 const actionChain = await lazy.actions.Chain.fromJSON( 295 inputState, 296 actions, 297 actionsOptions 298 ); 299 300 // Enqueue to serialize access to input state. 301 await inputState.enqueueAction(() => 302 actionChain.dispatch(inputState, actionsOptions) 303 ); 304 305 // Process async follow-up tasks in content before the reply is sent. 306 await this.#finalizeAction(context); 307 } 308 309 /** 310 * Reset the input state in the provided browsing context. 311 * 312 * @param {object=} options 313 * @param {string} options.context 314 * Id of the browsing context to reset the input state. 315 * 316 * @throws {InvalidArgumentError} 317 * If <var>context</var> is not valid type. 318 * @throws {NoSuchFrameError} 319 * If the browsing context cannot be found. 320 */ 321 async releaseActions(options = {}) { 322 const { context: contextId } = options; 323 324 lazy.assert.string( 325 contextId, 326 lazy.pprint`Expected "context" to be a string, got ${contextId}` 327 ); 328 329 const context = this._getNavigable(contextId); 330 331 // Bug 1821460: Fetch top-level browsing context. 332 const inputState = this.#getInputState(context); 333 const actionsOptions = { ...this.#actionsOptions, context }; 334 335 // Enqueue to serialize access to input state. 336 await inputState.enqueueAction(() => { 337 const undoActions = inputState.inputCancelList.reverse(); 338 return undoActions.dispatch(inputState, actionsOptions); 339 }); 340 341 this.#resetInputState(context); 342 343 // Process async follow-up tasks in content before the reply is sent. 344 await this.#finalizeAction(context); 345 } 346 347 /** 348 * Sets the file property of a given input element with type file to a set of file paths. 349 * 350 * @param {object=} options 351 * @param {string} options.context 352 * Id of the browsing context to set the file property 353 * of a given input element. 354 * @param {SharedReference} options.element 355 * A reference to a node, which is used as 356 * a target for setting files. 357 * @param {Array<string>} options.files 358 * A list of file paths which should be set. 359 * 360 * @throws {InvalidArgumentError} 361 * Raised if an argument is of an invalid type or value. 362 * @throws {NoSuchElementError} 363 * If the input element cannot be found. 364 * @throws {NoSuchFrameError} 365 * If the browsing context cannot be found. 366 * @throws {UnableToSetFileInputError} 367 * If the set of file paths was not set to the input element. 368 */ 369 async setFiles(options = {}) { 370 const { context: contextId, element, files } = options; 371 372 lazy.assert.string( 373 contextId, 374 lazy.pprint`Expected "context" to be a string, got ${contextId}` 375 ); 376 377 const context = this._getNavigable(contextId); 378 379 lazy.assert.array( 380 files, 381 lazy.pprint`Expected "files" to be an array, got ${files}` 382 ); 383 384 for (const file of files) { 385 lazy.assert.string( 386 file, 387 lazy.pprint`Expected an element of "files" to be a string, got ${file}` 388 ); 389 } 390 391 await this._forwardToWindowGlobal("setFiles", context.id, { 392 element, 393 files, 394 }); 395 } 396 397 static get supportedEvents() { 398 return ["input.fileDialogOpened"]; 399 } 400 } 401 402 export const input = InputModule;