tor-browser

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

test_ext_menu_startup.js (27616B)


      1 "use strict";
      2 
      3 ChromeUtils.defineESModuleGetters(this, {
      4  ExtensionMenus: "resource://gre/modules/ExtensionMenus.sys.mjs",
      5  KeyValueService: "resource://gre/modules/kvstore.sys.mjs",
      6  Management: "resource://gre/modules/Extension.sys.mjs",
      7 });
      8 
      9 const { AddonTestUtils } = ChromeUtils.importESModule(
     10  "resource://testing-common/AddonTestUtils.sys.mjs"
     11 );
     12 
     13 AddonTestUtils.init(this);
     14 AddonTestUtils.overrideCertDB();
     15 AddonTestUtils.createAppInfo(
     16  "xpcshell@tests.mozilla.org",
     17  "XPCShell",
     18  "42",
     19  "42"
     20 );
     21 
     22 Services.prefs.setBoolPref("extensions.eventPages.enabled", true);
     23 
     24 function getExtension(id, background, useAddonManager, version = "1.0") {
     25  return {
     26    useAddonManager,
     27    manifest: {
     28      version,
     29      browser_specific_settings: { gecko: { id } },
     30      permissions: ["menus"],
     31      background: { persistent: false },
     32    },
     33    background,
     34  };
     35 }
     36 
     37 async function expectPersistedMenus(extensionId, extensionVersion, expect) {
     38  let menusFromStore = await ExtensionMenus._getStoredMenusForTesting(
     39    extensionId,
     40    extensionVersion
     41  );
     42  equal(menusFromStore.size, expect.length, "stored menus size");
     43  let createProperties = Array.from(menusFromStore.values());
     44  // The menus are loaded from disk for extensions using a non-persistent
     45  // background page and kept in memory in a map, the order is significant
     46  // for recreating menus on startup.  Ensure that they are in
     47  // the expected order.
     48  for (let i in createProperties) {
     49    Assert.deepEqual(
     50      createProperties[i],
     51      expect[i],
     52      "expected properties exist in the menus store"
     53    );
     54  }
     55 }
     56 
     57 async function expectExtensionMenus(
     58  testExtension,
     59  expect,
     60  { checkSaved } = {}
     61 ) {
     62  const extension = WebExtensionPolicy.getByID(testExtension.id).extension;
     63  let menusInMemory = ExtensionMenus.getMenus(extension);
     64  let createProperties = Array.from(menusInMemory.values());
     65  equal(menusInMemory.size, expect.length, "menus map size");
     66  for (let i in createProperties) {
     67    Assert.deepEqual(
     68      createProperties[i],
     69      expect[i],
     70      "expected properties exist in the menus map"
     71    );
     72  }
     73 
     74  if (!checkSaved) {
     75    return;
     76  }
     77 
     78  await expectPersistedMenus(testExtension.id, testExtension.version, expect);
     79 }
     80 
     81 function promiseExtensionEvent(wrapper, event) {
     82  return new Promise(resolve => {
     83    wrapper.extension.once(event, (kind, data) => {
     84      resolve(data);
     85    });
     86  });
     87 }
     88 
     89 async function mockBrowserRestart(
     90  extTestWrapper,
     91  { shutdownAndRecreateStore = true, waitForMenuRecreated = true } = {}
     92 ) {
     93  if (shutdownAndRecreateStore) {
     94    info("Mock browser shutdown");
     95    let menusManager = ExtensionMenus._getManager(extTestWrapper.extension);
     96    await AddonTestUtils.promiseShutdownManager();
     97    // Wait data to be flushed as part of the extension shutdown and recreate the store.
     98    info("Wait for store to be flushed");
     99    await menusManager._finalizeStoreTaskForTesting();
    100    info("Recreate menus store");
    101    ExtensionMenus._recreateStoreForTesting();
    102  }
    103  let promiseMenusRecreated;
    104  if (waitForMenuRecreated) {
    105    Management.once("startup", (kind, ext) => {
    106      info(`management ${kind} ${ext.id}`);
    107      promiseMenusRecreated = promiseExtensionEvent(
    108        { extension: ext },
    109        "webext-menus-created"
    110      );
    111    });
    112  }
    113  info("Mock browser startup");
    114  await AddonTestUtils.promiseStartupManager();
    115  await extTestWrapper.awaitStartup();
    116  if (waitForMenuRecreated) {
    117    info("Wait for persisted menus to be recreated");
    118  }
    119  await promiseMenusRecreated;
    120 }
    121 
    122 function extPageScriptWithMenusCreateAndUpdateTestHandler() {
    123  browser.test.onMessage.addListener((msg, ...args) => {
    124    switch (msg) {
    125      case "menusCreate": {
    126        const menuDetails = args[0];
    127        browser.menus.create(menuDetails, () => {
    128          browser.test.assertEq(
    129            undefined,
    130            browser.runtime.lastError?.message,
    131            "Expect the menu to be created successfully"
    132          );
    133          browser.test.sendMessage(`${msg}:done`);
    134        });
    135        break;
    136      }
    137      case "menusUpdate": {
    138        const menuId = args[0];
    139        const menuDetails = args[1];
    140        browser.test.log(`Updating "${menuId}: ${JSON.stringify(menuDetails)}`);
    141        browser.menus.update(menuId, menuDetails, () => {
    142          browser.test.assertEq(
    143            undefined,
    144            browser.runtime.lastError?.message,
    145            "Expect the menu to be created successfully"
    146          );
    147          browser.test.sendMessage(`${msg}:done`);
    148        });
    149        break;
    150      }
    151      default:
    152        browser.test.fail(`Got unexpected test message: ${msg}`);
    153        browser.test.sendMessage(`${msg}:done`);
    154    }
    155  });
    156  browser.test.sendMessage("extpage:ready");
    157 }
    158 
    159 add_setup(async () => {
    160  // Reduce the amount of time we wait to write menus
    161  // data on disk while running this test.
    162  Services.prefs.setIntPref(
    163    "extensions.webextensions.menus.writeDebounceTime",
    164    200
    165  );
    166  await AddonTestUtils.promiseStartupManager();
    167 });
    168 
    169 add_task(async function test_menu_onInstalled() {
    170  async function background() {
    171    browser.runtime.onInstalled.addListener(async () => {
    172      const parentId = browser.menus.create({
    173        contexts: ["all"],
    174        title: "parent",
    175        id: "test-parent",
    176      });
    177      browser.menus.create({
    178        parentId,
    179        title: "click A",
    180        id: "test-click-a",
    181      });
    182      browser.menus.create(
    183        {
    184          parentId,
    185          title: "click B",
    186          id: "test-click-b",
    187        },
    188        () => {
    189          browser.test.sendMessage("onInstalled");
    190        }
    191      );
    192    });
    193    browser.menus.create(
    194      {
    195        contexts: ["tab"],
    196        title: "top-level",
    197        id: "test-top-level",
    198      },
    199      () => {
    200        browser.test.sendMessage("create", browser.runtime.lastError?.message);
    201      }
    202    );
    203 
    204    browser.test.onMessage.addListener(async msg => {
    205      browser.test.log(`onMessage ${msg}`);
    206      if (msg == "updatemenu") {
    207        await browser.menus.update("test-click-a", { title: "click updated" });
    208      } else if (msg == "removemenu") {
    209        await browser.menus.remove("test-click-b");
    210      } else if (msg == "removeall") {
    211        await browser.menus.removeAll();
    212      }
    213      browser.test.sendMessage("updated");
    214    });
    215  }
    216 
    217  const extension = ExtensionTestUtils.loadExtension(
    218    getExtension("test-persist@mochitest", background, "permanent")
    219  );
    220 
    221  await extension.startup();
    222  let lastError = await extension.awaitMessage("create");
    223  Assert.equal(lastError, undefined, "no error creating menu");
    224  await extension.awaitMessage("onInstalled");
    225  await extension.terminateBackground();
    226 
    227  await expectExtensionMenus(extension, [
    228    {
    229      contexts: ["tab"],
    230      id: "test-top-level",
    231      title: "top-level",
    232    },
    233    { contexts: ["all"], id: "test-parent", title: "parent" },
    234    {
    235      id: "test-click-a",
    236      parentId: "test-parent",
    237      title: "click A",
    238    },
    239    {
    240      id: "test-click-b",
    241      parentId: "test-parent",
    242      title: "click B",
    243    },
    244  ]);
    245 
    246  await extension.wakeupBackground();
    247  lastError = await extension.awaitMessage("create");
    248  Assert.equal(
    249    lastError,
    250    "The menu id test-top-level already exists in menus.create.",
    251    "correct error creating menu"
    252  );
    253 
    254  await mockBrowserRestart(extension);
    255 
    256  // After the extension or the AddonManager has been shutdown,
    257  // we expect the menu store task to have written the menus
    258  // on disk and so we expect these menus to be loaded back
    259  // in memory and also stored on disk.
    260  await expectExtensionMenus(
    261    extension,
    262    [
    263      {
    264        contexts: ["tab"],
    265        id: "test-top-level",
    266        title: "top-level",
    267      },
    268      { contexts: ["all"], id: "test-parent", title: "parent" },
    269      {
    270        id: "test-click-a",
    271        parentId: "test-parent",
    272        title: "click A",
    273      },
    274      {
    275        id: "test-click-b",
    276        parentId: "test-parent",
    277        title: "click B",
    278      },
    279    ],
    280    { checkSaved: true }
    281  );
    282 
    283  equal(
    284    extension.extension.backgroundState,
    285    "stopped",
    286    "background is not running"
    287  );
    288  await extension.wakeupBackground();
    289  lastError = await extension.awaitMessage("create");
    290  Assert.equal(
    291    lastError,
    292    "The menu id test-top-level already exists in menus.create.",
    293    "correct error creating menu"
    294  );
    295 
    296  let promisePersistedMenusUpdated = TestUtils.topicObserved(
    297    "webext-persisted-menus-updated"
    298  );
    299 
    300  extension.sendMessage("updatemenu");
    301  await extension.awaitMessage("updated");
    302  await extension.terminateBackground();
    303 
    304  // Title change is persisted.
    305  // (awaiting on the promise resolved when the ExtensionMenus
    306  // DeferredTask are been executed and the data expected to be
    307  // written on disk, to confirm the menu data is being persisted
    308  // on disk also while the extension and the app are still
    309  // running).
    310  await promisePersistedMenusUpdated;
    311 
    312  await expectExtensionMenus(
    313    extension,
    314    [
    315      {
    316        contexts: ["tab"],
    317        id: "test-top-level",
    318        title: "top-level",
    319      },
    320      { contexts: ["all"], id: "test-parent", title: "parent" },
    321      {
    322        id: "test-click-a",
    323        parentId: "test-parent",
    324        title: "click updated",
    325      },
    326      {
    327        id: "test-click-b",
    328        parentId: "test-parent",
    329        title: "click B",
    330      },
    331    ],
    332    { checkSaved: true }
    333  );
    334 
    335  await extension.wakeupBackground();
    336  lastError = await extension.awaitMessage("create");
    337  Assert.equal(
    338    lastError,
    339    "The menu id test-top-level already exists in menus.create.",
    340    "correct error creating menu"
    341  );
    342 
    343  extension.sendMessage("removemenu");
    344  await extension.awaitMessage("updated");
    345  await extension.terminateBackground();
    346 
    347  // menu removed
    348  await expectExtensionMenus(extension, [
    349    {
    350      contexts: ["tab"],
    351      id: "test-top-level",
    352      title: "top-level",
    353    },
    354    { contexts: ["all"], id: "test-parent", title: "parent" },
    355    {
    356      id: "test-click-a",
    357      parentId: "test-parent",
    358      title: "click updated",
    359    },
    360  ]);
    361 
    362  await extension.wakeupBackground();
    363  lastError = await extension.awaitMessage("create");
    364  Assert.equal(
    365    lastError,
    366    "The menu id test-top-level already exists in menus.create.",
    367    "correct error creating menu"
    368  );
    369 
    370  promisePersistedMenusUpdated = TestUtils.topicObserved(
    371    "webext-persisted-menus-updated"
    372  );
    373 
    374  extension.sendMessage("removeall");
    375  await extension.awaitMessage("updated");
    376  await extension.terminateBackground();
    377 
    378  // menus removed
    379 
    380  // We expect the persisted menus store to still have an
    381  // entry for the extension even when all menus have been
    382  // removed.
    383  await promisePersistedMenusUpdated;
    384  equal(
    385    await ExtensionMenus._hasStoredExtensionData(extension.id),
    386    true,
    387    "persisted menus store have an entry for the test extension"
    388  );
    389  await expectExtensionMenus(extension, [], { checkSaved: true });
    390 
    391  promisePersistedMenusUpdated = TestUtils.topicObserved(
    392    "webext-persisted-menus-updated"
    393  );
    394  await extension.unload();
    395  await promisePersistedMenusUpdated;
    396 
    397  // Expect the entry to have been removed completely from
    398  // the persised menus store the test extension is uninstalled.
    399  equal(
    400    await ExtensionMenus._hasStoredExtensionData(extension.id),
    401    false,
    402    "uninstalled extension should NOT have an entry in the persisted menus store"
    403  );
    404 });
    405 
    406 add_task(async function test_menu_persisted_cleared_after_ext_update() {
    407  async function background() {
    408    browser.test.onMessage.addListener(async (action, properties) => {
    409      browser.test.log(`onMessage ${action}`);
    410      switch (action) {
    411        case "create":
    412          await new Promise(resolve => {
    413            browser.menus.create(properties, resolve);
    414          });
    415          break;
    416        default:
    417          browser.test.fail(`Got unexpected test message "${action}"`);
    418          break;
    419      }
    420      browser.test.sendMessage("updated");
    421    });
    422  }
    423 
    424  const extension = ExtensionTestUtils.loadExtension(
    425    getExtension("test-nesting@mochitest", background, "permanent", "1.0")
    426  );
    427  await extension.startup();
    428 
    429  extension.sendMessage("create", {
    430    id: "stored-menu",
    431    contexts: ["all"],
    432    title: "some-menu",
    433  });
    434  await extension.awaitMessage("updated");
    435 
    436  const expectedMenus = [
    437    { contexts: ["all"], id: "stored-menu", title: "some-menu" },
    438  ];
    439  await expectExtensionMenus(extension, expectedMenus);
    440 
    441  info(
    442    "Re-install the same add-on version and expect persisted menus to still exist"
    443  );
    444  await extension.upgrade(
    445    getExtension("test-nesting@mochitest", background, "permanent", "1.0")
    446  );
    447  await expectExtensionMenus(extension, expectedMenus);
    448 
    449  info(
    450    "Upgrade to a new add-on version and expect persisted menus to be cleared"
    451  );
    452  await extension.upgrade(
    453    getExtension("test-nesting@mochitest", background, "permanent", "2.0")
    454  );
    455  await expectExtensionMenus(extension, []);
    456 
    457  await extension.unload();
    458 });
    459 
    460 add_task(async function test_menu_nested() {
    461  async function background() {
    462    browser.test.onMessage.addListener(async (action, properties) => {
    463      browser.test.log(`onMessage ${action}`);
    464      switch (action) {
    465        case "create":
    466          await new Promise(resolve => {
    467            browser.menus.create(properties, resolve);
    468          });
    469          break;
    470        case "update":
    471          {
    472            let { id, ...update } = properties;
    473            await browser.menus.update(id, update);
    474          }
    475          break;
    476        case "remove":
    477          {
    478            let { id } = properties;
    479            await browser.menus.remove(id);
    480          }
    481          break;
    482        case "removeAll":
    483          await browser.menus.removeAll();
    484          break;
    485      }
    486      browser.test.sendMessage("updated");
    487    });
    488  }
    489 
    490  const extension = ExtensionTestUtils.loadExtension(
    491    getExtension("test-nesting@mochitest", background, "permanent")
    492  );
    493  await extension.startup();
    494 
    495  extension.sendMessage("create", {
    496    id: "first",
    497    contexts: ["all"],
    498    title: "first",
    499  });
    500  await extension.awaitMessage("updated");
    501  await expectExtensionMenus(extension, [
    502    { contexts: ["all"], id: "first", title: "first" },
    503  ]);
    504 
    505  extension.sendMessage("create", {
    506    id: "second",
    507    contexts: ["all"],
    508    title: "second",
    509  });
    510  await extension.awaitMessage("updated");
    511  await expectExtensionMenus(extension, [
    512    { contexts: ["all"], id: "first", title: "first" },
    513    { contexts: ["all"], id: "second", title: "second" },
    514  ]);
    515 
    516  extension.sendMessage("create", {
    517    id: "third",
    518    contexts: ["all"],
    519    title: "third",
    520    parentId: "first",
    521  });
    522  await extension.awaitMessage("updated");
    523  await expectExtensionMenus(extension, [
    524    { contexts: ["all"], id: "first", title: "first" },
    525    { contexts: ["all"], id: "second", title: "second" },
    526    {
    527      contexts: ["all"],
    528      id: "third",
    529      parentId: "first",
    530      title: "third",
    531    },
    532  ]);
    533 
    534  extension.sendMessage("create", {
    535    id: "fourth",
    536    contexts: ["all"],
    537    title: "fourth",
    538  });
    539  await extension.awaitMessage("updated");
    540  await expectExtensionMenus(extension, [
    541    { contexts: ["all"], id: "first", title: "first" },
    542    { contexts: ["all"], id: "second", title: "second" },
    543    {
    544      contexts: ["all"],
    545      id: "third",
    546      parentId: "first",
    547      title: "third",
    548    },
    549    { contexts: ["all"], id: "fourth", title: "fourth" },
    550  ]);
    551 
    552  extension.sendMessage("update", {
    553    id: "first",
    554    parentId: "second",
    555  });
    556  await extension.awaitMessage("updated");
    557  await expectExtensionMenus(extension, [
    558    { contexts: ["all"], id: "second", title: "second" },
    559    { contexts: ["all"], id: "fourth", title: "fourth" },
    560    {
    561      contexts: ["all"],
    562      id: "first",
    563      title: "first",
    564      parentId: "second",
    565    },
    566    {
    567      contexts: ["all"],
    568      id: "third",
    569      parentId: "first",
    570      title: "third",
    571    },
    572  ]);
    573 
    574  await AddonTestUtils.promiseShutdownManager();
    575  // We need to attach an event listener before the
    576  // startup event is emitted.  Fortunately, we
    577  // emit via Management before emitting on extension.
    578  let promiseMenus;
    579  Management.once("startup", (kind, ext) => {
    580    info(`management ${kind} ${ext.id}`);
    581    promiseMenus = promiseExtensionEvent(
    582      { extension: ext },
    583      "webext-menus-created"
    584    );
    585  });
    586  await AddonTestUtils.promiseStartupManager();
    587  await extension.awaitStartup();
    588  await extension.wakeupBackground();
    589 
    590  await expectExtensionMenus(
    591    extension,
    592    [
    593      { contexts: ["all"], id: "second", title: "second" },
    594      { contexts: ["all"], id: "fourth", title: "fourth" },
    595      {
    596        contexts: ["all"],
    597        id: "first",
    598        title: "first",
    599        parentId: "second",
    600      },
    601      {
    602        contexts: ["all"],
    603        id: "third",
    604        parentId: "first",
    605        title: "third",
    606      },
    607    ],
    608    { checkSaved: true }
    609  );
    610  // validate nesting
    611  let menus = await promiseMenus;
    612  equal(menus.get("first").parentId, "second", "menuitem parent is correct");
    613  equal(
    614    menus.get("second").children.length,
    615    1,
    616    "menuitem parent has correct number of children"
    617  );
    618  equal(
    619    menus.get("second").root.children.length,
    620    2, // second and forth
    621    "menuitem root has correct number of children"
    622  );
    623 
    624  extension.sendMessage("remove", {
    625    id: "second",
    626  });
    627  await extension.awaitMessage("updated");
    628  await expectExtensionMenus(extension, [
    629    { contexts: ["all"], id: "fourth", title: "fourth" },
    630  ]);
    631 
    632  extension.sendMessage("removeAll");
    633  await extension.awaitMessage("updated");
    634  await expectExtensionMenus(extension, []);
    635 
    636  await extension.unload();
    637 });
    638 
    639 add_task(async function test_ExtensionMenus_after_extension_hasShutdown() {
    640  const assertEmptyMenusManagersMap = () => {
    641    let weakMapKeys = ChromeUtils.nondeterministicGetWeakMapKeys(
    642      ExtensionMenus._menusManagers
    643    );
    644    Assert.deepEqual(
    645      weakMapKeys.length,
    646      0,
    647      "Expect ExtensionMenus._menusManagers weakmap to be empty"
    648    );
    649  };
    650 
    651  // Sanity check.
    652  assertEmptyMenusManagersMap();
    653 
    654  const addonId = "test-menu-after-shutdown@mochitest";
    655  const testExtWrapper = ExtensionTestUtils.loadExtension(
    656    getExtension(addonId, () => {}, "permanent")
    657  );
    658  await testExtWrapper.startup();
    659  const { extension } = testExtWrapper;
    660  Assert.equal(
    661    extension.hasShutdown,
    662    false,
    663    "Extension hasShutdown should be false"
    664  );
    665  await testExtWrapper.unload();
    666  Assert.equal(
    667    extension.hasShutdown,
    668    true,
    669    "Extension hasShutdown should be true"
    670  );
    671 
    672  // Sanity check.
    673  assertEmptyMenusManagersMap();
    674 
    675  await Assert.rejects(
    676    ExtensionMenus.asyncInitForExtension(extension),
    677    new RegExp(
    678      `Error on creating new ExtensionMenusManager after extension shutdown: ${addonId}`
    679    ),
    680    "Got the expected error on ExtensionMenus.asyncInitForExtension called for a shutdown extension"
    681  );
    682  assertEmptyMenusManagersMap();
    683 
    684  Assert.throws(
    685    () => ExtensionMenus.getMenus(extension),
    686    new RegExp(`No ExtensionMenusManager instance found for ${addonId}`),
    687    "Got the expected error on ExtensionMenus.getMenus called for a shutdown extension"
    688  );
    689  assertEmptyMenusManagersMap();
    690 });
    691 
    692 // This test ensures that menus created by an extension without a background page
    693 // are not persisted.
    694 add_task(async function test_extension_without_background() {
    695  let extension = ExtensionTestUtils.loadExtension({
    696    useAddonManager: "permanent",
    697    manifest: {
    698      permissions: ["menus"],
    699    },
    700    files: {
    701      "extpage.html": `<!DOCTYPE html><script src="extpage.js"></script>`,
    702      "extpage.js": extPageScriptWithMenusCreateAndUpdateTestHandler,
    703    },
    704  });
    705 
    706  async function testCreateMenu() {
    707    const extPageUrl = extension.extension.baseURI.resolve("extpage.html");
    708    let page = await ExtensionTestUtils.loadContentPage(extPageUrl);
    709    await extension.awaitMessage("extpage:ready");
    710    const menuDetails = { id: "test-menu", title: "menu title" };
    711    extension.sendMessage("menusCreate", menuDetails);
    712    await extension.awaitMessage("menusCreate:done");
    713    await page.close();
    714  }
    715 
    716  await extension.startup();
    717  await testCreateMenu();
    718 
    719  info(
    720    "Simulated browser restart and verify no menu was persisted or restored"
    721  );
    722  await mockBrowserRestart(extension, { waitForMenuRecreated: false });
    723  // Try to create the same menu again, if it does fail the menu was unexpectectly
    724  // restored.
    725  await testCreateMenu();
    726  equal(
    727    await ExtensionMenus._hasStoredExtensionData(extension.id),
    728    false,
    729    "Extensions without a background page should not have any data stored for their menus"
    730  );
    731  await extension.unload();
    732 });
    733 
    734 // Verify that corrupted menus store data is handled gracefully.
    735 add_task(async function test_corrupted_menus_store_data() {
    736  let extension = ExtensionTestUtils.loadExtension({
    737    useAddonManager: "permanent",
    738    manifest: {
    739      permissions: ["menus"],
    740      background: { persistent: false },
    741    },
    742    background: extPageScriptWithMenusCreateAndUpdateTestHandler,
    743  });
    744 
    745  await extension.startup();
    746  await extension.awaitMessage("extpage:ready");
    747 
    748  const menuDetails = { id: "test-menu", title: "menu title" };
    749  const menuDetailsUnsupported = {
    750    new_unsupported_property: "fake-prop-value",
    751  };
    752  const menuDetailsUpdate = { title: "Updated menu title" };
    753 
    754  extension.sendMessage("menusCreate", menuDetails);
    755  await extension.awaitMessage("menusCreate:done");
    756 
    757  let menus = ExtensionMenus.getMenus(extension.extension);
    758  Assert.deepEqual(
    759    menus.get("test-menu"),
    760    menuDetails,
    761    "Got the expected menuDetails from ExtensionMenus.getMenus"
    762  );
    763  // Inject invalid menu properties into the store to simulate
    764  // restoring menus from menus data stored by a future version
    765  // with additional menus properties older versions would not
    766  // have support for.
    767  //
    768  // This test covers additive changes, but technically changes
    769  // to the format of existing properties may not be handled
    770  // gracefully (but changes to the type/format of existing menu
    771  // properties are more likely to be part of a manifest version
    772  // update, and so they may be less likely, and downgrades not
    773  // officially supported).
    774  info("Inject unsupported properties in the persisted menu details");
    775  let store = ExtensionMenus._getStoreForTesting();
    776  menus.set("test-menu", { ...menuDetails, ...menuDetailsUnsupported });
    777  await store.updatePersistedMenus(extension.id, extension.version, menus);
    778  equal(
    779    await ExtensionMenus._hasStoredExtensionData(extension.id),
    780    true,
    781    "persisted menus store have an entry for the test extension"
    782  );
    783 
    784  // Mock a browser restart and verify the unsupported property injected
    785  // in the persisted menu data is handled gracefully.
    786  await mockBrowserRestart(extension);
    787  await extension.awaitMessage("extpage:ready");
    788  menus = ExtensionMenus.getMenus(extension.extension);
    789 
    790  info("Verify the recreated menu can still be updated as expected");
    791  extension.sendMessage("menusUpdate", menuDetails.id, menuDetailsUpdate);
    792  await extension.awaitMessage("menusUpdate:done");
    793  menus = ExtensionMenus.getMenus(extension.extension);
    794  Assert.deepEqual(
    795    menus.get("test-menu"),
    796    Object.assign({}, menuDetails, menuDetailsUnsupported, menuDetailsUpdate),
    797    "Got the expected menuDetails from ExtensionMenus.getMenus"
    798  );
    799 
    800  info("Inject orphan menu entry in the persisted menus data");
    801  store = ExtensionMenus._getStoreForTesting();
    802  const orphanedMenuDetails = {
    803    id: "orphaned-test-menu",
    804    parentId: "non-existing-parent-id",
    805    title: "An orphaned menu item",
    806  };
    807  menus.set(orphanedMenuDetails.id, orphanedMenuDetails);
    808  await store.updatePersistedMenus(extension.id, extension.version, menus);
    809 
    810  // Mock a browser restart and verify that orphaned menus
    811  // from the persisted menu data are handled gracefully.
    812  {
    813    const { messages } = await AddonTestUtils.promiseConsoleOutput(async () => {
    814      await mockBrowserRestart(extension);
    815      await extension.awaitMessage("extpage:ready");
    816    });
    817 
    818    // Make sure the test is hitting the expected error.
    819    AddonTestUtils.checkMessages(messages, {
    820      expected: [
    821        {
    822          message: new RegExp(
    823            `Unexpected error on recreating persisted menu ${orphanedMenuDetails.id} for ${extension.id}`
    824          ),
    825        },
    826      ],
    827    });
    828  }
    829 
    830  menus = ExtensionMenus.getMenus(extension.extension);
    831 
    832  info("Verify the recreated menu can still be updated as expected");
    833  extension.sendMessage("menusUpdate", menuDetails.id, menuDetailsUpdate);
    834  await extension.awaitMessage("menusUpdate:done");
    835  menus = ExtensionMenus.getMenus(extension.extension);
    836  Assert.deepEqual(
    837    menus.get("test-menu"),
    838    Object.assign({}, menuDetails, menuDetailsUnsupported, menuDetailsUpdate),
    839    "Got the expected menuDetails from ExtensionMenus.getMenus"
    840  );
    841 
    842  info("Verify the orphaned menu has been dropped");
    843  Assert.equal(
    844    menus.has(orphanedMenuDetails.id),
    845    false,
    846    "Expect orphaned menu to not exist anymore"
    847  );
    848 
    849  info("Verify invalid stored json menus data is handled gracefully");
    850 
    851  await AddonTestUtils.promiseShutdownManager();
    852  ExtensionMenus._recreateStoreForTesting();
    853  let menuStorePath = PathUtils.join(
    854    PathUtils.profileDir,
    855    ExtensionMenus.KVSTORE_DIRNAME
    856  );
    857  const kvstore = await KeyValueService.getOrCreateWithOptions(
    858    menuStorePath,
    859    "menus",
    860    { strategy: KeyValueService.RecoveryStrategy.RENAME }
    861  );
    862  await kvstore.put(extension.id, "invalid-json-data");
    863 
    864  {
    865    const { messages } = await AddonTestUtils.promiseConsoleOutput(async () => {
    866      await mockBrowserRestart(extension, { shutdownAndRecreateStore: false });
    867      await extension.awaitMessage("extpage:ready");
    868    });
    869 
    870    // Make sure the test is hitting the expected error.
    871    AddonTestUtils.checkMessages(messages, {
    872      expected: [
    873        {
    874          message: new RegExp(
    875            `Error loading ${extension.id} persisted menus: SyntaxError`
    876          ),
    877        },
    878      ],
    879    });
    880  }
    881 
    882  menus = ExtensionMenus.getMenus(extension.extension);
    883  Assert.equal(menus.size, 0, "Expect persisted menus map to be empty");
    884 
    885  // Verify new menu can still be created.
    886  extension.sendMessage("menusCreate", menuDetails);
    887  await extension.awaitMessage("menusCreate:done");
    888  menus = ExtensionMenus.getMenus(extension.extension);
    889  Assert.equal(menus.size, 1, "Expect persisted menus map to not be empty");
    890 
    891  await extension.unload();
    892 });
    893 
    894 // Verify that ExtensionMenus.clearPersistedMenusOnUninstall isn't going
    895 // to create an unnecessary menus kvstore directory when that directory does
    896 // not exist yet, e.g. because the extension never used the contextMenus API.
    897 // This includes builds that do not support the contextMenus API.
    898 add_task(async function test_unnecessary_kvstore_dir_not_created() {
    899  // Create a new store instance to ensure the lazy store initialization
    900  // isn't already executed due to the previous test tasks.
    901  await AddonTestUtils.promiseRestartManager();
    902  ExtensionMenus._recreateStoreForTesting();
    903 
    904  let menuStorePath = PathUtils.join(
    905    PathUtils.profileDir,
    906    ExtensionMenus.KVSTORE_DIRNAME
    907  );
    908 
    909  await IOUtils.remove(menuStorePath, { ignoreAbsent: true, recursive: true });
    910  equal(
    911    await IOUtils.exists(menuStorePath),
    912    false,
    913    `Expect no ${ExtensionMenus.KVSTORE_DIRNAME} in the Gecko profile`
    914  );
    915 
    916  await ExtensionMenus.clearPersistedMenusOnUninstall("fakeextid@test");
    917 
    918  equal(
    919    await IOUtils.exists(menuStorePath),
    920    false,
    921    `Expect no ${ExtensionMenus.KVSTORE_DIRNAME} in the Gecko profile`
    922  );
    923 });