summaryrefslogtreecommitdiffstats
path: root/devtools/server/tests/xpcshell/test_extension_storage_actor.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/tests/xpcshell/test_extension_storage_actor.js')
-rw-r--r--devtools/server/tests/xpcshell/test_extension_storage_actor.js1155
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);
+});