diff options
Diffstat (limited to '')
-rw-r--r-- | devtools/server/tests/xpcshell/test_extension_storage_actor.js | 1155 |
1 files changed, 1155 insertions, 0 deletions
diff --git a/devtools/server/tests/xpcshell/test_extension_storage_actor.js b/devtools/server/tests/xpcshell/test_extension_storage_actor.js new file mode 100644 index 0000000000..9816854cf8 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_extension_storage_actor.js @@ -0,0 +1,1155 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* globals browser */ + +"use strict"; + +const { ExtensionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/ExtensionXPCShellUtils.sys.mjs" +); + +const { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); + +const { + createMissingIndexedDBDirs, + extensionScriptWithMessageListener, + ext_no_bg, + getExtensionConfig, + openAddonStoragePanel, + shutdown, + startupExtension, +} = require("resource://test/webextension-helpers.js"); + +const l10n = new Localization(["devtools/client/storage.ftl"], true); +const sessionString = l10n.formatValueSync("storage-expires-session"); + +// Ignore rejection related to the storage.onChanged listener being removed while the extension context is being closed. +const { PromiseTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromiseTestUtils.sys.mjs" +); +PromiseTestUtils.allowMatchingRejectionsGlobally( + /Message manager disconnected/ +); +PromiseTestUtils.allowMatchingRejectionsGlobally( + /sendRemoveListener on closed conduit/ +); + +const { createAppInfo, promiseStartupManager } = AddonTestUtils; + +const LEAVE_UUID_PREF = "extensions.webextensions.keepUuidOnUninstall"; +const LEAVE_STORAGE_PREF = "extensions.webextensions.keepStorageOnUninstall"; + +AddonTestUtils.init(this); +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42"); + +ExtensionTestUtils.init(this); + +add_setup(async function setup() { + await promiseStartupManager(); + const dir = createMissingIndexedDBDirs(); + + Assert.ok( + dir.exists(), + "Should have a 'storage/permanent' dir in the profile dir" + ); +}); + +add_task(async function test_extension_store_exists() { + const extension = await startupExtension(getExtensionConfig()); + + const { commands, extensionStorage } = await openAddonStoragePanel( + extension.id + ); + + ok(extensionStorage, "Should have an extensionStorage store"); + + await shutdown(extension, commands); +}); + +add_task( + { + // This test currently fails if the extension runs in the main process + // like in Thunderbird (see bug 1575183 comment #15 for details). + skip_if: () => !WebExtensionPolicy.useRemoteWebExtensions, + }, + async function test_extension_origin_matches_debugger_target() { + async function background() { + // window is available in background scripts + // eslint-disable-next-line no-undef + browser.test.sendMessage("extension-origin", window.location.origin); + } + + const extension = await startupExtension( + getExtensionConfig({ background }) + ); + + const { commands, extensionStorage } = await openAddonStoragePanel( + extension.id + ); + + const { hosts } = extensionStorage; + const expectedHost = await extension.awaitMessage("extension-origin"); + ok( + expectedHost in hosts, + "Should have the expected extension host in the extensionStorage store" + ); + + await shutdown(extension, commands); + } +); + +/** + * Test case: Background page modifies items while storage panel is open. + * - Load extension with background page. + * - Open the add-on debugger storage panel. + * - With the panel still open, from the extension background page: + * - Bulk add storage items + * - Edit the values of some of the storage items + * - Remove some storage items + * - Clear all storage items + * - For each modification, the storage data in the panel should match the + * changes made by the extension. + */ +add_task(async function test_panel_live_updates() { + const extension = await startupExtension( + getExtensionConfig({ background: extensionScriptWithMessageListener }) + ); + + const { commands, extensionStorage } = await openAddonStoragePanel( + extension.id + ); + + const host = await extension.awaitMessage("extension-origin"); + + let { data } = await extensionStorage.getStoreObjects(host); + Assert.deepEqual(data, [], "Got the expected results on empty storage.local"); + + info("Waiting for extension to bulk add 50 items to storage local"); + const bulkStorageItems = {}; + // limited by MAX_STORE_OBJECT_COUNT in devtools/server/actors/resources/storage/index.js + const numItems = 2; + for (let i = 1; i <= numItems; i++) { + bulkStorageItems[i] = i; + } + + // fireOnChanged avoids the race condition where the extension + // modifies storage then immediately tries to access storage before + // the storage actor has finished updating. + extension.sendMessage("storage-local-fireOnChanged"); + extension.sendMessage("storage-local-set", { + ...bulkStorageItems, + a: 123, + b: [4, 5], + c: { d: 678 }, + d: true, + e: "hi", + f: null, + }); + await extension.awaitMessage("storage-local-set:done"); + await extension.awaitMessage("storage-local-onChanged"); + + info( + "Confirming items added by extension match items in extensionStorage store" + ); + const bulkStorageObjects = []; + for (const [name, value] of Object.entries(bulkStorageItems)) { + bulkStorageObjects.push({ + area: "local", + name, + value: { str: String(value) }, + isValueEditable: true, + }); + } + data = (await extensionStorage.getStoreObjects(host, null, { sessionString })) + .data; + Assert.deepEqual( + data, + [ + ...bulkStorageObjects, + { + area: "local", + name: "a", + value: { str: "123" }, + isValueEditable: true, + }, + { + area: "local", + name: "b", + value: { str: "[4,5]" }, + isValueEditable: true, + }, + { + area: "local", + name: "c", + value: { str: '{"d":678}' }, + isValueEditable: true, + }, + { + area: "local", + name: "d", + value: { str: "true" }, + isValueEditable: true, + }, + { + area: "local", + name: "e", + value: { str: "hi" }, + isValueEditable: true, + }, + { + area: "local", + name: "f", + value: { str: "null" }, + isValueEditable: true, + }, + ], + "Got the expected results on populated storage.local" + ); + + info("Waiting for extension to edit a few storage item values"); + extension.sendMessage("storage-local-fireOnChanged"); + extension.sendMessage("storage-local-set", { + a: ["c", "d"], + b: 456, + c: false, + }); + await extension.awaitMessage("storage-local-set:done"); + await extension.awaitMessage("storage-local-onChanged"); + + info( + "Confirming items edited by extension match items in extensionStorage store" + ); + data = (await extensionStorage.getStoreObjects(host, null, { sessionString })) + .data; + Assert.deepEqual( + data, + [ + ...bulkStorageObjects, + { + area: "local", + name: "a", + value: { str: '["c","d"]' }, + isValueEditable: true, + }, + { + area: "local", + name: "b", + value: { str: "456" }, + isValueEditable: true, + }, + { + area: "local", + name: "c", + value: { str: "false" }, + isValueEditable: true, + }, + { + area: "local", + name: "d", + value: { str: "true" }, + isValueEditable: true, + }, + { + area: "local", + name: "e", + value: { str: "hi" }, + isValueEditable: true, + }, + { + area: "local", + name: "f", + value: { str: "null" }, + isValueEditable: true, + }, + ], + "Got the expected results on populated storage.local" + ); + + info("Waiting for extension to remove a few storage item values"); + extension.sendMessage("storage-local-fireOnChanged"); + extension.sendMessage("storage-local-remove", ["d", "e", "f"]); + await extension.awaitMessage("storage-local-remove:done"); + await extension.awaitMessage("storage-local-onChanged"); + + info( + "Confirming items removed by extension were removed in extensionStorage store" + ); + data = (await extensionStorage.getStoreObjects(host, null, { sessionString })) + .data; + Assert.deepEqual( + data, + [ + ...bulkStorageObjects, + { + area: "local", + name: "a", + value: { str: '["c","d"]' }, + isValueEditable: true, + }, + { + area: "local", + name: "b", + value: { str: "456" }, + isValueEditable: true, + }, + { + area: "local", + name: "c", + value: { str: "false" }, + isValueEditable: true, + }, + ], + "Got the expected results on populated storage.local" + ); + + info("Waiting for extension to remove all remaining storage items"); + extension.sendMessage("storage-local-fireOnChanged"); + extension.sendMessage("storage-local-clear"); + await extension.awaitMessage("storage-local-clear:done"); + await extension.awaitMessage("storage-local-onChanged"); + + info("Confirming extensionStorage store was cleared"); + data = (await extensionStorage.getStoreObjects(host)).data; + Assert.deepEqual( + data, + [], + "Got the expected results on populated storage.local" + ); + + await shutdown(extension, commands); +}); + +/** + * Test case: No bg page. Transient page adds item before storage panel opened. + * - Load extension with no background page. + * - Open an extension page in a tab that adds a local storage item. + * - With the extension page still open, open the add-on storage panel. + * - The data in the storage panel should match the items added by the extension. + */ +add_task( + async function test_panel_data_matches_extension_with_transient_page_open() { + const extension = await startupExtension( + getExtensionConfig({ files: ext_no_bg.files }) + ); + + const url = extension.extension.baseURI.resolve( + "extension_page_in_tab.html" + ); + const contentPage = await ExtensionTestUtils.loadContentPage(url, { + extension, + }); + + const host = await extension.awaitMessage("extension-origin"); + + extension.sendMessage("storage-local-set", { a: 123 }); + await extension.awaitMessage("storage-local-set:done"); + + const { commands, extensionStorage } = await openAddonStoragePanel( + extension.id + ); + + const { data } = await extensionStorage.getStoreObjects(host); + Assert.deepEqual( + data, + [ + { + area: "local", + name: "a", + value: { str: "123" }, + isValueEditable: true, + }, + ], + "Got the expected results on populated storage.local" + ); + + await contentPage.close(); + await shutdown(extension, commands); + } +); + +/** + * Test case: No bg page. Transient page adds item then closes before storage panel opened. + * - Load extension with no background page. + * - Open an extension page in a tab that adds a local storage item. + * - Close all extension pages. + * - Open the add-on storage panel. + * - The data in the storage panel should match the item added by the extension. + */ +add_task(async function test_panel_data_matches_extension_with_no_pages_open() { + const extension = await startupExtension( + getExtensionConfig({ files: ext_no_bg.files }) + ); + + const url = extension.extension.baseURI.resolve("extension_page_in_tab.html"); + const contentPage = await ExtensionTestUtils.loadContentPage(url, { + extension, + }); + + const host = await extension.awaitMessage("extension-origin"); + + extension.sendMessage("storage-local-fireOnChanged"); + extension.sendMessage("storage-local-set", { a: 123 }); + await extension.awaitMessage("storage-local-set:done"); + await extension.awaitMessage("storage-local-onChanged"); + await contentPage.close(); + + const { commands, extensionStorage } = await openAddonStoragePanel( + extension.id + ); + + const { data } = await extensionStorage.getStoreObjects(host); + Assert.deepEqual( + data, + [ + { + area: "local", + name: "a", + value: { str: "123" }, + isValueEditable: true, + }, + ], + "Got the expected results on populated storage.local" + ); + + await shutdown(extension, commands); +}); + +/** + * Test case: No bg page. Storage panel live updates when a transient page adds an item. + * - Load extension with no background page. + * - Open the add-on storage panel. + * - With the storage panel still open, open an extension page in a new tab that adds an + * item. + * - The data in the storage panel should live update to match the item added by the + * extension. + * - If an extension page adds the same data again, the data in the storage panel should + * not change. + */ +add_task( + async function test_panel_data_live_updates_for_extension_without_bg_page() { + const extension = await startupExtension( + getExtensionConfig({ files: ext_no_bg.files }) + ); + + const { commands, extensionStorage } = await openAddonStoragePanel( + extension.id + ); + + const url = extension.extension.baseURI.resolve( + "extension_page_in_tab.html" + ); + const contentPage = await ExtensionTestUtils.loadContentPage(url, { + extension, + }); + + const host = await extension.awaitMessage("extension-origin"); + + let { data } = await extensionStorage.getStoreObjects(host); + Assert.deepEqual( + data, + [], + "Got the expected results on empty storage.local" + ); + + extension.sendMessage("storage-local-fireOnChanged"); + extension.sendMessage("storage-local-set", { a: 123 }); + await extension.awaitMessage("storage-local-set:done"); + await extension.awaitMessage("storage-local-onChanged"); + + data = (await extensionStorage.getStoreObjects(host)).data; + Assert.deepEqual( + data, + [ + { + area: "local", + name: "a", + value: { str: "123" }, + isValueEditable: true, + }, + ], + "Got the expected results on populated storage.local" + ); + + extension.sendMessage("storage-local-fireOnChanged"); + extension.sendMessage("storage-local-set", { a: 123 }); + await extension.awaitMessage("storage-local-set:done"); + await extension.awaitMessage("storage-local-onChanged"); + + data = (await extensionStorage.getStoreObjects(host)).data; + Assert.deepEqual( + data, + [ + { + area: "local", + name: "a", + value: { str: "123" }, + isValueEditable: true, + }, + ], + "The results are unchanged when an extension page adds duplicate items" + ); + + await contentPage.close(); + await shutdown(extension, commands); + } +); + +/** + * Test case: Bg page adds item while storage panel is open. Panel edits item's value. + * - Load extension with background page. + * - Open the add-on storage panel. + * - With the storage panel still open, add item from the background page. + * - Edit the value of the item in the storage panel + * - The data in the storage panel should match the item added by the extension. + * - The storage actor is correctly parsing and setting the string representation of + * the value in the storage local database when the item's value is edited in the + * storage panel + */ +add_task( + async function test_editing_items_in_panel_parses_supported_values_correctly() { + const extension = await startupExtension( + getExtensionConfig({ background: extensionScriptWithMessageListener }) + ); + + const host = await extension.awaitMessage("extension-origin"); + + const { commands, extensionStorage } = await openAddonStoragePanel( + extension.id + ); + + const oldItem = { a: 123 }; + const key = Object.keys(oldItem)[0]; + const oldValue = oldItem[key]; + // A tuple representing information for a new value entered into the panel for oldItem: + // [ + // value, + // editItem string representation of value, + // toStoreObject string representation of value, + // ] + const valueInfo = [ + [true, "true", "true"], + ["hi", "hi", "hi"], + [456, "456", "456"], + [{ b: 789 }, "{b: 789}", '{"b":789}'], + [[1, 2, 3], "[1, 2, 3]", "[1,2,3]"], + [null, "null", "null"], + ]; + for (const [value, editItemValueStr, toStoreObjectValueStr] of valueInfo) { + info("Setting a storage item through the extension"); + extension.sendMessage("storage-local-fireOnChanged"); + extension.sendMessage("storage-local-set", oldItem); + await extension.awaitMessage("storage-local-set:done"); + await extension.awaitMessage("storage-local-onChanged"); + + info( + "Editing the storage item in the panel with a new value of a different type" + ); + // When the user edits an item in the panel, they are entering a string into a + // textbox. This string is parsed by the storage actor's editItem method. + await extensionStorage.editItem({ + host, + field: "value", + items: { name: key, value: editItemValueStr }, + oldValue, + }); + + info( + "Verifying item in the storage actor matches the item edited in the panel" + ); + const { data } = await extensionStorage.getStoreObjects(host); + Assert.deepEqual( + data, + [ + { + area: "local", + name: key, + value: { str: toStoreObjectValueStr }, + isValueEditable: true, + }, + ], + "Got the expected results on populated storage.local" + ); + + // The view layer is separate from the database layer; therefore while values are + // stringified (via toStoreObject) for display in the client, the value (and its type) + // in the database is unchanged. + info( + "Verifying the expected new value matches the value fetched in the extension" + ); + extension.sendMessage("storage-local-get", key); + const extItem = await extension.awaitMessage("storage-local-get:done"); + Assert.deepEqual( + value, + extItem[key], + `The string value ${editItemValueStr} was correctly parsed to ${value}` + ); + } + + await shutdown(extension, commands); + } +); + +/** + * Test case: Modifying storage items from the panel update extension storage local data. + * - Load extension with background page. + * - Open the add-on storage panel. From the panel: + * - Edit the value of a storage item, + * - Remove a storage item, + * - Remove all of the storage items, + * - For each modification, the storage data retrieved by the extension should match the + * data in the panel. + */ +add_task( + async function test_modifying_items_in_panel_updates_extension_storage_data() { + const extension = await startupExtension( + getExtensionConfig({ background: extensionScriptWithMessageListener }) + ); + + const host = await extension.awaitMessage("extension-origin"); + + const { commands, extensionStorage } = await openAddonStoragePanel( + extension.id + ); + + const DEFAULT_VALUE = "value"; // global in devtools/server/actors/resources/storage/index.js + let items = { + guid_1: DEFAULT_VALUE, + guid_2: DEFAULT_VALUE, + guid_3: DEFAULT_VALUE, + }; + + info("Adding storage items from the extension"); + let storesUpdate = extensionStorage.once("single-store-update"); + extension.sendMessage("storage-local-set", items); + await extension.awaitMessage("storage-local-set:done"); + + info("Waiting for the storage actor to emit a 'stores-update' event"); + let data = await storesUpdate; + Assert.deepEqual( + { + added: { + extensionStorage: { + [host]: ["guid_1", "guid_2", "guid_3"], + }, + }, + changed: undefined, + deleted: undefined, + }, + data, + "The change data from the storage actor's 'stores-update' event matches the changes made in the client." + ); + + info("Waiting for panel to edit some items"); + storesUpdate = extensionStorage.once("single-store-update"); + await extensionStorage.editItem({ + host, + field: "value", + items: { name: "guid_1", value: "anotherValue" }, + DEFAULT_VALUE, + }); + + info("Waiting for the storage actor to emit a 'stores-update' event"); + data = await storesUpdate; + Assert.deepEqual( + { + added: undefined, + changed: { + extensionStorage: { + [host]: ["guid_1"], + }, + }, + deleted: undefined, + }, + data, + "The change data from the storage actor's 'stores-update' event matches the changes made in the client." + ); + + items = { + guid_1: "anotherValue", + guid_2: DEFAULT_VALUE, + guid_3: DEFAULT_VALUE, + }; + extension.sendMessage("storage-local-get", Object.keys(items)); + let extItems = await extension.awaitMessage("storage-local-get:done"); + Assert.deepEqual( + items, + extItems, + `The storage items in the extension match the items in the panel` + ); + + info("Waiting for panel to remove an item"); + storesUpdate = extensionStorage.once("single-store-update"); + await extensionStorage.removeItem(host, "guid_3"); + + info("Waiting for the storage actor to emit a 'stores-update' event"); + data = await storesUpdate; + Assert.deepEqual( + { + added: undefined, + changed: undefined, + deleted: { + extensionStorage: { + [host]: ["guid_3"], + }, + }, + }, + data, + "The change data from the storage actor's 'stores-update' event matches the changes made in the client." + ); + + items = { + guid_1: "anotherValue", + guid_2: DEFAULT_VALUE, + }; + extension.sendMessage("storage-local-get", Object.keys(items)); + extItems = await extension.awaitMessage("storage-local-get:done"); + Assert.deepEqual( + items, + extItems, + `The storage items in the extension match the items in the panel` + ); + + info("Waiting for panel to remove all items"); + const storesCleared = extensionStorage.once("single-store-cleared"); + await extensionStorage.removeAll(host); + + info("Waiting for the storage actor to emit a 'stores-cleared' event"); + data = await storesCleared; + Assert.deepEqual( + { + clearedHostsOrPaths: { + [host]: [], + }, + }, + data, + "The change data from the storage actor's 'stores-cleared' event matches the changes made in the client." + ); + + items = {}; + extension.sendMessage("storage-local-get", Object.keys(items)); + extItems = await extension.awaitMessage("storage-local-get:done"); + Assert.deepEqual( + items, + extItems, + `The storage items in the extension match the items in the panel` + ); + + await shutdown(extension, commands); + } +); + +/** + * Test case: Storage panel shows extension storage data added prior to extension startup + * - Load extension that adds a storage item + * - Uninstall the extension + * - Reinstall the extension + * - Open the add-on storage panel. + * - The data in the storage panel should match the data added the first time the extension + * was installed + * Related test case: Storage panel shows extension storage data when an extension that has + * already migrated to the IndexedDB storage backend prior to extension startup adds + * another storage item. + * - (Building from previous steps) + * - The reinstalled extension adds a storage item + * - The data in the storage panel should live update with both items: the item added from + * the first and the item added from the reinstall. + */ +add_task( + async function test_panel_data_matches_data_added_prior_to_ext_startup() { + // The pref to leave the addonid->uuid mapping around after uninstall so that we can + // re-attach to the same storage + Services.prefs.setBoolPref(LEAVE_UUID_PREF, true); + + // The pref to prevent cleaning up storage on uninstall + Services.prefs.setBoolPref(LEAVE_STORAGE_PREF, true); + + let extension = await startupExtension( + getExtensionConfig({ background: extensionScriptWithMessageListener }) + ); + + const host = await extension.awaitMessage("extension-origin"); + + extension.sendMessage("storage-local-set", { a: 123 }); + await extension.awaitMessage("storage-local-set:done"); + + await shutdown(extension); + + // Reinstall the same extension + extension = await startupExtension( + getExtensionConfig({ background: extensionScriptWithMessageListener }) + ); + + await extension.awaitMessage("extension-origin"); + + const { commands, extensionStorage } = await openAddonStoragePanel( + extension.id + ); + + let { data } = await extensionStorage.getStoreObjects(host); + Assert.deepEqual( + data, + [ + { + area: "local", + name: "a", + value: { str: "123" }, + isValueEditable: true, + }, + ], + "Got the expected results on populated storage.local" + ); + + // Related test case + extension.sendMessage("storage-local-fireOnChanged"); + extension.sendMessage("storage-local-set", { b: 456 }); + await extension.awaitMessage("storage-local-set:done"); + await extension.awaitMessage("storage-local-onChanged"); + + data = ( + await extensionStorage.getStoreObjects(host, null, { sessionString }) + ).data; + Assert.deepEqual( + data, + [ + { + area: "local", + name: "a", + value: { str: "123" }, + isValueEditable: true, + }, + { + area: "local", + name: "b", + value: { str: "456" }, + isValueEditable: true, + }, + ], + "Got the expected results on populated storage.local" + ); + + Services.prefs.setBoolPref(LEAVE_STORAGE_PREF, false); + Services.prefs.setBoolPref(LEAVE_UUID_PREF, false); + + await shutdown(extension, commands); + } +); + +add_task( + function cleanup_for_test_panel_data_matches_data_added_prior_to_ext_startup() { + Services.prefs.clearUserPref(LEAVE_UUID_PREF); + Services.prefs.clearUserPref(LEAVE_STORAGE_PREF); + } +); + +/** + * Test case: Transient page adds an item to storage. With storage panel open, + * reload extension. + * - Load extension with no background page. + * - Open transient page that adds a storage item on message. + * - Open the add-on storage panel. + * - With the storage panel still open, reload the extension. + * - The data in the storage panel should match the item added prior to reloading. + */ +add_task(async function test_panel_live_reload_for_extension_without_bg_page() { + const EXTENSION_ID = "test_local_storage_live_reload@xpcshell.mozilla.org"; + let manifest = { + version: "1.0", + browser_specific_settings: { + gecko: { + id: EXTENSION_ID, + }, + }, + }; + + info("Loading and starting extension version 1.0"); + const extension = await startupExtension( + getExtensionConfig({ + manifest, + files: ext_no_bg.files, + }) + ); + + info("Opening extension page in a tab"); + const url = extension.extension.baseURI.resolve("extension_page_in_tab.html"); + const contentPage = await ExtensionTestUtils.loadContentPage(url, { + extension, + }); + + const host = await extension.awaitMessage("extension-origin"); + + info("Waiting for extension page in a tab to add storage item"); + extension.sendMessage("storage-local-fireOnChanged"); + extension.sendMessage("storage-local-set", { a: 123 }); + await extension.awaitMessage("storage-local-set:done"); + await extension.awaitMessage("storage-local-onChanged"); + await contentPage.close(); + + info("Opening storage panel"); + const { commands, extensionStorage } = await openAddonStoragePanel( + extension.id + ); + + manifest = { + ...manifest, + version: "2.0", + }; + // "Reload" is most similar to an upgrade, as e.g. storage data is preserved + info("Updating extension to version 2.0"); + await extension.upgrade( + getExtensionConfig({ + manifest, + files: ext_no_bg.files, + }) + ); + + const { data } = await extensionStorage.getStoreObjects(host); + Assert.deepEqual( + data, + [ + { + area: "local", + name: "a", + value: { str: "123" }, + isValueEditable: true, + }, + ], + "Got the expected results on populated storage.local" + ); + + await shutdown(extension, commands); +}); + +/** + * Test case: Bg page auto adds item(s). With storage panel open, reload extension. + * - Load extension with background page that automatically adds a storage item on startup. + * - Open the add-on storage panel. + * - With the storage panel still open, reload the extension. + * - The data in the storage panel should match the item(s) added by the reloaded + * extension. + */ +add_task( + async function test_panel_live_reload_when_extension_auto_adds_items() { + async function background() { + await browser.storage.local.set({ a: { b: 123 }, c: { d: 456 } }); + // window is available in background scripts + // eslint-disable-next-line no-undef + browser.test.sendMessage("extension-origin", window.location.origin); + } + const EXTENSION_ID = "test_local_storage_live_reload@xpcshell.mozilla.org"; + let manifest = { + version: "1.0", + browser_specific_settings: { + gecko: { + id: EXTENSION_ID, + }, + }, + }; + + info("Loading and starting extension version 1.0"); + const extension = await startupExtension( + getExtensionConfig({ manifest, background }) + ); + + info("Waiting for message from test extension"); + const host = await extension.awaitMessage("extension-origin"); + + info("Opening storage panel"); + const { commands, extensionStorage } = await openAddonStoragePanel( + extension.id + ); + + manifest = { + ...manifest, + version: "2.0", + }; + // "Reload" is most similar to an upgrade, as e.g. storage data is preserved + info("Update to version 2.0"); + await extension.upgrade( + getExtensionConfig({ + manifest, + background, + }) + ); + + await extension.awaitMessage("extension-origin"); + + const { data } = await extensionStorage.getStoreObjects(host, null, { + sessionString, + }); + Assert.deepEqual( + data, + [ + { + area: "local", + name: "a", + value: { str: '{"b":123}' }, + isValueEditable: true, + }, + { + area: "local", + name: "c", + value: { str: '{"d":456}' }, + isValueEditable: true, + }, + ], + "Got the expected results on populated storage.local" + ); + + await shutdown(extension, commands); + } +); + +/** + * Test case: Bg page adds one storage.local item and one storage.sync item. + * - Load extension with background page that automatically adds two storage items on startup. + * - Open the add-on storage panel. + * - Assert that only the storage.local item is shown in the panel. + */ +add_task( + async function test_panel_data_only_updates_for_storage_local_changes() { + async function background() { + await browser.storage.local.set({ a: { b: 123 } }); + await browser.storage.sync.set({ c: { d: 456 } }); + // window is available in background scripts + // eslint-disable-next-line no-undef + browser.test.sendMessage("extension-origin", window.location.origin); + } + + // Using the storage.sync API requires a non-temporary extension ID, see Bug 1323228. + const EXTENSION_ID = + "test_panel_data_only_updates_for_storage_local_changes@xpcshell.mozilla.org"; + const manifest = { + browser_specific_settings: { + gecko: { + id: EXTENSION_ID, + }, + }, + }; + + info("Loading and starting extension"); + const extension = await startupExtension( + getExtensionConfig({ manifest, background }) + ); + + info("Waiting for message from test extension"); + const host = await extension.awaitMessage("extension-origin"); + + info("Opening storage panel"); + const { commands, extensionStorage } = await openAddonStoragePanel( + extension.id + ); + + const { data } = await extensionStorage.getStoreObjects(host); + Assert.deepEqual( + data, + [ + { + area: "local", + name: "a", + value: { str: '{"b":123}' }, + isValueEditable: true, + }, + ], + "Got the expected results on populated storage.local" + ); + + await shutdown(extension, commands); + } +); + +// This test verifies that Bug 1802929 fix doesn't regress. +add_task(async function test_live_update_with_no_extension_listener() { + const EXTENSION_ID = "test_with_no_listeners@xpcshell.mozilla.org"; + let manifest = { + version: "1.0", + browser_specific_settings: { + gecko: { + id: EXTENSION_ID, + }, + }, + }; + + function background() { + browser.test.onMessage.addListener(async (msg, ...args) => { + if (msg !== "storage-local-api-call") { + browser.test.fail(`Got unexpected test message: ${msg}`); + return; + } + + const [{ method, methodArgs }] = args; + const res = await browser.storage.local[method](...methodArgs); + browser.test.sendMessage(`${msg}:done`, res); + }); + } + + const extension = await startupExtension( + getExtensionConfig({ manifest, background }) + ); + + const { target, extensionStorage } = await openAddonStoragePanel( + extension.id + ); + + const { baseURI } = extension.extension; + const host = `${baseURI.scheme}://${baseURI.host}`; + + let { data } = await extensionStorage.getStoreObjects(host); + Assert.deepEqual(data, [], "Got the expected results on empty storage.local"); + + async function testStorageLocalUpdate(storageValue) { + info("Store extension data"); + await extension.sendMessage("storage-local-api-call", { + method: "set", + methodArgs: [{ storageKeyName: storageValue }], + }); + await extension.awaitMessage("storage-local-api-call:done"); + + info("Verify stored extension data"); + await extension.sendMessage("storage-local-api-call", { + method: "get", + methodArgs: [], + }); + + Assert.deepEqual( + await extension.awaitMessage("storage-local-api-call:done"), + { storageKeyName: storageValue }, + "Got the expected value from browser.storage.local.get" + ); + + await TestUtils.waitForCondition(async () => { + const res = await extensionStorage.getStoreObjects(host); + return res.data?.length > 0; + }, "Wait for the extension storage panel updates"); + + data = (await extensionStorage.getStoreObjects(host)).data; + Assert.deepEqual( + data, + [ + { + area: "local", + name: "storageKeyName", + value: { str: `${storageValue}` }, + isValueEditable: true, + }, + ], + "Expected DevTools Storage panel data to have been updated" + ); + } + + await testStorageLocalUpdate("aStorageValue 01"); + + manifest = { + ...manifest, + version: "2.0", + }; + // "Reload" is most similar to an upgrade, as e.g. storage data is preserved + info("Update to version 2.0"); + await extension.upgrade(getExtensionConfig({ manifest, background })); + + await testStorageLocalUpdate("aStorageValue 02"); + + await shutdown(extension, target); +}); |