tor-browser

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

browser_aria_activedescendant.js (13169B)


      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 "use strict";
      6 
      7 /* import-globals-from ../../mochitest/role.js */
      8 /* import-globals-from ../../mochitest/states.js */
      9 loadScripts(
     10  { name: "role.js", dir: MOCHITESTS_DIR },
     11  { name: "states.js", dir: MOCHITESTS_DIR }
     12 );
     13 
     14 async function synthFocus(browser, container, item) {
     15  let focusPromise = waitForEvent(EVENT_FOCUS, item);
     16  await invokeContentTask(browser, [container], _container => {
     17    let elm = (
     18      content.document._testGetElementById || content.document.getElementById
     19    ).bind(content.document)(_container);
     20    elm.focus();
     21  });
     22  await focusPromise;
     23 }
     24 
     25 async function changeARIAActiveDescendant(
     26  browser,
     27  container,
     28  itemId,
     29  prevItemId,
     30  elementReflection
     31 ) {
     32  let expectedEvents = [[EVENT_FOCUS, itemId]];
     33 
     34  if (prevItemId) {
     35    info("A state change of the previous item precedes the new one.");
     36    expectedEvents.push(
     37      stateChangeEventArgs(prevItemId, EXT_STATE_ACTIVE, false, true)
     38    );
     39  }
     40 
     41  expectedEvents.push(
     42    stateChangeEventArgs(itemId, EXT_STATE_ACTIVE, true, true)
     43  );
     44 
     45  let expectedPromise = waitForEvents(expectedEvents);
     46  await invokeContentTask(
     47    browser,
     48    [container, itemId, elementReflection],
     49    (_container, _itemId, _elementReflection) => {
     50      let getElm = (
     51        content.document._testGetElementById || content.document.getElementById
     52      ).bind(content.document);
     53      let elm = getElm(_container);
     54      if (_elementReflection) {
     55        elm.ariaActiveDescendantElement = getElm(_itemId);
     56      } else {
     57        elm.setAttribute("aria-activedescendant", _itemId);
     58      }
     59    }
     60  );
     61 
     62  await expectedPromise;
     63 }
     64 
     65 async function clearARIAActiveDescendant(
     66  browser,
     67  container,
     68  prevItemId,
     69  defaultId,
     70  elementReflection
     71 ) {
     72  let expectedEvents = [[EVENT_FOCUS, defaultId || container]];
     73  if (prevItemId) {
     74    expectedEvents.push(
     75      stateChangeEventArgs(prevItemId, EXT_STATE_ACTIVE, false, true)
     76    );
     77  }
     78 
     79  if (defaultId) {
     80    expectedEvents.push(
     81      stateChangeEventArgs(defaultId, EXT_STATE_ACTIVE, true, true)
     82    );
     83  }
     84 
     85  let expectedPromise = waitForEvents(expectedEvents);
     86  await invokeContentTask(
     87    browser,
     88    [container, elementReflection],
     89    (_container, _elementReflection) => {
     90      let elm = (
     91        content.document._testGetElementById || content.document.getElementById
     92      ).bind(content.document)(_container);
     93      if (_elementReflection) {
     94        elm.ariaActiveDescendantElement = null;
     95      } else {
     96        elm.removeAttribute("aria-activedescendant");
     97      }
     98    }
     99  );
    100 
    101  await expectedPromise;
    102 }
    103 
    104 async function insertItemNFocus(
    105  browser,
    106  container,
    107  newItemID,
    108  prevItemId,
    109  elementReflection
    110 ) {
    111  let expectedEvents = [
    112    [EVENT_SHOW, newItemID],
    113    [EVENT_FOCUS, newItemID],
    114  ];
    115 
    116  if (prevItemId) {
    117    info("A state change of the previous item precedes the new one.");
    118    expectedEvents.push(
    119      stateChangeEventArgs(prevItemId, EXT_STATE_ACTIVE, false, true)
    120    );
    121  }
    122 
    123  expectedEvents.push(
    124    stateChangeEventArgs(newItemID, EXT_STATE_ACTIVE, true, true)
    125  );
    126 
    127  let expectedPromise = waitForEvents(expectedEvents);
    128 
    129  await invokeContentTask(
    130    browser,
    131    [container, newItemID, elementReflection],
    132    (_container, _newItemID, _elementReflection) => {
    133      let elm = (
    134        content.document._testGetElementById || content.document.getElementById
    135      ).bind(content.document)(_container);
    136      let itemElm = content.document.createElement("div");
    137      itemElm.setAttribute("id", _newItemID);
    138      itemElm.setAttribute("role", "listitem");
    139      itemElm.textContent = _newItemID;
    140      elm.appendChild(itemElm);
    141      if (_elementReflection) {
    142        elm.ariaActiveDescendantElement = itemElm;
    143      } else {
    144        elm.setAttribute("aria-activedescendant", _newItemID);
    145      }
    146    }
    147  );
    148 
    149  await expectedPromise;
    150 }
    151 
    152 async function moveARIAActiveDescendantID(browser, fromID, toID) {
    153  let expectedEvents = [
    154    [EVENT_FOCUS, toID],
    155    stateChangeEventArgs(toID, EXT_STATE_ACTIVE, true, true),
    156  ];
    157 
    158  let expectedPromise = waitForEvents(expectedEvents);
    159  await invokeContentTask(browser, [fromID, toID], (_fromID, _toID) => {
    160    let orig = (
    161      content.document._testGetElementById || content.document.getElementById
    162    ).bind(content.document)(_toID);
    163    if (orig) {
    164      orig.id = "";
    165    }
    166    (
    167      content.document._testGetElementById || content.document.getElementById
    168    ).bind(content.document)(_fromID).id = _toID;
    169  });
    170  await expectedPromise;
    171 }
    172 
    173 async function changeARIAActiveDescendantInvalid(
    174  browser,
    175  container,
    176  invalidID = "invalid",
    177  prevItemId = null
    178 ) {
    179  let expectedEvents = [[EVENT_FOCUS, container]];
    180  if (prevItemId) {
    181    expectedEvents.push(
    182      stateChangeEventArgs(prevItemId, EXT_STATE_ACTIVE, false, true)
    183    );
    184  }
    185 
    186  let expectedPromise = waitForEvents(expectedEvents);
    187  await invokeContentTask(
    188    browser,
    189    [container, invalidID],
    190    (_container, _invalidID) => {
    191      let elm = (
    192        content.document._testGetElementById || content.document.getElementById
    193      ).bind(content.document)(_container);
    194      elm.setAttribute("aria-activedescendant", _invalidID);
    195    }
    196  );
    197 
    198  await expectedPromise;
    199 }
    200 
    201 const LISTBOX_MARKUP = `
    202 <div role="listbox" aria-activedescendant="item1" id="listbox" tabindex="1"
    203 aria-owns="item3">
    204 <div role="listitem" id="item1">item1</div>
    205 <div role="listitem" id="item2">item2</div>
    206 <div role="listitem" id="roaming" data-id="roaming">roaming</div>
    207 <div role="listitem" id="roaming2" data-id="roaming2">roaming2</div>
    208 </div>
    209 <div role="listitem" id="item3">item3</div>
    210 <div role="combobox" id="combobox">
    211 <input id="combobox_entry">
    212 <ul>
    213  <li role="option" id="combobox_option1">option1</li>
    214  <li role="option" id="combobox_option2">option2</li>
    215 </ul>
    216 </div>`;
    217 
    218 async function basicListboxTest(browser, elementReflection) {
    219  await synthFocus(browser, "listbox", "item1");
    220  await changeARIAActiveDescendant(
    221    browser,
    222    "listbox",
    223    "item2",
    224    "item1",
    225    elementReflection
    226  );
    227  await changeARIAActiveDescendant(
    228    browser,
    229    "listbox",
    230    "item3",
    231    "item2",
    232    elementReflection
    233  );
    234 
    235  info("Focus out of listbox");
    236  await synthFocus(browser, "combobox_entry", "combobox_entry");
    237  await changeARIAActiveDescendant(
    238    browser,
    239    "combobox",
    240    "combobox_option2",
    241    null,
    242    elementReflection
    243  );
    244  await changeARIAActiveDescendant(
    245    browser,
    246    "combobox",
    247    "combobox_option1",
    248    null,
    249    elementReflection
    250  );
    251 
    252  info("Focus back in listbox");
    253  await synthFocus(browser, "listbox", "item3");
    254  await insertItemNFocus(
    255    browser,
    256    "listbox",
    257    "item4",
    258    "item3",
    259    elementReflection
    260  );
    261 
    262  await clearARIAActiveDescendant(
    263    browser,
    264    "listbox",
    265    "item4",
    266    null,
    267    elementReflection
    268  );
    269  await changeARIAActiveDescendant(
    270    browser,
    271    "listbox",
    272    "item1",
    273    null,
    274    elementReflection
    275  );
    276 }
    277 
    278 addAccessibleTask(
    279  LISTBOX_MARKUP,
    280  async function (browser) {
    281    info("Test aria-activedescendant content attribute");
    282    await basicListboxTest(browser, false);
    283 
    284    await changeARIAActiveDescendantInvalid(
    285      browser,
    286      "listbox",
    287      "invalid",
    288      "item1"
    289    );
    290 
    291    await changeARIAActiveDescendant(browser, "listbox", "roaming");
    292    await moveARIAActiveDescendantID(browser, "roaming2", "roaming");
    293    await changeARIAActiveDescendantInvalid(
    294      browser,
    295      "listbox",
    296      "roaming3",
    297      "roaming"
    298    );
    299    await moveARIAActiveDescendantID(browser, "roaming", "roaming3");
    300  },
    301  { topLevel: true, chrome: true }
    302 );
    303 
    304 addAccessibleTask(
    305  LISTBOX_MARKUP,
    306  async function (browser) {
    307    info("Test ariaActiveDescendantElement element reflection");
    308    await basicListboxTest(browser, true);
    309  },
    310  { topLevel: true, chrome: true }
    311 );
    312 
    313 addAccessibleTask(
    314  `
    315 <input id="activedesc_nondesc_input" aria-activedescendant="activedesc_nondesc_option">
    316 <div role="listbox">
    317  <div role="option" id="activedesc_nondesc_option">option</div>
    318 </div>`,
    319  async function (browser) {
    320    info("Test aria-activedescendant non-descendant");
    321    await synthFocus(
    322      browser,
    323      "activedesc_nondesc_input",
    324      "activedesc_nondesc_option"
    325    );
    326  },
    327  { topLevel: true, chrome: true }
    328 );
    329 
    330 addAccessibleTask(
    331  `<div id="shadow"></div>`,
    332  async function (browser) {
    333    info("Test aria-activedescendant in shadow root");
    334 
    335    await invokeContentTask(browser, [], () => {
    336      const doc = content.document;
    337 
    338      let host = doc.getElementById("shadow");
    339      let shadow = host.attachShadow({ mode: "open" });
    340      let listbox = doc.createElement("div");
    341      listbox.id = "shadowListbox";
    342      listbox.setAttribute("role", "listbox");
    343      listbox.setAttribute("tabindex", "0");
    344      shadow.appendChild(listbox);
    345      let item = doc.createElement("div");
    346      item.id = "shadowItem1";
    347      item.setAttribute("role", "option");
    348      listbox.appendChild(item);
    349      listbox.setAttribute("aria-activedescendant", "shadowItem1");
    350      item = doc.createElement("div");
    351      item.id = "shadowItem2";
    352      item.setAttribute("role", "option");
    353      listbox.appendChild(item);
    354 
    355      // We want to retrieve elements using their IDs inside the shadow root, so
    356      // we define a custom get element by ID method that our utility functions
    357      // above call into if it exists.
    358      doc._testGetElementById = id =>
    359        doc.getElementById("shadow").shadowRoot.getElementById(id);
    360    });
    361 
    362    await synthFocus(browser, "shadowListbox", "shadowItem1");
    363    await changeARIAActiveDescendant(
    364      browser,
    365      "shadowListbox",
    366      "shadowItem2",
    367      "shadowItem1"
    368    );
    369    info("Do it again with element reflection");
    370    await changeARIAActiveDescendant(
    371      browser,
    372      "shadowListbox",
    373      "shadowItem1",
    374      "shadowItem2",
    375      true
    376    );
    377  },
    378  { topLevel: true, chrome: true }
    379 );
    380 
    381 addAccessibleTask(
    382  `
    383 <div id="comboboxWithHiddenList" tabindex="0" role="combobox" aria-owns="hiddenList">
    384 </div>
    385 <div id="hiddenList" hidden role="listbox">
    386  <div id="hiddenListOption" role="option"></div>
    387 </div>`,
    388  async function (browser, docAcc) {
    389    info("Test simultaneous insertion, relocation and aria-activedescendant");
    390    await synthFocus(
    391      browser,
    392      "comboboxWithHiddenList",
    393      "comboboxWithHiddenList"
    394    );
    395 
    396    testStates(
    397      findAccessibleChildByID(docAcc, "comboboxWithHiddenList"),
    398      STATE_FOCUSED
    399    );
    400    let evtProm = Promise.all([
    401      waitForEvent(EVENT_FOCUS, "hiddenListOption"),
    402      waitForStateChange("hiddenListOption", EXT_STATE_ACTIVE, true, true),
    403    ]);
    404    await invokeContentTask(browser, [], () => {
    405      info("hiddenList is owned, so unhiding causes insertion and relocation.");
    406      (
    407        content.document._testGetElementById || content.document.getElementById
    408      ).bind(content.document)("hiddenList").hidden = false;
    409      content.document
    410        .getElementById("comboboxWithHiddenList")
    411        .setAttribute("aria-activedescendant", "hiddenListOption");
    412    });
    413    await evtProm;
    414    testStates(
    415      findAccessibleChildByID(docAcc, "hiddenListOption"),
    416      STATE_FOCUSED
    417    );
    418  },
    419  { topLevel: true, chrome: true }
    420 );
    421 
    422 addAccessibleTask(
    423  `
    424 <custom-listbox id="custom-listbox1">
    425  <div role="listitem" id="l1_1"></div>
    426  <div role="listitem" id="l1_2"></div>
    427  <div role="listitem" id="l1_3"></div>
    428 </custom-listbox>
    429 
    430 <custom-listbox id="custom-listbox2" aria-activedescendant="l2_1">
    431  <div role="listitem" id="l2_1"></div>
    432  <div role="listitem" id="l2_2"></div>
    433  <div role="listitem" id="l2_3"></div>
    434 </custom-listbox>
    435 
    436 <script>
    437 customElements.define("custom-listbox",
    438  class extends HTMLElement {
    439    constructor() {
    440      super();
    441      this.tabIndex = "0"
    442      this._internals = this.attachInternals();
    443      this._internals.role = "listbox";
    444      this._internals.ariaActiveDescendantElement = this.lastElementChild;
    445    }
    446    get internals() {
    447      return this._internals;
    448    }
    449  }
    450 );
    451 </script>`,
    452  async function (browser) {
    453    await synthFocus(browser, "custom-listbox1", "l1_3");
    454 
    455    let evtProm = Promise.all([
    456      waitForEvent(EVENT_FOCUS, "l1_2"),
    457      waitForStateChange("l1_3", EXT_STATE_ACTIVE, false, true),
    458      waitForStateChange("l1_2", EXT_STATE_ACTIVE, true, true),
    459    ]);
    460 
    461    await invokeContentTask(browser, [], () => {
    462      content.document.getElementById(
    463        "custom-listbox1"
    464      ).internals.ariaActiveDescendantElement =
    465        content.document.getElementById("l1_2");
    466    });
    467 
    468    await evtProm;
    469 
    470    evtProm = Promise.all([
    471      waitForEvent(EVENT_FOCUS, "custom-listbox1"),
    472      waitForStateChange("l1_2", EXT_STATE_ACTIVE, false, true),
    473    ]);
    474 
    475    await invokeContentTask(browser, [], () => {
    476      content.document.getElementById(
    477        "custom-listbox1"
    478      ).internals.ariaActiveDescendantElement = null;
    479    });
    480 
    481    await evtProm;
    482 
    483    await synthFocus(browser, "custom-listbox2", "l2_1");
    484    await clearARIAActiveDescendant(browser, "custom-listbox2", "l2_1", "l2_3");
    485  }
    486 );