tor-browser

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

test_l10nCache.js (17217B)


      1 /* Any copyright is dedicated to the Public Domain.
      2 * http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 // Tests L10nCache in UrlbarUtils.sys.mjs.
      5 
      6 "use strict";
      7 
      8 ChromeUtils.defineESModuleGetters(this, {
      9  L10nCache: "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs",
     10 });
     11 
     12 add_task(async function comprehensive() {
     13  // Set up a mock localization.
     14  let l10n = initL10n({
     15    args0a: "Zero args value",
     16    args0b: "Another zero args value",
     17    args1a: "One arg value is { $arg1 }",
     18    args1b: "Another one arg value is { $arg1 }",
     19    args2a: "Two arg values are { $arg1 } and { $arg2 }",
     20    args2b: "More two arg values are { $arg1 } and { $arg2 }",
     21    args3a: "Three arg values are { $arg1 }, { $arg2 }, and { $arg3 }",
     22    args3b: "More three arg values are { $arg1 }, { $arg2 }, and { $arg3 }",
     23    attrs1: [".label = attrs1 label has zero args"],
     24    attrs2: [
     25      ".label = attrs2 label has zero args",
     26      ".tooltiptext = attrs2 tooltiptext arg value is { $arg1 }",
     27    ],
     28    attrs3: [
     29      ".label = attrs3 label has zero args",
     30      ".tooltiptext = attrs3 tooltiptext arg value is { $arg1 }",
     31      ".alt = attrs3 alt arg values are { $arg1 } and { $arg2 }",
     32    ],
     33  });
     34 
     35  let tests = [
     36    // different strings with the same number of args and also the same strings
     37    // with different args
     38    {
     39      obj: {
     40        id: "args0a",
     41      },
     42      expected: {
     43        value: "Zero args value",
     44        attributes: null,
     45      },
     46    },
     47    {
     48      obj: {
     49        id: "args0b",
     50      },
     51      expected: {
     52        value: "Another zero args value",
     53        attributes: null,
     54      },
     55    },
     56    {
     57      obj: {
     58        id: "args1a",
     59        args: { arg1: "foo1" },
     60      },
     61      expected: {
     62        value: "One arg value is foo1",
     63        attributes: null,
     64      },
     65    },
     66    {
     67      obj: {
     68        id: "args1a",
     69        args: { arg1: "foo2" },
     70      },
     71      expected: {
     72        value: "One arg value is foo2",
     73        attributes: null,
     74      },
     75    },
     76    {
     77      obj: {
     78        id: "args1b",
     79        args: { arg1: "foo1" },
     80      },
     81      expected: {
     82        value: "Another one arg value is foo1",
     83        attributes: null,
     84      },
     85    },
     86    {
     87      obj: {
     88        id: "args1b",
     89        args: { arg1: "foo2" },
     90      },
     91      expected: {
     92        value: "Another one arg value is foo2",
     93        attributes: null,
     94      },
     95    },
     96    {
     97      obj: {
     98        id: "args2a",
     99        args: { arg1: "foo1", arg2: "bar1" },
    100      },
    101      expected: {
    102        value: "Two arg values are foo1 and bar1",
    103        attributes: null,
    104      },
    105    },
    106    {
    107      obj: {
    108        id: "args2a",
    109        args: { arg1: "foo2", arg2: "bar2" },
    110      },
    111      expected: {
    112        value: "Two arg values are foo2 and bar2",
    113        attributes: null,
    114      },
    115    },
    116    {
    117      obj: {
    118        id: "args2b",
    119        args: { arg1: "foo1", arg2: "bar1" },
    120      },
    121      expected: {
    122        value: "More two arg values are foo1 and bar1",
    123        attributes: null,
    124      },
    125    },
    126    {
    127      obj: {
    128        id: "args2b",
    129        args: { arg1: "foo2", arg2: "bar2" },
    130      },
    131      expected: {
    132        value: "More two arg values are foo2 and bar2",
    133        attributes: null,
    134      },
    135    },
    136    {
    137      obj: {
    138        id: "args3a",
    139        args: { arg1: "foo1", arg2: "bar1", arg3: "baz1" },
    140      },
    141      expected: {
    142        value: "Three arg values are foo1, bar1, and baz1",
    143        attributes: null,
    144      },
    145    },
    146    {
    147      obj: {
    148        id: "args3a",
    149        args: { arg1: "foo2", arg2: "bar2", arg3: "baz2" },
    150      },
    151      expected: {
    152        value: "Three arg values are foo2, bar2, and baz2",
    153        attributes: null,
    154      },
    155    },
    156    {
    157      obj: {
    158        id: "args3b",
    159        args: { arg1: "foo1", arg2: "bar1", arg3: "baz1" },
    160      },
    161      expected: {
    162        value: "More three arg values are foo1, bar1, and baz1",
    163        attributes: null,
    164      },
    165    },
    166    {
    167      obj: {
    168        id: "args3b",
    169        args: { arg1: "foo2", arg2: "bar2", arg3: "baz2" },
    170      },
    171      expected: {
    172        value: "More three arg values are foo2, bar2, and baz2",
    173        attributes: null,
    174      },
    175    },
    176 
    177    // two instances of the same string with their args swapped
    178    {
    179      obj: {
    180        id: "args2a",
    181        args: { arg1: "arg A", arg2: "arg B" },
    182      },
    183      expected: {
    184        value: "Two arg values are arg A and arg B",
    185        attributes: null,
    186      },
    187    },
    188    {
    189      obj: {
    190        id: "args2a",
    191        args: { arg1: "arg B", arg2: "arg A" },
    192      },
    193      expected: {
    194        value: "Two arg values are arg B and arg A",
    195        attributes: null,
    196      },
    197    },
    198 
    199    // strings with attributes
    200    {
    201      obj: {
    202        id: "attrs1",
    203      },
    204      expected: {
    205        value: null,
    206        attributes: {
    207          label: "attrs1 label has zero args",
    208        },
    209      },
    210    },
    211    {
    212      obj: {
    213        id: "attrs2",
    214        args: {
    215          arg1: "arg A",
    216        },
    217      },
    218      expected: {
    219        value: null,
    220        attributes: {
    221          label: "attrs2 label has zero args",
    222          tooltiptext: "attrs2 tooltiptext arg value is arg A",
    223        },
    224      },
    225    },
    226    {
    227      obj: {
    228        id: "attrs3",
    229        args: {
    230          arg1: "arg A",
    231          arg2: "arg B",
    232        },
    233      },
    234      expected: {
    235        value: null,
    236        attributes: {
    237          label: "attrs3 label has zero args",
    238          tooltiptext: "attrs3 tooltiptext arg value is arg A",
    239          alt: "attrs3 alt arg values are arg A and arg B",
    240        },
    241      },
    242    },
    243  ];
    244 
    245  let cache = new L10nCache(l10n);
    246 
    247  // Get some non-cached strings.
    248  Assert.ok(!cache.get({ id: "uncached1" }), "Uncached string 1");
    249  Assert.ok(!cache.get({ id: "uncached2", args: "foo" }), "Uncached string 2");
    250 
    251  // Add each test string and get it back.
    252  for (let { obj, expected } of tests) {
    253    await cache.add(obj);
    254    let message = cache.get(obj);
    255    Assert.deepEqual(
    256      message,
    257      expected,
    258      "Expected message for obj: " + JSON.stringify(obj)
    259    );
    260  }
    261 
    262  // Get each string again to make sure each add didn't somehow mess up the
    263  // previously added strings.
    264  for (let { obj, expected } of tests) {
    265    Assert.deepEqual(
    266      cache.get(obj),
    267      expected,
    268      "Expected message for obj: " + JSON.stringify(obj)
    269    );
    270  }
    271 
    272  // Delete some of the strings. We'll delete every other one to mix it up.
    273  for (let i = 0; i < tests.length; i++) {
    274    if (i % 2 == 0) {
    275      let { obj } = tests[i];
    276      cache.delete(obj);
    277      Assert.ok(!cache.get(obj), "Deleted from cache: " + JSON.stringify(obj));
    278    }
    279  }
    280 
    281  // Get each remaining string.
    282  for (let i = 0; i < tests.length; i++) {
    283    if (i % 2 != 0) {
    284      let { obj, expected } = tests[i];
    285      Assert.deepEqual(
    286        cache.get(obj),
    287        expected,
    288        "Expected message for obj: " + JSON.stringify(obj)
    289      );
    290    }
    291  }
    292 
    293  // Clear the cache.
    294  cache.clear();
    295  for (let { obj } of tests) {
    296    Assert.ok(!cache.get(obj), "After cache clear: " + JSON.stringify(obj));
    297  }
    298 
    299  // `ensure` each test string and get it back.
    300  for (let { obj, expected } of tests) {
    301    await cache.ensure(obj);
    302    let message = cache.get(obj);
    303    Assert.deepEqual(
    304      message,
    305      expected,
    306      "Expected message for obj: " + JSON.stringify(obj)
    307    );
    308 
    309    // Call `ensure` again. This time, `add` should not be called.
    310    let originalAdd = cache.add;
    311    cache.add = () => Assert.ok(false, "add erroneously called");
    312    await cache.ensure(obj);
    313    cache.add = originalAdd;
    314  }
    315 
    316  // Clear the cache again.
    317  cache.clear();
    318  for (let { obj } of tests) {
    319    Assert.ok(!cache.get(obj), "After cache clear: " + JSON.stringify(obj));
    320  }
    321 
    322  // `ensureAll` the test strings and get them back.
    323  let objects = tests.map(({ obj }) => obj);
    324  await cache.ensureAll(objects);
    325  for (let { obj, expected } of tests) {
    326    let message = cache.get(obj);
    327    Assert.deepEqual(
    328      message,
    329      expected,
    330      "Expected message for obj: " + JSON.stringify(obj)
    331    );
    332  }
    333 
    334  // Ensure the cache is cleared after the app locale changes
    335  Assert.greater(cache.size(), 0, "The cache has messages in it.");
    336  Services.obs.notifyObservers(null, "intl:app-locales-changed");
    337  Assert.equal(cache.size(), 0, "The cache is empty on app locale change");
    338 });
    339 
    340 // Tests cache eviction.
    341 add_task(async function eviction() {
    342  // Set up a mock localization.
    343  let l10n = initL10n({
    344    args0: "Zero args value",
    345    args1: "One arg value is { $arg1 }",
    346    args2: "Two arg values are { $arg1 } and { $arg2 }",
    347    args3: "Three arg values are { $arg1 }, { $arg2 }, and { $arg3 }",
    348 
    349    attrs0: [".label = attrs0 label has zero args"],
    350    attrs1: [
    351      ".label = attrs1 label has zero args",
    352      ".tooltiptext = attrs1 tooltiptext arg value is { $arg1 }",
    353    ],
    354    attrs2: [
    355      ".label = attrs2 label has zero args",
    356      ".tooltiptext = attrs2 tooltiptext arg value is { $arg1 }",
    357      ".alt = attrs2 alt arg values are { $arg1 } and { $arg2 }",
    358    ],
    359  });
    360 
    361  let cache = new L10nCache(l10n);
    362 
    363  // Get the max cache entries per l10n ID.
    364  let maxEntriesPerId = L10nCache.MAX_ENTRIES_PER_ID;
    365  Assert.equal(
    366    typeof maxEntriesPerId,
    367    "number",
    368    "MAX_ENTRIES_PER_ID should be a number"
    369  );
    370  Assert.greater(maxEntriesPerId, 0, "MAX_ENTRIES_PER_ID should be > 0");
    371 
    372  // Cache enough l10n objects with the same ID but different args to fill up
    373  // the ID's cache entries. The args will be "aaa-0", "aaa-1", etc.
    374  for (let i = 0; i < maxEntriesPerId; i++) {
    375    let arg1 = "aaa-" + i;
    376    let l10nObj = {
    377      id: "args1",
    378      args: { arg1 },
    379    };
    380    await cache.add(l10nObj);
    381 
    382    // The message should be cached.
    383    Assert.deepEqual(
    384      cache.get(l10nObj),
    385      {
    386        value: `One arg value is ${arg1}`,
    387        attributes: null,
    388      },
    389      "Message should be cached: " + JSON.stringify(l10nObj)
    390    );
    391 
    392    // The cache size should be incremented.
    393    Assert.equal(
    394      cache.size(),
    395      i + 1,
    396      "Expected cache size after adding l10n obj: " + JSON.stringify(l10nObj)
    397    );
    398  }
    399 
    400  // Check some l10n objects we did not cache.
    401  for (let arg1 of [`aaa-${maxEntriesPerId}`, "some other value"]) {
    402    let l10nObj = {
    403      id: "args1",
    404      args: { arg1 },
    405    };
    406    Assert.ok(
    407      !cache.get(l10nObj),
    408      "Message should not be cached since it wasn't added: " +
    409        JSON.stringify(l10nObj)
    410    );
    411  }
    412 
    413  // Now cache more l10n objects with the same ID as before but with new args:
    414  // "bbb-0", "bbb-1", etc. Each time we cache a new object, the oldest "aaa"
    415  // entry should be evicted since the ID's cache entries are filled up.
    416  for (let i = 0; i < maxEntriesPerId; i++) {
    417    let arg1 = "bbb-" + i;
    418    let l10nObj = {
    419      id: "args1",
    420      args: { arg1 },
    421    };
    422    await cache.add(l10nObj);
    423 
    424    // The message should be cached.
    425    Assert.deepEqual(
    426      cache.get(l10nObj),
    427      {
    428        value: `One arg value is ${arg1}`,
    429        attributes: null,
    430      },
    431      "Message should be cached: " + JSON.stringify(l10nObj)
    432    );
    433 
    434    // The cache size should remain maxed out.
    435    Assert.equal(
    436      cache.size(),
    437      maxEntriesPerId,
    438      "Cache size should remain maxed out after caching l10n obj: " +
    439        JSON.stringify(l10nObj)
    440    );
    441 
    442    // The oldest "aaa" entry should have been evicted, and all previous oldest
    443    // entries in prior iterations of this loop should remain evicted.
    444    for (let j = 0; j < maxEntriesPerId; j++) {
    445      let oldArg1 = "aaa-" + j;
    446      let oldL10nObj = {
    447        id: "args1",
    448        args: { arg1: oldArg1 },
    449      };
    450      if (j <= i) {
    451        Assert.deepEqual(
    452          cache.get(oldL10nObj),
    453          null,
    454          "Message should be evicted for old l10n obj: " +
    455            JSON.stringify(oldL10nObj)
    456        );
    457      } else {
    458        Assert.deepEqual(
    459          cache.get(oldL10nObj),
    460          {
    461            value: `One arg value is ${oldArg1}`,
    462            attributes: null,
    463          },
    464          "Message should not yet be evicted for old l10n obj: " +
    465            JSON.stringify(oldL10nObj)
    466        );
    467      }
    468    }
    469  }
    470 
    471  // Now cache more l10n objects just like before but with a different ID. Since
    472  // the ID is new, we should be able to fill up its cache entries.
    473  for (let i = 0; i < maxEntriesPerId; i++) {
    474    let arg1 = "yyy-" + i;
    475    let arg2 = "zzz-" + i;
    476    let l10nObj = {
    477      id: "args2",
    478      args: { arg1, arg2 },
    479    };
    480    await cache.add(l10nObj);
    481 
    482    // The message should be cached.
    483    Assert.deepEqual(
    484      cache.get(l10nObj),
    485      {
    486        value: `Two arg values are ${arg1} and ${arg2}`,
    487        attributes: null,
    488      },
    489      "Message should be cached: " + JSON.stringify(l10nObj)
    490    );
    491 
    492    // The cache size should start increasing again since we're caching l10n
    493    // objects with a different ID from before.
    494    Assert.equal(
    495      cache.size(),
    496      maxEntriesPerId + i + 1,
    497      "Cache size should start increasing again: " + JSON.stringify(l10nObj)
    498    );
    499 
    500    // All the messages with the "args1" ID from above should remain cached.
    501    for (let j = 0; j < maxEntriesPerId; j++) {
    502      let prevArg1 = "bbb-" + j;
    503      let prevL10nObj = {
    504        id: "args1",
    505        args: { arg1: prevArg1 },
    506      };
    507      Assert.deepEqual(
    508        cache.get(prevL10nObj),
    509        {
    510          value: `One arg value is ${prevArg1}`,
    511          attributes: null,
    512        },
    513        "Previous message should remain cached: " + JSON.stringify(prevL10nObj)
    514      );
    515    }
    516  }
    517 
    518  // Now re-cache some of the previously cached "args1" messages. This should
    519  // reorder the "args1" cache entries so that these re-cached messages are most
    520  // recently used. We'll re-cache messages with even-numbered args values.
    521  for (let i = 0; i < maxEntriesPerId; i++) {
    522    if (i % 2 == 0) {
    523      let arg1 = "bbb-" + i;
    524      let l10nObj = {
    525        id: "args1",
    526        args: { arg1 },
    527      };
    528      Assert.ok(
    529        await cache.get(l10nObj),
    530        "Sanity check: Message should still be cached: " +
    531          JSON.stringify(l10nObj)
    532      );
    533      await cache.add(l10nObj);
    534 
    535      // The cache size should remain maxed out.
    536      Assert.equal(
    537        cache.size(),
    538        2 * maxEntriesPerId,
    539        "Cache size should remain maxed out after caching l10n obj: " +
    540          JSON.stringify(l10nObj)
    541      );
    542    }
    543  }
    544 
    545  // Build a list of args in the expected cached "args1" entries sorted from
    546  // least recently used to most recently used. Since we just re-cached messages
    547  // with even-numbered args, they should be at the end of this list, and
    548  // messages with odd-numbered args should be at the front.
    549  let expected = [];
    550  for (let i = 0; i < maxEntriesPerId; i++) {
    551    if (i % 2) {
    552      // odd
    553      expected.push("bbb-" + i);
    554    }
    555  }
    556  for (let i = 0; i < maxEntriesPerId; i++) {
    557    if (i % 2 == 0) {
    558      // even
    559      expected.push("bbb-" + i);
    560    }
    561  }
    562 
    563  // Now cache more l10n objects with the same "args1" ID but with new args.
    564  // The old "bbb" entries should be evicted in the expected order.
    565  for (let i = 0; i < maxEntriesPerId; i++) {
    566    let arg1 = "ccc-" + i;
    567    let l10nObj = {
    568      id: "args1",
    569      args: { arg1 },
    570    };
    571    await cache.add(l10nObj);
    572 
    573    // The message should be cached.
    574    Assert.deepEqual(
    575      cache.get(l10nObj),
    576      {
    577        value: `One arg value is ${arg1}`,
    578        attributes: null,
    579      },
    580      "Message should be cached: " + JSON.stringify(l10nObj)
    581    );
    582 
    583    // The cache size should remain maxed out.
    584    Assert.equal(
    585      cache.size(),
    586      2 * maxEntriesPerId,
    587      "Cache size should remain maxed out after caching l10n obj: " +
    588        JSON.stringify(l10nObj)
    589    );
    590 
    591    // The oldest entry should have been evicted, and all previous oldest
    592    // entries in prior iterations of this loop should remain evicted.
    593    for (let j = 0; j < expected.length; j++) {
    594      let oldArg1 = expected[j];
    595      let oldL10nObj = {
    596        id: "args1",
    597        args: { arg1: oldArg1 },
    598      };
    599      if (j <= i) {
    600        Assert.deepEqual(
    601          cache.get(oldL10nObj),
    602          null,
    603          "Message should be evicted for old l10n obj: " +
    604            JSON.stringify(oldL10nObj)
    605        );
    606      } else {
    607        Assert.deepEqual(
    608          cache.get(oldL10nObj),
    609          {
    610            value: `One arg value is ${oldArg1}`,
    611            attributes: null,
    612          },
    613          "Message should not yet be evicted for old l10n obj: " +
    614            JSON.stringify(oldL10nObj)
    615        );
    616      }
    617    }
    618  }
    619 });
    620 
    621 /**
    622 * Sets up a mock localization.
    623 *
    624 * @param {object} pairs
    625 *   Fluent strings as key-value pairs.
    626 * @returns {Localization}
    627 *   The mock Localization object.
    628 */
    629 function initL10n(pairs) {
    630  let source = Object.entries(pairs)
    631    .map(([key, value]) => {
    632      if (Array.isArray(value)) {
    633        value = value.map(s => "  \n" + s).join("");
    634      }
    635      return `${key} = ${value}`;
    636    })
    637    .join("\n");
    638  let registry = new L10nRegistry();
    639  registry.registerSources([
    640    L10nFileSource.createMock(
    641      "test",
    642      "app",
    643      ["en-US"],
    644      "/localization/{locale}",
    645      [{ source, path: "/localization/en-US/test.ftl" }]
    646    ),
    647  ]);
    648  return new Localization(["/test.ftl"], true, registry, ["en-US"]);
    649 }