tor-browser

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

head.js (26871B)


      1 /* Any copyright is dedicated to the Public Domain.
      2 http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 /* global waitUntilState, gBrowser */
      5 /* exported addTestTab, checkTreeState, checkSidebarState, checkAuditState, selectRow,
      6            toggleRow, toggleMenuItem, addA11yPanelTestsTask, navigate,
      7            openSimulationMenu, toggleSimulationOption, TREE_FILTERS_MENU_ID,
      8            PREFS_MENU_ID */
      9 
     10 "use strict";
     11 
     12 // Import framework's shared head.
     13 Services.scriptloader.loadSubScript(
     14  "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
     15  this
     16 );
     17 
     18 // Import inspector's shared head.
     19 Services.scriptloader.loadSubScript(
     20  "chrome://mochitests/content/browser/devtools/client/inspector/test/shared-head.js",
     21  this
     22 );
     23 
     24 const {
     25  ORDERED_PROPS,
     26  PREF_KEYS,
     27 } = require("resource://devtools/client/accessibility/constants.js");
     28 
     29 const SIMULATION_MENU_BUTTON_ID = "simulation-menu-button";
     30 const SIMULATION_MENU_ID = "simulation-menu-button-menu";
     31 const TREE_FILTERS_MENU_ID = "accessibility-tree-filters-menu";
     32 const PREFS_MENU_ID = "accessibility-tree-filters-prefs-menu";
     33 
     34 const MENU_INDEXES = {
     35  [TREE_FILTERS_MENU_ID]: 0,
     36  [PREFS_MENU_ID]: 1,
     37 };
     38 
     39 /**
     40 * Wait for accessibility service to shut down. We consider it shut down when
     41 * an "a11y-init-or-shutdown" event is received with a value of "0".
     42 */
     43 function waitForAccessibilityShutdown() {
     44  return new Promise(resolve => {
     45    if (!Services.appinfo.accessibilityEnabled) {
     46      resolve();
     47      return;
     48    }
     49 
     50    const observe = (subject, topic, data) => {
     51      if (data === "0") {
     52        Services.obs.removeObserver(observe, "a11y-init-or-shutdown");
     53        // Sanity check
     54        ok(
     55          !Services.appinfo.accessibilityEnabled,
     56          "Accessibility disabled in this process"
     57        );
     58        resolve();
     59      }
     60    };
     61    // This event is coming from Gecko accessibility module when the
     62    // accessibility service is shutdown or initialzied. We attempt to shutdown
     63    // accessibility service naturally if there are no more XPCOM references to
     64    // a11y related objects (after GC/CC).
     65    Services.obs.addObserver(observe, "a11y-init-or-shutdown");
     66 
     67    // Force garbage collection.
     68    SpecialPowers.gc();
     69    SpecialPowers.forceShrinkingGC();
     70    SpecialPowers.forceCC();
     71  });
     72 }
     73 
     74 /**
     75 * Ensure that accessibility is completely shutdown.
     76 */
     77 async function shutdownAccessibility(browser) {
     78  await waitForAccessibilityShutdown();
     79  await SpecialPowers.spawn(browser, [], waitForAccessibilityShutdown);
     80 }
     81 
     82 const EXPANDABLE_PROPS = ["actions", "states", "attributes"];
     83 
     84 /**
     85 * Add a new test tab in the browser and load the given url.
     86 *
     87 * @param {string} url
     88 *        The url to be loaded in the new tab
     89 * @param {object} options
     90 * @param {boolean} options.waitUntilDocumentAccessibleInState
     91 *        Whether we should wait for the state to have the document accessible.
     92 *        Defaults to true.
     93 * @return a promise that resolves to the tab object when
     94 *        the url is loaded
     95 */
     96 async function addTestTab(
     97  url,
     98  { waitUntilDocumentAccessibleInState = true } = {}
     99 ) {
    100  info("Adding a new test tab with URL: '" + url + "'");
    101 
    102  const tab = await addTab(url);
    103  const panel = await initAccessibilityPanel(tab);
    104  const win = panel.panelWin;
    105  const doc = win.document;
    106  const store = win.view.store;
    107 
    108  win.focus();
    109 
    110  if (waitUntilDocumentAccessibleInState) {
    111    await waitUntilState(
    112      store,
    113      state =>
    114        state.accessibles.size === 1 &&
    115        state.details.accessible?.role === "document"
    116    );
    117  }
    118 
    119  return {
    120    tab,
    121    browser: tab.linkedBrowser,
    122    panel,
    123    win,
    124    toolbox: panel._toolbox,
    125    doc,
    126    store,
    127  };
    128 }
    129 
    130 /**
    131 * Open the Accessibility panel for the given tab.
    132 *
    133 * @param {Element} tab
    134 *        Optional tab element for which you want open the Accessibility panel.
    135 *        The default tab is taken from the global variable |tab|.
    136 * @return a promise that is resolved once the panel is open.
    137 */
    138 async function initAccessibilityPanel(tab = gBrowser.selectedTab) {
    139  const toolbox = await gDevTools.showToolboxForTab(tab, {
    140    toolId: "accessibility",
    141  });
    142  return toolbox.getCurrentPanel();
    143 }
    144 
    145 /**
    146 * Compare text within the list of potential badges rendered for accessibility
    147 * tree row when its accessible object has accessibility failures.
    148 *
    149 * @param {DOMNode} badges
    150 *        Container element that contains badge elements.
    151 * @param {Array|null} expected
    152 *        List of expected badge labels for failing accessibility checks.
    153 */
    154 function compareBadges(badges, expected = []) {
    155  const badgeEls = badges ? [...badges.querySelectorAll(".badge")] : [];
    156  return (
    157    badgeEls.length === expected.length &&
    158    badgeEls.every((badge, i) => badge.textContent === expected[i])
    159  );
    160 }
    161 
    162 /**
    163 * Find an ancestor that is scrolled for a given DOMNode.
    164 *
    165 * @param {DOMNode} node
    166 *        DOMNode that to find an ancestor for that is scrolled.
    167 */
    168 function closestScrolledParent(node) {
    169  if (node == null) {
    170    return null;
    171  }
    172 
    173  if (node.scrollHeight > node.clientHeight) {
    174    return node;
    175  }
    176 
    177  return closestScrolledParent(node.parentNode);
    178 }
    179 
    180 /**
    181 * Check if a given element is visible to the user and is not scrolled off
    182 * because of the overflow.
    183 *
    184 * @param   {Element} element
    185 *          Element to be checked whether it is visible and is not scrolled off.
    186 *
    187 * @returns {boolean}
    188 *          True if the element is visible.
    189 */
    190 function isVisible(element) {
    191  const { top, bottom } = element.getBoundingClientRect();
    192  const scrolledParent = closestScrolledParent(element.parentNode);
    193  const scrolledParentRect = scrolledParent
    194    ? scrolledParent.getBoundingClientRect()
    195    : null;
    196  return (
    197    !scrolledParent ||
    198    (top >= scrolledParentRect.top && bottom <= scrolledParentRect.bottom)
    199  );
    200 }
    201 
    202 /**
    203 * Check selected styling and visibility for a given row in the accessibility
    204 * tree.
    205 *
    206 * @param   {DOMNode} row
    207 *          DOMNode for a given accessibility row.
    208 * @param   {boolean} expected
    209 *          Expected selected state.
    210 *
    211 * @returns {boolean}
    212 *          True if visibility and styling matches expected selected state.
    213 */
    214 function checkSelected(row, expected) {
    215  if (!expected) {
    216    return true;
    217  }
    218 
    219  if (row.classList.contains("selected") !== expected) {
    220    return false;
    221  }
    222 
    223  return isVisible(row);
    224 }
    225 
    226 /**
    227 * Check level for a given row in the accessibility tree.
    228 *
    229 * @param   {DOMNode} row
    230 *          DOMNode for a given accessibility row.
    231 * @param   {boolean} expected
    232 *          Expected row level (aria-level).
    233 *
    234 * @returns {boolean}
    235 *          True if the aria-level for the row is as expected.
    236 */
    237 function checkLevel(row, expected) {
    238  if (!expected) {
    239    return true;
    240  }
    241 
    242  return parseInt(row.getAttribute("aria-level"), 10) === expected;
    243 }
    244 
    245 /**
    246 * Check the state of the accessibility tree.
    247 *
    248 * @param  {document} doc       panel documnent.
    249 * @param  {Array}    expected  an array that represents an expected row list.
    250 */
    251 async function checkTreeState(doc, expected) {
    252  info("Checking tree state.");
    253  const hasExpectedStructure = await BrowserTestUtils.waitForCondition(() => {
    254    const rows = [...doc.querySelectorAll(".treeRow")];
    255    if (rows.length !== expected.length) {
    256      return false;
    257    }
    258 
    259    return rows.every((row, i) => {
    260      const { role, name, badges, selected, level } = expected[i];
    261      return (
    262        row.querySelector(".treeLabelCell").textContent === role &&
    263        row.querySelector(".treeValueCell").textContent === name &&
    264        compareBadges(row.querySelector(".badges"), badges) &&
    265        checkSelected(row, selected) &&
    266        checkLevel(row, level)
    267      );
    268    });
    269  }, "Wait for the right tree update.");
    270 
    271  ok(hasExpectedStructure, "Tree structure is correct.");
    272 }
    273 
    274 /**
    275 * Check if relations object matches what is expected. Note: targets are matched by their
    276 * name and role.
    277 *
    278 * @param  {object} relations  Relations to test.
    279 * @param  {object} expected   Expected relations.
    280 * @return {boolean}           True if relation types and their targers match what is
    281 *                             expected.
    282 */
    283 function relationsMatch(relations, expected) {
    284  for (const relationType in expected) {
    285    let expTargets = expected[relationType];
    286    expTargets = Array.isArray(expTargets) ? expTargets : [expTargets];
    287 
    288    let targets = relations ? relations[relationType] : [];
    289    targets = Array.isArray(targets) ? targets : [targets];
    290 
    291    for (const index in expTargets) {
    292      if (!targets[index]) {
    293        return false;
    294      }
    295      if (
    296        expTargets[index].name !== targets[index].name ||
    297        expTargets[index].role !== targets[index].role
    298      ) {
    299        return false;
    300      }
    301    }
    302  }
    303 
    304  return true;
    305 }
    306 
    307 /**
    308 * When comparing numerical values (for example contrast), we only care about the 2
    309 * decimal points.
    310 *
    311 * @param  {string} _
    312 *         Key of the property that is parsed.
    313 * @param  {Any} value
    314 *         Value of the property that is parsed.
    315 * @return {Any}
    316 *         Newly formatted value in case of the numeric value.
    317 */
    318 function parseNumReplacer(_, value) {
    319  if (typeof value === "number") {
    320    return value.toFixed(2);
    321  }
    322 
    323  return value;
    324 }
    325 
    326 /**
    327 * Check the state of the accessibility sidebar audit(checks).
    328 *
    329 * @param  {object} store         React store for the panel (includes store for
    330 *                                the audit).
    331 * @param  {object} expectedState Expected state of the sidebar audit(checks).
    332 */
    333 async function checkAuditState(store, expectedState) {
    334  info("Checking audit state.");
    335  await waitUntilState(store, ({ details }) => {
    336    const { audit } = details;
    337 
    338    for (const key in expectedState) {
    339      const expected = expectedState[key];
    340      if (expected && typeof expected === "object") {
    341        if (
    342          JSON.stringify(audit[key], parseNumReplacer) !==
    343          JSON.stringify(expected, parseNumReplacer)
    344        ) {
    345          return false;
    346        }
    347      } else if (audit && audit[key] !== expected) {
    348        return false;
    349      }
    350    }
    351 
    352    ok(true, "Audit state is correct.");
    353    return true;
    354  });
    355 }
    356 
    357 /**
    358 * Check the state of the accessibility sidebar.
    359 *
    360 * @param  {object} store         React store for the panel (includes store for
    361 *                                the sidebar).
    362 * @param  {object} expectedState Expected state of the sidebar.
    363 */
    364 async function checkSidebarState(store, expectedState) {
    365  info("Checking sidebar state.");
    366  await waitUntilState(store, ({ details }) => {
    367    for (const key of ORDERED_PROPS) {
    368      const expected = expectedState[key];
    369      if (expected === undefined) {
    370        continue;
    371      }
    372 
    373      if (key === "relations") {
    374        if (!relationsMatch(details.relations, expected)) {
    375          return false;
    376        }
    377      } else if (EXPANDABLE_PROPS.includes(key)) {
    378        if (
    379          JSON.stringify(details.accessible[key]) !== JSON.stringify(expected)
    380        ) {
    381          return false;
    382        }
    383      } else if (details.accessible && details.accessible[key] !== expected) {
    384        return false;
    385      }
    386    }
    387 
    388    ok(true, "Sidebar state is correct.");
    389    return true;
    390  });
    391 }
    392 
    393 /**
    394 * Check the state of the accessibility related prefs.
    395 *
    396 * @param  {Document} doc
    397 *         accessibility inspector panel document.
    398 * @param  {object}   toolbarPrefValues
    399 *         Expected state of the panel prefs as well as the redux state that
    400 *         keeps track of it. Includes:
    401 *         - SCROLL_INTO_VIEW (devtools.accessibility.scroll-into-view)
    402 * @param  {object}   store
    403 *         React store for the panel (includes store for the sidebar).
    404 */
    405 async function checkToolbarPrefsState(doc, toolbarPrefValues, store) {
    406  info("Checking toolbar prefs state.");
    407  const [hasExpectedStructure] = await Promise.all([
    408    // Check that appropriate preferences are set as expected.
    409    BrowserTestUtils.waitForCondition(() => {
    410      return Object.keys(toolbarPrefValues).every(
    411        name =>
    412          Services.prefs.getBoolPref(PREF_KEYS[name], false) ===
    413          toolbarPrefValues[name]
    414      );
    415    }, "Wait for the right prefs state."),
    416    // Check that ui state is set as expected.
    417    waitUntilState(store, ({ ui }) => {
    418      for (const name in toolbarPrefValues) {
    419        if (ui[name] !== toolbarPrefValues[name]) {
    420          return false;
    421        }
    422      }
    423 
    424      ok(true, "UI pref state is correct.");
    425      return true;
    426    }),
    427  ]);
    428  ok(hasExpectedStructure, "Prefs state is correct.");
    429 }
    430 
    431 /**
    432 * Check the state of the accessibility checks toolbar.
    433 *
    434 * @param  {object} store
    435 *         React store for the panel (includes store for the sidebar).
    436 * @param  {object} activeToolbarFilters
    437 *         Expected active state of the filters in the toolbar.
    438 */
    439 async function checkToolbarState(doc, activeToolbarFilters) {
    440  info("Checking toolbar state.");
    441  const hasExpectedStructure = await BrowserTestUtils.waitForCondition(
    442    () =>
    443      [
    444        ...doc.querySelectorAll("#accessibility-tree-filters-menu .command"),
    445      ].every(
    446        (filter, i) =>
    447          (activeToolbarFilters[i] ? "true" : null) ===
    448          filter.getAttribute("aria-checked")
    449      ),
    450    "Wait for the right toolbar state."
    451  );
    452 
    453  ok(hasExpectedStructure, "Toolbar state is correct.");
    454 }
    455 
    456 /**
    457 * Check the state of the simulation button and menu components.
    458 *
    459 * @param  {object} doc         Panel document.
    460 * @param  {object} toolboxDoc  Toolbox document.
    461 * @param  {object} expected    Expected states of the simulation components:
    462 * @param  {boolean} expected.buttonActive
    463 * @param  {Array<number>} expected.checkedOptionIndices
    464 * @param  {Array<number>} expected.colorMatrix
    465 */
    466 async function checkSimulationState(doc, toolboxDoc, expected) {
    467  const { buttonActive, checkedOptionIndices, colorMatrix } = expected;
    468 
    469  // Check simulation menu button state
    470  await waitFor(
    471    () =>
    472      doc
    473        .getElementById(SIMULATION_MENU_BUTTON_ID)
    474        .classList.contains("active") === buttonActive
    475  );
    476  ok(
    477    true,
    478    `Simulation menu button contains ${buttonActive ? "active" : "base"} class.`
    479  );
    480 
    481  // Check simulation menu options states, if specified
    482  if (checkedOptionIndices) {
    483    const simulationMenuOptions = toolboxDoc
    484      .getElementById(SIMULATION_MENU_ID)
    485      .querySelectorAll(".menuitem");
    486 
    487    simulationMenuOptions.forEach((menuListItem, index) => {
    488      const isChecked = checkedOptionIndices.includes(index);
    489      const button = menuListItem.firstChild;
    490 
    491      is(
    492        button.getAttribute("aria-checked"),
    493        isChecked ? "true" : null,
    494        `Simulation option ${index} is ${isChecked ? "" : "not "}selected.`
    495      );
    496    });
    497  }
    498 
    499  const docShellColorMatrix = await SpecialPowers.spawn(
    500    gBrowser.selectedBrowser,
    501    [],
    502    () => content.window.docShell.getColorMatrix()
    503  );
    504  Assert.deepEqual(
    505    // The values we get from getColorMatrix have higher precisions than what is defined
    506    // in the simulation matrix in the accessibility panel
    507    docShellColorMatrix.map(v => v.toFixed(6)),
    508    colorMatrix,
    509    `docShell color matrix has expected value`
    510  );
    511 }
    512 
    513 /**
    514 * Focus accessibility properties tree in the a11y inspector sidebar. If focused for the
    515 * first time, the tree will select first rendered node as defult selection for keyboard
    516 * purposes.
    517 *
    518 * @param  {Document} doc  accessibility inspector panel document.
    519 */
    520 async function focusAccessibleProperties(doc) {
    521  const tree = doc.querySelector(".tree");
    522  if (doc.activeElement !== tree) {
    523    tree.focus();
    524    await BrowserTestUtils.waitForCondition(
    525      () => tree.querySelector(".node.focused"),
    526      "Tree selected."
    527    );
    528  }
    529 }
    530 
    531 /**
    532 * Select accessibility property in the sidebar.
    533 *
    534 * @param  {Document} doc  accessibility inspector panel document.
    535 * @param  {string} id     id of the property to be selected.
    536 * @return {DOMNode}       Node that corresponds to the selected accessibility property.
    537 */
    538 async function selectProperty(doc, id) {
    539  const win = doc.defaultView;
    540  let selected = false;
    541  let node;
    542 
    543  await focusAccessibleProperties(doc);
    544  await BrowserTestUtils.waitForCondition(() => {
    545    node = doc.getElementById(`${id}`);
    546    if (node) {
    547      if (selected) {
    548        return node.firstChild.classList.contains("focused");
    549      }
    550 
    551      AccessibilityUtils.setEnv({
    552        // Keyboard navigation is handled on the container level using arrow
    553        // keys.
    554        nonNegativeTabIndexRule: false,
    555      });
    556      EventUtils.sendMouseEvent({ type: "click" }, node, win);
    557      AccessibilityUtils.resetEnv();
    558      selected = true;
    559    } else {
    560      const tree = doc.querySelector(".tree");
    561      tree.scrollTop = parseFloat(win.getComputedStyle(tree).height);
    562    }
    563 
    564    return false;
    565  });
    566 
    567  return node;
    568 }
    569 
    570 /**
    571 * Select tree row.
    572 *
    573 * @param  {document} doc       panel documnent.
    574 * @param  {number}   rowNumber number of the row/tree node to be selected.
    575 */
    576 function selectRow(doc, rowNumber) {
    577  info(`Selecting row ${rowNumber}.`);
    578  AccessibilityUtils.setEnv({
    579    // Keyboard navigation is handled on the container level using arrow keys.
    580    nonNegativeTabIndexRule: false,
    581  });
    582  EventUtils.sendMouseEvent(
    583    { type: "click" },
    584    doc.querySelectorAll(".treeRow")[rowNumber],
    585    doc.defaultView
    586  );
    587  AccessibilityUtils.resetEnv();
    588 }
    589 
    590 /**
    591 * Toggle an expandable tree row.
    592 *
    593 * @param  {document} doc       panel documnent.
    594 * @param  {number}   rowNumber number of the row/tree node to be toggled.
    595 */
    596 async function toggleRow(doc, rowNumber) {
    597  const win = doc.defaultView;
    598  const row = doc.querySelectorAll(".treeRow")[rowNumber];
    599  const twisty = row.querySelector(".theme-twisty");
    600  const expected = !twisty.classList.contains("open");
    601 
    602  info(`${expected ? "Expanding" : "Collapsing"} row ${rowNumber}.`);
    603 
    604  AccessibilityUtils.setEnv({
    605    // We intentionally remove the twisty from the accessibility tree in the
    606    // TreeView component and handle keyboard navigation using the arrow keys.
    607    mustHaveAccessibleRule: false,
    608  });
    609  EventUtils.sendMouseEvent({ type: "click" }, twisty, win);
    610  AccessibilityUtils.resetEnv();
    611  await BrowserTestUtils.waitForCondition(
    612    () =>
    613      !twisty.classList.contains("devtools-throbber") &&
    614      expected === twisty.classList.contains("open"),
    615    "Twisty updated."
    616  );
    617 }
    618 
    619 /**
    620 * Toggle a specific menu item based on its index in the menu.
    621 *
    622 * @param  {document} toolboxDoc
    623 *         toolbox document.
    624 * @param  {document} doc
    625 *         panel document.
    626 * @param  {string} menuId
    627 *         The id of the menu (menuId passed to the MenuButton component)
    628 * @param  {number}   menuItemIndex
    629 *         index of the menu item to be toggled.
    630 */
    631 async function toggleMenuItem(doc, toolboxDoc, menuId, menuItemIndex) {
    632  const toolboxWin = toolboxDoc.defaultView;
    633  const panelWin = doc.defaultView;
    634 
    635  const menuButton = doc.querySelectorAll(".toolbar-menu-button")[
    636    MENU_INDEXES[menuId]
    637  ];
    638  ok(menuButton, "Expected menu button");
    639 
    640  const menuEl = toolboxDoc.getElementById(menuId);
    641  const menuItem = menuEl.querySelectorAll(".command")[menuItemIndex];
    642  ok(menuItem, "Expected menu item");
    643 
    644  const expected =
    645    menuItem.getAttribute("aria-checked") === "true" ? null : "true";
    646 
    647  // Make the menu visible first.
    648  const onPopupShown = new Promise(r =>
    649    toolboxDoc.addEventListener("popupshown", r, { once: true })
    650  );
    651  EventUtils.synthesizeMouseAtCenter(menuButton, {}, panelWin);
    652  await onPopupShown;
    653  const boundingRect = menuItem.getBoundingClientRect();
    654  ok(
    655    boundingRect.width > 0 && boundingRect.height > 0,
    656    "Menu item is visible."
    657  );
    658 
    659  EventUtils.synthesizeMouseAtCenter(menuItem, {}, toolboxWin);
    660  await BrowserTestUtils.waitForCondition(
    661    () => expected === menuItem.getAttribute("aria-checked"),
    662    "Menu item updated."
    663  );
    664 }
    665 
    666 /**
    667 * Open the simulation menu.
    668 *
    669 * @param {document} doc
    670 *        panel document.
    671 * @param {document} toolboxDoc
    672 *        toolbox document.
    673 */
    674 async function openSimulationMenu(doc, toolboxDoc) {
    675  doc.getElementById(SIMULATION_MENU_BUTTON_ID).click();
    676 
    677  await BrowserTestUtils.waitForCondition(() =>
    678    toolboxDoc
    679      .getElementById(SIMULATION_MENU_ID)
    680      .classList.contains("tooltip-visible")
    681  );
    682 }
    683 
    684 /**
    685 * Toggle an option in the the simulation menu.
    686 *
    687 * @param {document} toolboxDoc
    688 *        toolbox document.
    689 * @param {number}
    690 *        index of the option in the menu
    691 */
    692 async function toggleSimulationOption(toolboxDoc, optionIndex) {
    693  const simulationMenu = toolboxDoc.getElementById(SIMULATION_MENU_ID);
    694  const menuItemButton =
    695    simulationMenu.querySelectorAll(".menuitem")[optionIndex].firstChild;
    696  const previousAriaCheckedValue = menuItemButton.getAttribute("aria-checked");
    697  menuItemButton.click();
    698 
    699  // wait for the button state to be updated
    700  await waitFor(
    701    () =>
    702      menuItemButton.getAttribute("aria-checked") !== previousAriaCheckedValue
    703  );
    704 
    705  // wait for the menu to be hidden
    706  await BrowserTestUtils.waitForCondition(
    707    () => !simulationMenu.classList.contains("tooltip-visible")
    708  );
    709 }
    710 
    711 async function findAccessibleFor(
    712  {
    713    toolbox: { target },
    714    panel: {
    715      accessibilityProxy: {
    716        accessibilityFront: { accessibleWalkerFront },
    717      },
    718    },
    719  },
    720  selector
    721 ) {
    722  const domWalker = (await target.getFront("inspector")).walker;
    723  const node = await domWalker.querySelector(domWalker.rootNode, selector);
    724  return accessibleWalkerFront.getAccessibleFor(node);
    725 }
    726 
    727 async function selectAccessibleForNode(env, selector) {
    728  const { panel, win } = env;
    729  const front = await findAccessibleFor(env, selector);
    730  const { EVENTS } = win;
    731  const onSelected = win.once(EVENTS.NEW_ACCESSIBLE_FRONT_SELECTED);
    732  panel.selectAccessible(front);
    733  await onSelected;
    734 }
    735 
    736 /**
    737 * Iterate over setups/tests structure and test the state of the
    738 * accessibility panel.
    739 *
    740 * @param  {JSON}   tests
    741 *         test data that has the format of:
    742 *         {
    743 *           desc     {String}    description for better logging
    744 *           setup    {Function}  An optional setup that needs to be
    745 *                                performed before the state of the
    746 *                                tree and the sidebar can be checked
    747 *           expected {JSON}      An expected states for parts of
    748 *                                accessibility panel:
    749 *            - tree:                 state of the accessibility tree widget
    750 *            - sidebar:              state of the accessibility panel sidebar
    751 *            - audit:                state of the audit redux state of the
    752 *                                    panel
    753 *            - toolbarPrefValues:    state of the accessibility panel
    754 *                                    toolbar prefs and corresponding user
    755 *                                    preferences.
    756 *            - activeToolbarFilters: state of the accessibility panel
    757 *                                    toolbar filters.
    758 *         }
    759 * @param  {object} env
    760 *         contains all relevant environment objects (same structure as the
    761 *         return value of 'addTestTab' funciton)
    762 */
    763 async function runA11yPanelTests(tests, env) {
    764  for (const { desc, setup, expected } of tests) {
    765    info(desc);
    766 
    767    if (setup) {
    768      await setup(env);
    769    }
    770 
    771    const {
    772      tree,
    773      sidebar,
    774      audit,
    775      toolbarPrefValues,
    776      activeToolbarFilters,
    777      simulation,
    778    } = expected;
    779    if (tree) {
    780      await checkTreeState(env.doc, tree);
    781    }
    782 
    783    if (sidebar) {
    784      await checkSidebarState(env.store, sidebar);
    785    }
    786 
    787    if (activeToolbarFilters) {
    788      await checkToolbarState(env.doc, activeToolbarFilters);
    789    }
    790 
    791    if (toolbarPrefValues) {
    792      await checkToolbarPrefsState(env.doc, toolbarPrefValues, env.store);
    793    }
    794 
    795    if (typeof audit !== "undefined") {
    796      await checkAuditState(env.store, audit);
    797    }
    798 
    799    if (simulation) {
    800      await checkSimulationState(env.doc, env.toolbox.doc, simulation);
    801    }
    802  }
    803 }
    804 
    805 /**
    806 * Build a valid URL from an HTML snippet.
    807 *
    808 * @param  {string}  uri      HTML snippet
    809 * @param  {object}  options  options for the test
    810 * @return {string}     built URL
    811 */
    812 function buildURL(uri, options = {}) {
    813  if (options.remoteIframe) {
    814    const srcURL = new URL(`http://example.net/document-builder.sjs`);
    815    srcURL.searchParams.append(
    816      "html",
    817      `<html>
    818        <head>
    819          <meta charset="utf-8"/>
    820          <title>Accessibility Panel Test (OOP)</title>
    821        </head>
    822        <body>${uri}</body>
    823      </html>`
    824    );
    825    uri = `<iframe title="Accessibility Panel Test (OOP)" src="${srcURL.href}"/>`;
    826  }
    827 
    828  return `data:text/html;charset=UTF-8,${encodeURIComponent(uri)}`;
    829 }
    830 
    831 /**
    832 * Add a test task based on the test structure and a test URL.
    833 *
    834 * @param  {JSON}   tests    test data that has the format of:
    835 *                    {
    836 *                      desc     {string}    description for better logging
    837 *                      setup   {Function}   An optional setup that needs to be
    838 *                                           performed before the state of the
    839 *                                           tree and the sidebar can be checked
    840 *                      expected {JSON}      An expected states for the tree and
    841 *                                           the sidebar
    842 *                    }
    843 * @param {string}  uri      test URL
    844 * @param {string}  msg      a message that is printed for the test
    845 * @param {object}  options  options for the test
    846 */
    847 function addA11yPanelTestsTask(tests, uri, msg, options) {
    848  addA11YPanelTask(msg, uri, env => runA11yPanelTests(tests, env), options);
    849 }
    850 
    851 /**
    852 * Borrowed from framework's shared head. Close toolbox, completely disable
    853 * accessibility and remove the tab.
    854 *
    855 * @param  {Tab}
    856 *         tab The tab to close.
    857 * @return {Promise}
    858 *         Resolves when the toolbox and tab have been destroyed and closed.
    859 */
    860 async function closeTabToolboxAccessibility(tab = gBrowser.selectedTab) {
    861  if (gDevTools.hasToolboxForTab(tab)) {
    862    await gDevTools.closeToolboxForTab(tab);
    863  }
    864 
    865  await shutdownAccessibility(gBrowser.getBrowserForTab(tab));
    866  await removeTab(tab);
    867  await new Promise(resolve => setTimeout(resolve, 0));
    868 }
    869 
    870 /**
    871 * A wrapper function around add_task that sets up the test environment, runs
    872 * the test and then disables accessibility tools.
    873 *
    874 * @param {string}   msg    a message that is printed for the test
    875 * @param {string}   uri    absolute test URL or HTML snippet
    876 * @param {Function} task   task function containing the tests.
    877 * @param {object}   options  options for the test
    878 */
    879 function addA11YPanelTask(msg, uri, task, options = {}) {
    880  add_task(async function a11YPanelTask() {
    881    info(msg);
    882 
    883    const env = await addTestTab(
    884      uri.startsWith("http") ? uri : buildURL(uri, options)
    885    );
    886    await task(env);
    887    await closeTabToolboxAccessibility(env.tab);
    888  });
    889 }