tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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>&lt;option&gt;</tt> element in a <tt>&lt;select&gt;</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>&lt;option&gt;</tt>
    300 *     element.
    301 * @throws {Error}
    302 *     If unable to find <var>el</var>'s parent <tt>&lt;select&gt;</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>&lt;input type=file&gt;</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>&lt;input type=text&gt;</code> or
    477 * <code>&lt;textarea&gt;</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 }