MarionetteCommandsParent.sys.mjs (13203B)
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 const lazy = {}; 6 7 ChromeUtils.defineESModuleGetters(lazy, { 8 capture: "chrome://remote/content/shared/Capture.sys.mjs", 9 error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs", 10 getSeenNodesForBrowsingContext: 11 "chrome://remote/content/shared/webdriver/Session.sys.mjs", 12 json: "chrome://remote/content/marionette/json.sys.mjs", 13 Log: "chrome://remote/content/shared/Log.sys.mjs", 14 }); 15 16 ChromeUtils.defineLazyGetter(lazy, "logger", () => 17 lazy.Log.get(lazy.Log.TYPES.MARIONETTE) 18 ); 19 20 // Because Marionette supports a single session only we store its id 21 // globally so that the parent actor can access it. 22 let webDriverSessionId = null; 23 24 export class MarionetteCommandsParent extends JSWindowActorParent { 25 #deferredDialogOpened; 26 27 actorCreated() { 28 this.#deferredDialogOpened = null; 29 } 30 31 assertInViewPort(target, _context) { 32 return this.sendQuery("MarionetteCommandsParent:_assertInViewPort", { 33 target, 34 }); 35 } 36 37 dispatchEvent(eventName, details) { 38 return this.sendQuery("MarionetteCommandsParent:_dispatchEvent", { 39 eventName, 40 details, 41 }); 42 } 43 44 finalizeAction() { 45 return this.sendQuery("MarionetteCommandsParent:_finalizeAction"); 46 } 47 48 getClientRects(webEl, _context) { 49 return this.sendQuery("MarionetteCommandsParent:_getClientRects", { 50 elem: webEl, 51 }); 52 } 53 54 getInViewCentrePoint(rect, _context) { 55 return this.sendQuery("MarionetteCommandsParent:_getInViewCentrePoint", { 56 rect, 57 }); 58 } 59 60 toBrowserWindowCoordinates(position, _context) { 61 return this.sendQuery( 62 "MarionetteCommandsParent:_toBrowserWindowCoordinates", 63 { 64 position, 65 } 66 ); 67 } 68 69 async sendQuery(name, serializedValue) { 70 const seenNodes = lazy.getSeenNodesForBrowsingContext( 71 webDriverSessionId, 72 this.manager.browsingContext 73 ); 74 75 // return early if a dialog is opened 76 this.#deferredDialogOpened = Promise.withResolvers(); 77 let { 78 error, 79 isWebDriverError, 80 seenNodeIds, 81 serializedValue: serializedResult, 82 hasSerializedWindows, 83 } = await Promise.race([ 84 super.sendQuery(name, serializedValue), 85 this.#deferredDialogOpened.promise, 86 ]).finally(() => { 87 this.#deferredDialogOpened = null; 88 }); 89 90 if (error) { 91 if (isWebDriverError) { 92 // If it's a WebDriver error we need to deserialize it. 93 error = lazy.error.WebDriverError.fromJSON(error); 94 } 95 96 this.#handleError(error, seenNodes); 97 } 98 99 // Update seen nodes for serialized element and shadow root nodes. 100 seenNodeIds?.forEach(nodeId => seenNodes.add(nodeId)); 101 102 if (hasSerializedWindows) { 103 // The serialized data contains WebWindow references that need to be 104 // converted to unique identifiers. 105 serializedResult = lazy.json.mapToNavigableIds(serializedResult); 106 } 107 108 return serializedResult; 109 } 110 111 /** 112 * Handle an error and replace error type if necessary. 113 * 114 * @param {Error} error 115 * The error to handle. 116 * @param {Set<string>} seenNodes 117 * List of node ids already seen in this navigable. 118 * 119 * @throws {Error} 120 * The original or replaced error. 121 */ 122 #handleError(error, seenNodes) { 123 // If an element hasn't been found during deserialization check if it 124 // may be a stale reference. 125 if ( 126 error instanceof lazy.error.NoSuchElementError && 127 error.data.elementId !== undefined && 128 seenNodes.has(error.data.elementId) 129 ) { 130 throw new lazy.error.StaleElementReferenceError(error); 131 } 132 133 // If a shadow root hasn't been found during deserialization check if it 134 // may be a detached reference. 135 if ( 136 error instanceof lazy.error.NoSuchShadowRootError && 137 error.data.shadowId !== undefined && 138 seenNodes.has(error.data.shadowId) 139 ) { 140 throw new lazy.error.DetachedShadowRootError(error); 141 } 142 143 throw error; 144 } 145 146 notifyDialogOpened() { 147 if (this.#deferredDialogOpened) { 148 this.#deferredDialogOpened.resolve({ data: null }); 149 } 150 } 151 152 // Proxying methods for WebDriver commands 153 154 clearElement(webEl) { 155 return this.sendQuery("MarionetteCommandsParent:clearElement", { 156 elem: webEl, 157 }); 158 } 159 160 clickElement(webEl, capabilities) { 161 return this.sendQuery("MarionetteCommandsParent:clickElement", { 162 elem: webEl, 163 capabilities: capabilities.toJSON(), 164 }); 165 } 166 167 async executeScript(script, args, opts) { 168 return this.sendQuery("MarionetteCommandsParent:executeScript", { 169 script, 170 args: lazy.json.mapFromNavigableIds(args), 171 opts, 172 }); 173 } 174 175 findElement(strategy, selector, opts) { 176 return this.sendQuery("MarionetteCommandsParent:findElement", { 177 strategy, 178 selector, 179 opts, 180 }); 181 } 182 183 findElements(strategy, selector, opts) { 184 return this.sendQuery("MarionetteCommandsParent:findElements", { 185 strategy, 186 selector, 187 opts, 188 }); 189 } 190 191 generateTestReport(messageBody, messageGroup) { 192 return this.sendQuery("MarionetteCommandsParent:generateTestReport", { 193 message: messageBody, 194 group: messageGroup, 195 }); 196 } 197 198 async getShadowRoot(webEl) { 199 return this.sendQuery("MarionetteCommandsParent:getShadowRoot", { 200 elem: webEl, 201 }); 202 } 203 204 async getActiveElement() { 205 return this.sendQuery("MarionetteCommandsParent:getActiveElement"); 206 } 207 208 async getComputedLabel(webEl) { 209 return this.sendQuery("MarionetteCommandsParent:getComputedLabel", { 210 elem: webEl, 211 }); 212 } 213 214 async getComputedRole(webEl) { 215 return this.sendQuery("MarionetteCommandsParent:getComputedRole", { 216 elem: webEl, 217 }); 218 } 219 220 async getElementAttribute(webEl, name) { 221 return this.sendQuery("MarionetteCommandsParent:getElementAttribute", { 222 elem: webEl, 223 name, 224 }); 225 } 226 227 async getElementProperty(webEl, name) { 228 return this.sendQuery("MarionetteCommandsParent:getElementProperty", { 229 elem: webEl, 230 name, 231 }); 232 } 233 234 async getElementRect(webEl) { 235 return this.sendQuery("MarionetteCommandsParent:getElementRect", { 236 elem: webEl, 237 }); 238 } 239 240 async getElementTagName(webEl) { 241 return this.sendQuery("MarionetteCommandsParent:getElementTagName", { 242 elem: webEl, 243 }); 244 } 245 246 async getElementText(webEl) { 247 return this.sendQuery("MarionetteCommandsParent:getElementText", { 248 elem: webEl, 249 }); 250 } 251 252 async getElementValueOfCssProperty(webEl, name) { 253 return this.sendQuery( 254 "MarionetteCommandsParent:getElementValueOfCssProperty", 255 { 256 elem: webEl, 257 name, 258 } 259 ); 260 } 261 262 async getPageSource() { 263 return this.sendQuery("MarionetteCommandsParent:getPageSource"); 264 } 265 266 async isElementDisplayed(webEl, capabilities) { 267 return this.sendQuery("MarionetteCommandsParent:isElementDisplayed", { 268 capabilities: capabilities.toJSON(), 269 elem: webEl, 270 }); 271 } 272 273 async isElementEnabled(webEl, capabilities) { 274 return this.sendQuery("MarionetteCommandsParent:isElementEnabled", { 275 capabilities: capabilities.toJSON(), 276 elem: webEl, 277 }); 278 } 279 280 async isElementSelected(webEl, capabilities) { 281 return this.sendQuery("MarionetteCommandsParent:isElementSelected", { 282 capabilities: capabilities.toJSON(), 283 elem: webEl, 284 }); 285 } 286 287 async sendKeysToElement(webEl, text, capabilities) { 288 return this.sendQuery("MarionetteCommandsParent:sendKeysToElement", { 289 capabilities: capabilities.toJSON(), 290 elem: webEl, 291 text, 292 }); 293 } 294 295 async switchToFrame(id) { 296 const { browsingContextId } = await this.sendQuery( 297 "MarionetteCommandsParent:switchToFrame", 298 { id } 299 ); 300 301 return { 302 browsingContext: BrowsingContext.get(browsingContextId), 303 }; 304 } 305 306 async switchToParentFrame() { 307 const { browsingContextId } = await this.sendQuery( 308 "MarionetteCommandsParent:switchToParentFrame" 309 ); 310 311 return { 312 browsingContext: BrowsingContext.get(browsingContextId), 313 }; 314 } 315 316 async takeScreenshot(webEl, format, full, scroll) { 317 const rect = await this.sendQuery( 318 "MarionetteCommandsParent:getScreenshotRect", 319 { 320 elem: webEl, 321 full, 322 scroll, 323 } 324 ); 325 326 // If no element has been specified use the top-level browsing context. 327 // Otherwise use the browsing context from the currently selected frame. 328 const browsingContext = webEl 329 ? this.browsingContext 330 : this.browsingContext.top; 331 332 let canvas = await lazy.capture.canvas( 333 browsingContext.topChromeWindow, 334 browsingContext, 335 rect.x, 336 rect.y, 337 rect.width, 338 rect.height 339 ); 340 341 switch (format) { 342 case lazy.capture.Format.Hash: 343 return lazy.capture.toHash(canvas); 344 345 case lazy.capture.Format.Base64: 346 return lazy.capture.toBase64(canvas, "image/png"); 347 348 default: 349 throw new TypeError(`Invalid capture format: ${format}`); 350 } 351 } 352 } 353 354 /** 355 * Proxy that will dynamically create MarionetteCommands actors for a dynamically 356 * provided browsing context until the method can be fully executed by the 357 * JSWindowActor pair. 358 * 359 * @param {function(): BrowsingContext} browsingContextFn 360 * A function that returns the reference to the browsing context for which 361 * the query should run. 362 */ 363 export function getMarionetteCommandsActorProxy(browsingContextFn) { 364 const MAX_ATTEMPTS = 10; 365 366 /** 367 * Methods which modify the content page cannot be retried safely. 368 * See Bug 1673345. 369 */ 370 const NO_RETRY_METHODS = [ 371 "clickElement", 372 "executeScript", 373 "sendKeysToElement", 374 ]; 375 376 return new Proxy( 377 {}, 378 { 379 get(target, methodName) { 380 return async (...args) => { 381 let attempts = 0; 382 // eslint-disable-next-line no-constant-condition 383 while (true) { 384 let browsingContext = browsingContextFn(); 385 386 // If a top-level browsing context was replaced and retrying is allowed, 387 // retrieve the new one for the current browser. 388 if ( 389 browsingContext?.isReplaced && 390 browsingContext.top === browsingContext && 391 !NO_RETRY_METHODS.includes(methodName) 392 ) { 393 browsingContext = BrowsingContext.getCurrentTopByBrowserId( 394 browsingContext.browserId 395 ); 396 } 397 398 if (!browsingContext || browsingContext.isDiscarded) { 399 throw new lazy.error.NoSuchWindowError( 400 `BrowsingContext does no longer exist` 401 ); 402 } 403 404 try { 405 const actor = 406 browsingContext.currentWindowGlobal.getActor( 407 "MarionetteCommands" 408 ); 409 410 const result = await actor[methodName](...args); 411 return result; 412 } catch (e) { 413 if (!["AbortError", "InactiveActor"].includes(e.name)) { 414 // Only retry when the JSWindowActor pair gets destroyed, or 415 // gets inactive eg. when the page is moved into bfcache. 416 throw e; 417 } 418 419 if (NO_RETRY_METHODS.includes(methodName)) { 420 lazy.logger.trace( 421 `[${browsingContext.id}] Querying "${methodName}"` + 422 ` failed with ${e.name}, returning "null" as fallback` 423 ); 424 return null; 425 } 426 427 if (++attempts > MAX_ATTEMPTS) { 428 lazy.logger.trace( 429 `[${browsingContext.id}] Querying "${methodName}"` + 430 ` reached the limit of retry attempts (${MAX_ATTEMPTS})` 431 ); 432 throw e; 433 } 434 435 lazy.logger.trace( 436 `[${browsingContext.id}] Retrying "${methodName}"` + 437 `, attempt: ${attempts}` 438 ); 439 await new Promise(resolve => 440 Services.tm.dispatchToMainThread(resolve) 441 ); 442 } 443 } 444 }; 445 }, 446 } 447 ); 448 } 449 450 /** 451 * Register the MarionetteCommands actor that holds all the commands. 452 * 453 * @param {string} sessionId 454 * The id of the current WebDriver session. 455 */ 456 export function registerCommandsActor(sessionId) { 457 try { 458 ChromeUtils.registerWindowActor("MarionetteCommands", { 459 kind: "JSWindowActor", 460 parent: { 461 esModuleURI: 462 "chrome://remote/content/marionette/actors/MarionetteCommandsParent.sys.mjs", 463 }, 464 child: { 465 esModuleURI: 466 "chrome://remote/content/marionette/actors/MarionetteCommandsChild.sys.mjs", 467 }, 468 469 allFrames: true, 470 includeChrome: true, 471 }); 472 } catch (e) { 473 if (e.name === "NotSupportedError") { 474 lazy.logger.warn(`MarionetteCommands actor is already registered!`); 475 } else { 476 throw e; 477 } 478 } 479 480 webDriverSessionId = sessionId; 481 } 482 483 export function unregisterCommandsActor() { 484 webDriverSessionId = null; 485 486 ChromeUtils.unregisterWindowActor("MarionetteCommands"); 487 }