tor-browser

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

util.js (9618B)


      1 const kValidAvailabilities =
      2    ['unavailable', 'downloadable', 'downloading', 'available'];
      3 const kAvailableAvailabilities = ['downloadable', 'downloading', 'available'];
      4 
      5 const kTestPrompt = 'Please write a sentence in English.';
      6 const kTestContext = 'This is a test; this is only a test.';
      7 
      8 const getId = (() => {
      9  let idCount = 0;
     10  return () => idCount++;
     11 })();
     12 
     13 // Takes an array of dictionaries mapping keys to value arrays, e.g.:
     14 //   [ {Shape: ["Square", "Circle", undefined]}, {Count: [1, 2]} ]
     15 // Returns an array of dictionaries with all value combinations, i.e.:
     16 //  [ {Shape: "Square", Count: 1}, {Shape: "Square", Count: 2},
     17 //    {Shape: "Circle", Count: 1}, {Shape: "Circle", Count: 2},
     18 //    {Shape: undefined, Count: 1}, {Shape: undefined, Count: 2} ]
     19 // Omits dictionary members when the value is undefined; supports array values.
     20 function generateOptionCombinations(optionsSpec) {
     21  // 1. Extract keys from the input specification.
     22  const keys = optionsSpec.map(o => Object.keys(o)[0]);
     23  // 2. Extract the arrays of possible values for each key.
     24  const valueArrays = optionsSpec.map(o => Object.values(o)[0]);
     25  // 3. Compute the Cartesian product of the value arrays using reduce.
     26  const valueCombinations = valueArrays.reduce((accumulator, currentValues) => {
     27    // Init the empty accumulator (first iteration), with single-element
     28    // arrays.
     29    if (accumulator.length === 0) {
     30      return currentValues.map(value => [value]);
     31    }
     32    // Otherwise, expand existing combinations with current values.
     33    return accumulator.flatMap(
     34        existingCombo => currentValues.map(
     35            currentValue => [...existingCombo, currentValue]));
     36  }, []);
     37 
     38  // 4. Map each value combination to a result dictionary, skipping
     39  // undefined.
     40  return valueCombinations.map(combination => {
     41    const result = {};
     42    keys.forEach((key, index) => {
     43      if (combination[index] !== undefined) {
     44        result[key] = combination[index];
     45      }
     46    });
     47    return result;
     48  });
     49 }
     50 
     51 // The method should take the AbortSignal as an option and return a promise.
     52 async function testAbortPromise(t, method) {
     53  // Test abort signal without custom error.
     54  {
     55    const controller = new AbortController();
     56    const promise = method(controller.signal);
     57    controller.abort();
     58    await promise_rejects_dom(t, 'AbortError', promise);
     59 
     60    // Using the same aborted controller will get the `AbortError` as well.
     61    const anotherPromise = method(controller.signal);
     62    await promise_rejects_dom(t, 'AbortError', anotherPromise);
     63  }
     64 
     65  // Test abort signal with custom error.
     66  {
     67    const err = new Error('test');
     68    const controller = new AbortController();
     69    const promise = method(controller.signal);
     70    controller.abort(err);
     71    await promise_rejects_exactly(t, err, promise);
     72 
     73    // Using the same aborted controller will get the same error as well.
     74    const anotherPromise = method(controller.signal);
     75    await promise_rejects_exactly(t, err, anotherPromise);
     76  }
     77 };
     78 
     79 async function testCreateMonitorWithAbortAt(
     80    t, loadedToAbortAt, method, options = {}) {
     81  const {promise: eventPromise, resolve} = Promise.withResolvers();
     82  let hadEvent = false;
     83  function monitor(m) {
     84    m.addEventListener('downloadprogress', e => {
     85      if (e.loaded != loadedToAbortAt) {
     86        return;
     87      }
     88 
     89      if (hadEvent) {
     90        assert_unreached(
     91            'This should never be reached since LanguageDetector.create() was aborted.');
     92        return;
     93      }
     94 
     95      resolve();
     96      hadEvent = true;
     97    });
     98  }
     99 
    100  const controller = new AbortController();
    101 
    102  const createPromise =
    103      method({...options, monitor, signal: controller.signal});
    104 
    105  await eventPromise;
    106 
    107  const err = new Error('test');
    108  controller.abort(err);
    109  await promise_rejects_exactly(t, err, createPromise);
    110 }
    111 
    112 async function testCreateMonitorWithAbort(t, method, options = {}) {
    113  await testCreateMonitorWithAbortAt(t, 0, method, options);
    114  await testCreateMonitorWithAbortAt(t, 1, method, options);
    115 }
    116 
    117 // The method should take the AbortSignal as an option and return a
    118 // ReadableStream.
    119 async function testAbortReadableStream(t, method) {
    120  // Test abort signal without custom error.
    121  {
    122    const controller = new AbortController();
    123    const stream = method(controller.signal);
    124    controller.abort();
    125    let writableStream = new WritableStream();
    126    await promise_rejects_dom(t, 'AbortError', stream.pipeTo(writableStream));
    127 
    128    // Using the same aborted controller will get the `AbortError` as well.
    129    await promise_rejects_dom(t, 'AbortError', new Promise(() => {
    130                                method(controller.signal);
    131                              }));
    132  }
    133 
    134  // Test abort signal with custom error.
    135  {
    136    const error = new DOMException('test', 'VersionError');
    137    const controller = new AbortController();
    138    const stream = method(controller.signal);
    139    controller.abort(error);
    140    let writableStream = new WritableStream();
    141    await promise_rejects_exactly(t, error, stream.pipeTo(writableStream));
    142 
    143    // Using the same aborted controller will get the same error.
    144    await promise_rejects_exactly(t, error, new Promise(() => {
    145                                    method(controller.signal);
    146                                  }));
    147  }
    148 };
    149 
    150 async function testMonitor(createFunc, options = {}) {
    151  let created = false;
    152  const progressEvents = [];
    153  function monitor(m) {
    154    m.addEventListener('downloadprogress', e => {
    155      // No progress events should be fired after `createFunc` resolves.
    156      assert_false(created);
    157 
    158      progressEvents.push(e);
    159    });
    160  }
    161 
    162  result = await createFunc({...options, monitor});
    163  created = true;
    164 
    165  assert_greater_than_equal(progressEvents.length, 2);
    166  assert_equals(progressEvents.at(0).loaded, 0);
    167  assert_equals(progressEvents.at(-1).loaded, 1);
    168 
    169  let lastProgressEventLoaded = -1;
    170  for (const progressEvent of progressEvents) {
    171    assert_equals(progressEvent.lengthComputable, true);
    172    assert_equals(progressEvent.total, 1);
    173    assert_less_than_equal(progressEvent.loaded, progressEvent.total);
    174 
    175    // `loaded` must be rounded to the nearest 0x10000th.
    176    assert_equals(progressEvent.loaded % (1 / 0x10000), 0);
    177 
    178    // Progress events should have monotonically increasing `loaded` values.
    179    assert_greater_than(progressEvent.loaded, lastProgressEventLoaded);
    180    lastProgressEventLoaded = progressEvent.loaded;
    181  }
    182  return result;
    183 }
    184 
    185 async function testCreateMonitorCallbackThrowsError(
    186    t, createFunc, options = {}) {
    187  const error = new Error('CreateMonitorCallback threw an error');
    188  function monitor(m) {
    189    m.addEventListener('downloadprogress', e => {
    190      assert_unreached(
    191          'This should never be reached since monitor throws an error.');
    192    });
    193    throw error;
    194  }
    195 
    196  await promise_rejects_exactly(t, error, createFunc({...options, monitor}));
    197 }
    198 
    199 function run_iframe_test(iframe, test_name) {
    200  const id = getId();
    201  iframe.contentWindow.postMessage({id, type: test_name}, '*');
    202  const {promise, resolve, reject} = Promise.withResolvers();
    203  window.onmessage = message => {
    204    if (message.data.id !== id) {
    205      return;
    206    }
    207    if (message.data.success) {
    208      resolve(message.data.success);
    209    } else {
    210      reject(message.data.err)
    211    }
    212  };
    213  return promise;
    214 }
    215 
    216 function load_iframe(src, permission_policy) {
    217  let iframe = document.createElement('iframe');
    218  const {promise, resolve} = Promise.withResolvers();
    219  iframe.onload = () => {
    220    resolve(iframe);
    221  };
    222  iframe.src = src;
    223  iframe.allow = permission_policy;
    224  document.body.appendChild(iframe);
    225  return promise;
    226 }
    227 
    228 async function createLanguageModel(options = {}) {
    229  await test_driver.bless();
    230  return LanguageModel.create(options);
    231 }
    232 
    233 async function createSummarizer(options = {}) {
    234  await test_driver.bless();
    235  return await Summarizer.create(options);
    236 }
    237 
    238 async function createWriter(options = {}) {
    239  await test_driver.bless();
    240  return await Writer.create(options);
    241 }
    242 
    243 async function createRewriter(options = {}) {
    244  await test_driver.bless();
    245  return await Rewriter.create(options);
    246 }
    247 
    248 async function createProofreader(options = {}) {
    249  await test_driver.bless();
    250  return await Proofreader.create(options);
    251 }
    252 
    253 async function ensureLanguageModel(options = {}) {
    254  assert_true(!!LanguageModel);
    255  const availability = await LanguageModel.availability(options);
    256  assert_in_array(availability, kValidAvailabilities);
    257  // Yield PRECONDITION_FAILED if the API is unavailable on this device.
    258  assert_implements_optional(availability != 'unavailable', 'API unavailable');
    259 };
    260 
    261 async function testDestroy(t, createMethod, options, instanceMethods) {
    262  const instance = await createMethod(options);
    263 
    264  const promises = instanceMethods.map(method => method(instance));
    265 
    266  instance.destroy();
    267 
    268  promises.push(...instanceMethods.map(method => method(instance)));
    269 
    270  for (const promise of promises) {
    271    await promise_rejects_dom(t, 'AbortError', promise);
    272  }
    273 }
    274 
    275 async function testCreateAbort(t, createMethod, options, instanceMethods) {
    276  const controller = new AbortController();
    277  const instance = await createMethod({...options, signal: controller.signal});
    278 
    279  const promises = instanceMethods.map(method => method(instance));
    280 
    281  const error = new Error('The create abort signal was aborted.');
    282  controller.abort(error);
    283 
    284  promises.push(...instanceMethods.map(method => method(instance)));
    285 
    286  for (const promise of promises) {
    287    await promise_rejects_exactly(t, error, promise);
    288  }
    289 }
    290 
    291 function consumeTransientUserActivation() {
    292  const win = window.open('about:blank', '_blank');
    293  if (win)
    294    win.close();
    295 }