MarionetteCommandsChild.sys.mjs (20260B)
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 LayoutUtils: "resource://gre/modules/LayoutUtils.sys.mjs", 9 10 accessibility: 11 "chrome://remote/content/shared/webdriver/Accessibility.sys.mjs", 12 AnimationFramePromise: "chrome://remote/content/shared/Sync.sys.mjs", 13 assertTargetInViewPort: 14 "chrome://remote/content/shared/webdriver/Actions.sys.mjs", 15 atom: "chrome://remote/content/marionette/atom.sys.mjs", 16 dom: "chrome://remote/content/shared/DOM.sys.mjs", 17 error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs", 18 evaluate: "chrome://remote/content/marionette/evaluate.sys.mjs", 19 event: "chrome://remote/content/shared/webdriver/Event.sys.mjs", 20 executeSoon: "chrome://remote/content/shared/Sync.sys.mjs", 21 interaction: "chrome://remote/content/marionette/interaction.sys.mjs", 22 json: "chrome://remote/content/marionette/json.sys.mjs", 23 Log: "chrome://remote/content/shared/Log.sys.mjs", 24 sandbox: "chrome://remote/content/marionette/evaluate.sys.mjs", 25 Sandboxes: "chrome://remote/content/marionette/evaluate.sys.mjs", 26 }); 27 28 ChromeUtils.defineLazyGetter(lazy, "logger", () => 29 lazy.Log.get(lazy.Log.TYPES.MARIONETTE) 30 ); 31 32 export class MarionetteCommandsChild extends JSWindowActorChild { 33 #processActor; 34 35 constructor() { 36 super(); 37 38 this.#processActor = ChromeUtils.domProcessChild.getActor( 39 "WebDriverProcessData" 40 ); 41 42 // sandbox storage and name of the current sandbox 43 this.sandboxes = new lazy.Sandboxes(() => this.document.defaultView); 44 } 45 46 get innerWindowId() { 47 return this.manager.innerWindowId; 48 } 49 50 actorCreated() { 51 lazy.logger.trace( 52 `[${this.browsingContext.id}] MarionetteCommands actor created ` + 53 `for window id ${this.innerWindowId}` 54 ); 55 } 56 57 didDestroy() { 58 lazy.logger.trace( 59 `[${this.browsingContext.id}] MarionetteCommands actor destroyed ` + 60 `for window id ${this.innerWindowId}` 61 ); 62 } 63 64 #assertInViewPort(options) { 65 const { target } = options; 66 67 return lazy.assertTargetInViewPort(target, this.contentWindow); 68 } 69 70 async #dispatchEvent(options) { 71 const { eventName, details } = options; 72 const win = this.contentWindow; 73 74 const windowUtils = win.windowUtils; 75 const microTaskLevel = windowUtils.microTaskLevel; 76 // Since we're being called as a webidl callback, 77 // CallbackObjectBase::CallSetup::CallSetup has increased the microtask 78 // level. Undo that temporarily so that microtask handling works closer 79 // the way it would work when dispatching events natively. 80 windowUtils.microTaskLevel = 0; 81 try { 82 switch (eventName) { 83 case "synthesizeKeyDown": 84 lazy.event.sendKeyDown(details.eventData, win); 85 break; 86 case "synthesizeKeyUp": 87 lazy.event.sendKeyUp(details.eventData, win); 88 break; 89 case "synthesizeMouseAtPoint": 90 await lazy.event.synthesizeMouseAtPoint( 91 details.x, 92 details.y, 93 details.eventData, 94 win 95 ); 96 break; 97 case "synthesizeMultiTouch": 98 lazy.event.synthesizeMultiTouch(details.eventData, win); 99 break; 100 case "synthesizeWheelAtPoint": 101 await lazy.event.synthesizeWheelAtPoint( 102 details.x, 103 details.y, 104 details.eventData, 105 win 106 ); 107 break; 108 default: 109 throw new Error( 110 `${eventName} is not a supported event dispatch method` 111 ); 112 } 113 } catch (e) { 114 if (e.message.includes("NS_ERROR_FAILURE")) { 115 // Event dispatch failed. Re-throwing as AbortError to allow retrying 116 // to dispatch the event. 117 throw new DOMException( 118 `Failed to dispatch event "${eventName}": ${e.message}`, 119 "AbortError" 120 ); 121 } 122 123 throw e; 124 } finally { 125 windowUtils.microTaskLevel = microTaskLevel; 126 } 127 } 128 129 async #finalizeAction() { 130 // Terminate the current wheel transaction if there is one. Wheel 131 // transactions should not live longer than a single action chain. 132 await ChromeUtils.endWheelTransaction(this.contentWindow); 133 134 // Wait for the next animation frame to make sure the page's content 135 // was updated. 136 await lazy.AnimationFramePromise(this.contentWindow); 137 } 138 139 #getClientRects(options, _context) { 140 const { elem } = options; 141 142 return elem.getClientRects(); 143 } 144 145 #getInViewCentrePoint(options) { 146 const { rect } = options; 147 148 return lazy.dom.getInViewCentrePoint(rect, this.contentWindow); 149 } 150 151 #toBrowserWindowCoordinates(options, _context) { 152 const { position } = options; 153 154 const [x, y] = position; 155 const dpr = this.contentWindow.devicePixelRatio; 156 157 const val = lazy.LayoutUtils.rectToTopLevelWidgetRect(this.contentWindow, { 158 left: x, 159 top: y, 160 height: 0, 161 width: 0, 162 }); 163 164 return [val.x / dpr, val.y / dpr]; 165 } 166 167 // eslint-disable-next-line complexity 168 async receiveMessage(msg) { 169 if (!this.contentWindow) { 170 throw new DOMException("Actor is no longer active", "InactiveActor"); 171 } 172 173 try { 174 let result; 175 let waitForNextTick = false; 176 177 const { name, data: serializedData } = msg; 178 179 const data = lazy.json.deserialize( 180 serializedData, 181 this.#processActor.getNodeCache(), 182 this.contentWindow.browsingContext 183 ); 184 185 switch (name) { 186 case "MarionetteCommandsParent:_assertInViewPort": 187 result = this.#assertInViewPort(data); 188 break; 189 case "MarionetteCommandsParent:_dispatchEvent": 190 await this.#dispatchEvent(data); 191 waitForNextTick = true; 192 break; 193 case "MarionetteCommandsParent:_getClientRects": 194 result = this.#getClientRects(data); 195 break; 196 case "MarionetteCommandsParent:_getInViewCentrePoint": 197 result = this.#getInViewCentrePoint(data); 198 break; 199 case "MarionetteCommandsParent:_finalizeAction": 200 this.#finalizeAction(); 201 break; 202 case "MarionetteCommandsParent:_toBrowserWindowCoordinates": 203 result = this.#toBrowserWindowCoordinates(data); 204 break; 205 case "MarionetteCommandsParent:clearElement": 206 this.clearElement(data); 207 waitForNextTick = true; 208 break; 209 case "MarionetteCommandsParent:clickElement": 210 result = await this.clickElement(data); 211 waitForNextTick = true; 212 break; 213 case "MarionetteCommandsParent:executeScript": 214 result = await this.executeScript(data); 215 waitForNextTick = true; 216 break; 217 case "MarionetteCommandsParent:findElement": 218 result = await this.findElement(data); 219 break; 220 case "MarionetteCommandsParent:findElements": 221 result = await this.findElements(data); 222 break; 223 case "MarionetteCommandsParent:generateTestReport": 224 result = await this.generateTestReport(data); 225 break; 226 case "MarionetteCommandsParent:getActiveElement": 227 result = await this.getActiveElement(); 228 break; 229 case "MarionetteCommandsParent:getComputedLabel": 230 result = await this.getComputedLabel(data); 231 break; 232 case "MarionetteCommandsParent:getComputedRole": 233 result = await this.getComputedRole(data); 234 break; 235 case "MarionetteCommandsParent:getElementAttribute": 236 result = await this.getElementAttribute(data); 237 break; 238 case "MarionetteCommandsParent:getElementProperty": 239 result = await this.getElementProperty(data); 240 break; 241 case "MarionetteCommandsParent:getElementRect": 242 result = await this.getElementRect(data); 243 break; 244 case "MarionetteCommandsParent:getElementTagName": 245 result = await this.getElementTagName(data); 246 break; 247 case "MarionetteCommandsParent:getElementText": 248 result = await this.getElementText(data); 249 break; 250 case "MarionetteCommandsParent:getElementValueOfCssProperty": 251 result = await this.getElementValueOfCssProperty(data); 252 break; 253 case "MarionetteCommandsParent:getPageSource": 254 result = await this.getPageSource(); 255 break; 256 case "MarionetteCommandsParent:getScreenshotRect": 257 result = await this.getScreenshotRect(data); 258 break; 259 case "MarionetteCommandsParent:getShadowRoot": 260 result = await this.getShadowRoot(data); 261 break; 262 case "MarionetteCommandsParent:isElementDisplayed": 263 result = await this.isElementDisplayed(data); 264 break; 265 case "MarionetteCommandsParent:isElementEnabled": 266 result = await this.isElementEnabled(data); 267 break; 268 case "MarionetteCommandsParent:isElementSelected": 269 result = await this.isElementSelected(data); 270 break; 271 case "MarionetteCommandsParent:sendKeysToElement": 272 result = await this.sendKeysToElement(data); 273 waitForNextTick = true; 274 break; 275 case "MarionetteCommandsParent:switchToFrame": 276 result = await this.switchToFrame(data); 277 waitForNextTick = true; 278 break; 279 case "MarionetteCommandsParent:switchToParentFrame": 280 result = await this.switchToParentFrame(); 281 waitForNextTick = true; 282 break; 283 } 284 285 // Inform the content process that the command has completed. It allows 286 // it to process async follow-up tasks before the reply is sent. 287 if (waitForNextTick) { 288 await new Promise(resolve => lazy.executeSoon(resolve)); 289 } 290 291 const { seenNodeIds, serializedValue, hasSerializedWindows } = 292 lazy.json.clone(result, this.#processActor.getNodeCache()); 293 294 // Because in WebDriver classic nodes can only be returned from the same 295 // browsing context, we only need the seen unique ids as flat array. 296 return { 297 seenNodeIds: [...seenNodeIds.values()].flat(), 298 serializedValue, 299 hasSerializedWindows, 300 }; 301 } catch (e) { 302 if (lazy.error.isWebDriverError(e)) { 303 // If it's a WebDriver error always serialize it because it could 304 // contain objects that are not serializable by default. 305 return { error: e.toJSON(), isWebDriverError: true }; 306 } 307 return { error: e, isWebDriverError: false }; 308 } 309 } 310 311 // Implementation of WebDriver commands 312 313 /** 314 * Clear the text of an element. 315 * 316 * @param {object} options 317 * @param {Element} options.elem 318 */ 319 clearElement(options = {}) { 320 const { elem } = options; 321 322 lazy.interaction.clearElement(elem); 323 } 324 325 /** 326 * Click an element. 327 */ 328 async clickElement(options = {}) { 329 const { capabilities, elem } = options; 330 331 return lazy.interaction.clickElement( 332 elem, 333 capabilities["moz:accessibilityChecks"], 334 capabilities["moz:webdriverClick"] 335 ); 336 } 337 338 /** 339 * Executes a JavaScript function. 340 */ 341 async executeScript(options = {}) { 342 const { args, opts = {}, script } = options; 343 344 let sb; 345 if (opts.sandboxName) { 346 sb = this.sandboxes.get(opts.sandboxName, opts.newSandbox); 347 } else { 348 sb = lazy.sandbox.createMutable(this.document.defaultView); 349 } 350 351 return lazy.evaluate.sandbox(sb, script, args, opts); 352 } 353 354 /** 355 * Find an element in the current browsing context's document using the 356 * given search strategy. 357 * 358 * @param {object=} options 359 * @param {string} options.strategy 360 * @param {string} options.selector 361 * @param {object} options.opts 362 * @param {Element} options.opts.startNode 363 */ 364 async findElement(options = {}) { 365 const { strategy, selector, opts } = options; 366 367 opts.all = false; 368 369 const container = { frame: this.document.defaultView }; 370 return lazy.dom.find(container, strategy, selector, opts); 371 } 372 373 /** 374 * Find elements in the current browsing context's document using the 375 * given search strategy. 376 * 377 * @param {object=} options 378 * @param {string} options.strategy 379 * @param {string} options.selector 380 * @param {object} options.opts 381 * @param {Element} options.opts.startNode 382 */ 383 async findElements(options = {}) { 384 const { strategy, selector, opts } = options; 385 386 opts.all = true; 387 388 const container = { frame: this.document.defaultView }; 389 return lazy.dom.find(container, strategy, selector, opts); 390 } 391 392 /** 393 * Generates and sends a test report to be observed by any registered reporting observers 394 */ 395 async generateTestReport(options = {}) { 396 const { message, group } = options; 397 return this.browsingContext.window.TestReportGenerator.generateReport({ 398 message, 399 group, 400 }); 401 } 402 403 /** 404 * Return the active element in the document. 405 */ 406 async getActiveElement() { 407 let elem = this.document.activeElement; 408 if (!elem) { 409 throw new lazy.error.NoSuchElementError(); 410 } 411 412 return elem; 413 } 414 415 /** 416 * Return the accessible label for a given element. 417 */ 418 async getComputedLabel(options = {}) { 419 const { elem } = options; 420 421 return lazy.accessibility.getAccessibleName(elem); 422 } 423 424 /** 425 * Return the accessible role for a given element. 426 */ 427 async getComputedRole(options = {}) { 428 const { elem } = options; 429 430 return lazy.accessibility.getComputedRole(elem); 431 } 432 433 /** 434 * Get the value of an attribute for the given element. 435 */ 436 async getElementAttribute(options = {}) { 437 const { name, elem } = options; 438 439 if (lazy.dom.isBooleanAttribute(elem, name)) { 440 if (elem.hasAttribute(name)) { 441 return "true"; 442 } 443 return null; 444 } 445 return elem.getAttribute(name); 446 } 447 448 /** 449 * Get the value of a property for the given element. 450 */ 451 async getElementProperty(options = {}) { 452 const { name, elem } = options; 453 454 // Waive Xrays to get unfiltered access to the untrusted element. 455 const el = Cu.waiveXrays(elem); 456 return typeof el[name] != "undefined" ? el[name] : null; 457 } 458 459 /** 460 * Get the position and dimensions of the element. 461 */ 462 async getElementRect(options = {}) { 463 const { elem } = options; 464 465 const rect = elem.getBoundingClientRect(); 466 return { 467 x: rect.x + this.document.defaultView.pageXOffset, 468 y: rect.y + this.document.defaultView.pageYOffset, 469 width: rect.width, 470 height: rect.height, 471 }; 472 } 473 474 /** 475 * Get the tagName for the given element. 476 */ 477 async getElementTagName(options = {}) { 478 const { elem } = options; 479 480 return elem.tagName.toLowerCase(); 481 } 482 483 /** 484 * Get the text content for the given element. 485 */ 486 async getElementText(options = {}) { 487 const { elem } = options; 488 489 try { 490 return await lazy.atom.getVisibleText(elem, this.document.defaultView); 491 } catch (e) { 492 lazy.logger.warn(`Atom getVisibleText failed: "${e.message}"`); 493 494 // Fallback in case the atom implementation is broken. 495 // As known so far this only happens for XML documents (bug 1794099). 496 return elem.textContent; 497 } 498 } 499 500 /** 501 * Get the value of a css property for the given element. 502 */ 503 async getElementValueOfCssProperty(options = {}) { 504 const { name, elem } = options; 505 506 const style = this.document.defaultView.getComputedStyle(elem); 507 return style.getPropertyValue(name); 508 } 509 510 /** 511 * Get the source of the current browsing context's document. 512 */ 513 async getPageSource() { 514 return this.document.documentElement.outerHTML; 515 } 516 517 /** 518 * Returns the rect of the element to screenshot. 519 * 520 * Because the screen capture takes place in the parent process the dimensions 521 * for the screenshot have to be determined in the appropriate child process. 522 * 523 * Also it takes care of scrolling an element into view if requested. 524 * 525 * @param {object} options 526 * @param {Element} options.elem 527 * Optional element to take a screenshot of. 528 * @param {boolean=} options.full 529 * True to take a screenshot of the entire document element. 530 * Defaults to true. 531 * @param {boolean=} options.scroll 532 * When <var>elem</var> is given, scroll it into view. 533 * Defaults to true. 534 * 535 * @returns {DOMRect} 536 * The area to take a snapshot from. 537 */ 538 async getScreenshotRect(options = {}) { 539 const { elem, full = true, scroll = true } = options; 540 const win = elem 541 ? this.document.defaultView 542 : this.browsingContext.top.window; 543 544 let rect; 545 546 if (elem) { 547 if (scroll) { 548 lazy.dom.scrollIntoView(elem); 549 } 550 rect = this.getElementRect({ elem }); 551 } else if (full) { 552 const docEl = win.document.documentElement; 553 rect = new DOMRect(0, 0, docEl.scrollWidth, docEl.scrollHeight); 554 } else { 555 // viewport 556 rect = new DOMRect( 557 win.pageXOffset, 558 win.pageYOffset, 559 win.innerWidth, 560 win.innerHeight 561 ); 562 } 563 564 return rect; 565 } 566 567 /** 568 * Return the shadowRoot attached to an element 569 */ 570 async getShadowRoot(options = {}) { 571 const { elem } = options; 572 573 return lazy.dom.getShadowRoot(elem); 574 } 575 576 /** 577 * Determine the element displayedness of the given web element. 578 */ 579 async isElementDisplayed(options = {}) { 580 const { capabilities, elem } = options; 581 582 return lazy.interaction.isElementDisplayed( 583 elem, 584 capabilities["moz:accessibilityChecks"] 585 ); 586 } 587 588 /** 589 * Check if element is enabled. 590 */ 591 async isElementEnabled(options = {}) { 592 const { capabilities, elem } = options; 593 594 return lazy.interaction.isElementEnabled( 595 elem, 596 capabilities["moz:accessibilityChecks"] 597 ); 598 } 599 600 /** 601 * Determine whether the referenced element is selected or not. 602 */ 603 async isElementSelected(options = {}) { 604 const { capabilities, elem } = options; 605 606 return lazy.interaction.isElementSelected( 607 elem, 608 capabilities["moz:accessibilityChecks"] 609 ); 610 } 611 612 /* 613 * Send key presses to element after focusing on it. 614 */ 615 async sendKeysToElement(options = {}) { 616 const { capabilities, elem, text } = options; 617 618 const opts = { 619 strictFileInteractability: capabilities.strictFileInteractability, 620 accessibilityChecks: capabilities["moz:accessibilityChecks"], 621 webdriverClick: capabilities["moz:webdriverClick"], 622 }; 623 624 return lazy.interaction.sendKeysToElement(elem, text, opts); 625 } 626 627 /** 628 * Switch to the specified frame. 629 * 630 * @param {object=} options 631 * @param {(number|Element)=} options.id 632 * If it's a number treat it as the index for all the existing frames. 633 * If it's an Element switch to this specific frame. 634 * If not specified or `null` switch to the top-level browsing context. 635 */ 636 async switchToFrame(options = {}) { 637 const { id } = options; 638 639 const childContexts = this.browsingContext.children; 640 let browsingContext; 641 642 if (id == null) { 643 browsingContext = this.browsingContext.top; 644 } else if (typeof id == "number") { 645 if (id < 0 || id >= childContexts.length) { 646 throw new lazy.error.NoSuchFrameError( 647 `Unable to locate frame with index: ${id}` 648 ); 649 } 650 browsingContext = childContexts[id]; 651 } else { 652 const context = childContexts.find(childContext => { 653 return childContext.embedderElement === id; 654 }); 655 if (!context) { 656 throw new lazy.error.NoSuchFrameError( 657 `Unable to locate frame for element: ${id}` 658 ); 659 } 660 browsingContext = context; 661 } 662 663 // For in-process iframes the window global is lazy-loaded for optimization 664 // reasons. As such force the currentWindowGlobal to be created so we always 665 // have a window (bug 1691348). 666 browsingContext.window; 667 668 return { browsingContextId: browsingContext.id }; 669 } 670 671 /** 672 * Switch to the parent frame. 673 */ 674 async switchToParentFrame() { 675 const browsingContext = this.browsingContext.parent || this.browsingContext; 676 677 return { browsingContextId: browsingContext.id }; 678 } 679 }