tor-browser

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

shared-head.js (36052B)


      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/common.js */
      8 /* import-globals-from ../mochitest/layout.js */
      9 /* import-globals-from ../mochitest/promisified-events.js */
     10 
     11 /* exported Logger, MOCHITESTS_DIR, invokeSetAttribute, invokeFocus,
     12            invokeSetStyle, getAccessibleDOMNodeID, getAccessibleTagName,
     13            addAccessibleTask, findAccessibleChildByID, isDefunct,
     14            CURRENT_CONTENT_DIR, loadScripts, loadContentScripts, snippetToURL,
     15            Cc, Cu, arrayFromChildren, forceGC, contentSpawnMutation,
     16            DEFAULT_IFRAME_ID, DEFAULT_IFRAME_DOC_BODY_ID, invokeContentTask,
     17            matchContentDoc, currentContentDoc, getContentDPR,
     18            waitForImageMap, getContentBoundsForDOMElm, untilCacheIs,
     19            untilCacheOk, testBoundsWithContent, waitForContentPaint,
     20            runPython */
     21 
     22 const CURRENT_FILE_DIR = "/browser/accessible/tests/browser/";
     23 
     24 /**
     25 * Current browser test directory path used to load subscripts.
     26 */
     27 const CURRENT_DIR = `chrome://mochitests/content${CURRENT_FILE_DIR}`;
     28 /**
     29 * A11y mochitest directory where we find common files used in both browser and
     30 * plain tests.
     31 */
     32 const MOCHITESTS_DIR =
     33  "chrome://mochitests/content/a11y/accessible/tests/mochitest/";
     34 /**
     35 * A base URL for test files used in content.
     36 */
     37 // eslint-disable-next-line @microsoft/sdl/no-insecure-url
     38 const CURRENT_CONTENT_DIR = `http://example.com${CURRENT_FILE_DIR}`;
     39 
     40 const LOADED_CONTENT_SCRIPTS = new Map();
     41 
     42 const DEFAULT_CONTENT_DOC_BODY_ID = "body";
     43 const DEFAULT_IFRAME_ID = "default-iframe-id";
     44 const DEFAULT_IFRAME_DOC_BODY_ID = "default-iframe-body-id";
     45 
     46 const HTML_MIME_TYPE = "text/html";
     47 const XHTML_MIME_TYPE = "application/xhtml+xml";
     48 
     49 function loadHTMLFromFile(path) {
     50  // Load the HTML to return in the response from file.
     51  // Since it's relative to the cwd of the test runner, we start there and
     52  // append to get to the actual path of the file.
     53  const testHTMLFile = Services.dirsvc.get("CurWorkD", Ci.nsIFile);
     54  const dirs = path.split("/");
     55  for (let i = 0; i < dirs.length; i++) {
     56    testHTMLFile.append(dirs[i]);
     57  }
     58 
     59  const testHTMLFileStream = Cc[
     60    "@mozilla.org/network/file-input-stream;1"
     61  ].createInstance(Ci.nsIFileInputStream);
     62  testHTMLFileStream.init(testHTMLFile, -1, 0, 0);
     63  const testHTML = NetUtil.readInputStreamToString(
     64    testHTMLFileStream,
     65    testHTMLFileStream.available()
     66  );
     67 
     68  return testHTML;
     69 }
     70 
     71 let gIsIframe = false;
     72 let gIsRemoteIframe = false;
     73 
     74 function currentContentDoc() {
     75  return gIsIframe ? DEFAULT_IFRAME_DOC_BODY_ID : DEFAULT_CONTENT_DOC_BODY_ID;
     76 }
     77 
     78 /**
     79 * Accessible event match criteria based on the id of the current document
     80 * accessible in test.
     81 *
     82 * @param   {nsIAccessibleEvent}  event
     83 *        Accessible event to be tested for a match.
     84 *
     85 * @return  {boolean}
     86 *          True if accessible event's accessible object ID matches current
     87 *          document accessible ID.
     88 */
     89 function matchContentDoc(event) {
     90  return getAccessibleDOMNodeID(event.accessible) === currentContentDoc();
     91 }
     92 
     93 /**
     94 * Used to dump debug information.
     95 */
     96 let Logger = {
     97  /**
     98   * Set up this variable to dump log messages into console.
     99   */
    100  dumpToConsole: false,
    101 
    102  /**
    103   * Set up this variable to dump log messages into error console.
    104   */
    105  dumpToAppConsole: false,
    106 
    107  /**
    108   * Return true if dump is enabled.
    109   */
    110  get enabled() {
    111    return this.dumpToConsole || this.dumpToAppConsole;
    112  },
    113 
    114  /**
    115   * Dump information into console if applicable.
    116   */
    117  log(msg) {
    118    if (this.enabled) {
    119      this.logToConsole(msg);
    120      this.logToAppConsole(msg);
    121    }
    122  },
    123 
    124  /**
    125   * Log message to console.
    126   */
    127  logToConsole(msg) {
    128    if (this.dumpToConsole) {
    129      dump(`\n${msg}\n`);
    130    }
    131  },
    132 
    133  /**
    134   * Log message to error console.
    135   */
    136  logToAppConsole(msg) {
    137    if (this.dumpToAppConsole) {
    138      Services.console.logStringMessage(`${msg}`);
    139    }
    140  },
    141 };
    142 
    143 /**
    144 * Asynchronously set or remove content element's attribute (in content process
    145 * if e10s is enabled).
    146 *
    147 * @param  {object}  browser  current "tabbrowser" element
    148 * @param  {string}  id       content element id
    149 * @param  {string}  attr     attribute name
    150 * @param  {string?} value    optional attribute value, if not present, remove
    151 *                            attribute
    152 * @return {Promise}          promise indicating that attribute is set/removed
    153 */
    154 function invokeSetAttribute(browser, id, attr, value = null) {
    155  if (value !== null) {
    156    Logger.log(`Setting ${attr} attribute to ${value} for node with id: ${id}`);
    157  } else {
    158    Logger.log(`Removing ${attr} attribute from node with id: ${id}`);
    159  }
    160 
    161  return invokeContentTask(
    162    browser,
    163    [id, attr, value],
    164    (contentId, contentAttr, contentValue) => {
    165      let elm = content.document.getElementById(contentId);
    166      if (contentValue !== null) {
    167        elm.setAttribute(contentAttr, contentValue);
    168      } else {
    169        elm.removeAttribute(contentAttr);
    170      }
    171    }
    172  );
    173 }
    174 
    175 /**
    176 * Asynchronously set or remove content element's style (in content process if
    177 * e10s is enabled, or in fission process if fission is enabled and a fission
    178 * frame is present).
    179 *
    180 * @param  {object}  browser  current "tabbrowser" element
    181 * @param  {string}  id       content element id
    182 * @param  {string}  aStyle   style property name
    183 * @param  {string?} aValue   optional style property value, if not present,
    184 *                            remove style
    185 * @return {Promise}          promise indicating that style is set/removed
    186 */
    187 function invokeSetStyle(browser, id, style, value) {
    188  if (value) {
    189    Logger.log(`Setting ${style} style to ${value} for node with id: ${id}`);
    190  } else {
    191    Logger.log(`Removing ${style} style from node with id: ${id}`);
    192  }
    193 
    194  return invokeContentTask(
    195    browser,
    196    [id, style, value],
    197    (contentId, contentStyle, contentValue) => {
    198      const elm = content.document.getElementById(contentId);
    199      if (contentValue) {
    200        elm.style[contentStyle] = contentValue;
    201      } else {
    202        delete elm.style[contentStyle];
    203      }
    204    }
    205  );
    206 }
    207 
    208 /**
    209 * Asynchronously set focus on a content element (in content process if e10s is
    210 * enabled, or in fission process if fission is enabled and a fission frame is
    211 * present).
    212 *
    213 * @param  {object}  browser  current "tabbrowser" element
    214 * @param  {string}  id       content element id
    215 * @return {Promise} promise  indicating that focus is set
    216 */
    217 function invokeFocus(browser, id) {
    218  Logger.log(`Setting focus on a node with id: ${id}`);
    219 
    220  return invokeContentTask(browser, [id], contentId => {
    221    const elm = content.document.getElementById(contentId);
    222    if (elm.editor) {
    223      elm.selectionStart = elm.selectionEnd = elm.value.length;
    224    }
    225 
    226    elm.focus();
    227  });
    228 }
    229 
    230 /**
    231 * Get DPR for a specific content window.
    232 *
    233 * @param  browser
    234 *         Browser for which we want its content window's DPR reported.
    235 *
    236 * @return {Promise}
    237 *         Promise with the value that resolves to the devicePixelRatio of the
    238 *         content window of a given browser.
    239 */
    240 function getContentDPR(browser) {
    241  return invokeContentTask(browser, [], () => content.window.devicePixelRatio);
    242 }
    243 
    244 /**
    245 * Asynchronously perform a task in content (in content process if e10s is
    246 * enabled, or in fission process if fission is enabled and a fission frame is
    247 * present).
    248 *
    249 * @param  {object}    browser  current "tabbrowser" element
    250 * @param  {Array}     args     arguments for the content task
    251 * @param  {Function}  task     content task function
    252 *
    253 * @return {Promise} promise  indicating that content task is complete
    254 */
    255 function invokeContentTask(browser, args, task) {
    256  return SpecialPowers.spawn(
    257    browser,
    258    [DEFAULT_IFRAME_ID, task.toString(), ...args],
    259    (iframeId, contentTask, ...contentArgs) => {
    260      // eslint-disable-next-line no-eval
    261      const runnableTask = eval(`
    262      (() => {
    263        return (${contentTask});
    264      })();`);
    265      const frame = content.document.getElementById(iframeId);
    266 
    267      return frame
    268        ? SpecialPowers.spawn(frame, contentArgs, runnableTask)
    269        : runnableTask.call(this, ...contentArgs);
    270    }
    271  );
    272 }
    273 
    274 /**
    275 * Compare process ID's between the top level content process and possible
    276 * remote/local iframe proccess.
    277 *
    278 * @param {object}  browser
    279 *        Top level browser object for a tab.
    280 * @param {boolean} isRemote
    281 *        Indicates if we expect the iframe content process to be remote or not.
    282 */
    283 async function comparePIDs(browser, isRemote) {
    284  function getProcessID() {
    285    return Services.appinfo.processID;
    286  }
    287 
    288  const contentPID = await SpecialPowers.spawn(browser, [], getProcessID);
    289  const iframePID = await invokeContentTask(browser, [], getProcessID);
    290  is(
    291    isRemote,
    292    contentPID !== iframePID,
    293    isRemote
    294      ? "Remote IFRAME is in a different process."
    295      : "IFRAME is in the same process."
    296  );
    297 }
    298 
    299 /**
    300 * Load a list of scripts into the test
    301 *
    302 * @param {Array} scripts  a list of scripts to load
    303 */
    304 function loadScripts(...scripts) {
    305  for (let script of scripts) {
    306    let path =
    307      typeof script === "string"
    308        ? `${CURRENT_DIR}${script}`
    309        : `${script.dir}${script.name}`;
    310    Services.scriptloader.loadSubScript(path, this);
    311  }
    312 }
    313 
    314 /**
    315 * Load a list of scripts into target's content.
    316 *
    317 * @param {object} target
    318 *        target for loading scripts into
    319 * @param {Array}  scripts
    320 *        a list of scripts to load into content
    321 */
    322 async function loadContentScripts(target, ...scripts) {
    323  for (let { script, symbol } of scripts) {
    324    let contentScript = `${CURRENT_DIR}${script}`;
    325    let loadedScriptSet = LOADED_CONTENT_SCRIPTS.get(contentScript);
    326    if (!loadedScriptSet) {
    327      loadedScriptSet = new WeakSet();
    328      LOADED_CONTENT_SCRIPTS.set(contentScript, loadedScriptSet);
    329    } else if (loadedScriptSet.has(target)) {
    330      continue;
    331    }
    332 
    333    await SpecialPowers.spawn(
    334      target,
    335      [contentScript, symbol],
    336      async (_contentScript, importSymbol) => {
    337        let module = ChromeUtils.importESModule(_contentScript);
    338        content.window[importSymbol] = module[importSymbol];
    339      }
    340    );
    341    loadedScriptSet.add(target);
    342  }
    343 }
    344 
    345 function attrsToString(attrs) {
    346  return Object.entries(attrs)
    347    .map(([attr, value]) => `${attr}=${JSON.stringify(value)}`)
    348    .join(" ");
    349 }
    350 
    351 function wrapWithIFrame(doc, options = {}) {
    352  let src;
    353  let { iframeAttrs = {}, iframeDocBodyAttrs = {} } = options;
    354  iframeDocBodyAttrs = {
    355    id: DEFAULT_IFRAME_DOC_BODY_ID,
    356    ...iframeDocBodyAttrs,
    357  };
    358  if (options.contentSetup) {
    359    // Hide the body initially so we can ensure that any changes made by
    360    // contentSetup are included when the body's content is initially added to
    361    // the accessibility tree. Use `hidden` instead of `aria-hidden` because the
    362    // latter is ignored when applied to top level docs/<body> elements and we
    363    // want to remain consistent with our handling for non-iframe docs.
    364    iframeDocBodyAttrs.hidden = true;
    365  }
    366  if (options.remoteIframe) {
    367    // eslint-disable-next-line @microsoft/sdl/no-insecure-url
    368    const srcURL = new URL(`http://example.net/document-builder.sjs`);
    369    if (doc.endsWith("html")) {
    370      srcURL.searchParams.append("file", `${CURRENT_FILE_DIR}${doc}`);
    371    } else {
    372      // document-builder.sjs can't handle non-ASCII characters. Convert them
    373      // to HTML character entities; e.g. &#8226;.
    374      doc = doc.replace(/[\u00A0-\u2666]/g, c => `&#${c.charCodeAt(0)}`);
    375      srcURL.searchParams.append(
    376        "html",
    377        `<!doctype html>
    378        <html>
    379          <head>
    380            <meta charset="utf-8"/>
    381            <title>Accessibility Fission Test</title>
    382          </head>
    383          <body ${attrsToString(iframeDocBodyAttrs)}>${doc}</body>
    384        </html>`
    385      );
    386    }
    387    src = srcURL.href;
    388  } else {
    389    const mimeType = doc.endsWith("xhtml") ? XHTML_MIME_TYPE : HTML_MIME_TYPE;
    390    if (doc.endsWith("html")) {
    391      doc = loadHTMLFromFile(`${CURRENT_FILE_DIR}${doc}`);
    392      doc = doc.replace(
    393        /<body[.\s\S]*?>/,
    394        `<body ${attrsToString(iframeDocBodyAttrs)}>`
    395      );
    396    } else {
    397      doc = `<!doctype html>
    398      <body ${attrsToString(iframeDocBodyAttrs)}>${doc}</body>`;
    399    }
    400 
    401    src = `data:${mimeType};charset=utf-8,${encodeURIComponent(doc)}`;
    402  }
    403 
    404  if (options.urlSuffix) {
    405    src += options.urlSuffix;
    406  }
    407 
    408  iframeAttrs = {
    409    id: DEFAULT_IFRAME_ID,
    410    src,
    411    ...iframeAttrs,
    412  };
    413 
    414  return `<iframe ${attrsToString(iframeAttrs)}/>`;
    415 }
    416 
    417 /**
    418 * Takes an HTML snippet or HTML doc url and returns an encoded URI for a full
    419 * document with the snippet or the URL as a source for the IFRAME.
    420 *
    421 * @param {string} doc
    422 *        a markup snippet or url.
    423 * @param {object} options (see options in addAccessibleTask).
    424 *
    425 * @return {string}
    426 *        a base64 encoded data url of the document container the snippet.
    427 */
    428 function snippetToURL(doc, options = {}) {
    429  const { contentDocBodyAttrs = {} } = options;
    430  const attrs = {
    431    id: DEFAULT_CONTENT_DOC_BODY_ID,
    432    ...contentDocBodyAttrs,
    433  };
    434 
    435  if (gIsIframe) {
    436    doc = wrapWithIFrame(doc, options);
    437  } else if (options.contentSetup) {
    438    // Hide the body initially so we can ensure that any changes made by
    439    // contentSetup are included when the body's content is initially added to
    440    // the accessibility tree. Use `hidden` instead of `aria-hidden` because the
    441    // latter is ignored when applied to top level docs/<body> elements.
    442    attrs.hidden = true;
    443  }
    444 
    445  const encodedDoc = encodeURIComponent(
    446    `<!doctype html>
    447    <html>
    448      <head>
    449        <meta charset="utf-8"/>
    450        <title>Accessibility Test</title>
    451      </head>
    452      <body ${attrsToString(attrs)}>${doc}</body>
    453    </html>`
    454  );
    455 
    456  let url = `data:text/html;charset=utf-8,${encodedDoc}`;
    457  if (!gIsIframe && options.urlSuffix) {
    458    url += options.urlSuffix;
    459  }
    460  return url;
    461 }
    462 
    463 const CacheDomain = {
    464  None: 0,
    465  NameAndDescription: 0x1 << 0,
    466  Value: 0x1 << 1,
    467  Bounds: 0x1 << 2,
    468  Resolution: 0x1 << 3,
    469  Text: 0x1 << 4,
    470  DOMNodeIDAndClass: 0x1 << 5,
    471  State: 0x1 << 6,
    472  GroupInfo: 0x1 << 7,
    473  Actions: 0x1 << 8,
    474  Style: 0x1 << 9,
    475  TransformMatrix: 0x1 << 10,
    476  ScrollPosition: 0x1 << 11,
    477  Table: 0x1 << 12,
    478  TextOffsetAttributes: 0x1 << 13,
    479  Viewport: 0x1 << 14,
    480  ARIA: 0x1 << 15,
    481  Relations: 0x1 << 16,
    482  InnerHTML: 0x1 << 17,
    483  TextBounds: 0x1 << 18,
    484  All: ~0x0,
    485 };
    486 
    487 function accessibleTask(doc, task, options = {}) {
    488  const wrapped = async function () {
    489    let cacheDomains;
    490    if (!("cacheDomains" in options)) {
    491      cacheDomains = CacheDomain.All;
    492    } else {
    493      // The DOMNodeIDAndClass domain is required for the tests to initialize.
    494      cacheDomains = options.cacheDomains | CacheDomain.DOMNodeIDAndClass;
    495    }
    496 
    497    // Set the required cache domains for the test. Note that this also
    498    // instantiates the accessibility service if it hasn't been already, since
    499    // gAccService is defined lazily.
    500    gAccService.setCacheDomains(cacheDomains);
    501 
    502    gIsRemoteIframe = options.remoteIframe;
    503    gIsIframe = options.iframe || gIsRemoteIframe;
    504    const urlSuffix = options.urlSuffix || "";
    505    let url;
    506    if (options.chrome && doc.endsWith("html")) {
    507      // Load with a chrome:// URL so this loads as a chrome document in the
    508      // parent process.
    509      url = `${CURRENT_DIR}${doc}${urlSuffix}`;
    510    } else if (doc.endsWith("html") && !gIsIframe) {
    511      url = `${CURRENT_CONTENT_DIR}${doc}${urlSuffix}`;
    512    } else {
    513      url = snippetToURL(doc, options);
    514    }
    515 
    516    registerCleanupFunction(() => {
    517      // XXX Bug 1906779: This will run once for each call to addAccessibleTask,
    518      // but only after the entire test file has completed. This doesn't make
    519      // sense and almost certainly wasn't the intent.
    520      for (let observer of Services.obs.enumerateObservers(
    521        "accessible-event"
    522      )) {
    523        Services.obs.removeObserver(observer, "accessible-event");
    524      }
    525    });
    526 
    527    let onContentDocLoad;
    528    if (!options.chrome) {
    529      onContentDocLoad = waitForEvent(
    530        EVENT_DOCUMENT_LOAD_COMPLETE,
    531        DEFAULT_CONTENT_DOC_BODY_ID
    532      );
    533    }
    534 
    535    let onIframeDocLoad;
    536    if (options.remoteIframe && !options.skipFissionDocLoad) {
    537      onIframeDocLoad = waitForEvent(
    538        EVENT_DOCUMENT_LOAD_COMPLETE,
    539        DEFAULT_IFRAME_DOC_BODY_ID
    540      );
    541    }
    542 
    543    await BrowserTestUtils.withNewTab(
    544      {
    545        gBrowser,
    546        // For chrome, we need a non-remote browser.
    547        opening: !options.chrome
    548          ? url
    549          : () => {
    550              // Passing forceNotRemote: true still sets maychangeremoteness,
    551              // which will cause data: URIs to load remotely. There's no way to
    552              // avoid this with gBrowser or BrowserTestUtils. Therefore, we
    553              // load a blank document initially and replace it below.
    554              gBrowser.selectedTab = BrowserTestUtils.addTab(
    555                gBrowser,
    556                "about:blank",
    557                {
    558                  allowInheritPrincipal: true,
    559                  forceNotRemote: true,
    560                }
    561              );
    562            },
    563      },
    564      async function (browser) {
    565        registerCleanupFunction(() => {
    566          if (browser) {
    567            let tab = gBrowser.getTabForBrowser(browser);
    568            if (tab && !tab.closing && tab.linkedBrowser) {
    569              gBrowser.removeTab(tab);
    570            }
    571          }
    572        });
    573 
    574        if (options.chrome) {
    575          await SpecialPowers.pushPrefEnv({
    576            set: [["security.allow_unsafe_parent_loads", true]],
    577          });
    578          // Ensure this never becomes a remote browser.
    579          browser.removeAttribute("maychangeremoteness");
    580          // Now we can load our page without it becoming remote.
    581          browser.setAttribute("src", url);
    582        }
    583 
    584        await SimpleTest.promiseFocus(browser);
    585 
    586        if (options.chrome) {
    587          ok(!browser.isRemoteBrowser, "Not remote browser");
    588        } else if (Services.appinfo.browserTabsRemoteAutostart) {
    589          ok(browser.isRemoteBrowser, "Actually remote browser");
    590        }
    591 
    592        let docAccessible;
    593        if (options.chrome) {
    594          // Chrome documents don't fire DOCUMENT_LOAD_COMPLETE. Instead, wait
    595          // until we can get the DocAccessible and it doesn't have the busy
    596          // state.
    597          await BrowserTestUtils.waitForCondition(() => {
    598            docAccessible = getAccessible(browser.contentWindow.document);
    599            if (!docAccessible) {
    600              return false;
    601            }
    602            const state = {};
    603            docAccessible.getState(state, {});
    604            return !(state.value & STATE_BUSY);
    605          });
    606        } else {
    607          ({ accessible: docAccessible } = await onContentDocLoad);
    608        }
    609        // The test may want to access document methods/attributes such as URL
    610        // and browsingContext.
    611        docAccessible.QueryInterface(nsIAccessibleDocument);
    612        let iframeDocAccessible;
    613        if (gIsIframe) {
    614          if (!options.skipFissionDocLoad) {
    615            await comparePIDs(browser, options.remoteIframe);
    616            iframeDocAccessible = onIframeDocLoad
    617              ? (await onIframeDocLoad).accessible
    618              : findAccessibleChildByID(docAccessible, DEFAULT_IFRAME_ID)
    619                  .firstChild;
    620            iframeDocAccessible.QueryInterface(nsIAccessibleDocument);
    621          }
    622        }
    623 
    624        if (options.contentSetup) {
    625          info("Executing contentSetup");
    626          const ready = waitForEvent(EVENT_REORDER, currentContentDoc());
    627          await invokeContentTask(browser, [], options.contentSetup);
    628          // snippetToURL set hidden on the body. We now Remove hidden
    629          // and wait for a reorder on the body. This guarantees that any
    630          // changes made by contentSetup are included when the body's content
    631          // is initially added to the accessibility tree and that the
    632          // accessibility tree is up to date.
    633          await invokeContentTask(browser, [], () => {
    634            content.document.body.removeAttribute("hidden");
    635          });
    636          await ready;
    637          info("contentSetup done");
    638        }
    639        await loadContentScripts(browser, {
    640          script: "Common.sys.mjs",
    641          symbol: "CommonUtils",
    642        });
    643 
    644        await task(
    645          browser,
    646          iframeDocAccessible || docAccessible,
    647          iframeDocAccessible && docAccessible
    648        );
    649      }
    650    );
    651 
    652    if (gPythonSocket) {
    653      // Remove any globals set by Python code run in this test. We do this here
    654      // rather than using registerCleanupFunction because
    655      // registerCleanupFunction runs after all tests in the file, whereas we
    656      // need this to run after each task.
    657      await runPython(`__reset__`);
    658    }
    659  };
    660  // Propagate the name of the task function to our wrapper function so it shows
    661  // up in test run output. Suffix with the test type. For example:
    662  // 0:39.16 INFO Entering test bound testProtected_remoteIframe
    663  // Even if the name is empty, we still propagate it here to override the
    664  // implicit "wrapped" name derived from the assignment at the top of this
    665  // function.
    666  let name = task.name;
    667  if (name) {
    668    if (options.chrome) {
    669      name += "_chrome";
    670    } else if (options.iframe) {
    671      name += "_iframe";
    672    } else if (options.remoteIframe) {
    673      name += "_remoteIframe";
    674    } else {
    675      name += "_topLevel";
    676    }
    677  }
    678  // The "name" property of functions is not writable, but we can override that
    679  // using Object.defineProperty.
    680  Object.defineProperty(wrapped, "name", { value: name });
    681  return wrapped;
    682 }
    683 
    684 /**
    685 * A wrapper around browser test add_task that triggers an accessible test task
    686 * as a new browser test task with given document, data URL or markup snippet.
    687 *
    688 * @param  {string} doc
    689 *         URL (relative to current directory) or data URL or markup snippet
    690 *         that is used to test content with
    691 * @param  {Function|AsyncFunction} task
    692 *         a generator or a function with tests to run
    693 * @param  {null | object} options
    694 *         Options for running accessibility test tasks:
    695 *         - {Boolean} topLevel
    696 *           Flag to run the test with content in the top level content process.
    697 *           Default is true.
    698 *         - {Boolean} chrome
    699 *           Flag to run the test with content as a chrome document in the
    700 *           parent process. Default is false. Although url can be a markup
    701 *           snippet, a snippet cannot be used for XUL content. To load XUL,
    702 *           specify a relative URL to a XUL document. In that case, toplevel
    703 *           should usually be set to false, since XUL documents don't work in
    704 *           content processes.
    705 *         - {Boolean} iframe
    706 *           Flag to run the test with content wrapped in an iframe. Default is
    707 *           false.
    708 *         - {Boolean} remoteIframe
    709 *           Flag to run the test with content wrapped in a remote iframe.
    710 *           Default is false.
    711 *         - {Object} iframeAttrs
    712 *           A map of attribute/value pairs to be applied to IFRAME element.
    713 *         - {Boolean} skipFissionDocLoad
    714 *           If true, the test will not wait for iframe document document
    715 *           loaded event (useful for when IFRAME is initially hidden).
    716 *         - {Object} contentDocBodyAttrs
    717 *           a set of attributes to be applied to a top level content document
    718 *           body
    719 *         - {Object} iframeDocBodyAttrs
    720 *           a set of attributes to be applied to a iframe content document body
    721 *         - {String} urlSuffix
    722 *           String to append to the document URL. For example, this could be
    723 *           "#test" to scroll to the "test" id in the document.
    724 *         - {CacheDomain} cacheDomains
    725 *           The set of cache domains that should be present at the start of the
    726 *           test. If not set, all cache domains will be present.
    727 *         - {Function|AsyncFunction} contentSetup
    728 *           An optional task to run to set up the content document before the
    729 *           test starts. If this test is to be run as a chrome document in the
    730 *           parent process (chrome: true), This should be used instead of an
    731 *           inline <script> element in the test snippet, since inline script is
    732 *           not allowed in such documents. This task is ultimately executed
    733 *           using SpecialPowers.spawn. Any updates to the content within the
    734 *           body will be included when the content is initially added to the
    735 *           accessibility tree. The accessibility tree is guaranteed to be up
    736 *           to date when the test starts. This will not work correctly for
    737 *           changes to the html or body elements themselves. Note that you will
    738 *           need to define this exactly as follows:
    739 *           contentSetup: async function contentSetup() { ... }
    740 *           async contentSetup() will fail when the task is serialized.
    741 *           contentSetup: async function() will be changed to
    742 *           async contentSetup() by the linter and likewise fail.
    743 */
    744 function addAccessibleTask(doc, task, options = {}) {
    745  const {
    746    topLevel = true,
    747    chrome = false,
    748    iframe = false,
    749    remoteIframe = false,
    750  } = options;
    751  if (topLevel) {
    752    add_task(
    753      accessibleTask(doc, task, {
    754        ...options,
    755        chrome: false,
    756        iframe: false,
    757        remoteIframe: false,
    758      })
    759    );
    760  }
    761 
    762  if (chrome) {
    763    add_task(
    764      accessibleTask(doc, task, {
    765        ...options,
    766        topLevel: false,
    767        iframe: false,
    768        remoteIframe: false,
    769      })
    770    );
    771  }
    772 
    773  if (iframe) {
    774    add_task(
    775      accessibleTask(doc, task, {
    776        ...options,
    777        topLevel: false,
    778        chrome: false,
    779        remoteIframe: false,
    780      })
    781    );
    782  }
    783 
    784  if (gFissionBrowser && remoteIframe) {
    785    add_task(
    786      accessibleTask(doc, task, {
    787        ...options,
    788        topLevel: false,
    789        chrome: false,
    790        iframe: false,
    791      })
    792    );
    793  }
    794 }
    795 
    796 /**
    797 * Check if an accessible object has a defunct test.
    798 *
    799 * @param  {nsIAccessible}  accessible object to test defunct state for
    800 * @return {boolean}        flag indicating defunct state
    801 */
    802 function isDefunct(accessible) {
    803  let defunct = false;
    804  try {
    805    let extState = {};
    806    accessible.getState({}, extState);
    807    defunct = extState.value & Ci.nsIAccessibleStates.EXT_STATE_DEFUNCT;
    808  } catch (x) {
    809    defunct = true;
    810  } finally {
    811    if (defunct) {
    812      Logger.log(`Defunct accessible: ${prettyName(accessible)}`);
    813    }
    814  }
    815  return defunct;
    816 }
    817 
    818 /**
    819 * Get the DOM tag name for a given accessible.
    820 *
    821 * @param  {nsIAccessible}  accessible accessible
    822 * @return {string?}                   tag name of associated DOM node, or null.
    823 */
    824 function getAccessibleTagName(acc) {
    825  try {
    826    return acc.attributes.getStringProperty("tag");
    827  } catch (e) {
    828    return null;
    829  }
    830 }
    831 
    832 /**
    833 * Traverses the accessible tree starting from a given accessible as a root and
    834 * looks for an accessible that matches based on its DOMNode id.
    835 *
    836 * @param  {nsIAccessible}  accessible root accessible
    837 * @param  {string}         id         id to look up accessible for
    838 * @param  {Array?}         interfaces the interface or an array interfaces
    839 *                                     to query it/them from obtained accessible
    840 * @return {nsIAccessible?}            found accessible if any
    841 */
    842 function findAccessibleChildByID(accessible, id, interfaces) {
    843  if (getAccessibleDOMNodeID(accessible) === id) {
    844    return queryInterfaces(accessible, interfaces);
    845  }
    846  for (let i = 0; i < accessible.children.length; ++i) {
    847    let found = findAccessibleChildByID(accessible.getChildAt(i), id);
    848    if (found) {
    849      return queryInterfaces(found, interfaces);
    850    }
    851  }
    852  return null;
    853 }
    854 
    855 function queryInterfaces(accessible, interfaces) {
    856  if (!interfaces) {
    857    return accessible;
    858  }
    859 
    860  for (let iface of interfaces.filter(i => !(accessible instanceof i))) {
    861    try {
    862      accessible.QueryInterface(iface);
    863    } catch (e) {
    864      ok(false, "Can't query " + iface);
    865    }
    866  }
    867 
    868  return accessible;
    869 }
    870 
    871 function arrayFromChildren(accessible) {
    872  return Array.from({ length: accessible.childCount }, (c, i) =>
    873    accessible.getChildAt(i)
    874  );
    875 }
    876 
    877 /**
    878 * Force garbage collection.
    879 */
    880 function forceGC() {
    881  SpecialPowers.gc();
    882  SpecialPowers.forceShrinkingGC();
    883  SpecialPowers.forceCC();
    884  SpecialPowers.gc();
    885  SpecialPowers.forceShrinkingGC();
    886  SpecialPowers.forceCC();
    887 }
    888 
    889 /*
    890 * This function spawns a content task and awaits expected mutation events from
    891 * various content changes. It's good at catching events we did *not* expect. We
    892 * do this advancing the layout refresh to flush the relocations/insertions
    893 * queue.
    894 */
    895 async function contentSpawnMutation(browser, waitFor, func, args = []) {
    896  let onReorders = waitForEvents({ expected: waitFor.expected || [] });
    897  let unexpectedListener = new UnexpectedEvents(waitFor.unexpected || []);
    898 
    899  function tick() {
    900    // 100ms is an arbitrary positive number to advance the clock.
    901    // We don't need to advance the clock for a11y mutations, but other
    902    // tick listeners may depend on an advancing clock with each refresh.
    903    content.windowUtils.advanceTimeAndRefresh(100);
    904  }
    905 
    906  // This stops the refreh driver from doing its regular ticks, and leaves
    907  // us in control.
    908  await invokeContentTask(browser, [], tick);
    909 
    910  // Perform the tree mutation.
    911  await invokeContentTask(browser, args, func);
    912 
    913  // Do one tick to flush our queue (insertions, relocations, etc.)
    914  await invokeContentTask(browser, [], tick);
    915 
    916  let events = await onReorders;
    917 
    918  unexpectedListener.stop();
    919 
    920  // Go back to normal refresh driver ticks.
    921  await invokeContentTask(browser, [], function () {
    922    content.windowUtils.restoreNormalRefresh();
    923  });
    924 
    925  return events;
    926 }
    927 
    928 async function waitForImageMap(browser, accDoc, id = "imgmap") {
    929  let acc = findAccessibleChildByID(accDoc, id);
    930 
    931  if (!acc) {
    932    const onShow = waitForEvent(EVENT_SHOW, id);
    933    acc = (await onShow).accessible;
    934  }
    935 
    936  if (acc.firstChild) {
    937    return;
    938  }
    939 
    940  const onReorder = waitForEvent(EVENT_REORDER, id);
    941  // Wave over image map
    942  await invokeContentTask(browser, [id], contentId => {
    943    const { ContentTaskUtils } = ChromeUtils.importESModule(
    944      "resource://testing-common/ContentTaskUtils.sys.mjs"
    945    );
    946    const EventUtils = ContentTaskUtils.getEventUtils(content);
    947    EventUtils.synthesizeMouse(
    948      content.document.getElementById(contentId),
    949      10,
    950      10,
    951      { type: "mousemove" },
    952      content
    953    );
    954  });
    955  await onReorder;
    956 }
    957 
    958 async function getContentBoundsForDOMElm(browser, id) {
    959  return invokeContentTask(browser, [id], contentId => {
    960    const { Layout: LayoutUtils } = ChromeUtils.importESModule(
    961      "chrome://mochitests/content/browser/accessible/tests/browser/Layout.sys.mjs"
    962    );
    963 
    964    return LayoutUtils.getBoundsForDOMElm(contentId, content.document);
    965  });
    966 }
    967 
    968 const CACHE_WAIT_TIMEOUT_MS = 5000;
    969 
    970 /**
    971 * Wait for a predicate to be true after cache ticks.
    972 * This function takes two callbacks, the condition is evaluated
    973 * by calling the first callback with the arguments returned by the second.
    974 * This allows us to asynchronously return the arguments as a result if the condition
    975 * of the first callback is met, or if it times out. The returned arguments can then
    976 * be used to record a pass or fail in the test.
    977 */
    978 function untilCacheCondition(conditionFunc, argsFunc) {
    979  return new Promise(resolve => {
    980    let args = argsFunc();
    981    if (conditionFunc(...args)) {
    982      resolve(args);
    983      return;
    984    }
    985 
    986    let cacheObserver = {
    987      observe() {
    988        args = argsFunc();
    989        if (conditionFunc(...args)) {
    990          clearTimeout(this.timer);
    991          Services.obs.removeObserver(this, "accessible-cache");
    992          resolve(args);
    993        }
    994      },
    995 
    996      timeout() {
    997        ok(false, "Timeout while waiting for cache update");
    998        Services.obs.removeObserver(this, "accessible-cache");
    999        args = argsFunc();
   1000        resolve(args);
   1001      },
   1002    };
   1003 
   1004    cacheObserver.timer = setTimeout(
   1005      cacheObserver.timeout.bind(cacheObserver),
   1006      CACHE_WAIT_TIMEOUT_MS
   1007    );
   1008    Services.obs.addObserver(cacheObserver, "accessible-cache");
   1009  });
   1010 }
   1011 
   1012 function untilCacheOk(conditionFunc, message) {
   1013  return untilCacheCondition(
   1014    (v, _unusedMessage) => v,
   1015    () => [conditionFunc(), message]
   1016  ).then(([v, msg]) => ok(v, msg));
   1017 }
   1018 
   1019 function untilCacheIs(retrievalFunc, expected, message) {
   1020  return untilCacheCondition(
   1021    (a, b, _unusedMessage) => Object.is(a, b),
   1022    () => [retrievalFunc(), expected, message]
   1023  ).then(([got, exp, msg]) => is(got, exp, msg));
   1024 }
   1025 
   1026 async function waitForContentPaint(browser) {
   1027  await SpecialPowers.spawn(browser, [], () => {
   1028    return new Promise(function (r) {
   1029      content.requestAnimationFrame(() => content.setTimeout(r));
   1030    });
   1031  });
   1032 }
   1033 
   1034 // Returns true if both number arrays match within `FUZZ`.
   1035 function areBoundsFuzzyEqual(actual, expected) {
   1036  const FUZZ = 1;
   1037  return actual
   1038    .map((val, i) => Math.abs(val - expected[i]) <= FUZZ)
   1039    .reduce((a, b) => a && b, true);
   1040 }
   1041 
   1042 function assertBoundsFuzzyEqual(actual, expected) {
   1043  ok(
   1044    areBoundsFuzzyEqual(actual, expected),
   1045    `${actual} fuzzily matches expected ${expected}`
   1046  );
   1047 }
   1048 
   1049 async function testBoundsWithContent(iframeDocAcc, id, browser) {
   1050  // Retrieve layout bounds from content
   1051  let expectedBounds = await invokeContentTask(browser, [id], _id => {
   1052    const { Layout: LayoutUtils } = ChromeUtils.importESModule(
   1053      "chrome://mochitests/content/browser/accessible/tests/browser/Layout.sys.mjs"
   1054    );
   1055    return LayoutUtils.getBoundsForDOMElm(_id, content.document);
   1056  });
   1057 
   1058  function isWithinExpected(bounds) {
   1059    return areBoundsFuzzyEqual(bounds, expectedBounds);
   1060  }
   1061 
   1062  const acc = findAccessibleChildByID(iframeDocAcc, id);
   1063  let [accBounds] = await untilCacheCondition(isWithinExpected, () => [
   1064    getBounds(acc),
   1065  ]);
   1066 
   1067  assertBoundsFuzzyEqual(accBounds, expectedBounds);
   1068 
   1069  return accBounds;
   1070 }
   1071 
   1072 let gPythonSocket = null;
   1073 
   1074 /**
   1075 * Run some Python code. This is useful for testing OS APIs.
   1076 * This function returns a Promise which is resolved or rejected when the Python
   1077 * code completes. The Python code can return a result with the return
   1078 * statement, as long as the result can be serialized to JSON. For convenience,
   1079 * if the code is a single line which does not begin with return, it will be
   1080 * treated as an expression and its result will be returned. The JS Promise will
   1081 * be resolved with the deserialized result. If the Python code raises an
   1082 * exception, the JS Promise will be rejected with the Python traceback.
   1083 * An info() function is provided in Python to log an info message.
   1084 * See windows/a11y_setup.py for other things available in the Python
   1085 * environment.
   1086 */
   1087 function runPython(code) {
   1088  if (!gPythonSocket) {
   1089    // Keep the socket open across calls to avoid repeated setup overhead.
   1090    gPythonSocket = new WebSocket(
   1091      "ws://mochi.test:8888/browser/accessible/tests/browser/python_runner"
   1092    );
   1093    if (gPythonSocket.readyState != WebSocket.OPEN) {
   1094      gPythonSocket.onopen = () => {
   1095        gPythonSocket.send(code);
   1096        gPythonSocket.onopen = null;
   1097      };
   1098    }
   1099  }
   1100  return new Promise((resolve, reject) => {
   1101    gPythonSocket.onmessage = evt => {
   1102      const message = JSON.parse(evt.data);
   1103      if (message[0] == "return") {
   1104        gPythonSocket.onmessage = null;
   1105        resolve(message[1]);
   1106      } else if (message[0] == "exception") {
   1107        gPythonSocket.onmessage = null;
   1108        reject(new Error(message[1]));
   1109      } else if (message[0] == "info") {
   1110        info(message[1]);
   1111      }
   1112    };
   1113    // If gPythonSocket isn't open yet, we'll send the message when .onopen is
   1114    // called. If it's open, we can send it immediately.
   1115    if (gPythonSocket.readyState == WebSocket.OPEN) {
   1116      gPythonSocket.send(code);
   1117    }
   1118  });
   1119 }