diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /devtools/client/storage/test | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/storage/test')
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> |