tor-browser

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

ContentTaskUtils.sys.mjs (8012B)


      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 /*
      6 * This module implements a number of utility functions that can be loaded
      7 * into content scope.
      8 *
      9 * All asynchronous helper methods should return promises, rather than being
     10 * callback based.
     11 */
     12 
     13 // Disable ownerGlobal use since that's not available on content-privileged elements.
     14 
     15 /* eslint-disable mozilla/use-ownerGlobal */
     16 
     17 import { setTimeout } from "resource://gre/modules/Timer.sys.mjs";
     18 
     19 export var ContentTaskUtils = {
     20  /**
     21   * Checks if a DOM element is hidden.
     22   *
     23   * @param {Element} element
     24   *        The element which is to be checked.
     25   *
     26   * @return {boolean}
     27   */
     28  isHidden(element) {
     29    let style = element.ownerDocument.defaultView.getComputedStyle(element);
     30    if (style.display == "none") {
     31      return true;
     32    }
     33    if (style.visibility != "visible") {
     34      return true;
     35    }
     36 
     37    // Hiding a parent element will hide all its children
     38    if (
     39      element.parentNode != element.ownerDocument &&
     40      element.parentNode.nodeType != Node.DOCUMENT_FRAGMENT_NODE
     41    ) {
     42      return ContentTaskUtils.isHidden(element.parentNode);
     43    }
     44 
     45    // Walk up the shadow DOM if we've reached the top of the shadow root
     46    if (element.parentNode.host) {
     47      return ContentTaskUtils.isHidden(element.parentNode.host);
     48    }
     49 
     50    return false;
     51  },
     52 
     53  /**
     54   * Checks if a DOM element is visible.
     55   *
     56   * @param {Element} element
     57   *        The element which is to be checked.
     58   *
     59   * @return {boolean}
     60   */
     61  isVisible(element) {
     62    return !this.isHidden(element);
     63  },
     64 
     65  /**
     66   * Will poll a condition function until it returns true.
     67   *
     68   * @param condition
     69   *        A condition function that must return true or false. If the
     70   *        condition ever throws, this is also treated as a false.
     71   * @param msg
     72   *        The message to use when the returned promise is rejected.
     73   *        This message will be extended with additional information
     74   *        about the number of tries or the thrown exception.
     75   * @param interval
     76   *        The time interval to poll the condition function. Defaults
     77   *        to 100ms.
     78   * @param maxTries
     79   *        The number of times to poll before giving up and rejecting
     80   *        if the condition has not yet returned true. Defaults to 50
     81   *        (~5 seconds for 100ms intervals)
     82   * @return Promise
     83   *        Resolves when condition is true.
     84   *        Rejects if timeout is exceeded or condition ever throws.
     85   */
     86  async waitForCondition(condition, msg, interval = 100, maxTries = 50) {
     87    let startTime = ChromeUtils.now();
     88    for (let tries = 0; tries < maxTries; ++tries) {
     89      await new Promise(resolve => setTimeout(resolve, interval));
     90 
     91      let conditionPassed = false;
     92      try {
     93        conditionPassed = await condition();
     94      } catch (e) {
     95        msg += ` - threw exception: ${e}`;
     96        ChromeUtils.addProfilerMarker(
     97          "ContentTaskUtils",
     98          { startTime, category: "Test" },
     99          `waitForCondition - ${msg}`
    100        );
    101        throw msg;
    102      }
    103      if (conditionPassed) {
    104        ChromeUtils.addProfilerMarker(
    105          "ContentTaskUtils",
    106          { startTime, category: "Test" },
    107          `waitForCondition succeeded after ${tries} retries - ${msg}`
    108        );
    109        return conditionPassed;
    110      }
    111    }
    112 
    113    msg += ` - timed out after ${maxTries} tries.`;
    114    ChromeUtils.addProfilerMarker(
    115      "ContentTaskUtils",
    116      { startTime, category: "Test" },
    117      `waitForCondition - ${msg}`
    118    );
    119    throw msg;
    120  },
    121 
    122  /**
    123   * Waits for an event to be fired on a specified element.
    124   *
    125   * Usage:
    126   *    let promiseEvent = ContentTasKUtils.waitForEvent(element, "eventName");
    127   *    // Do some processing here that will cause the event to be fired
    128   *    // ...
    129   *    // Now yield until the Promise is fulfilled
    130   *    let receivedEvent = yield promiseEvent;
    131   *
    132   * @param {Element} subject
    133   *        The element that should receive the event.
    134   * @param {string} eventName
    135   *        Name of the event to listen to.
    136   * @param {bool} capture [optional]
    137   *        True to use a capturing listener.
    138   * @param {function} checkFn [optional]
    139   *        Called with the Event object as argument, should return true if the
    140   *        event is the expected one, or false if it should be ignored and
    141   *        listening should continue. If not specified, the first event with
    142   *        the specified name resolves the returned promise.
    143   *
    144   * Note: Because this function is intended for testing, any error in checkFn
    145   *       will cause the returned promise to be rejected instead of waiting for
    146   *       the next event, since this is probably a bug in the test.
    147   *
    148   * @returns {Promise<Event>}
    149   */
    150  waitForEvent(subject, eventName, capture, checkFn, wantsUntrusted = false) {
    151    return new Promise((resolve, reject) => {
    152      let startTime = ChromeUtils.now();
    153      subject.addEventListener(
    154        eventName,
    155        function listener(event) {
    156          try {
    157            if (checkFn && !checkFn(event)) {
    158              return;
    159            }
    160            subject.removeEventListener(eventName, listener, capture);
    161            setTimeout(() => {
    162              ChromeUtils.addProfilerMarker(
    163                "ContentTaskUtils",
    164                { category: "Test", startTime },
    165                "waitForEvent - " + eventName
    166              );
    167              resolve(event);
    168            }, 0);
    169          } catch (ex) {
    170            try {
    171              subject.removeEventListener(eventName, listener, capture);
    172            } catch (ex2) {
    173              // Maybe the provided object does not support removeEventListener.
    174            }
    175            setTimeout(() => reject(ex), 0);
    176          }
    177        },
    178        capture,
    179        wantsUntrusted
    180      );
    181    });
    182  },
    183 
    184  /**
    185   * Wait until DOM mutations cause the condition expressed in checkFn to pass.
    186   * Intended as an easy-to-use alternative to waitForCondition.
    187   *
    188   * @param {Element} subject
    189   *        The element on which to observe mutations.
    190   * @param {object} options
    191   *        The options to pass to MutationObserver.observe();
    192   * @param {function} checkFn [optional]
    193   *        Function that returns true when it wants the promise to be resolved.
    194   *        If not specified, the first mutation will resolve the promise.
    195   *
    196   * @returns {Promise<void>}
    197   */
    198  waitForMutationCondition(subject, options, checkFn) {
    199    if (checkFn?.()) {
    200      return Promise.resolve();
    201    }
    202    return new Promise(resolve => {
    203      let obs = new subject.ownerGlobal.MutationObserver(function () {
    204        if (checkFn && !checkFn()) {
    205          return;
    206        }
    207        obs.disconnect();
    208        resolve();
    209      });
    210      obs.observe(subject, options);
    211    });
    212  },
    213 
    214  /**
    215   * Gets an instance of the `EventUtils` helper module for usage in
    216   * content tasks. See https://searchfox.org/mozilla-central/source/testing/mochitest/tests/SimpleTest/EventUtils.js
    217   *
    218   * @param content
    219   *        The `content` global object from your content task.
    220   *
    221   * @returns an EventUtils instance.
    222   */
    223  getEventUtils(content) {
    224    if (content._EventUtils) {
    225      return content._EventUtils;
    226    }
    227 
    228    let EventUtils = (content._EventUtils = {});
    229 
    230    EventUtils.window = {};
    231    EventUtils.setTimeout = setTimeout;
    232    EventUtils.parent = EventUtils.window;
    233    /* eslint-disable camelcase */
    234    EventUtils._EU_Ci = Ci;
    235    EventUtils._EU_Cc = Cc;
    236    /* eslint-enable camelcase */
    237    // EventUtils' `sendChar` function relies on the navigator to synthetize events.
    238    EventUtils.navigator = content.navigator;
    239    EventUtils.KeyboardEvent = content.KeyboardEvent;
    240 
    241    Services.scriptloader.loadSubScript(
    242      "chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
    243      EventUtils
    244    );
    245 
    246    return EventUtils;
    247  },
    248 };