summaryrefslogtreecommitdiffstats
path: root/devtools/client/storage/test
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/storage/test')
-rw-r--r--devtools/client/storage/test/browser.toml194
-rw-r--r--devtools/client/storage/test/browser_storage_basic.js172
-rw-r--r--devtools/client/storage/test/browser_storage_basic_usercontextid_1.js162
-rw-r--r--devtools/client/storage/test/browser_storage_basic_usercontextid_2.js169
-rw-r--r--devtools/client/storage/test/browser_storage_basic_with_fragment.js177
-rw-r--r--devtools/client/storage/test/browser_storage_cache_delete.js53
-rw-r--r--devtools/client/storage/test/browser_storage_cache_error.js35
-rw-r--r--devtools/client/storage/test/browser_storage_cache_navigation.js84
-rw-r--r--devtools/client/storage/test/browser_storage_cache_overflow.js32
-rw-r--r--devtools/client/storage/test/browser_storage_cookies_add.js59
-rw-r--r--devtools/client/storage/test/browser_storage_cookies_delete_all.js185
-rw-r--r--devtools/client/storage/test/browser_storage_cookies_domain.js25
-rw-r--r--devtools/client/storage/test/browser_storage_cookies_domain_port.js25
-rw-r--r--devtools/client/storage/test/browser_storage_cookies_edit.js27
-rw-r--r--devtools/client/storage/test/browser_storage_cookies_edit_keyboard.js23
-rw-r--r--devtools/client/storage/test/browser_storage_cookies_hostOnly.js27
-rw-r--r--devtools/client/storage/test/browser_storage_cookies_navigation.js139
-rw-r--r--devtools/client/storage/test/browser_storage_cookies_samesite.js42
-rw-r--r--devtools/client/storage/test/browser_storage_cookies_sort.js64
-rw-r--r--devtools/client/storage/test/browser_storage_cookies_tab_navigation.js25
-rw-r--r--devtools/client/storage/test/browser_storage_delete.js79
-rw-r--r--devtools/client/storage/test/browser_storage_delete_all.js115
-rw-r--r--devtools/client/storage/test/browser_storage_delete_tree.js93
-rw-r--r--devtools/client/storage/test/browser_storage_delete_usercontextid.js238
-rw-r--r--devtools/client/storage/test/browser_storage_dfpi.js164
-rw-r--r--devtools/client/storage/test/browser_storage_dfpi_always_partition_storage.js70
-rw-r--r--devtools/client/storage/test/browser_storage_dynamic_updates_cookies.js239
-rw-r--r--devtools/client/storage/test/browser_storage_dynamic_updates_localStorage.js70
-rw-r--r--devtools/client/storage/test/browser_storage_dynamic_updates_sessionStorage.js90
-rw-r--r--devtools/client/storage/test/browser_storage_empty_objectstores.js90
-rw-r--r--devtools/client/storage/test/browser_storage_file_url.js64
-rw-r--r--devtools/client/storage/test/browser_storage_fission_cache.js44
-rw-r--r--devtools/client/storage/test/browser_storage_fission_cookies.js64
-rw-r--r--devtools/client/storage/test/browser_storage_fission_hide_aboutblank.js26
-rw-r--r--devtools/client/storage/test/browser_storage_fission_indexeddb.js62
-rw-r--r--devtools/client/storage/test/browser_storage_fission_local_storage.js45
-rw-r--r--devtools/client/storage/test/browser_storage_fission_session_storage.js45
-rw-r--r--devtools/client/storage/test/browser_storage_indexeddb_add_button_hidden.js35
-rw-r--r--devtools/client/storage/test/browser_storage_indexeddb_delete.js54
-rw-r--r--devtools/client/storage/test/browser_storage_indexeddb_delete_blocked.js60
-rw-r--r--devtools/client/storage/test/browser_storage_indexeddb_duplicate_names.js24
-rw-r--r--devtools/client/storage/test/browser_storage_indexeddb_hide_internal_dbs.js62
-rw-r--r--devtools/client/storage/test/browser_storage_indexeddb_navigation.js72
-rw-r--r--devtools/client/storage/test/browser_storage_indexeddb_overflow.js36
-rw-r--r--devtools/client/storage/test/browser_storage_keys.js164
-rw-r--r--devtools/client/storage/test/browser_storage_localstorage_add.js20
-rw-r--r--devtools/client/storage/test/browser_storage_localstorage_edit.js24
-rw-r--r--devtools/client/storage/test/browser_storage_localstorage_error.js25
-rw-r--r--devtools/client/storage/test/browser_storage_localstorage_navigation.js63
-rw-r--r--devtools/client/storage/test/browser_storage_localstorage_rapid_add_remove.js30
-rw-r--r--devtools/client/storage/test/browser_storage_overflow.js104
-rw-r--r--devtools/client/storage/test/browser_storage_search.js140
-rw-r--r--devtools/client/storage/test/browser_storage_search_keyboard_trap.js15
-rw-r--r--devtools/client/storage/test/browser_storage_sessionstorage_add.js20
-rw-r--r--devtools/client/storage/test/browser_storage_sessionstorage_edit.js24
-rw-r--r--devtools/client/storage/test/browser_storage_sessionstorage_navigation.js60
-rw-r--r--devtools/client/storage/test/browser_storage_sidebar.js136
-rw-r--r--devtools/client/storage/test/browser_storage_sidebar_filter.js43
-rw-r--r--devtools/client/storage/test/browser_storage_sidebar_parsetree.js115
-rw-r--r--devtools/client/storage/test/browser_storage_sidebar_toggle.js65
-rw-r--r--devtools/client/storage/test/browser_storage_sidebar_update.js45
-rw-r--r--devtools/client/storage/test/browser_storage_type_descriptions.js79
-rw-r--r--devtools/client/storage/test/browser_storage_values.js265
-rw-r--r--devtools/client/storage/test/browser_storage_webext_storage_local.js296
-rw-r--r--devtools/client/storage/test/head.js1186
-rw-r--r--devtools/client/storage/test/storage-blank.html9
-rw-r--r--devtools/client/storage/test/storage-cache-basic-iframe.html21
-rw-r--r--devtools/client/storage/test/storage-cache-basic.html22
-rw-r--r--devtools/client/storage/test/storage-cache-error.html11
-rw-r--r--devtools/client/storage/test/storage-cache-overflow.html23
-rw-r--r--devtools/client/storage/test/storage-complex-keys.html78
-rw-r--r--devtools/client/storage/test/storage-complex-values.html124
-rw-r--r--devtools/client/storage/test/storage-cookies-samesite.html17
-rw-r--r--devtools/client/storage/test/storage-cookies-sort.html26
-rw-r--r--devtools/client/storage/test/storage-cookies.html24
-rw-r--r--devtools/client/storage/test/storage-dfpi.html11
-rw-r--r--devtools/client/storage/test/storage-empty-objectstores.html62
-rw-r--r--devtools/client/storage/test/storage-file-url.html59
-rw-r--r--devtools/client/storage/test/storage-idb-delete-blocked.html47
-rw-r--r--devtools/client/storage/test/storage-indexeddb-duplicate-names.html43
-rw-r--r--devtools/client/storage/test/storage-indexeddb-iframe.html37
-rw-r--r--devtools/client/storage/test/storage-indexeddb-simple-alt.html38
-rw-r--r--devtools/client/storage/test/storage-indexeddb-simple.html38
-rw-r--r--devtools/client/storage/test/storage-listings-usercontextid.html131
-rw-r--r--devtools/client/storage/test/storage-listings-with-fragment.html134
-rw-r--r--devtools/client/storage/test/storage-listings.html145
-rw-r--r--devtools/client/storage/test/storage-localstorage.html23
-rw-r--r--devtools/client/storage/test/storage-overflow-indexeddb.html49
-rw-r--r--devtools/client/storage/test/storage-overflow.html19
-rw-r--r--devtools/client/storage/test/storage-search.html28
-rw-r--r--devtools/client/storage/test/storage-secured-iframe-usercontextid.html91
-rw-r--r--devtools/client/storage/test/storage-secured-iframe.html94
-rw-r--r--devtools/client/storage/test/storage-sessionstorage.html23
-rw-r--r--devtools/client/storage/test/storage-sidebar-parsetree.html40
-rw-r--r--devtools/client/storage/test/storage-unsecured-iframe-usercontextid.html19
-rw-r--r--devtools/client/storage/test/storage-unsecured-iframe.html22
-rw-r--r--devtools/client/storage/test/storage-updates.html68
97 files changed, 8155 insertions, 0 deletions
diff --git a/devtools/client/storage/test/browser.toml b/devtools/client/storage/test/browser.toml
new file mode 100644
index 0000000000..28b4b04258
--- /dev/null
+++ b/devtools/client/storage/test/browser.toml
@@ -0,0 +1,194 @@
+[DEFAULT]
+tags = "devtools"
+subsuite = "devtools"
+skip-if = [
+ "http3", # Many tests relying on test1/test2.example.org
+ "http2",
+]
+support-files = [
+ "storage-blank.html",
+ "storage-cache-basic-iframe.html",
+ "storage-cache-basic.html",
+ "storage-cache-error.html",
+ "storage-cache-overflow.html",
+ "storage-complex-keys.html",
+ "storage-complex-values.html",
+ "storage-cookies.html",
+ "storage-cookies-samesite.html",
+ "storage-cookies-sort.html",
+ "storage-dfpi.html",
+ "storage-empty-objectstores.html",
+ "storage-file-url.html",
+ "storage-idb-delete-blocked.html",
+ "storage-indexeddb-duplicate-names.html",
+ "storage-indexeddb-iframe.html",
+ "storage-indexeddb-simple.html",
+ "storage-indexeddb-simple-alt.html",
+ "storage-listings.html",
+ "storage-listings-usercontextid.html",
+ "storage-listings-with-fragment.html",
+ "storage-localstorage.html",
+ "storage-overflow-indexeddb.html",
+ "storage-overflow.html",
+ "storage-search.html",
+ "storage-secured-iframe.html",
+ "storage-secured-iframe-usercontextid.html",
+ "storage-sessionstorage.html",
+ "storage-sidebar-parsetree.html",
+ "storage-unsecured-iframe.html",
+ "storage-unsecured-iframe-usercontextid.html",
+ "storage-updates.html",
+ "head.js",
+ "!/devtools/client/shared/test/shared-head.js",
+ "!/devtools/client/shared/test/telemetry-test-helpers.js",
+]
+
+["browser_storage_basic.js"]
+
+["browser_storage_basic_usercontextid_1.js"]
+
+["browser_storage_basic_usercontextid_2.js"]
+tags = "usercontextid"
+
+["browser_storage_basic_with_fragment.js"]
+
+["browser_storage_cache_delete.js"]
+
+["browser_storage_cache_error.js"]
+
+["browser_storage_cache_navigation.js"]
+skip-if = ["win11_2009"] # Bug 1797751
+
+["browser_storage_cache_overflow.js"]
+
+["browser_storage_cookies_add.js"]
+
+["browser_storage_cookies_delete_all.js"]
+
+["browser_storage_cookies_domain.js"]
+
+["browser_storage_cookies_domain_port.js"]
+
+["browser_storage_cookies_edit.js"]
+
+["browser_storage_cookies_edit_keyboard.js"]
+
+["browser_storage_cookies_hostOnly.js"]
+
+["browser_storage_cookies_navigation.js"]
+skip-if = [
+ "os == 'linux' && debug && fission && socketprocess_networking", # high frequency intermittent
+]
+
+["browser_storage_cookies_samesite.js"]
+skip-if = ["true"] # Bug 1448484 - sameSite1 is "Unset" - Got undefined, expected Unset
+
+["browser_storage_cookies_sort.js"]
+skip-if = ["a11y_checks"] # Bug 1858037 to investigate intermittent a11y_checks results (fails on Autoland, passes on Try)
+
+["browser_storage_cookies_tab_navigation.js"]
+
+["browser_storage_delete.js"]
+
+["browser_storage_delete_all.js"]
+
+["browser_storage_delete_tree.js"]
+
+["browser_storage_delete_usercontextid.js"]
+tags = "usercontextid"
+
+["browser_storage_dfpi.js"]
+
+["browser_storage_dfpi_always_partition_storage.js"]
+
+["browser_storage_dynamic_updates_cookies.js"]
+
+["browser_storage_dynamic_updates_localStorage.js"]
+
+["browser_storage_dynamic_updates_sessionStorage.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_storage_empty_objectstores.js"]
+
+["browser_storage_file_url.js"]
+
+["browser_storage_fission_cache.js"]
+
+["browser_storage_fission_cookies.js"]
+
+["browser_storage_fission_hide_aboutblank.js"]
+
+["browser_storage_fission_indexeddb.js"]
+
+["browser_storage_fission_local_storage.js"]
+
+["browser_storage_fission_session_storage.js"]
+
+["browser_storage_indexeddb_add_button_hidden.js"]
+
+["browser_storage_indexeddb_delete.js"]
+
+["browser_storage_indexeddb_delete_blocked.js"]
+
+["browser_storage_indexeddb_duplicate_names.js"]
+skip-if = [
+ "win11_2009", # Bug 1797751
+]
+
+["browser_storage_indexeddb_hide_internal_dbs.js"]
+skip-if = ["asan"] # Bug 1591064
+
+["browser_storage_indexeddb_navigation.js"]
+skip-if = [
+ "win10_2009 && bits == 64", # Bug 1694274
+ "os == 'linux' && bits == 64", # Bug 1694274
+]
+
+["browser_storage_indexeddb_overflow.js"]
+
+["browser_storage_keys.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_storage_localstorage_add.js"]
+
+["browser_storage_localstorage_edit.js"]
+
+["browser_storage_localstorage_error.js"]
+
+["browser_storage_localstorage_navigation.js"]
+
+["browser_storage_localstorage_rapid_add_remove.js"]
+
+["browser_storage_overflow.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_storage_search.js"]
+
+["browser_storage_search_keyboard_trap.js"]
+
+["browser_storage_sessionstorage_add.js"]
+
+["browser_storage_sessionstorage_edit.js"]
+
+["browser_storage_sessionstorage_navigation.js"]
+
+["browser_storage_sidebar.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_storage_sidebar_filter.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_storage_sidebar_parsetree.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_storage_sidebar_toggle.js"]
+
+["browser_storage_sidebar_update.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_storage_type_descriptions.js"]
+
+["browser_storage_values.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_storage_webext_storage_local.js"]
diff --git a/devtools/client/storage/test/browser_storage_basic.js b/devtools/client/storage/test/browser_storage_basic.js
new file mode 100644
index 0000000000..4a160f641b
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_basic.js
@@ -0,0 +1,172 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Basic test to assert that the storage tree and table corresponding to each
+// item in the storage tree is correctly displayed
+
+// Entries that should be present in the tree for this test
+// Format for each entry in the array :
+// [
+// ["path", "to", "tree", "item"], - The path to the tree item to click formed
+// by id of each item
+// ["key_value1", "key_value2", ...] - The value of the first (unique) column
+// for each row in the table corresponding
+// to the tree item selected.
+// ]
+// These entries are formed by the cookies, local storage, session storage and
+// indexedDB entries created in storage-listings.html,
+// storage-secured-iframe.html and storage-unsecured-iframe.html
+
+"use strict";
+
+const testCases = [
+ [
+ ["cookies", "http://test1.example.org"],
+ [
+ getCookieId("c1", "test1.example.org", "/browser"),
+ getCookieId("cs2", ".example.org", "/"),
+ getCookieId("c3", "test1.example.org", "/"),
+ getCookieId("c4", ".example.org", "/"),
+ getCookieId("uc1", ".example.org", "/"),
+ getCookieId("uc2", ".example.org", "/"),
+ ],
+ ],
+ [
+ ["cookies", "https://sectest1.example.org"],
+ [
+ getCookieId("uc1", ".example.org", "/"),
+ getCookieId("uc2", ".example.org", "/"),
+ getCookieId("cs2", ".example.org", "/"),
+ getCookieId("c4", ".example.org", "/"),
+ getCookieId(
+ "sc1",
+ "sectest1.example.org",
+ "/browser/devtools/client/storage/test"
+ ),
+ getCookieId(
+ "sc2",
+ "sectest1.example.org",
+ "/browser/devtools/client/storage/test"
+ ),
+ ],
+ ],
+ [
+ ["localStorage", "http://test1.example.org"],
+ ["key", "ls1", "ls2"],
+ ],
+ [["localStorage", "http://sectest1.example.org"], ["iframe-u-ls1"]],
+ [["localStorage", "https://sectest1.example.org"], ["iframe-s-ls1"]],
+ [
+ ["sessionStorage", "http://test1.example.org"],
+ ["key", "ss1"],
+ ],
+ [
+ ["sessionStorage", "http://sectest1.example.org"],
+ ["iframe-u-ss1", "iframe-u-ss2"],
+ ],
+ [["sessionStorage", "https://sectest1.example.org"], ["iframe-s-ss1"]],
+ [
+ ["indexedDB", "http://test1.example.org"],
+ ["idb1 (default)", "idb2 (default)"],
+ ],
+ [
+ ["indexedDB", "http://test1.example.org", "idb1 (default)"],
+ ["obj1", "obj2"],
+ ],
+ [["indexedDB", "http://test1.example.org", "idb2 (default)"], ["obj3"]],
+ [
+ ["indexedDB", "http://test1.example.org", "idb1 (default)", "obj1"],
+ [1, 2, 3],
+ ],
+ [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj2"], [1]],
+ [["indexedDB", "http://test1.example.org", "idb2 (default)", "obj3"], []],
+ [["indexedDB", "http://sectest1.example.org"], []],
+ [
+ ["indexedDB", "https://sectest1.example.org"],
+ ["idb-s1 (default)", "idb-s2 (default)"],
+ ],
+ [
+ ["indexedDB", "https://sectest1.example.org", "idb-s1 (default)"],
+ ["obj-s1"],
+ ],
+ [
+ ["indexedDB", "https://sectest1.example.org", "idb-s2 (default)"],
+ ["obj-s2"],
+ ],
+ [
+ ["indexedDB", "https://sectest1.example.org", "idb-s1 (default)", "obj-s1"],
+ [6, 7],
+ ],
+ [
+ ["indexedDB", "https://sectest1.example.org", "idb-s2 (default)", "obj-s2"],
+ [16],
+ ],
+ [
+ ["Cache", "http://test1.example.org", "plop"],
+ [
+ MAIN_DOMAIN + "404_cached_file.js",
+ MAIN_DOMAIN + "browser_storage_basic.js",
+ ],
+ ],
+];
+
+/**
+ * Test that the desired number of tree items are present
+ */
+function testTree() {
+ const doc = gPanelWindow.document;
+ for (const [item] of testCases) {
+ ok(
+ doc.querySelector("[data-id='" + JSON.stringify(item) + "']"),
+ `Tree item ${item.toSource()} should be present in the storage tree`
+ );
+ }
+}
+
+/**
+ * Test that correct table entries are shown for each of the tree item
+ */
+async function testTables() {
+ const doc = gPanelWindow.document;
+ // Expand all nodes so that the synthesized click event actually works
+ gUI.tree.expandAll();
+
+ // First tree item is already selected so no clicking and waiting for update
+ for (const id of testCases[0][1]) {
+ ok(
+ doc.querySelector(".table-widget-cell[data-id='" + id + "']"),
+ "Table item " + id + " should be present"
+ );
+ }
+
+ // Click rest of the tree items and wait for the table to be updated
+ for (const [treeItem, items] of testCases.slice(1)) {
+ await selectTreeItem(treeItem);
+
+ // Check whether correct number of items are present in the table
+ is(
+ doc.querySelectorAll(
+ ".table-widget-column:first-of-type .table-widget-cell"
+ ).length,
+ items.length,
+ "Number of items in table is correct"
+ );
+
+ // Check if all the desired items are present in the table
+ for (const id of items) {
+ ok(
+ doc.querySelector(".table-widget-cell[data-id='" + id + "']"),
+ "Table item " + id + " should be present"
+ );
+ }
+ }
+}
+
+add_task(async function () {
+ await pushPref("dom.security.https_first", false);
+ await openTabAndSetupStorage(MAIN_DOMAIN + "storage-listings.html");
+
+ testTree();
+ await testTables();
+});
diff --git a/devtools/client/storage/test/browser_storage_basic_usercontextid_1.js b/devtools/client/storage/test/browser_storage_basic_usercontextid_1.js
new file mode 100644
index 0000000000..fbd477b61d
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_basic_usercontextid_1.js
@@ -0,0 +1,162 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// A test to check that the storage inspector is working correctly without
+// userContextId.
+
+"use strict";
+
+const testCases = [
+ [
+ ["cookies", "http://test1.example.org"],
+ [
+ getCookieId("c1", "test1.example.org", "/browser"),
+ getCookieId("cs2", ".example.org", "/"),
+ getCookieId("c3", "test1.example.org", "/"),
+ getCookieId("c4", ".example.org", "/"),
+ getCookieId("uc1", ".example.org", "/"),
+ getCookieId("uc2", ".example.org", "/"),
+ ],
+ ],
+ [
+ ["cookies", "https://sectest1.example.org"],
+ [
+ getCookieId("uc1", ".example.org", "/"),
+ getCookieId("uc2", ".example.org", "/"),
+ getCookieId("cs2", ".example.org", "/"),
+ getCookieId("c4", ".example.org", "/"),
+ getCookieId(
+ "sc1",
+ "sectest1.example.org",
+ "/browser/devtools/client/storage/test"
+ ),
+ getCookieId(
+ "sc2",
+ "sectest1.example.org",
+ "/browser/devtools/client/storage/test"
+ ),
+ ],
+ ],
+ [
+ ["localStorage", "http://test1.example.org"],
+ ["key", "ls1", "ls2"],
+ ],
+ [["localStorage", "http://sectest1.example.org"], ["iframe-u-ls1"]],
+ [["localStorage", "https://sectest1.example.org"], ["iframe-s-ls1"]],
+ [
+ ["sessionStorage", "http://test1.example.org"],
+ ["key", "ss1"],
+ ],
+ [
+ ["sessionStorage", "http://sectest1.example.org"],
+ ["iframe-u-ss1", "iframe-u-ss2"],
+ ],
+ [["sessionStorage", "https://sectest1.example.org"], ["iframe-s-ss1"]],
+ [
+ ["indexedDB", "http://test1.example.org"],
+ ["idb1 (default)", "idb2 (default)"],
+ ],
+ [
+ ["indexedDB", "http://test1.example.org", "idb1 (default)"],
+ ["obj1", "obj2"],
+ ],
+ [["indexedDB", "http://test1.example.org", "idb2 (default)"], ["obj3"]],
+ [
+ ["indexedDB", "http://test1.example.org", "idb1 (default)", "obj1"],
+ [1, 2, 3],
+ ],
+ [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj2"], [1]],
+ [["indexedDB", "http://test1.example.org", "idb2 (default)", "obj3"], []],
+ [["indexedDB", "http://sectest1.example.org"], []],
+ [
+ ["indexedDB", "https://sectest1.example.org"],
+ ["idb-s1 (default)", "idb-s2 (default)"],
+ ],
+ [
+ ["indexedDB", "https://sectest1.example.org", "idb-s1 (default)"],
+ ["obj-s1"],
+ ],
+ [
+ ["indexedDB", "https://sectest1.example.org", "idb-s2 (default)"],
+ ["obj-s2"],
+ ],
+ [
+ ["indexedDB", "https://sectest1.example.org", "idb-s1 (default)", "obj-s1"],
+ [6, 7],
+ ],
+ [
+ ["indexedDB", "https://sectest1.example.org", "idb-s2 (default)", "obj-s2"],
+ [16],
+ ],
+ [
+ ["Cache", "http://test1.example.org", "plop"],
+ [
+ MAIN_DOMAIN + "404_cached_file.js",
+ MAIN_DOMAIN + "browser_storage_basic.js",
+ ],
+ ],
+];
+
+/**
+ * Test that the desired number of tree items are present
+ */
+function testTree(tests) {
+ const doc = gPanelWindow.document;
+ for (const [item] of tests) {
+ ok(
+ doc.querySelector("[data-id='" + JSON.stringify(item) + "']"),
+ `Tree item ${item.toSource()} should be present in the storage tree`
+ );
+ }
+}
+
+/**
+ * Test that correct table entries are shown for each of the tree item
+ */
+async function testTables(tests) {
+ const doc = gPanelWindow.document;
+ // Expand all nodes so that the synthesized click event actually works
+ gUI.tree.expandAll();
+
+ // First tree item is already selected so no clicking and waiting for update
+ for (const id of tests[0][1]) {
+ ok(
+ doc.querySelector(".table-widget-cell[data-id='" + id + "']"),
+ "Table item " + id + " should be present"
+ );
+ }
+
+ // Click rest of the tree items and wait for the table to be updated
+ for (const [treeItem, items] of tests.slice(1)) {
+ await selectTreeItem(treeItem);
+
+ // Check whether correct number of items are present in the table
+ is(
+ doc.querySelectorAll(
+ ".table-widget-column:first-of-type .table-widget-cell"
+ ).length,
+ items.length,
+ "Number of items in table is correct"
+ );
+
+ // Check if all the desired items are present in the table
+ for (const id of items) {
+ ok(
+ doc.querySelector(".table-widget-cell[data-id='" + id + "']"),
+ "Table item " + id + " should be present"
+ );
+ }
+ }
+}
+
+add_task(async function () {
+ // storage-listings.html explicitly mixes secure and insecure frames.
+ // We should not enforce https for tests using this page.
+ await pushPref("dom.security.https_first", false);
+
+ await openTabAndSetupStorage(MAIN_DOMAIN + "storage-listings.html");
+
+ testTree(testCases);
+ await testTables(testCases);
+});
diff --git a/devtools/client/storage/test/browser_storage_basic_usercontextid_2.js b/devtools/client/storage/test/browser_storage_basic_usercontextid_2.js
new file mode 100644
index 0000000000..e6c3151cd3
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_basic_usercontextid_2.js
@@ -0,0 +1,169 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// A test to check that the storage inspector is working correctly with
+// userContextId.
+
+"use strict";
+
+const testCasesUserContextId = [
+ [
+ ["cookies", "http://test1.example.org"],
+ [
+ getCookieId("c1uc1", "test1.example.org", "/browser"),
+ getCookieId("cs2uc1", ".example.org", "/"),
+ getCookieId("c3uc1", "test1.example.org", "/"),
+ getCookieId("uc1uc1", ".example.org", "/"),
+ ],
+ ],
+ [
+ ["cookies", "https://sectest1.example.org"],
+ [
+ getCookieId("uc1uc1", ".example.org", "/"),
+ getCookieId("cs2uc1", ".example.org", "/"),
+ getCookieId(
+ "sc1uc1",
+ "sectest1.example.org",
+ "/browser/devtools/client/storage/test"
+ ),
+ ],
+ ],
+ [
+ ["localStorage", "http://test1.example.org"],
+ ["ls1uc1", "ls2uc1"],
+ ],
+ [["localStorage", "http://sectest1.example.org"], ["iframe-u-ls1uc1"]],
+ [["localStorage", "https://sectest1.example.org"], ["iframe-s-ls1uc1"]],
+ [["sessionStorage", "http://test1.example.org"], ["ss1uc1"]],
+ [
+ ["sessionStorage", "http://sectest1.example.org"],
+ ["iframe-u-ss1uc1", "iframe-u-ss2uc1"],
+ ],
+ [["sessionStorage", "https://sectest1.example.org"], ["iframe-s-ss1uc1"]],
+ [
+ ["indexedDB", "http://test1.example.org"],
+ ["idb1uc1 (default)", "idb2uc1 (default)"],
+ ],
+ [
+ ["indexedDB", "http://test1.example.org", "idb1uc1 (default)"],
+ ["obj1uc1", "obj2uc1"],
+ ],
+ [["indexedDB", "http://test1.example.org", "idb2uc1 (default)"], ["obj3uc1"]],
+ [
+ ["indexedDB", "http://test1.example.org", "idb1uc1 (default)", "obj1uc1"],
+ [1, 2, 3],
+ ],
+ [
+ ["indexedDB", "http://test1.example.org", "idb1uc1 (default)", "obj2uc1"],
+ [1],
+ ],
+ [
+ ["indexedDB", "http://test1.example.org", "idb2uc1 (default)", "obj3uc1"],
+ [],
+ ],
+ [["indexedDB", "http://sectest1.example.org"], []],
+ [
+ ["indexedDB", "https://sectest1.example.org"],
+ ["idb-s1uc1 (default)", "idb-s2uc1 (default)"],
+ ],
+ [
+ ["indexedDB", "https://sectest1.example.org", "idb-s1uc1 (default)"],
+ ["obj-s1uc1"],
+ ],
+ [
+ ["indexedDB", "https://sectest1.example.org", "idb-s2uc1 (default)"],
+ ["obj-s2uc1"],
+ ],
+ [
+ [
+ "indexedDB",
+ "https://sectest1.example.org",
+ "idb-s1uc1 (default)",
+ "obj-s1uc1",
+ ],
+ [6, 7],
+ ],
+ [
+ [
+ "indexedDB",
+ "https://sectest1.example.org",
+ "idb-s2uc1 (default)",
+ "obj-s2uc1",
+ ],
+ [16],
+ ],
+ [
+ ["Cache", "http://test1.example.org", "plopuc1"],
+ [
+ MAIN_DOMAIN + "404_cached_file.js",
+ MAIN_DOMAIN + "browser_storage_basic.js",
+ ],
+ ],
+];
+
+/**
+ * Test that the desired number of tree items are present
+ */
+function testTree(tests) {
+ const doc = gPanelWindow.document;
+ for (const [item] of tests) {
+ ok(
+ doc.querySelector("[data-id='" + JSON.stringify(item) + "']"),
+ `Tree item ${item.toSource()} should be present in the storage tree`
+ );
+ }
+}
+
+/**
+ * Test that correct table entries are shown for each of the tree item
+ */
+async function testTables(tests) {
+ const doc = gPanelWindow.document;
+ // Expand all nodes so that the synthesized click event actually works
+ gUI.tree.expandAll();
+
+ // First tree item is already selected so no clicking and waiting for update
+ for (const id of tests[0][1]) {
+ ok(
+ doc.querySelector(".table-widget-cell[data-id='" + id + "']"),
+ "Table item " + id + " should be present"
+ );
+ }
+
+ // Click rest of the tree items and wait for the table to be updated
+ for (const [treeItem, items] of tests.slice(1)) {
+ await selectTreeItem(treeItem);
+
+ // Check whether correct number of items are present in the table
+ is(
+ doc.querySelectorAll(
+ ".table-widget-column:first-of-type .table-widget-cell"
+ ).length,
+ items.length,
+ "Number of items in table is correct"
+ );
+
+ // Check if all the desired items are present in the table
+ for (const id of items) {
+ ok(
+ doc.querySelector(".table-widget-cell[data-id='" + id + "']"),
+ "Table item " + id + " should be present"
+ );
+ }
+ }
+}
+
+add_task(async function () {
+ // storage-listings.html explicitly mixes secure and insecure frames.
+ // We should not enforce https for tests using this page.
+ await pushPref("dom.security.https_first", false);
+
+ await openTabAndSetupStorage(
+ MAIN_DOMAIN + "storage-listings-usercontextid.html",
+ { userContextId: 1 }
+ );
+
+ testTree(testCasesUserContextId);
+ await testTables(testCasesUserContextId);
+});
diff --git a/devtools/client/storage/test/browser_storage_basic_with_fragment.js b/devtools/client/storage/test/browser_storage_basic_with_fragment.js
new file mode 100644
index 0000000000..dcb18c7752
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_basic_with_fragment.js
@@ -0,0 +1,177 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// A second basic test to assert that the storage tree and table corresponding
+// to each item in the storage tree is correctly displayed.
+
+// This test differs from browser_storage_basic.js because the URLs we load
+// include fragments e.g. http://example.com/test.js#abcdefg
+// ^^^^^^^^
+// fragment
+
+// Entries that should be present in the tree for this test
+// Format for each entry in the array :
+// [
+// ["path", "to", "tree", "item"], - The path to the tree item to click formed
+// by id of each item
+// ["key_value1", "key_value2", ...] - The value of the first (unique) column
+// for each row in the table corresponding
+// to the tree item selected.
+// ]
+// These entries are formed by the cookies, local storage, session storage and
+// indexedDB entries created in storage-listings.html,
+// storage-secured-iframe.html and storage-unsecured-iframe.html
+
+"use strict";
+
+const testCases = [
+ [
+ ["cookies", "http://test1.example.org"],
+ [
+ getCookieId("c1", "test1.example.org", "/browser"),
+ getCookieId("cs2", ".example.org", "/"),
+ getCookieId("c3", "test1.example.org", "/"),
+ getCookieId("uc1", ".example.org", "/"),
+ getCookieId("uc2", ".example.org", "/"),
+ ],
+ ],
+ [
+ ["cookies", "https://sectest1.example.org"],
+ [
+ getCookieId("uc1", ".example.org", "/"),
+ getCookieId("uc2", ".example.org", "/"),
+ getCookieId("cs2", ".example.org", "/"),
+ getCookieId(
+ "sc1",
+ "sectest1.example.org",
+ "/browser/devtools/client/storage/test"
+ ),
+ getCookieId(
+ "sc2",
+ "sectest1.example.org",
+ "/browser/devtools/client/storage/test"
+ ),
+ ],
+ ],
+ [
+ ["localStorage", "http://test1.example.org"],
+ ["ls1", "ls2"],
+ ],
+ [["localStorage", "http://sectest1.example.org"], ["iframe-u-ls1"]],
+ [["localStorage", "https://sectest1.example.org"], ["iframe-s-ls1"]],
+ [["sessionStorage", "http://test1.example.org"], ["ss1"]],
+ [
+ ["sessionStorage", "http://sectest1.example.org"],
+ ["iframe-u-ss1", "iframe-u-ss2"],
+ ],
+ [["sessionStorage", "https://sectest1.example.org"], ["iframe-s-ss1"]],
+ [
+ ["indexedDB", "http://test1.example.org"],
+ ["idb1 (default)", "idb2 (default)"],
+ ],
+ [
+ ["indexedDB", "http://test1.example.org", "idb1 (default)"],
+ ["obj1", "obj2"],
+ ],
+ [["indexedDB", "http://test1.example.org", "idb2 (default)"], ["obj3"]],
+ [
+ ["indexedDB", "http://test1.example.org", "idb1 (default)", "obj1"],
+ [1, 2, 3],
+ ],
+ [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj2"], [1]],
+ [["indexedDB", "http://test1.example.org", "idb2 (default)", "obj3"], []],
+ [["indexedDB", "http://sectest1.example.org"], []],
+ [
+ ["indexedDB", "https://sectest1.example.org"],
+ ["idb-s1 (default)", "idb-s2 (default)"],
+ ],
+ [
+ ["indexedDB", "https://sectest1.example.org", "idb-s1 (default)"],
+ ["obj-s1"],
+ ],
+ [
+ ["indexedDB", "https://sectest1.example.org", "idb-s2 (default)"],
+ ["obj-s2"],
+ ],
+ [
+ ["indexedDB", "https://sectest1.example.org", "idb-s1 (default)", "obj-s1"],
+ [6, 7],
+ ],
+ [
+ ["indexedDB", "https://sectest1.example.org", "idb-s2 (default)", "obj-s2"],
+ [16],
+ ],
+ [
+ ["Cache", "http://test1.example.org", "plop"],
+ [
+ MAIN_DOMAIN + "404_cached_file.js",
+ MAIN_DOMAIN + "browser_storage_basic.js",
+ ],
+ ],
+];
+
+/**
+ * Test that the desired number of tree items are present
+ */
+function testTree() {
+ const doc = gPanelWindow.document;
+ for (const [item] of testCases) {
+ ok(
+ doc.querySelector("[data-id='" + JSON.stringify(item) + "']"),
+ `Tree item ${item.toSource()} should be present in the storage tree`
+ );
+ }
+}
+
+/**
+ * Test that correct table entries are shown for each of the tree item
+ */
+async function testTables() {
+ const doc = gPanelWindow.document;
+ // Expand all nodes so that the synthesized click event actually works
+ gUI.tree.expandAll();
+
+ // First tree item is already selected so no clicking and waiting for update
+ for (const id of testCases[0][1]) {
+ ok(
+ doc.querySelector(".table-widget-cell[data-id='" + id + "']"),
+ "Table item " + id + " should be present"
+ );
+ }
+
+ // Click rest of the tree items and wait for the table to be updated
+ for (const [treeItem, items] of testCases.slice(1)) {
+ await selectTreeItem(treeItem);
+
+ // Check whether correct number of items are present in the table
+ is(
+ doc.querySelectorAll(
+ ".table-widget-column:first-of-type .table-widget-cell"
+ ).length,
+ items.length,
+ "Number of items in table is correct"
+ );
+
+ // Check if all the desired items are present in the table
+ for (const id of items) {
+ ok(
+ doc.querySelector(".table-widget-cell[data-id='" + id + "']"),
+ "Table item " + id + " should be present"
+ );
+ }
+ }
+}
+
+add_task(async function () {
+ // storage-listings.html explicitly mixes secure and insecure frames.
+ // We should not enforce https for tests using this page.
+ await pushPref("dom.security.https_first", false);
+
+ await openTabAndSetupStorage(
+ MAIN_DOMAIN + "storage-listings-with-fragment.html#abc"
+ );
+
+ testTree();
+ await testTables();
+});
diff --git a/devtools/client/storage/test/browser_storage_cache_delete.js b/devtools/client/storage/test/browser_storage_cache_delete.js
new file mode 100644
index 0000000000..991ce22891
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_cache_delete.js
@@ -0,0 +1,53 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Test deleting a Cache object from the tree using context menu
+
+add_task(async function () {
+ await pushPref("dom.security.https_first", false);
+ await openTabAndSetupStorage(MAIN_DOMAIN + "storage-listings.html");
+
+ const contextMenu =
+ gPanelWindow.document.getElementById("storage-tree-popup");
+ const menuDeleteItem = contextMenu.querySelector(
+ "#storage-tree-popup-delete"
+ );
+
+ const cacheToDelete = ["Cache", "http://test1.example.org", "plop"];
+
+ info("test state before delete");
+ await selectTreeItem(cacheToDelete);
+ ok(gUI.tree.isSelected(cacheToDelete), "Cache item is present in the tree");
+
+ info("do the delete");
+ const eventWait = gUI.once("store-objects-updated");
+
+ const selector = `[data-id='${JSON.stringify(
+ cacheToDelete
+ )}'] > .tree-widget-item`;
+ const target = gPanelWindow.document.querySelector(selector);
+ ok(target, "Cache item's tree element is present");
+
+ await waitForContextMenu(contextMenu, target, () => {
+ info("Opened tree context menu");
+ menuDeleteItem.click();
+
+ const cacheName = cacheToDelete[2];
+ ok(
+ menuDeleteItem.getAttribute("label").includes(cacheName),
+ `Context menu item label contains '${cacheName}')`
+ );
+ });
+
+ await eventWait;
+
+ info("test state after delete");
+ await selectTreeItem(cacheToDelete);
+ ok(
+ !gUI.tree.isSelected(cacheToDelete),
+ "Cache item is no longer present in the tree"
+ );
+});
diff --git a/devtools/client/storage/test/browser_storage_cache_error.js b/devtools/client/storage/test/browser_storage_cache_error.js
new file mode 100644
index 0000000000..4aef96d0a8
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_cache_error.js
@@ -0,0 +1,35 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Test handling errors in CacheStorage
+
+add_task(async function () {
+ // Open the URL in a private browsing window.
+ const win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+ const tab = win.gBrowser.selectedBrowser;
+ const triggeringPrincipal =
+ Services.scriptSecurityManager.getSystemPrincipal();
+ tab.loadURI(
+ Services.io.newURI(ALT_DOMAIN_SECURED + "storage-cache-error.html"),
+ { triggeringPrincipal }
+ );
+ await BrowserTestUtils.browserLoaded(tab);
+
+ // On enumerating cache storages, CacheStorage::Keys would throw a
+ // DOM security exception. We'd like to verify storage panel still work in
+ // this case.
+ await openStoragePanel({ tab: win.gBrowser.selectedTab });
+
+ const cacheItemId = ["Cache", "https://test2.example.org"];
+
+ await selectTreeItem(cacheItemId);
+ ok(
+ gUI.tree.isSelected(cacheItemId),
+ `The item ${cacheItemId.join(" > ")} is present in the tree`
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/devtools/client/storage/test/browser_storage_cache_navigation.js b/devtools/client/storage/test/browser_storage_cache_navigation.js
new file mode 100644
index 0000000000..5421b85028
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_cache_navigation.js
@@ -0,0 +1,84 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function () {
+ const URL1 = buildURLWithContent(
+ "example.com",
+ `<h1>example.com</h1>` +
+ `<script>
+ caches.open("lorem").then(cache => {
+ cache.add("${URL_ROOT_COM_SSL}storage-blank.html");
+ });
+ function clear() {
+ caches.delete("lorem");
+ }
+ </script>`
+ );
+ const URL2 = buildURLWithContent(
+ "example.net",
+ `<h1>example.net</h1>` +
+ `<script>
+ caches.open("foo").then(cache => {
+ cache.add("${URL_ROOT_NET_SSL}storage-blank.html");
+ });
+ function clear() {
+ caches.delete("foo");
+ }
+ </script>`
+ );
+
+ // open tab
+ await openTabAndSetupStorage(URL1);
+ const doc = gPanelWindow.document;
+
+ // Check first domain
+ // check that host appears in the storage tree
+ checkTree(doc, ["Cache", "https://example.com", "lorem"]);
+ // check the table for values
+ await selectTreeItem(["Cache", "https://example.com", "lorem"]);
+ checkCacheData(URL_ROOT_COM_SSL + "storage-blank.html", "OK");
+
+ // clear up the cache before navigating
+ info("Cleaning up cache…");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ const win = content.wrappedJSObject;
+ await win.clear();
+ });
+
+ // Check second domain
+ await navigateTo(URL2);
+
+ // Select the Cache view in order to force updating it
+ await selectTreeItem(["Cache", "https://example.net"]);
+
+ // wait for storage tree refresh, and check host
+ info("Waiting for storage tree to update…");
+ await waitUntil(() => isInTree(doc, ["Cache", "https://example.net", "foo"]));
+
+ ok(
+ !isInTree(doc, ["Cache", "https://example.com"]),
+ "example.com item is not in the tree anymore"
+ );
+
+ // check the table for values
+ await selectTreeItem(["Cache", "https://example.net", "foo"]);
+ checkCacheData(URL_ROOT_NET_SSL + "storage-blank.html", "OK");
+
+ info("Check that the Cache node still has the expected label");
+ is(
+ getTreeNodeLabel(doc, ["Cache"]),
+ "Cache Storage",
+ "Cache item is properly displayed"
+ );
+});
+
+function checkCacheData(url, status) {
+ is(
+ gUI.table.items.get(url)?.status,
+ status,
+ `Table row has an entry for: ${url} with status: ${status}`
+ );
+}
diff --git a/devtools/client/storage/test/browser_storage_cache_overflow.js b/devtools/client/storage/test/browser_storage_cache_overflow.js
new file mode 100644
index 0000000000..86e761b77b
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_cache_overflow.js
@@ -0,0 +1,32 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Test endless scrolling when a lot of items are present in the storage
+// inspector table for Cache storage.
+"use strict";
+
+const ITEMS_PER_PAGE = 50;
+
+add_task(async function () {
+ await openTabAndSetupStorage(
+ URL_ROOT_COM_SSL + "storage-cache-overflow.html"
+ );
+
+ gUI.tree.expandAll();
+
+ await selectTreeItem(["Cache", "https://example.com", "lorem"]);
+ await waitFor(
+ () => getCellLength() == ITEMS_PER_PAGE,
+ "Wait until the first 50 messages have been rendered"
+ );
+
+ await scroll();
+ await waitFor(
+ () => getCellLength() == ITEMS_PER_PAGE * 2,
+ "Wait until 100 messages have been rendered"
+ );
+
+ info("Close Toolbox");
+ await gDevTools.closeToolboxForTab(gBrowser.selectedTab);
+});
diff --git a/devtools/client/storage/test/browser_storage_cookies_add.js b/devtools/client/storage/test/browser_storage_cookies_add.js
new file mode 100644
index 0000000000..a4eda3cd0e
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_cookies_add.js
@@ -0,0 +1,59 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Basic test to check the adding of cookies.
+
+"use strict";
+
+add_task(async function () {
+ const TEST_URL = MAIN_DOMAIN + "storage-cookies.html";
+ await openTabAndSetupStorage(TEST_URL);
+ showAllColumns(true);
+
+ const rowId = await performAdd(["cookies", "http://test1.example.org"]);
+ checkCookieData(rowId);
+
+ await performAdd(["cookies", "http://test1.example.org"]);
+ await performAdd(["cookies", "http://test1.example.org"]);
+ await performAdd(["cookies", "http://test1.example.org"]);
+ await performAdd(["cookies", "http://test1.example.org"]);
+
+ info("Check it does work in private tabs too");
+ const privateWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ ok(PrivateBrowsingUtils.isWindowPrivate(privateWindow), "window is private");
+ const privateTab = await addTab(TEST_URL, { window: privateWindow });
+ await openStoragePanel({ tab: privateTab });
+ const privateTabRowId = await performAdd([
+ "cookies",
+ "http://test1.example.org",
+ ]);
+ checkCookieData(privateTabRowId);
+
+ await performAdd(["cookies", "http://test1.example.org"]);
+ privateWindow.close();
+});
+
+function checkCookieData(rowId) {
+ is(getCellValue(rowId, "value"), "value", "value is correct");
+ is(getCellValue(rowId, "host"), "test1.example.org", "host is correct");
+ is(getCellValue(rowId, "path"), "/", "path is correct");
+ const actualExpiry = Math.floor(
+ new Date(getCellValue(rowId, "expires")) / 1000
+ );
+ const ONE_DAY_IN_SECONDS = 60 * 60 * 24;
+ const time = Math.floor(new Date(getCellValue(rowId, "creationTime")) / 1000);
+ const expectedExpiry = time + ONE_DAY_IN_SECONDS;
+ Assert.lessOrEqual(
+ actualExpiry - expectedExpiry,
+ 2,
+ "expiry is in expected range"
+ );
+ is(getCellValue(rowId, "size"), "43", "size is correct");
+ is(getCellValue(rowId, "isHttpOnly"), "false", "httpOnly is not set");
+ is(getCellValue(rowId, "isSecure"), "false", "secure is not set");
+ is(getCellValue(rowId, "sameSite"), "Lax", "sameSite is Lax");
+ is(getCellValue(rowId, "hostOnly"), "true", "hostOnly is not set");
+}
diff --git a/devtools/client/storage/test/browser_storage_cookies_delete_all.js b/devtools/client/storage/test/browser_storage_cookies_delete_all.js
new file mode 100644
index 0000000000..a637b8a4ab
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_cookies_delete_all.js
@@ -0,0 +1,185 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Test deleting all cookies
+
+async function performDelete(store, rowName, action) {
+ const contextMenu = gPanelWindow.document.getElementById(
+ "storage-table-popup"
+ );
+ const menuDeleteAllItem = contextMenu.querySelector(
+ "#storage-table-popup-delete-all"
+ );
+ const menuDeleteAllSessionCookiesItem = contextMenu.querySelector(
+ "#storage-table-popup-delete-all-session-cookies"
+ );
+ const menuDeleteAllFromItem = contextMenu.querySelector(
+ "#storage-table-popup-delete-all-from"
+ );
+
+ const storeName = store.join(" > ");
+
+ await selectTreeItem(store);
+
+ const eventWait = gUI.once("store-objects-edit");
+ const cells = getRowCells(rowName, true);
+
+ await waitForContextMenu(contextMenu, cells.name, () => {
+ info(`Opened context menu in ${storeName}, row '${rowName}'`);
+ switch (action) {
+ case "deleteAll":
+ menuDeleteAllItem.click();
+ break;
+ case "deleteAllSessionCookies":
+ menuDeleteAllSessionCookiesItem.click();
+ break;
+ case "deleteAllFrom":
+ menuDeleteAllFromItem.click();
+ const hostName = cells.host.value;
+ ok(
+ menuDeleteAllFromItem.getAttribute("label").includes(hostName),
+ `Context menu item label contains '${hostName}'`
+ );
+ break;
+ }
+ });
+
+ await eventWait;
+}
+
+add_task(async function () {
+ // storage-listings.html explicitly mixes secure and insecure frames.
+ // We should not enforce https for tests using this page.
+ await pushPref("dom.security.https_first", false);
+
+ await openTabAndSetupStorage(MAIN_DOMAIN + "storage-listings.html");
+
+ info("test state before delete");
+ await checkState([
+ [
+ ["cookies", "http://test1.example.org"],
+ [
+ getCookieId("c1", "test1.example.org", "/browser"),
+ getCookieId("c3", "test1.example.org", "/"),
+ getCookieId("cs2", ".example.org", "/"),
+ getCookieId("c4", ".example.org", "/"),
+ getCookieId("uc1", ".example.org", "/"),
+ getCookieId("uc2", ".example.org", "/"),
+ ],
+ ],
+ [
+ ["cookies", "https://sectest1.example.org"],
+ [
+ getCookieId("cs2", ".example.org", "/"),
+ getCookieId("c4", ".example.org", "/"),
+ getCookieId(
+ "sc1",
+ "sectest1.example.org",
+ "/browser/devtools/client/storage/test"
+ ),
+ getCookieId(
+ "sc2",
+ "sectest1.example.org",
+ "/browser/devtools/client/storage/test"
+ ),
+ getCookieId("uc1", ".example.org", "/"),
+ getCookieId("uc2", ".example.org", "/"),
+ ],
+ ],
+ ]);
+
+ info("delete all from domain");
+ // delete only cookies that match the host exactly
+ let id = getCookieId("c1", "test1.example.org", "/browser");
+ await performDelete(
+ ["cookies", "http://test1.example.org"],
+ id,
+ "deleteAllFrom"
+ );
+
+ info("test state after delete all from domain");
+ await checkState([
+ // Domain cookies (.example.org) must not be deleted.
+ [
+ ["cookies", "http://test1.example.org"],
+ [
+ getCookieId("cs2", ".example.org", "/"),
+ getCookieId("c4", ".example.org", "/"),
+ getCookieId("uc1", ".example.org", "/"),
+ getCookieId("uc2", ".example.org", "/"),
+ ],
+ ],
+ [
+ ["cookies", "https://sectest1.example.org"],
+ [
+ getCookieId("cs2", ".example.org", "/"),
+ getCookieId("c4", ".example.org", "/"),
+ getCookieId("uc1", ".example.org", "/"),
+ getCookieId("uc2", ".example.org", "/"),
+ getCookieId(
+ "sc1",
+ "sectest1.example.org",
+ "/browser/devtools/client/storage/test"
+ ),
+ getCookieId(
+ "sc2",
+ "sectest1.example.org",
+ "/browser/devtools/client/storage/test"
+ ),
+ ],
+ ],
+ ]);
+
+ info("delete all session cookies");
+ // delete only session cookies
+ id = getCookieId("cs2", ".example.org", "/");
+ await performDelete(
+ ["cookies", "http://sectest1.example.org"],
+ id,
+ "deleteAllSessionCookies"
+ );
+
+ info("test state after delete all session cookies");
+ await checkState([
+ // Cookies with expiry date must not be deleted.
+ [
+ ["cookies", "http://test1.example.org"],
+ [
+ getCookieId("c4", ".example.org", "/"),
+ getCookieId("uc2", ".example.org", "/"),
+ ],
+ ],
+ [
+ ["cookies", "https://sectest1.example.org"],
+ [
+ getCookieId("c4", ".example.org", "/"),
+ getCookieId("uc2", ".example.org", "/"),
+ getCookieId(
+ "sc2",
+ "sectest1.example.org",
+ "/browser/devtools/client/storage/test"
+ ),
+ ],
+ ],
+ ]);
+
+ info("delete all");
+ // delete all cookies for host, including domain cookies
+ id = getCookieId("uc2", ".example.org", "/");
+ await performDelete(
+ ["cookies", "http://sectest1.example.org"],
+ id,
+ "deleteAll"
+ );
+
+ info("test state after delete all");
+ await checkState([
+ // Domain cookies (.example.org) are deleted too, so deleting in sectest1
+ // also removes stuff from test1.
+ [["cookies", "http://test1.example.org"], []],
+ [["cookies", "https://sectest1.example.org"], []],
+ ]);
+});
diff --git a/devtools/client/storage/test/browser_storage_cookies_domain.js b/devtools/client/storage/test/browser_storage_cookies_domain.js
new file mode 100644
index 0000000000..5e4510196a
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_cookies_domain.js
@@ -0,0 +1,25 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Test that cookies with domain equal to full host name are listed.
+// E.g., ".example.org" vs. example.org). Bug 1149497.
+
+add_task(async function () {
+ await openTabAndSetupStorage(MAIN_DOMAIN + "storage-cookies.html");
+
+ await checkState([
+ [
+ ["cookies", "http://test1.example.org"],
+ [
+ getCookieId("test1", ".test1.example.org", "/browser"),
+ getCookieId("test2", "test1.example.org", "/browser"),
+ getCookieId("test3", ".test1.example.org", "/browser"),
+ getCookieId("test4", "test1.example.org", "/browser"),
+ getCookieId("test5", ".test1.example.org", "/browser"),
+ ],
+ ],
+ ]);
+});
diff --git a/devtools/client/storage/test/browser_storage_cookies_domain_port.js b/devtools/client/storage/test/browser_storage_cookies_domain_port.js
new file mode 100644
index 0000000000..8dd8f25d0a
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_cookies_domain_port.js
@@ -0,0 +1,25 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Test that cookies with domain equal to full host name and port are listed.
+// E.g., ".example.org:8000" vs. example.org:8000).
+
+add_task(async function () {
+ await openTabAndSetupStorage(MAIN_DOMAIN_WITH_PORT + "storage-cookies.html");
+
+ await checkState([
+ [
+ ["cookies", "http://test1.example.org:8000"],
+ [
+ getCookieId("test1", ".test1.example.org", "/browser"),
+ getCookieId("test2", "test1.example.org", "/browser"),
+ getCookieId("test3", ".test1.example.org", "/browser"),
+ getCookieId("test4", "test1.example.org", "/browser"),
+ getCookieId("test5", ".test1.example.org", "/browser"),
+ ],
+ ],
+ ]);
+});
diff --git a/devtools/client/storage/test/browser_storage_cookies_edit.js b/devtools/client/storage/test/browser_storage_cookies_edit.js
new file mode 100644
index 0000000000..f49635750f
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_cookies_edit.js
@@ -0,0 +1,27 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Basic test to check the editing of cookies.
+
+"use strict";
+
+add_task(async function () {
+ await openTabAndSetupStorage(MAIN_DOMAIN + "storage-cookies.html");
+ showAllColumns(true);
+
+ let id = getCookieId("test3", ".test1.example.org", "/browser");
+ await editCell(id, "name", "newTest3");
+
+ id = getCookieId("newTest3", ".test1.example.org", "/browser");
+ await editCell(id, "host", "test1.example.org");
+
+ id = getCookieId("newTest3", "test1.example.org", "/browser");
+ await editCell(id, "path", "/");
+
+ id = getCookieId("newTest3", "test1.example.org", "/");
+ await editCell(id, "expires", "Tue, 14 Feb 2040 17:41:14 GMT");
+ await editCell(id, "value", "newValue3");
+ await editCell(id, "isSecure", "true");
+ await editCell(id, "isHttpOnly", "true");
+});
diff --git a/devtools/client/storage/test/browser_storage_cookies_edit_keyboard.js b/devtools/client/storage/test/browser_storage_cookies_edit_keyboard.js
new file mode 100644
index 0000000000..c7f7857cf5
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_cookies_edit_keyboard.js
@@ -0,0 +1,23 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Basic test to check the editing of cookies with the keyboard.
+
+"use strict";
+
+add_task(async function () {
+ await openTabAndSetupStorage(MAIN_DOMAIN + "storage-cookies.html");
+ showAllColumns(true);
+ showColumn("uniqueKey", false);
+
+ const id = getCookieId("test4", "test1.example.org", "/browser");
+ await startCellEdit(id, "name");
+ await typeWithTerminator("test6", "KEY_Tab");
+ await typeWithTerminator("test6value", "KEY_Tab");
+ await typeWithTerminator(".example.org", "KEY_Tab");
+ await typeWithTerminator("/", "KEY_Tab");
+ await typeWithTerminator("Tue, 25 Dec 2040 12:00:00 GMT", "KEY_Tab");
+ await typeWithTerminator("false", "KEY_Tab");
+ await typeWithTerminator("false", "KEY_Tab");
+});
diff --git a/devtools/client/storage/test/browser_storage_cookies_hostOnly.js b/devtools/client/storage/test/browser_storage_cookies_hostOnly.js
new file mode 100644
index 0000000000..99280cff9f
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_cookies_hostOnly.js
@@ -0,0 +1,27 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Test that the HostOnly values displayed in the table are correct.
+
+SpecialPowers.pushPrefEnv({
+ set: [["security.allow_eval_with_system_principal", true]],
+});
+
+add_task(async function () {
+ await openTabAndSetupStorage(MAIN_DOMAIN + "storage-complex-values.html");
+
+ gUI.tree.expandAll();
+
+ showColumn("hostOnly", true);
+
+ const c1id = getCookieId("c1", "test1.example.org", "/browser");
+ await selectTableItem(c1id);
+ checkCell(c1id, "hostOnly", "true");
+
+ const c2id = getCookieId("cs2", ".example.org", "/");
+ await selectTableItem(c2id);
+ checkCell(c2id, "hostOnly", "false");
+});
diff --git a/devtools/client/storage/test/browser_storage_cookies_navigation.js b/devtools/client/storage/test/browser_storage_cookies_navigation.js
new file mode 100644
index 0000000000..3dc1406451
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_cookies_navigation.js
@@ -0,0 +1,139 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function () {
+ // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default"
+ await SpecialPowers.pushPrefEnv({
+ set: [["network.cookie.sameSite.laxByDefault", false]],
+ });
+
+ const URL1 = buildURLWithContent(
+ "example.com",
+ `<h1>example.com</h1>` + `<script>document.cookie = "lorem=ipsum";</script>`
+ );
+ const URL2 = buildURLWithContent(
+ "example.net",
+ `<h1>example.net</h1>` +
+ `<iframe></iframe>` +
+ `<script>document.cookie = "foo=bar";</script>`
+ );
+ const URL_IFRAME = buildURLWithContent(
+ "example.org",
+ `<h1>example.org</h1>` + `<script>document.cookie = "hello=world";</script>`
+ );
+
+ // open tab
+ await openTabAndSetupStorage(URL1);
+ const doc = gPanelWindow.document;
+
+ // Check first domain
+ // check that both host appear in the storage tree
+ checkTree(doc, ["cookies", "https://example.com"]);
+ // check the table for values
+ await selectTreeItem(["cookies", "https://example.com"]);
+ checkCookieData("lorem", "ipsum");
+
+ // NOTE: No need to clean up cookies since Services.cookies.removeAll() from
+ // the registered clean up function will remove all of them.
+
+ // Check second domain
+ await navigateTo(URL2);
+ // wait for storage tree refresh, and check host
+ info("Waiting for storage tree to refresh and show correct host…");
+ await waitUntil(
+ () =>
+ isInTree(doc, ["cookies", "https://example.net"]) &&
+ !isInTree(doc, ["cookies", "https://example.com"])
+ );
+
+ ok(
+ !isInTree(doc, ["cookies", "https://example.com"]),
+ "example.com item is not in the tree anymore"
+ );
+
+ // check the table for values
+ // NOTE: there's an issue with the TreeWidget in which `selectedItem` is set
+ // but we have nothing selected in the UI. See Bug 1712706.
+ // Here we are forcing selecting a different item first.
+ await selectTreeItem(["cookies"]);
+ await selectTreeItem(["cookies", "https://example.net"]);
+ info("Waiting for table data to update and show correct values");
+ await waitUntil(() => hasCookieData("foo", "bar"));
+
+ // reload the current page, and check again
+ await reloadBrowser();
+ // wait for storage tree refresh, and check host
+ info("Waiting for storage tree to refresh and show correct host…");
+ await waitUntil(() => isInTree(doc, ["cookies", "https://example.net"]));
+ // check the table for values
+ // NOTE: there's an issue with the TreeWidget in which `selectedItem` is set
+ // but we have nothing selected in the UI. See Bug 1712706.
+ // Here we are forcing selecting a different item first.
+ await selectTreeItem(["cookies"]);
+ await selectTreeItem(["cookies", "https://example.net"]);
+ info("Waiting for table data to update and show correct values");
+ await waitUntil(() => hasCookieData("foo", "bar"));
+
+ // make the iframe navigate to a different domain
+ const onStorageTreeUpdated = gUI.once("store-objects-edit");
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [URL_IFRAME],
+ async function (url) {
+ const iframe = content.document.querySelector("iframe");
+ const onIframeLoaded = new Promise(loaded =>
+ iframe.addEventListener("load", loaded, { once: true })
+ );
+ iframe.src = url;
+ await onIframeLoaded;
+ }
+ );
+ info("Waiting for storage tree to update");
+ await onStorageTreeUpdated;
+
+ info("Waiting for storage tree to refresh and show correct host…");
+ await waitUntil(() => isInTree(doc, ["cookies", "https://example.org"]));
+ info("Checking cookie data");
+ await selectTreeItem(["cookies", "https://example.org"]);
+ checkCookieData("hello", "world");
+
+ info(
+ "Navigate to the first URL to check that the multiple hosts in the current document are all removed"
+ );
+ await navigateTo(URL1);
+ ok(true, "navigated");
+ await waitUntil(() => isInTree(doc, ["cookies", "https://example.com"]));
+ ok(
+ !isInTree(doc, ["cookies", "https://example.net"]),
+ "host of previous document (example.net) is not in the tree anymore"
+ );
+ ok(
+ !isInTree(doc, ["cookies", "https://example.org"]),
+ "host of iframe in previous document (example.org) is not in the tree anymore"
+ );
+
+ info("Navigate backward to test bfcache navigation");
+ gBrowser.goBack();
+ await waitUntil(
+ () =>
+ isInTree(doc, ["cookies", "https://example.net"]) &&
+ isInTree(doc, ["cookies", "https://example.org"])
+ );
+
+ ok(
+ !isInTree(doc, ["cookies", "https://example.com"]),
+ "host of previous document (example.com) is not in the tree anymore"
+ );
+
+ info("Check that the Cookies node still has the expected label");
+ is(
+ getTreeNodeLabel(doc, ["cookies"]),
+ "Cookies",
+ "Cookies item is properly displayed"
+ );
+
+ SpecialPowers.clearUserPref("network.cookie.sameSite.laxByDefault");
+});
diff --git a/devtools/client/storage/test/browser_storage_cookies_samesite.js b/devtools/client/storage/test/browser_storage_cookies_samesite.js
new file mode 100644
index 0000000000..dc58aaa9f6
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_cookies_samesite.js
@@ -0,0 +1,42 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Test that the samesite cookie attribute is displayed correctly.
+
+add_task(async function () {
+ await openTabAndSetupStorage(MAIN_DOMAIN + "storage-cookies-samesite.html");
+
+ const id1 = getCookieId(
+ "test1",
+ "test1.example.org",
+ "/browser/devtools/client/storage/test"
+ );
+ const id2 = getCookieId(
+ "test2",
+ "test1.example.org",
+ "/browser/devtools/client/storage/test"
+ );
+ const id3 = getCookieId(
+ "test3",
+ "test1.example.org",
+ "/browser/devtools/client/storage/test"
+ );
+
+ await checkState([
+ [
+ ["cookies", "http://test1.example.org"],
+ [id1, id2, id3],
+ ],
+ ]);
+
+ const sameSite1 = getRowValues(id1).sameSite;
+ const sameSite2 = getRowValues(id2).sameSite;
+ const sameSite3 = getRowValues(id3).sameSite;
+
+ is(sameSite1, "None", `sameSite1 is "None"`);
+ is(sameSite2, "Lax", `sameSite2 is "Lax"`);
+ is(sameSite3, "Strict", `sameSite3 is "Strict"`);
+});
diff --git a/devtools/client/storage/test/browser_storage_cookies_sort.js b/devtools/client/storage/test/browser_storage_cookies_sort.js
new file mode 100644
index 0000000000..2b4316af53
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_cookies_sort.js
@@ -0,0 +1,64 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Test column sorting works and sorts dates correctly (including "session"
+// cookies).
+
+"use strict";
+
+add_task(async function () {
+ const TEST_URL = MAIN_DOMAIN + "storage-cookies-sort.html";
+ await openTabAndSetupStorage(TEST_URL);
+ showAllColumns(true);
+
+ info("Sort on the expires column, ascending order");
+ clickColumnHeader("expires");
+
+ // Note that here we only specify `test_session` for `test_session1` and
+ // `test_session2`. Since we sort on the "expires" column, there is no point
+ // in asserting the order between those 2 items.
+ checkCells([
+ "test_session",
+ "test_session",
+ "test_hour",
+ "test_day",
+ "test_year",
+ ]);
+
+ info("Sort on the expires column, descending order");
+ clickColumnHeader("expires");
+
+ // Again, only assert `test_session` for `test_session1` and `test_session2`.
+ checkCells([
+ "test_year",
+ "test_day",
+ "test_hour",
+ "test_session",
+ "test_session",
+ ]);
+
+ info("Sort on the name column, ascending order");
+ clickColumnHeader("name");
+ checkCells([
+ "test_day",
+ "test_hour",
+ "test_session1",
+ "test_session2",
+ "test_year",
+ ]);
+});
+
+function checkCells(expected) {
+ const cells = [
+ ...gPanelWindow.document.querySelectorAll("#name .table-widget-cell"),
+ ];
+ cells.forEach(function (cell, i, arr) {
+ // We use startsWith in order to avoid asserting the relative order of
+ // "session" cookies when sorting on the "expires" column.
+ ok(
+ cell.value.startsWith(expected[i]),
+ `Cell value starts with "${expected[i]}".`
+ );
+ });
+}
diff --git a/devtools/client/storage/test/browser_storage_cookies_tab_navigation.js b/devtools/client/storage/test/browser_storage_cookies_tab_navigation.js
new file mode 100644
index 0000000000..b7f1aaf159
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_cookies_tab_navigation.js
@@ -0,0 +1,25 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Basic test to check cookie table tab navigation.
+
+"use strict";
+
+add_task(async function () {
+ await openTabAndSetupStorage(MAIN_DOMAIN + "storage-cookies.html");
+ showAllColumns(true);
+
+ const id = getCookieId("test1", ".test1.example.org", "/browser");
+ await startCellEdit(id, "name");
+
+ PressKeyXTimes("VK_TAB", 15);
+ is(getCurrentEditorValue(), "value3", "We have tabbed to the correct cell.");
+
+ PressKeyXTimes("VK_TAB", 15, { shiftKey: true });
+ is(
+ getCurrentEditorValue(),
+ "test1",
+ "We have shift-tabbed to the correct cell."
+ );
+});
diff --git a/devtools/client/storage/test/browser_storage_delete.js b/devtools/client/storage/test/browser_storage_delete.js
new file mode 100644
index 0000000000..96af0ca15d
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_delete.js
@@ -0,0 +1,79 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Test deleting storage items
+
+const TEST_CASES = [
+ [["localStorage", "http://test1.example.org"], "ls1", "name"],
+ [["sessionStorage", "http://test1.example.org"], "ss1", "name"],
+ [
+ ["cookies", "http://test1.example.org"],
+ getCookieId("c1", "test1.example.org", "/browser"),
+ "name",
+ ],
+ [
+ ["indexedDB", "http://test1.example.org", "idb1 (default)", "obj1"],
+ 1,
+ "name",
+ ],
+ [
+ ["Cache", "http://test1.example.org", "plop"],
+ MAIN_DOMAIN + "404_cached_file.js",
+ "url",
+ ],
+];
+
+add_task(async function () {
+ // storage-listings.html explicitly mixes secure and insecure frames.
+ // We should not enforce https for tests using this page.
+ await pushPref("dom.security.https_first", false);
+
+ await openTabAndSetupStorage(MAIN_DOMAIN + "storage-listings.html");
+
+ const contextMenu = gPanelWindow.document.getElementById(
+ "storage-table-popup"
+ );
+ const menuDeleteItem = contextMenu.querySelector(
+ "#storage-table-popup-delete"
+ );
+
+ for (const [treeItem, rowName, cellToClick] of TEST_CASES) {
+ const treeItemName = treeItem.join(" > ");
+
+ info(`Selecting tree item ${treeItemName}`);
+ await selectTreeItem(treeItem);
+
+ const row = getRowCells(rowName);
+ ok(
+ gUI.table.items.has(rowName),
+ `There is a row '${rowName}' in ${treeItemName}`
+ );
+
+ const eventWait = gUI.once("store-objects-edit");
+
+ await waitForContextMenu(contextMenu, row[cellToClick], () => {
+ info(`Opened context menu in ${treeItemName}, row '${rowName}'`);
+ contextMenu.activateItem(menuDeleteItem);
+ const truncatedRowName = String(rowName)
+ .replace(SEPARATOR_GUID, "-")
+ .substr(0, 16);
+ ok(
+ JSON.parse(
+ menuDeleteItem.getAttribute("data-l10n-args")
+ ).itemName.includes(truncatedRowName),
+ `Context menu item label contains '${rowName}' (maybe truncated)`
+ );
+ });
+
+ info("Awaiting for store-objects-edit event");
+ await eventWait;
+
+ ok(
+ !gUI.table.items.has(rowName),
+ `There is no row '${rowName}' in ${treeItemName} after deletion`
+ );
+ }
+});
diff --git a/devtools/client/storage/test/browser_storage_delete_all.js b/devtools/client/storage/test/browser_storage_delete_all.js
new file mode 100644
index 0000000000..4585821cee
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_delete_all.js
@@ -0,0 +1,115 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Test deleting all storage items
+
+add_task(async function () {
+ // storage-listings.html explicitly mixes secure and insecure frames.
+ // We should not enforce https for tests using this page.
+ await pushPref("dom.security.https_first", false);
+
+ await openTabAndSetupStorage(MAIN_DOMAIN + "storage-listings.html");
+
+ const contextMenu = gPanelWindow.document.getElementById(
+ "storage-table-popup"
+ );
+ const menuDeleteAllItem = contextMenu.querySelector(
+ "#storage-table-popup-delete-all"
+ );
+
+ info("test state before delete");
+ const beforeState = [
+ [
+ ["localStorage", "http://test1.example.org"],
+ ["key", "ls1", "ls2"],
+ ],
+ [["localStorage", "http://sectest1.example.org"], ["iframe-u-ls1"]],
+ [["localStorage", "https://sectest1.example.org"], ["iframe-s-ls1"]],
+ [
+ ["sessionStorage", "http://test1.example.org"],
+ ["key", "ss1"],
+ ],
+ [
+ ["sessionStorage", "http://sectest1.example.org"],
+ ["iframe-u-ss1", "iframe-u-ss2"],
+ ],
+ [["sessionStorage", "https://sectest1.example.org"], ["iframe-s-ss1"]],
+ [
+ ["indexedDB", "http://test1.example.org", "idb1 (default)", "obj1"],
+ [1, 2, 3],
+ ],
+ [
+ ["Cache", "http://test1.example.org", "plop"],
+ [
+ MAIN_DOMAIN + "404_cached_file.js",
+ MAIN_DOMAIN + "browser_storage_basic.js",
+ ],
+ ],
+ ];
+
+ await checkState(beforeState);
+
+ info("do the delete");
+ const deleteHosts = [
+ [["localStorage", "https://sectest1.example.org"], "iframe-s-ls1", "name"],
+ [
+ ["sessionStorage", "https://sectest1.example.org"],
+ "iframe-s-ss1",
+ "name",
+ ],
+ [
+ ["indexedDB", "http://test1.example.org", "idb1 (default)", "obj1"],
+ 1,
+ "name",
+ ],
+ [
+ ["Cache", "http://test1.example.org", "plop"],
+ MAIN_DOMAIN + "404_cached_file.js",
+ "url",
+ ],
+ ];
+
+ for (const [store, rowName, cellToClick] of deleteHosts) {
+ const storeName = store.join(" > ");
+
+ await selectTreeItem(store);
+
+ const eventWait = gUI.once("store-objects-cleared");
+
+ const cell = getRowCells(rowName)[cellToClick];
+ await waitForContextMenu(contextMenu, cell, () => {
+ info(`Opened context menu in ${storeName}, row '${rowName}'`);
+ contextMenu.activateItem(menuDeleteAllItem);
+ });
+
+ await eventWait;
+ }
+
+ info("test state after delete");
+ const afterState = [
+ // iframes from the same host, one secure, one unsecure, are independent
+ // from each other. Delete all in one doesn't touch the other one.
+ [
+ ["localStorage", "http://test1.example.org"],
+ ["key", "ls1", "ls2"],
+ ],
+ [["localStorage", "http://sectest1.example.org"], ["iframe-u-ls1"]],
+ [["localStorage", "https://sectest1.example.org"], []],
+ [
+ ["sessionStorage", "http://test1.example.org"],
+ ["key", "ss1"],
+ ],
+ [
+ ["sessionStorage", "http://sectest1.example.org"],
+ ["iframe-u-ss1", "iframe-u-ss2"],
+ ],
+ [["sessionStorage", "https://sectest1.example.org"], []],
+ [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj1"], []],
+ [["Cache", "http://test1.example.org", "plop"], []],
+ ];
+
+ await checkState(afterState);
+});
diff --git a/devtools/client/storage/test/browser_storage_delete_tree.js b/devtools/client/storage/test/browser_storage_delete_tree.js
new file mode 100644
index 0000000000..047536acd7
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_delete_tree.js
@@ -0,0 +1,93 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Test deleting all storage items from the tree.
+
+add_task(async function () {
+ // storage-listings.html explicitly mixes secure and insecure frames.
+ // We should not enforce https for tests using this page.
+ await pushPref("dom.security.https_first", false);
+
+ await openTabAndSetupStorage(MAIN_DOMAIN + "storage-listings.html");
+
+ const contextMenu =
+ gPanelWindow.document.getElementById("storage-tree-popup");
+ const menuDeleteAllItem = contextMenu.querySelector(
+ "#storage-tree-popup-delete-all"
+ );
+
+ info("test state before delete");
+ await checkState([
+ [
+ ["cookies", "http://test1.example.org"],
+ [
+ getCookieId("c1", "test1.example.org", "/browser"),
+ getCookieId("cs2", ".example.org", "/"),
+ getCookieId("c3", "test1.example.org", "/"),
+ getCookieId("c4", ".example.org", "/"),
+ getCookieId("uc1", ".example.org", "/"),
+ getCookieId("uc2", ".example.org", "/"),
+ ],
+ ],
+ [
+ ["localStorage", "http://test1.example.org"],
+ ["key", "ls1", "ls2"],
+ ],
+ [
+ ["sessionStorage", "http://test1.example.org"],
+ ["key", "ss1"],
+ ],
+ [
+ ["indexedDB", "http://test1.example.org", "idb1 (default)", "obj1"],
+ [1, 2, 3],
+ ],
+ [
+ ["Cache", "http://test1.example.org", "plop"],
+ [
+ MAIN_DOMAIN + "404_cached_file.js",
+ MAIN_DOMAIN + "browser_storage_basic.js",
+ ],
+ ],
+ ]);
+
+ info("do the delete");
+ const deleteHosts = [
+ ["cookies", "http://test1.example.org"],
+ ["localStorage", "http://test1.example.org"],
+ ["sessionStorage", "http://test1.example.org"],
+ ["indexedDB", "http://test1.example.org", "idb1 (default)", "obj1"],
+ ["Cache", "http://test1.example.org", "plop"],
+ ];
+
+ for (const store of deleteHosts) {
+ const storeName = store.join(" > ");
+
+ await selectTreeItem(store);
+
+ const eventName =
+ "store-objects-" + (store[0] == "cookies" ? "edit" : "cleared");
+ const eventWait = gUI.once(eventName);
+
+ const selector = `[data-id='${JSON.stringify(store)}'] > .tree-widget-item`;
+ const target = gPanelWindow.document.querySelector(selector);
+ ok(target, `tree item found in ${storeName}`);
+ await waitForContextMenu(contextMenu, target, () => {
+ info(`Opened tree context menu in ${storeName}`);
+ contextMenu.activateItem(menuDeleteAllItem);
+ });
+
+ await eventWait;
+ }
+
+ info("test state after delete");
+ await checkState([
+ [["cookies", "http://test1.example.org"], []],
+ [["localStorage", "http://test1.example.org"], []],
+ [["sessionStorage", "http://test1.example.org"], []],
+ [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj1"], []],
+ [["Cache", "http://test1.example.org", "plop"], []],
+ ]);
+});
diff --git a/devtools/client/storage/test/browser_storage_delete_usercontextid.js b/devtools/client/storage/test/browser_storage_delete_usercontextid.js
new file mode 100644
index 0000000000..5e89028f9d
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_delete_usercontextid.js
@@ -0,0 +1,238 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Test deleting storage items with userContextId.
+
+// The items that will be deleted.
+const TEST_CASES = [
+ [["localStorage", "http://test1.example.org"], "ls1", "name"],
+ [["sessionStorage", "http://test1.example.org"], "ss1", "name"],
+ [
+ ["cookies", "http://test1.example.org"],
+ getCookieId("c1", "test1.example.org", "/browser"),
+ "name",
+ ],
+ [
+ ["indexedDB", "http://test1.example.org", "idb1 (default)", "obj1"],
+ 1,
+ "name",
+ ],
+ [
+ ["Cache", "http://test1.example.org", "plop"],
+ MAIN_DOMAIN + "404_cached_file.js",
+ "url",
+ ],
+];
+
+// The storage items that should exist for default userContextId
+const storageItemsForDefault = [
+ [
+ ["cookies", "http://test1.example.org"],
+ [
+ getCookieId("c1", "test1.example.org", "/browser"),
+ getCookieId("cs2", ".example.org", "/"),
+ getCookieId("c3", "test1.example.org", "/"),
+ getCookieId("c4", ".example.org", "/"),
+ getCookieId("uc1", ".example.org", "/"),
+ getCookieId("uc2", ".example.org", "/"),
+ ],
+ ],
+ [
+ ["cookies", "https://sectest1.example.org"],
+ [
+ getCookieId("uc1", ".example.org", "/"),
+ getCookieId("uc2", ".example.org", "/"),
+ getCookieId("cs2", ".example.org", "/"),
+ getCookieId("c4", ".example.org", "/"),
+ getCookieId(
+ "sc1",
+ "sectest1.example.org",
+ "/browser/devtools/client/storage/test"
+ ),
+ getCookieId(
+ "sc2",
+ "sectest1.example.org",
+ "/browser/devtools/client/storage/test"
+ ),
+ ],
+ ],
+ [
+ ["localStorage", "http://test1.example.org"],
+ ["key", "ls1", "ls2"],
+ ],
+ [["localStorage", "http://sectest1.example.org"], ["iframe-u-ls1"]],
+ [["localStorage", "https://sectest1.example.org"], ["iframe-s-ls1"]],
+ [
+ ["sessionStorage", "http://test1.example.org"],
+ ["key", "ss1"],
+ ],
+ [
+ ["sessionStorage", "http://sectest1.example.org"],
+ ["iframe-u-ss1", "iframe-u-ss2"],
+ ],
+ [["sessionStorage", "https://sectest1.example.org"], ["iframe-s-ss1"]],
+ [
+ ["indexedDB", "http://test1.example.org"],
+ ["idb1 (default)", "idb2 (default)"],
+ ],
+ [
+ ["indexedDB", "http://test1.example.org", "idb1 (default)"],
+ ["obj1", "obj2"],
+ ],
+ [["indexedDB", "http://test1.example.org", "idb2 (default)"], ["obj3"]],
+ [
+ ["indexedDB", "http://test1.example.org", "idb1 (default)", "obj1"],
+ [1, 2, 3],
+ ],
+ [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj2"], [1]],
+ [["indexedDB", "http://test1.example.org", "idb2 (default)", "obj3"], []],
+ [["indexedDB", "http://sectest1.example.org"], []],
+ [
+ ["indexedDB", "https://sectest1.example.org"],
+ ["idb-s1 (default)", "idb-s2 (default)"],
+ ],
+ [
+ ["indexedDB", "https://sectest1.example.org", "idb-s1 (default)"],
+ ["obj-s1"],
+ ],
+ [
+ ["indexedDB", "https://sectest1.example.org", "idb-s2 (default)"],
+ ["obj-s2"],
+ ],
+ [
+ ["indexedDB", "https://sectest1.example.org", "idb-s1 (default)", "obj-s1"],
+ [6, 7],
+ ],
+ [
+ ["indexedDB", "https://sectest1.example.org", "idb-s2 (default)", "obj-s2"],
+ [16],
+ ],
+ [
+ ["Cache", "http://test1.example.org", "plop"],
+ [
+ MAIN_DOMAIN + "404_cached_file.js",
+ MAIN_DOMAIN + "browser_storage_basic.js",
+ ],
+ ],
+];
+
+/**
+ * Test that the desired number of tree items are present
+ */
+function testTree(tests) {
+ const doc = gPanelWindow.document;
+ for (const [item] of tests) {
+ ok(
+ doc.querySelector("[data-id='" + JSON.stringify(item) + "']"),
+ `Tree item ${item.toSource()} should be present in the storage tree`
+ );
+ }
+}
+
+/**
+ * Test that correct table entries are shown for each of the tree item
+ */
+async function testTables(tests) {
+ const doc = gPanelWindow.document;
+ // Expand all nodes so that the synthesized click event actually works
+ gUI.tree.expandAll();
+
+ // First tree item is already selected so no clicking and waiting for update
+ for (const id of tests[0][1]) {
+ ok(
+ doc.querySelector(".table-widget-cell[data-id='" + id + "']"),
+ "Table item " + id + " should be present"
+ );
+ }
+
+ // Click rest of the tree items and wait for the table to be updated
+ for (const [treeItem, items] of tests.slice(1)) {
+ await selectTreeItem(treeItem);
+
+ // Check whether correct number of items are present in the table
+ is(
+ doc.querySelectorAll(
+ ".table-widget-column:first-of-type .table-widget-cell"
+ ).length,
+ items.length,
+ "Number of items in table is correct"
+ );
+
+ // Check if all the desired items are present in the table
+ for (const id of items) {
+ ok(
+ doc.querySelector(".table-widget-cell[data-id='" + id + "']"),
+ "Table item " + id + " should be present"
+ );
+ }
+ }
+}
+
+add_task(async function () {
+ // storage-listings.html explicitly mixes secure and insecure frames.
+ // We should not enforce https for tests using this page.
+ await pushPref("dom.security.https_first", false);
+
+ // First, open a tab with the default userContextId and setup its storages.
+ const tabDefault = await openTab(MAIN_DOMAIN + "storage-listings.html");
+
+ // Second, start testing for userContextId 1.
+ // We use the same item name as the default page has to see deleting items
+ // from userContextId 1 will affect default one or not.
+ await openTabAndSetupStorage(MAIN_DOMAIN + "storage-listings.html", {
+ userContextId: 1,
+ });
+
+ const contextMenu = gPanelWindow.document.getElementById(
+ "storage-table-popup"
+ );
+ const menuDeleteItem = contextMenu.querySelector(
+ "#storage-table-popup-delete"
+ );
+
+ for (const [treeItem, rowName, cellToClick] of TEST_CASES) {
+ const treeItemName = treeItem.join(" > ");
+
+ info(`Selecting tree item ${treeItemName}`);
+ await selectTreeItem(treeItem);
+
+ const row = getRowCells(rowName);
+ ok(
+ gUI.table.items.has(rowName),
+ `There is a row '${rowName}' in ${treeItemName}`
+ );
+
+ const eventWait = gUI.once("store-objects-edit");
+
+ await waitForContextMenu(contextMenu, row[cellToClick], () => {
+ info(`Opened context menu in ${treeItemName}, row '${rowName}'`);
+ contextMenu.activateItem(menuDeleteItem);
+ const truncatedRowName = String(rowName)
+ .replace(SEPARATOR_GUID, "-")
+ .substr(0, 16);
+ ok(
+ JSON.parse(
+ menuDeleteItem.getAttribute("data-l10n-args")
+ ).itemName.includes(truncatedRowName),
+ `Context menu item label contains '${rowName}' (maybe truncated)`
+ );
+ });
+
+ await eventWait;
+
+ ok(
+ !gUI.table.items.has(rowName),
+ `There is no row '${rowName}' in ${treeItemName} after deletion`
+ );
+ }
+
+ // Final, we see that the default tab is intact or not.
+ await BrowserTestUtils.switchTab(gBrowser, tabDefault);
+ await openStoragePanel();
+
+ testTree(storageItemsForDefault);
+ await testTables(storageItemsForDefault);
+});
diff --git a/devtools/client/storage/test/browser_storage_dfpi.js b/devtools/client/storage/test/browser_storage_dfpi.js
new file mode 100644
index 0000000000..14d625910e
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_dfpi.js
@@ -0,0 +1,164 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Basic test to assert that the storage tree and table corresponding to each
+// item in the storage tree is correctly displayed
+
+"use strict";
+
+const { SiteDataTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/SiteDataTestUtils.sys.mjs"
+);
+
+// Ensure iframe.src in storage-dfpi.html starts with PREFIX.
+const PREFIX = "https://sub1.test1.example";
+const ORIGIN = `${PREFIX}.org`;
+const ORIGIN_THIRD_PARTY = `${PREFIX}.com`;
+const TEST_URL = `${ORIGIN}/${PATH}storage-dfpi.html`;
+
+function listOrigins() {
+ return new Promise(resolve => {
+ SpecialPowers.Services.qms.listOrigins().callback = req => {
+ resolve(req.result);
+ };
+ });
+}
+
+add_task(async function () {
+ await pushPref(
+ "network.cookie.cookieBehavior",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN
+ );
+
+ await pushPref(
+ "privacy.partition.always_partition_third_party_non_cookie_storage",
+ false
+ );
+
+ registerCleanupFunction(SiteDataTestUtils.clear);
+
+ // `Services.qms.listOrigins()` may or contain results created by other tests.
+ // And it's unsafe to clear existing origins by `Services.qms.clear()`.
+ // In order to obtain correct results, we need to compare the results before
+ // and after `openTabAndSetupStorage` is called.
+ // To ensure more accurate results, try choosing a uncommon origin for PREFIX.
+ const EXISTING_ORIGINS = await listOrigins();
+ ok(!EXISTING_ORIGINS.includes(ORIGIN), `${ORIGIN} doesn't exist`);
+
+ await openTabAndSetupStorage(TEST_URL);
+
+ const origins = await listOrigins();
+ for (const origin of origins) {
+ ok(
+ EXISTING_ORIGINS.includes(origin) || origin === ORIGIN,
+ `check origin: ${origin}`
+ );
+ }
+ ok(origins.includes(ORIGIN), `${ORIGIN} is added`);
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+async function setPartitionedStorage(browser, type, key) {
+ const handler = async (storageType, storageKey, storageValue) => {
+ if (storageType == "cookie") {
+ content.document.cookie = `${storageKey}=${storageValue}`;
+ return;
+ }
+ content.localStorage.setItem(storageKey, storageValue);
+ };
+
+ // Set first party storage.
+ await SpecialPowers.spawn(browser, [type, key, "first"], handler);
+ // Set third-party (partitioned) storage in the iframe.
+ await SpecialPowers.spawn(
+ browser.browsingContext.children[0],
+ [type, key, "third"],
+ handler
+ );
+}
+
+async function checkData(storageType, key, value) {
+ if (storageType == "cookie") {
+ checkCookieData(key, value);
+ return;
+ }
+ await waitForStorageData(key, value);
+}
+
+async function testPartitionedStorage(
+ storageType,
+ treeItemLabel = storageType
+) {
+ await pushPref(
+ "network.cookie.cookieBehavior",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN
+ );
+ // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default"
+ await pushPref("network.cookie.sameSite.laxByDefault", false);
+
+ info(
+ "Open the test url in a new tab and add storage entries *before* opening the storage panel."
+ );
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ await setPartitionedStorage(browser, storageType, "contextA");
+ });
+
+ await openTabAndSetupStorage(TEST_URL);
+
+ const doc = gPanelWindow.document;
+
+ info("check that both hosts appear in the storage tree");
+ checkTree(doc, [treeItemLabel, ORIGIN]);
+ checkTree(doc, [treeItemLabel, ORIGIN_THIRD_PARTY]);
+
+ info(
+ "check that items for both first and third party host have the initial storage entries"
+ );
+
+ await selectTreeItem([treeItemLabel, ORIGIN]);
+ await checkData(storageType, "contextA", "first");
+
+ await selectTreeItem([treeItemLabel, ORIGIN_THIRD_PARTY]);
+ await checkData(storageType, "contextA", "third");
+
+ info("Add more entries while the storage panel is open");
+ const onUpdated = gUI.once("store-objects-edit");
+ await setPartitionedStorage(
+ gBrowser.selectedBrowser,
+ storageType,
+ "contextB"
+ );
+ await onUpdated;
+
+ info("check that both hosts appear in the storage tree");
+ checkTree(doc, [treeItemLabel, ORIGIN]);
+ checkTree(doc, [treeItemLabel, ORIGIN_THIRD_PARTY]);
+
+ info(
+ "check that items for both first and third party host have the updated storage entries"
+ );
+
+ await selectTreeItem([treeItemLabel, ORIGIN]);
+ await checkData(storageType, "contextA", "first");
+ await checkData(storageType, "contextB", "first");
+
+ await selectTreeItem([treeItemLabel, ORIGIN_THIRD_PARTY]);
+ await checkData(storageType, "contextA", "third");
+ await checkData(storageType, "contextB", "third");
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+}
+
+// Tests that partitioned storage is shown in the storage panel.
+
+add_task(async function test_partitioned_cookies() {
+ registerCleanupFunction(SiteDataTestUtils.clear);
+ await testPartitionedStorage("cookie", "cookies");
+});
+
+add_task(async function test_partitioned_localStorage() {
+ registerCleanupFunction(SiteDataTestUtils.clear);
+ await testPartitionedStorage("localStorage");
+});
diff --git a/devtools/client/storage/test/browser_storage_dfpi_always_partition_storage.js b/devtools/client/storage/test/browser_storage_dfpi_always_partition_storage.js
new file mode 100644
index 0000000000..e2615fb951
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_dfpi_always_partition_storage.js
@@ -0,0 +1,70 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Basic test to assert that the storage tree and table corresponding to each
+// item in the storage tree is correctly displayed, bearing in mind the origin
+// is partitioned when always_partition_third_party_non_cookie_storage is true.
+
+"use strict";
+
+const { SiteDataTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/SiteDataTestUtils.sys.mjs"
+);
+
+// Ensure iframe.src in storage-dfpi.html starts with PREFIX.
+const PREFIX = "https://sub1.test1.example";
+const ORIGIN = `${PREFIX}.org`;
+const ORIGIN_PARTITIONED = `${PREFIX}.com^partitionKey=%28https%2Cexample.org%29`;
+const TEST_URL = `${ORIGIN}/document-builder.sjs?html=
+ <iframe src="${PREFIX}.com/browser/devtools/client/storage/test/storage-blank.html"></iframe>
+`;
+
+function listOrigins() {
+ return new Promise(resolve => {
+ SpecialPowers.Services.qms.listOrigins().callback = req => {
+ resolve(req.result);
+ };
+ });
+}
+
+add_task(async function () {
+ await pushPref(
+ "network.cookie.cookieBehavior",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN
+ );
+
+ await pushPref(
+ "privacy.partition.always_partition_third_party_non_cookie_storage",
+ true
+ );
+
+ registerCleanupFunction(SiteDataTestUtils.clear);
+
+ const expectedOrigins = [ORIGIN, ORIGIN_PARTITIONED];
+
+ // `Services.qms.listOrigins()` may or contain results created by other tests.
+ // And it's unsafe to clear existing origins by `Services.qms.clear()`.
+ // In order to obtain correct results, we need to compare the results before
+ // and after `openTabAndSetupStorage` is called.
+ // To ensure more accurate results, try choosing a uncommon origin for PREFIX.
+ const EXISTING_ORIGINS = await listOrigins();
+ expectedOrigins.forEach(expected => {
+ ok(!EXISTING_ORIGINS.includes(expected), `${expected} doesn't exist`);
+ });
+
+ await openTabAndSetupStorage(TEST_URL);
+
+ const origins = await listOrigins();
+ for (const origin of origins) {
+ ok(
+ EXISTING_ORIGINS.includes(origin) || expectedOrigins.includes(origin),
+ `check origin: ${origin}`
+ );
+ }
+ expectedOrigins.forEach(expected => {
+ ok(origins.includes(expected), `${expected} is added`);
+ });
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/devtools/client/storage/test/browser_storage_dynamic_updates_cookies.js b/devtools/client/storage/test/browser_storage_dynamic_updates_cookies.js
new file mode 100644
index 0000000000..77c8047fc3
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_dynamic_updates_cookies.js
@@ -0,0 +1,239 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
+
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
+});
+
+// Test dynamic updates in the storage inspector for cookies.
+
+add_task(async function () {
+ await openTabAndSetupStorage(MAIN_DOMAIN_SECURED + "storage-updates.html");
+
+ gUI.tree.expandAll();
+
+ ok(gUI.sidebar.hidden, "Sidebar is initially hidden");
+ const c1id = getCookieId("c1", "test1.example.org", "/browser");
+ await selectTableItem(c1id);
+
+ // test that value is something initially
+ const initialValue = [
+ [
+ { name: "c1", value: "1.2.3.4.5.6.7" },
+ { name: "c1.Path", value: "/browser" },
+ ],
+ [
+ { name: "c1", value: "Array" },
+ { name: "c1.0", value: "1" },
+ { name: "c1.6", value: "7" },
+ ],
+ ];
+
+ // test that value is something initially
+ const finalValue = [
+ [
+ { name: "c1", value: '{"foo": 4,"bar":6}' },
+ { name: "c1.Path", value: "/browser" },
+ ],
+ [
+ { name: "c1", value: "Object" },
+ { name: "c1.foo", value: "4" },
+ { name: "c1.bar", value: "6" },
+ ],
+ ];
+
+ // Check that sidebar shows correct initial value
+ await findVariableViewProperties(initialValue[0], false);
+
+ await findVariableViewProperties(initialValue[1], true);
+
+ // Check if table shows correct initial value
+ await checkState([
+ [
+ ["cookies", "https://test1.example.org"],
+ [
+ getCookieId("c1", "test1.example.org", "/browser"),
+ getCookieId("c2", "test1.example.org", "/browser"),
+ ],
+ ],
+ ]);
+ checkCell(c1id, "value", "1.2.3.4.5.6.7");
+
+ await addCookie("c1", '{"foo": 4,"bar":6}', "/browser");
+ await gUI.once("store-objects-edit");
+
+ await findVariableViewProperties(finalValue[0], false);
+ await findVariableViewProperties(finalValue[1], true);
+
+ await checkState([
+ [
+ ["cookies", "https://test1.example.org"],
+ [
+ getCookieId("c1", "test1.example.org", "/browser"),
+ getCookieId("c2", "test1.example.org", "/browser"),
+ ],
+ ],
+ ]);
+ checkCell(c1id, "value", '{"foo": 4,"bar":6}');
+
+ // Add a new entry
+ await addCookie("c3", "booyeah");
+
+ await gUI.once("store-objects-edit");
+
+ await checkState([
+ [
+ ["cookies", "https://test1.example.org"],
+ [
+ getCookieId("c1", "test1.example.org", "/browser"),
+ getCookieId("c2", "test1.example.org", "/browser"),
+ getCookieId(
+ "c3",
+ "test1.example.org",
+ "/browser/devtools/client/storage/test"
+ ),
+ ],
+ ],
+ ]);
+ const c3id = getCookieId(
+ "c3",
+ "test1.example.org",
+ "/browser/devtools/client/storage/test"
+ );
+ checkCell(c3id, "value", "booyeah");
+
+ // Add another
+ await addCookie("c4", "booyeah");
+
+ await gUI.once("store-objects-edit");
+
+ await checkState([
+ [
+ ["cookies", "https://test1.example.org"],
+ [
+ getCookieId("c1", "test1.example.org", "/browser"),
+ getCookieId("c2", "test1.example.org", "/browser"),
+ getCookieId(
+ "c3",
+ "test1.example.org",
+ "/browser/devtools/client/storage/test"
+ ),
+ getCookieId(
+ "c4",
+ "test1.example.org",
+ "/browser/devtools/client/storage/test"
+ ),
+ ],
+ ],
+ ]);
+ const c4id = getCookieId(
+ "c4",
+ "test1.example.org",
+ "/browser/devtools/client/storage/test"
+ );
+ checkCell(c4id, "value", "booyeah");
+
+ // Removing cookies
+ await removeCookie("c1", "/browser");
+
+ await gUI.once("store-objects-edit");
+
+ await checkState([
+ [
+ ["cookies", "https://test1.example.org"],
+ [
+ getCookieId("c2", "test1.example.org", "/browser"),
+ getCookieId(
+ "c3",
+ "test1.example.org",
+ "/browser/devtools/client/storage/test"
+ ),
+ getCookieId(
+ "c4",
+ "test1.example.org",
+ "/browser/devtools/client/storage/test"
+ ),
+ ],
+ ],
+ ]);
+
+ ok(!gUI.sidebar.hidden, "Sidebar still visible for next row");
+
+ // Check if next element's value is visible in sidebar
+ await findVariableViewProperties([{ name: "c2", value: "foobar" }]);
+
+ // Keep deleting till no rows
+ await removeCookie("c3");
+
+ await gUI.once("store-objects-edit");
+
+ await checkState([
+ [
+ ["cookies", "https://test1.example.org"],
+ [
+ getCookieId("c2", "test1.example.org", "/browser"),
+ getCookieId(
+ "c4",
+ "test1.example.org",
+ "/browser/devtools/client/storage/test"
+ ),
+ ],
+ ],
+ ]);
+
+ // Check if next element's value is visible in sidebar
+ await findVariableViewProperties([{ name: "c2", value: "foobar" }]);
+
+ await removeCookie("c2", "/browser");
+
+ await gUI.once("store-objects-edit");
+
+ await checkState([
+ [
+ ["cookies", "https://test1.example.org"],
+ [
+ getCookieId(
+ "c4",
+ "test1.example.org",
+ "/browser/devtools/client/storage/test"
+ ),
+ ],
+ ],
+ ]);
+
+ // Check if next element's value is visible in sidebar
+ await findVariableViewProperties([{ name: "c4", value: "booyeah" }]);
+
+ await removeCookie("c4");
+
+ await gUI.once("store-objects-edit");
+
+ await checkState([[["cookies", "https://test1.example.org"], []]]);
+
+ ok(gUI.sidebar.hidden, "Sidebar is hidden when no rows");
+});
+
+async function addCookie(name, value, path) {
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [[name, value, path]],
+ ([nam, valu, pat]) => {
+ content.wrappedJSObject.addCookie(nam, valu, pat);
+ }
+ );
+}
+
+async function removeCookie(name, path) {
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [[name, path]],
+ ([nam, pat]) => {
+ content.wrappedJSObject.removeCookie(nam, pat);
+ }
+ );
+}
diff --git a/devtools/client/storage/test/browser_storage_dynamic_updates_localStorage.js b/devtools/client/storage/test/browser_storage_dynamic_updates_localStorage.js
new file mode 100644
index 0000000000..f220d16b0c
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_dynamic_updates_localStorage.js
@@ -0,0 +1,70 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Test dynamic updates in the storage inspector for localStorage.
+
+add_task(async function () {
+ const TEST_HOST = "https://test1.example.org";
+
+ await openTabAndSetupStorage(MAIN_DOMAIN_SECURED + "storage-updates.html");
+
+ gUI.tree.expandAll();
+
+ ok(gUI.sidebar.hidden, "Sidebar is initially hidden");
+
+ const expectedKeys = ["1", "2", "3", "4", "5", "null", "non-json-parsable"];
+
+ // Test on string keys that JSON.parse can parse without throwing
+ // (to verify the issue fixed by Bug 1578447 doesn't regress).
+ await testRemoveAndChange("null", expectedKeys, TEST_HOST);
+ await testRemoveAndChange("4", expectedKeys, TEST_HOST);
+ // Test on a string that makes JSON.parse to throw.
+ await testRemoveAndChange("non-json-parsable", expectedKeys, TEST_HOST);
+
+ // Clearing items.
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ content.wrappedJSObject.clear();
+ });
+
+ await gUI.once("store-objects-cleared");
+
+ await checkState([[["localStorage", TEST_HOST], []]]);
+});
+
+async function testRemoveAndChange(targetKey, expectedKeys, host) {
+ await checkState([[["localStorage", host], expectedKeys]]);
+
+ await removeLocalStorageItem(targetKey);
+ await gUI.once("store-objects-edit");
+ await checkState([
+ [["localStorage", host], expectedKeys.filter(key => key !== targetKey)],
+ ]);
+
+ await setLocalStorageItem(targetKey, "again");
+ await gUI.once("store-objects-edit");
+ await checkState([[["localStorage", host], expectedKeys]]);
+
+ // Updating a row set to the string "null"
+ await setLocalStorageItem(targetKey, `key-${targetKey}-changed`);
+ await gUI.once("store-objects-edit");
+ checkCell(targetKey, "value", `key-${targetKey}-changed`);
+}
+
+async function setLocalStorageItem(key, value) {
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [[key, value]],
+ ([innerKey, innerValue]) => {
+ content.wrappedJSObject.localStorage.setItem(innerKey, innerValue);
+ }
+ );
+}
+
+async function removeLocalStorageItem(key) {
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [key], innerKey => {
+ content.wrappedJSObject.localStorage.removeItem(innerKey);
+ });
+}
diff --git a/devtools/client/storage/test/browser_storage_dynamic_updates_sessionStorage.js b/devtools/client/storage/test/browser_storage_dynamic_updates_sessionStorage.js
new file mode 100644
index 0000000000..a6aa6890d5
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_dynamic_updates_sessionStorage.js
@@ -0,0 +1,90 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Test dynamic updates in the storage inspector for sessionStorage.
+
+add_task(async function () {
+ await openTabAndSetupStorage(MAIN_DOMAIN_SECURED + "storage-updates.html");
+
+ gUI.tree.expandAll();
+
+ ok(gUI.sidebar.hidden, "Sidebar is initially hidden");
+
+ await checkState([
+ [
+ ["sessionStorage", "https://test1.example.org"],
+ ["ss1", "ss2", "ss3"],
+ ],
+ ]);
+
+ await setSessionStorageItem("ss4", "new-item");
+
+ await gUI.once("store-objects-edit");
+
+ await checkState([
+ [
+ ["sessionStorage", "https://test1.example.org"],
+ ["ss1", "ss2", "ss3", "ss4"],
+ ],
+ ]);
+
+ // deleting item
+
+ await removeSessionStorageItem("ss3");
+
+ await gUI.once("store-objects-edit");
+
+ await removeSessionStorageItem("ss1");
+
+ await gUI.once("store-objects-edit");
+
+ await checkState([
+ [
+ ["sessionStorage", "https://test1.example.org"],
+ ["ss2", "ss4"],
+ ],
+ ]);
+
+ await selectTableItem("ss2");
+
+ ok(!gUI.sidebar.hidden, "sidebar is visible");
+
+ // Checking for correct value in sidebar before update
+ await findVariableViewProperties([{ name: "ss2", value: "foobar" }]);
+
+ await setSessionStorageItem("ss2", "changed=ss2");
+
+ await gUI.once("sidebar-updated");
+
+ checkCell("ss2", "value", "changed=ss2");
+
+ await findVariableViewProperties([{ name: "ss2", value: "changed=ss2" }]);
+
+ // Clearing items.
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ content.wrappedJSObject.clear();
+ });
+
+ await gUI.once("store-objects-cleared");
+
+ await checkState([[["sessionStorage", "https://test1.example.org"], []]]);
+});
+
+async function setSessionStorageItem(key, value) {
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [[key, value]],
+ ([innerKey, innerValue]) => {
+ content.wrappedJSObject.sessionStorage.setItem(innerKey, innerValue);
+ }
+ );
+}
+
+async function removeSessionStorageItem(key) {
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [key], innerKey => {
+ content.wrappedJSObject.sessionStorage.removeItem(innerKey);
+ });
+}
diff --git a/devtools/client/storage/test/browser_storage_empty_objectstores.js b/devtools/client/storage/test/browser_storage_empty_objectstores.js
new file mode 100644
index 0000000000..647e1b362a
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_empty_objectstores.js
@@ -0,0 +1,90 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Basic test to assert that the storage tree and table corresponding to each
+// item in the storage tree is correctly displayed.
+
+"use strict";
+
+// Entries that should be present in the tree for this test
+// Format for each entry in the array:
+// [
+// ["path", "to", "tree", "item"],
+// - The path to the tree item to click formed by id of each item
+// ["key_value1", "key_value2", ...]
+// - The value of the first (unique) column for each row in the table
+// corresponding to the tree item selected.
+// ]
+// These entries are formed by the cookies, local storage, session storage and
+// indexedDB entries created in storage-listings.html,
+// storage-secured-iframe.html and storage-unsecured-iframe.html
+const storeItems = [
+ [
+ ["indexedDB", "https://test1.example.org"],
+ ["idb1 (default)", "idb2 (default)"],
+ ],
+ [
+ ["indexedDB", "https://test1.example.org", "idb1 (default)"],
+ ["obj1", "obj2"],
+ ],
+ [["indexedDB", "https://test1.example.org", "idb2 (default)"], []],
+ [
+ ["indexedDB", "https://test1.example.org", "idb1 (default)", "obj1"],
+ [1, 2, 3],
+ ],
+ [["indexedDB", "https://test1.example.org", "idb1 (default)", "obj2"], [1]],
+];
+
+/**
+ * Test that the desired number of tree items are present
+ */
+function testTree() {
+ const doc = gPanelWindow.document;
+ for (const [item] of storeItems) {
+ ok(
+ doc.querySelector(`[data-id='${JSON.stringify(item)}']`),
+ `Tree item ${item} should be present in the storage tree`
+ );
+ }
+}
+
+/**
+ * Test that correct table entries are shown for each of the tree item
+ */
+const testTables = async function () {
+ const doc = gPanelWindow.document;
+ // Expand all nodes so that the synthesized click event actually works
+ gUI.tree.expandAll();
+
+ // Click the tree items and wait for the table to be updated
+ for (const [item, ids] of storeItems) {
+ await selectTreeItem(item);
+
+ // Check whether correct number of items are present in the table
+ is(
+ doc.querySelectorAll(
+ ".table-widget-column:first-of-type .table-widget-cell"
+ ).length,
+ ids.length,
+ "Number of items in table is correct"
+ );
+
+ // Check if all the desired items are present in the table
+ for (const id of ids) {
+ ok(
+ doc.querySelector(".table-widget-cell[data-id='" + id + "']"),
+ `Table item ${id} should be present`
+ );
+ }
+ }
+};
+
+add_task(async function () {
+ await openTabAndSetupStorage(
+ MAIN_DOMAIN_SECURED + "storage-empty-objectstores.html"
+ );
+
+ testTree();
+ await testTables();
+});
diff --git a/devtools/client/storage/test/browser_storage_file_url.js b/devtools/client/storage/test/browser_storage_file_url.js
new file mode 100644
index 0000000000..7e0d3c0283
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_file_url.js
@@ -0,0 +1,64 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Test to verify that various storage types work when using file:// URLs.
+
+"use strict";
+
+add_task(async function () {
+ const TESTPAGE = "storage-file-url.html";
+
+ // We need to load TESTPAGE using a file:// path so we need to get that from
+ // the current test path.
+ const testPath = getResolvedURI(gTestPath);
+ const dir = getChromeDir(testPath);
+
+ // Then append TESTPAGE to the test path.
+ dir.append(TESTPAGE);
+
+ // Then generate a FileURI to ensure the path is valid.
+ const uriString = Services.io.newFileURI(dir).spec;
+
+ // Now we have a valid file:// URL pointing to TESTPAGE.
+ await openTabAndSetupStorage(uriString);
+
+ // uriString points to the test inside objdir e.g.
+ // `/path/to/fx/objDir/_tests/testing/mochitest/browser/devtools/client/
+ // storage/test/storage-file-url.html`.
+ //
+ // When opened in the browser this may resolve to a different path e.g.
+ // `path/to/fx/repo/devtools/client/storage/test/storage-file-url.html`.
+ //
+ // The easiest way to get the actual path is to request it from the content
+ // process.
+ const browser = gBrowser.selectedBrowser;
+ const actualPath = await SpecialPowers.spawn(browser, [], () => {
+ return content.document.location.href;
+ });
+
+ const cookiePath = actualPath
+ .substr(0, actualPath.lastIndexOf("/"))
+ .replace(/file:\/\//g, "");
+ await checkState([
+ [
+ ["cookies", actualPath],
+ [
+ getCookieId("test1", "", cookiePath),
+ getCookieId("test2", "", cookiePath),
+ ],
+ ],
+ [
+ ["indexedDB", actualPath, "MyDatabase (default)", "MyObjectStore"],
+ [12345, 54321, 67890, 98765],
+ ],
+ [
+ ["localStorage", actualPath],
+ ["test3", "test4"],
+ ],
+ [
+ ["sessionStorage", actualPath],
+ ["test5", "test6"],
+ ],
+ ]);
+});
diff --git a/devtools/client/storage/test/browser_storage_fission_cache.js b/devtools/client/storage/test/browser_storage_fission_cache.js
new file mode 100644
index 0000000000..4b9223198c
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_fission_cache.js
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Bug 1746646: Make mochitests work with TCP enabled (cookieBehavior = 5)
+// All instances of addPermission and removePermission set up 3rd-party storage
+// access in a way that allows the test to proceed with TCP enabled.
+
+add_task(async function () {
+ // open tab
+ const URL = URL_ROOT_COM_SSL + "storage-cache-basic.html";
+ await SpecialPowers.addPermission(
+ "3rdPartyStorage^https://example.net",
+ true,
+ URL
+ );
+ await openTabAndSetupStorage(URL);
+ const doc = gPanelWindow.document;
+
+ // check that host appears in the storage tree
+ checkTree(doc, ["Cache", "https://example.com", "lorem"]);
+ checkTree(doc, ["Cache", "https://example.net", "foo"]);
+ // Check top level page
+ await selectTreeItem(["Cache", "https://example.com", "lorem"]);
+ checkCacheData(URL_ROOT_COM_SSL + "storage-blank.html", "OK");
+ // Check iframe
+ await selectTreeItem(["Cache", "https://example.net", "foo"]);
+ checkCacheData(URL_ROOT_NET_SSL + "storage-blank.html", "OK");
+
+ await SpecialPowers.removePermission(
+ "3rdPartyStorage^http://example.net",
+ URL
+ );
+});
+
+function checkCacheData(url, status) {
+ is(
+ gUI.table.items.get(url)?.status,
+ status,
+ `Table row has an entry for: ${url} with status: ${status}`
+ );
+}
diff --git a/devtools/client/storage/test/browser_storage_fission_cookies.js b/devtools/client/storage/test/browser_storage_fission_cookies.js
new file mode 100644
index 0000000000..4628643188
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_fission_cookies.js
@@ -0,0 +1,64 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default"
+ set: [
+ ["network.cookie.sameSite.laxByDefault", false],
+ [
+ "privacy.partition.always_partition_third_party_non_cookie_storage",
+ false,
+ ],
+ ],
+ });
+
+ const URL_IFRAME = buildURLWithContent(
+ "example.net",
+ `<h1>iframe</h1>` + `<script>document.cookie = "lorem=ipsum";</script>`
+ );
+
+ const URL_MAIN = buildURLWithContent(
+ "example.com",
+ `<h1>Main</h1>` +
+ `<script>document.cookie="foo=bar";</script>` +
+ `<iframe src="${URL_IFRAME}">`
+ );
+
+ // open tab
+ await openTabAndSetupStorage(URL_MAIN);
+ const doc = gPanelWindow.document;
+
+ // check that both hosts appear in the storage tree
+ checkTree(doc, ["cookies", "https://example.com"]);
+ checkTree(doc, ["cookies", "https://example.net"]);
+ // check the table for values
+ await selectTreeItem(["cookies", "https://example.com"]);
+ checkCookieData("foo", "bar");
+ await selectTreeItem(["cookies", "https://example.net"]);
+ checkCookieData("lorem", "ipsum");
+
+ info("Add more cookies");
+ const onUpdated = gUI.once("store-objects-edit");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ content.window.document.cookie = "foo2=bar2";
+
+ const iframe = content.document.querySelector("iframe");
+ return SpecialPowers.spawn(iframe, [], () => {
+ content.document.cookie = "lorem2=ipsum2";
+ });
+ });
+ await onUpdated;
+
+ // check that the new data is shown in the table for the iframe document
+ checkCookieData("lorem2", "ipsum2");
+
+ // check that the new data is shown in the table for the top-level document
+ await selectTreeItem(["cookies", "https://example.com"]);
+ checkCookieData("foo2", "bar2");
+
+ SpecialPowers.clearUserPref("network.cookie.sameSite.laxByDefault");
+});
diff --git a/devtools/client/storage/test/browser_storage_fission_hide_aboutblank.js b/devtools/client/storage/test/browser_storage_fission_hide_aboutblank.js
new file mode 100644
index 0000000000..4dab607cc3
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_fission_hide_aboutblank.js
@@ -0,0 +1,26 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function () {
+ const html = `<h1>about:blank iframe</h1><iframe src="about:blank"></iframe>`;
+ const url = `https://example.com/document-builder.sjs?html=${encodeURI(
+ html
+ )}`;
+ // open tab
+ await openTabAndSetupStorage(url);
+ const doc = gPanelWindow.document;
+
+ checkTree(doc, ["localStorage", "https://example.com"], true);
+ checkTree(doc, ["localStorage", "about:blank"], false);
+});
+
+add_task(async function () {
+ // open tab with about:blank as top-level page
+ await openTabAndSetupStorage("about:blank");
+ const doc = gPanelWindow.document;
+
+ checkTree(doc, ["localStorage"], true);
+});
diff --git a/devtools/client/storage/test/browser_storage_fission_indexeddb.js b/devtools/client/storage/test/browser_storage_fission_indexeddb.js
new file mode 100644
index 0000000000..ae43a77249
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_fission_indexeddb.js
@@ -0,0 +1,62 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function () {
+ const URL = URL_ROOT_COM_SSL + "storage-indexeddb-iframe.html";
+
+ // open tab
+ await openTabAndSetupStorage(URL);
+ const doc = gPanelWindow.document;
+
+ // check that host appears in the storage tree
+ checkTree(doc, ["indexedDB", "https://example.com"]);
+ // check the table for values
+ await selectTreeItem([
+ "indexedDB",
+ "https://example.com",
+ "db (default)",
+ "store",
+ ]);
+ checkStorageData("foo", JSON.stringify({ key: "foo", value: "bar" }));
+
+ // check that host appears in the storage tree
+ checkTree(doc, ["indexedDB", "https://example.net"]);
+ // check the table for values
+ await selectTreeItem([
+ "indexedDB",
+ "https://example.net",
+ "db (default)",
+ "store",
+ ]);
+ checkStorageData("lorem", JSON.stringify({ key: "lorem", value: "ipsum" }));
+
+ info("Add new data to the iframe DB");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ const iframe = content.document.querySelector("iframe");
+ return SpecialPowers.spawn(iframe, [], async function () {
+ return new Promise(resolve => {
+ const request = content.window.indexedDB.open("db", 1);
+ request.onsuccess = event => {
+ const db = event.target.result;
+ const transaction = db.transaction(["store"], "readwrite");
+ const addRequest = transaction
+ .objectStore("store")
+ .add({ key: "hello", value: "world" });
+ addRequest.onsuccess = () => resolve();
+ };
+ });
+ });
+ });
+
+ info("Refreshing table");
+ doc.querySelector("#refresh-button").click();
+
+ info("Check that table has new row");
+ await waitUntil(() =>
+ hasStorageData("hello", JSON.stringify({ key: "hello", value: "world" }))
+ );
+ ok(true, "Table has the new data");
+});
diff --git a/devtools/client/storage/test/browser_storage_fission_local_storage.js b/devtools/client/storage/test/browser_storage_fission_local_storage.js
new file mode 100644
index 0000000000..ae49516031
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_fission_local_storage.js
@@ -0,0 +1,45 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function () {
+ const URL_IFRAME = buildURLWithContent(
+ "example.net",
+ `<h1>iframe</h1>` +
+ `<script>localStorage.setItem("lorem", "ipsum");</script>`
+ );
+ const URL_MAIN = buildURLWithContent(
+ "example.com",
+ `<h1>Main</h1>` +
+ `<script>localStorage.setItem("foo", "bar");</script>` +
+ `<iframe src="${URL_IFRAME}">`
+ );
+
+ // open tab
+ await openTabAndSetupStorage(URL_MAIN);
+ const doc = gPanelWindow.document;
+
+ // check that both hosts appear in the storage tree
+ checkTree(doc, ["localStorage", "https://example.com"]);
+ // check the table for values
+ await selectTreeItem(["localStorage", "https://example.com"]);
+ await waitForStorageData("foo", "bar");
+ await selectTreeItem(["localStorage", "https://example.net"]);
+ await waitForStorageData("lorem", "ipsum");
+
+ // add more storage data to the main wrapper
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ content.window.localStorage.setItem("foo2", "bar2");
+ const iframe = content.document.querySelector("iframe");
+ return SpecialPowers.spawn(iframe, [], () => {
+ content.window.localStorage.setItem("lorem2", "ipsum2");
+ });
+ });
+ // check that the new data is shown in the table
+ await selectTreeItem(["localStorage", "https://example.com"]);
+ await waitForStorageData("foo2", "bar2");
+ await selectTreeItem(["localStorage", "https://example.net"]);
+ await waitForStorageData("lorem2", "ipsum2");
+});
diff --git a/devtools/client/storage/test/browser_storage_fission_session_storage.js b/devtools/client/storage/test/browser_storage_fission_session_storage.js
new file mode 100644
index 0000000000..a3aa475868
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_fission_session_storage.js
@@ -0,0 +1,45 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function () {
+ const URL_IFRAME = buildURLWithContent(
+ "example.net",
+ `<h1>iframe</h1>` +
+ `<script>sessionStorage.setItem("lorem", "ipsum");</script>`
+ );
+ const URL_MAIN = buildURLWithContent(
+ "example.com",
+ `<h1>Main</h1>` +
+ `<script>sessionStorage.setItem("foo", "bar");</script>` +
+ `<iframe src="${URL_IFRAME}">`
+ );
+
+ // open tab
+ await openTabAndSetupStorage(URL_MAIN);
+ const doc = gPanelWindow.document;
+
+ // check that both hosts appear in the storage tree
+ checkTree(doc, ["sessionStorage", "https://example.com"]);
+ // check the table for values
+ await selectTreeItem(["sessionStorage", "https://example.com"]);
+ await waitForStorageData("foo", "bar");
+ await selectTreeItem(["sessionStorage", "https://example.net"]);
+ await waitForStorageData("lorem", "ipsum");
+
+ // add more storage data to the main wrapper
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ content.window.sessionStorage.setItem("foo2", "bar2");
+ const iframe = content.document.querySelector("iframe");
+ return SpecialPowers.spawn(iframe, [], () => {
+ content.window.sessionStorage.setItem("lorem2", "ipsum2");
+ });
+ });
+ // check that the new data is shown in the table
+ await selectTreeItem(["sessionStorage", "https://example.com"]);
+ await waitForStorageData("foo2", "bar2");
+ await selectTreeItem(["sessionStorage", "https://example.net"]);
+ await waitForStorageData("lorem2", "ipsum2");
+});
diff --git a/devtools/client/storage/test/browser_storage_indexeddb_add_button_hidden.js b/devtools/client/storage/test/browser_storage_indexeddb_add_button_hidden.js
new file mode 100644
index 0000000000..6cee0dc493
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_indexeddb_add_button_hidden.js
@@ -0,0 +1,35 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Test that the add button is hidden for the indexedDB storage type.
+add_task(async function () {
+ await openTabAndSetupStorage(
+ MAIN_DOMAIN_SECURED + "storage-empty-objectstores.html"
+ );
+
+ info("Select an indexedDB item");
+ const idbItem = ["indexedDB", "https://test1.example.org", "idb1 (default)"];
+ await selectTreeItem(idbItem);
+ checkAddButtonState({ expectHidden: true });
+
+ // Note: test only one of the other stoage types to check that the logic to
+ // find the add button is not outdated. Other storage types have more detailed
+ // tests focused on the add feature.
+ info("Select a cookie item");
+ const cookieItem = ["cookies", "https://test1.example.org"];
+ await selectTreeItem(cookieItem);
+ checkAddButtonState({ expectHidden: false });
+});
+
+function checkAddButtonState({ expectHidden }) {
+ const toolbar = gPanelWindow.document.getElementById("storage-toolbar");
+ const addButton = toolbar.querySelector("#add-button");
+ is(
+ addButton.hidden,
+ expectHidden,
+ `The add button is ${expectHidden ? "hidden" : "displayed"}`
+ );
+}
diff --git a/devtools/client/storage/test/browser_storage_indexeddb_delete.js b/devtools/client/storage/test/browser_storage_indexeddb_delete.js
new file mode 100644
index 0000000000..8c2f6bbf71
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_indexeddb_delete.js
@@ -0,0 +1,54 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Test deleting indexedDB database from the tree using context menu
+
+add_task(async function () {
+ await openTabAndSetupStorage(
+ MAIN_DOMAIN_SECURED + "storage-empty-objectstores.html"
+ );
+
+ const contextMenu =
+ gPanelWindow.document.getElementById("storage-tree-popup");
+ const menuDeleteDb = contextMenu.querySelector("#storage-tree-popup-delete");
+
+ info("test state before delete");
+ await checkState([
+ [
+ ["indexedDB", "https://test1.example.org"],
+ ["idb1 (default)", "idb2 (default)"],
+ ],
+ ]);
+
+ info("do the delete");
+ const deletedDb = [
+ "indexedDB",
+ "https://test1.example.org",
+ "idb1 (default)",
+ ];
+
+ await selectTreeItem(deletedDb);
+
+ // Wait once for update and another time for value fetching
+ const eventWait = gUI.once("store-objects-updated");
+
+ const selector = `[data-id='${JSON.stringify(
+ deletedDb
+ )}'] > .tree-widget-item`;
+ const target = gPanelWindow.document.querySelector(selector);
+ ok(target, `tree item found in ${deletedDb.join(" > ")}`);
+ await waitForContextMenu(contextMenu, target, () => {
+ info(`Opened tree context menu in ${deletedDb.join(" > ")}`);
+ menuDeleteDb.click();
+ });
+
+ await eventWait;
+
+ info("test state after delete");
+ await checkState([
+ [["indexedDB", "https://test1.example.org"], ["idb2 (default)"]],
+ ]);
+});
diff --git a/devtools/client/storage/test/browser_storage_indexeddb_delete_blocked.js b/devtools/client/storage/test/browser_storage_indexeddb_delete_blocked.js
new file mode 100644
index 0000000000..5b27816426
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_indexeddb_delete_blocked.js
@@ -0,0 +1,60 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Test what happens when deleting indexedDB database is blocked
+
+add_task(async function () {
+ await openTabAndSetupStorage(
+ MAIN_DOMAIN_SECURED + "storage-idb-delete-blocked.html"
+ );
+
+ info("test state before delete");
+ await checkState([
+ [["indexedDB", "https://test1.example.org"], ["idb (default)"]],
+ ]);
+
+ info("do the delete");
+ await selectTreeItem(["indexedDB", "https://test1.example.org"]);
+ const front = gUI.getCurrentFront();
+ let result = await front.removeDatabase(
+ "https://test1.example.org",
+ "idb (default)"
+ );
+
+ ok(result.blocked, "removeDatabase attempt is blocked");
+
+ info("test state after blocked delete");
+ await checkState([
+ [["indexedDB", "https://test1.example.org"], ["idb (default)"]],
+ ]);
+
+ const eventWait = gUI.once("store-objects-edit");
+
+ info("telling content to close the db");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ const win = content.wrappedJSObject;
+ await win.closeDb();
+ });
+
+ info("waiting for store edit events");
+ await eventWait;
+
+ info("test state after real delete");
+ await checkState([[["indexedDB", "https://test1.example.org"], []]]);
+
+ info("try to delete database from nonexistent host");
+ let errorThrown = false;
+ try {
+ result = await front.removeDatabase(
+ "https://test2.example.org",
+ "idb (default)"
+ );
+ } catch (ex) {
+ errorThrown = true;
+ }
+
+ ok(errorThrown, "error was reported when trying to delete");
+});
diff --git a/devtools/client/storage/test/browser_storage_indexeddb_duplicate_names.js b/devtools/client/storage/test/browser_storage_indexeddb_duplicate_names.js
new file mode 100644
index 0000000000..cc6ac951fc
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_indexeddb_duplicate_names.js
@@ -0,0 +1,24 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Test to verify that indexedDBs with duplicate names (different types / paths)
+// work as expected.
+
+"use strict";
+
+add_task(async function () {
+ const TESTPAGE =
+ MAIN_DOMAIN_SECURED + "storage-indexeddb-duplicate-names.html";
+
+ setPermission(TESTPAGE, "indexedDB");
+
+ await openTabAndSetupStorage(TESTPAGE);
+
+ await checkState([
+ [
+ ["indexedDB", "https://test1.example.org"],
+ ["idb1 (default)", "idb2 (default)"],
+ ],
+ ]);
+});
diff --git a/devtools/client/storage/test/browser_storage_indexeddb_hide_internal_dbs.js b/devtools/client/storage/test/browser_storage_indexeddb_hide_internal_dbs.js
new file mode 100644
index 0000000000..35906256b1
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_indexeddb_hide_internal_dbs.js
@@ -0,0 +1,62 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/framework/browser-toolbox/test/helpers-browser-toolbox.js",
+ this
+);
+
+// Test that internal DBs are hidden in the regular toolbox,but visible in the
+// Browser Toolbox
+add_task(async function () {
+ await openTabAndSetupStorage(
+ MAIN_DOMAIN_SECURED + "storage-empty-objectstores.html"
+ );
+ const doc = gPanelWindow.document;
+
+ // check regular toolbox
+ info("Check indexedDB tree in toolbox");
+ const hosts = getDBHostsInTree(doc);
+ is(hosts.length, 1, "There is only one host for indexedDB storage");
+ is(hosts[0], "https://test1.example.org", "Host is test1.example.org");
+
+ // check browser toolbox
+ info("awaiting to open browser toolbox");
+ const ToolboxTask = await initBrowserToolboxTask();
+ await ToolboxTask.importFunctions({ getDBHostsInTree });
+
+ await ToolboxTask.spawn(null, async () => {
+ info("Selecting storage panel");
+ await gToolbox.selectTool("storage");
+ info("Check indexedDB tree in browser toolbox");
+ const browserToolboxDoc = gToolbox.getCurrentPanel().panelWindow.document;
+
+ const browserToolboxHosts = getDBHostsInTree(browserToolboxDoc);
+ ok(browserToolboxHosts.length > 1, "There are more than 1 indexedDB hosts");
+ ok(
+ browserToolboxHosts.includes("about:devtools-toolbox"),
+ "about:devtools-toolbox host is present"
+ );
+ ok(browserToolboxHosts.includes("chrome"), "chrome host is present");
+ ok(
+ browserToolboxHosts.includes("indexeddb+++fx-devtools"),
+ "fx-devtools host is present"
+ );
+ });
+
+ info("Destroying browser toolbox");
+ await ToolboxTask.destroy();
+});
+
+function getDBHostsInTree(doc) {
+ const treeId = JSON.stringify(["indexedDB"]);
+ const items = doc.querySelectorAll(
+ `[data-id='${treeId}'] > .tree-widget-children > *`
+ );
+
+ // the host is located at the 2nd element of the array in data-id
+ return [...items].map(x => JSON.parse(x.dataset.id)[1]);
+}
diff --git a/devtools/client/storage/test/browser_storage_indexeddb_navigation.js b/devtools/client/storage/test/browser_storage_indexeddb_navigation.js
new file mode 100644
index 0000000000..3235f10121
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_indexeddb_navigation.js
@@ -0,0 +1,72 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+requestLongerTimeout(3);
+
+add_task(async function () {
+ const URL1 = URL_ROOT_COM_SSL + "storage-indexeddb-simple.html";
+ const URL2 = URL_ROOT_NET_SSL + "storage-indexeddb-simple-alt.html";
+
+ // open tab
+ await openTabAndSetupStorage(URL1);
+ const doc = gPanelWindow.document;
+
+ // Check first domain
+ // check that host appears in the storage tree
+ checkTree(doc, ["indexedDB", "https://example.com"]);
+ // check the table for values
+ await selectTreeItem([
+ "indexedDB",
+ "https://example.com",
+ "db (default)",
+ "store",
+ ]);
+ checkStorageData("lorem", JSON.stringify({ key: "lorem", value: "ipsum" }));
+
+ // clear db before navigating to a new domain
+ info("Removing database…");
+ await clearStorage();
+
+ // Check second domain
+ await navigateTo(URL2);
+ info("Creating database in the second domain…");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ const win = content.wrappedJSObject;
+ await win.setup();
+ });
+ // wait for storage tree refresh, and check host
+ info("Checking storage tree…");
+ await waitUntil(() => isInTree(doc, ["indexedDB", "https://example.net"]));
+
+ ok(
+ !isInTree(doc, ["indexedDB", "https://example.com"]),
+ "example.com item is not in the tree anymore"
+ );
+
+ // TODO: select tree and check on storage data.
+ // We cannot do it yet since we do not detect newly created indexed db's when
+ // navigating. See Bug 1273802
+
+ // reload the current tab, and check again
+ await reloadBrowser();
+ // wait for storage tree refresh, and check host
+ info("Checking storage tree…");
+ await waitUntil(() => isInTree(doc, ["indexedDB", "https://example.net"]));
+
+ info("Check that the indexedDB node still has the expected label");
+ is(
+ getTreeNodeLabel(doc, ["indexedDB"]),
+ "Indexed DB",
+ "indexedDB item is properly displayed"
+ );
+});
+
+async function clearStorage() {
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ const win = content.wrappedJSObject;
+ await win.clear();
+ });
+}
diff --git a/devtools/client/storage/test/browser_storage_indexeddb_overflow.js b/devtools/client/storage/test/browser_storage_indexeddb_overflow.js
new file mode 100644
index 0000000000..7ec65ea010
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_indexeddb_overflow.js
@@ -0,0 +1,36 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Test endless scrolling when a lot of items are present in the storage
+// inspector table for IndexedDB.
+"use strict";
+
+const ITEMS_PER_PAGE = 50;
+
+add_task(async function () {
+ await openTabAndSetupStorage(
+ MAIN_DOMAIN_SECURED + "storage-overflow-indexeddb.html"
+ );
+
+ info("Run the tests with short DevTools");
+ await runTests();
+
+ info("Close Toolbox");
+ await gDevTools.closeToolboxForTab(gBrowser.selectedTab);
+});
+
+async function runTests() {
+ gUI.tree.expandAll();
+
+ await selectTreeItem([
+ "indexedDB",
+ "https://test1.example.org",
+ "database (default)",
+ "store",
+ ]);
+ checkCellLength(ITEMS_PER_PAGE);
+
+ await scroll();
+ checkCellLength(ITEMS_PER_PAGE * 2);
+}
diff --git a/devtools/client/storage/test/browser_storage_keys.js b/devtools/client/storage/test/browser_storage_keys.js
new file mode 100644
index 0000000000..d0c0bcaad0
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_keys.js
@@ -0,0 +1,164 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Test to verify that the keys shown in sidebar are correct
+
+// Format of the test cases: {
+// action: Either "selectTreeItem" to select a tree item or
+// "assertTableItem" to select a table item,
+// ids: ID array for tree item to select if `action` is "selectTreeItem",
+// id: ID of the table item if `action` is "assertTableItem",
+// keyValuePairs: Array of key value pair objects which will be asserted
+// to exist in the storage sidebar (optional)
+// }
+
+"use strict";
+
+const LONG_WORD = "a".repeat(1000);
+
+add_task(async function () {
+ await openTabAndSetupStorage(
+ MAIN_DOMAIN_SECURED + "storage-complex-keys.html"
+ );
+
+ gUI.tree.expandAll();
+
+ await testLocalStorage();
+ await testSessionStorage();
+ await testIndexedDB();
+});
+
+async function testLocalStorage() {
+ const tests = [
+ {
+ action: "selectTreeItem",
+ ids: ["localStorage", "https://test1.example.org"],
+ },
+ {
+ action: "assertTableItem",
+ id: "",
+ value: "1",
+ },
+ {
+ action: "assertTableItem",
+ id: "键",
+ value: "2",
+ },
+ ];
+
+ await makeTests(tests);
+}
+
+async function testSessionStorage() {
+ const tests = [
+ {
+ action: "selectTreeItem",
+ ids: ["sessionStorage", "https://test1.example.org"],
+ },
+ {
+ action: "assertTableItem",
+ id: "Key with spaces",
+ value: "3",
+ },
+ {
+ action: "assertTableItem",
+ id: "Key#with~special$characters",
+ value: "4",
+ },
+ {
+ action: "assertTableItem",
+ id: LONG_WORD,
+ value: "5",
+ },
+ ];
+
+ await makeTests(tests);
+}
+
+async function testIndexedDB() {
+ const tests = [
+ {
+ action: "selectTreeItem",
+ ids: ["indexedDB", "https://test1.example.org", "idb (default)", "obj"],
+ },
+ {
+ action: "assertTableItem",
+ id: "",
+ value: JSON.stringify({ id: "", name: "foo" }),
+ keyValuePairs: [
+ { name: ".id", value: "" },
+ { name: ".name", value: "foo" },
+ ],
+ },
+ {
+ action: "assertTableItem",
+ id: "键",
+ value: JSON.stringify({ id: "键", name: "foo2" }),
+ keyValuePairs: [
+ { name: "键.id", value: "键" },
+ { name: "键.name", value: "foo2" },
+ ],
+ },
+ {
+ action: "assertTableItem",
+ id: "Key with spaces",
+ value: JSON.stringify({ id: "Key with spaces", name: "foo3" }),
+ keyValuePairs: [
+ { name: "Key with spaces.id", value: "Key with spaces" },
+ { name: "Key with spaces.name", value: "foo3" },
+ ],
+ },
+ {
+ action: "assertTableItem",
+ id: "Key#with~special$characters",
+ value: JSON.stringify({
+ id: "Key#with~special$characters",
+ name: "foo4",
+ }),
+ keyValuePairs: [
+ {
+ name: "Key#with~special$characters.id",
+ value: "Key#with~special$characters",
+ },
+ { name: "Key#with~special$characters.name", value: "foo4" },
+ ],
+ },
+ {
+ action: "assertTableItem",
+ id: LONG_WORD,
+ value: JSON.stringify({ id: LONG_WORD, name: "foo5" }),
+ keyValuePairs: [
+ { name: `${LONG_WORD}.id`, value: LONG_WORD },
+ { name: `${LONG_WORD}.name`, value: "foo5" },
+ ],
+ },
+ ];
+
+ await makeTests(tests);
+}
+
+async function makeTests(tests) {
+ for (const item of tests) {
+ info(`Selecting item ${JSON.stringify(item)}`);
+
+ switch (item.action) {
+ case "selectTreeItem":
+ await selectTreeItem(item.ids);
+ break;
+
+ case "assertTableItem":
+ await selectTableItem(item.id);
+ // Check the ID and value in the data section
+ await findVariableViewProperties([
+ { name: item.id, value: item.value },
+ ]);
+ // If there are key value pairs defined, check those in the
+ // parsed value section
+ if (item.keyValuePairs) {
+ await findVariableViewProperties(item.keyValuePairs, true);
+ }
+ break;
+ }
+ }
+}
diff --git a/devtools/client/storage/test/browser_storage_localstorage_add.js b/devtools/client/storage/test/browser_storage_localstorage_add.js
new file mode 100644
index 0000000000..a8c6d23585
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_localstorage_add.js
@@ -0,0 +1,20 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Basic test to check the adding of localStorage entries.
+
+"use strict";
+
+add_task(async function () {
+ await openTabAndSetupStorage(
+ MAIN_DOMAIN_SECURED + "storage-localstorage.html"
+ );
+ showAllColumns(true);
+
+ await performAdd(["localStorage", "https://test1.example.org"]);
+ await performAdd(["localStorage", "https://test1.example.org"]);
+ await performAdd(["localStorage", "https://test1.example.org"]);
+ await performAdd(["localStorage", "https://test1.example.org"]);
+ await performAdd(["localStorage", "https://test1.example.org"]);
+});
diff --git a/devtools/client/storage/test/browser_storage_localstorage_edit.js b/devtools/client/storage/test/browser_storage_localstorage_edit.js
new file mode 100644
index 0000000000..0fc6cb4d7d
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_localstorage_edit.js
@@ -0,0 +1,24 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Basic test to check the editing of localStorage.
+
+"use strict";
+
+add_task(async function () {
+ await openTabAndSetupStorage(
+ MAIN_DOMAIN_SECURED + "storage-localstorage.html"
+ );
+
+ await selectTreeItem(["localStorage", "https://test1.example.org"]);
+
+ await editCell("TestLS1", "name", "newTestLS1");
+ await editCell("newTestLS1", "value", "newValueLS1");
+
+ await editCell("TestLS3", "name", "newTestLS3");
+ await editCell("newTestLS3", "value", "newValueLS3");
+
+ await editCell("TestLS5", "name", "newTestLS5");
+ await editCell("newTestLS5", "value", "newValueLS5");
+});
diff --git a/devtools/client/storage/test/browser_storage_localstorage_error.js b/devtools/client/storage/test/browser_storage_localstorage_error.js
new file mode 100644
index 0000000000..2faa53c213
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_localstorage_error.js
@@ -0,0 +1,25 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Test that for pages where local/sessionStorage is not available (like about:home),
+// the host still appears in the storage tree and no unhandled exception is thrown.
+
+add_task(async function () {
+ await openTabAndSetupStorage("about:home");
+
+ const itemsToOpen = [
+ ["localStorage", "about:home"],
+ ["sessionStorage", "about:home"],
+ ];
+
+ for (const item of itemsToOpen) {
+ await selectTreeItem(item);
+ ok(
+ gUI.tree.isSelected(item),
+ `Item ${item.join(" > ")} is present in the tree`
+ );
+ }
+});
diff --git a/devtools/client/storage/test/browser_storage_localstorage_navigation.js b/devtools/client/storage/test/browser_storage_localstorage_navigation.js
new file mode 100644
index 0000000000..92a8eab210
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_localstorage_navigation.js
@@ -0,0 +1,63 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function () {
+ const URL1 = buildURLWithContent(
+ "example.com",
+ `<h1>example.com</h1>` +
+ `<script>localStorage.setItem("lorem", "ipsum");</script>`
+ );
+ const URL2 = buildURLWithContent(
+ "example.net",
+ `<h1>example.net</h1>` +
+ `<script>localStorage.setItem("foo", "bar");</script>`
+ );
+
+ // open tab
+ await openTabAndSetupStorage(URL1);
+ const doc = gPanelWindow.document;
+
+ // Check first domain
+ // check that both host appear in the storage tree
+ checkTree(doc, ["localStorage", "https://example.com"]);
+ // check the table for values
+ await selectTreeItem(["localStorage", "https://example.com"]);
+ checkStorageData("lorem", "ipsum");
+
+ // clear up local storage data before navigating
+ info("Cleaning up localStorage…");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ const win = content.wrappedJSObject;
+ await win.localStorage.clear();
+ });
+
+ // Check second domain
+ await navigateTo(URL2);
+ // wait for storage tree refresh, and check host
+ info("Waiting for storage tree to refresh and show correct host…");
+ await waitUntil(() => isInTree(doc, ["localStorage", "https://example.net"]));
+ ok(
+ !isInTree(doc, ["localStorage", "https://example.com"]),
+ "example.com item is not in the tree anymore"
+ );
+
+ // reload the current tab and check data
+ await reloadBrowser();
+ // wait for storage tree refresh, and check host
+ info("Waiting for storage tree to refresh and show correct host…");
+ await waitUntil(() => isInTree(doc, ["localStorage", "https://example.net"]));
+
+ // check the table for values
+ await selectTreeItem(["localStorage", "https://example.net"]);
+ checkStorageData("foo", "bar");
+
+ info("Check that the localStorage node still has the expected label");
+ is(
+ getTreeNodeLabel(doc, ["localStorage"]),
+ "Local Storage",
+ "localStorage item is properly displayed"
+ );
+});
diff --git a/devtools/client/storage/test/browser_storage_localstorage_rapid_add_remove.js b/devtools/client/storage/test/browser_storage_localstorage_rapid_add_remove.js
new file mode 100644
index 0000000000..56636903bb
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_localstorage_rapid_add_remove.js
@@ -0,0 +1,30 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Basic test to check the rapid adding and removing of localStorage entries.
+
+"use strict";
+
+add_task(async function () {
+ await openTabAndSetupStorage(MAIN_DOMAIN_SECURED + "storage-blank.html");
+ await selectTreeItem(["localStorage", "https://test1.example.org"]);
+
+ ok(isTableEmpty(), "Table empty on init");
+
+ for (let i = 0; i < 10; i++) {
+ await addRemove(`test${i}`);
+ }
+});
+
+async function addRemove(name) {
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [name], innerName => {
+ content.localStorage.setItem(innerName, "true");
+ content.localStorage.removeItem(innerName);
+ });
+
+ info("Waiting for store objects to be changed");
+ await gUI.once("store-objects-edit");
+
+ ok(isTableEmpty(), `Table empty after rapid add/remove of "${name}"`);
+}
diff --git a/devtools/client/storage/test/browser_storage_overflow.js b/devtools/client/storage/test/browser_storage_overflow.js
new file mode 100644
index 0000000000..b5f3989fa8
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_overflow.js
@@ -0,0 +1,104 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Test endless scrolling when a lot of items are present in the storage
+// inspector table.
+"use strict";
+
+const ITEMS_PER_PAGE = 50;
+
+add_task(async function () {
+ await openTabAndSetupStorage(MAIN_DOMAIN_SECURED + "storage-overflow.html");
+
+ info("Run the tests with short DevTools");
+ await runTests();
+
+ info("Close Toolbox");
+ await gDevTools.closeToolboxForTab(gBrowser.selectedTab);
+
+ info("Set a toolbox height of 1000px");
+ await pushPref("devtools.toolbox.footer.height", 1000);
+
+ info("Open storage panel again");
+ await openStoragePanel();
+
+ info("Run the tests with tall DevTools");
+ await runTests(true);
+});
+
+async function runTests(tall) {
+ if (tall) {
+ // We need to zoom out and a tall storage panel in order to fit more than 50
+ // items in the table. We do this to ensure that we load enough content to
+ // show a scrollbar so that we can still use infinite scrolling.
+ zoom(0.5);
+ }
+
+ gUI.tree.expandAll();
+ await selectTreeItem(["localStorage", "https://test1.example.org"]);
+
+ if (tall) {
+ if (getCellLength() === ITEMS_PER_PAGE) {
+ await scrollToAddItems();
+ await waitForStorageData("item-100", "value-100");
+ }
+
+ if (getCellLength() === ITEMS_PER_PAGE * 2) {
+ await scrollToAddItems();
+ await waitForStorageData("item-150", "value-150");
+ }
+
+ if (getCellLength() === ITEMS_PER_PAGE * 3) {
+ await scrollToAddItems();
+ await waitForStorageData("item-151", "value-151");
+ }
+ } else {
+ checkCellLength(ITEMS_PER_PAGE);
+ await scrollToAddItems();
+ await waitForStorageData("item-100", "value-100");
+
+ checkCellLength(ITEMS_PER_PAGE * 2);
+ await scrollToAddItems();
+ await waitForStorageData("item-150", "value-150");
+
+ checkCellLength(ITEMS_PER_PAGE * 3);
+ await scrollToAddItems();
+ await waitForStorageData("item-151", "value-151");
+ }
+
+ is(getCellLength(), 151, "Storage table contains 151 items");
+
+ // Check that the columns are sorted in a human readable way (ascending).
+ checkCellValues("ASC");
+
+ // Sort descending.
+ clickColumnHeader("name");
+
+ // Check that the columns are sorted in a human readable way (descending).
+ checkCellValues("DEC");
+
+ if (tall) {
+ zoom(1);
+ }
+}
+
+function checkCellValues(order) {
+ const cells = [
+ ...gPanelWindow.document.querySelectorAll("#name .table-widget-cell"),
+ ];
+ cells.forEach(function (cell, index, arr) {
+ const i = order === "ASC" ? index + 1 : arr.length - index;
+ is(cell.value, `item-${i}`, `Cell value is "item-${i}" (${order}).`);
+ });
+}
+
+async function scrollToAddItems() {
+ info(`Scrolling to add ${ITEMS_PER_PAGE} items`);
+ await scroll();
+}
+
+function zoom(zoomValue) {
+ const bc = BrowsingContext.getFromWindow(gPanelWindow);
+ bc.fullZoom = zoomValue;
+}
diff --git a/devtools/client/storage/test/browser_storage_search.js b/devtools/client/storage/test/browser_storage_search.js
new file mode 100644
index 0000000000..1f7c8a5c06
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_search.js
@@ -0,0 +1,140 @@
+// Tests the filter search box in the storage inspector
+"use strict";
+
+add_task(async function () {
+ await openTabAndSetupStorage(MAIN_DOMAIN_SECURED + "storage-search.html");
+
+ gUI.tree.expandAll();
+ await selectTreeItem(["cookies", "https://test1.example.org"]);
+
+ showColumn("expires", false);
+ showColumn("host", false);
+ showColumn("isHttpOnly", false);
+ showColumn("lastAccessed", false);
+ showColumn("path", false);
+
+ // Results: 0=hidden, 1=visible
+ const testcases = [
+ // Test that search isn't case-sensitive
+ {
+ value: "FoO",
+ results: [0, 0, 1, 1, 0, 1, 0],
+ },
+ {
+ value: "OR",
+ results: [0, 1, 0, 0, 0, 1, 0],
+ },
+ {
+ value: "aNImAl",
+ results: [0, 1, 0, 0, 0, 0, 0],
+ },
+ // Test numbers
+ {
+ value: "01",
+ results: [1, 0, 0, 0, 0, 0, 1],
+ },
+ {
+ value: "2016",
+ results: [0, 0, 0, 0, 0, 0, 1],
+ },
+ {
+ value: "56789",
+ results: [1, 0, 0, 0, 0, 0, 0],
+ },
+ // Test filtering by value
+ {
+ value: "horse",
+ results: [0, 1, 0, 0, 0, 0, 0],
+ },
+ {
+ value: "$$$",
+ results: [0, 0, 0, 0, 1, 0, 0],
+ },
+ {
+ value: "bar",
+ results: [0, 0, 1, 1, 0, 0, 0],
+ },
+ // Test input with whitespace
+ {
+ value: "energy b",
+ results: [0, 0, 1, 0, 0, 0, 0],
+ },
+ // Test no input at all
+ {
+ value: "",
+ results: [1, 1, 1, 1, 1, 1, 1],
+ },
+ // Test input that matches nothing
+ {
+ value: "input that matches nothing",
+ results: [0, 0, 0, 0, 0, 0, 0],
+ },
+ ];
+
+ const testcasesAfterHiding = [
+ // Test that search isn't case-sensitive
+ {
+ value: "OR",
+ results: [0, 0, 0, 0, 0, 1, 0],
+ },
+ {
+ value: "01",
+ results: [1, 0, 0, 0, 0, 0, 0],
+ },
+ {
+ value: "2016",
+ results: [0, 0, 0, 0, 0, 0, 0],
+ },
+ {
+ value: "56789",
+ results: [0, 0, 0, 0, 0, 0, 0],
+ },
+ // Test filtering by value
+ {
+ value: "horse",
+ results: [0, 0, 0, 0, 0, 0, 0],
+ },
+ {
+ value: "$$$",
+ results: [0, 0, 0, 0, 0, 0, 0],
+ },
+ {
+ value: "bar",
+ results: [0, 0, 0, 0, 0, 0, 0],
+ },
+ // Test input with whitespace
+ {
+ value: "energy b",
+ results: [0, 0, 0, 0, 0, 0, 0],
+ },
+ ];
+
+ runTests(testcases);
+ showColumn("value", false);
+ runTests(testcasesAfterHiding);
+});
+
+function runTests(testcases) {
+ const $$ = sel => gPanelWindow.document.querySelectorAll(sel);
+ const names = $$("#name .table-widget-cell");
+ const rows = $$("#value .table-widget-cell");
+ for (const testcase of testcases) {
+ const { value, results } = testcase;
+
+ info(`Testing input: ${value}`);
+
+ gUI.searchBox.value = value;
+ gUI.filterItems();
+
+ for (let i = 0; i < rows.length; i++) {
+ info(`Testing row ${i} for "${value}"`);
+ info(`key: ${names[i].value}, value: ${rows[i].value}`);
+ const state = results[i] ? "visible" : "hidden";
+ is(
+ rows[i].hasAttribute("hidden"),
+ !results[i],
+ `Row ${i} should be ${state}`
+ );
+ }
+ }
+}
diff --git a/devtools/client/storage/test/browser_storage_search_keyboard_trap.js b/devtools/client/storage/test/browser_storage_search_keyboard_trap.js
new file mode 100644
index 0000000000..21ccd12980
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_search_keyboard_trap.js
@@ -0,0 +1,15 @@
+// Test ability to focus search field by using keyboard
+"use strict";
+
+add_task(async function () {
+ await openTabAndSetupStorage(MAIN_DOMAIN_SECURED + "storage-search.html");
+
+ gUI.tree.expandAll();
+ await selectTreeItem(["localStorage", "https://test1.example.org"]);
+
+ await focusSearchBoxUsingShortcut(gPanelWindow);
+ ok(
+ containsFocus(gPanelWindow.document, gUI.searchBox),
+ "Focus is in a searchbox"
+ );
+});
diff --git a/devtools/client/storage/test/browser_storage_sessionstorage_add.js b/devtools/client/storage/test/browser_storage_sessionstorage_add.js
new file mode 100644
index 0000000000..9f5ed9476f
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_sessionstorage_add.js
@@ -0,0 +1,20 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Basic test to check the adding of sessionStorage entries.
+
+"use strict";
+
+add_task(async function () {
+ await openTabAndSetupStorage(
+ MAIN_DOMAIN_SECURED + "storage-sessionstorage.html"
+ );
+ showAllColumns(true);
+
+ await performAdd(["sessionStorage", "https://test1.example.org"]);
+ await performAdd(["sessionStorage", "https://test1.example.org"]);
+ await performAdd(["sessionStorage", "https://test1.example.org"]);
+ await performAdd(["sessionStorage", "https://test1.example.org"]);
+ await performAdd(["sessionStorage", "https://test1.example.org"]);
+});
diff --git a/devtools/client/storage/test/browser_storage_sessionstorage_edit.js b/devtools/client/storage/test/browser_storage_sessionstorage_edit.js
new file mode 100644
index 0000000000..51e9657585
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_sessionstorage_edit.js
@@ -0,0 +1,24 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Basic test to check the editing of localStorage.
+
+"use strict";
+
+add_task(async function () {
+ await openTabAndSetupStorage(
+ MAIN_DOMAIN_SECURED + "storage-sessionstorage.html"
+ );
+
+ await selectTreeItem(["sessionStorage", "https://test1.example.org"]);
+
+ await editCell("TestSS1", "name", "newTestSS1");
+ await editCell("newTestSS1", "value", "newValueSS1");
+
+ await editCell("TestSS3", "name", "newTestSS3");
+ await editCell("newTestSS3", "value", "newValueSS3");
+
+ await editCell("TestSS5", "name", "newTestSS5");
+ await editCell("newTestSS5", "value", "newValueSS5");
+});
diff --git a/devtools/client/storage/test/browser_storage_sessionstorage_navigation.js b/devtools/client/storage/test/browser_storage_sessionstorage_navigation.js
new file mode 100644
index 0000000000..22d6d5661f
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_sessionstorage_navigation.js
@@ -0,0 +1,60 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function () {
+ const URL1 = buildURLWithContent(
+ "example.com",
+ `<h1>example.com</h1>` +
+ `<script>sessionStorage.setItem("lorem", "ipsum");</script>`
+ );
+ const URL2 = buildURLWithContent(
+ "example.net",
+ `<h1>example.net</h1>` +
+ `<script>sessionStorage.setItem("foo", "bar");</script>`
+ );
+
+ // open tab
+ await openTabAndSetupStorage(URL1);
+ const doc = gPanelWindow.document;
+
+ // Check first domain
+ // check that both host appear in the storage tree
+ checkTree(doc, ["sessionStorage", "https://example.com"]);
+ // check the table for values
+ await selectTreeItem(["sessionStorage", "https://example.com"]);
+ checkStorageData("lorem", "ipsum");
+
+ // clear up session storage data before navigating
+ info("Cleaning up sessionStorage…");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ const win = content.wrappedJSObject;
+ await win.sessionStorage.clear();
+ });
+
+ // Check second domain
+ await navigateTo(URL2);
+ // wait for storage tree refresh, and check host
+ info("Waiting for storage tree to refresh and show correct host…");
+ await waitUntil(() =>
+ isInTree(doc, ["sessionStorage", "https://example.net"])
+ );
+
+ ok(
+ !isInTree(doc, ["sessionStorage", "https://example.com"]),
+ "example.com item is not in the tree anymore"
+ );
+
+ // check the table for values
+ await selectTreeItem(["sessionStorage", "https://example.net"]);
+ checkStorageData("foo", "bar");
+
+ info("Check that the sessionStorage node still has the expected label");
+ is(
+ getTreeNodeLabel(doc, ["sessionStorage"]),
+ "Session Storage",
+ "sessionStorage item is properly displayed"
+ );
+});
diff --git a/devtools/client/storage/test/browser_storage_sidebar.js b/devtools/client/storage/test/browser_storage_sidebar.js
new file mode 100644
index 0000000000..e55a0365f1
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_sidebar.js
@@ -0,0 +1,136 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Test to verify that the sidebar opens, closes and updates
+// This test is not testing the values in the sidebar, being tested in _values
+
+// Format: [
+// <id of the table item to click> or <id array for tree item to select> or
+// null to press Escape,
+// <do we wait for the async "sidebar-updated" event>,
+// <is the sidebar open>
+// ]
+
+"use strict";
+
+const testCases = [
+ {
+ location: ["cookies", "https://sectest1.example.org"],
+ sidebarHidden: true,
+ },
+ {
+ location: getCookieId("cs2", ".example.org", "/"),
+ sidebarHidden: false,
+ },
+ {
+ sendEscape: true,
+ },
+ {
+ location: getCookieId("cs2", ".example.org", "/"),
+ sidebarHidden: true,
+ },
+ {
+ location: getCookieId("uc1", ".example.org", "/"),
+ sidebarHidden: true,
+ },
+ {
+ location: getCookieId("uc1", ".example.org", "/"),
+ sidebarHidden: true,
+ },
+
+ {
+ location: ["localStorage", "http://sectest1.example.org"],
+ sidebarHidden: true,
+ },
+ {
+ location: "iframe-u-ls1",
+ sidebarHidden: false,
+ },
+ {
+ location: "iframe-u-ls1",
+ sidebarHidden: false,
+ },
+ {
+ sendEscape: true,
+ },
+
+ {
+ location: ["sessionStorage", "http://test1.example.org"],
+ sidebarHidden: true,
+ },
+ {
+ location: "ss1",
+ sidebarHidden: false,
+ },
+ {
+ sendEscape: true,
+ },
+
+ {
+ location: ["indexedDB", "http://test1.example.org"],
+ sidebarHidden: true,
+ },
+ {
+ location: "idb2 (default)",
+ sidebarHidden: false,
+ },
+
+ {
+ location: [
+ "indexedDB",
+ "http://test1.example.org",
+ "idb2 (default)",
+ "obj3",
+ ],
+ sidebarHidden: true,
+ },
+
+ {
+ location: ["indexedDB", "https://sectest1.example.org", "idb-s2 (default)"],
+ sidebarHidden: true,
+ },
+ {
+ location: "obj-s2",
+ sidebarHidden: false,
+ },
+ {
+ sendEscape: true,
+ },
+ {
+ location: "obj-s2",
+ sidebarHidden: true,
+ },
+];
+
+add_task(async function () {
+ // storage-listings.html explicitly mixes secure and insecure frames.
+ // We should not enforce https for tests using this page.
+ await pushPref("dom.security.https_first", false);
+
+ await openTabAndSetupStorage(MAIN_DOMAIN + "storage-listings.html");
+
+ for (const test of testCases) {
+ const { location, sidebarHidden, sendEscape } = test;
+
+ info("running " + JSON.stringify(test));
+
+ if (Array.isArray(location)) {
+ await selectTreeItem(location);
+ } else if (location) {
+ await selectTableItem(location);
+ }
+
+ if (sendEscape) {
+ EventUtils.sendKey("ESCAPE", gPanelWindow);
+ } else {
+ is(
+ gUI.sidebar.hidden,
+ sidebarHidden,
+ "correct visibility state of sidebar."
+ );
+ }
+
+ info("-".repeat(80));
+ }
+});
diff --git a/devtools/client/storage/test/browser_storage_sidebar_filter.js b/devtools/client/storage/test/browser_storage_sidebar_filter.js
new file mode 100644
index 0000000000..2efeb7ad8a
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_sidebar_filter.js
@@ -0,0 +1,43 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Test to verify that the filter value input works in the sidebar.
+
+"use strict";
+
+add_task(async function () {
+ await openTabAndSetupStorage(
+ MAIN_DOMAIN_SECURED + "storage-complex-values.html"
+ );
+
+ const updated = gUI.once("sidebar-updated");
+ await selectTreeItem(["localStorage", "https://test1.example.org"]);
+ await selectTableItem("ls1");
+ await updated;
+
+ const doc = gPanelWindow.document;
+
+ let properties = doc.querySelectorAll(`.variables-view-property`);
+ let unmatched = doc.querySelectorAll(`.variables-view-property[unmatched]`);
+ is(properties.length, 5, "5 properties in total before filtering");
+ is(unmatched.length, 0, "No unmatched properties before filtering");
+
+ info("Focus the filter input and type 'es6' to filter out entries");
+ doc.querySelector(".variables-view-searchinput").focus();
+ EventUtils.synthesizeKey("es6", {}, gPanelWindow);
+
+ await waitFor(
+ () =>
+ doc.querySelectorAll(`.variables-view-property[unmatched]`).length == 3
+ );
+
+ info("Updates performed, going to verify result");
+ properties = doc.querySelectorAll(`.variables-view-property`);
+ unmatched = doc.querySelectorAll(`.variables-view-property[unmatched]`);
+ is(properties.length, 5, "5 properties in total after filtering");
+ // Note: even though only one entry matches, since the VariablesView displays
+ // it as a tree, we also have one matched .variables-view-property for the
+ // parent.
+ is(unmatched.length, 3, "Three unmatched properties after filtering");
+});
diff --git a/devtools/client/storage/test/browser_storage_sidebar_parsetree.js b/devtools/client/storage/test/browser_storage_sidebar_parsetree.js
new file mode 100644
index 0000000000..e5c169dfbd
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_sidebar_parsetree.js
@@ -0,0 +1,115 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Test to verify that the sidebar parsetree is used for only values it makes sense to
+// parse into a tree.
+
+"use strict";
+
+const testCases = [
+ {
+ row: "ampersand",
+ parseTreeVisible: true,
+ },
+ {
+ row: "asterisk",
+ parseTreeVisible: true,
+ },
+ {
+ row: "base64",
+ parseTreeVisible: false,
+ },
+ {
+ row: "boolean",
+ parseTreeVisible: false,
+ },
+ {
+ row: "colon",
+ parseTreeVisible: true,
+ },
+ {
+ row: "color",
+ parseTreeVisible: false,
+ },
+ {
+ row: "comma",
+ parseTreeVisible: true,
+ },
+ {
+ row: "dataURI",
+ parseTreeVisible: false,
+ },
+ {
+ row: "date",
+ parseTreeVisible: false,
+ },
+ {
+ row: "email",
+ parseTreeVisible: false,
+ },
+ {
+ row: "equals",
+ parseTreeVisible: true,
+ },
+ {
+ row: "FQDN",
+ parseTreeVisible: false,
+ },
+ {
+ row: "hash",
+ parseTreeVisible: true,
+ },
+ {
+ row: "IP",
+ parseTreeVisible: false,
+ },
+ {
+ row: "MacAddress",
+ parseTreeVisible: false,
+ },
+ {
+ row: "maths",
+ parseTreeVisible: false,
+ },
+ {
+ row: "numbers",
+ parseTreeVisible: false,
+ },
+ {
+ row: "period",
+ parseTreeVisible: true,
+ },
+ {
+ row: "SemVer",
+ parseTreeVisible: false,
+ },
+ {
+ row: "tilde",
+ parseTreeVisible: true,
+ },
+ {
+ row: "URL",
+ parseTreeVisible: false,
+ },
+ {
+ row: "URL2",
+ parseTreeVisible: false,
+ },
+];
+
+add_task(async function () {
+ await openTabAndSetupStorage(
+ MAIN_DOMAIN_SECURED + "storage-sidebar-parsetree.html"
+ );
+
+ await selectTreeItem(["localStorage", "https://test1.example.org"]);
+
+ for (const test of testCases) {
+ const { parseTreeVisible, row } = test;
+
+ await selectTableItem(row);
+
+ sidebarParseTreeVisible(parseTreeVisible);
+ }
+});
diff --git a/devtools/client/storage/test/browser_storage_sidebar_toggle.js b/devtools/client/storage/test/browser_storage_sidebar_toggle.js
new file mode 100644
index 0000000000..3e27e23e88
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_sidebar_toggle.js
@@ -0,0 +1,65 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Test to verify that the sidebar toggles when the toggle button is clicked.
+
+"use strict";
+
+const testCases = [
+ {
+ location: ["cookies", "https://sectest1.example.org"],
+ sidebarHidden: true,
+ toggleButtonVisible: false,
+ },
+ {
+ location: getCookieId("cs2", ".example.org", "/"),
+ sidebarHidden: false,
+ toggleButtonVisible: true,
+ },
+ {
+ clickToggle: true,
+ },
+ {
+ location: getCookieId("cs2", ".example.org", "/"),
+ sidebarHidden: true,
+ },
+];
+
+add_task(async function () {
+ // storage-listings.html explicitly mixes secure and insecure frames.
+ // We should not enforce https for tests using this page.
+ await pushPref("dom.security.https_first", false);
+
+ await openTabAndSetupStorage(MAIN_DOMAIN + "storage-listings.html");
+
+ for (const test of testCases) {
+ const { location, sidebarHidden, clickToggle, toggleButtonVisible } = test;
+
+ info("running " + JSON.stringify(test));
+
+ if (Array.isArray(location)) {
+ await selectTreeItem(location);
+ } else if (location) {
+ await selectTableItem(location);
+ }
+
+ if (clickToggle) {
+ toggleSidebar();
+ } else if (typeof toggleButtonHidden !== "undefined") {
+ is(
+ sidebarToggleVisible(),
+ toggleButtonVisible,
+ "correct visibility state of toggle button"
+ );
+ } else {
+ is(
+ gUI.sidebar.hidden,
+ sidebarHidden,
+ "correct visibility state of sidebar."
+ );
+ }
+
+ info("-".repeat(80));
+ }
+});
diff --git a/devtools/client/storage/test/browser_storage_sidebar_update.js b/devtools/client/storage/test/browser_storage_sidebar_update.js
new file mode 100644
index 0000000000..b8f18d0ca7
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_sidebar_update.js
@@ -0,0 +1,45 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Test to verify that the sidebar is not broken when several updates
+// come in quick succession. See bug 1260380 - it could happen that the
+// "Parsed Value" section gets duplicated.
+
+"use strict";
+
+add_task(async function () {
+ const ITEM_NAME = "ls1";
+ const UPDATE_COUNT = 3;
+
+ await openTabAndSetupStorage(
+ MAIN_DOMAIN_SECURED + "storage-complex-values.html"
+ );
+
+ const updated = gUI.once("sidebar-updated");
+ await selectTreeItem(["localStorage", "https://test1.example.org"]);
+ await selectTableItem(ITEM_NAME);
+ await updated;
+
+ is(gUI.sidebar.hidden, false, "sidebar is visible");
+
+ // do several updates in a row and wait for them to finish
+ const updates = [];
+ for (let i = 0; i < UPDATE_COUNT; i++) {
+ info(`Performing update #${i}`);
+ updates.push(gUI.once("sidebar-updated"));
+ gUI.updateObjectSidebar();
+ }
+ await Promise.all(updates);
+
+ info("Updates performed, going to verify result");
+ const parsedScope = gUI.view.getScopeAtIndex(1);
+ const elements = parsedScope.target.querySelectorAll(
+ `.name[value="${ITEM_NAME}"]`
+ );
+ is(
+ elements.length,
+ 1,
+ `There is only one displayed variable named '${ITEM_NAME}'`
+ );
+});
diff --git a/devtools/client/storage/test/browser_storage_type_descriptions.js b/devtools/client/storage/test/browser_storage_type_descriptions.js
new file mode 100644
index 0000000000..cad28e569b
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_type_descriptions.js
@@ -0,0 +1,79 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Basic test to assert that the descriptions for the different storage types
+// are correctly displayed and the links referring to pages with further
+// information are set.
+
+"use strict";
+
+const getStorageTypeURL = require("resource://devtools/client/storage/utils/doc-utils.js");
+
+const storeItems = [
+ "Cache",
+ "cookies",
+ "indexedDB",
+ "localStorage",
+ "sessionStorage",
+];
+
+/**
+ * Test that the desired number of tree items are present
+ */
+function testTree() {
+ const doc = gPanelWindow.document;
+ for (const type of storeItems) {
+ ok(
+ doc.querySelector(`[data-id='${JSON.stringify([type])}']`),
+ `Tree item ${type} should be present in the storage tree`
+ );
+ }
+}
+
+/**
+ * Test that description is shown for each of the tree items
+ */
+const testDescriptions = async function () {
+ const doc = gPanelWindow.document;
+ const win = doc.defaultView;
+ // Expand all nodes so that the synthesized click event actually works
+ gUI.tree.expandAll();
+
+ // Click the tree items and wait for the content to be updated
+ for (const type of storeItems) {
+ await selectTreeItem([type]);
+
+ // Check whether the table is hidden
+ is(
+ win.getComputedStyle(doc.querySelector(".table-widget-body")).display,
+ "none",
+ "Table must be hidden"
+ );
+
+ // Check whether the description shown
+ is(
+ win.getComputedStyle(doc.querySelector(".table-widget-empty-text"))
+ .display,
+ "block",
+ "Description for the type must be shown"
+ );
+
+ // Check learn more link
+ const learnMoreLink = doc.querySelector(".table-widget-empty-text > a");
+ ok(learnMoreLink, "There is a [Learn more] link");
+ const expectedURL = getStorageTypeURL(type);
+ is(
+ learnMoreLink.href,
+ expectedURL,
+ `Learn more link refers to ${expectedURL}`
+ );
+ }
+};
+
+add_task(async function () {
+ await openTabAndSetupStorage(MAIN_DOMAIN + "storage-empty-objectstores.html");
+
+ testTree();
+ await testDescriptions();
+});
diff --git a/devtools/client/storage/test/browser_storage_values.js b/devtools/client/storage/test/browser_storage_values.js
new file mode 100644
index 0000000000..e069781546
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_values.js
@@ -0,0 +1,265 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Test to verify that the values shown in sidebar are correct
+
+// Format: [
+// <id of the table item to click> or <id array for tree item to select> or
+// null do click nothing,
+// null to skip checking value in variables view or a key value pair object
+// which will be asserted to exist in the storage sidebar,
+// true if the check is to be made in the parsed value section
+// ]
+
+"use strict";
+
+const LONG_WORD = "a".repeat(1000);
+
+const testCases = [
+ [
+ getCookieId("cs2", ".example.org", "/"),
+ [
+ { name: "cs2", value: "sessionCookie" },
+ { name: "cs2.Path", value: "/" },
+ { name: "cs2.HostOnly", value: "false" },
+ { name: "cs2.HttpOnly", value: "false" },
+ { name: "cs2.Domain", value: ".example.org" },
+ { name: "cs2.Expires / Max-Age", value: "Session" },
+ { name: "cs2.Secure", value: "false" },
+ ],
+ ],
+ [
+ getCookieId("c1", "test1.example.org", "/browser"),
+ [
+ { name: "c1", value: JSON.stringify(["foo", "Bar", { foo: "Bar" }]) },
+ { name: "c1.Path", value: "/browser" },
+ { name: "c1.HostOnly", value: "true" },
+ { name: "c1.HttpOnly", value: "false" },
+ { name: "c1.Domain", value: "test1.example.org" },
+ {
+ name: "c1.Expires / Max-Age",
+ value: new Date(2000000000000).toUTCString(),
+ },
+ { name: "c1.Secure", value: "false" },
+ ],
+ ],
+ [
+ null,
+ [
+ { name: "c1", value: "Array" },
+ { name: "c1.0", value: "foo" },
+ { name: "c1.1", value: "Bar" },
+ { name: "c1.2", value: "Object" },
+ { name: "c1.2.foo", value: "Bar" },
+ ],
+ true,
+ ],
+ [
+ getCookieId(
+ "c_encoded",
+ "test1.example.org",
+ "/browser/devtools/client/storage/test"
+ ),
+ [
+ {
+ name: "c_encoded",
+ value: encodeURIComponent(JSON.stringify({ foo: { foo1: "bar" } })),
+ },
+ ],
+ ],
+ [
+ null,
+ [
+ { name: "c_encoded", value: "Object" },
+ { name: "c_encoded.foo", value: "Object" },
+ { name: "c_encoded.foo.foo1", value: "bar" },
+ ],
+ true,
+ ],
+ [["localStorage", "https://test1.example.org"]],
+ ["ls2", [{ name: "ls2", value: "foobar-2" }]],
+ [
+ "ls1",
+ [
+ {
+ name: "ls1",
+ value: JSON.stringify({
+ es6: "for",
+ the: "win",
+ baz: [
+ 0,
+ 2,
+ 3,
+ {
+ deep: "down",
+ nobody: "cares",
+ },
+ ],
+ }),
+ },
+ ],
+ ],
+ [
+ null,
+ [
+ { name: "ls1", value: "Object" },
+ { name: "ls1.es6", value: "for" },
+ { name: "ls1.the", value: "win" },
+ { name: "ls1.baz", value: "Array" },
+ { name: "ls1.baz.0", value: "0" },
+ { name: "ls1.baz.1", value: "2" },
+ { name: "ls1.baz.2", value: "3" },
+ { name: "ls1.baz.3", value: "Object" },
+ { name: "ls1.baz.3.deep", value: "down" },
+ { name: "ls1.baz.3.nobody", value: "cares" },
+ ],
+ true,
+ ],
+ ["ls3", [{ name: "ls3", value: "http://foobar.com/baz.php" }]],
+ [
+ null,
+ [{ name: "ls3", value: "http://foobar.com/baz.php", dontMatch: true }],
+ true,
+ ],
+ ["ls4", [{ name: "ls4", value: "0x1" }], false],
+ [["sessionStorage", "https://test1.example.org"]],
+ ["ss1", [{ name: "ss1", value: "This#is#an#array" }]],
+ [
+ null,
+ [
+ { name: "ss1", value: "Array" },
+ { name: "ss1.0", value: "This" },
+ { name: "ss1.1", value: "is" },
+ { name: "ss1.2", value: "an" },
+ { name: "ss1.3", value: "array" },
+ ],
+ true,
+ ],
+ [
+ "ss2",
+ [
+ { name: "ss2", value: "Array" },
+ { name: "ss2.0", value: "This" },
+ { name: "ss2.1", value: "is" },
+ { name: "ss2.2", value: "another" },
+ { name: "ss2.3", value: "array" },
+ ],
+ true,
+ ],
+ [
+ "ss3",
+ [
+ { name: "ss3", value: "Object" },
+ { name: "ss3.this", value: "is" },
+ { name: "ss3.an", value: "object" },
+ { name: "ss3.foo", value: "bar" },
+ ],
+ true,
+ ],
+ [
+ "ss4",
+ [
+ { name: "ss4", value: "Array" },
+ { name: "ss4.0", value: "" },
+ { name: "ss4.1", value: "array" },
+ { name: "ss4.2", value: "" },
+ { name: "ss4.3", value: "with" },
+ { name: "ss4.4", value: "empty" },
+ { name: "ss4.5", value: "items" },
+ ],
+ true,
+ ],
+ [
+ "ss5",
+ [
+ { name: "ss5", value: "Array" },
+ { name: "ss5.0", value: LONG_WORD },
+ { name: "ss5.1", value: LONG_WORD },
+ { name: "ss5.2", value: LONG_WORD },
+ { name: "ss5.3", value: `${LONG_WORD}&${LONG_WORD}` },
+ { name: "ss5.4", value: `${LONG_WORD}&${LONG_WORD}` },
+ ],
+ true,
+ ],
+ [["indexedDB", "https://test1.example.org", "idb1 (default)", "obj1"]],
+ [
+ 1,
+ [
+ {
+ name: 1,
+ value: JSON.stringify({ id: 1, name: "foo", email: "foo@bar.com" }),
+ },
+ ],
+ ],
+ [
+ null,
+ [
+ { name: "1.id", value: "1" },
+ { name: "1.name", value: "foo" },
+ { name: "1.email", value: "foo@bar.com" },
+ ],
+ true,
+ ],
+ [["indexedDB", "https://test1.example.org", "idb1 (default)", "obj2"]],
+ [
+ 1,
+ [
+ {
+ name: 1,
+ value: JSON.stringify({
+ id2: 1,
+ name: "foo",
+ email: "foo@bar.com",
+ extra: "baz".repeat(10000),
+ }),
+ },
+ ],
+ ],
+ [
+ null,
+ [
+ { name: "1.id2", value: "1" },
+ { name: "1.name", value: "foo" },
+ { name: "1.email", value: "foo@bar.com" },
+ { name: "1.extra", value: "baz".repeat(10000) },
+ ],
+ true,
+ ],
+];
+
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.documentCookies.maxage", 0]],
+ });
+
+ await openTabAndSetupStorage(
+ MAIN_DOMAIN_SECURED + "storage-complex-values.html"
+ );
+
+ gUI.tree.expandAll();
+
+ for (const item of testCases) {
+ info("clicking for item " + item);
+ const [path, ruleArray, parsed] = item;
+ const start = performance.now();
+
+ if (Array.isArray(path)) {
+ await selectTreeItem(path);
+ continue;
+ } else if (path) {
+ await selectTableItem(path);
+ }
+
+ // Parsing "0x1" used to be very slow and memory intensive.
+ // Check that each test case completes in less than 15000ms.
+ const time = performance.now() - start;
+ Assert.less(
+ time,
+ 15000,
+ `item ${item} completed in less than 15000ms ${time}`
+ );
+
+ await findVariableViewProperties(ruleArray, parsed);
+ }
+});
diff --git a/devtools/client/storage/test/browser_storage_webext_storage_local.js b/devtools/client/storage/test/browser_storage_webext_storage_local.js
new file mode 100644
index 0000000000..3100a16f5b
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_webext_storage_local.js
@@ -0,0 +1,296 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* globals browser BigInt */
+
+"use strict";
+
+add_setup(async function () {
+ // Always on top mode mess up with toolbox focus and openStoragePanelForAddon would timeout
+ // waiting for toolbox focus.
+ await pushPref("devtools.toolbox.alwaysOnTop", false);
+});
+
+/**
+ * Since storage item values are represented in the client as strings in textboxes, not all
+ * JavaScript object types supported by the WE storage local API and its IndexedDB backend
+ * can be successfully stringified for display in the table much less parsed correctly when
+ * the user tries to edit a value in the panel. This test is expected to change over time
+ * as more and more value types are supported.
+ */
+add_task(
+ async function test_extension_toolbox_only_supported_values_editable() {
+ async function background() {
+ browser.test.onMessage.addListener(async (msg, ...args) => {
+ switch (msg) {
+ case "storage-local-set":
+ await browser.storage.local.set(args[0]);
+ break;
+ case "storage-local-get": {
+ const items = await browser.storage.local.get(args[0]);
+ for (const [key, val] of Object.entries(items)) {
+ browser.test.assertTrue(
+ val === args[1],
+ `New value ${val} is set for key ${key}.`
+ );
+ }
+ break;
+ }
+ case "storage-local-fireOnChanged": {
+ const listener = () => {
+ browser.storage.onChanged.removeListener(listener);
+ browser.test.sendMessage("storage-local-onChanged");
+ };
+ browser.storage.onChanged.addListener(listener);
+ // Call an API method implemented in the parent process
+ // to ensure that the listener has been registered
+ // in the main process before the test proceeds.
+ await browser.runtime.getPlatformInfo();
+ break;
+ }
+ default:
+ browser.test.fail(`Unexpected test message: ${msg}`);
+ }
+
+ browser.test.sendMessage(`${msg}:done`);
+ });
+ browser.test.sendMessage("extension-origin", window.location.origin);
+ }
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["storage"],
+ },
+ background,
+ useAddonManager: "temporary",
+ });
+
+ await extension.startup();
+
+ const host = await extension.awaitMessage("extension-origin");
+
+ const itemsSupported = {
+ arr: [1, 2],
+ bool: true,
+ null: null,
+ num: 4,
+ obj: { a: 123 },
+ str: "hi",
+ // Nested objects or arrays at most 2 levels deep should be editable
+ nestedArr: [
+ {
+ a: "b",
+ },
+ "c",
+ ],
+ nestedObj: {
+ a: [1, 2, "long-".repeat(10000)],
+ b: 3,
+ },
+ };
+
+ const itemsUnsupported = {
+ arrBuffer: new ArrayBuffer(8),
+ bigint: BigInt(1),
+ blob: new Blob(
+ [
+ JSON.stringify(
+ {
+ hello: "world",
+ },
+ null,
+ 2
+ ),
+ ],
+ {
+ type: "application/json",
+ }
+ ),
+ date: new Date(0),
+ map: new Map().set("a", "b"),
+ regexp: /regexp/,
+ set: new Set().add(1).add("a"),
+ undef: undefined,
+ // Arrays and object literals with non-JSONifiable values should not be editable
+ arrWithMap: [1, new Map().set("a", 1)],
+ objWithArrayBuffer: { a: new ArrayBuffer(8) },
+ // Nested objects or arrays more than 2 levels deep should not be editable
+ deepNestedArr: [[{ a: "b" }, 3], 4],
+ deepNestedObj: {
+ a: {
+ b: [1, 2],
+ },
+ },
+ };
+
+ info("Add storage items from the extension");
+ const allItems = { ...itemsSupported, ...itemsUnsupported };
+ extension.sendMessage("storage-local-fireOnChanged");
+ await extension.awaitMessage("storage-local-fireOnChanged:done");
+ extension.sendMessage("storage-local-set", allItems);
+ info(
+ "Wait for the extension to add storage items and receive the 'onChanged' event"
+ );
+ await extension.awaitMessage("storage-local-set:done");
+ await extension.awaitMessage("storage-local-onChanged");
+
+ info("Open the addon toolbox storage panel");
+ const { toolbox } = await openStoragePanelForAddon(extension.id);
+
+ await selectTreeItem(["extensionStorage", host]);
+ await waitForStorageData("str", "hi");
+
+ info("Verify that values are displayed as expected in the sidebar");
+ const expectedRenderedData = {
+ arr: {
+ sidebarItems: [
+ { name: "arr", value: "Array" },
+ { name: "arr.0", value: "1" },
+ { name: "arr.1", value: "2" },
+ ],
+ parsed: true,
+ },
+ arrBuffer: {
+ sidebarItems: [{ name: "arrBuffer", value: "Object" }],
+ parsed: true,
+ },
+ arrWithMap: {
+ sidebarItems: [
+ { name: "arrWithMap", value: "Array" },
+ { name: "arrWithMap.0", value: "1" },
+ { name: "arrWithMap.1", value: "Object" },
+ ],
+ parsed: true,
+ },
+ bigint: { sidebarItems: [{ name: "bigint", value: "1n" }] },
+ blob: { sidebarItems: [{ name: "blob", value: "Object" }], parsed: true },
+ bool: {
+ sidebarItems: [{ name: "bool", value: "true" }],
+ },
+ date: {
+ sidebarItems: [{ name: "date", value: "1970-01-01T00:00:00.000Z" }],
+ },
+ deepNestedArr: {
+ sidebarItems: [
+ { name: "deepNestedArr", value: "Array" },
+ { name: "deepNestedArr.0", value: "Array" },
+ { name: "deepNestedArr.1", value: "4" },
+ { name: "deepNestedArr.length", value: "2" },
+ ],
+ parsed: true,
+ },
+ deepNestedObj: {
+ sidebarItems: [
+ { name: "deepNestedObj", value: "Object" },
+ { name: "deepNestedObj.a", value: "Object" },
+ ],
+ parsed: true,
+ },
+ map: { sidebarItems: [{ name: "map", value: "Object" }], parsed: true },
+ nestedArr: {
+ sidebarItems: [
+ { name: "nestedArr", value: "Array" },
+ { name: "nestedArr.0", value: "Object" },
+ { name: "nestedArr.0.a", value: "b" },
+ { name: "nestedArr.1", value: "c" },
+ ],
+ parsed: true,
+ },
+ nestedObj: {
+ sidebarItems: [
+ { name: "nestedObj", value: "Object" },
+ { name: "nestedObj.a", value: "Array" },
+ { name: "nestedObj.a.0", value: "1" },
+ { name: "nestedObj.a.1", value: "2" },
+ { name: "nestedObj.a.2", value: "long-".repeat(10000) },
+ { name: "nestedObj.b", value: "3" },
+ ],
+ parsed: true,
+ },
+ null: {
+ sidebarItems: [{ name: "null", value: "null" }],
+ },
+ num: {
+ sidebarItems: [{ name: "num", value: itemsSupported.num }],
+ },
+ obj: {
+ sidebarItems: [
+ { name: "obj", value: "Object" },
+ { name: "obj.a", value: "123" },
+ ],
+ parsed: true,
+ },
+ objWithArrayBuffer: {
+ sidebarItems: [
+ { name: "objWithArrayBuffer", value: "Object" },
+ { name: "objWithArrayBuffer.a", value: "Object" },
+ ],
+ parsed: true,
+ },
+ regexp: {
+ sidebarItems: [{ name: "regexp", value: "Object" }],
+ parsed: true,
+ },
+ set: { sidebarItems: [{ name: "set", value: "Object" }], parsed: true },
+ str: {
+ sidebarItems: [{ name: "str", value: itemsSupported.str }],
+ },
+
+ undef: { sidebarItems: [{ name: "undef", value: "undefined" }] },
+ };
+
+ for (const [id, { sidebarItems, parsed }] of Object.entries(
+ expectedRenderedData
+ )) {
+ info(`Verify "${id}" entry`);
+ await selectTableItem(id);
+ await findVariableViewProperties(sidebarItems, parsed);
+ }
+
+ info("Verify that value types supported by the storage actor are editable");
+ let validate = true;
+ const newValue = "anotherValue";
+ const supportedIds = Object.keys(itemsSupported);
+
+ for (const id of supportedIds) {
+ startCellEdit(id, "value", newValue);
+ await editCell(id, "value", newValue, validate);
+ }
+
+ info("Verify that associated values have been changed in the extension");
+ extension.sendMessage(
+ "storage-local-get",
+ Object.keys(itemsSupported),
+ newValue
+ );
+ await extension.awaitMessage("storage-local-get:done");
+
+ info(
+ "Verify that value types not supported by the storage actor are uneditable"
+ );
+ const expectedValStrings = {
+ arrBuffer: "{}",
+ bigint: "1n",
+ blob: "{}",
+ date: "1970-01-01T00:00:00.000Z",
+ map: "{}",
+ regexp: "{}",
+ set: "{}",
+ undef: "undefined",
+ arrWithMap: "[1,{}]",
+ objWithArrayBuffer: '{"a":{}}',
+ deepNestedArr: '[[{"a":"b"},3],4]',
+ deepNestedObj: '{"a":{"b":[1,2]}}',
+ };
+ validate = false;
+ for (const id of Object.keys(itemsUnsupported)) {
+ startCellEdit(id, "value", validate);
+ checkCellUneditable(id, "value");
+ checkCell(id, "value", expectedValStrings[id]);
+ }
+
+ info("Shut down the test");
+ await toolbox.destroy();
+ await extension.unload();
+ }
+);
diff --git a/devtools/client/storage/test/head.js b/devtools/client/storage/test/head.js
new file mode 100644
index 0000000000..f50f573342
--- /dev/null
+++ b/devtools/client/storage/test/head.js
@@ -0,0 +1,1186 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+/* import-globals-from ../../shared/test/shared-head.js */
+
+// Sometimes HTML pages have a `clear` function that cleans up the storage they
+// created. To make sure it's always called, we are registering as a cleanup
+// function, but since this needs to run before tabs are closed, we need to
+// do this registration before importing `shared-head`, since declaration
+// order matters.
+registerCleanupFunction(async () => {
+ const browser = gBrowser.selectedBrowser;
+ const contexts = browser.browsingContext.getAllBrowsingContextsInSubtree();
+ for (const context of contexts) {
+ await SpecialPowers.spawn(context, [], async () => {
+ const win = content.wrappedJSObject;
+
+ // Some windows (e.g., about: URLs) don't have storage available
+ try {
+ win.localStorage.clear();
+ win.sessionStorage.clear();
+ } catch (ex) {
+ // ignore
+ }
+
+ if (win.clear) {
+ // Do not get hung into win.clear() forever
+ await Promise.race([
+ new Promise(r => win.setTimeout(r, 10000)),
+ win.clear(),
+ ]);
+ }
+ });
+ }
+
+ Services.cookies.removeAll();
+
+ // Close tabs and force memory collection to happen
+ while (gBrowser.tabs.length > 1) {
+ await closeTabAndToolbox(gBrowser.selectedTab);
+ }
+ forceCollections();
+});
+
+// shared-head.js handles imports, constants, and utility functions
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
+ this
+);
+
+const {
+ TableWidget,
+} = require("resource://devtools/client/shared/widgets/TableWidget.js");
+const {
+ LocalTabCommandsFactory,
+} = require("resource://devtools/client/framework/local-tab-commands-factory.js");
+const STORAGE_PREF = "devtools.storage.enabled";
+const DUMPEMIT_PREF = "devtools.dump.emit";
+const DEBUGGERLOG_PREF = "devtools.debugger.log";
+
+// Allows Cache API to be working on usage `http` test page
+const CACHES_ON_HTTP_PREF = "dom.caches.testing.enabled";
+const PATH = "browser/devtools/client/storage/test/";
+const MAIN_DOMAIN = "http://test1.example.org/" + PATH;
+const MAIN_DOMAIN_SECURED = "https://test1.example.org/" + PATH;
+const MAIN_DOMAIN_WITH_PORT = "http://test1.example.org:8000/" + PATH;
+const ALT_DOMAIN = "http://sectest1.example.org/" + PATH;
+const ALT_DOMAIN_SECURED = "https://sectest1.example.org:443/" + PATH;
+
+// GUID to be used as a separator in compound keys. This must match the same
+// constant in devtools/server/actors/resources/storage/index.js,
+// devtools/client/storage/ui.js and devtools/server/tests/browser/head.js
+const SEPARATOR_GUID = "{9d414cc5-8319-0a04-0586-c0a6ae01670a}";
+
+var gToolbox, gPanelWindow, gUI;
+
+// Services.prefs.setBoolPref(DUMPEMIT_PREF, true);
+// Services.prefs.setBoolPref(DEBUGGERLOG_PREF, true);
+
+Services.prefs.setBoolPref(STORAGE_PREF, true);
+Services.prefs.setBoolPref(CACHES_ON_HTTP_PREF, true);
+registerCleanupFunction(() => {
+ gToolbox = gPanelWindow = gUI = null;
+ Services.prefs.clearUserPref(CACHES_ON_HTTP_PREF);
+ Services.prefs.clearUserPref(DEBUGGERLOG_PREF);
+ Services.prefs.clearUserPref(DUMPEMIT_PREF);
+ Services.prefs.clearUserPref(STORAGE_PREF);
+});
+
+/**
+ * This generator function opens the given url in a new tab, then sets up the
+ * page by waiting for all cookies, indexedDB items etc.
+ *
+ * @param url {String} The url to be opened in the new tab
+ * @param options {Object} The tab options for the new tab
+ *
+ * @return {Promise} A promise that resolves after the tab is ready
+ */
+async function openTab(url, options = {}) {
+ const tab = await addTab(url, options);
+
+ const browser = gBrowser.selectedBrowser;
+ const contexts = browser.browsingContext.getAllBrowsingContextsInSubtree();
+
+ for (const context of contexts) {
+ await SpecialPowers.spawn(context, [], async () => {
+ const win = content.wrappedJSObject;
+ const readyState = win.document.readyState;
+ info(`Found a window: ${readyState}`);
+ if (readyState != "complete") {
+ await new Promise(resolve => {
+ const onLoad = () => {
+ win.removeEventListener("load", onLoad);
+ resolve();
+ };
+ win.addEventListener("load", onLoad);
+ });
+ }
+ if (win.setup) {
+ await win.setup();
+ }
+ });
+ }
+
+ return tab;
+}
+
+/**
+ * This generator function opens the given url in a new tab, then sets up the
+ * page by waiting for all cookies, indexedDB items etc. to be created; Then
+ * opens the storage inspector and waits for the storage tree and table to be
+ * populated.
+ *
+ * @param url {String} The url to be opened in the new tab
+ * @param options {Object} The tab options for the new tab
+ *
+ * @return {Promise} A promise that resolves after storage inspector is ready
+ */
+async function openTabAndSetupStorage(url, options = {}) {
+ // open tab
+ await openTab(url, options);
+
+ // open storage inspector
+ return openStoragePanel();
+}
+
+/**
+ * Open a toolbox with the storage panel opened by default
+ * for a given Web Extension.
+ *
+ * @param {String} addonId
+ * The ID of the Web Extension to debug.
+ */
+var openStoragePanelForAddon = async function (addonId) {
+ const toolbox = await gDevTools.showToolboxForWebExtension(addonId, {
+ toolId: "storage",
+ });
+
+ info("Making sure that the toolbox's frame is focused");
+ await SimpleTest.promiseFocus(toolbox.win);
+
+ const storage = _setupStoragePanelForTest(toolbox);
+
+ return {
+ toolbox,
+ storage,
+ };
+};
+
+/**
+ * Open the toolbox, with the storage tool visible.
+ *
+ * @param tab {XULTab} Optional, the tab for the toolbox; defaults to selected tab
+ * @param commands {Object} Optional, the commands for the toolbox; defaults to a tab commands
+ * @param hostType {Toolbox.HostType} Optional, type of host that will host the toolbox
+ *
+ * @return {Promise} a promise that resolves when the storage inspector is ready
+ */
+var openStoragePanel = async function ({ tab, hostType } = {}) {
+ const toolbox = await openToolboxForTab(
+ tab || gBrowser.selectedTab,
+ "storage",
+ hostType
+ );
+
+ const storage = _setupStoragePanelForTest(toolbox);
+
+ return {
+ toolbox,
+ storage,
+ };
+};
+
+/**
+ * Set global variables needed in helper functions
+ *
+ * @param toolbox {Toolbox}
+ * @return {StoragePanel}
+ */
+function _setupStoragePanelForTest(toolbox) {
+ const storage = toolbox.getPanel("storage");
+ gPanelWindow = storage.panelWindow;
+ gUI = storage.UI;
+ gToolbox = toolbox;
+
+ // The table animation flash causes some timeouts on Linux debug tests,
+ // so we disable it
+ gUI.animationsEnabled = false;
+
+ return storage;
+}
+
+/**
+ * Forces GC, CC and Shrinking GC to get rid of disconnected docshells and
+ * windows.
+ */
+function forceCollections() {
+ Cu.forceGC();
+ Cu.forceCC();
+ Cu.forceShrinkingGC();
+}
+
+// Sends a click event on the passed DOM node in an async manner
+function click(node) {
+ node.scrollIntoView();
+
+ return new Promise(resolve => {
+ // We need setTimeout here to allow any scrolling to complete before clicking
+ // the node.
+ setTimeout(() => {
+ node.click();
+ resolve();
+ }, 200);
+ });
+}
+
+/**
+ * Recursively expand the variables view up to a given property.
+ *
+ * @param options
+ * Options for view expansion:
+ * - rootVariable: start from the given scope/variable/property.
+ * - expandTo: string made up of property names you want to expand.
+ * For example: "body.firstChild.nextSibling" given |rootVariable:
+ * document|.
+ * @return object
+ * A promise that is resolved only when the last property in |expandTo|
+ * is found, and rejected otherwise. Resolution reason is always the
+ * last property - |nextSibling| in the example above. Rejection is
+ * always the last property that was found.
+ */
+function variablesViewExpandTo(options) {
+ const root = options.rootVariable;
+ const expandTo = options.expandTo.split(".");
+
+ return new Promise((resolve, reject) => {
+ function getNext(prop) {
+ const name = expandTo.shift();
+ const newProp = prop.get(name);
+
+ if (expandTo.length) {
+ ok(newProp, "found property " + name);
+ if (newProp && newProp.expand) {
+ newProp.expand();
+ getNext(newProp);
+ } else {
+ reject(prop);
+ }
+ } else if (newProp) {
+ resolve(newProp);
+ } else {
+ reject(prop);
+ }
+ }
+
+ if (root && root.expand) {
+ root.expand();
+ getNext(root);
+ } else {
+ resolve(root);
+ }
+ });
+}
+
+/**
+ * Find variables or properties in a VariablesView instance.
+ *
+ * @param array ruleArray
+ * The array of rules you want to match. Each rule is an object with:
+ * - name (string|regexp): property name to match.
+ * - value (string|regexp): property value to match.
+ * - dontMatch (boolean): make sure the rule doesn't match any property.
+ * @param boolean parsed
+ * true if we want to test the rules in the parse value section of the
+ * storage sidebar
+ * @return object
+ * A promise object that is resolved when all the rules complete
+ * matching. The resolved callback is given an array of all the rules
+ * you wanted to check. Each rule has a new property: |matchedProp|
+ * which holds a reference to the Property object instance from the
+ * VariablesView. If the rule did not match, then |matchedProp| is
+ * undefined.
+ */
+function findVariableViewProperties(ruleArray, parsed) {
+ // Initialize the search.
+ function init() {
+ // If parsed is true, we are checking rules in the parsed value section of
+ // the storage sidebar. That scope uses a blank variable as a placeholder
+ // Thus, adding a blank parent to each name
+ if (parsed) {
+ ruleArray = ruleArray.map(({ name, value, dontMatch }) => {
+ return { name: "." + name, value, dontMatch };
+ });
+ }
+ // Separate out the rules that require expanding properties throughout the
+ // view.
+ const expandRules = [];
+ const rules = ruleArray.filter(rule => {
+ if (typeof rule.name == "string" && rule.name.indexOf(".") > -1) {
+ expandRules.push(rule);
+ return false;
+ }
+ return true;
+ });
+
+ // Search through the view those rules that do not require any properties to
+ // be expanded. Build the array of matchers, outstanding promises to be
+ // resolved.
+ const outstanding = [];
+
+ finder(rules, gUI.view, outstanding);
+
+ // Process the rules that need to expand properties.
+ const lastStep = processExpandRules.bind(null, expandRules);
+
+ // Return the results - a promise resolved to hold the updated ruleArray.
+ const returnResults = onAllRulesMatched.bind(null, ruleArray);
+
+ return Promise.all(outstanding).then(lastStep).then(returnResults);
+ }
+
+ function onMatch(prop, rule, matched) {
+ if (matched && !rule.matchedProp) {
+ rule.matchedProp = prop;
+ }
+ }
+
+ function finder(rules, view, promises) {
+ for (const scope of view) {
+ for (const [, prop] of scope) {
+ for (const rule of rules) {
+ const matcher = matchVariablesViewProperty(prop, rule);
+ promises.push(matcher.then(onMatch.bind(null, prop, rule)));
+ }
+ }
+ }
+ }
+
+ function processExpandRules(rules) {
+ return new Promise(resolve => {
+ const rule = rules.shift();
+ if (!rule) {
+ resolve(null);
+ }
+
+ const expandOptions = {
+ rootVariable: gUI.view.getScopeAtIndex(parsed ? 1 : 0),
+ expandTo: rule.name,
+ };
+
+ variablesViewExpandTo(expandOptions)
+ .then(
+ function onSuccess(prop) {
+ const name = rule.name;
+ const lastName = name.split(".").pop();
+ rule.name = lastName;
+
+ const matched = matchVariablesViewProperty(prop, rule);
+ return matched
+ .then(onMatch.bind(null, prop, rule))
+ .then(function () {
+ rule.name = name;
+ });
+ },
+ function onFailure() {
+ resolve(null);
+ }
+ )
+ .then(processExpandRules.bind(null, rules))
+ .then(function () {
+ resolve(null);
+ });
+ });
+ }
+
+ function onAllRulesMatched(rules) {
+ for (const rule of rules) {
+ const matched = rule.matchedProp;
+ if (matched && !rule.dontMatch) {
+ ok(true, "rule " + rule.name + " matched for property " + matched.name);
+ } else if (matched && rule.dontMatch) {
+ ok(
+ false,
+ "rule " + rule.name + " should not match property " + matched.name
+ );
+ } else {
+ ok(rule.dontMatch, "rule " + rule.name + " did not match any property");
+ }
+ }
+ return rules;
+ }
+
+ return init();
+}
+
+/**
+ * Check if a given Property object from the variables view matches the given
+ * rule.
+ *
+ * @param object prop
+ * The variable's view Property instance.
+ * @param object rule
+ * Rules for matching the property. See findVariableViewProperties() for
+ * details.
+ * @return object
+ * A promise that is resolved when all the checks complete. Resolution
+ * result is a boolean that tells your promise callback the match
+ * result: true or false.
+ */
+function matchVariablesViewProperty(prop, rule) {
+ function resolve(result) {
+ return Promise.resolve(result);
+ }
+
+ if (!prop) {
+ return resolve(false);
+ }
+
+ // Any kind of string is accepted as name, including empty ones
+ if (typeof rule.name == "string") {
+ const match =
+ rule.name instanceof RegExp
+ ? rule.name.test(prop.name)
+ : prop.name == rule.name;
+ if (!match) {
+ return resolve(false);
+ }
+ }
+
+ if ("value" in rule) {
+ let displayValue = prop.displayValue;
+ if (prop.displayValueClassName == "token-string") {
+ displayValue = displayValue.substring(1, displayValue.length - 1);
+ }
+
+ const match =
+ rule.value instanceof RegExp
+ ? rule.value.test(displayValue)
+ : displayValue == rule.value;
+ if (!match) {
+ info(
+ "rule " +
+ rule.name +
+ " did not match value, expected '" +
+ rule.value +
+ "', found '" +
+ displayValue +
+ "'"
+ );
+ return resolve(false);
+ }
+ }
+
+ return resolve(true);
+}
+
+/**
+ * Click selects a row in the table.
+ *
+ * @param {[String]} ids
+ * The array id of the item in the tree
+ */
+async function selectTreeItem(ids) {
+ if (gUI.tree.isSelected(ids)) {
+ info(`"${ids}" is already selected, returning.`);
+ return;
+ }
+ if (!gUI.tree.exists(ids)) {
+ info(`"${ids}" does not exist, returning.`);
+ return;
+ }
+
+ // The item exists but is not selected... select it.
+ info(`Selecting "${ids}".`);
+ if (ids.length > 1) {
+ const updated = gUI.once("store-objects-updated");
+ gUI.tree.selectedItem = ids;
+ await updated;
+ } else {
+ // If the length of the IDs array is 1, a storage type
+ // gets selected and no 'store-objects-updated' event
+ // will be fired in that case.
+ gUI.tree.selectedItem = ids;
+ }
+}
+
+/**
+ * Click selects a row in the table.
+ *
+ * @param {String} id
+ * The id of the row in the table widget
+ */
+async function selectTableItem(id) {
+ const table = gUI.table;
+ const selector =
+ ".table-widget-column#" +
+ table.uniqueId +
+ " .table-widget-cell[value='" +
+ id +
+ "']";
+ const target = gPanelWindow.document.querySelector(selector);
+
+ ok(target, `row found with id "${id}"`);
+
+ if (!target) {
+ showAvailableIds();
+ }
+
+ const updated = gUI.once("sidebar-updated");
+
+ info(`selecting row "${id}"`);
+ await click(target);
+ await updated;
+}
+
+/**
+ * Wait for eventName on target.
+ * @param {Object} target An observable object that either supports on/off or
+ * addEventListener/removeEventListener
+ * @param {String} eventName
+ * @param {Boolean} useCapture Optional, for addEventListener/removeEventListener
+ * @return A promise that resolves when the event has been handled
+ */
+function once(target, eventName, useCapture = false) {
+ info("Waiting for event: '" + eventName + "' on " + target + ".");
+
+ return new Promise(resolve => {
+ for (const [add, remove] of [
+ ["addEventListener", "removeEventListener"],
+ ["addListener", "removeListener"],
+ ["on", "off"],
+ ]) {
+ if (add in target && remove in target) {
+ target[add](
+ eventName,
+ function onEvent(...aArgs) {
+ info("Got event: '" + eventName + "' on " + target + ".");
+ target[remove](eventName, onEvent, useCapture);
+ resolve(...aArgs);
+ },
+ useCapture
+ );
+ break;
+ }
+ }
+ });
+}
+
+/**
+ * Get values for a row.
+ *
+ * @param {String} id
+ * The uniqueId of the given row.
+ * @param {Boolean} includeHidden
+ * Include hidden columns.
+ *
+ * @return {Object}
+ * An object of column names to values for the given row.
+ */
+function getRowValues(id, includeHidden = false) {
+ const cells = getRowCells(id, includeHidden);
+ const values = {};
+
+ for (const name in cells) {
+ const cell = cells[name];
+
+ values[name] = cell.value;
+ }
+
+ return values;
+}
+
+/**
+ * Get cells for a row.
+ *
+ * @param {String} id
+ * The uniqueId of the given row.
+ * @param {Boolean} includeHidden
+ * Include hidden columns.
+ *
+ * @return {Object}
+ * An object of column names to cells for the given row.
+ */
+function getRowCells(id, includeHidden = false) {
+ const doc = gPanelWindow.document;
+ const table = gUI.table;
+ const item = doc.querySelector(
+ ".table-widget-column#" +
+ table.uniqueId +
+ " .table-widget-cell[value='" +
+ id +
+ "']"
+ );
+
+ if (!item) {
+ ok(
+ false,
+ `The row id '${id}' that was passed to getRowCells() does not ` +
+ `exist. ${getAvailableIds()}`
+ );
+ }
+
+ const index = table.columns.get(table.uniqueId).cellNodes.indexOf(item);
+ const cells = {};
+
+ for (const [name, column] of [...table.columns]) {
+ if (!includeHidden && column.column.parentNode.hidden) {
+ continue;
+ }
+ cells[name] = column.cellNodes[index];
+ }
+
+ return cells;
+}
+
+/**
+ * Check for an empty table.
+ */
+function isTableEmpty() {
+ const doc = gPanelWindow.document;
+ const table = gUI.table;
+ const cells = doc.querySelectorAll(
+ ".table-widget-column#" + table.uniqueId + " .table-widget-cell"
+ );
+ return cells.length === 0;
+}
+
+/**
+ * Get available ids... useful for error reporting.
+ */
+function getAvailableIds() {
+ const doc = gPanelWindow.document;
+ const table = gUI.table;
+
+ let out = "Available ids:\n";
+ const cells = doc.querySelectorAll(
+ ".table-widget-column#" + table.uniqueId + " .table-widget-cell"
+ );
+ for (const cell of cells) {
+ out += ` - ${cell.getAttribute("value")}\n`;
+ }
+
+ return out;
+}
+
+/**
+ * Show available ids.
+ */
+function showAvailableIds() {
+ info(getAvailableIds());
+}
+
+/**
+ * Get a cell value.
+ *
+ * @param {String} id
+ * The uniqueId of the row.
+ * @param {String} column
+ * The id of the column
+ *
+ * @yield {String}
+ * The cell value.
+ */
+function getCellValue(id, column) {
+ const row = getRowValues(id, true);
+
+ if (typeof row[column] === "undefined") {
+ let out = "";
+ for (const key in row) {
+ const value = row[key];
+
+ out += ` - ${key} = ${value}\n`;
+ }
+
+ ok(
+ false,
+ `The column name '${column}' that was passed to ` +
+ `getCellValue() does not exist. Current column names and row ` +
+ `values are:\n${out}`
+ );
+ }
+
+ return row[column];
+}
+
+/**
+ * Edit a cell value. The cell is assumed to be in edit mode, see startCellEdit.
+ *
+ * @param {String} id
+ * The uniqueId of the row.
+ * @param {String} column
+ * The id of the column
+ * @param {String} newValue
+ * Replacement value.
+ * @param {Boolean} validate
+ * Validate result? Default true.
+ *
+ * @yield {String}
+ * The uniqueId of the changed row.
+ */
+async function editCell(id, column, newValue, validate = true) {
+ const row = getRowCells(id, true);
+ const editableFieldsEngine = gUI.table._editableFieldsEngine;
+
+ editableFieldsEngine.edit(row[column]);
+
+ await typeWithTerminator(newValue, "KEY_Enter", validate);
+}
+
+/**
+ * Begin edit mode for a cell.
+ *
+ * @param {String} id
+ * The uniqueId of the row.
+ * @param {String} column
+ * The id of the column
+ * @param {Boolean} selectText
+ * Select text? Default true.
+ */
+function startCellEdit(id, column, selectText = true) {
+ const row = getRowCells(id, true);
+ const editableFieldsEngine = gUI.table._editableFieldsEngine;
+ const cell = row[column];
+
+ info("Selecting row " + id);
+ gUI.table.selectedRow = id;
+
+ info("Starting cell edit (" + id + ", " + column + ")");
+ editableFieldsEngine.edit(cell);
+
+ if (!selectText) {
+ const textbox = gUI.table._editableFieldsEngine.textbox;
+ textbox.selectionEnd = textbox.selectionStart;
+ }
+}
+
+/**
+ * Check a cell value.
+ *
+ * @param {String} id
+ * The uniqueId of the row.
+ * @param {String} column
+ * The id of the column
+ * @param {String} expected
+ * Expected value.
+ */
+function checkCell(id, column, expected) {
+ is(
+ getCellValue(id, column),
+ expected,
+ column + " column has the right value for " + id
+ );
+}
+
+/**
+ * Check that a cell is not in edit mode.
+ *
+ * @param {String} id
+ * The uniqueId of the row.
+ * @param {String} column
+ * The id of the column
+ */
+function checkCellUneditable(id, column) {
+ const row = getRowCells(id, true);
+ const cell = row[column];
+
+ const editableFieldsEngine = gUI.table._editableFieldsEngine;
+ const textbox = editableFieldsEngine.textbox;
+
+ // When a field is being edited, the cell is hidden, and the textbox is made visible.
+ ok(
+ !cell.hidden && textbox.hidden,
+ `The cell located in column ${column} and row ${id} is not editable.`
+ );
+}
+
+/**
+ * Show or hide a column.
+ *
+ * @param {String} id
+ * The uniqueId of the given column.
+ * @param {Boolean} state
+ * true = show, false = hide
+ */
+function showColumn(id, state) {
+ const columns = gUI.table.columns;
+ const column = columns.get(id);
+ column.column.hidden = !state;
+}
+
+/**
+ * Toggle sort direction on a column by clicking on the column header.
+ *
+ * @param {String} id
+ * The uniqueId of the given column.
+ */
+function clickColumnHeader(id) {
+ const columns = gUI.table.columns;
+ const column = columns.get(id);
+ const header = column.header;
+
+ header.click();
+}
+
+/**
+ * Show or hide all columns.
+ *
+ * @param {Boolean} state
+ * true = show, false = hide
+ */
+function showAllColumns(state) {
+ const columns = gUI.table.columns;
+
+ for (const [id] of columns) {
+ showColumn(id, state);
+ }
+}
+
+/**
+ * Type a string in the currently selected editor and then wait for the row to
+ * be updated.
+ *
+ * @param {String} str
+ * The string to type.
+ * @param {String} terminator
+ * The terminating key e.g. KEY_Enter or KEY_Tab
+ * @param {Boolean} validate
+ * Validate result? Default true.
+ */
+async function typeWithTerminator(str, terminator, validate = true) {
+ const editableFieldsEngine = gUI.table._editableFieldsEngine;
+ const textbox = editableFieldsEngine.textbox;
+ const colName = textbox.closest(".table-widget-column").id;
+
+ const changeExpected = str !== textbox.value;
+
+ if (!changeExpected) {
+ return editableFieldsEngine.currentTarget.getAttribute("data-id");
+ }
+
+ info("Typing " + str);
+ EventUtils.sendString(str, gPanelWindow);
+
+ info("Pressing " + terminator);
+ EventUtils.synthesizeKey(terminator, null, gPanelWindow);
+
+ if (validate) {
+ info("Validating results... waiting for ROW_EDIT event.");
+ const uniqueId = await gUI.table.once(TableWidget.EVENTS.ROW_EDIT);
+
+ checkCell(uniqueId, colName, str);
+ return uniqueId;
+ }
+
+ return gUI.table.once(TableWidget.EVENTS.ROW_EDIT);
+}
+
+function getCurrentEditorValue() {
+ const editableFieldsEngine = gUI.table._editableFieldsEngine;
+ const textbox = editableFieldsEngine.textbox;
+
+ return textbox.value;
+}
+
+/**
+ * Press a key x times.
+ *
+ * @param {String} key
+ * The key to press e.g. VK_RETURN or VK_TAB
+ * @param {Number} x
+ * The number of times to press the key.
+ * @param {Object} modifiers
+ * The event modifier e.g. {shiftKey: true}
+ */
+function PressKeyXTimes(key, x, modifiers = {}) {
+ for (let i = 0; i < x; i++) {
+ EventUtils.synthesizeKey(key, modifiers);
+ }
+}
+
+/**
+ * Verify the storage inspector state: check that given type/host exists
+ * in the tree, and that the table contains rows with specified names.
+ *
+ * @param {Array} state Array of state specifications. For example,
+ * [["cookies", "example.com"], ["c1", "c2"]] means to select the
+ * "example.com" host in cookies and then verify there are "c1" and "c2"
+ * cookies (and no other ones).
+ */
+async function checkState(state) {
+ for (const [store, names] of state) {
+ const storeName = store.join(" > ");
+ info(`Selecting tree item ${storeName}`);
+ await selectTreeItem(store);
+
+ const items = gUI.table.items;
+
+ is(
+ items.size,
+ names.length,
+ `There is correct number of rows in ${storeName}`
+ );
+
+ if (names.length === 0) {
+ showAvailableIds();
+ }
+
+ for (const name of names) {
+ if (!items.has(name)) {
+ showAvailableIds();
+ }
+ ok(items.has(name), `There is item with name '${name}' in ${storeName}`);
+ }
+ }
+}
+
+/**
+ * Checks if document's active element is within the given element.
+ * @param {HTMLDocument} doc document with active element in question
+ * @param {DOMNode} container element tested on focus containment
+ * @return {Boolean}
+ */
+function containsFocus(doc, container) {
+ let elm = doc.activeElement;
+ while (elm) {
+ if (elm === container) {
+ return true;
+ }
+ elm = elm.parentNode;
+ }
+ return false;
+}
+
+var focusSearchBoxUsingShortcut = async function (panelWin, callback) {
+ info("Focusing search box");
+ const searchBox = panelWin.document.getElementById("storage-searchbox");
+ const focused = once(searchBox, "focus");
+
+ panelWin.focus();
+
+ const shortcut = await panelWin.document.l10n.formatValue(
+ "storage-filter-key"
+ );
+ synthesizeKeyShortcut(shortcut);
+
+ await focused;
+
+ if (callback) {
+ callback();
+ }
+};
+
+function getCookieId(name, domain, path) {
+ return `${name}${SEPARATOR_GUID}${domain}${SEPARATOR_GUID}${path}`;
+}
+
+function setPermission(url, permission) {
+ const nsIPermissionManager = Ci.nsIPermissionManager;
+
+ const uri = Services.io.newURI(url);
+ const principal = Services.scriptSecurityManager.createContentPrincipal(
+ uri,
+ {}
+ );
+
+ Cc["@mozilla.org/permissionmanager;1"]
+ .getService(nsIPermissionManager)
+ .addFromPrincipal(principal, permission, nsIPermissionManager.ALLOW_ACTION);
+}
+
+function toggleSidebar() {
+ gUI.sidebarToggleBtn.click();
+}
+
+function sidebarToggleVisible() {
+ return !gUI.sidebarToggleBtn.hidden;
+}
+
+/**
+ * Check whether the variables view in the sidebar contains a tree.
+ *
+ * @param {Boolean} state
+ * Should a tree be visible?
+ */
+function sidebarParseTreeVisible(state) {
+ if (state) {
+ Assert.greater(
+ gUI.view._currHierarchy.size,
+ 2,
+ "Parse tree should be visible."
+ );
+ } else {
+ Assert.lessOrEqual(
+ gUI.view._currHierarchy.size,
+ 2,
+ "Parse tree should not be visible."
+ );
+ }
+}
+
+/**
+ * Add an item.
+ * @param {Array} store
+ * An array containing the path to the store to which we wish to add an
+ * item.
+ * @return {Promise} A Promise that resolves to the row id of the added item.
+ */
+async function performAdd(store) {
+ const storeName = store.join(" > ");
+ const toolbar = gPanelWindow.document.getElementById("storage-toolbar");
+ const type = store[0];
+
+ await selectTreeItem(store);
+
+ const menuAdd = toolbar.querySelector("#add-button");
+
+ if (menuAdd.hidden) {
+ is(
+ menuAdd.hidden,
+ false,
+ `performAdd called for ${storeName} but it is not supported`
+ );
+ return "";
+ }
+
+ const eventEdit = gUI.table.once("row-edit");
+ const eventWait = gUI.once("store-objects-edit");
+
+ menuAdd.click();
+
+ const rowId = await eventEdit;
+ await eventWait;
+
+ const key = type === "cookies" ? "uniqueKey" : "name";
+ const value = getCellValue(rowId, key);
+
+ is(rowId, value, `Row '${rowId}' was successfully added.`);
+
+ return rowId;
+}
+
+// Cell css selector that can be used to count or select cells.
+// The selector is restricted to a single column to avoid counting duplicates.
+const CELL_SELECTOR =
+ "#storage-table .table-widget-column:first-child .table-widget-cell";
+
+function getCellLength() {
+ return gPanelWindow.document.querySelectorAll(CELL_SELECTOR).length;
+}
+
+function checkCellLength(len) {
+ is(getCellLength(), len, `Table should contain ${len} items`);
+}
+
+async function scroll() {
+ const $ = id => gPanelWindow.document.querySelector(id);
+ const table = $("#storage-table .table-widget-body");
+ const cell = $(CELL_SELECTOR);
+ const cellHeight = cell.getBoundingClientRect().height;
+
+ const onStoresUpdate = gUI.once("store-objects-updated");
+ table.scrollTop += cellHeight * 50;
+ await onStoresUpdate;
+}
+
+/**
+ * Asserts that the given tree path exists
+ * @param {Document} doc
+ * @param {Array} path
+ * @param {Boolean} isExpected
+ */
+function checkTree(doc, path, isExpected = true) {
+ const doesExist = isInTree(doc, path);
+ ok(
+ isExpected ? doesExist : !doesExist,
+ `${path.join(" > ")} is ${isExpected ? "" : "not "}in the tree`
+ );
+}
+
+/**
+ * Returns whether a tree path exists
+ * @param {Document} doc
+ * @param {Array} path
+ */
+function isInTree(doc, path) {
+ const treeId = JSON.stringify(path);
+ return !!doc.querySelector(`[data-id='${treeId}']`);
+}
+
+/**
+ * Returns the label of the node for the provided tree path
+ * @param {Document} doc
+ * @param {Array} path
+ * @returns {String}
+ */
+function getTreeNodeLabel(doc, path) {
+ const treeId = JSON.stringify(path);
+ return doc.querySelector(`[data-id='${treeId}'] .tree-widget-item`)
+ .textContent;
+}
+
+/**
+ * Checks that the pair <name, value> is displayed at the data table
+ * @param {String} name
+ * @param {any} value
+ */
+function checkStorageData(name, value) {
+ ok(
+ hasStorageData(name, value),
+ `Table row has an entry for: ${name} with value: ${value}`
+ );
+}
+
+async function waitForStorageData(name, value) {
+ info("Waiting for data to appear in the table");
+ await waitFor(() => hasStorageData(name, value));
+ ok(true, `Table row has an entry for: ${name} with value: ${value}`);
+}
+
+/**
+ * Returns whether the pair <name, value> is displayed at the data table
+ * @param {String} name
+ * @param {any} value
+ */
+function hasStorageData(name, value) {
+ return gUI.table.items.get(name)?.value === value;
+}
+
+/**
+ * Returns an URL of a page that uses the document-builder to generate its content
+ * @param {String} domain
+ * @param {String} html
+ * @param {String} protocol
+ */
+function buildURLWithContent(domain, html, protocol = "https") {
+ return `${protocol}://${domain}/document-builder.sjs?html=${encodeURI(html)}`;
+}
+
+/**
+ * Asserts that the given cookie holds the provided value in the data table
+ * @param {String} name
+ * @param {String} value
+ */
+function checkCookieData(name, value) {
+ ok(
+ hasCookieData(name, value),
+ `Table row has an entry for: ${name} with value: ${value}`
+ );
+}
+
+/**
+ * Returns whether the given cookie holds the provided value in the data table
+ * @param {String} name
+ * @param {String} value
+ */
+function hasCookieData(name, value) {
+ const rows = Array.from(gUI.table.items);
+ const cookie = rows.map(([, data]) => data).find(x => x.name === name);
+
+ info(`found ${cookie?.value}`);
+ return cookie?.value === value;
+}
diff --git a/devtools/client/storage/test/storage-blank.html b/devtools/client/storage/test/storage-blank.html
new file mode 100644
index 0000000000..81342ed690
--- /dev/null
+++ b/devtools/client/storage/test/storage-blank.html
@@ -0,0 +1,9 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ </head>
+ <body>
+ <h2>storage-blank.html</h2>
+ </body>
+</html>
diff --git a/devtools/client/storage/test/storage-cache-basic-iframe.html b/devtools/client/storage/test/storage-cache-basic-iframe.html
new file mode 100644
index 0000000000..929660dc42
--- /dev/null
+++ b/devtools/client/storage/test/storage-cache-basic-iframe.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Storage inspector test for Cache</title>
+</head>
+<body>
+ <h1>Cache (iframe)</h1>
+<script>
+ "use strict";
+ async function setup() { // eslint-disable-line no-unused-vars
+ const cache = await caches.open("foo");
+ await cache.add("storage-blank.html");
+ }
+ function clear() { // eslint-disable-line no-unused-vars
+ return caches.delete("foo");
+ }
+</script>
+
+</body>
+</html>
diff --git a/devtools/client/storage/test/storage-cache-basic.html b/devtools/client/storage/test/storage-cache-basic.html
new file mode 100644
index 0000000000..b87023ece1
--- /dev/null
+++ b/devtools/client/storage/test/storage-cache-basic.html
@@ -0,0 +1,22 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Storage inspector test for Cache</title>
+</head>
+<body>
+ <h1>Cache</h1>
+<script>
+ "use strict";
+ async function setup() { // eslint-disable-line no-unused-vars
+ const cache = await caches.open("lorem");
+ await cache.add("storage-blank.html");
+ }
+ function clear() { // eslint-disable-line no-unused-vars
+ return caches.delete("lorem");
+ }
+</script>
+
+<iframe src="https://example.net/browser/devtools/client/storage/test/storage-cache-basic-iframe.html"></iframe>
+</body>
+</html>
diff --git a/devtools/client/storage/test/storage-cache-error.html b/devtools/client/storage/test/storage-cache-error.html
new file mode 100644
index 0000000000..1941c0dce0
--- /dev/null
+++ b/devtools/client/storage/test/storage-cache-error.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Storage inspector test for handling errors in CacheStorage</title>
+</head>
+<!-- The test case would load this page in a private browsing window -->
+<body>
+ <iframe src="https://test2.example.org"></iframe>
+</body>
+</html>
diff --git a/devtools/client/storage/test/storage-cache-overflow.html b/devtools/client/storage/test/storage-cache-overflow.html
new file mode 100644
index 0000000000..83e6636817
--- /dev/null
+++ b/devtools/client/storage/test/storage-cache-overflow.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Storage inspector test for Cache</title>
+</head>
+<body>
+ <h1>Cache overflow</h1>
+<script>
+ "use strict";
+ async function setup() { // eslint-disable-line no-unused-vars
+ const cache = await caches.open("lorem");
+ for (let i = 0; i < 100; i++) {
+ await cache.add(`storage-blank.html?${i}`);
+ }
+ }
+ function clear() { // eslint-disable-line no-unused-vars
+ return caches.delete("lorem");
+ }
+</script>
+
+</body>
+</html>
diff --git a/devtools/client/storage/test/storage-complex-keys.html b/devtools/client/storage/test/storage-complex-keys.html
new file mode 100644
index 0000000000..b037190dea
--- /dev/null
+++ b/devtools/client/storage/test/storage-complex-keys.html
@@ -0,0 +1,78 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Storage inspector test for correct keys in the sidebar</title>
+</head>
+<body>
+<script type="application/javascript">
+"use strict";
+
+// Some local storage items ...
+localStorage.setItem("", "1");
+localStorage.setItem("键", "2");
+// ... and finally some session storage items too
+sessionStorage.setItem("Key with spaces", "3");
+sessionStorage.setItem("Key#with~special$characters", "4");
+// long string
+const longKey = "a".repeat(1000);
+sessionStorage.setItem(longKey, "5");
+
+const idbGenerator = async function () {
+ const request = indexedDB.open("idb", 1);
+ request.onerror = function() {
+ throw new Error("Error opening database connection");
+ };
+
+ const db = await new Promise(done => {
+ request.onupgradeneeded = event => {
+ const _db = event.target.result;
+ const store = _db.createObjectStore("obj", { keyPath: "id" });
+ store.createIndex("name", "name", { unique: false });
+ store.transaction.oncomplete = () => {
+ done(_db);
+ };
+ };
+ });
+
+ // Prevents AbortError
+ await new Promise(done => {
+ request.onsuccess = done;
+ });
+
+ const transaction = db.transaction("obj", "readwrite");
+ const store = transaction.objectStore("obj");
+
+ store.add({id: "", name: "foo"});
+ store.add({id: "键", name: "foo2"});
+ store.add({id: "Key with spaces", name: "foo3"});
+ store.add({id: "Key#with~special$characters", name: "foo4"});
+ store.add({id: longKey, name: "foo5"});
+
+ db.close();
+
+ console.log("Added local and session storage items and indexedDB");
+};
+
+function deleteDB(dbName) {
+ return new Promise(resolve => {
+ dump("removing database " + dbName + " from " + document.location + "\n");
+ indexedDB.deleteDatabase(dbName).onsuccess = resolve;
+ });
+}
+
+window.setup = async function () {
+ await idbGenerator();
+};
+
+window.clear = async function () {
+ localStorage.clear();
+ sessionStorage.clear();
+
+ await deleteDB("idb");
+
+ dump("Removed data from " + document.location + "\n");
+};
+</script>
+</body>
+</html>
diff --git a/devtools/client/storage/test/storage-complex-values.html b/devtools/client/storage/test/storage-complex-values.html
new file mode 100644
index 0000000000..db7bc5e2ed
--- /dev/null
+++ b/devtools/client/storage/test/storage-complex-values.html
@@ -0,0 +1,124 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 970517 - Storage inspector front end - tests
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Storage inspector test for correct values in the sidebar</title>
+</head>
+<body>
+<script type="application/javascript">
+"use strict";
+const partialHostname = location.hostname.match(/^[^.]+(\..*)$/)[1];
+const cookieExpiresTime = 2000000000000;
+// Setting up some cookies to eat.
+document.cookie = "c1=" + JSON.stringify([
+ "foo", "Bar", {
+ foo: "Bar"
+ }]) + "; expires=" + new Date(cookieExpiresTime).toGMTString() +
+ "; path=/browser";
+document.cookie = "cs2=sessionCookie; path=/; domain=" + partialHostname;
+// URLEncoded cookie
+document.cookie = "c_encoded=" + encodeURIComponent(JSON.stringify({foo: {foo1: "bar"}}));
+
+// ... and some local storage items ..
+const es6 = "for";
+localStorage.setItem("ls1", JSON.stringify({
+ es6, the: "win", baz: [0, 2, 3, {
+ deep: "down",
+ nobody: "cares"
+ }]}));
+localStorage.setItem("ls2", "foobar-2");
+localStorage.setItem("ls3", "http://foobar.com/baz.php");
+localStorage.setItem("ls4", "0x1");
+// ... and finally some session storage items too
+sessionStorage.setItem("ss1", "This#is#an#array");
+sessionStorage.setItem("ss2", "This~is~another~array");
+sessionStorage.setItem("ss3", "this#is~an#object~foo#bar");
+sessionStorage.setItem("ss4", "#array##with#empty#items");
+// long string that is almost an object and might trigger exponential
+// regexp backtracking
+const s = "a".repeat(1000);
+sessionStorage.setItem("ss5", `${s}=${s}=${s}=${s}&${s}=${s}&${s}`);
+console.log("added cookies and stuff from main page");
+
+const idbGenerator = async function () {
+ let request = indexedDB.open("idb1", 1);
+ request.onerror = function() {
+ throw new Error("error opening db connection");
+ };
+ const db = await new Promise(done => {
+ request.onupgradeneeded = event => {
+ const _db = event.target.result;
+ const store1 = _db.createObjectStore("obj1", { keyPath: "id" });
+ store1.createIndex("name", "name", { unique: false });
+ store1.createIndex("email", "email", { unique: true });
+ _db.createObjectStore("obj2", { keyPath: "id2" });
+ store1.transaction.oncomplete = () => {
+ done(_db);
+ };
+ };
+ });
+
+ // Prevents AbortError
+ await new Promise(done => {
+ request.onsuccess = done;
+ });
+
+ const transaction = db.transaction(["obj1", "obj2"], "readwrite");
+ const store1 = transaction.objectStore("obj1");
+ const store2 = transaction.objectStore("obj2");
+
+ store1.add({id: 1, name: "foo", email: "foo@bar.com"});
+ store1.add({id: 2, name: "foo2", email: "foo2@bar.com"});
+ store1.add({id: 3, name: "foo2", email: "foo3@bar.com"});
+ store2.add({
+ id2: 1,
+ name: "foo",
+ email: "foo@bar.com",
+ extra: "baz".repeat(10000)});
+
+ db.close();
+
+ request = indexedDB.open("idb2", 1);
+ const db2 = await new Promise(done => {
+ request.onupgradeneeded = event => {
+ const _db2 = event.target.result;
+ const store3 = _db2.createObjectStore("obj3", { keyPath: "id3" });
+ store3.createIndex("name2", "name2", { unique: true });
+ store3.transaction.oncomplete = () => {
+ done(_db2);
+ };
+ };
+ });
+
+ // Prevents AbortError during close()
+ await new Promise(done => {
+ request.onsuccess = done;
+ });
+
+ db2.close();
+ console.log("added cookies and stuff from main page");
+};
+
+function deleteDB(dbName) {
+ return new Promise(resolve => {
+ dump("removing database " + dbName + " from " + document.location + "\n");
+ indexedDB.deleteDatabase(dbName).onsuccess = resolve;
+ });
+}
+
+window.setup = async function () {
+ await idbGenerator();
+};
+
+window.clear = async function () {
+ await deleteDB("idb1");
+ await deleteDB("idb2");
+
+ dump("removed indexedDB data from " + document.location + "\n");
+};
+</script>
+</body>
+</html>
diff --git a/devtools/client/storage/test/storage-cookies-samesite.html b/devtools/client/storage/test/storage-cookies-samesite.html
new file mode 100644
index 0000000000..90eb75d95e
--- /dev/null
+++ b/devtools/client/storage/test/storage-cookies-samesite.html
@@ -0,0 +1,17 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Storage inspector cookie samesite test</title>
+ </head>
+ <body>
+ <script type="application/javascript">
+ "use strict";
+ const expiresIn24Hours = new Date(Date.now() + 60 * 60 * 24 * 1000).toUTCString();
+
+ document.cookie = "test1=value1;expires=" + expiresIn24Hours + ";";
+ document.cookie = "test2=value2;expires=" + expiresIn24Hours + ";SameSite=lax";
+ document.cookie = "test3=value3;expires=" + expiresIn24Hours + ";SameSite=strict";
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/storage/test/storage-cookies-sort.html b/devtools/client/storage/test/storage-cookies-sort.html
new file mode 100644
index 0000000000..fb590d5cb2
--- /dev/null
+++ b/devtools/client/storage/test/storage-cookies-sort.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+ <!--
+ Bug 970517 - Storage inspector front end - tests
+ -->
+ <head>
+ <meta charset="utf-8">
+ <title>Storage inspector cookie test</title>
+ </head>
+ <body>
+ <script type="application/javascript">
+ "use strict";
+ const ONE_HOUR = 60 * 60 * 1000;
+ const ONE_DAY = 24 * ONE_HOUR;
+ const expiresOneHour = new Date(Date.now() + 1 * ONE_HOUR).toUTCString();
+ const expiresOneDay = new Date(Date.now() + 1 * ONE_DAY).toUTCString();
+ const expiresOneYear = new Date(Date.now() + 365 * ONE_DAY).toUTCString();
+
+ document.cookie = "test_hour=hour;expires=" + expiresOneHour;
+ document.cookie = "test_session1=session1";
+ document.cookie = "test_day=day;expires=" + expiresOneDay;
+ document.cookie = "test_session2=session2";
+ document.cookie = "test_year=year;expires=" + expiresOneYear;
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/storage/test/storage-cookies.html b/devtools/client/storage/test/storage-cookies.html
new file mode 100644
index 0000000000..c0d0522961
--- /dev/null
+++ b/devtools/client/storage/test/storage-cookies.html
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML>
+<html>
+ <!--
+ Bug 970517 - Storage inspector front end - tests
+ -->
+ <head>
+ <meta charset="utf-8">
+ <title>Storage inspector cookie test</title>
+ </head>
+ <body>
+ <script type="application/javascript">
+ "use strict";
+ const expiresIn24Hours = new Date(Date.now() + 60 * 60 * 24 * 1000).toUTCString();
+ for (let i = 1; i <= 5; i++) {
+ let cookieString = "test" + i + "=value" + i +
+ ";expires=" + expiresIn24Hours + ";path=/browser";
+ if (i % 2) {
+ cookieString += ";domain=test1.example.org";
+ }
+ document.cookie = cookieString;
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/storage/test/storage-dfpi.html b/devtools/client/storage/test/storage-dfpi.html
new file mode 100644
index 0000000000..e440819df7
--- /dev/null
+++ b/devtools/client/storage/test/storage-dfpi.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ </head>
+ <body>
+ <h2>storage-iframe.html</h2>
+ <!-- Sync iframe.src to browser_storage_dfpi.js:PREFIX -->
+ <iframe src="https://sub1.test1.example.com/browser/devtools/client/storage/test/storage-blank.html"></iframe>
+ </body>
+</html>
diff --git a/devtools/client/storage/test/storage-empty-objectstores.html b/devtools/client/storage/test/storage-empty-objectstores.html
new file mode 100644
index 0000000000..4479fc0972
--- /dev/null
+++ b/devtools/client/storage/test/storage-empty-objectstores.html
@@ -0,0 +1,62 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for proper listing indexedDB databases with no object stores</title>
+</head>
+<body>
+<script type="application/javascript">
+"use strict";
+window.setup = async function () {
+ let request = indexedDB.open("idb1", 1);
+ const db = await new Promise((resolve, reject) => {
+ request.onerror = e => reject(Error("error opening db connection"));
+ request.onupgradeneeded = event => {
+ const _db = event.target.result;
+ const store1 = _db.createObjectStore("obj1", { keyPath: "id" });
+ store1.createIndex("name", "name", { unique: false });
+ store1.createIndex("email", "email", { unique: true });
+ _db.createObjectStore("obj2", { keyPath: "id2" });
+ store1.transaction.oncomplete = () => resolve(_db);
+ };
+ });
+
+ await new Promise(resolve => (request.onsuccess = resolve));
+
+ const transaction = db.transaction(["obj1", "obj2"], "readwrite");
+ const store1 = transaction.objectStore("obj1");
+ const store2 = transaction.objectStore("obj2");
+
+ store1.add({id: 1, name: "foo", email: "foo@bar.com"});
+ store1.add({id: 2, name: "foo2", email: "foo2@bar.com"});
+ store1.add({id: 3, name: "foo2", email: "foo3@bar.com"});
+ store2.add({id2: 1, name: "foo", email: "foo@bar.com", extra: "baz"});
+
+ await new Promise(resolve => (transaction.oncomplete = resolve));
+
+ db.close();
+
+ request = indexedDB.open("idb2", 1);
+ const db2 = await new Promise((resolve, reject) => {
+ request.onerror = e => reject(Error("error opening db2 connection"));
+ request.onupgradeneeded = event => resolve(event.target.result);
+ });
+
+ await new Promise(resolve => (request.onsuccess = resolve));
+
+ db2.close();
+ dump("added indexedDB items from main page\n");
+};
+
+window.clear = async function () {
+ for (const dbName of ["idb1", "idb2"]) {
+ await new Promise(resolve => {
+ indexedDB.deleteDatabase(dbName).onsuccess = resolve;
+ });
+ }
+ dump("removed indexedDB items from main page\n");
+};
+
+</script>
+</body>
+</html>
diff --git a/devtools/client/storage/test/storage-file-url.html b/devtools/client/storage/test/storage-file-url.html
new file mode 100644
index 0000000000..1d10ab12b3
--- /dev/null
+++ b/devtools/client/storage/test/storage-file-url.html
@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>Storage Test</title>
+ <script>
+ "use strict";
+ /* exported setup */
+ function setup() {
+ createIndexedDB();
+ createCookies();
+ createLocalStorage();
+ createSessionStorage();
+ }
+
+ function createIndexedDB() {
+ const open = indexedDB.open("MyDatabase", 1);
+
+ open.onupgradeneeded = function () {
+ const db = open.result;
+ db.createObjectStore("MyObjectStore", {keyPath: "id"});
+ };
+
+ open.onsuccess = function () {
+ const db = open.result;
+ const tx = db.transaction("MyObjectStore", "readwrite");
+ const store = tx.objectStore("MyObjectStore");
+
+ store.put({id: 12345, name: {first: "John", last: "Doe"}, age: 42});
+ store.put({id: 54321, name: {first: "Ralph", last: "Wood"}, age: 38});
+ store.put({id: 67890, name: {first: "Bob", last: "Smith"}, age: 35});
+ store.put({id: 98765, name: {first: "Freddie", last: "Krueger"}, age: 40});
+
+ tx.oncomplete = function () {
+ db.close();
+ };
+ };
+ }
+
+ function createCookies() {
+ document.cookie = "test1=Jean Dupond";
+ document.cookie = "test2=dnopuD naeJ";
+ }
+
+ function createLocalStorage() {
+ localStorage.setItem("test3", "John Doe");
+ localStorage.setItem("test4", "eoD nhoJ");
+ }
+
+ function createSessionStorage() {
+ sessionStorage.setItem("test5", "John Smith");
+ sessionStorage.setItem("test6", "htimS nhoJ");
+ }
+ </script>
+</head>
+<body>
+ <h1>IndexedDB Test</h1>
+</body>
+</html>
diff --git a/devtools/client/storage/test/storage-idb-delete-blocked.html b/devtools/client/storage/test/storage-idb-delete-blocked.html
new file mode 100644
index 0000000000..7c7b597421
--- /dev/null
+++ b/devtools/client/storage/test/storage-idb-delete-blocked.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for proper listing indexedDB databases with no object stores</title>
+</head>
+<body>
+<script type="application/javascript">
+"use strict";
+let db;
+
+window.setup = async function () {
+ db = await new Promise((resolve, reject) => {
+ const request = indexedDB.open("idb", 1);
+
+ request.onsuccess = e => resolve(e.target.result);
+ request.onerror = e => reject(new Error("error opening db connection"));
+ });
+
+ dump("opened indexedDB\n");
+};
+
+window.closeDb = function() {
+ db.close();
+};
+
+window.deleteDb = async function () {
+ await new Promise((resolve, reject) => {
+ const request = indexedDB.deleteDatabase("idb");
+
+ request.onsuccess = resolve;
+ request.onerror = e => reject(new Error("error deleting db"));
+ });
+};
+
+window.clear = async function () {
+ for (const dbName of ["idb1", "idb2"]) {
+ await new Promise(resolve => {
+ indexedDB.deleteDatabase(dbName).onsuccess = resolve;
+ });
+ }
+ dump("removed indexedDB items from main page\n");
+};
+
+</script>
+</body>
+</html>
diff --git a/devtools/client/storage/test/storage-indexeddb-duplicate-names.html b/devtools/client/storage/test/storage-indexeddb-duplicate-names.html
new file mode 100644
index 0000000000..0f448f3727
--- /dev/null
+++ b/devtools/client/storage/test/storage-indexeddb-duplicate-names.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html><head>
+<meta http-equiv="content-type" content="text/html; charset=UTF-8">
+ <meta charset="utf-8">
+ <title>Storage inspector IndexedDBs with duplicate names</title>
+
+ <script type="application/javascript">
+ "use strict";
+
+ /* exported setup */
+ function setup() {
+ createIndexedDB("idb1");
+ createIndexedDB("idb2");
+ }
+
+ function createIndexedDB(name) {
+ const open = indexedDB.open(name);
+
+ open.onsuccess = function () {
+ const db = open.result;
+ db.close();
+ };
+ }
+
+ function deleteDB(dbName) {
+ return new Promise(resolve => {
+ dump(`removing database ${dbName} from ${document.location}\n`);
+ indexedDB.deleteDatabase(dbName).onsuccess = resolve;
+ });
+ }
+
+ window.clear = async function () {
+ await deleteDB("idb1");
+ await deleteDB("idb2");
+
+ dump(`removed indexedDB data from ${document.location}\n`);
+ };
+ </script>
+</head>
+<body>
+ <h1>storage-indexeddb-duplicate-names.html</h1>
+</body>
+</html>
diff --git a/devtools/client/storage/test/storage-indexeddb-iframe.html b/devtools/client/storage/test/storage-indexeddb-iframe.html
new file mode 100644
index 0000000000..8cf0071bd0
--- /dev/null
+++ b/devtools/client/storage/test/storage-indexeddb-iframe.html
@@ -0,0 +1,37 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Storage inspector test for indexedDB - simple (alt)</title>
+</head>
+
+<body>
+ <h1>IndexedDB storage - with iframe</h1>
+ <iframe src="https://example.net/browser/devtools/client/storage/test/storage-indexeddb-simple.html"></iframe>
+
+<script>
+"use strict";
+
+const DB_NAME = "db";
+
+async function setup() { // eslint-disable-line no-unused-vars
+ await new Promise((resolve, reject) => {
+ const request = indexedDB.open(DB_NAME, 1);
+ request.onerror = event => reject(Error("Error opening DB"));
+ request.onupgradeneeded = event => {
+ const db = event.target.result;
+ const store = db.createObjectStore("store", { keyPath: "key" });
+ store.add({key: "foo", value: "bar"});
+ store.transaction.oncomplete = () => resolve(db);
+ }
+ });
+}
+
+async function clear() { // eslint-disable-line no-unused-vars
+ await new Promise(resolve => {
+ indexedDB.deleteDatabase(DB_NAME).onsuccess = resolve;
+ });
+}
+</script>
+</body>
+</html>
diff --git a/devtools/client/storage/test/storage-indexeddb-simple-alt.html b/devtools/client/storage/test/storage-indexeddb-simple-alt.html
new file mode 100644
index 0000000000..0c5e56f795
--- /dev/null
+++ b/devtools/client/storage/test/storage-indexeddb-simple-alt.html
@@ -0,0 +1,38 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Storage inspector test for indexedDB - simple (alt)</title>
+</head>
+
+<body>
+ <h1>IndexedDB storage - simple (alt)</h1>
+
+<script>
+"use strict";
+
+const DB_NAME = "db-alt";
+
+async function setup() { // eslint-disable-line no-unused-vars
+ await new Promise((resolve, reject) => {
+ const request = indexedDB.open(DB_NAME, 1);
+ request.onerror = event => reject(Error("Error opening DB"));
+ request.onupgradeneeded = event => {
+ const db = event.target.result;
+ const store = db.createObjectStore("store", { keyPath: "key" });
+ store.add({key: "foo", value: "bar"});
+ store.transaction.oncomplete = () => resolve(db);
+ }
+ });
+}
+
+async function clear() { // eslint-disable-line no-unused-vars
+ await new Promise((resolve, reject) => {
+ const request = indexedDB.deleteDatabase(DB_NAME);
+ request.onsuccess = resolve;
+ request.onerror = () => reject(Error("Error deleting DB"));
+ });
+}
+</script>
+</body>
+</html>
diff --git a/devtools/client/storage/test/storage-indexeddb-simple.html b/devtools/client/storage/test/storage-indexeddb-simple.html
new file mode 100644
index 0000000000..9839240646
--- /dev/null
+++ b/devtools/client/storage/test/storage-indexeddb-simple.html
@@ -0,0 +1,38 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Storage inspector test for indexedDB - simple</title>
+</head>
+
+<body>
+ <h1>IndexedDB storage - simple</h1>
+
+<script>
+"use strict";
+
+const DB_NAME = "db";
+
+async function setup() { // eslint-disable-line no-unused-vars
+ await new Promise((resolve, reject) => {
+ const request = indexedDB.open(DB_NAME, 1);
+ request.onerror = event => reject(Error("Error opening DB"));
+ request.onupgradeneeded = event => {
+ const db = event.target.result;
+ const store = db.createObjectStore("store", { keyPath: "key" });
+ store.add({key: "lorem", value: "ipsum"});
+ store.transaction.oncomplete = () => resolve(db);
+ }
+ });
+}
+
+async function clear() { // eslint-disable-line no-unused-vars
+ await new Promise((resolve, reject) => {
+ const request = indexedDB.deleteDatabase(DB_NAME)
+ request.onsuccess = resolve;
+ request.onerror = () => reject(Error("Error deleting DB"));
+ });
+}
+</script>
+</body>
+</html>
diff --git a/devtools/client/storage/test/storage-listings-usercontextid.html b/devtools/client/storage/test/storage-listings-usercontextid.html
new file mode 100644
index 0000000000..d6eb6baf3c
--- /dev/null
+++ b/devtools/client/storage/test/storage-listings-usercontextid.html
@@ -0,0 +1,131 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Storage inspector front end for userContextId - tests
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Storage inspector test for listing hosts and storages</title>
+</head>
+<body>
+<iframe src="http://sectest1.example.org/browser/devtools/client/storage/test/storage-unsecured-iframe-usercontextid.html"></iframe>
+<iframe src="https://sectest1.example.org:443/browser/devtools/client/storage/test/storage-secured-iframe-usercontextid.html"></iframe>
+<script type="application/javascript">
+"use strict";
+const partialHostname = location.hostname.match(/^[^.]+(\..*)$/)[1];
+const cookieExpiresTime1 = 2000000000000;
+const cookieExpiresTime2 = 2000000001000;
+// Setting up some cookies to eat.
+document.cookie = "c1uc1=foobar; expires=" +
+ new Date(cookieExpiresTime1).toGMTString() + "; path=/browser";
+document.cookie = "cs2uc1=sessionCookie; path=/; domain=" + partialHostname;
+document.cookie = "c3uc1=foobar-2; expires=" +
+ new Date(cookieExpiresTime2).toGMTString() + "; path=/";
+// ... and some local storage items ..
+localStorage.setItem("ls1uc1", "foobar");
+localStorage.setItem("ls2uc1", "foobar-2");
+// ... and finally some session storage items too
+sessionStorage.setItem("ss1uc1", "foobar-3");
+dump("added cookies and storage from main page\n");
+
+const idbGenerator = async function () {
+ let request = indexedDB.open("idb1uc1", 1);
+ request.onerror = function() {
+ throw new Error("error opening db connection");
+ };
+ const db = await new Promise(done => {
+ request.onupgradeneeded = event => {
+ const _db = event.target.result;
+ const store1 = _db.createObjectStore("obj1uc1", { keyPath: "id" });
+ store1.createIndex("name", "name", { unique: false });
+ store1.createIndex("email", "email", { unique: true });
+ _db.createObjectStore("obj2uc1", { keyPath: "id2" });
+ store1.transaction.oncomplete = () => {
+ done(_db);
+ };
+ };
+ });
+
+ // Prevents AbortError
+ await new Promise(done => {
+ request.onsuccess = done;
+ });
+
+ const transaction = db.transaction(["obj1uc1", "obj2uc1"], "readwrite");
+ const store1 = transaction.objectStore("obj1uc1");
+ const store2 = transaction.objectStore("obj2uc1");
+ store1.add({id: 1, name: "foo", email: "foo@bar.com"});
+ store1.add({id: 2, name: "foo2", email: "foo2@bar.com"});
+ store1.add({id: 3, name: "foo2", email: "foo3@bar.com"});
+ store2.add({
+ id2: 1,
+ name: "foo",
+ email: "foo@bar.com",
+ extra: "baz"
+ });
+ // Prevents AbortError during close()
+ await new Promise(success => {
+ transaction.oncomplete = success;
+ });
+
+ db.close();
+
+ request = indexedDB.open("idb2uc1", 1);
+ const db2 = await new Promise(done => {
+ request.onupgradeneeded = event => {
+ const _db2 = event.target.result;
+ const store3 = _db2.createObjectStore("obj3uc1", { keyPath: "id3" });
+ store3.createIndex("name2", "name2", { unique: true });
+ store3.transaction.oncomplete = () => {
+ done(_db2);
+ }
+ };
+ });
+ // Prevents AbortError during close()
+ await new Promise(done => {
+ request.onsuccess = done;
+ });
+ db2.close();
+
+ dump("added indexedDB from main page\n");
+};
+
+function deleteDB(dbName) {
+ return new Promise(resolve => {
+ dump("removing database " + dbName + " from " + document.location + "\n");
+ indexedDB.deleteDatabase(dbName).onsuccess = resolve;
+ });
+}
+
+async function fetchPut(cache, url) {
+ const response = await fetch(url);
+ await cache.put(url, response);
+}
+
+const cacheGenerator = async function () {
+ const cache = await caches.open("plopuc1");
+ await fetchPut(cache, "404_cached_file.js");
+ await fetchPut(cache, "browser_storage_basic.js");
+};
+
+window.setup = async function () {
+ await idbGenerator();
+
+ if (window.caches) {
+ await cacheGenerator();
+ }
+};
+
+window.clear = async function () {
+ await deleteDB("idb1uc1");
+ await deleteDB("idb2uc1");
+
+ if (window.caches) {
+ await caches.delete("plopuc1");
+ }
+
+ dump("removed indexedDB and cache data from " + document.location + "\n");
+};
+</script>
+</body>
+</html>
diff --git a/devtools/client/storage/test/storage-listings-with-fragment.html b/devtools/client/storage/test/storage-listings-with-fragment.html
new file mode 100644
index 0000000000..2698f6ebac
--- /dev/null
+++ b/devtools/client/storage/test/storage-listings-with-fragment.html
@@ -0,0 +1,134 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+This test differs from browser_storage_listings.html only because the URLs we load
+include fragments e.g. http://example.com/test.js#abcdefg
+ ^^^^^^^^
+ fragment
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Storage inspector test for listing hosts and storages with URL fragments</title>
+</head>
+<body>
+<iframe src="http://sectest1.example.org/browser/devtools/client/storage/test/storage-unsecured-iframe.html#def"></iframe>
+<iframe src="https://sectest1.example.org:443/browser/devtools/client/storage/test/storage-secured-iframe.html#ghi"></iframe>
+<script type="application/javascript">
+"use strict";
+const partialHostname = location.hostname.match(/^[^.]+(\..*)$/)[1];
+const cookieExpiresTime1 = 2000000000000;
+const cookieExpiresTime2 = 2000000001000;
+// Setting up some cookies to eat.
+document.cookie = "c1=foobar; expires=" +
+ new Date(cookieExpiresTime1).toGMTString() + "; path=/browser";
+document.cookie = "cs2=sessionCookie; path=/; domain=" + partialHostname;
+document.cookie = "c3=foobar-2; expires=" +
+ new Date(cookieExpiresTime2).toGMTString() + "; path=/";
+// ... and some local storage items ..
+localStorage.setItem("ls1", "foobar");
+localStorage.setItem("ls2", "foobar-2");
+// ... and finally some session storage items too
+sessionStorage.setItem("ss1", "foobar-3");
+dump("added cookies and storage from main page\n");
+
+const idbGenerator = async function () {
+ let request = indexedDB.open("idb1", 1);
+ request.onerror = function() {
+ throw new Error("error opening db connection");
+ };
+ const db = await new Promise(done => {
+ request.onupgradeneeded = event => {
+ const _db = event.target.result;
+ const store1 = _db.createObjectStore("obj1", { keyPath: "id" });
+ store1.createIndex("name", "name", { unique: false });
+ store1.createIndex("email", "email", { unique: true });
+ _db.createObjectStore("obj2", { keyPath: "id2" }); // eslint-disable-line no-unused-vars
+ store1.transaction.oncomplete = () => {
+ done(_db);
+ };
+ };
+ });
+
+ // Prevents AbortError
+ await new Promise(done => {
+ request.onsuccess = done;
+ });
+
+ const transaction = db.transaction(["obj1", "obj2"], "readwrite");
+ const store1 = transaction.objectStore("obj1");
+ const store2 = transaction.objectStore("obj2");
+ store1.add({id: 1, name: "foo", email: "foo@bar.com"});
+ store1.add({id: 2, name: "foo2", email: "foo2@bar.com"});
+ store1.add({id: 3, name: "foo2", email: "foo3@bar.com"});
+ store2.add({
+ id2: 1,
+ name: "foo",
+ email: "foo@bar.com",
+ extra: "baz"
+ });
+ // Prevents AbortError during close()
+ await new Promise(success => {
+ transaction.oncomplete = success;
+ });
+
+ db.close();
+
+ request = indexedDB.open("idb2", 1);
+ const db2 = await new Promise(done => {
+ request.onupgradeneeded = event => {
+ const _db2 = event.target.result;
+ const store3 = _db2.createObjectStore("obj3", { keyPath: "id3" });
+ store3.createIndex("name2", "name2", { unique: true });
+ store3.transaction.oncomplete = () => {
+ done(_db2);
+ }
+ };
+ });
+ // Prevents AbortError during close()
+ await new Promise(done => {
+ request.onsuccess = done;
+ });
+ db2.close();
+
+ dump("added indexedDB from main page\n");
+};
+
+function deleteDB(dbName) {
+ return new Promise(resolve => {
+ dump("removing database " + dbName + " from " + document.location + "\n");
+ indexedDB.deleteDatabase(dbName).onsuccess = resolve;
+ });
+}
+
+async function fetchPut(cache, url) {
+ const response = await fetch(url);
+ await cache.put(url, response);
+}
+
+const cacheGenerator = async function () {
+ const cache = await caches.open("plop");
+ await fetchPut(cache, "404_cached_file.js");
+ await fetchPut(cache, "browser_storage_basic.js");
+};
+
+window.setup = async function () {
+ await idbGenerator();
+
+ if (window.caches) {
+ await cacheGenerator();
+ }
+};
+
+window.clear = async function () {
+ await deleteDB("idb1");
+ await deleteDB("idb2");
+
+ if (window.caches) {
+ await caches.delete("plop");
+ }
+
+ dump("removed indexedDB and cache data from " + document.location + "\n");
+};
+</script>
+</body>
+</html>
diff --git a/devtools/client/storage/test/storage-listings.html b/devtools/client/storage/test/storage-listings.html
new file mode 100644
index 0000000000..84ab005c50
--- /dev/null
+++ b/devtools/client/storage/test/storage-listings.html
@@ -0,0 +1,145 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Storage inspector front end - tests
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Storage inspector test for listing hosts and storages</title>
+</head>
+<body>
+<iframe src="http://sectest1.example.org/browser/devtools/client/storage/test/storage-unsecured-iframe.html"></iframe>
+<iframe src="https://sectest1.example.org:443/browser/devtools/client/storage/test/storage-secured-iframe.html"></iframe>
+<script type="application/javascript">
+"use strict";
+const partialHostname = location.hostname.match(/^[^.]+(\..*)$/)[1];
+const cookieExpiresTime1 = 2000000000000;
+const cookieExpiresTime2 = 2000000001000;
+// Setting up some cookies to eat.
+document.cookie = "c1=foobar; expires=" +
+ new Date(cookieExpiresTime1).toGMTString() + "; path=/browser";
+document.cookie = "cs2=sessionCookie; path=/; domain=" + partialHostname;
+document.cookie = "c3=foobar-2; expires=" +
+ new Date(cookieExpiresTime1).toGMTString() + "; path=/";
+document.cookie = "c4=foobar-3; expires=" +
+ new Date(cookieExpiresTime2).toGMTString() + "; path=/; domain=" +
+ partialHostname;
+// ... and some local storage items ..
+localStorage.setItem("ls1", "foobar");
+localStorage.setItem("ls2", "foobar-2");
+
+// Because localStorage contains key() on the prototype and it can't be iterated
+// using object.keys() we check the the value "key" exists.
+// See bug 1451991 for details.
+localStorage.setItem("key", "value1");
+
+// ... and finally some session storage items too
+sessionStorage.setItem("ss1", "foobar-3");
+
+// Because sessionStorage contains key() on the prototype and it can't be
+// iterated using object.keys() we check the the value "key" exists.
+// See bug 1451991 for details.
+sessionStorage.setItem("key", "value2");
+
+dump("added cookies and storage from main page\n");
+
+const idbGenerator = async function () {
+ let request = indexedDB.open("idb1", 1);
+ request.onerror = function() {
+ throw new Error("error opening db connection");
+ };
+ const db = await new Promise(done => {
+ request.onupgradeneeded = event => {
+ const _db = event.target.result;
+ const store1 = _db.createObjectStore("obj1", { keyPath: "id" });
+ store1.createIndex("name", "name", { unique: false });
+ store1.createIndex("email", "email", { unique: true });
+ _db.createObjectStore("obj2", { keyPath: "id2" }); // eslint-disable-line no-unused-vars
+ store1.transaction.oncomplete = () => {
+ done(_db);
+ };
+ };
+ });
+
+ // Prevents AbortError
+ await new Promise(done => {
+ request.onsuccess = done;
+ });
+
+ const transaction = db.transaction(["obj1", "obj2"], "readwrite");
+ const store1 = transaction.objectStore("obj1");
+ const store2 = transaction.objectStore("obj2");
+ store1.add({id: 1, name: "foo", email: "foo@bar.com"});
+ store1.add({id: 2, name: "foo2", email: "foo2@bar.com"});
+ store1.add({id: 3, name: "foo2", email: "foo3@bar.com"});
+ store2.add({
+ id2: 1,
+ name: "foo",
+ email: "foo@bar.com",
+ extra: "baz"
+ });
+ // Prevents AbortError during close()
+ await new Promise(success => {
+ transaction.oncomplete = success;
+ });
+
+ db.close();
+
+ request = indexedDB.open("idb2", 1);
+ const db2 = await new Promise(done => {
+ request.onupgradeneeded = event => {
+ const _db2 = event.target.result;
+ const store3 = _db2.createObjectStore("obj3", { keyPath: "id3" });
+ store3.createIndex("name2", "name2", { unique: true });
+ store3.transaction.oncomplete = () => {
+ done(_db2);
+ }
+ };
+ });
+ // Prevents AbortError during close()
+ await new Promise(done => {
+ request.onsuccess = done;
+ });
+ db2.close();
+
+ dump("added indexedDB from main page\n");
+};
+
+function deleteDB(dbName) {
+ return new Promise(resolve => {
+ dump("removing database " + dbName + " from " + document.location + "\n");
+ indexedDB.deleteDatabase(dbName).onsuccess = resolve;
+ });
+}
+
+async function fetchPut(cache, url) {
+ const response = await fetch(url);
+ await cache.put(url, response);
+}
+
+const cacheGenerator = async function () {
+ const cache = await caches.open("plop");
+ await fetchPut(cache, "404_cached_file.js");
+ await fetchPut(cache, "browser_storage_basic.js");
+};
+
+window.setup = async function () {
+ await idbGenerator();
+
+ if (window.caches) {
+ await cacheGenerator();
+ }
+};
+
+window.clear = async function () {
+ await deleteDB("idb1");
+ await deleteDB("idb2");
+
+ if (window.caches) {
+ await caches.delete("plop");
+ }
+ dump("removed indexedDB and cache data from " + document.location + "\n");
+};
+</script>
+</body>
+</html>
diff --git a/devtools/client/storage/test/storage-localstorage.html b/devtools/client/storage/test/storage-localstorage.html
new file mode 100644
index 0000000000..b22c7d42f6
--- /dev/null
+++ b/devtools/client/storage/test/storage-localstorage.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<html>
+ <!--
+ Bug 1231155 - Storage inspector front end - tests
+ -->
+ <head>
+ <meta charset="utf-8" />
+ <title>Storage inspector localStorage test</title>
+ <script type="application/javascript">
+ "use strict";
+ /* exported setup */
+ function setup() {
+ localStorage.setItem("TestLS1", "ValueLS1");
+ localStorage.setItem("TestLS2", "ValueLS2");
+ localStorage.setItem("TestLS3", "ValueLS3");
+ localStorage.setItem("TestLS4", "ValueLS4");
+ localStorage.setItem("TestLS5", "ValueLS5");
+ }
+ </script>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/devtools/client/storage/test/storage-overflow-indexeddb.html b/devtools/client/storage/test/storage-overflow-indexeddb.html
new file mode 100644
index 0000000000..68bc6522b0
--- /dev/null
+++ b/devtools/client/storage/test/storage-overflow-indexeddb.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 1171903 - Storage Inspector endless scrolling
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Storage inspector endless scrolling test</title>
+</head>
+<body>
+<script type="text/javascript">
+"use strict";
+
+window.setup = async function() {
+ await new Promise(resolve => {
+ const open = indexedDB.open("database", 1);
+ open.onupgradeneeded = function() {
+ const db = open.result;
+ const store = db.createObjectStore("store", {keyPath: "id"});
+ store.transaction.oncomplete = () => {
+ const transaction = db.transaction(["store"], "readwrite");
+ for (let i = 1; i < 150; i++) {
+ transaction.objectStore("store").add({id: i});
+ }
+
+ transaction.oncomplete = function() {
+ db.close();
+ resolve();
+ };
+ };
+ };
+ });
+};
+
+function deleteDB(dbName) {
+ return new Promise(resolve => {
+ indexedDB.deleteDatabase(dbName).onsuccess = resolve;
+ });
+}
+
+window.clear = async function() {
+ await deleteDB("database");
+
+ dump(`removed indexedDB data from ${document.location}\n`);
+};
+
+</script>
+</body>
+</html>
diff --git a/devtools/client/storage/test/storage-overflow.html b/devtools/client/storage/test/storage-overflow.html
new file mode 100644
index 0000000000..c922a18e69
--- /dev/null
+++ b/devtools/client/storage/test/storage-overflow.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 1171903 - Storage Inspector endless scrolling
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Storage inspector endless scrolling test</title>
+</head>
+<body>
+<script type="text/javascript">
+"use strict";
+
+for (let i = 1; i < 152; i++) {
+ localStorage.setItem(`item-${i}`, `value-${i}`);
+}
+</script>
+</body>
+</html>
diff --git a/devtools/client/storage/test/storage-search.html b/devtools/client/storage/test/storage-search.html
new file mode 100644
index 0000000000..914138aa39
--- /dev/null
+++ b/devtools/client/storage/test/storage-search.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 1224115 - Storage Inspector table filtering
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Storage inspector table filtering test</title>
+
+ <script type="text/javascript">
+ "use strict";
+
+ /* exported setup */
+ function setup() {
+ document.cookie = "01234=56789";
+ document.cookie = "ANIMAL=hOrSe";
+ document.cookie = "food=energy bar";
+ document.cookie = "FOO=bArBaz";
+ document.cookie = "money=##$$$**";
+ document.cookie = "sport=football";
+ document.cookie = "year=2016";
+ }
+ </script>
+
+</head>
+<body>
+</body>
+</html>
diff --git a/devtools/client/storage/test/storage-secured-iframe-usercontextid.html b/devtools/client/storage/test/storage-secured-iframe-usercontextid.html
new file mode 100644
index 0000000000..c3958661d4
--- /dev/null
+++ b/devtools/client/storage/test/storage-secured-iframe-usercontextid.html
@@ -0,0 +1,91 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Iframe for testing multiple host detetion in storage actor
+-->
+<head>
+ <meta charset="utf-8">
+</head>
+<body>
+<script type="application/javascript">
+"use strict";
+document.cookie = "sc1uc1=foobar;";
+localStorage.setItem("iframe-s-ls1uc1", "foobar");
+sessionStorage.setItem("iframe-s-ss1uc1", "foobar-2");
+dump("added cookies and storage from secured iframe\n");
+
+const idbGenerator = async function () {
+ let request = indexedDB.open("idb-s1uc1", 1);
+ request.onerror = function() {
+ throw new Error("error opening db connection");
+ };
+ const db = await new Promise(done => {
+ request.onupgradeneeded = event => {
+ const _db = event.target.result;
+ const store1 = _db.createObjectStore("obj-s1uc1", { keyPath: "id" });
+ store1.transaction.oncomplete = () => {
+ done(_db);
+ };
+ };
+ });
+ await new Promise(done => {
+ request.onsuccess = done;
+ });
+
+ let transaction = db.transaction(["obj-s1uc1"], "readwrite");
+ const store1 = transaction.objectStore("obj-s1uc1");
+ store1.add({id: 6, name: "foo", email: "foo@bar.com"});
+ store1.add({id: 7, name: "foo2", email: "foo2@bar.com"});
+ await new Promise(success => {
+ transaction.oncomplete = success;
+ });
+
+ db.close();
+
+ request = indexedDB.open("idb-s2uc1", 1);
+ const db2 = await new Promise(done => {
+ request.onupgradeneeded = event => {
+ const _db2 = event.target.result;
+ const store3 =
+ _db2.createObjectStore("obj-s2uc1", { keyPath: "id3", autoIncrement: true });
+ store3.createIndex("name2", "name2", { unique: true });
+ store3.transaction.oncomplete = () => {
+ done(_db2);
+ };
+ };
+ });
+ await new Promise(done => {
+ request.onsuccess = done;
+ });
+
+ transaction = db2.transaction(["obj-s2uc1"], "readwrite");
+ const store3 = transaction.objectStore("obj-s2uc1");
+ store3.add({id3: 16, name2: "foo", email: "foo@bar.com"});
+ await new Promise(success => {
+ transaction.oncomplete = success;
+ });
+
+ db2.close();
+ dump("added indexedDB from secured iframe\n");
+};
+
+function deleteDB(dbName) {
+ return new Promise(resolve => {
+ dump("removing database " + dbName + " from " + document.location + "\n");
+ indexedDB.deleteDatabase(dbName).onsuccess = resolve;
+ });
+}
+
+window.setup = async function () {
+ await idbGenerator();
+};
+
+window.clear = async function () {
+ await deleteDB("idb-s1uc1");
+ await deleteDB("idb-s2uc1");
+
+ dump("removed indexedDB data from " + document.location + "\n");
+};
+</script>
+</body>
+</html>
diff --git a/devtools/client/storage/test/storage-secured-iframe.html b/devtools/client/storage/test/storage-secured-iframe.html
new file mode 100644
index 0000000000..e0b7cc3716
--- /dev/null
+++ b/devtools/client/storage/test/storage-secured-iframe.html
@@ -0,0 +1,94 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Iframe for testing multiple host detetion in storage actor
+-->
+<head>
+ <meta charset="utf-8">
+</head>
+<body>
+<script type="application/javascript">
+"use strict";
+const cookieExpiresTime = 2000000000000;
+document.cookie = "sc1=foobar;";
+document.cookie = "sc2=foobar-2; expires=" +
+ new Date(cookieExpiresTime).toGMTString() + ";";
+localStorage.setItem("iframe-s-ls1", "foobar");
+sessionStorage.setItem("iframe-s-ss1", "foobar-2");
+dump("added cookies and storage from secured iframe\n");
+
+const idbGenerator = async function () {
+ let request = indexedDB.open("idb-s1", 1);
+ request.onerror = function() {
+ throw new Error("error opening db connection");
+ };
+ const db = await new Promise(done => {
+ request.onupgradeneeded = event => {
+ const _db = event.target.result;
+ const store1 = _db.createObjectStore("obj-s1", { keyPath: "id" });
+ store1.transaction.oncomplete = () => {
+ done(_db);
+ };
+ };
+ });
+ await new Promise(done => {
+ request.onsuccess = done;
+ });
+
+ let transaction = db.transaction(["obj-s1"], "readwrite");
+ const store1 = transaction.objectStore("obj-s1");
+ store1.add({id: 6, name: "foo", email: "foo@bar.com"});
+ store1.add({id: 7, name: "foo2", email: "foo2@bar.com"});
+ await new Promise(success => {
+ transaction.oncomplete = success;
+ });
+
+ db.close();
+
+ request = indexedDB.open("idb-s2", 1);
+ const db2 = await new Promise(done => {
+ request.onupgradeneeded = event => {
+ const _db2 = event.target.result;
+ const store3 =
+ _db2.createObjectStore("obj-s2", { keyPath: "id3", autoIncrement: true });
+ store3.createIndex("name2", "name2", { unique: true });
+ store3.transaction.oncomplete = () => {
+ done(_db2);
+ };
+ };
+ });
+ await new Promise(done => {
+ request.onsuccess = done;
+ });
+
+ transaction = db2.transaction(["obj-s2"], "readwrite");
+ const store3 = transaction.objectStore("obj-s2");
+ store3.add({id3: 16, name2: "foo", email: "foo@bar.com"});
+ await new Promise(success => {
+ transaction.oncomplete = success;
+ });
+
+ db2.close();
+ dump("added indexedDB from secured iframe\n");
+};
+
+function deleteDB(dbName) {
+ return new Promise(resolve => {
+ dump("removing database " + dbName + " from " + document.location + "\n");
+ indexedDB.deleteDatabase(dbName).onsuccess = resolve;
+ });
+}
+
+window.setup = async function () {
+ await idbGenerator();
+};
+
+window.clear = async function () {
+ await deleteDB("idb-s1");
+ await deleteDB("idb-s2");
+
+ dump("removed indexedDB data from " + document.location + "\n");
+};
+</script>
+</body>
+</html>
diff --git a/devtools/client/storage/test/storage-sessionstorage.html b/devtools/client/storage/test/storage-sessionstorage.html
new file mode 100644
index 0000000000..2e0fb2f131
--- /dev/null
+++ b/devtools/client/storage/test/storage-sessionstorage.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<html>
+ <!--
+ Bug 1231179 - Storage inspector front end - tests
+ -->
+ <head>
+ <meta charset="utf-8" />
+ <title>Storage inspector sessionStorage test</title>
+ <script type="application/javascript">
+ "use strict";
+ /* exported setup */
+ function setup() {
+ sessionStorage.setItem("TestSS1", "ValueSS1");
+ sessionStorage.setItem("TestSS2", "ValueSS2");
+ sessionStorage.setItem("TestSS3", "ValueSS3");
+ sessionStorage.setItem("TestSS4", "ValueSS4");
+ sessionStorage.setItem("TestSS5", "ValueSS5");
+ }
+ </script>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/devtools/client/storage/test/storage-sidebar-parsetree.html b/devtools/client/storage/test/storage-sidebar-parsetree.html
new file mode 100644
index 0000000000..47f669dd18
--- /dev/null
+++ b/devtools/client/storage/test/storage-sidebar-parsetree.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <title>Storage inspector sidebar parsetree test</title>
+ <script type="application/javascript">
+ "use strict";
+ /* exported setup */
+ function setup() {
+ // These values should not be parsed into a tree.
+ localStorage.setItem("base64", "aGVsbG93b3JsZA==");
+ localStorage.setItem("boolean", "true");
+ localStorage.setItem("color", "#ff0034");
+ localStorage.setItem("dataURI", "data:,Hello World!");
+ localStorage.setItem("date", "2009-05-19 14:39:22-01");
+ localStorage.setItem("email", "foo@bar.co.uk");
+ localStorage.setItem("FQDN", "xn--froschgrn-x9a.co.uk");
+ localStorage.setItem("IP", "192.168.1.1");
+ localStorage.setItem("MacAddress", "01:AB:03:04:05:06");
+ localStorage.setItem("maths", "9-1");
+ localStorage.setItem("numbers", "10,123,456");
+ localStorage.setItem("SemVer", "1.0.4");
+ localStorage.setItem("URL", "www.google.co.uk");
+ localStorage.setItem("URL2", "http://www.google.co.uk");
+
+ // These values should be parsed into a tree.
+ localStorage.setItem("ampersand", "a&b&c&d&e&f&g");
+ localStorage.setItem("asterisk", "a*b*c*d*e*f*g");
+ localStorage.setItem("colon", "a:b:c:d:e:f:g");
+ localStorage.setItem("comma", "a,b,c,d,e,f,g");
+ localStorage.setItem("equals", "a=b=c=d=e=f=g");
+ localStorage.setItem("hash", "a#b#c#d#e#f#g");
+ localStorage.setItem("period", "a.b.c.d.e.f.g");
+ localStorage.setItem("tilde", "a~b~c~d~e~f~g");
+ }
+ </script>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/devtools/client/storage/test/storage-unsecured-iframe-usercontextid.html b/devtools/client/storage/test/storage-unsecured-iframe-usercontextid.html
new file mode 100644
index 0000000000..c40ac8a761
--- /dev/null
+++ b/devtools/client/storage/test/storage-unsecured-iframe-usercontextid.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Iframe for testing multiple host detetion in storage actor
+-->
+<head>
+ <meta charset="utf-8">
+</head>
+<body>
+<script>
+"use strict";
+document.cookie = "uc1uc1=foobar; domain=.example.org; path=/";
+localStorage.setItem("iframe-u-ls1uc1", "foobar");
+sessionStorage.setItem("iframe-u-ss1uc1", "foobar1");
+sessionStorage.setItem("iframe-u-ss2uc1", "foobar2");
+dump("added cookies and storage from unsecured iframe\n");
+</script>
+</body>
+</html>
diff --git a/devtools/client/storage/test/storage-unsecured-iframe.html b/devtools/client/storage/test/storage-unsecured-iframe.html
new file mode 100644
index 0000000000..ad737dae0c
--- /dev/null
+++ b/devtools/client/storage/test/storage-unsecured-iframe.html
@@ -0,0 +1,22 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Iframe for testing multiple host detetion in storage actor
+-->
+<head>
+ <meta charset="utf-8">
+</head>
+<body>
+<script>
+"use strict";
+const cookieExpiresTime = 2000000000000;
+document.cookie = "uc1=foobar; domain=.example.org; path=/";
+document.cookie = "uc2=foobar-2; expires=" +
+ new Date(cookieExpiresTime).toGMTString() + "; path=/; domain=.example.org";
+localStorage.setItem("iframe-u-ls1", "foobar");
+sessionStorage.setItem("iframe-u-ss1", "foobar1");
+sessionStorage.setItem("iframe-u-ss2", "foobar2");
+dump("added cookies and storage from unsecured iframe\n");
+</script>
+</body>
+</html>
diff --git a/devtools/client/storage/test/storage-updates.html b/devtools/client/storage/test/storage-updates.html
new file mode 100644
index 0000000000..5fcbd10c4c
--- /dev/null
+++ b/devtools/client/storage/test/storage-updates.html
@@ -0,0 +1,68 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 965872 - Storage inspector actor with cookies, local storage and session storage.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Storage inspector blank html for tests</title>
+</head>
+<body>
+<script type="application/javascript">
+"use strict";
+window.addCookie = function(name, value, path, domain, expires, secure) {
+ let cookieString = name + "=" + value + ";";
+ if (path) {
+ cookieString += "path=" + path + ";";
+ }
+ if (domain) {
+ cookieString += "domain=" + domain + ";";
+ }
+ if (expires) {
+ cookieString += "expires=" + expires + ";";
+ }
+ if (secure) {
+ cookieString += "secure=true;";
+ }
+ document.cookie = cookieString;
+};
+
+window.removeCookie = function(name, path) {
+ document.cookie =
+ name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=" + path;
+};
+
+/**
+ * We keep this method here even though these items are automatically cleared
+ * after the test is complete. this is so that the store-objects-cleared event
+ * can be tested.
+ */
+window.clear = function() {
+ localStorage.clear();
+ dump("removed localStorage from " + document.location + "\n");
+
+ sessionStorage.clear();
+ dump("removed sessionStorage from " + document.location + "\n");
+};
+
+window.onload = function() {
+ window.addCookie("c1", "1.2.3.4.5.6.7", "/browser");
+ window.addCookie("c2", "foobar", "/browser");
+
+ // Some keys have to be set to strings that JSON.parse can parse successfully
+ // instead of throwings (to verify the issue fixed by Bug 1578447 doesn't regress).
+ localStorage.setItem("1", "testing");
+ localStorage.setItem("2", "testing");
+ localStorage.setItem("3", "testing");
+ localStorage.setItem("4", "testing");
+ localStorage.setItem("5", "testing");
+ localStorage.setItem("null", "testing");
+ localStorage.setItem("non-json-parsable", "testing");
+
+ sessionStorage.setItem("ss1", "foobar");
+ sessionStorage.setItem("ss2", "foobar");
+ sessionStorage.setItem("ss3", "foobar");
+};
+</script>
+</body>
+</html>