interaction.sys.mjs (22640B)
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 file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 const lazy = {}; 6 7 ChromeUtils.defineESModuleGetters(lazy, { 8 setTimeout: "resource://gre/modules/Timer.sys.mjs", 9 10 accessibility: 11 "chrome://remote/content/shared/webdriver/Accessibility.sys.mjs", 12 atom: "chrome://remote/content/marionette/atom.sys.mjs", 13 dom: "chrome://remote/content/shared/DOM.sys.mjs", 14 error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs", 15 event: "chrome://remote/content/shared/webdriver/Event.sys.mjs", 16 Log: "chrome://remote/content/shared/Log.sys.mjs", 17 pprint: "chrome://remote/content/shared/Format.sys.mjs", 18 TimedPromise: "chrome://remote/content/shared/Sync.sys.mjs", 19 }); 20 21 ChromeUtils.defineLazyGetter(lazy, "logger", () => 22 lazy.Log.get(lazy.Log.TYPES.MARIONETTE) 23 ); 24 25 // dragService may be null if it's in the headless mode (e.g., on Linux). 26 // It depends on the platform, though. 27 ChromeUtils.defineLazyGetter(lazy, "dragService", () => { 28 try { 29 return Cc["@mozilla.org/widget/dragservice;1"].getService( 30 Ci.nsIDragService 31 ); 32 } catch (e) { 33 // If we're in the headless mode, the drag service may be never 34 // instantiated. In this case, an exception is thrown. Let's ignore 35 // any exceptions since without the drag service, nobody can create a 36 // drag session. 37 return null; 38 } 39 }); 40 41 /** XUL elements that support disabled attribute. */ 42 const DISABLED_ATTRIBUTE_SUPPORTED_XUL = new Set([ 43 "ARROWSCROLLBOX", 44 "BUTTON", 45 "CHECKBOX", 46 "COMMAND", 47 "DESCRIPTION", 48 "KEY", 49 "KEYSET", 50 "LABEL", 51 "MENU", 52 "MENUITEM", 53 "MENULIST", 54 "MENUSEPARATOR", 55 "RADIO", 56 "RADIOGROUP", 57 "RICHLISTBOX", 58 "RICHLISTITEM", 59 "TAB", 60 "TABS", 61 "TOOLBARBUTTON", 62 "TREE", 63 ]); 64 65 /** 66 * Common form controls that user can change the value property 67 * interactively. 68 */ 69 const COMMON_FORM_CONTROLS = new Set(["input", "textarea", "select"]); 70 71 /** 72 * Input elements that do not fire <tt>input</tt> and <tt>change</tt> 73 * events when value property changes. 74 */ 75 const INPUT_TYPES_NO_EVENT = new Set([ 76 "checkbox", 77 "radio", 78 "file", 79 "hidden", 80 "image", 81 "reset", 82 "button", 83 "submit", 84 ]); 85 86 /** @namespace */ 87 export const interaction = {}; 88 89 /** 90 * Interact with an element by clicking it. 91 * 92 * The element is scrolled into view before visibility- or interactability 93 * checks are performed. 94 * 95 * Selenium-style visibility checks will be performed 96 * if <var>specCompat</var> is false (default). Otherwise 97 * pointer-interactability checks will be performed. If either of these 98 * fail an {@link ElementNotInteractableError} is thrown. 99 * 100 * If <var>strict</var> is enabled (defaults to disabled), further 101 * accessibility checks will be performed, and these may result in an 102 * {@link ElementNotAccessibleError} being returned. 103 * 104 * When <var>el</var> is not enabled, an {@link InvalidElementStateError} 105 * is returned. 106 * 107 * @param {(DOMElement|XULElement)} el 108 * Element to click. 109 * @param {boolean=} [strict=false] strict 110 * Enforce strict accessibility tests. 111 * @param {boolean=} [specCompat=false] specCompat 112 * Use WebDriver specification compatible interactability definition. 113 * 114 * @throws {ElementNotInteractableError} 115 * If either Selenium-style visibility check or 116 * pointer-interactability check fails. 117 * @throws {ElementClickInterceptedError} 118 * If <var>el</var> is obscured by another element and a click would 119 * not hit, in <var>specCompat</var> mode. 120 * @throws {ElementNotAccessibleError} 121 * If <var>strict</var> is true and element is not accessible. 122 * @throws {InvalidElementStateError} 123 * If <var>el</var> is not enabled. 124 */ 125 interaction.clickElement = async function ( 126 el, 127 strict = false, 128 specCompat = false 129 ) { 130 const a11y = lazy.accessibility.get(strict); 131 if (lazy.dom.isXULElement(el)) { 132 await chromeClick(el, a11y); 133 } else if (specCompat) { 134 await webdriverClickElement(el, a11y); 135 } else { 136 lazy.logger.trace(`Using non spec-compatible element click`); 137 await seleniumClickElement(el, a11y); 138 } 139 }; 140 141 async function webdriverClickElement(el, a11y) { 142 const win = getWindow(el); 143 144 // step 3 145 if (el.localName == "input" && el.type == "file") { 146 throw new lazy.error.InvalidArgumentError( 147 "Cannot click <input type=file> elements" 148 ); 149 } 150 151 let containerEl = lazy.dom.getContainer(el); 152 153 // step 4 154 if (!lazy.dom.isInView(containerEl)) { 155 lazy.dom.scrollIntoView(containerEl); 156 } 157 158 // step 5 159 // TODO(ato): wait for containerEl to be in view 160 161 // step 6 162 // if we cannot bring the container element into the viewport 163 // there is no point in checking if it is pointer-interactable 164 if (!lazy.dom.isInView(containerEl)) { 165 throw new lazy.error.ElementNotInteractableError( 166 lazy.pprint`Element ${el} could not be scrolled into view` 167 ); 168 } 169 170 // step 7 171 let rects = containerEl.getClientRects(); 172 let clickPoint = lazy.dom.getInViewCentrePoint(rects[0], win); 173 174 if (lazy.dom.isObscured(containerEl)) { 175 throw new lazy.error.ElementClickInterceptedError( 176 null, 177 {}, 178 containerEl, 179 clickPoint 180 ); 181 } 182 183 let acc = await a11y.assertAccessible(el, true); 184 a11y.assertVisible(acc, el, true); 185 a11y.assertEnabled(acc, el, true); 186 a11y.assertActionable(acc, el); 187 188 // step 8 189 if (el.localName == "option") { 190 interaction.selectOption(el); 191 } else { 192 // Synthesize a pointerMove action. 193 await lazy.event.synthesizeMouseAtPoint( 194 clickPoint.x, 195 clickPoint.y, 196 { 197 type: "mousemove", 198 allowToHandleDragDrop: true, 199 }, 200 win 201 ); 202 203 if (lazy.dragService?.getCurrentSession(win)) { 204 // Special handling is required if the mousemove started a drag session. 205 // In this case, mousedown event shouldn't be fired, and the mouseup should 206 // end the session. Therefore, we should synthesize only mouseup. 207 await lazy.event.synthesizeMouseAtPoint( 208 clickPoint.x, 209 clickPoint.y, 210 { 211 type: "mouseup", 212 allowToHandleDragDrop: true, 213 }, 214 win 215 ); 216 } else { 217 // step 9 218 let clicked = interaction.flushEventLoop(containerEl); 219 220 // Synthesize a pointerDown + pointerUp action. 221 await lazy.event.synthesizeMouseAtPoint( 222 clickPoint.x, 223 clickPoint.y, 224 { allowToHandleDragDrop: true }, 225 win 226 ); 227 228 await clicked; 229 } 230 } 231 232 // step 10 233 // if the click causes navigation, the post-navigation checks are 234 // handled by navigate.js 235 } 236 237 async function chromeClick(el, a11y) { 238 if (!(await lazy.dom.isEnabled(el))) { 239 throw new lazy.error.InvalidElementStateError("Element is not enabled"); 240 } 241 242 let acc = await a11y.assertAccessible(el, true); 243 a11y.assertVisible(acc, el, true); 244 a11y.assertEnabled(acc, el, true); 245 a11y.assertActionable(acc, el); 246 247 if (el.localName == "option") { 248 interaction.selectOption(el); 249 } else { 250 el.click(); 251 } 252 } 253 254 async function seleniumClickElement(el, a11y) { 255 let win = getWindow(el); 256 257 let visibilityCheckEl = el; 258 if (el.localName == "option") { 259 visibilityCheckEl = lazy.dom.getContainer(el); 260 } 261 262 if (!(await lazy.dom.isVisible(visibilityCheckEl))) { 263 throw new lazy.error.ElementNotInteractableError(); 264 } 265 266 if (!(await lazy.dom.isEnabled(el))) { 267 throw new lazy.error.InvalidElementStateError("Element is not enabled"); 268 } 269 270 let acc = await a11y.assertAccessible(el, true); 271 a11y.assertVisible(acc, el, true); 272 a11y.assertEnabled(acc, el, true); 273 a11y.assertActionable(acc, el); 274 275 if (el.localName == "option") { 276 interaction.selectOption(el); 277 } else { 278 let rects = el.getClientRects(); 279 let centre = lazy.dom.getInViewCentrePoint(rects[0], win); 280 let opts = {}; 281 await lazy.event.synthesizeMouseAtPoint(centre.x, centre.y, opts, win); 282 } 283 } 284 285 /** 286 * Select <tt><option></tt> element in a <tt><select></tt> 287 * list. 288 * 289 * Because the dropdown list of select elements are implemented using 290 * native widget technology, our trusted synthesised events are not able 291 * to reach them. Dropdowns are instead handled mimicking DOM events, 292 * which for obvious reasons is not ideal, but at the current point in 293 * time considered to be good enough. 294 * 295 * @param {HTMLOptionElement} el 296 * Option element to select. 297 * 298 * @throws {TypeError} 299 * If <var>el</var> is a XUL element or not an <tt><option></tt> 300 * element. 301 * @throws {Error} 302 * If unable to find <var>el</var>'s parent <tt><select></tt> 303 * element. 304 */ 305 interaction.selectOption = function (el) { 306 if (lazy.dom.isXULElement(el)) { 307 throw new TypeError("XUL dropdowns not supported"); 308 } 309 if (el.localName != "option") { 310 throw new TypeError(lazy.pprint`Expected <option> element, got ${el}`); 311 } 312 313 let containerEl = lazy.dom.getContainer(el); 314 315 lazy.event.mouseover(containerEl); 316 lazy.event.mousemove(containerEl); 317 lazy.event.mousedown(containerEl); 318 containerEl.focus(); 319 320 if (!el.disabled) { 321 // Clicking <option> in <select> should not be deselected if selected. 322 // However, clicking one in a <select multiple> should toggle 323 // selectedness the way holding down Control works. 324 if (containerEl.multiple) { 325 el.selected = !el.selected; 326 } else if (!el.selected) { 327 el.selected = true; 328 } 329 lazy.event.input(containerEl); 330 lazy.event.change(containerEl); 331 } 332 333 lazy.event.mouseup(containerEl); 334 lazy.event.click(containerEl); 335 containerEl.blur(); 336 }; 337 338 /** 339 * Clears the form control or the editable element, if required. 340 * 341 * Before clearing the element, it will attempt to scroll it into 342 * view if it is not already in the viewport. An error is raised 343 * if the element cannot be brought into view. 344 * 345 * If the element is a submittable form control and it is empty 346 * (it has no value or it has no files associated with it, in the 347 * case it is a <code><input type=file></code> element) or 348 * it is an editing host and its <code>innerHTML</code> content IDL 349 * attribute is empty, this function acts as a no-op. 350 * 351 * @param {Element} el 352 * Element to clear. 353 * 354 * @throws {InvalidElementStateError} 355 * If element is disabled, read-only, non-editable, not a submittable 356 * element or not an editing host, or cannot be scrolled into view. 357 */ 358 interaction.clearElement = function (el) { 359 if (lazy.dom.isDisabled(el)) { 360 throw new lazy.error.InvalidElementStateError( 361 lazy.pprint`Element is disabled: ${el}` 362 ); 363 } 364 if (lazy.dom.isReadOnly(el)) { 365 throw new lazy.error.InvalidElementStateError( 366 lazy.pprint`Element is read-only: ${el}` 367 ); 368 } 369 if (!lazy.dom.isEditable(el)) { 370 throw new lazy.error.InvalidElementStateError( 371 lazy.pprint`Unable to clear element that cannot be edited: ${el}` 372 ); 373 } 374 375 if (!lazy.dom.isInView(el)) { 376 lazy.dom.scrollIntoView(el); 377 } 378 if (!lazy.dom.isInView(el)) { 379 throw new lazy.error.ElementNotInteractableError( 380 lazy.pprint`Element ${el} could not be scrolled into view` 381 ); 382 } 383 384 if (lazy.dom.isEditingHost(el)) { 385 clearContentEditableElement(el); 386 } else { 387 clearResettableElement(el); 388 } 389 }; 390 391 function clearContentEditableElement(el) { 392 if (el.innerHTML === "") { 393 return; 394 } 395 el.focus(); 396 el.innerHTML = ""; 397 el.blur(); 398 } 399 400 function clearResettableElement(el) { 401 if (!lazy.dom.isMutableFormControl(el)) { 402 throw new lazy.error.InvalidElementStateError( 403 lazy.pprint`Not an editable form control: ${el}` 404 ); 405 } 406 407 let isEmpty; 408 switch (el.type) { 409 case "file": 410 isEmpty = !el.files.length; 411 break; 412 413 default: 414 isEmpty = el.value === ""; 415 break; 416 } 417 418 if (el.validity.valid && isEmpty) { 419 return; 420 } 421 422 el.focus(); 423 el.value = ""; 424 lazy.event.change(el); 425 el.blur(); 426 } 427 428 /** 429 * Waits until the event loop has spun enough times to process the 430 * DOM events generated by clicking an element, or until the document 431 * is unloaded. 432 * 433 * @param {Element} el 434 * Element that is expected to receive the click. 435 * 436 * @returns {Promise} 437 * Promise is resolved once <var>el</var> has been clicked 438 * (its <code>click</code> event fires), the document is unloaded, 439 * or a 500 ms timeout is reached. 440 */ 441 interaction.flushEventLoop = async function (el) { 442 const win = el.ownerGlobal; 443 let unloadEv, clickEv; 444 445 let spinEventLoop = resolve => { 446 unloadEv = resolve; 447 clickEv = event => { 448 lazy.logger.trace(`Received DOM event click for ${event.target}`); 449 if (win.closed) { 450 resolve(); 451 } else { 452 lazy.setTimeout(resolve, 0); 453 } 454 }; 455 456 win.addEventListener("unload", unloadEv, { mozSystemGroup: true }); 457 el.addEventListener("click", clickEv, { mozSystemGroup: true }); 458 }; 459 let removeListeners = () => { 460 // only one event fires 461 win.removeEventListener("unload", unloadEv); 462 el.removeEventListener("click", clickEv); 463 }; 464 465 return new lazy.TimedPromise(spinEventLoop, { 466 timeout: 500, 467 throws: null, 468 }).then(removeListeners); 469 }; 470 471 /** 472 * If <var>el<var> is a textual form control, or is contenteditable, 473 * and no previous selection state exists, move the caret to the end 474 * of the form control. 475 * 476 * The element has to be a <code><input type=text></code> or 477 * <code><textarea></code> element, or have the contenteditable 478 * attribute set, for the cursor to be moved. 479 * 480 * @param {Element} el 481 * Element to potential move the caret in. 482 */ 483 interaction.moveCaretToEnd = function (el) { 484 if (!lazy.dom.isDOMElement(el)) { 485 return; 486 } 487 488 let isTextarea = el.localName == "textarea"; 489 let isInputText = el.localName == "input" && el.type == "text"; 490 491 if (isTextarea || isInputText) { 492 if (el.selectionEnd == 0) { 493 let len = el.value.length; 494 el.setSelectionRange(len, len); 495 } 496 } else if (el.isContentEditable) { 497 let selection = getWindow(el).getSelection(); 498 selection.setPosition(el, el.childNodes.length); 499 } 500 }; 501 502 /** 503 * Performs checks if <var>el</var> is keyboard-interactable. 504 * 505 * To decide if an element is keyboard-interactable various properties, 506 * and computed CSS styles have to be evaluated. Whereby it has to be taken 507 * into account that the element can be part of a container (eg. option), 508 * and as such the container has to be checked instead. 509 * 510 * @param {Element} el 511 * Element to check. 512 * 513 * @returns {boolean} 514 * True if element is keyboard-interactable, false otherwise. 515 */ 516 interaction.isKeyboardInteractable = function (el) { 517 const win = getWindow(el); 518 519 // body and document element are always keyboard-interactable 520 if (el.localName === "body" || el === win.document.documentElement) { 521 return true; 522 } 523 524 // context menu popups do not take the focus from the document. 525 const menuPopup = el.closest("menupopup"); 526 if (menuPopup) { 527 if (menuPopup.state !== "open") { 528 // closed menupopups are not keyboard interactable. 529 return false; 530 } 531 532 const menuItem = el.closest("menuitem"); 533 if (menuItem) { 534 // hidden or disabled menu items are not keyboard interactable. 535 return !menuItem.disabled && !menuItem.hidden; 536 } 537 538 return true; 539 } 540 541 return Services.focus.elementIsFocusable(el, 0); 542 }; 543 544 /** 545 * Updates an `<input type=file>`'s file list with given `paths`. 546 * 547 * Hereby will the file list be appended with `paths` if the 548 * element allows multiple files. Otherwise the list will be 549 * replaced. 550 * 551 * @param {HTMLInputElement} el 552 * An `input type=file` element. 553 * @param {Array.<string>} paths 554 * List of full paths to any of the files to be uploaded. 555 * 556 * @throws {InvalidArgumentError} 557 * If `path` doesn't exist. 558 */ 559 interaction.uploadFiles = async function (el, paths) { 560 let files = []; 561 562 if (el.hasAttribute("multiple")) { 563 // for multiple file uploads new files will be appended 564 files = Array.prototype.slice.call(el.files); 565 } else if (paths.length > 1) { 566 throw new lazy.error.InvalidArgumentError( 567 lazy.pprint`Element ${el} doesn't accept multiple files` 568 ); 569 } 570 571 for (let path of paths) { 572 let file; 573 574 try { 575 file = await File.createFromFileName(path); 576 } catch (e) { 577 throw new lazy.error.InvalidArgumentError("File not found: " + path); 578 } 579 580 files.push(file); 581 } 582 583 el.mozSetFileArray(files); 584 }; 585 586 /** 587 * Sets a form element's value. 588 * 589 * @param {DOMElement} el 590 * An form element, e.g. input, textarea, etc. 591 * @param {string} value 592 * The value to be set. 593 * 594 * @throws {TypeError} 595 * If <var>el</var> is not an supported form element. 596 */ 597 interaction.setFormControlValue = function (el, value) { 598 if (!COMMON_FORM_CONTROLS.has(el.localName)) { 599 throw new TypeError("This function is for form elements only"); 600 } 601 602 el.value = value; 603 604 if (INPUT_TYPES_NO_EVENT.has(el.type)) { 605 return; 606 } 607 608 lazy.event.input(el); 609 lazy.event.change(el); 610 }; 611 612 /** 613 * Send keys to element. 614 * 615 * @param {DOMElement|XULElement} el 616 * Element to send key events to. 617 * @param {Array.<string>} value 618 * Sequence of keystrokes to send to the element. 619 * @param {object=} options 620 * @param {boolean=} options.strictFileInteractability 621 * Run interactability checks on `<input type=file>` elements. 622 * @param {boolean=} options.accessibilityChecks 623 * Enforce strict accessibility tests. 624 * @param {boolean=} options.webdriverClick 625 * Use WebDriver specification compatible interactability definition. 626 */ 627 interaction.sendKeysToElement = async function ( 628 el, 629 value, 630 { 631 strictFileInteractability = false, 632 accessibilityChecks = false, 633 webdriverClick = false, 634 } = {} 635 ) { 636 const a11y = lazy.accessibility.get(accessibilityChecks); 637 638 if (webdriverClick) { 639 await webdriverSendKeysToElement( 640 el, 641 value, 642 a11y, 643 strictFileInteractability 644 ); 645 } else { 646 await legacySendKeysToElement(el, value, a11y); 647 } 648 }; 649 650 async function webdriverSendKeysToElement( 651 el, 652 value, 653 a11y, 654 strictFileInteractability 655 ) { 656 const win = getWindow(el); 657 658 if (el.type !== "file" || strictFileInteractability) { 659 let containerEl = lazy.dom.getContainer(el); 660 661 if (!lazy.dom.isInView(containerEl)) { 662 lazy.dom.scrollIntoView(containerEl); 663 } 664 665 // TODO: Wait for element to be keyboard-interactible 666 if (!interaction.isKeyboardInteractable(containerEl)) { 667 throw new lazy.error.ElementNotInteractableError( 668 lazy.pprint`Element ${el} is not reachable by keyboard` 669 ); 670 } 671 672 if (win.document.activeElement !== containerEl) { 673 containerEl.focus(); 674 // This validates the correct element types internally 675 interaction.moveCaretToEnd(containerEl); 676 } 677 } 678 679 let acc = await a11y.assertAccessible(el, true); 680 a11y.assertActionable(acc, el); 681 682 if (el.type == "file") { 683 let paths = value.split("\n"); 684 await interaction.uploadFiles(el, paths); 685 686 lazy.event.input(el); 687 lazy.event.change(el); 688 } else if (el.type == "date" || el.type == "time") { 689 interaction.setFormControlValue(el, value); 690 } else { 691 lazy.event.sendKeys(value, win); 692 } 693 } 694 695 async function legacySendKeysToElement(el, value, a11y) { 696 const win = getWindow(el); 697 698 if (el.type == "file") { 699 el.focus(); 700 await interaction.uploadFiles(el, [value]); 701 702 lazy.event.input(el); 703 lazy.event.change(el); 704 } else if (el.type == "date" || el.type == "time") { 705 interaction.setFormControlValue(el, value); 706 } else { 707 let visibilityCheckEl = el; 708 if (el.localName == "option") { 709 visibilityCheckEl = lazy.dom.getContainer(el); 710 } 711 712 if (!(await lazy.dom.isVisible(visibilityCheckEl))) { 713 throw new lazy.error.ElementNotInteractableError( 714 "Element is not visible" 715 ); 716 } 717 718 let acc = await a11y.assertAccessible(el, true); 719 a11y.assertActionable(acc, el); 720 721 interaction.moveCaretToEnd(el); 722 el.focus(); 723 lazy.event.sendKeys(value, win); 724 } 725 } 726 727 /** 728 * Determine the element displayedness of an element. 729 * 730 * @param {DOMElement|XULElement} el 731 * Element to determine displayedness of. 732 * @param {boolean=} [strict=false] strict 733 * Enforce strict accessibility tests. 734 * 735 * @returns {boolean} 736 * True if element is displayed, false otherwise. 737 */ 738 interaction.isElementDisplayed = async function (el, strict = false) { 739 let win = getWindow(el); 740 let displayed = await lazy.atom.isElementDisplayed(el, win); 741 742 let a11y = lazy.accessibility.get(strict); 743 return a11y.assertAccessible(el).then(acc => { 744 a11y.assertVisible(acc, el, displayed); 745 return displayed; 746 }); 747 }; 748 749 /** 750 * Check if element is enabled. 751 * 752 * @param {DOMElement|XULElement} el 753 * Element to test if is enabled. 754 * 755 * @returns {boolean} 756 * True if enabled, false otherwise. 757 */ 758 interaction.isElementEnabled = async function (el, strict = false) { 759 let enabled = true; 760 let win = getWindow(el); 761 762 if (lazy.dom.isXULElement(el)) { 763 // check if XUL element supports disabled attribute 764 if (DISABLED_ATTRIBUTE_SUPPORTED_XUL.has(el.tagName.toUpperCase())) { 765 if ( 766 el.hasAttribute("disabled") && 767 el.getAttribute("disabled") === "true" 768 ) { 769 enabled = false; 770 } 771 } 772 } else if ( 773 ["application/xml", "text/xml"].includes(win.document.contentType) 774 ) { 775 enabled = false; 776 } else { 777 enabled = await lazy.dom.isEnabled(el); 778 } 779 780 let a11y = lazy.accessibility.get(strict); 781 return a11y.assertAccessible(el).then(acc => { 782 a11y.assertEnabled(acc, el, enabled); 783 return enabled; 784 }); 785 }; 786 787 /** 788 * Determines if the referenced element is selected or not, with 789 * an additional accessibility check if <var>strict</var> is true. 790 * 791 * This operation only makes sense on input elements of the checkbox- 792 * and radio button states, and option elements. 793 * 794 * @param {(DOMElement|XULElement)} el 795 * Element to test if is selected. 796 * @param {boolean=} [strict=false] strict 797 * Enforce strict accessibility tests. 798 * 799 * @returns {boolean} 800 * True if element is selected, false otherwise. 801 * 802 * @throws {ElementNotAccessibleError} 803 * If <var>el</var> is not accessible when <var>strict</var> is true. 804 */ 805 interaction.isElementSelected = function (el, strict = false) { 806 let selected = lazy.dom.isSelected(el); 807 808 let a11y = lazy.accessibility.get(strict); 809 return a11y.assertAccessible(el).then(acc => { 810 a11y.assertSelected(acc, el, selected); 811 return selected; 812 }); 813 }; 814 815 function getWindow(el) { 816 // eslint-disable-next-line mozilla/use-ownerGlobal 817 return el.ownerDocument.defaultView; 818 }