test_extension_storage_actor.js (34883B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 /* globals browser */ 5 6 "use strict"; 7 8 const { ExtensionTestUtils } = ChromeUtils.importESModule( 9 "resource://testing-common/ExtensionXPCShellUtils.sys.mjs" 10 ); 11 12 const { TestUtils } = ChromeUtils.importESModule( 13 "resource://testing-common/TestUtils.sys.mjs" 14 ); 15 16 const { 17 createMissingIndexedDBDirs, 18 extensionScriptWithMessageListener, 19 ext_no_bg, 20 getExtensionConfig, 21 openAddonStoragePanel, 22 shutdown, 23 startupExtension, 24 } = require("resource://test/webextension-helpers.js"); 25 26 const l10n = new Localization(["devtools/client/storage.ftl"], true); 27 const sessionString = l10n.formatValueSync("storage-expires-session"); 28 29 // Ignore rejection related to the storage.onChanged listener being removed while the extension context is being closed. 30 const { PromiseTestUtils } = ChromeUtils.importESModule( 31 "resource://testing-common/PromiseTestUtils.sys.mjs" 32 ); 33 PromiseTestUtils.allowMatchingRejectionsGlobally( 34 /Message manager disconnected/ 35 ); 36 PromiseTestUtils.allowMatchingRejectionsGlobally( 37 /sendRemoveListener on closed conduit/ 38 ); 39 40 const { createAppInfo, promiseStartupManager } = AddonTestUtils; 41 42 const LEAVE_UUID_PREF = "extensions.webextensions.keepUuidOnUninstall"; 43 const LEAVE_STORAGE_PREF = "extensions.webextensions.keepStorageOnUninstall"; 44 45 AddonTestUtils.init(this); 46 createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42"); 47 48 ExtensionTestUtils.init(this); 49 50 add_setup(async function setup() { 51 await promiseStartupManager(); 52 const dir = createMissingIndexedDBDirs(); 53 54 Assert.ok( 55 dir.exists(), 56 "Should have a 'storage/permanent' dir in the profile dir" 57 ); 58 }); 59 60 add_task(async function test_extension_store_exists() { 61 const extension = await startupExtension(getExtensionConfig()); 62 63 const { commands, extensionStorage } = await openAddonStoragePanel( 64 extension.id 65 ); 66 67 ok(extensionStorage, "Should have an extensionStorage store"); 68 69 await shutdown(extension, commands); 70 }); 71 72 add_task( 73 { 74 // This test currently fails if the extension runs in the main process 75 // like in Thunderbird (see bug 1575183 comment #15 for details). 76 skip_if: () => !WebExtensionPolicy.useRemoteWebExtensions, 77 }, 78 async function test_extension_origin_matches_debugger_target() { 79 async function background() { 80 // window is available in background scripts 81 // eslint-disable-next-line no-undef 82 browser.test.sendMessage("extension-origin", window.location.origin); 83 } 84 85 const extension = await startupExtension( 86 getExtensionConfig({ background }) 87 ); 88 89 const { commands, extensionStorage } = await openAddonStoragePanel( 90 extension.id 91 ); 92 93 const { hosts } = extensionStorage; 94 const expectedHost = await extension.awaitMessage("extension-origin"); 95 ok( 96 expectedHost in hosts, 97 "Should have the expected extension host in the extensionStorage store" 98 ); 99 100 await shutdown(extension, commands); 101 } 102 ); 103 104 /** 105 * Test case: Background page modifies items while storage panel is open. 106 * - Load extension with background page. 107 * - Open the add-on debugger storage panel. 108 * - With the panel still open, from the extension background page: 109 * - Bulk add storage items 110 * - Edit the values of some of the storage items 111 * - Remove some storage items 112 * - Clear all storage items 113 * - For each modification, the storage data in the panel should match the 114 * changes made by the extension. 115 */ 116 add_task(async function test_panel_live_updates() { 117 const extension = await startupExtension( 118 getExtensionConfig({ background: extensionScriptWithMessageListener }) 119 ); 120 121 const { commands, extensionStorage } = await openAddonStoragePanel( 122 extension.id 123 ); 124 125 const host = await extension.awaitMessage("extension-origin"); 126 127 let { data } = await extensionStorage.getStoreObjects(host); 128 Assert.deepEqual(data, [], "Got the expected results on empty storage.local"); 129 130 info("Waiting for extension to bulk add 50 items to storage local"); 131 const bulkStorageItems = {}; 132 // limited by MAX_STORE_OBJECT_COUNT in devtools/server/actors/resources/storage/index.js 133 const numItems = 2; 134 for (let i = 1; i <= numItems; i++) { 135 bulkStorageItems[i] = i; 136 } 137 138 // fireOnChanged avoids the race condition where the extension 139 // modifies storage then immediately tries to access storage before 140 // the storage actor has finished updating. 141 extension.sendMessage("storage-local-fireOnChanged"); 142 extension.sendMessage("storage-local-set", { 143 ...bulkStorageItems, 144 a: 123, 145 b: [4, 5], 146 c: { d: 678 }, 147 d: true, 148 e: "hi", 149 f: null, 150 }); 151 await extension.awaitMessage("storage-local-set:done"); 152 await extension.awaitMessage("storage-local-onChanged"); 153 154 info( 155 "Confirming items added by extension match items in extensionStorage store" 156 ); 157 const bulkStorageObjects = []; 158 for (const [name, value] of Object.entries(bulkStorageItems)) { 159 bulkStorageObjects.push({ 160 area: "local", 161 name, 162 value: { str: String(value) }, 163 isValueEditable: true, 164 }); 165 } 166 data = (await extensionStorage.getStoreObjects(host, null, { sessionString })) 167 .data; 168 Assert.deepEqual( 169 data, 170 [ 171 ...bulkStorageObjects, 172 { 173 area: "local", 174 name: "a", 175 value: { str: "123" }, 176 isValueEditable: true, 177 }, 178 { 179 area: "local", 180 name: "b", 181 value: { str: "[4,5]" }, 182 isValueEditable: true, 183 }, 184 { 185 area: "local", 186 name: "c", 187 value: { str: '{"d":678}' }, 188 isValueEditable: true, 189 }, 190 { 191 area: "local", 192 name: "d", 193 value: { str: "true" }, 194 isValueEditable: true, 195 }, 196 { 197 area: "local", 198 name: "e", 199 value: { str: "hi" }, 200 isValueEditable: true, 201 }, 202 { 203 area: "local", 204 name: "f", 205 value: { str: "null" }, 206 isValueEditable: true, 207 }, 208 ], 209 "Got the expected results on populated storage.local" 210 ); 211 212 info("Waiting for extension to edit a few storage item values"); 213 extension.sendMessage("storage-local-fireOnChanged"); 214 extension.sendMessage("storage-local-set", { 215 a: ["c", "d"], 216 b: 456, 217 c: false, 218 }); 219 await extension.awaitMessage("storage-local-set:done"); 220 await extension.awaitMessage("storage-local-onChanged"); 221 222 info( 223 "Confirming items edited by extension match items in extensionStorage store" 224 ); 225 data = (await extensionStorage.getStoreObjects(host, null, { sessionString })) 226 .data; 227 Assert.deepEqual( 228 data, 229 [ 230 ...bulkStorageObjects, 231 { 232 area: "local", 233 name: "a", 234 value: { str: '["c","d"]' }, 235 isValueEditable: true, 236 }, 237 { 238 area: "local", 239 name: "b", 240 value: { str: "456" }, 241 isValueEditable: true, 242 }, 243 { 244 area: "local", 245 name: "c", 246 value: { str: "false" }, 247 isValueEditable: true, 248 }, 249 { 250 area: "local", 251 name: "d", 252 value: { str: "true" }, 253 isValueEditable: true, 254 }, 255 { 256 area: "local", 257 name: "e", 258 value: { str: "hi" }, 259 isValueEditable: true, 260 }, 261 { 262 area: "local", 263 name: "f", 264 value: { str: "null" }, 265 isValueEditable: true, 266 }, 267 ], 268 "Got the expected results on populated storage.local" 269 ); 270 271 info("Waiting for extension to remove a few storage item values"); 272 extension.sendMessage("storage-local-fireOnChanged"); 273 extension.sendMessage("storage-local-remove", ["d", "e", "f"]); 274 await extension.awaitMessage("storage-local-remove:done"); 275 await extension.awaitMessage("storage-local-onChanged"); 276 277 info( 278 "Confirming items removed by extension were removed in extensionStorage store" 279 ); 280 data = (await extensionStorage.getStoreObjects(host, null, { sessionString })) 281 .data; 282 Assert.deepEqual( 283 data, 284 [ 285 ...bulkStorageObjects, 286 { 287 area: "local", 288 name: "a", 289 value: { str: '["c","d"]' }, 290 isValueEditable: true, 291 }, 292 { 293 area: "local", 294 name: "b", 295 value: { str: "456" }, 296 isValueEditable: true, 297 }, 298 { 299 area: "local", 300 name: "c", 301 value: { str: "false" }, 302 isValueEditable: true, 303 }, 304 ], 305 "Got the expected results on populated storage.local" 306 ); 307 308 info("Waiting for extension to remove all remaining storage items"); 309 extension.sendMessage("storage-local-fireOnChanged"); 310 extension.sendMessage("storage-local-clear"); 311 await extension.awaitMessage("storage-local-clear:done"); 312 await extension.awaitMessage("storage-local-onChanged"); 313 314 info("Confirming extensionStorage store was cleared"); 315 data = (await extensionStorage.getStoreObjects(host)).data; 316 Assert.deepEqual( 317 data, 318 [], 319 "Got the expected results on populated storage.local" 320 ); 321 322 await shutdown(extension, commands); 323 }); 324 325 /** 326 * Test case: No bg page. Transient page adds item before storage panel opened. 327 * - Load extension with no background page. 328 * - Open an extension page in a tab that adds a local storage item. 329 * - With the extension page still open, open the add-on storage panel. 330 * - The data in the storage panel should match the items added by the extension. 331 */ 332 add_task( 333 async function test_panel_data_matches_extension_with_transient_page_open() { 334 const extension = await startupExtension( 335 getExtensionConfig({ files: ext_no_bg.files }) 336 ); 337 338 const url = extension.extension.baseURI.resolve( 339 "extension_page_in_tab.html" 340 ); 341 const contentPage = await ExtensionTestUtils.loadContentPage(url, { 342 extension, 343 }); 344 345 const host = await extension.awaitMessage("extension-origin"); 346 347 extension.sendMessage("storage-local-set", { a: 123 }); 348 await extension.awaitMessage("storage-local-set:done"); 349 350 const { commands, extensionStorage } = await openAddonStoragePanel( 351 extension.id 352 ); 353 354 const { data } = await extensionStorage.getStoreObjects(host); 355 Assert.deepEqual( 356 data, 357 [ 358 { 359 area: "local", 360 name: "a", 361 value: { str: "123" }, 362 isValueEditable: true, 363 }, 364 ], 365 "Got the expected results on populated storage.local" 366 ); 367 368 await contentPage.close(); 369 await shutdown(extension, commands); 370 } 371 ); 372 373 /** 374 * Test case: No bg page. Transient page adds item then closes before storage panel opened. 375 * - Load extension with no background page. 376 * - Open an extension page in a tab that adds a local storage item. 377 * - Close all extension pages. 378 * - Open the add-on storage panel. 379 * - The data in the storage panel should match the item added by the extension. 380 */ 381 add_task(async function test_panel_data_matches_extension_with_no_pages_open() { 382 const extension = await startupExtension( 383 getExtensionConfig({ files: ext_no_bg.files }) 384 ); 385 386 const url = extension.extension.baseURI.resolve("extension_page_in_tab.html"); 387 const contentPage = await ExtensionTestUtils.loadContentPage(url, { 388 extension, 389 }); 390 391 const host = await extension.awaitMessage("extension-origin"); 392 393 extension.sendMessage("storage-local-fireOnChanged"); 394 extension.sendMessage("storage-local-set", { a: 123 }); 395 await extension.awaitMessage("storage-local-set:done"); 396 await extension.awaitMessage("storage-local-onChanged"); 397 await contentPage.close(); 398 399 const { commands, extensionStorage } = await openAddonStoragePanel( 400 extension.id 401 ); 402 403 const { data } = await extensionStorage.getStoreObjects(host); 404 Assert.deepEqual( 405 data, 406 [ 407 { 408 area: "local", 409 name: "a", 410 value: { str: "123" }, 411 isValueEditable: true, 412 }, 413 ], 414 "Got the expected results on populated storage.local" 415 ); 416 417 await shutdown(extension, commands); 418 }); 419 420 /** 421 * Test case: No bg page. Storage panel live updates when a transient page adds an item. 422 * - Load extension with no background page. 423 * - Open the add-on storage panel. 424 * - With the storage panel still open, open an extension page in a new tab that adds an 425 * item. 426 * - The data in the storage panel should live update to match the item added by the 427 * extension. 428 * - If an extension page adds the same data again, the data in the storage panel should 429 * not change. 430 */ 431 add_task( 432 async function test_panel_data_live_updates_for_extension_without_bg_page() { 433 const extension = await startupExtension( 434 getExtensionConfig({ files: ext_no_bg.files }) 435 ); 436 437 const { commands, extensionStorage } = await openAddonStoragePanel( 438 extension.id 439 ); 440 441 const url = extension.extension.baseURI.resolve( 442 "extension_page_in_tab.html" 443 ); 444 const contentPage = await ExtensionTestUtils.loadContentPage(url, { 445 extension, 446 }); 447 448 const host = await extension.awaitMessage("extension-origin"); 449 450 let { data } = await extensionStorage.getStoreObjects(host); 451 Assert.deepEqual( 452 data, 453 [], 454 "Got the expected results on empty storage.local" 455 ); 456 457 extension.sendMessage("storage-local-fireOnChanged"); 458 extension.sendMessage("storage-local-set", { a: 123 }); 459 await extension.awaitMessage("storage-local-set:done"); 460 await extension.awaitMessage("storage-local-onChanged"); 461 462 data = (await extensionStorage.getStoreObjects(host)).data; 463 Assert.deepEqual( 464 data, 465 [ 466 { 467 area: "local", 468 name: "a", 469 value: { str: "123" }, 470 isValueEditable: true, 471 }, 472 ], 473 "Got the expected results on populated storage.local" 474 ); 475 476 extension.sendMessage("storage-local-fireOnChanged"); 477 extension.sendMessage("storage-local-set", { a: 123 }); 478 await extension.awaitMessage("storage-local-set:done"); 479 await extension.awaitMessage("storage-local-onChanged"); 480 481 data = (await extensionStorage.getStoreObjects(host)).data; 482 Assert.deepEqual( 483 data, 484 [ 485 { 486 area: "local", 487 name: "a", 488 value: { str: "123" }, 489 isValueEditable: true, 490 }, 491 ], 492 "The results are unchanged when an extension page adds duplicate items" 493 ); 494 495 await contentPage.close(); 496 await shutdown(extension, commands); 497 } 498 ); 499 500 /** 501 * Test case: Bg page adds item while storage panel is open. Panel edits item's value. 502 * - Load extension with background page. 503 * - Open the add-on storage panel. 504 * - With the storage panel still open, add item from the background page. 505 * - Edit the value of the item in the storage panel 506 * - The data in the storage panel should match the item added by the extension. 507 * - The storage actor is correctly parsing and setting the string representation of 508 * the value in the storage local database when the item's value is edited in the 509 * storage panel 510 */ 511 add_task( 512 async function test_editing_items_in_panel_parses_supported_values_correctly() { 513 const extension = await startupExtension( 514 getExtensionConfig({ background: extensionScriptWithMessageListener }) 515 ); 516 517 const host = await extension.awaitMessage("extension-origin"); 518 519 const { commands, extensionStorage } = await openAddonStoragePanel( 520 extension.id 521 ); 522 523 const oldItem = { a: 123 }; 524 const key = Object.keys(oldItem)[0]; 525 const oldValue = oldItem[key]; 526 // A tuple representing information for a new value entered into the panel for oldItem: 527 // [ 528 // value, 529 // editItem string representation of value, 530 // toStoreObject string representation of value, 531 // ] 532 const valueInfo = [ 533 [true, "true", "true"], 534 ["hi", "hi", "hi"], 535 [456, "456", "456"], 536 [{ b: 789 }, "{b: 789}", '{"b":789}'], 537 [[1, 2, 3], "[1, 2, 3]", "[1,2,3]"], 538 [null, "null", "null"], 539 ]; 540 for (const [value, editItemValueStr, toStoreObjectValueStr] of valueInfo) { 541 info("Setting a storage item through the extension"); 542 extension.sendMessage("storage-local-fireOnChanged"); 543 extension.sendMessage("storage-local-set", oldItem); 544 await extension.awaitMessage("storage-local-set:done"); 545 await extension.awaitMessage("storage-local-onChanged"); 546 547 info( 548 "Editing the storage item in the panel with a new value of a different type" 549 ); 550 // When the user edits an item in the panel, they are entering a string into a 551 // textbox. This string is parsed by the storage actor's editItem method. 552 await extensionStorage.editItem({ 553 host, 554 field: "value", 555 items: { name: key, value: editItemValueStr }, 556 oldValue, 557 }); 558 559 info( 560 "Verifying item in the storage actor matches the item edited in the panel" 561 ); 562 const { data } = await extensionStorage.getStoreObjects(host); 563 Assert.deepEqual( 564 data, 565 [ 566 { 567 area: "local", 568 name: key, 569 value: { str: toStoreObjectValueStr }, 570 isValueEditable: true, 571 }, 572 ], 573 "Got the expected results on populated storage.local" 574 ); 575 576 // The view layer is separate from the database layer; therefore while values are 577 // stringified (via toStoreObject) for display in the client, the value (and its type) 578 // in the database is unchanged. 579 info( 580 "Verifying the expected new value matches the value fetched in the extension" 581 ); 582 extension.sendMessage("storage-local-get", key); 583 const extItem = await extension.awaitMessage("storage-local-get:done"); 584 Assert.deepEqual( 585 value, 586 extItem[key], 587 `The string value ${editItemValueStr} was correctly parsed to ${value}` 588 ); 589 } 590 591 await shutdown(extension, commands); 592 } 593 ); 594 595 /** 596 * Test case: Modifying storage items from the panel update extension storage local data. 597 * - Load extension with background page. 598 * - Open the add-on storage panel. From the panel: 599 * - Edit the value of a storage item, 600 * - Remove a storage item, 601 * - Remove all of the storage items, 602 * - For each modification, the storage data retrieved by the extension should match the 603 * data in the panel. 604 */ 605 add_task( 606 async function test_modifying_items_in_panel_updates_extension_storage_data() { 607 const extension = await startupExtension( 608 getExtensionConfig({ background: extensionScriptWithMessageListener }) 609 ); 610 611 const host = await extension.awaitMessage("extension-origin"); 612 613 const { commands, extensionStorage } = await openAddonStoragePanel( 614 extension.id 615 ); 616 617 const DEFAULT_VALUE = "value"; // global in devtools/server/actors/resources/storage/index.js 618 let items = { 619 guid_1: DEFAULT_VALUE, 620 guid_2: DEFAULT_VALUE, 621 guid_3: DEFAULT_VALUE, 622 }; 623 624 info("Adding storage items from the extension"); 625 let storesUpdate = extensionStorage.once("single-store-update"); 626 extension.sendMessage("storage-local-set", items); 627 await extension.awaitMessage("storage-local-set:done"); 628 629 info("Waiting for the storage actor to emit a 'stores-update' event"); 630 let data = await storesUpdate; 631 Assert.deepEqual( 632 { 633 added: { 634 extensionStorage: { 635 [host]: ["guid_1", "guid_2", "guid_3"], 636 }, 637 }, 638 changed: undefined, 639 deleted: undefined, 640 }, 641 data, 642 "The change data from the storage actor's 'stores-update' event matches the changes made in the client." 643 ); 644 645 info("Waiting for panel to edit some items"); 646 storesUpdate = extensionStorage.once("single-store-update"); 647 await extensionStorage.editItem({ 648 host, 649 field: "value", 650 items: { name: "guid_1", value: "anotherValue" }, 651 DEFAULT_VALUE, 652 }); 653 654 info("Waiting for the storage actor to emit a 'stores-update' event"); 655 data = await storesUpdate; 656 Assert.deepEqual( 657 { 658 added: undefined, 659 changed: { 660 extensionStorage: { 661 [host]: ["guid_1"], 662 }, 663 }, 664 deleted: undefined, 665 }, 666 data, 667 "The change data from the storage actor's 'stores-update' event matches the changes made in the client." 668 ); 669 670 items = { 671 guid_1: "anotherValue", 672 guid_2: DEFAULT_VALUE, 673 guid_3: DEFAULT_VALUE, 674 }; 675 extension.sendMessage("storage-local-get", Object.keys(items)); 676 let extItems = await extension.awaitMessage("storage-local-get:done"); 677 Assert.deepEqual( 678 items, 679 extItems, 680 `The storage items in the extension match the items in the panel` 681 ); 682 683 info("Waiting for panel to remove an item"); 684 storesUpdate = extensionStorage.once("single-store-update"); 685 await extensionStorage.removeItem(host, "guid_3"); 686 687 info("Waiting for the storage actor to emit a 'stores-update' event"); 688 data = await storesUpdate; 689 Assert.deepEqual( 690 { 691 added: undefined, 692 changed: undefined, 693 deleted: { 694 extensionStorage: { 695 [host]: ["guid_3"], 696 }, 697 }, 698 }, 699 data, 700 "The change data from the storage actor's 'stores-update' event matches the changes made in the client." 701 ); 702 703 items = { 704 guid_1: "anotherValue", 705 guid_2: DEFAULT_VALUE, 706 }; 707 extension.sendMessage("storage-local-get", Object.keys(items)); 708 extItems = await extension.awaitMessage("storage-local-get:done"); 709 Assert.deepEqual( 710 items, 711 extItems, 712 `The storage items in the extension match the items in the panel` 713 ); 714 715 info("Waiting for panel to remove all items"); 716 const storesCleared = extensionStorage.once("single-store-cleared"); 717 await extensionStorage.removeAll(host); 718 719 info("Waiting for the storage actor to emit a 'stores-cleared' event"); 720 data = await storesCleared; 721 Assert.deepEqual( 722 { 723 clearedHostsOrPaths: { 724 [host]: [], 725 }, 726 }, 727 data, 728 "The change data from the storage actor's 'stores-cleared' event matches the changes made in the client." 729 ); 730 731 items = {}; 732 extension.sendMessage("storage-local-get", Object.keys(items)); 733 extItems = await extension.awaitMessage("storage-local-get:done"); 734 Assert.deepEqual( 735 items, 736 extItems, 737 `The storage items in the extension match the items in the panel` 738 ); 739 740 await shutdown(extension, commands); 741 } 742 ); 743 744 /** 745 * Test case: Storage panel shows extension storage data added prior to extension startup 746 * - Load extension that adds a storage item 747 * - Uninstall the extension 748 * - Reinstall the extension 749 * - Open the add-on storage panel. 750 * - The data in the storage panel should match the data added the first time the extension 751 * was installed 752 * Related test case: Storage panel shows extension storage data when an extension that has 753 * already migrated to the IndexedDB storage backend prior to extension startup adds 754 * another storage item. 755 * - (Building from previous steps) 756 * - The reinstalled extension adds a storage item 757 * - The data in the storage panel should live update with both items: the item added from 758 * the first and the item added from the reinstall. 759 */ 760 add_task( 761 async function test_panel_data_matches_data_added_prior_to_ext_startup() { 762 // The pref to leave the addonid->uuid mapping around after uninstall so that we can 763 // re-attach to the same storage 764 Services.prefs.setBoolPref(LEAVE_UUID_PREF, true); 765 766 // The pref to prevent cleaning up storage on uninstall 767 Services.prefs.setBoolPref(LEAVE_STORAGE_PREF, true); 768 769 let extension = await startupExtension( 770 getExtensionConfig({ background: extensionScriptWithMessageListener }) 771 ); 772 773 const host = await extension.awaitMessage("extension-origin"); 774 775 extension.sendMessage("storage-local-set", { a: 123 }); 776 await extension.awaitMessage("storage-local-set:done"); 777 778 await shutdown(extension); 779 780 // Reinstall the same extension 781 extension = await startupExtension( 782 getExtensionConfig({ background: extensionScriptWithMessageListener }) 783 ); 784 785 await extension.awaitMessage("extension-origin"); 786 787 const { commands, extensionStorage } = await openAddonStoragePanel( 788 extension.id 789 ); 790 791 let { data } = await extensionStorage.getStoreObjects(host); 792 Assert.deepEqual( 793 data, 794 [ 795 { 796 area: "local", 797 name: "a", 798 value: { str: "123" }, 799 isValueEditable: true, 800 }, 801 ], 802 "Got the expected results on populated storage.local" 803 ); 804 805 // Related test case 806 extension.sendMessage("storage-local-fireOnChanged"); 807 extension.sendMessage("storage-local-set", { b: 456 }); 808 await extension.awaitMessage("storage-local-set:done"); 809 await extension.awaitMessage("storage-local-onChanged"); 810 811 data = ( 812 await extensionStorage.getStoreObjects(host, null, { sessionString }) 813 ).data; 814 Assert.deepEqual( 815 data, 816 [ 817 { 818 area: "local", 819 name: "a", 820 value: { str: "123" }, 821 isValueEditable: true, 822 }, 823 { 824 area: "local", 825 name: "b", 826 value: { str: "456" }, 827 isValueEditable: true, 828 }, 829 ], 830 "Got the expected results on populated storage.local" 831 ); 832 833 Services.prefs.setBoolPref(LEAVE_STORAGE_PREF, false); 834 Services.prefs.setBoolPref(LEAVE_UUID_PREF, false); 835 836 await shutdown(extension, commands); 837 } 838 ); 839 840 add_task( 841 function cleanup_for_test_panel_data_matches_data_added_prior_to_ext_startup() { 842 Services.prefs.clearUserPref(LEAVE_UUID_PREF); 843 Services.prefs.clearUserPref(LEAVE_STORAGE_PREF); 844 } 845 ); 846 847 /** 848 * Test case: Transient page adds an item to storage. With storage panel open, 849 * reload extension. 850 * - Load extension with no background page. 851 * - Open transient page that adds a storage item on message. 852 * - Open the add-on storage panel. 853 * - With the storage panel still open, reload the extension. 854 * - The data in the storage panel should match the item added prior to reloading. 855 */ 856 add_task(async function test_panel_live_reload_for_extension_without_bg_page() { 857 const EXTENSION_ID = "test_local_storage_live_reload@xpcshell.mozilla.org"; 858 let manifest = { 859 version: "1.0", 860 browser_specific_settings: { 861 gecko: { 862 id: EXTENSION_ID, 863 }, 864 }, 865 }; 866 867 info("Loading and starting extension version 1.0"); 868 const extension = await startupExtension( 869 getExtensionConfig({ 870 manifest, 871 files: ext_no_bg.files, 872 }) 873 ); 874 875 info("Opening extension page in a tab"); 876 const url = extension.extension.baseURI.resolve("extension_page_in_tab.html"); 877 const contentPage = await ExtensionTestUtils.loadContentPage(url, { 878 extension, 879 }); 880 881 const host = await extension.awaitMessage("extension-origin"); 882 883 info("Waiting for extension page in a tab to add storage item"); 884 extension.sendMessage("storage-local-fireOnChanged"); 885 extension.sendMessage("storage-local-set", { a: 123 }); 886 await extension.awaitMessage("storage-local-set:done"); 887 await extension.awaitMessage("storage-local-onChanged"); 888 await contentPage.close(); 889 890 info("Opening storage panel"); 891 const { commands, extensionStorage } = await openAddonStoragePanel( 892 extension.id 893 ); 894 895 manifest = { 896 ...manifest, 897 version: "2.0", 898 }; 899 // "Reload" is most similar to an upgrade, as e.g. storage data is preserved 900 info("Updating extension to version 2.0"); 901 await extension.upgrade( 902 getExtensionConfig({ 903 manifest, 904 files: ext_no_bg.files, 905 }) 906 ); 907 908 const { data } = await extensionStorage.getStoreObjects(host); 909 Assert.deepEqual( 910 data, 911 [ 912 { 913 area: "local", 914 name: "a", 915 value: { str: "123" }, 916 isValueEditable: true, 917 }, 918 ], 919 "Got the expected results on populated storage.local" 920 ); 921 922 await shutdown(extension, commands); 923 }); 924 925 /** 926 * Test case: Bg page auto adds item(s). With storage panel open, reload extension. 927 * - Load extension with background page that automatically adds a storage item on startup. 928 * - Open the add-on storage panel. 929 * - With the storage panel still open, reload the extension. 930 * - The data in the storage panel should match the item(s) added by the reloaded 931 * extension. 932 */ 933 add_task( 934 async function test_panel_live_reload_when_extension_auto_adds_items() { 935 async function background() { 936 await browser.storage.local.set({ a: { b: 123 }, c: { d: 456 } }); 937 // window is available in background scripts 938 // eslint-disable-next-line no-undef 939 browser.test.sendMessage("extension-origin", window.location.origin); 940 } 941 const EXTENSION_ID = "test_local_storage_live_reload@xpcshell.mozilla.org"; 942 let manifest = { 943 version: "1.0", 944 browser_specific_settings: { 945 gecko: { 946 id: EXTENSION_ID, 947 }, 948 }, 949 }; 950 951 info("Loading and starting extension version 1.0"); 952 const extension = await startupExtension( 953 getExtensionConfig({ manifest, background }) 954 ); 955 956 info("Waiting for message from test extension"); 957 const host = await extension.awaitMessage("extension-origin"); 958 959 info("Opening storage panel"); 960 const { commands, extensionStorage } = await openAddonStoragePanel( 961 extension.id 962 ); 963 964 manifest = { 965 ...manifest, 966 version: "2.0", 967 }; 968 // "Reload" is most similar to an upgrade, as e.g. storage data is preserved 969 info("Update to version 2.0"); 970 await extension.upgrade( 971 getExtensionConfig({ 972 manifest, 973 background, 974 }) 975 ); 976 977 await extension.awaitMessage("extension-origin"); 978 979 const { data } = await extensionStorage.getStoreObjects(host, null, { 980 sessionString, 981 }); 982 Assert.deepEqual( 983 data, 984 [ 985 { 986 area: "local", 987 name: "a", 988 value: { str: '{"b":123}' }, 989 isValueEditable: true, 990 }, 991 { 992 area: "local", 993 name: "c", 994 value: { str: '{"d":456}' }, 995 isValueEditable: true, 996 }, 997 ], 998 "Got the expected results on populated storage.local" 999 ); 1000 1001 await shutdown(extension, commands); 1002 } 1003 ); 1004 1005 /** 1006 * Test case: Bg page adds one storage.local item and one storage.sync item. 1007 * - Load extension with background page that automatically adds two storage items on startup. 1008 * - Open the add-on storage panel. 1009 * - Assert that only the storage.local item is shown in the panel. 1010 */ 1011 add_task( 1012 async function test_panel_data_only_updates_for_storage_local_changes() { 1013 async function background() { 1014 await browser.storage.local.set({ a: { b: 123 } }); 1015 await browser.storage.sync.set({ c: { d: 456 } }); 1016 // window is available in background scripts 1017 // eslint-disable-next-line no-undef 1018 browser.test.sendMessage("extension-origin", window.location.origin); 1019 } 1020 1021 // Using the storage.sync API requires a non-temporary extension ID, see Bug 1323228. 1022 const EXTENSION_ID = 1023 "test_panel_data_only_updates_for_storage_local_changes@xpcshell.mozilla.org"; 1024 const manifest = { 1025 browser_specific_settings: { 1026 gecko: { 1027 id: EXTENSION_ID, 1028 }, 1029 }, 1030 }; 1031 1032 info("Loading and starting extension"); 1033 const extension = await startupExtension( 1034 getExtensionConfig({ manifest, background }) 1035 ); 1036 1037 info("Waiting for message from test extension"); 1038 const host = await extension.awaitMessage("extension-origin"); 1039 1040 info("Opening storage panel"); 1041 const { commands, extensionStorage } = await openAddonStoragePanel( 1042 extension.id 1043 ); 1044 1045 const { data } = await extensionStorage.getStoreObjects(host); 1046 Assert.deepEqual( 1047 data, 1048 [ 1049 { 1050 area: "local", 1051 name: "a", 1052 value: { str: '{"b":123}' }, 1053 isValueEditable: true, 1054 }, 1055 ], 1056 "Got the expected results on populated storage.local" 1057 ); 1058 1059 await shutdown(extension, commands); 1060 } 1061 ); 1062 1063 // This test verifies that Bug 1802929 fix doesn't regress. 1064 add_task(async function test_live_update_with_no_extension_listener() { 1065 const EXTENSION_ID = "test_with_no_listeners@xpcshell.mozilla.org"; 1066 let manifest = { 1067 version: "1.0", 1068 browser_specific_settings: { 1069 gecko: { 1070 id: EXTENSION_ID, 1071 }, 1072 }, 1073 }; 1074 1075 function background() { 1076 browser.test.onMessage.addListener(async (msg, ...args) => { 1077 if (msg !== "storage-local-api-call") { 1078 browser.test.fail(`Got unexpected test message: ${msg}`); 1079 return; 1080 } 1081 1082 const [{ method, methodArgs }] = args; 1083 const res = await browser.storage.local[method](...methodArgs); 1084 browser.test.sendMessage(`${msg}:done`, res); 1085 }); 1086 } 1087 1088 const extension = await startupExtension( 1089 getExtensionConfig({ manifest, background }) 1090 ); 1091 1092 const { target, extensionStorage } = await openAddonStoragePanel( 1093 extension.id 1094 ); 1095 1096 const { baseURI } = extension.extension; 1097 const host = `${baseURI.scheme}://${baseURI.host}`; 1098 1099 let { data } = await extensionStorage.getStoreObjects(host); 1100 Assert.deepEqual(data, [], "Got the expected results on empty storage.local"); 1101 1102 async function testStorageLocalUpdate(storageValue) { 1103 info("Store extension data"); 1104 await extension.sendMessage("storage-local-api-call", { 1105 method: "set", 1106 methodArgs: [{ storageKeyName: storageValue }], 1107 }); 1108 await extension.awaitMessage("storage-local-api-call:done"); 1109 1110 info("Verify stored extension data"); 1111 await extension.sendMessage("storage-local-api-call", { 1112 method: "get", 1113 methodArgs: [], 1114 }); 1115 1116 Assert.deepEqual( 1117 await extension.awaitMessage("storage-local-api-call:done"), 1118 { storageKeyName: storageValue }, 1119 "Got the expected value from browser.storage.local.get" 1120 ); 1121 1122 await TestUtils.waitForCondition(async () => { 1123 const res = await extensionStorage.getStoreObjects(host); 1124 return res.data?.length > 0; 1125 }, "Wait for the extension storage panel updates"); 1126 1127 data = (await extensionStorage.getStoreObjects(host)).data; 1128 Assert.deepEqual( 1129 data, 1130 [ 1131 { 1132 area: "local", 1133 name: "storageKeyName", 1134 value: { str: `${storageValue}` }, 1135 isValueEditable: true, 1136 }, 1137 ], 1138 "Expected DevTools Storage panel data to have been updated" 1139 ); 1140 } 1141 1142 await testStorageLocalUpdate("aStorageValue 01"); 1143 1144 manifest = { 1145 ...manifest, 1146 version: "2.0", 1147 }; 1148 // "Reload" is most similar to an upgrade, as e.g. storage data is preserved 1149 info("Update to version 2.0"); 1150 await extension.upgrade(getExtensionConfig({ manifest, background })); 1151 1152 await testStorageLocalUpdate("aStorageValue 02"); 1153 1154 await shutdown(extension, target); 1155 });