tor-browser

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

helpers.mjs (6809B)


      1 const variants = new Set((new URLSearchParams(location.search)).keys());
      2 
      3 export function hasVariant(name) {
      4  return variants.has(name);
      5 }
      6 
      7 export class Recorder {
      8  #events = [];
      9  #errors = [];
     10  #navigationAPI;
     11  #domExceptionConstructor;
     12  #location;
     13  #skipCurrentChange;
     14  #finalExpectedEvent;
     15  #finalExpectedEventCount;
     16  #currentFinalEventCount = 0;
     17 
     18  #readyToAssertResolve;
     19  #readyToAssertPromise = new Promise(resolve => { this.#readyToAssertResolve = resolve; });
     20 
     21  constructor({ window = self, skipCurrentChange = false, finalExpectedEvent, finalExpectedEventCount = 1 }) {
     22    assert_equals(typeof finalExpectedEvent, "string", "Must pass a string for finalExpectedEvent");
     23 
     24    this.#navigationAPI = window.navigation;
     25    this.#domExceptionConstructor = window.DOMException;
     26    this.#location = window.location;
     27 
     28    this.#skipCurrentChange = skipCurrentChange;
     29    this.#finalExpectedEvent = finalExpectedEvent;
     30    this.#finalExpectedEventCount = finalExpectedEventCount;
     31  }
     32 
     33  setUpNavigationAPIListeners() {
     34    this.#navigationAPI.addEventListener("navigate", e => {
     35      this.record("navigate");
     36 
     37      e.signal.addEventListener("abort", () => {
     38        this.recordWithError("AbortSignal abort", e.signal.reason);
     39      });
     40    });
     41 
     42    this.#navigationAPI.addEventListener("navigateerror", e => {
     43      this.recordWithError("navigateerror", e.error);
     44 
     45      this.#navigationAPI.transition?.finished.then(
     46        () => this.record("transition.finished fulfilled"),
     47        err => this.recordWithError("transition.finished rejected", err)
     48      );
     49    });
     50 
     51    this.#navigationAPI.addEventListener("navigatesuccess", () => {
     52      this.record("navigatesuccess");
     53 
     54      this.#navigationAPI.transition?.finished.then(
     55        () => this.record("transition.finished fulfilled"),
     56        err => this.recordWithError("transition.finished rejected", err)
     57      );
     58    });
     59 
     60    if (!this.#skipCurrentChange) {
     61      this.#navigationAPI.addEventListener("currententrychange", () => this.record("currententrychange"));
     62    }
     63  }
     64 
     65  setUpResultListeners(result, suffix = "") {
     66 
     67    result.committed.then(
     68      () => this.record(`committed fulfilled${suffix}`),
     69      err => this.recordWithError(`committed rejected${suffix}`, err)
     70    );
     71 
     72    result.finished.then(
     73      () => this.record(`finished fulfilled${suffix}`),
     74      err => this.recordWithError(`finished rejected${suffix}`, err)
     75    );
     76 
     77    this.#navigationAPI.transition?.committed?.then(
     78      () => this.record(`transition.committed fulfilled${suffix}`),
     79      err => this.recordWithError(`transition.committed rejected${suffix}`, err)
     80    );
     81  }
     82 
     83  record(name) {
     84    const transitionProps = this.#navigationAPI.transition === null ? null : {
     85      from: this.#navigationAPI.transition.from,
     86      navigationType: this.#navigationAPI.transition.navigationType
     87    };
     88 
     89    this.#events.push({ name, location: this.#location.hash, transitionProps });
     90 
     91    if (name === this.#finalExpectedEvent && ++this.#currentFinalEventCount === this.#finalExpectedEventCount) {
     92      this.#readyToAssertResolve();
     93    }
     94  }
     95 
     96  recordWithError(name, errorObject) {
     97    this.record(name);
     98    this.#errors.push({ name, errorObject });
     99  }
    100 
    101  get readyToAssert() {
    102    return this.#readyToAssertPromise;
    103  }
    104 
    105  // Usage:
    106  //   recorder.assert([
    107  //     /* event name, location.hash value, navigation.transition properties */
    108  //     ["currententrychange", "", null],
    109  //     ["committed fulfilled", "#1", { from, navigationType }],
    110  //     ...
    111  //   ]);
    112  //
    113  // The array format is to avoid repitition at the call site, but I recommend
    114  // you document it like above.
    115  //
    116  // This will automatically also assert that any error objects recorded are
    117  // equal to each other. Use the other assert functions to check the actual
    118  // contents of the error objects.
    119  assert(expectedAsArray) {
    120    if (this.#skipCurrentChange) {
    121      expectedAsArray = expectedAsArray.filter(expected => expected[0] !== "currententrychange");
    122    }
    123    // TODO: Remove once https://github.com/whatwg/html/pull/10919 is merged.
    124    if (!('committed' in NavigationTransition.prototype)) {
    125      expectedAsArray = expectedAsArray.filter(expected => {
    126         return !expected[0].includes("transition.committed fulfilled") &&
    127                !expected[0].includes("transition.committed rejected")
    128      });
    129    }
    130 
    131    // Doing this up front gives nicer error messages because
    132    // assert_array_equals is nice.
    133    const recordedNames = this.#events.map(e => e.name);
    134    const expectedNames = expectedAsArray.map(e => e[0]);
    135    assert_array_equals(recordedNames, expectedNames);
    136 
    137    for (let i = 0; i < expectedAsArray.length; ++i) {
    138      const recorded = this.#events[i];
    139      const expected = expectedAsArray[i];
    140 
    141      assert_equals(
    142        recorded.location,
    143        expected[1],
    144        `event ${i} (${recorded.name}): location.hash value`
    145      );
    146 
    147      if (expected[2] === null) {
    148        assert_equals(
    149          recorded.transitionProps,
    150          null,
    151          `event ${i} (${recorded.name}): navigation.transition expected to be null`
    152        );
    153      } else {
    154        assert_not_equals(
    155          recorded.transitionProps,
    156          null,
    157          `event ${i} (${recorded.name}): navigation.transition expected not to be null`
    158        );
    159        assert_equals(
    160          recorded.transitionProps.from,
    161          expected[2].from,
    162          `event ${i} (${recorded.name}): navigation.transition.from`
    163        );
    164        assert_equals(
    165          recorded.transitionProps.navigationType,
    166          expected[2].navigationType,
    167          `event ${i} (${recorded.name}): navigation.transition.navigationType`
    168        );
    169      }
    170    }
    171 
    172    if (this.#errors.length > 1) {
    173      for (let i = 1; i < this.#errors.length; ++i) {
    174        assert_equals(
    175          this.#errors[i].errorObject,
    176          this.#errors[0].errorObject,
    177          `error objects must match: error object for ${this.#errors[i].name} did not match the one for ${this.#errors[0].name}`
    178        );
    179      }
    180    }
    181  }
    182 
    183  assertErrorsAreAbortErrors() {
    184    assert_greater_than(
    185      this.#errors.length,
    186      0,
    187      "No errors were recorded but assertErrorsAreAbortErrors() was called"
    188    );
    189 
    190    // Assume assert() has been called so all error objects are the same.
    191    const { errorObject } = this.#errors[0];
    192    assert_throws_dom("AbortError", this.#domExceptionConstructor, () => { throw errorObject; });
    193  }
    194 
    195  assertErrorsAre(expectedErrorObject) {
    196    assert_greater_than(
    197      this.#errors.length,
    198      0,
    199      "No errors were recorded but assertErrorsAre() was called"
    200    );
    201 
    202    // Assume assert() has been called so all error objects are the same.
    203    const { errorObject } = this.#errors[0];
    204    assert_equals(errorObject, expectedErrorObject);
    205  }
    206 }