tor-browser

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

utils.js (11403B)


      1 /**
      2 * GlobalOverrider - Utility that allows you to override properties on the global object.
      3 *                   See unit-entry.js for example usage.
      4 */
      5 export class GlobalOverrider {
      6  constructor() {
      7    this.originalGlobals = new Map();
      8    this.sandbox = sinon.createSandbox();
      9  }
     10 
     11  /**
     12   * _override - Internal method to override properties on the global object.
     13   *             The first time a given key is overridden, we cache the original
     14   *             value in this.originalGlobals so that later it can be restored.
     15   *
     16   * @param  {string} key The identifier of the property
     17   * @param  {any} value The value to which the property should be reassigned
     18   */
     19  _override(key, value) {
     20    if (!this.originalGlobals.has(key)) {
     21      this.originalGlobals.set(key, global[key]);
     22    }
     23    global[key] = value;
     24  }
     25 
     26  /**
     27   * set - Override a given property, or all properties on an object
     28   *
     29   * @param  {string|object} key If a string, the identifier of the property
     30   *                             If an object, a number of properties and values to which they should be reassigned.
     31   * @param  {any} value The value to which the property should be reassigned
     32   * @return {type}       description
     33   */
     34  set(key, value) {
     35    if (!value && typeof key === "object") {
     36      const overrides = key;
     37      Object.keys(overrides).forEach(k => this._override(k, overrides[k]));
     38    } else {
     39      this._override(key, value);
     40    }
     41    return value;
     42  }
     43 
     44  /**
     45   * reset - Reset the global sandbox, so all state on spies, stubs etc. is cleared.
     46   *         You probably want to call this after each test.
     47   */
     48  reset() {
     49    this.sandbox.reset();
     50  }
     51 
     52  /**
     53   * restore - Restore the global sandbox and reset all overriden properties to
     54   *           their original values. You should call this after all tests have completed.
     55   */
     56  restore() {
     57    this.sandbox.restore();
     58    this.originalGlobals.forEach((value, key) => {
     59      global[key] = value;
     60    });
     61  }
     62 }
     63 
     64 /**
     65 * A map of mocked preference names and values, used by `FakensIPrefBranch`,
     66 * `FakensIPrefService`, and `FakePrefs`.
     67 *
     68 * Tests should add entries to this map for any preferences they'd like to set,
     69 * and remove any entries during teardown for preferences that shouldn't be
     70 * shared between tests.
     71 */
     72 export const FAKE_GLOBAL_PREFS = new Map();
     73 
     74 /**
     75 * Very simple fake for the most basic semantics of nsIPrefBranch. Lots of
     76 * things aren't yet supported.  Feel free to add them in.
     77 *
     78 * @param {object} args - optional arguments
     79 * @param {Function} args.initHook - if present, will be called back
     80 *                   inside the constructor. Typically used from tests
     81 *                   to save off a pointer to the created instance so that
     82 *                   stubs and spies can be inspected by the test code.
     83 */
     84 export class FakensIPrefBranch {
     85  PREF_INVALID = "invalid";
     86  PREF_INT = "integer";
     87  PREF_BOOL = "boolean";
     88  PREF_STRING = "string";
     89 
     90  constructor(args) {
     91    if (args) {
     92      if ("initHook" in args) {
     93        args.initHook.call(this);
     94      }
     95      if (args.defaultBranch) {
     96        this.prefs = new Map();
     97      } else {
     98        this.prefs = FAKE_GLOBAL_PREFS;
     99      }
    100    } else {
    101      this.prefs = FAKE_GLOBAL_PREFS;
    102    }
    103    this._prefBranch = {};
    104    this.observers = new Map();
    105  }
    106  addObserver(prefix, callback) {
    107    this.observers.set(prefix, callback);
    108  }
    109  removeObserver(prefix, callback) {
    110    this.observers.delete(prefix, callback);
    111  }
    112  setStringPref(prefName, value) {
    113    this.set(prefName, value);
    114  }
    115  getStringPref(prefName, defaultValue) {
    116    return this.get(prefName, defaultValue);
    117  }
    118  setBoolPref(prefName, value) {
    119    this.set(prefName, value);
    120  }
    121  getBoolPref(prefName, defaultValue) {
    122    return this.get(prefName, defaultValue);
    123  }
    124  setIntPref(prefName, value) {
    125    this.set(prefName, value);
    126  }
    127  getIntPref(prefName) {
    128    return this.get(prefName);
    129  }
    130  setCharPref(prefName, value) {
    131    this.set(prefName, value);
    132  }
    133  getCharPref(prefName) {
    134    return this.get(prefName);
    135  }
    136  clearUserPref(prefName) {
    137    this.prefs.delete(prefName);
    138  }
    139  get(prefName, defaultValue) {
    140    let value = this.prefs.get(prefName);
    141    return typeof value === "undefined" ? defaultValue : value;
    142  }
    143  getPrefType(prefName) {
    144    let value = this.prefs.get(prefName);
    145    switch (typeof value) {
    146      case "number":
    147        return this.PREF_INT;
    148 
    149      case "boolean":
    150        return this.PREF_BOOL;
    151 
    152      case "string":
    153        return this.PREF_STRING;
    154 
    155      default:
    156        return this.PREF_INVALID;
    157    }
    158  }
    159  set(prefName, value) {
    160    this.prefs.set(prefName, value);
    161 
    162    // Trigger all observers for prefixes of the changed pref name. This matches
    163    // the semantics of `nsIPrefBranch`.
    164    let observerPrefixes = [...this.observers.keys()].filter(prefix =>
    165      prefName.startsWith(prefix)
    166    );
    167    for (let observerPrefix of observerPrefixes) {
    168      this.observers.get(observerPrefix)("", "", prefName);
    169    }
    170  }
    171  getChildList(prefix) {
    172    return [...this.prefs.keys()].filter(prefName =>
    173      prefName.startsWith(prefix)
    174    );
    175  }
    176  prefHasUserValue(prefName) {
    177    return this.prefs.has(prefName);
    178  }
    179  prefIsLocked(_prefName) {
    180    return false;
    181  }
    182 }
    183 
    184 /**
    185 * A fake `Services.prefs` implementation that extends `FakensIPrefBranch`
    186 * with methods specific to `nsIPrefService`.
    187 */
    188 export class FakensIPrefService extends FakensIPrefBranch {
    189  getBranch() {}
    190  getDefaultBranch(_prefix) {
    191    return {
    192      setBoolPref() {},
    193      setIntPref() {},
    194      setStringPref() {},
    195      clearUserPref() {},
    196    };
    197  }
    198 }
    199 
    200 /**
    201 * Very simple fake for the most basic semantics of Preferences.sys.mjs.
    202 * Extends FakensIPrefBranch.
    203 */
    204 export class FakePrefs extends FakensIPrefBranch {
    205  observe(prefName, callback) {
    206    super.addObserver(prefName, callback);
    207  }
    208  ignore(prefName, callback) {
    209    super.removeObserver(prefName, callback);
    210  }
    211  observeBranch(_listener) {}
    212  ignoreBranch(_listener) {}
    213  set(prefName, value) {
    214    this.prefs.set(prefName, value);
    215 
    216    // Trigger observers for just the changed pref name, not any of its
    217    // prefixes. This matches the semantics of `Preferences.sys.mjs`.
    218    if (this.observers.has(prefName)) {
    219      this.observers.get(prefName)(value);
    220    }
    221  }
    222 }
    223 
    224 /**
    225 * Slimmed down version of toolkit/modules/EventEmitter.sys.mjs
    226 */
    227 export function EventEmitter() {}
    228 EventEmitter.decorate = function (objectToDecorate) {
    229  let emitter = new EventEmitter();
    230  objectToDecorate.on = emitter.on.bind(emitter);
    231  objectToDecorate.off = emitter.off.bind(emitter);
    232  objectToDecorate.once = emitter.once.bind(emitter);
    233  objectToDecorate.emit = emitter.emit.bind(emitter);
    234 };
    235 EventEmitter.prototype = {
    236  on(event, listener) {
    237    if (!this._eventEmitterListeners) {
    238      this._eventEmitterListeners = new Map();
    239    }
    240    if (!this._eventEmitterListeners.has(event)) {
    241      this._eventEmitterListeners.set(event, []);
    242    }
    243    this._eventEmitterListeners.get(event).push(listener);
    244  },
    245  off(event, listener) {
    246    if (!this._eventEmitterListeners) {
    247      return;
    248    }
    249    let listeners = this._eventEmitterListeners.get(event);
    250    if (listeners) {
    251      this._eventEmitterListeners.set(
    252        event,
    253        listeners.filter(
    254          l => l !== listener && l._originalListener !== listener
    255        )
    256      );
    257    }
    258  },
    259  once(event, listener) {
    260    return new Promise(resolve => {
    261      let handler = (_, first, ...rest) => {
    262        this.off(event, handler);
    263        if (listener) {
    264          listener(event, first, ...rest);
    265        }
    266        resolve(first);
    267      };
    268 
    269      handler._originalListener = listener;
    270      this.on(event, handler);
    271    });
    272  },
    273  // All arguments to this method will be sent to listeners
    274  emit(event, ...args) {
    275    if (
    276      !this._eventEmitterListeners ||
    277      !this._eventEmitterListeners.has(event)
    278    ) {
    279      return;
    280    }
    281    let originalListeners = this._eventEmitterListeners.get(event);
    282    for (let listener of this._eventEmitterListeners.get(event)) {
    283      // If the object was destroyed during event emission, stop
    284      // emitting.
    285      if (!this._eventEmitterListeners) {
    286        break;
    287      }
    288      // If listeners were removed during emission, make sure the
    289      // event handler we're going to fire wasn't removed.
    290      if (
    291        originalListeners === this._eventEmitterListeners.get(event) ||
    292        this._eventEmitterListeners.get(event).some(l => l === listener)
    293      ) {
    294        try {
    295          listener(event, ...args);
    296        } catch (ex) {
    297          // error with a listener
    298        }
    299      }
    300    }
    301  },
    302 };
    303 
    304 export function FakePerformance() {}
    305 FakePerformance.prototype = {
    306  marks: new Map(),
    307  now() {
    308    return window.performance.now();
    309  },
    310  timing: { navigationStart: 222222.123 },
    311  get timeOrigin() {
    312    return 10000.234;
    313  },
    314  // XXX assumes type == "mark"
    315  getEntriesByName(name, _type) {
    316    if (this.marks.has(name)) {
    317      return this.marks.get(name);
    318    }
    319    return [];
    320  },
    321  callsToMark: 0,
    322 
    323  /**
    324   * Note: The "startTime" for each mark is simply the number of times mark
    325   * has been called in this object.
    326   */
    327  mark(name) {
    328    let markObj = {
    329      name,
    330      entryType: "mark",
    331      startTime: ++this.callsToMark,
    332      duration: 0,
    333    };
    334 
    335    if (this.marks.has(name)) {
    336      this.marks.get(name).push(markObj);
    337      return;
    338    }
    339 
    340    this.marks.set(name, [markObj]);
    341  },
    342 };
    343 
    344 /**
    345 * addNumberReducer - a simple dummy reducer for testing that adds a number
    346 */
    347 export function addNumberReducer(prevState = 0, action) {
    348  return action.type === "ADD" ? prevState + action.data : prevState;
    349 }
    350 
    351 export class FakeConsoleAPI {
    352  static LOG_LEVELS = {
    353    all: Number.MIN_VALUE,
    354    debug: 2,
    355    log: 3,
    356    info: 3,
    357    clear: 3,
    358    trace: 3,
    359    timeEnd: 3,
    360    time: 3,
    361    assert: 3,
    362    group: 3,
    363    groupEnd: 3,
    364    profile: 3,
    365    profileEnd: 3,
    366    dir: 3,
    367    dirxml: 3,
    368    warn: 4,
    369    error: 5,
    370    off: Number.MAX_VALUE,
    371  };
    372 
    373  constructor({ prefix = "", maxLogLevel = "all" } = {}) {
    374    this.prefix = prefix;
    375    this.prefixStr = prefix ? `${prefix}: ` : "";
    376    this.maxLogLevel = maxLogLevel;
    377 
    378    for (const level of Object.keys(FakeConsoleAPI.LOG_LEVELS)) {
    379      // eslint-disable-next-line no-console
    380      if (typeof console[level] === "function") {
    381        this[level] = this.shouldLog(level)
    382          ? this._log.bind(this, level)
    383          : () => {};
    384      }
    385    }
    386  }
    387  shouldLog(level) {
    388    return (
    389      FakeConsoleAPI.LOG_LEVELS[this.maxLogLevel] <=
    390      FakeConsoleAPI.LOG_LEVELS[level]
    391    );
    392  }
    393  _log(level, ...args) {
    394    console[level](this.prefixStr, ...args); // eslint-disable-line no-console
    395  }
    396 }
    397 
    398 export function FakeNimbusFeature() {
    399  return {
    400    getEnrollmentMetadata() {},
    401    getVariable() {},
    402    getAllVariables() {},
    403    getAllEnrollments() {},
    404    onUpdate() {},
    405    offUpdate() {},
    406  };
    407 }
    408 
    409 export function FakeNimbusFeatures(featureIds) {
    410  return Object.fromEntries(
    411    featureIds.map(featureId => [featureId, FakeNimbusFeature()])
    412  );
    413 }
    414 
    415 export class FakeLogger extends FakeConsoleAPI {
    416  constructor() {
    417    super({
    418      // Don't use a prefix because the first instance gets cached and reused by
    419      // other consumers that would otherwise pass their own identifying prefix.
    420      maxLogLevel: "off", // Change this to "debug" or "all" to get more logging in tests
    421    });
    422  }
    423 }