diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /devtools/client/storage | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'devtools/client/storage')
44 files changed, 4290 insertions, 0 deletions
diff --git a/devtools/client/storage/moz.build b/devtools/client/storage/moz.build new file mode 100644 index 000000000..0c7f2f46b --- /dev/null +++ b/devtools/client/storage/moz.build @@ -0,0 +1,12 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +BROWSER_CHROME_MANIFESTS += ['test/browser.ini'] + +DevToolsModules( + 'panel.js', + 'ui.js' +) diff --git a/devtools/client/storage/panel.js b/devtools/client/storage/panel.js new file mode 100644 index 000000000..c24059d8e --- /dev/null +++ b/devtools/client/storage/panel.js @@ -0,0 +1,87 @@ +/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* 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"; + +const EventEmitter = require("devtools/shared/event-emitter"); + +loader.lazyRequireGetter(this, "StorageFront", + "devtools/shared/fronts/storage", true); +loader.lazyRequireGetter(this, "StorageUI", + "devtools/client/storage/ui", true); + +var StoragePanel = this.StoragePanel = +function StoragePanel(panelWin, toolbox) { + EventEmitter.decorate(this); + + this._toolbox = toolbox; + this._target = toolbox.target; + this._panelWin = panelWin; + + this.destroy = this.destroy.bind(this); +}; + +exports.StoragePanel = StoragePanel; + +StoragePanel.prototype = { + get target() { + return this._toolbox.target; + }, + + get panelWindow() { + return this._panelWin; + }, + + /** + * open is effectively an asynchronous constructor + */ + open: function () { + let targetPromise; + // We always interact with the target as if it were remote + if (!this.target.isRemote) { + targetPromise = this.target.makeRemote(); + } else { + targetPromise = Promise.resolve(this.target); + } + + return targetPromise.then(() => { + this.target.on("close", this.destroy); + this._front = new StorageFront(this.target.client, this.target.form); + + this.UI = new StorageUI(this._front, this._target, + this._panelWin, this._toolbox); + this.isReady = true; + this.emit("ready"); + + return this; + }).catch(e => { + console.log("error while opening storage panel", e); + this.destroy(); + }); + }, + + /** + * Destroy the storage inspector. + */ + destroy: function () { + if (!this._destroyed) { + this.UI.destroy(); + this.UI = null; + + // Destroy front to ensure packet handler is removed from client + this._front.destroy(); + this._front = null; + this._destroyed = true; + + this._target.off("close", this.destroy); + this._target = null; + this._toolbox = null; + this._panelWin = null; + } + + return Promise.resolve(null); + }, +}; diff --git a/devtools/client/storage/storage.xul b/devtools/client/storage/storage.xul new file mode 100644 index 000000000..9fbef5199 --- /dev/null +++ b/devtools/client/storage/storage.xul @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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/. --> +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://devtools/content/shared/widgets/widgets.css" type="text/css"?> +<?xml-stylesheet href="chrome://devtools/skin/widgets.css" type="text/css"?> +<?xml-stylesheet href="chrome://devtools/skin/storage.css" type="text/css"?> + +<!DOCTYPE window [ + <!ENTITY % storageDTD SYSTEM "chrome://devtools/locale/storage.dtd"> + %storageDTD; +]> + +<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript;version=1.8" + src="chrome://devtools/content/shared/theme-switching.js"/> + <script type="text/javascript" src="chrome://global/content/globalOverlay.js"/> + + <commandset id="editMenuCommands"/> + + <popupset id="storagePopupSet"> + <menupopup id="storage-tree-popup"> + <menuitem id="storage-tree-popup-delete-all" + label="&storage.popupMenu.deleteAllLabel;"/> + <menuitem id="storage-tree-popup-delete"/> + </menupopup> + <menupopup id="storage-table-popup"> + <menuitem id="storage-table-popup-delete"/> + <menuitem id="storage-table-popup-delete-all-from"/> + <menuitem id="storage-table-popup-delete-all" + label="&storage.popupMenu.deleteAllLabel;"/> + </menupopup> + </popupset> + + <box flex="1" class="devtools-responsive-container theme-body"> + <vbox id="storage-tree"/> + <splitter class="devtools-side-splitter"/> + <vbox flex="1"> + <hbox id="storage-toolbar" class="devtools-toolbar"> + <textbox id="storage-searchbox" + class="devtools-filterinput" + type="search" + timeout="200" + placeholder="&searchBox.placeholder;"/> + </hbox> + <vbox id="storage-table" class="theme-sidebar" flex="1"/> + </vbox> + <splitter class="devtools-side-splitter"/> + <vbox id="storage-sidebar" class="devtools-sidebar-tabs" hidden="true"> + <vbox flex="1"/> + </vbox> + </box> + +</window> diff --git a/devtools/client/storage/test/.eslintrc.js b/devtools/client/storage/test/.eslintrc.js new file mode 100644 index 000000000..8d15a76d9 --- /dev/null +++ b/devtools/client/storage/test/.eslintrc.js @@ -0,0 +1,6 @@ +"use strict"; + +module.exports = { + // Extend from the shared list of defined globals for mochitests. + "extends": "../../../.eslintrc.mochitests.js" +}; diff --git a/devtools/client/storage/test/browser.ini b/devtools/client/storage/test/browser.ini new file mode 100644 index 000000000..dd7f48bd7 --- /dev/null +++ b/devtools/client/storage/test/browser.ini @@ -0,0 +1,44 @@ +[DEFAULT] +tags = devtools +subsuite = devtools +support-files = + storage-cache-error.html + storage-complex-values.html + storage-cookies.html + storage-empty-objectstores.html + storage-idb-delete-blocked.html + storage-listings.html + storage-localstorage.html + storage-overflow.html + storage-search.html + storage-secured-iframe.html + storage-sessionstorage.html + storage-unsecured-iframe.html + storage-updates.html + head.js + !/devtools/client/framework/test/shared-head.js + +[browser_storage_basic.js] +[browser_storage_cache_delete.js] +[browser_storage_cache_error.js] +[browser_storage_cookies_delete_all.js] +[browser_storage_cookies_domain.js] +[browser_storage_cookies_edit.js] +[browser_storage_cookies_edit_keyboard.js] +[browser_storage_cookies_tab_navigation.js] +[browser_storage_delete.js] +[browser_storage_delete_all.js] +[browser_storage_delete_tree.js] +[browser_storage_dynamic_updates.js] +[browser_storage_empty_objectstores.js] +[browser_storage_indexeddb_delete.js] +[browser_storage_indexeddb_delete_blocked.js] +[browser_storage_localstorage_edit.js] +[browser_storage_localstorage_error.js] +[browser_storage_overflow.js] +[browser_storage_search.js] +[browser_storage_search_keyboard_trap.js] +[browser_storage_sessionstorage_edit.js] +[browser_storage_sidebar.js] +[browser_storage_sidebar_update.js] +[browser_storage_values.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 000000000..343d46170 --- /dev/null +++ b/devtools/client/storage/test/browser_storage_basic.js @@ -0,0 +1,118 @@ +/* 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", "test1.example.org"], + ["c1", "cs2", "c3", "uc1"]], + [["cookies", "sectest1.example.org"], + ["uc1", "cs2", "sc1"]], + [["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", "idb2"]], + [["indexedDB", "http://test1.example.org", "idb1"], + ["obj1", "obj2"]], + [["indexedDB", "http://test1.example.org", "idb2"], + ["obj3"]], + [["indexedDB", "http://test1.example.org", "idb1", "obj1"], + [1, 2, 3]], + [["indexedDB", "http://test1.example.org", "idb1", "obj2"], + [1]], + [["indexedDB", "http://test1.example.org", "idb2", "obj3"], + []], + [["indexedDB", "http://sectest1.example.org"], + []], + [["indexedDB", "https://sectest1.example.org"], + ["idb-s1", "idb-s2"]], + [["indexedDB", "https://sectest1.example.org", "idb-s1"], + ["obj-s1"]], + [["indexedDB", "https://sectest1.example.org", "idb-s2"], + ["obj-s2"]], + [["indexedDB", "https://sectest1.example.org", "idb-s1", "obj-s1"], + [6, 7]], + [["indexedDB", "https://sectest1.example.org", "idb-s2", "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() { + let doc = gPanelWindow.document; + for (let item of testCases) { + ok(doc.querySelector("[data-id='" + JSON.stringify(item[0]) + "']"), + "Tree item " + item[0] + " should be present in the storage tree"); + } +} + +/** + * Test that correct table entries are shown for each of the tree item + */ +function* testTables() { + let 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 (let 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 (let item of testCases.slice(1)) { + yield selectTreeItem(item[0]); + + // Check whether correct number of items are present in the table + is(doc.querySelectorAll( + ".table-widget-wrapper:first-of-type .table-widget-cell" + ).length, item[1].length, "Number of items in table is correct"); + + // Check if all the desired items are present in the table + for (let id of item[1]) { + ok(doc.querySelector(".table-widget-cell[data-id='" + id + "']"), + "Table item " + id + " should be present"); + } + } +} + +add_task(function* () { + yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-listings.html"); + + testTree(); + yield testTables(); + + yield finishTests(); +}); 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 000000000..f87aa66e8 --- /dev/null +++ b/devtools/client/storage/test/browser_storage_cache_delete.js @@ -0,0 +1,46 @@ +/* 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/. */ + +/* import-globals-from ../../framework/test/shared-head.js */ + +"use strict"; + +// Test deleting a Cache object from the tree using context menu + +add_task(function* () { + yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-listings.html"); + + let contextMenu = gPanelWindow.document.getElementById("storage-tree-popup"); + let menuDeleteItem = contextMenu.querySelector("#storage-tree-popup-delete"); + + let cacheToDelete = ["Cache", "http://test1.example.org", "plop"]; + + info("test state before delete"); + yield selectTreeItem(cacheToDelete); + ok(gUI.tree.isSelected(cacheToDelete), "Cache item is present in the tree"); + + info("do the delete"); + let eventWait = gUI.once("store-objects-updated"); + + let selector = `[data-id='${JSON.stringify(cacheToDelete)}'] > .tree-widget-item`; + let target = gPanelWindow.document.querySelector(selector); + ok(target, "Cache item's tree element is present"); + + yield waitForContextMenu(contextMenu, target, () => { + info("Opened tree context menu"); + menuDeleteItem.click(); + + let cacheName = cacheToDelete[2]; + ok(menuDeleteItem.getAttribute("label").includes(cacheName), + `Context menu item label contains '${cacheName}')`); + }); + + yield eventWait; + + info("test state after delete"); + yield selectTreeItem(cacheToDelete); + ok(!gUI.tree.isSelected(cacheToDelete), "Cache item is no longer present in the tree"); + + yield finishTests(); +}); 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 000000000..dfc6056a7 --- /dev/null +++ b/devtools/client/storage/test/browser_storage_cache_error.js @@ -0,0 +1,19 @@ +/* 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(function* () { + yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-cache-error.html"); + + const cacheItemId = ["Cache", "javascript:parent.frameContent"]; + + yield selectTreeItem(cacheItemId); + ok(gUI.tree.isSelected(cacheItemId), + `The item ${cacheItemId.join(" > ")} is present in the tree`); + + yield finishTests(); +}); 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 000000000..6e6008e66 --- /dev/null +++ b/devtools/client/storage/test/browser_storage_cookies_delete_all.js @@ -0,0 +1,74 @@ +/* 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/. */ + +/* import-globals-from ../../framework/test/shared-head.js */ + +"use strict"; + +// Test deleting all cookies + +function* performDelete(store, rowName, deleteAll) { + let contextMenu = gPanelWindow.document.getElementById( + "storage-table-popup"); + let menuDeleteAllItem = contextMenu.querySelector( + "#storage-table-popup-delete-all"); + let menuDeleteAllFromItem = contextMenu.querySelector( + "#storage-table-popup-delete-all-from"); + + let storeName = store.join(" > "); + + yield selectTreeItem(store); + + let eventWait = gUI.once("store-objects-updated"); + + let cells = getRowCells(rowName); + yield waitForContextMenu(contextMenu, cells.name, () => { + info(`Opened context menu in ${storeName}, row '${rowName}'`); + if (deleteAll) { + menuDeleteAllItem.click(); + } else { + menuDeleteAllFromItem.click(); + let hostName = cells.host.value; + ok(menuDeleteAllFromItem.getAttribute("label").includes(hostName), + `Context menu item label contains '${hostName}'`); + } + }); + + yield eventWait; +} + +add_task(function* () { + yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-listings.html"); + + info("test state before delete"); + yield checkState([ + [["cookies", "test1.example.org"], ["c1", "c3", "cs2", "uc1"]], + [["cookies", "sectest1.example.org"], ["cs2", "sc1", "uc1"]], + ]); + + info("delete all from domain"); + // delete only cookies that match the host exactly + yield performDelete(["cookies", "test1.example.org"], "c1", false); + + info("test state after delete all from domain"); + yield checkState([ + // Domain cookies (.example.org) must not be deleted. + [["cookies", "test1.example.org"], ["cs2", "uc1"]], + [["cookies", "sectest1.example.org"], ["cs2", "sc1", "uc1"]], + ]); + + info("delete all"); + // delete all cookies for host, including domain cookies + yield performDelete(["cookies", "sectest1.example.org"], "uc1", true); + + info("test state after delete all"); + yield checkState([ + // Domain cookies (.example.org) are deleted too, so deleting in sectest1 + // also removes stuff from test1. + [["cookies", "test1.example.org"], []], + [["cookies", "sectest1.example.org"], []], + ]); + + yield finishTests(); +}); 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 000000000..dc93d6e67 --- /dev/null +++ b/devtools/client/storage/test/browser_storage_cookies_domain.js @@ -0,0 +1,21 @@ +/* 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/. */ + +/* import-globals-from ../../framework/test/shared-head.js */ + +"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(function* () { + yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-cookies.html"); + + yield checkState([ + [["cookies", "test1.example.org"], + ["test1", "test2", "test3", "test4", "test5"]], + ]); + + yield finishTests(); +}); 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 000000000..5818e4864 --- /dev/null +++ b/devtools/client/storage/test/browser_storage_cookies_edit.js @@ -0,0 +1,22 @@ +/* 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(function* () { + yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-cookies.html"); + showAllColumns(true); + + yield editCell("test3", "name", "newTest3"); + yield editCell("newTest3", "path", "/"); + yield editCell("newTest3", "host", "test1.example.org"); + yield editCell("newTest3", "expires", "Tue, 14 Feb 2040 17:41:14 GMT"); + yield editCell("newTest3", "value", "newValue3"); + yield editCell("newTest3", "isSecure", "true"); + yield editCell("newTest3", "isHttpOnly", "true"); + + yield finishTests(); +}); 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 000000000..1208c4376 --- /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(function* () { + yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-cookies.html"); + showAllColumns(true); + + yield startCellEdit("test4", "name"); + yield typeWithTerminator("test6", "VK_TAB"); + yield typeWithTerminator("/", "VK_TAB"); + yield typeWithTerminator(".example.org", "VK_TAB"); + yield typeWithTerminator("Tue, 25 Dec 2040 12:00:00 GMT", "VK_TAB"); + yield typeWithTerminator("test6value", "VK_TAB"); + yield typeWithTerminator("false", "VK_TAB"); + yield typeWithTerminator("false", "VK_TAB"); + + yield finishTests(); +}); 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 000000000..783a0c844 --- /dev/null +++ b/devtools/client/storage/test/browser_storage_cookies_tab_navigation.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 cookie table tab navigation. + +"use strict"; + +add_task(function* () { + yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-cookies.html"); + showAllColumns(true); + + yield startCellEdit("test1", "name"); + + PressKeyXTimes("VK_TAB", 18); + is(getCurrentEditorValue(), "value3", + "We have tabbed to the correct cell."); + + PressKeyXTimes("VK_TAB", 18, {shiftKey: true}); + is(getCurrentEditorValue(), "test1", + "We have shift-tabbed to the correct cell."); + + yield finishTests(); +}); 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 000000000..c0e2b0ad7 --- /dev/null +++ b/devtools/client/storage/test/browser_storage_delete.js @@ -0,0 +1,56 @@ +/* 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/. */ + +/* import-globals-from ../../framework/test/shared-head.js */ + +"use strict"; + +// Test deleting storage items + +const TEST_CASES = [ + [["localStorage", "http://test1.example.org"], + "ls1", "name"], + [["sessionStorage", "http://test1.example.org"], + "ss1", "name"], + [["cookies", "test1.example.org"], + "c1", "name"], + [["indexedDB", "http://test1.example.org", "idb1", "obj1"], + 1, "name"], + [["Cache", "http://test1.example.org", "plop"], + MAIN_DOMAIN + "404_cached_file.js", "url"], +]; + +add_task(function* () { + yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-listings.html"); + + let contextMenu = gPanelWindow.document.getElementById("storage-table-popup"); + let menuDeleteItem = contextMenu.querySelector("#storage-table-popup-delete"); + + for (let [ treeItem, rowName, cellToClick] of TEST_CASES) { + let treeItemName = treeItem.join(" > "); + + info(`Selecting tree item ${treeItemName}`); + yield selectTreeItem(treeItem); + + let row = getRowCells(rowName); + ok(gUI.table.items.has(rowName), `There is a row '${rowName}' in ${treeItemName}`); + + let eventWait = gUI.once("store-objects-updated"); + + yield waitForContextMenu(contextMenu, row[cellToClick], () => { + info(`Opened context menu in ${treeItemName}, row '${rowName}'`); + menuDeleteItem.click(); + let truncatedRowName = String(rowName).substr(0, 16); + ok(menuDeleteItem.getAttribute("label").includes(truncatedRowName), + `Context menu item label contains '${rowName}' (maybe truncated)`); + }); + + yield eventWait; + + ok(!gUI.table.items.has(rowName), + `There is no row '${rowName}' in ${treeItemName} after deletion`); + } + + yield finishTests(); +}); 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 000000000..c4b6048fb --- /dev/null +++ b/devtools/client/storage/test/browser_storage_delete_all.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/. */ + +/* import-globals-from ../../framework/test/shared-head.js */ + +"use strict"; + +// Test deleting all storage items + +add_task(function* () { + yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-listings.html"); + + let contextMenu = gPanelWindow.document.getElementById("storage-table-popup"); + let menuDeleteAllItem = contextMenu.querySelector( + "#storage-table-popup-delete-all"); + + info("test state before delete"); + const beforeState = [ + [["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", "obj1"], + [1, 2, 3]], + [["Cache", "http://test1.example.org", "plop"], + [MAIN_DOMAIN + "404_cached_file.js", MAIN_DOMAIN + "browser_storage_basic.js"]], + ]; + + yield 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", "obj1"], 1, "name"], + [["Cache", "http://test1.example.org", "plop"], + MAIN_DOMAIN + "404_cached_file.js", "url"], + ]; + + for (let [store, rowName, cellToClick] of deleteHosts) { + let storeName = store.join(" > "); + + yield selectTreeItem(store); + + let eventWait = gUI.once("store-objects-cleared"); + + let cell = getRowCells(rowName)[cellToClick]; + yield waitForContextMenu(contextMenu, cell, () => { + info(`Opened context menu in ${storeName}, row '${rowName}'`); + menuDeleteAllItem.click(); + }); + + yield 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"], + ["ls1", "ls2"]], + [["localStorage", "http://sectest1.example.org"], + ["iframe-u-ls1"]], + [["localStorage", "https://sectest1.example.org"], + []], + [["sessionStorage", "http://test1.example.org"], + ["ss1"]], + [["sessionStorage", "http://sectest1.example.org"], + ["iframe-u-ss1", "iframe-u-ss2"]], + [["sessionStorage", "https://sectest1.example.org"], + []], + [["indexedDB", "http://test1.example.org", "idb1", "obj1"], + []], + [["Cache", "http://test1.example.org", "plop"], + []], + ]; + + yield checkState(afterState); + + yield finishTests(); +}); 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 000000000..867a1c8b6 --- /dev/null +++ b/devtools/client/storage/test/browser_storage_delete_tree.js @@ -0,0 +1,67 @@ +/* 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/. */ + +/* import-globals-from ../../framework/test/shared-head.js */ + +"use strict"; + +// Test deleting all storage items from the tree. + +add_task(function* () { + yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-listings.html"); + + let contextMenu = gPanelWindow.document.getElementById("storage-tree-popup"); + let menuDeleteAllItem = contextMenu.querySelector( + "#storage-tree-popup-delete-all"); + + info("test state before delete"); + yield checkState([ + [["cookies", "test1.example.org"], ["c1", "c3", "cs2", "uc1"]], + [["localStorage", "http://test1.example.org"], ["ls1", "ls2"]], + [["sessionStorage", "http://test1.example.org"], ["ss1"]], + [["indexedDB", "http://test1.example.org", "idb1", "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", "test1.example.org"], + ["localStorage", "http://test1.example.org"], + ["sessionStorage", "http://test1.example.org"], + ["indexedDB", "http://test1.example.org", "idb1", "obj1"], + ["Cache", "http://test1.example.org", "plop"], + ]; + + for (let store of deleteHosts) { + let storeName = store.join(" > "); + + yield selectTreeItem(store); + + let eventName = "store-objects-" + + (store[0] == "cookies" ? "updated" : "cleared"); + let eventWait = gUI.once(eventName); + + let selector = `[data-id='${JSON.stringify(store)}'] > .tree-widget-item`; + let target = gPanelWindow.document.querySelector(selector); + ok(target, `tree item found in ${storeName}`); + yield waitForContextMenu(contextMenu, target, () => { + info(`Opened tree context menu in ${storeName}`); + menuDeleteAllItem.click(); + }); + + yield eventWait; + } + + info("test state after delete"); + yield checkState([ + [["cookies", "test1.example.org"], []], + [["localStorage", "http://test1.example.org"], []], + [["sessionStorage", "http://test1.example.org"], []], + [["indexedDB", "http://test1.example.org", "idb1", "obj1"], []], + [["Cache", "http://test1.example.org", "plop"], []], + ]); + + yield finishTests(); +}); diff --git a/devtools/client/storage/test/browser_storage_dynamic_updates.js b/devtools/client/storage/test/browser_storage_dynamic_updates.js new file mode 100644 index 000000000..f881146d2 --- /dev/null +++ b/devtools/client/storage/test/browser_storage_dynamic_updates.js @@ -0,0 +1,213 @@ +/* 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(function* () { + yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-updates.html"); + + let $ = id => gPanelWindow.document.querySelector(id); + let $$ = sel => gPanelWindow.document.querySelectorAll(sel); + + gUI.tree.expandAll(); + + ok(gUI.sidebar.hidden, "Sidebar is initially hidden"); + yield selectTableItem("c1"); + + // test that value is something initially + let 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 + let 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 + yield findVariableViewProperties(initialValue[0], false); + yield findVariableViewProperties(initialValue[1], true); + // Check if table shows correct initial value + ok($("#value [data-id='c1'].table-widget-cell"), "cell is present"); + is($("#value [data-id='c1'].table-widget-cell").value, "1.2.3.4.5.6.7", + "correct initial value in table"); + gWindow.addCookie("c1", '{"foo": 4,"bar":6}', "/browser"); + yield gUI.once("sidebar-updated"); + + yield findVariableViewProperties(finalValue[0], false); + yield findVariableViewProperties(finalValue[1], true); + ok($("#value [data-id='c1'].table-widget-cell"), + "cell is present after update"); + is($("#value [data-id='c1'].table-widget-cell").value, '{"foo": 4,"bar":6}', + "correct final value in table"); + + // Add a new entry + is($$("#value .table-widget-cell").length, 2, + "Correct number of rows before update 0"); + + gWindow.addCookie("c3", "booyeah"); + + // Wait once for update and another time for value fetching + yield gUI.once("store-objects-updated"); + yield gUI.once("store-objects-updated"); + + is($$("#value .table-widget-cell").length, 3, + "Correct number of rows after update 1"); + + // Add another + gWindow.addCookie("c4", "booyeah"); + + // Wait once for update and another time for value fetching + yield gUI.once("store-objects-updated"); + yield gUI.once("store-objects-updated"); + + is($$("#value .table-widget-cell").length, 4, + "Correct number of rows after update 2"); + + // Removing cookies + gWindow.removeCookie("c1", "/browser"); + + yield gUI.once("sidebar-updated"); + + is($$("#value .table-widget-cell").length, 3, + "Correct number of rows after delete update 3"); + + ok(!$("#c1"), "Correct row got deleted"); + + ok(!gUI.sidebar.hidden, "Sidebar still visible for next row"); + + // Check if next element's value is visible in sidebar + yield findVariableViewProperties([{name: "c2", value: "foobar"}]); + + // Keep deleting till no rows + + gWindow.removeCookie("c3"); + + yield gUI.once("store-objects-updated"); + + is($$("#value .table-widget-cell").length, 2, + "Correct number of rows after delete update 4"); + + // Check if next element's value is visible in sidebar + yield findVariableViewProperties([{name: "c2", value: "foobar"}]); + + gWindow.removeCookie("c2", "/browser"); + + yield gUI.once("sidebar-updated"); + + yield findVariableViewProperties([{name: "c4", value: "booyeah"}]); + + is($$("#value .table-widget-cell").length, 1, + "Correct number of rows after delete update 5"); + + gWindow.removeCookie("c4"); + + yield gUI.once("store-objects-updated"); + + is($$("#value .table-widget-cell").length, 0, + "Correct number of rows after delete update 6"); + ok(gUI.sidebar.hidden, "Sidebar is hidden when no rows"); + + // Testing in local storage + yield selectTreeItem(["localStorage", "http://test1.example.org"]); + + is($$("#value .table-widget-cell").length, 7, + "Correct number of rows after delete update 7"); + + ok($(".table-widget-cell[data-id='ls4']"), "ls4 exists before deleting"); + + gWindow.localStorage.removeItem("ls4"); + + yield gUI.once("store-objects-updated"); + + is($$("#value .table-widget-cell").length, 6, + "Correct number of rows after delete update 8"); + ok(!$(".table-widget-cell[data-id='ls4']"), + "ls4 does not exists after deleting"); + + gWindow.localStorage.setItem("ls4", "again"); + + yield gUI.once("store-objects-updated"); + yield gUI.once("store-objects-updated"); + + is($$("#value .table-widget-cell").length, 7, + "Correct number of rows after delete update 9"); + ok($(".table-widget-cell[data-id='ls4']"), + "ls4 came back after adding it again"); + + // Updating a row + gWindow.localStorage.setItem("ls2", "ls2-changed"); + + yield gUI.once("store-objects-updated"); + yield gUI.once("store-objects-updated"); + + is($("#value [data-id='ls2']").value, "ls2-changed", + "Value got updated for local storage"); + + // Testing in session storage + yield selectTreeItem(["sessionStorage", "http://test1.example.org"]); + + is($$("#value .table-widget-cell").length, 3, + "Correct number of rows for session storage"); + + gWindow.sessionStorage.setItem("ss4", "new-item"); + + yield gUI.once("store-objects-updated"); + yield gUI.once("store-objects-updated"); + + is($$("#value .table-widget-cell").length, 4, + "Correct number of rows after session storage update"); + + // deleting item + + gWindow.sessionStorage.removeItem("ss3"); + + yield gUI.once("store-objects-updated"); + + gWindow.sessionStorage.removeItem("ss1"); + + yield gUI.once("store-objects-updated"); + + is($$("#value .table-widget-cell").length, 2, + "Correct number of rows after removing items from session storage"); + + yield selectTableItem("ss2"); + + ok(!gUI.sidebar.hidden, "sidebar is visible"); + + // Checking for correct value in sidebar before update + yield findVariableViewProperties([{name: "ss2", value: "foobar"}]); + + gWindow.sessionStorage.setItem("ss2", "changed=ss2"); + + yield gUI.once("sidebar-updated"); + + is($("#value [data-id='ss2']").value, "changed=ss2", + "Value got updated for session storage in the table"); + + yield findVariableViewProperties([{name: "ss2", value: "changed=ss2"}]); + + // Clearing items. Bug 1233497 makes it so that we can no longer yield + // CPOWs from Tasks. We work around this by calling clear via a ContentTask + // instead. + yield ContentTask.spawn(gBrowser.selectedBrowser, null, function* () { + return Task.spawn(content.wrappedJSObject.clear); + }); + + yield gUI.once("store-objects-cleared"); + + is($$("#value .table-widget-cell").length, 0, + "Table should be cleared"); + + yield finishTests(); +}); 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 000000000..1749c91b8 --- /dev/null +++ b/devtools/client/storage/test/browser_storage_empty_objectstores.js @@ -0,0 +1,77 @@ +/* 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", "http://test1.example.org"], + ["idb1", "idb2"]], + [["indexedDB", "http://test1.example.org", "idb1"], + ["obj1", "obj2"]], + [["indexedDB", "http://test1.example.org", "idb2"], + []], + [["indexedDB", "http://test1.example.org", "idb1", "obj1"], + [1, 2, 3]], + [["indexedDB", "http://test1.example.org", "idb1", "obj2"], + [1]] +]; + +/** + * Test that the desired number of tree items are present + */ +function testTree() { + let doc = gPanelWindow.document; + for (let [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 + */ +let testTables = function* () { + let 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 (let [item, ids] of storeItems) { + yield selectTreeItem(item); + + // Check whether correct number of items are present in the table + is(doc.querySelectorAll( + ".table-widget-wrapper: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 (let id of ids) { + ok(doc.querySelector(".table-widget-cell[data-id='" + id + "']"), + `Table item ${id} should be present`); + } + } +}; + +add_task(function* () { + yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-empty-objectstores.html"); + + testTree(); + yield testTables(); + yield finishTests(); +}); 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 000000000..18a0daf69 --- /dev/null +++ b/devtools/client/storage/test/browser_storage_indexeddb_delete.js @@ -0,0 +1,47 @@ +/* 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/. */ + +/* import-globals-from ../../framework/test/shared-head.js */ + +"use strict"; + +// Test deleting indexedDB database from the tree using context menu + +add_task(function* () { + yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-empty-objectstores.html"); + + let contextMenu = gPanelWindow.document.getElementById("storage-tree-popup"); + let menuDeleteDb = contextMenu.querySelector("#storage-tree-popup-delete"); + + info("test state before delete"); + yield checkState([ + [["indexedDB", "http://test1.example.org"], ["idb1", "idb2"]], + ]); + + info("do the delete"); + const deletedDb = ["indexedDB", "http://test1.example.org", "idb1"]; + + yield selectTreeItem(deletedDb); + + // Wait once for update and another time for value fetching + let eventWait = gUI.once("store-objects-updated").then( + () => gUI.once("store-objects-updated")); + + let selector = `[data-id='${JSON.stringify(deletedDb)}'] > .tree-widget-item`; + let target = gPanelWindow.document.querySelector(selector); + ok(target, `tree item found in ${deletedDb.join(" > ")}`); + yield waitForContextMenu(contextMenu, target, () => { + info(`Opened tree context menu in ${deletedDb.join(" > ")}`); + menuDeleteDb.click(); + }); + + yield eventWait; + + info("test state after delete"); + yield checkState([ + [["indexedDB", "http://test1.example.org"], ["idb2"]], + ]); + + yield finishTests(); +}); 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 000000000..6e89c4f28 --- /dev/null +++ b/devtools/client/storage/test/browser_storage_indexeddb_delete_blocked.js @@ -0,0 +1,58 @@ +/* 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/. */ + +/* import-globals-from ../../framework/test/shared-head.js */ + +"use strict"; + +// Test what happens when deleting indexedDB database is blocked + +add_task(function* () { + yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-idb-delete-blocked.html"); + + info("test state before delete"); + yield checkState([ + [["indexedDB", "http://test1.example.org"], ["idb"]] + ]); + + info("do the delete"); + yield selectTreeItem(["indexedDB", "http://test1.example.org"]); + let actor = gUI.getCurrentActor(); + let result = yield actor.removeDatabase("http://test1.example.org", "idb"); + + ok(result.blocked, "removeDatabase attempt is blocked"); + + info("test state after blocked delete"); + yield checkState([ + [["indexedDB", "http://test1.example.org"], ["idb"]] + ]); + + let eventWait = gUI.once("store-objects-updated"); + + info("telling content to close the db"); + yield ContentTask.spawn(gBrowser.selectedBrowser, null, function* () { + let win = content.wrappedJSObject; + yield win.closeDb(); + }); + + info("waiting for store update events"); + yield eventWait; + + info("test state after real delete"); + yield checkState([ + [["indexedDB", "http://test1.example.org"], []] + ]); + + info("try to delete database from nonexistent host"); + let errorThrown = false; + try { + result = yield actor.removeDatabase("http://test2.example.org", "idb"); + } catch (ex) { + errorThrown = true; + } + + ok(errorThrown, "error was reported when trying to delete"); + + yield finishTests(); +}); 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 000000000..86409e0ac --- /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(function* () { + yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-localstorage.html"); + + yield selectTreeItem(["localStorage", "http://test1.example.org"]); + + yield editCell("TestLS1", "name", "newTestLS1"); + yield editCell("newTestLS1", "value", "newValueLS1"); + + yield editCell("TestLS3", "name", "newTestLS3"); + yield editCell("newTestLS3", "value", "newValueLS3"); + + yield editCell("TestLS5", "name", "newTestLS5"); + yield editCell("newTestLS5", "value", "newValueLS5"); + + yield finishTests(); +}); 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 000000000..923ca6ca9 --- /dev/null +++ b/devtools/client/storage/test/browser_storage_localstorage_error.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/. */ + +"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(function* () { + yield openTabAndSetupStorage("about:home"); + + let itemsToOpen = [ + ["localStorage", "about:home"], + ["sessionStorage", "about:home"] + ]; + + for (let item of itemsToOpen) { + yield selectTreeItem(item); + ok(gUI.tree.isSelected(item), `Item ${item.join(" > ")} is present in the tree`); + } + + yield finishTests(); +}); 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 000000000..88181ca05 --- /dev/null +++ b/devtools/client/storage/test/browser_storage_overflow.js @@ -0,0 +1,41 @@ +// Test endless scrolling when a lot of items are present in the storage +// inspector table. +"use strict"; + +add_task(function* () { + yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-overflow.html"); + + let $ = id => gPanelWindow.document.querySelector(id); + let $$ = sel => gPanelWindow.document.querySelectorAll(sel); + + gUI.tree.expandAll(); + yield selectTreeItem(["localStorage", "http://test1.example.org"]); + + let table = $("#storage-table .table-widget-body"); + let cellHeight = $(".table-widget-cell").getBoundingClientRect().height; + + is($$("#value .table-widget-cell").length, 50, + "Table should initially display 50 items"); + + let onStoresUpdate = gUI.once("store-objects-updated"); + table.scrollTop += cellHeight * 50; + yield onStoresUpdate; + + is($$("#value .table-widget-cell").length, 100, + "Table should display 100 items after scrolling"); + + onStoresUpdate = gUI.once("store-objects-updated"); + table.scrollTop += cellHeight * 50; + yield onStoresUpdate; + + is($$("#value .table-widget-cell").length, 150, + "Table should display 150 items after scrolling"); + + onStoresUpdate = gUI.once("store-objects-updated"); + table.scrollTop += cellHeight * 50; + yield onStoresUpdate; + + is($$("#value .table-widget-cell").length, 160, + "Table should display all 160 items after scrolling"); + yield finishTests(); +}); 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 000000000..bbe0947b9 --- /dev/null +++ b/devtools/client/storage/test/browser_storage_search.js @@ -0,0 +1,87 @@ +// Tests the filter search box in the storage inspector +"use strict"; + +add_task(function* () { + yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-search.html"); + + let $$ = sel => gPanelWindow.document.querySelectorAll(sel); + gUI.tree.expandAll(); + yield selectTreeItem(["localStorage", "http://test1.example.org"]); + + // Results: 0=hidden, 1=visible + let 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, 0, 1, 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] + } + ]; + + let names = $$("#name .table-widget-cell"); + let rows = $$("#value .table-widget-cell"); + for (let testcase of testcases) { + info(`Testing input: ${testcase.value}`); + + gUI.searchBox.value = testcase.value; + gUI.filterItems(); + + for (let i = 0; i < rows.length; i++) { + info(`Testing row ${i}`); + info(`key: ${names[i].value}, value: ${rows[i].value}`); + let state = testcase.results[i] ? "visible" : "hidden"; + is(rows[i].hasAttribute("hidden"), !testcase.results[i], + `Row ${i} should be ${state}`); + } + } + + yield finishTests(); +}); 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 000000000..71dfd32c0 --- /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(function* () { + yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-search.html"); + + gUI.tree.expandAll(); + yield selectTreeItem(["localStorage", "http://test1.example.org"]); + + yield focusSearchBoxUsingShortcut(gPanelWindow); + ok(containsFocus(gPanelWindow.document, gUI.searchBox), + "Focus is in a searchbox"); + + yield finishTests(); +}); 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 000000000..9629eec0b --- /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(function* () { + yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-sessionstorage.html"); + + yield selectTreeItem(["sessionStorage", "http://test1.example.org"]); + + yield editCell("TestSS1", "name", "newTestSS1"); + yield editCell("newTestSS1", "value", "newValueSS1"); + + yield editCell("TestSS3", "name", "newTestSS3"); + yield editCell("newTestSS3", "value", "newValueSS3"); + + yield editCell("TestSS5", "name", "newTestSS5"); + yield editCell("newTestSS5", "value", "newValueSS5"); + + yield finishTests(); +}); 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 000000000..9b60026a0 --- /dev/null +++ b/devtools/client/storage/test/browser_storage_sidebar.js @@ -0,0 +1,125 @@ +/* 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", "sectest1.example.org"], + sidebarHidden: true + }, + { + location: "cs2", + sidebarHidden: false + }, + { + sendEscape: true + }, + { + location: "cs2", + sidebarHidden: false + }, + { + location: "uc1", + sidebarHidden: false + }, + { + location: "uc1", + sidebarHidden: false + }, + + { + 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", + sidebarHidden: false + }, + + { + location: ["indexedDB", "http://test1.example.org", "idb2", "obj3"], + sidebarHidden: true + }, + + { + location: ["indexedDB", "https://sectest1.example.org", "idb-s2"], + sidebarHidden: true + }, + { + location: "obj-s2", + sidebarHidden: false + }, + { + sendEscape: true + }, { + location: "obj-s2", + sidebarHidden: false + } +]; + +add_task(function* () { + yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-listings.html"); + + for (let test of testCases) { + let { location, sidebarHidden, sendEscape } = test; + + info("running " + JSON.stringify(test)); + + if (Array.isArray(location)) { + yield selectTreeItem(location); + } else if (location) { + yield selectTableItem(location); + } + + if (sendEscape) { + EventUtils.sendKey("ESCAPE", gPanelWindow); + } else { + is(gUI.sidebar.hidden, sidebarHidden, + "correct visibility state of sidebar."); + } + + info("-".repeat(80)); + } + + yield finishTests(); +}); 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 000000000..419d63020 --- /dev/null +++ b/devtools/client/storage/test/browser_storage_sidebar_update.js @@ -0,0 +1,41 @@ +/* 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(function* () { + const ITEM_NAME = "ls1"; + const UPDATE_COUNT = 3; + + yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-complex-values.html"); + + let updated = gUI.once("sidebar-updated"); + yield selectTreeItem(["localStorage", "http://test1.example.org"]); + yield selectTableItem(ITEM_NAME); + yield updated; + + is(gUI.sidebar.hidden, false, "sidebar is visible"); + + // do several updates in a row and wait for them to finish + let updates = []; + for (let i = 0; i < UPDATE_COUNT; i++) { + info(`Performing update #${i}`); + updates.push(gUI.once("sidebar-updated")); + gUI.displayObjectSidebar(); + } + yield promise.all(updates); + + info("Updates performed, going to verify result"); + let parsedScope = gUI.view.getScopeAtIndex(1); + let elements = parsedScope.target.querySelectorAll( + `.name[value="${ITEM_NAME}"]`); + is(elements.length, 1, + `There is only one displayed variable named '${ITEM_NAME}'`); + + yield finishTests(); +}); 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 000000000..920ce350e --- /dev/null +++ b/devtools/client/storage/test/browser_storage_values.js @@ -0,0 +1,165 @@ +/* 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 = [ + ["cs2", [ + {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", value: "Session"}, + {name: "cs2.Secure", value: "false"}, + ]], + ["c1", [ + {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", 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], + ["c_encoded", [ + {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", "http://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], + [["sessionStorage", "http://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", "http://test1.example.org", "idb1", "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", "http://test1.example.org", "idb1", "obj2"]], + [1, [ + {name: 1, value: JSON.stringify({ + id2: 1, name: "foo", email: "foo@bar.com", extra: "baz" + })} + ]], + [null, [ + {name: "1.id2", value: "1"}, + {name: "1.name", value: "foo"}, + {name: "1.email", value: "foo@bar.com"}, + {name: "1.extra", value: "baz"}, + ], true] +]; + +add_task(function* () { + yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-complex-values.html"); + + gUI.tree.expandAll(); + + for (let item of testCases) { + info("clicking for item " + item); + + if (Array.isArray(item[0])) { + yield selectTreeItem(item[0]); + continue; + } else if (item[0]) { + yield selectTableItem(item[0]); + } + + yield findVariableViewProperties(item[1], item[2]); + } + + yield finishTests(); +}); diff --git a/devtools/client/storage/test/head.js b/devtools/client/storage/test/head.js new file mode 100644 index 000000000..9662393cf --- /dev/null +++ b/devtools/client/storage/test/head.js @@ -0,0 +1,840 @@ +/* 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 ../../framework/test/shared-head.js */ + +// shared-head.js handles imports, constants, and utility functions +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js", + this); + +const {TableWidget} = require("devtools/client/shared/widgets/TableWidget"); +const SPLIT_CONSOLE_PREF = "devtools.toolbox.splitconsoleEnabled"; +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 ALT_DOMAIN = "http://sectest1.example.org/" + PATH; +const ALT_DOMAIN_SECURED = "https://sectest1.example.org:443/" + PATH; + +var gToolbox, gPanelWindow, gWindow, 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 = gWindow = gUI = null; + Services.prefs.clearUserPref(STORAGE_PREF); + Services.prefs.clearUserPref(SPLIT_CONSOLE_PREF); + Services.prefs.clearUserPref(DUMPEMIT_PREF); + Services.prefs.clearUserPref(DEBUGGERLOG_PREF); + Services.prefs.clearUserPref(CACHES_ON_HTTP_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. 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 + * + * @return {Promise} A promise that resolves after storage inspector is ready + */ +function* openTabAndSetupStorage(url) { + let tab = yield addTab(url); + let content = tab.linkedBrowser.contentWindow; + + gWindow = content.wrappedJSObject; + + // Setup the async storages in main window and for all its iframes + yield ContentTask.spawn(gBrowser.selectedBrowser, null, function* () { + /** + * Get all windows including frames recursively. + * + * @param {Window} [baseWindow] + * The base window at which to start looking for child windows + * (optional). + * @return {Set} + * A set of windows. + */ + function getAllWindows(baseWindow) { + let windows = new Set(); + + let _getAllWindows = function (win) { + windows.add(win.wrappedJSObject); + + for (let i = 0; i < win.length; i++) { + _getAllWindows(win[i]); + } + }; + _getAllWindows(baseWindow); + + return windows; + } + + let windows = getAllWindows(content); + for (let win of windows) { + let readyState = win.document.readyState; + info(`Found a window: ${readyState}`); + if (readyState != "complete") { + yield new Promise(resolve => { + let onLoad = () => { + win.removeEventListener("load", onLoad); + resolve(); + }; + win.addEventListener("load", onLoad); + }); + } + if (win.setup) { + yield win.setup(); + } + } + }); + + // open storage inspector + return yield openStoragePanel(); +} + +/** + * Open the toolbox, with the storage tool visible. + * + * @param cb {Function} Optional callback, if you don't want to use the returned + * promise + * + * @return {Promise} a promise that resolves when the storage inspector is ready + */ +var openStoragePanel = Task.async(function* (cb) { + info("Opening the storage inspector"); + let target = TargetFactory.forTab(gBrowser.selectedTab); + + let storage, toolbox; + + // Checking if the toolbox and the storage are already loaded + // The storage-updated event should only be waited for if the storage + // isn't loaded yet + toolbox = gDevTools.getToolbox(target); + if (toolbox) { + storage = toolbox.getPanel("storage"); + if (storage) { + gPanelWindow = storage.panelWindow; + gUI = storage.UI; + gToolbox = toolbox; + info("Toolbox and storage already open"); + if (cb) { + return cb(storage, toolbox); + } + + return { + toolbox: toolbox, + storage: storage + }; + } + } + + info("Opening the toolbox"); + toolbox = yield gDevTools.showToolbox(target, "storage"); + 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; + + info("Waiting for the stores to update"); + yield gUI.once("store-objects-updated"); + + yield waitForToolboxFrameFocus(toolbox); + + if (cb) { + return cb(storage, toolbox); + } + + return { + toolbox: toolbox, + storage: storage + }; +}); + +/** + * Wait for the toolbox frame to receive focus after it loads + * + * @param toolbox {Toolbox} + * + * @return a promise that resolves when focus has been received + */ +function waitForToolboxFrameFocus(toolbox) { + info("Making sure that the toolbox's frame is focused"); + let def = promise.defer(); + waitForFocus(def.resolve, toolbox.win); + return def.promise; +} + +/** + * Forces GC, CC and Shrinking GC to get rid of disconnected docshells and + * windows. + */ +function forceCollections() { + Cu.forceGC(); + Cu.forceCC(); + Cu.forceShrinkingGC(); +} + +/** + * Cleans up and finishes the test + */ +function* finishTests() { + // Bug 1233497 makes it so that we can no longer yield CPOWs from Tasks. + // We work around this by calling clear() via a ContentTask instead. + yield ContentTask.spawn(gBrowser.selectedBrowser, null, function* () { + /** + * Get all windows including frames recursively. + * + * @param {Window} [baseWindow] + * The base window at which to start looking for child windows + * (optional). + * @return {Set} + * A set of windows. + */ + function getAllWindows(baseWindow) { + let windows = new Set(); + + let _getAllWindows = function (win) { + windows.add(win.wrappedJSObject); + + for (let i = 0; i < win.length; i++) { + _getAllWindows(win[i]); + } + }; + _getAllWindows(baseWindow); + + return windows; + } + + let windows = getAllWindows(content); + for (let win of windows) { + // Some windows (e.g., about: URLs) don't have storage available + try { + win.localStorage.clear(); + win.sessionStorage.clear(); + } catch (ex) { + // ignore + } + + if (win.clear) { + yield win.clear(); + } + } + }); + + Services.cookies.removeAll(); + forceCollections(); + finish(); +} + +// Sends a click event on the passed DOM node in an async manner +function* click(node) { + let def = promise.defer(); + + node.scrollIntoView(); + + // We need setTimeout here to allow any scrolling to complete before clicking + // the node. + setTimeout(() => { + node.click(); + def.resolve(); + }, 200); + + return def; +} + +/** + * 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) { + let root = options.rootVariable; + let expandTo = options.expandTo.split("."); + let lastDeferred = promise.defer(); + + function getNext(prop) { + let name = expandTo.shift(); + let newProp = prop.get(name); + + if (expandTo.length > 0) { + ok(newProp, "found property " + name); + if (newProp && newProp.expand) { + newProp.expand(); + getNext(newProp); + } else { + lastDeferred.reject(prop); + } + } else if (newProp) { + lastDeferred.resolve(newProp); + } else { + lastDeferred.reject(prop); + } + } + + if (root && root.expand) { + root.expand(); + getNext(root); + } else { + lastDeferred.resolve(root); + } + + return lastDeferred.promise; +} + +/** + * 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. + let expandRules = []; + let 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. + let outstanding = []; + + finder(rules, gUI.view, outstanding); + + // Process the rules that need to expand properties. + let lastStep = processExpandRules.bind(null, expandRules); + + // Return the results - a promise resolved to hold the updated ruleArray. + let 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 (let scope of view) { + for (let [, prop] of scope) { + for (let rule of rules) { + let matcher = matchVariablesViewProperty(prop, rule); + promises.push(matcher.then(onMatch.bind(null, prop, rule))); + } + } + } + } + + function processExpandRules(rules) { + let rule = rules.shift(); + if (!rule) { + return promise.resolve(null); + } + + let deferred = promise.defer(); + let expandOptions = { + rootVariable: gUI.view.getScopeAtIndex(parsed ? 1 : 0), + expandTo: rule.name + }; + + variablesViewExpandTo(expandOptions).then(function onSuccess(prop) { + let name = rule.name; + let lastName = name.split(".").pop(); + rule.name = lastName; + + let matched = matchVariablesViewProperty(prop, rule); + return matched.then(onMatch.bind(null, prop, rule)).then(function () { + rule.name = name; + }); + }, function onFailure() { + return promise.resolve(null); + }).then(processExpandRules.bind(null, rules)).then(function () { + deferred.resolve(null); + }); + + return deferred.promise; + } + + function onAllRulesMatched(rules) { + for (let rule of rules) { + let 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); + } + + if (rule.name) { + let 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); + } + + let 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 + */ +function* selectTreeItem(ids) { + /* If this item is already selected, return */ + if (gUI.tree.isSelected(ids)) { + return; + } + + let updated = gUI.once("store-objects-updated"); + gUI.tree.selectedItem = ids; + yield updated; +} + +/** + * Click selects a row in the table. + * + * @param {String} id + * The id of the row in the table widget + */ +function* selectTableItem(id) { + let selector = ".table-widget-cell[data-id='" + id + "']"; + let target = gPanelWindow.document.querySelector(selector); + ok(target, "table item found with ids " + id); + + yield click(target); + yield gUI.once("sidebar-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] 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 + "."); + + let deferred = promise.defer(); + + for (let [add, remove] of [ + ["addEventListener", "removeEventListener"], + ["addListener", "removeListener"], + ["on", "off"] + ]) { + if ((add in target) && (remove in target)) { + target[add](eventName, function onEvent(...aArgs) { + target[remove](eventName, onEvent, useCapture); + deferred.resolve.apply(deferred, aArgs); + }, useCapture); + break; + } + } + + return deferred.promise; +} + +/** + * 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) { + let cells = getRowCells(id, includeHidden); + let values = {}; + + for (let name in cells) { + let 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) { + let doc = gPanelWindow.document; + let table = gUI.table; + let item = doc.querySelector(".table-widget-column#" + table.uniqueId + + " .table-widget-cell[value='" + id + "']"); + + if (!item) { + ok(false, "Row id '" + id + "' exists"); + } + + let index = table.columns.get(table.uniqueId).visibleCellNodes.indexOf(item); + let cells = {}; + + for (let [name, column] of [...table.columns]) { + if (!includeHidden && column.column.parentNode.hidden) { + continue; + } + cells[name] = column.visibleCellNodes[index]; + } + + return cells; +} + +/** + * 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) { + let row = getRowValues(id, true); + + 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. + */ +function* editCell(id, column, newValue, validate = true) { + let row = getRowCells(id, true); + let editableFieldsEngine = gUI.table._editableFieldsEngine; + + editableFieldsEngine.edit(row[column]); + + yield typeWithTerminator(newValue, "VK_RETURN", 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) { + let row = getRowCells(id, true); + let editableFieldsEngine = gUI.table._editableFieldsEngine; + let cell = row[column]; + + info("Selecting row " + id); + gUI.table.selectedRow = id; + + info("Starting cell edit (" + id + ", " + column + ")"); + editableFieldsEngine.edit(cell); + + if (!selectText) { + let 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); +} + +/** + * 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) { + let columns = gUI.table.columns; + let column = columns.get(id); + + if (state) { + column.wrapper.removeAttribute("hidden"); + } else { + column.wrapper.setAttribute("hidden", true); + } +} + +/** + * Show or hide all columns. + * + * @param {Boolean} state + * true = show, false = hide + */ +function showAllColumns(state) { + let columns = gUI.table.columns; + + for (let [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. VK_RETURN or VK_TAB + * @param {Boolean} validate + * Validate result? Default true. + */ +function* typeWithTerminator(str, terminator, validate = true) { + let editableFieldsEngine = gUI.table._editableFieldsEngine; + let textbox = editableFieldsEngine.textbox; + let colName = textbox.closest(".table-widget-column").id; + + let changeExpected = str !== textbox.value; + + if (!changeExpected) { + return editableFieldsEngine.currentTarget.getAttribute("data-id"); + } + + info("Typing " + str); + EventUtils.sendString(str); + + info("Pressing " + terminator); + EventUtils.synthesizeKey(terminator, {}); + + if (validate) { + info("Validating results... waiting for ROW_EDIT event."); + let uniqueId = yield gUI.table.once(TableWidget.EVENTS.ROW_EDIT); + + checkCell(uniqueId, colName, str); + return uniqueId; + } + + return yield gUI.table.once(TableWidget.EVENTS.ROW_EDIT); +} + +function getCurrentEditorValue() { + let editableFieldsEngine = gUI.table._editableFieldsEngine; + let 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). + */ +function* checkState(state) { + for (let [store, names] of state) { + let storeName = store.join(" > "); + info(`Selecting tree item ${storeName}`); + yield selectTreeItem(store); + + let items = gUI.table.items; + + is(items.size, names.length, + `There is correct number of rows in ${storeName}`); + for (let name of names) { + 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 = Task.async(function* (panelWin, callback) { + info("Focusing search box"); + let searchBox = panelWin.document.getElementById("storage-searchbox"); + let focused = once(searchBox, "focus"); + + panelWin.focus(); + let strings = Services.strings.createBundle( + "chrome://devtools/locale/storage.properties"); + synthesizeKeyShortcut(strings.GetStringFromName("storage.filter.key")); + + yield focused; + + if (callback) { + callback(); + } +}); 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 000000000..80b14e287 --- /dev/null +++ b/devtools/client/storage/test/storage-cache-error.html @@ -0,0 +1,20 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Storage inspector test for handling errors in CacheStorage</title> +</head> +<body> +<script type="application/javascript;version=1.7"> +"use strict"; + +// Create an iframe with a javascript: source URL. Such iframes are +// considered untrusted by the CacheStorage. +let frameEl = document.createElement("iframe"); +document.body.appendChild(frameEl); + +window.frameContent = 'Hello World'; +frameEl.contentWindow.location.href = "javascript:parent.frameContent"; +</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 000000000..d96da1932 --- /dev/null +++ b/devtools/client/storage/test/storage-complex-values.html @@ -0,0 +1,123 @@ +<!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;version=1.7"> +"use strict"; +let partialHostname = location.hostname.match(/^[^.]+(\..*)$/)[1]; +let 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"); +// ... 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 +let s = "a".repeat(1000); +sessionStorage.setItem("ss5", `${s}=${s}=${s}=${s}&${s}=${s}&${s}`); +console.log("added cookies and stuff from main page"); + +let idbGenerator = function*() { + let request = indexedDB.open("idb1", 1); + request.onerror = function() { + throw new Error("error opening db connection"); + }; + let db = yield new Promise(done => { + request.onupgradeneeded = event => { + let db = event.target.result; + let 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 + yield new Promise(done => { + request.onsuccess = done; + }); + + let transaction = db.transaction(["obj1", "obj2"], "readwrite"); + let store1 = transaction.objectStore("obj1"); + let 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"}); + + db.close(); + + request = indexedDB.open("idb2", 1); + let db2 = yield new Promise(done => { + request.onupgradeneeded = event => { + let db2 = event.target.result; + let store3 = db2.createObjectStore("obj3", { keyPath: "id3" }); + store3.createIndex("name2", "name2", { unique: true }); + store3.transaction.oncomplete = () => { + done(db2); + }; + }; + }); + + // Prevents AbortError during close() + yield 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 = function*() { + yield idbGenerator(); +}; + +window.clear = function*() { + yield deleteDB("idb1"); + yield deleteDB("idb2"); + + dump("removed indexedDB data from " + document.location + "\n"); +}; +</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 000000000..97c15abaa --- /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;version=1.7"> + "use strict"; + let 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-empty-objectstores.html b/devtools/client/storage/test/storage-empty-objectstores.html new file mode 100644 index 000000000..096e90a32 --- /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;version=1.7"> + +window.setup = function* () { + let request = indexedDB.open("idb1", 1); + let db = yield new Promise((resolve, reject) => { + request.onerror = e => reject(Error("error opening db connection")); + request.onupgradeneeded = event => { + let db = event.target.result; + let store1 = db.createObjectStore("obj1", { keyPath: "id" }); + store1.createIndex("name", "name", { unique: false }); + store1.createIndex("email", "email", { unique: true }); + let store2 = db.createObjectStore("obj2", { keyPath: "id2" }); + store1.transaction.oncomplete = () => resolve(db); + }; + }); + + yield new Promise(resolve => request.onsuccess = resolve); + + let transaction = db.transaction(["obj1", "obj2"], "readwrite"); + let store1 = transaction.objectStore("obj1"); + let 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"}); + + yield new Promise(resolve => transaction.oncomplete = resolve); + + db.close(); + + request = indexedDB.open("idb2", 1); + let db2 = yield new Promise((resolve, reject) => { + request.onerror = e => reject(Error("error opening db2 connection")); + request.onupgradeneeded = event => resolve(event.target.result); + }); + + yield new Promise(resolve => request.onsuccess = resolve); + + db2.close(); + dump("added indexedDB items from main page\n"); +}; + +window.clear = function* () { + for (let dbName of ["idb1", "idb2"]) { + yield 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-idb-delete-blocked.html b/devtools/client/storage/test/storage-idb-delete-blocked.html new file mode 100644 index 000000000..ef7017f08 --- /dev/null +++ b/devtools/client/storage/test/storage-idb-delete-blocked.html @@ -0,0 +1,52 @@ +<!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;version=1.7"> + +let db; + +window.setup = function* () { + db = yield new Promise((resolve, reject) => { + let request = indexedDB.open("idb", 1); + + request.onsuccess = e => resolve(e.target.result); + request.onerror = e => reject(new Error("error opening db connection")); + + request.onupgradeneeded = e => { + let db = e.target.result; + let store = db.createObjectStore("obj", { keyPath: "id" }); + }; + }); + + dump("opened indexedDB\n"); +}; + +window.closeDb = function* () { + db.close(); +}; + +window.deleteDb = function* () { + yield new Promise((resolve, reject) => { + let request = indexedDB.deleteDatabase("idb"); + + request.onsuccess = resolve; + request.onerror = e => reject(new Error("error deleting db")); + }); +}; + +window.clear = function* () { + for (let dbName of ["idb1", "idb2"]) { + yield 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-listings.html b/devtools/client/storage/test/storage-listings.html new file mode 100644 index 000000000..de3054d3a --- /dev/null +++ b/devtools/client/storage/test/storage-listings.html @@ -0,0 +1,126 @@ +<!DOCTYPE HTML> +<html> +<!-- +Bug 970517 - 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;version=1.7"> +"use strict"; +let partialHostname = location.hostname.match(/^[^.]+(\..*)$/)[1]; +let cookieExpiresTime1 = 2000000000000; +let 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"); + +let idbGenerator = function*() { + let request = indexedDB.open("idb1", 1); + request.onerror = function() { + throw new Error("error opening db connection"); + }; + let db = yield new Promise(done => { + request.onupgradeneeded = event => { + let db = event.target.result; + let store1 = db.createObjectStore("obj1", { keyPath: "id" }); + store1.createIndex("name", "name", { unique: false }); + store1.createIndex("email", "email", { unique: true }); + let store2 = db.createObjectStore("obj2", { keyPath: "id2" }); + store1.transaction.oncomplete = () => { + done(db); + }; + }; + }); + + // Prevents AbortError + yield new Promise(done => { + request.onsuccess = done; + }); + + let transaction = db.transaction(["obj1", "obj2"], "readwrite"); + let store1 = transaction.objectStore("obj1"); + let 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() + yield new Promise(success => { + transaction.oncomplete = success; + }); + + db.close(); + + request = indexedDB.open("idb2", 1); + let db2 = yield new Promise(done => { + request.onupgradeneeded = event => { + let db2 = event.target.result; + let store3 = db2.createObjectStore("obj3", { keyPath: "id3" }); + store3.createIndex("name2", "name2", { unique: true }); + store3.transaction.oncomplete = () => { + done(db2); + } + }; + }); + // Prevents AbortError during close() + yield 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; + }); +} + +function fetchPut(cache, url) { + let response = yield fetch(url); + yield cache.put(url, response); +} + +let cacheGenerator = function*() { + let cache = yield caches.open("plop"); + yield fetchPut(cache, "404_cached_file.js"); + yield fetchPut(cache, "browser_storage_basic.js"); +}; + +window.setup = function*() { + yield idbGenerator(); + yield cacheGenerator(); +}; + +window.clear = function*() { + yield deleteDB("idb1"); + yield deleteDB("idb2"); + + yield 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 000000000..3a560b096 --- /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;version=1.7"> + "use strict"; + + 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 onload="setup()"> + </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 000000000..ee8db36e6 --- /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;version=1.8"> +"use strict"; + +for (let i = 0; i < 160; 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 000000000..1a84ff622 --- /dev/null +++ b/devtools/client/storage/test/storage-search.html @@ -0,0 +1,23 @@ +<!DOCTYPE HTML> +<html> +<!-- +Bug 1224115 - Storage Inspector table filtering +--> +<head> + <meta charset="utf-8"> + <title>Storage inspector table filtering test</title> +</head> +<body> +<script type="text/javascript;version=1.8"> +"use strict"; + +localStorage.setItem("01234", "56789"); +localStorage.setItem("ANIMAL", "hOrSe"); +localStorage.setItem("FOO", "bArBaz"); +localStorage.setItem("food", "energy bar"); +localStorage.setItem("money", "##$$$**"); +localStorage.setItem("sport", "football"); +localStorage.setItem("year", "2016"); +</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 000000000..8424fd4cd --- /dev/null +++ b/devtools/client/storage/test/storage-secured-iframe.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;version=1.7"> +"use strict"; +document.cookie = "sc1=foobar;"; +localStorage.setItem("iframe-s-ls1", "foobar"); +sessionStorage.setItem("iframe-s-ss1", "foobar-2"); +dump("added cookies and storage from secured iframe\n"); + +let idbGenerator = function*() { + let request = indexedDB.open("idb-s1", 1); + request.onerror = function() { + throw new Error("error opening db connection"); + }; + let db = yield new Promise(done => { + request.onupgradeneeded = event => { + let db = event.target.result; + let store1 = db.createObjectStore("obj-s1", { keyPath: "id" }); + store1.transaction.oncomplete = () => { + done(db); + }; + }; + }); + yield new Promise(done => { + request.onsuccess = done; + }); + + let transaction = db.transaction(["obj-s1"], "readwrite"); + let 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"}); + yield new Promise(success => { + transaction.oncomplete = success; + }); + + db.close(); + + request = indexedDB.open("idb-s2", 1); + let db2 = yield new Promise(done => { + request.onupgradeneeded = event => { + let db2 = event.target.result; + let store3 = + db2.createObjectStore("obj-s2", { keyPath: "id3", autoIncrement: true }); + store3.createIndex("name2", "name2", { unique: true }); + store3.transaction.oncomplete = () => { + done(db2); + }; + }; + }); + yield new Promise(done => { + request.onsuccess = done; + }); + + transaction = db2.transaction(["obj-s2"], "readwrite"); + let store3 = transaction.objectStore("obj-s2"); + store3.add({id3: 16, name2: "foo", email: "foo@bar.com"}); + yield 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 = function*() { + yield idbGenerator(); +}; + +window.clear = function*() { + yield deleteDB("idb-s1"); + yield 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 000000000..12de07e13 --- /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;version=1.7"> + "use strict"; + + 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 onload="setup()"> + </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 000000000..a69ffdfd1 --- /dev/null +++ b/devtools/client/storage/test/storage-unsecured-iframe.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 = "uc1=foobar; domain=.example.org; path=/"; +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 000000000..a009814b2 --- /dev/null +++ b/devtools/client/storage/test/storage-updates.html @@ -0,0 +1,64 @@ +<!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;version=1.7"> +"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*() { + sessionStorage.clear(); + + dump("removed sessionStorage from " + document.location + "\n"); +}; + +window.onload = function() { + addCookie("c1", "1.2.3.4.5.6.7", "/browser"); + addCookie("c2", "foobar", "/browser"); + + localStorage.setItem("ls1", "testing"); + localStorage.setItem("ls2", "testing"); + localStorage.setItem("ls3", "testing"); + localStorage.setItem("ls4", "testing"); + localStorage.setItem("ls5", "testing"); + localStorage.setItem("ls6", "testing"); + localStorage.setItem("ls7", "testing"); + + sessionStorage.setItem("ss1", "foobar"); + sessionStorage.setItem("ss2", "foobar"); + sessionStorage.setItem("ss3", "foobar"); +}; +</script> +</body> +</html> diff --git a/devtools/client/storage/ui.js b/devtools/client/storage/ui.js new file mode 100644 index 000000000..6af493e44 --- /dev/null +++ b/devtools/client/storage/ui.js @@ -0,0 +1,1073 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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"; + +const {Task} = require("devtools/shared/task"); +const EventEmitter = require("devtools/shared/event-emitter"); +const {LocalizationHelper, ELLIPSIS} = require("devtools/shared/l10n"); +const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts"); +const JSOL = require("devtools/client/shared/vendor/jsol"); +const {KeyCodes} = require("devtools/client/shared/keycodes"); + +loader.lazyRequireGetter(this, "TreeWidget", + "devtools/client/shared/widgets/TreeWidget", true); +loader.lazyRequireGetter(this, "TableWidget", + "devtools/client/shared/widgets/TableWidget", true); +loader.lazyRequireGetter(this, "ViewHelpers", + "devtools/client/shared/widgets/view-helpers"); +loader.lazyImporter(this, "VariablesView", + "resource://devtools/client/shared/widgets/VariablesView.jsm"); + +/** + * Localization convenience methods. + */ +const STORAGE_STRINGS = "devtools/client/locales/storage.properties"; +const L10N = new LocalizationHelper(STORAGE_STRINGS); + +const GENERIC_VARIABLES_VIEW_SETTINGS = { + lazyEmpty: true, + // ms + lazyEmptyDelay: 10, + searchEnabled: true, + searchPlaceholder: L10N.getStr("storage.search.placeholder"), + preventDescriptorModifiers: true +}; + +// Columns which are hidden by default in the storage table +const HIDDEN_COLUMNS = [ + "creationTime", + "isDomain", + "isSecure" +]; + +const REASON = { + NEW_ROW: "new-row", + NEXT_50_ITEMS: "next-50-items", + POPULATE: "populate", + UPDATE: "update" +}; + +const COOKIE_KEY_MAP = { + path: "Path", + host: "Domain", + expires: "Expires", + isSecure: "Secure", + isHttpOnly: "HttpOnly", + isDomain: "HostOnly", + creationTime: "CreationTime", + lastAccessed: "LastAccessed" +}; + +// Maximum length of item name to show in context menu label - will be +// trimmed with ellipsis if it's longer. +const ITEM_NAME_MAX_LENGTH = 32; + +function addEllipsis(name) { + if (name.length > ITEM_NAME_MAX_LENGTH) { + if (/^https?:/.test(name)) { + // For URLs, add ellipsis in the middle + const halfLen = ITEM_NAME_MAX_LENGTH / 2; + return name.slice(0, halfLen) + ELLIPSIS + name.slice(-halfLen); + } + + // For other strings, add ellipsis at the end + return name.substr(0, ITEM_NAME_MAX_LENGTH) + ELLIPSIS; + } + + return name; +} + +/** + * StorageUI is controls and builds the UI of the Storage Inspector. + * + * @param {Front} front + * Front for the storage actor + * @param {Target} target + * Interface for the page we're debugging + * @param {Window} panelWin + * Window of the toolbox panel to populate UI in. + */ +function StorageUI(front, target, panelWin, toolbox) { + EventEmitter.decorate(this); + + this._target = target; + this._window = panelWin; + this._panelDoc = panelWin.document; + this._toolbox = toolbox; + this.front = front; + + let treeNode = this._panelDoc.getElementById("storage-tree"); + this.tree = new TreeWidget(treeNode, { + defaultType: "dir", + contextMenuId: "storage-tree-popup" + }); + this.onHostSelect = this.onHostSelect.bind(this); + this.tree.on("select", this.onHostSelect); + + let tableNode = this._panelDoc.getElementById("storage-table"); + this.table = new TableWidget(tableNode, { + emptyText: L10N.getStr("table.emptyText"), + highlightUpdated: true, + cellContextMenuId: "storage-table-popup" + }); + + this.displayObjectSidebar = this.displayObjectSidebar.bind(this); + this.table.on(TableWidget.EVENTS.ROW_SELECTED, this.displayObjectSidebar); + + this.handleScrollEnd = this.handleScrollEnd.bind(this); + this.table.on(TableWidget.EVENTS.SCROLL_END, this.handleScrollEnd); + + this.editItem = this.editItem.bind(this); + this.table.on(TableWidget.EVENTS.CELL_EDIT, this.editItem); + + this.sidebar = this._panelDoc.getElementById("storage-sidebar"); + this.sidebar.setAttribute("width", "300"); + this.view = new VariablesView(this.sidebar.firstChild, + GENERIC_VARIABLES_VIEW_SETTINGS); + + this.searchBox = this._panelDoc.getElementById("storage-searchbox"); + this.filterItems = this.filterItems.bind(this); + this.searchBox.addEventListener("command", this.filterItems); + + let shortcuts = new KeyShortcuts({ + window: this._panelDoc.defaultView, + }); + let key = L10N.getStr("storage.filter.key"); + shortcuts.on(key, (name, event) => { + event.preventDefault(); + this.searchBox.focus(); + }); + + this.front.listStores().then(storageTypes => { + this.populateStorageTree(storageTypes); + }).then(null, console.error); + + this.onUpdate = this.onUpdate.bind(this); + this.front.on("stores-update", this.onUpdate); + this.onCleared = this.onCleared.bind(this); + this.front.on("stores-cleared", this.onCleared); + + this.handleKeypress = this.handleKeypress.bind(this); + this._panelDoc.addEventListener("keypress", this.handleKeypress); + + this.onTreePopupShowing = this.onTreePopupShowing.bind(this); + this._treePopup = this._panelDoc.getElementById("storage-tree-popup"); + this._treePopup.addEventListener("popupshowing", this.onTreePopupShowing); + + this.onTablePopupShowing = this.onTablePopupShowing.bind(this); + this._tablePopup = this._panelDoc.getElementById("storage-table-popup"); + this._tablePopup.addEventListener("popupshowing", this.onTablePopupShowing); + + this.onRemoveItem = this.onRemoveItem.bind(this); + this.onRemoveAllFrom = this.onRemoveAllFrom.bind(this); + this.onRemoveAll = this.onRemoveAll.bind(this); + this.onRemoveTreeItem = this.onRemoveTreeItem.bind(this); + + this._tablePopupDelete = this._panelDoc.getElementById( + "storage-table-popup-delete"); + this._tablePopupDelete.addEventListener("command", this.onRemoveItem); + + this._tablePopupDeleteAllFrom = this._panelDoc.getElementById( + "storage-table-popup-delete-all-from"); + this._tablePopupDeleteAllFrom.addEventListener("command", + this.onRemoveAllFrom); + + this._tablePopupDeleteAll = this._panelDoc.getElementById( + "storage-table-popup-delete-all"); + this._tablePopupDeleteAll.addEventListener("command", this.onRemoveAll); + + this._treePopupDeleteAll = this._panelDoc.getElementById( + "storage-tree-popup-delete-all"); + this._treePopupDeleteAll.addEventListener("command", this.onRemoveAll); + + this._treePopupDelete = this._panelDoc.getElementById("storage-tree-popup-delete"); + this._treePopupDelete.addEventListener("command", this.onRemoveTreeItem); +} + +exports.StorageUI = StorageUI; + +StorageUI.prototype = { + + storageTypes: null, + shouldLoadMoreItems: true, + + set animationsEnabled(value) { + this._panelDoc.documentElement.classList.toggle("no-animate", !value); + }, + + destroy: function () { + this.table.off(TableWidget.EVENTS.ROW_SELECTED, this.displayObjectSidebar); + this.table.off(TableWidget.EVENTS.SCROLL_END, this.handleScrollEnd); + this.table.off(TableWidget.EVENTS.CELL_EDIT, this.editItem); + this.table.destroy(); + + this.front.off("stores-update", this.onUpdate); + this.front.off("stores-cleared", this.onCleared); + this._panelDoc.removeEventListener("keypress", this.handleKeypress); + this.searchBox.removeEventListener("input", this.filterItems); + this.searchBox = null; + + this._treePopup.removeEventListener("popupshowing", this.onTreePopupShowing); + this._treePopupDeleteAll.removeEventListener("command", this.onRemoveAll); + this._treePopupDelete.removeEventListener("command", this.onRemoveTreeItem); + + this._tablePopup.removeEventListener("popupshowing", this.onTablePopupShowing); + this._tablePopupDelete.removeEventListener("command", this.onRemoveItem); + this._tablePopupDeleteAllFrom.removeEventListener("command", this.onRemoveAllFrom); + this._tablePopupDeleteAll.removeEventListener("command", this.onRemoveAll); + }, + + /** + * Empties and hides the object viewer sidebar + */ + hideSidebar: function () { + this.view.empty(); + this.sidebar.hidden = true; + this.table.clearSelection(); + }, + + getCurrentActor: function () { + let type = this.table.datatype; + + return this.storageTypes[type]; + }, + + /** + * Make column fields editable + * + * @param {Array} editableFields + * An array of keys of columns to be made editable + */ + makeFieldsEditable: function* (editableFields) { + if (editableFields && editableFields.length > 0) { + this.table.makeFieldsEditable(editableFields); + } else if (this.table._editableFieldsEngine) { + this.table._editableFieldsEngine.destroy(); + } + }, + + editItem: function (eventType, data) { + let actor = this.getCurrentActor(); + + actor.editItem(data); + }, + + /** + * Removes the given item from the storage table. Reselects the next item in + * the table and repopulates the sidebar with that item's data if the item + * being removed was selected. + */ + removeItemFromTable: function (name) { + if (this.table.isSelected(name)) { + if (this.table.selectedIndex == 0) { + this.table.selectNextRow(); + } else { + this.table.selectPreviousRow(); + } + this.table.remove(name); + this.displayObjectSidebar(); + } else { + this.table.remove(name); + } + }, + + /** + * Event handler for "stores-cleared" event coming from the storage actor. + * + * @param {object} response + * An object containing which storage types were cleared + */ + onCleared: function (response) { + function* enumPaths() { + for (let type in response) { + if (Array.isArray(response[type])) { + // Handle the legacy response with array of hosts + for (let host of response[type]) { + yield [type, host]; + } + } else { + // Handle the new format that supports clearing sub-stores in a host + for (let host in response[type]) { + let paths = response[type][host]; + + if (!paths.length) { + yield [type, host]; + } else { + for (let path of paths) { + try { + path = JSON.parse(path); + yield [type, host, ...path]; + } catch (ex) { + // ignore + } + } + } + } + } + } + } + + for (let path of enumPaths()) { + // Find if the path is selected (there is max one) and clear it + if (this.tree.isSelected(path)) { + this.table.clear(); + this.hideSidebar(); + this.emit("store-objects-cleared"); + break; + } + } + }, + + /** + * Event handler for "stores-update" event coming from the storage actor. + * + * @param {object} argument0 + * An object containing the details of the added, changed and deleted + * storage objects. + * Each of these 3 objects are of the following format: + * { + * <store_type1>: { + * <host1>: [<store_names1>, <store_name2>...], + * <host2>: [<store_names34>...], ... + * }, + * <store_type2>: { + * <host1>: [<store_names1>, <store_name2>...], + * <host2>: [<store_names34>...], ... + * }, ... + * } + * Where store_type1 and store_type2 is one of cookies, indexedDB, + * sessionStorage and localStorage; host1, host2 are the host in which + * this change happened; and [<store_namesX] is an array of the names + * of the changed store objects. This array is empty for deleted object + * if the host was completely removed. + */ + onUpdate: function ({ changed, added, deleted }) { + if (deleted) { + this.handleDeletedItems(deleted); + } + + if (added) { + this.handleAddedItems(added); + } + + if (changed) { + this.handleChangedItems(changed); + } + + if (added || deleted || changed) { + this.emit("store-objects-updated"); + } + }, + + /** + * Handle added items received by onUpdate + * + * @param {object} See onUpdate docs + */ + handleAddedItems: function (added) { + for (let type in added) { + for (let host in added[type]) { + this.tree.add([type, {id: host, type: "url"}]); + for (let name of added[type][host]) { + try { + name = JSON.parse(name); + if (name.length == 3) { + name.splice(2, 1); + } + this.tree.add([type, host, ...name]); + if (!this.tree.selectedItem) { + this.tree.selectedItem = [type, host, name[0], name[1]]; + this.fetchStorageObjects(type, host, [JSON.stringify(name)], + REASON.NEW_ROW); + } + } catch (ex) { + // Do nothing + } + } + + if (this.tree.isSelected([type, host])) { + this.fetchStorageObjects(type, host, added[type][host], + REASON.NEW_ROW); + } + } + } + }, + + /** + * Handle deleted items received by onUpdate + * + * @param {object} See onUpdate docs + */ + handleDeletedItems: function (deleted) { + for (let type in deleted) { + for (let host in deleted[type]) { + if (!deleted[type][host].length) { + // This means that the whole host is deleted, thus the item should + // be removed from the storage tree + if (this.tree.isSelected([type, host])) { + this.table.clear(); + this.hideSidebar(); + this.tree.selectPreviousItem(); + } + + this.tree.remove([type, host]); + } else { + for (let name of deleted[type][host]) { + try { + // trying to parse names in case of indexedDB or cache + let names = JSON.parse(name); + // Is a whole cache, database or objectstore deleted? + // Then remove it from the tree. + if (names.length < 3) { + if (this.tree.isSelected([type, host, ...names])) { + this.table.clear(); + this.hideSidebar(); + this.tree.selectPreviousItem(); + } + this.tree.remove([type, host, ...names]); + } + + // Remove the item from table if currently displayed. + if (names.length > 0) { + let tableItemName = names.pop(); + if (this.tree.isSelected([type, host, ...names])) { + this.removeItemFromTable(tableItemName); + } + } + } catch (ex) { + if (this.tree.isSelected([type, host])) { + this.removeItemFromTable(name); + } + } + } + } + } + } + }, + + /** + * Handle changed items received by onUpdate + * + * @param {object} See onUpdate docs + */ + handleChangedItems: function (changed) { + let [type, host, db, objectStore] = this.tree.selectedItem; + if (!changed[type] || !changed[type][host] || + changed[type][host].length == 0) { + return; + } + try { + let toUpdate = []; + for (let name of changed[type][host]) { + let names = JSON.parse(name); + if (names[0] == db && names[1] == objectStore && names[2]) { + toUpdate.push(name); + } + } + this.fetchStorageObjects(type, host, toUpdate, REASON.UPDATE); + } catch (ex) { + this.fetchStorageObjects(type, host, changed[type][host], REASON.UPDATE); + } + }, + + /** + * Fetches the storage objects from the storage actor and populates the + * storage table with the returned data. + * + * @param {string} type + * The type of storage. Ex. "cookies" + * @param {string} host + * Hostname + * @param {array} names + * Names of particular store objects. Empty if all are requested + * @param {Constant} reason + * See REASON constant at top of file. + */ + fetchStorageObjects: Task.async(function* (type, host, names, reason) { + let fetchOpts = reason === REASON.NEXT_50_ITEMS ? {offset: this.itemOffset} + : {}; + let storageType = this.storageTypes[type]; + + if (reason !== REASON.NEXT_50_ITEMS && + reason !== REASON.UPDATE && + reason !== REASON.NEW_ROW && + reason !== REASON.POPULATE) { + throw new Error("Invalid reason specified"); + } + + try { + if (reason === REASON.POPULATE) { + let subType = null; + // The indexedDB type could have sub-type data to fetch. + // If having names specified, then it means + // we are fetching details of specific database or of object store. + if (type == "indexedDB" && names) { + let [ dbName, objectStoreName ] = JSON.parse(names[0]); + if (dbName) { + subType = "database"; + } + if (objectStoreName) { + subType = "object store"; + } + } + yield this.resetColumns(type, host, subType); + } + + let {data} = yield storageType.getStoreObjects(host, names, fetchOpts); + if (data.length) { + this.populateTable(data, reason); + } + this.emit("store-objects-updated"); + } catch (ex) { + console.error(ex); + } + }), + + /** + * Populates the storage tree which displays the list of storages present for + * the page. + * + * @param {object} storageTypes + * List of storages and their corresponding hosts returned by the + * StorageFront.listStores call. + */ + populateStorageTree: function (storageTypes) { + this.storageTypes = {}; + for (let type in storageTypes) { + // Ignore `from` field, which is just a protocol.js implementation + // artifact. + if (type === "from") { + continue; + } + let typeLabel = type; + try { + typeLabel = L10N.getStr("tree.labels." + type); + } catch (e) { + console.error("Unable to localize tree label type:" + type); + } + this.tree.add([{id: type, label: typeLabel, type: "store"}]); + if (!storageTypes[type].hosts) { + continue; + } + this.storageTypes[type] = storageTypes[type]; + for (let host in storageTypes[type].hosts) { + this.tree.add([type, {id: host, type: "url"}]); + for (let name of storageTypes[type].hosts[host]) { + try { + let names = JSON.parse(name); + this.tree.add([type, host, ...names]); + if (!this.tree.selectedItem) { + this.tree.selectedItem = [type, host, names[0], names[1]]; + } + } catch (ex) { + // Do Nothing + } + } + if (!this.tree.selectedItem) { + this.tree.selectedItem = [type, host]; + } + } + } + }, + + /** + * Populates the selected entry from teh table in the sidebar for a more + * detailed view. + */ + displayObjectSidebar: Task.async(function* () { + let item = this.table.selectedRow; + if (!item) { + // Make sure that sidebar is hidden and return + this.sidebar.hidden = true; + return; + } + + // Get the string value (async action) and the update the UI synchronously. + let value; + if (item.name && item.valueActor) { + value = yield item.valueActor.string(); + } + + // Start updating the UI. Everything is sync beyond this point. + this.sidebar.hidden = false; + this.view.empty(); + let mainScope = this.view.addScope(L10N.getStr("storage.data.label")); + mainScope.expanded = true; + + if (value) { + let itemVar = mainScope.addItem(item.name + "", {}, {relaxed: true}); + + // The main area where the value will be displayed + itemVar.setGrip(value); + + // May be the item value is a json or a key value pair itself + this.parseItemValue(item.name, value); + + // By default the item name and value are shown. If this is the only + // information available, then nothing else is to be displayed. + let itemProps = Object.keys(item); + if (itemProps.length > 3) { + // Display any other information other than the item name and value + // which may be available. + let rawObject = Object.create(null); + let otherProps = itemProps.filter( + e => !["name", "value", "valueActor"].includes(e)); + for (let prop of otherProps) { + let cookieProp = COOKIE_KEY_MAP[prop] || prop; + // The pseduo property of HostOnly refers to converse of isDomain property + rawObject[cookieProp] = (prop === "isDomain") ? !item[prop] : item[prop]; + } + itemVar.populate(rawObject, {sorted: true}); + itemVar.twisty = true; + itemVar.expanded = true; + } + } else { + // Case when displaying IndexedDB db/object store properties. + for (let key in item) { + mainScope.addItem(key, {}, true).setGrip(item[key]); + this.parseItemValue(key, item[key]); + } + } + + this.emit("sidebar-updated"); + }), + + /** + * Tries to parse a string value into either a json or a key-value separated + * object and populates the sidebar with the parsed value. The value can also + * be a key separated array. + * + * @param {string} name + * The key corresponding to the `value` string in the object + * @param {string} value + * The string to be parsed into an object + */ + parseItemValue: function (name, originalValue) { + // Find if value is URLEncoded ie + let decodedValue = ""; + try { + decodedValue = decodeURIComponent(originalValue); + } catch (e) { + // Unable to decode, nothing to do + } + let value = (decodedValue && decodedValue !== originalValue) + ? decodedValue : originalValue; + + let json = null; + try { + json = JSOL.parse(value); + } catch (ex) { + json = null; + } + + if (!json && value) { + json = this._extractKeyValPairs(value); + } + + // return if json is null, or same as value, or just a string. + if (!json || json == value || typeof json == "string") { + return; + } + + // One special case is a url which gets separated as key value pair on : + if ((json.length == 2 || Object.keys(json).length == 1) && + ((json[0] || Object.keys(json)[0]) + "").match(/^(http|file|ftp)/)) { + return; + } + + let jsonObject = Object.create(null); + let view = this.view; + jsonObject[name] = json; + let valueScope = view.getScopeAtIndex(1) || + view.addScope(L10N.getStr("storage.parsedValue.label")); + valueScope.expanded = true; + let jsonVar = valueScope.addItem("", Object.create(null), {relaxed: true}); + jsonVar.expanded = true; + jsonVar.twisty = true; + jsonVar.populate(jsonObject, {expanded: true}); + }, + + /** + * Tries to parse a string into an object on the basis of key-value pairs, + * separated by various separators. If failed, tries to parse for single + * separator separated values to form an array. + * + * @param {string} value + * The string to be parsed into an object or array + */ + _extractKeyValPairs: function (value) { + let makeObject = (keySep, pairSep) => { + let object = {}; + for (let pair of value.split(pairSep)) { + let [key, val] = pair.split(keySep); + object[key] = val; + } + return object; + }; + + // Possible separators. + const separators = ["=", ":", "~", "#", "&", "\\*", ",", "\\."]; + // Testing for object + for (let i = 0; i < separators.length; i++) { + let kv = separators[i]; + for (let j = 0; j < separators.length; j++) { + if (i == j) { + continue; + } + let p = separators[j]; + let word = `[^${kv}${p}]*`; + let keyValue = `${word}${kv}${word}`; + let keyValueList = `${keyValue}(${p}${keyValue})*`; + let regex = new RegExp(`^${keyValueList}$`); + if (value.match && value.match(regex) && value.includes(kv) && + (value.includes(p) || value.split(kv).length == 2)) { + return makeObject(kv, p); + } + } + } + // Testing for array + for (let p of separators) { + let word = `[^${p}]*`; + let wordList = `(${word}${p})+${word}`; + let regex = new RegExp(`^${wordList}$`); + if (value.match && value.match(regex)) { + return value.split(p.replace(/\\*/g, "")); + } + } + return null; + }, + + /** + * Select handler for the storage tree. Fetches details of the selected item + * from the storage details and populates the storage tree. + * + * @param {string} event + * The name of the event fired + * @param {array} item + * An array of ids which represent the location of the selected item in + * the storage tree + */ + onHostSelect: function (event, item) { + this.table.clear(); + this.hideSidebar(); + this.searchBox.value = ""; + + let [type, host] = item; + let names = null; + if (!host) { + return; + } + if (item.length > 2) { + names = [JSON.stringify(item.slice(2))]; + } + this.fetchStorageObjects(type, host, names, REASON.POPULATE); + this.itemOffset = 0; + }, + + /** + * Resets the column headers in the storage table with the pased object `data` + * + * @param {string} type + * The type of storage corresponding to the after-reset columns in the + * table. + * @param {string} host + * The host name corresponding to the table after reset. + * + * @param {string} [subType] + * The sub type under the given type. + */ + resetColumns: function* (type, host, subtype) { + this.table.host = host; + this.table.datatype = type; + + let uniqueKey = null; + let columns = {}; + let editableFields = []; + let fields = yield this.getCurrentActor().getFields(subtype); + + fields.forEach(f => { + if (!uniqueKey) { + this.table.uniqueId = uniqueKey = f.name; + } + + if (f.editable) { + editableFields.push(f.name); + } + + columns[f.name] = f.name; + let columnName; + try { + columnName = L10N.getStr("table.headers." + type + "." + f.name); + } catch (e) { + columnName = COOKIE_KEY_MAP[f.name]; + } + + if (!columnName) { + console.error("Unable to localize table header type:" + type + " key:" + f.name); + } else { + columns[f.name] = columnName; + } + }); + + this.table.setColumns(columns, null, HIDDEN_COLUMNS); + this.hideSidebar(); + + yield this.makeFieldsEditable(editableFields); + }, + + /** + * Populates or updates the rows in the storage table. + * + * @param {array[object]} data + * Array of objects to be populated in the storage table + * @param {Constant} reason + * See REASON constant at top of file. + */ + populateTable: function (data, reason) { + for (let item of data) { + if (item.value) { + item.valueActor = item.value; + item.value = item.value.initial || ""; + } + if (item.expires != null) { + item.expires = item.expires + ? new Date(item.expires).toUTCString() + : L10N.getStr("label.expires.session"); + } + if (item.creationTime != null) { + item.creationTime = new Date(item.creationTime).toUTCString(); + } + if (item.lastAccessed != null) { + item.lastAccessed = new Date(item.lastAccessed).toUTCString(); + } + + switch (reason) { + case REASON.POPULATE: + // Update without flashing the row. + this.table.push(item, true); + break; + case REASON.NEW_ROW: + case REASON.NEXT_50_ITEMS: + // Update and flash the row. + this.table.push(item, false); + break; + case REASON.UPDATE: + this.table.update(item); + if (item == this.table.selectedRow && !this.sidebar.hidden) { + this.displayObjectSidebar(); + } + break; + } + + this.shouldLoadMoreItems = true; + } + }, + + /** + * Handles keypress event on the body table to close the sidebar when open + * + * @param {DOMEvent} event + * The event passed by the keypress event. + */ + handleKeypress: function (event) { + if (event.keyCode == KeyCodes.DOM_VK_ESCAPE && !this.sidebar.hidden) { + // Stop Propagation to prevent opening up of split console + this.hideSidebar(); + event.stopPropagation(); + event.preventDefault(); + } + }, + + /** + * Handles filtering the table + */ + filterItems() { + let value = this.searchBox.value; + this.table.filterItems(value, ["valueActor"]); + this._panelDoc.documentElement.classList.toggle("filtering", !!value); + }, + + /** + * Handles endless scrolling for the table + */ + handleScrollEnd: function () { + if (!this.shouldLoadMoreItems) { + return; + } + this.shouldLoadMoreItems = false; + this.itemOffset += 50; + + let item = this.tree.selectedItem; + let [type, host] = item; + let names = null; + if (item.length > 2) { + names = [JSON.stringify(item.slice(2))]; + } + this.fetchStorageObjects(type, host, names, REASON.NEXT_50_ITEMS); + }, + + /** + * Fires before a cell context menu with the "Delete" action is shown. + * If the currently selected storage object doesn't support removing items, prevent + * showing the menu. + */ + onTablePopupShowing: function (event) { + let selectedItem = this.tree.selectedItem; + let type = selectedItem[0]; + let actor = this.getCurrentActor(); + + // IndexedDB only supports removing items from object stores (level 4 of the tree) + if (!actor.removeItem || (type === "indexedDB" && selectedItem.length !== 4)) { + event.preventDefault(); + return; + } + + let rowId = this.table.contextMenuRowId; + let data = this.table.items.get(rowId); + let name = addEllipsis(data[this.table.uniqueId]); + + this._tablePopupDelete.setAttribute("label", + L10N.getFormatStr("storage.popupMenu.deleteLabel", name)); + + if (type === "cookies") { + let host = addEllipsis(data.host); + + this._tablePopupDeleteAllFrom.hidden = false; + this._tablePopupDeleteAllFrom.setAttribute("label", + L10N.getFormatStr("storage.popupMenu.deleteAllFromLabel", host)); + } else { + this._tablePopupDeleteAllFrom.hidden = true; + } + }, + + onTreePopupShowing: function (event) { + let showMenu = false; + let selectedItem = this.tree.selectedItem; + + if (selectedItem) { + let type = selectedItem[0]; + let actor = this.storageTypes[type]; + + // The delete all (aka clear) action is displayed for IndexedDB object stores + // (level 4 of tree), for Cache objects (level 3) and for the whole host (level 2) + // for other storage types (cookies, localStorage, ...). + let showDeleteAll = false; + if (actor.removeAll) { + let level; + if (type == "indexedDB") { + level = 4; + } else if (type == "Cache") { + level = 3; + } else { + level = 2; + } + + if (selectedItem.length == level) { + showDeleteAll = true; + } + } + + this._treePopupDeleteAll.hidden = !showDeleteAll; + + // The delete action is displayed for: + // - IndexedDB databases (level 3 of the tree) + // - Cache objects (level 3 of the tree) + let showDelete = (type == "indexedDB" || type == "Cache") && + selectedItem.length == 3; + this._treePopupDelete.hidden = !showDelete; + if (showDelete) { + let itemName = addEllipsis(selectedItem[selectedItem.length - 1]); + this._treePopupDelete.setAttribute("label", + L10N.getFormatStr("storage.popupMenu.deleteLabel", itemName)); + } + + showMenu = showDeleteAll || showDelete; + } + + if (!showMenu) { + event.preventDefault(); + } + }, + + /** + * Handles removing an item from the storage + */ + onRemoveItem: function () { + let [, host, ...path] = this.tree.selectedItem; + let actor = this.getCurrentActor(); + let rowId = this.table.contextMenuRowId; + let data = this.table.items.get(rowId); + let name = data[this.table.uniqueId]; + if (path.length > 0) { + name = JSON.stringify([...path, name]); + } + actor.removeItem(host, name); + }, + + /** + * Handles removing all items from the storage + */ + onRemoveAll: function () { + // Cannot use this.currentActor() if the handler is called from the + // tree context menu: it returns correct value only after the table + // data from server are successfully fetched (and that's async). + let [type, host, ...path] = this.tree.selectedItem; + let actor = this.storageTypes[type]; + let name = path.length > 0 ? JSON.stringify(path) : undefined; + actor.removeAll(host, name); + }, + + /** + * Handles removing all cookies with exactly the same domain as the + * cookie in the selected row. + */ + onRemoveAllFrom: function () { + let [, host] = this.tree.selectedItem; + let actor = this.getCurrentActor(); + let rowId = this.table.contextMenuRowId; + let data = this.table.items.get(rowId); + + actor.removeAll(host, data.host); + }, + + onRemoveTreeItem: function () { + let [type, host, ...path] = this.tree.selectedItem; + + if (type == "indexedDB" && path.length == 1) { + this.removeDatabase(host, path[0]); + } else if (type == "Cache" && path.length == 1) { + this.removeCache(host, path[0]); + } + }, + + removeDatabase: function (host, dbName) { + let actor = this.storageTypes.indexedDB; + + actor.removeDatabase(host, dbName).then(result => { + if (result.blocked) { + let notificationBox = this._toolbox.getNotificationBox(); + notificationBox.appendNotification( + L10N.getFormatStr("storage.idb.deleteBlocked", dbName), + "storage-idb-delete-blocked", + null, + notificationBox.PRIORITY_WARNING_LOW); + } + }).catch(error => { + let notificationBox = this._toolbox.getNotificationBox(); + notificationBox.appendNotification( + L10N.getFormatStr("storage.idb.deleteError", dbName), + "storage-idb-delete-error", + null, + notificationBox.PRIORITY_CRITICAL_LOW); + }); + }, + + removeCache: function (host, cacheName) { + let actor = this.storageTypes.Cache; + + actor.removeItem(host, JSON.stringify([ cacheName ])); + }, +}; |