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 });