summaryrefslogtreecommitdiffstats
path: root/devtools/client/storage
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /devtools/client/storage
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-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')
-rw-r--r--devtools/client/storage/moz.build12
-rw-r--r--devtools/client/storage/panel.js87
-rw-r--r--devtools/client/storage/storage.xul58
-rw-r--r--devtools/client/storage/test/.eslintrc.js6
-rw-r--r--devtools/client/storage/test/browser.ini44
-rw-r--r--devtools/client/storage/test/browser_storage_basic.js118
-rw-r--r--devtools/client/storage/test/browser_storage_cache_delete.js46
-rw-r--r--devtools/client/storage/test/browser_storage_cache_error.js19
-rw-r--r--devtools/client/storage/test/browser_storage_cookies_delete_all.js74
-rw-r--r--devtools/client/storage/test/browser_storage_cookies_domain.js21
-rw-r--r--devtools/client/storage/test/browser_storage_cookies_edit.js22
-rw-r--r--devtools/client/storage/test/browser_storage_cookies_edit_keyboard.js23
-rw-r--r--devtools/client/storage/test/browser_storage_cookies_tab_navigation.js24
-rw-r--r--devtools/client/storage/test/browser_storage_delete.js56
-rw-r--r--devtools/client/storage/test/browser_storage_delete_all.js90
-rw-r--r--devtools/client/storage/test/browser_storage_delete_tree.js67
-rw-r--r--devtools/client/storage/test/browser_storage_dynamic_updates.js213
-rw-r--r--devtools/client/storage/test/browser_storage_empty_objectstores.js77
-rw-r--r--devtools/client/storage/test/browser_storage_indexeddb_delete.js47
-rw-r--r--devtools/client/storage/test/browser_storage_indexeddb_delete_blocked.js58
-rw-r--r--devtools/client/storage/test/browser_storage_localstorage_edit.js24
-rw-r--r--devtools/client/storage/test/browser_storage_localstorage_error.js24
-rw-r--r--devtools/client/storage/test/browser_storage_overflow.js41
-rw-r--r--devtools/client/storage/test/browser_storage_search.js87
-rw-r--r--devtools/client/storage/test/browser_storage_search_keyboard_trap.js15
-rw-r--r--devtools/client/storage/test/browser_storage_sessionstorage_edit.js24
-rw-r--r--devtools/client/storage/test/browser_storage_sidebar.js125
-rw-r--r--devtools/client/storage/test/browser_storage_sidebar_update.js41
-rw-r--r--devtools/client/storage/test/browser_storage_values.js165
-rw-r--r--devtools/client/storage/test/head.js840
-rw-r--r--devtools/client/storage/test/storage-cache-error.html20
-rw-r--r--devtools/client/storage/test/storage-complex-values.html123
-rw-r--r--devtools/client/storage/test/storage-cookies.html24
-rw-r--r--devtools/client/storage/test/storage-empty-objectstores.html62
-rw-r--r--devtools/client/storage/test/storage-idb-delete-blocked.html52
-rw-r--r--devtools/client/storage/test/storage-listings.html126
-rw-r--r--devtools/client/storage/test/storage-localstorage.html23
-rw-r--r--devtools/client/storage/test/storage-overflow.html19
-rw-r--r--devtools/client/storage/test/storage-search.html23
-rw-r--r--devtools/client/storage/test/storage-secured-iframe.html91
-rw-r--r--devtools/client/storage/test/storage-sessionstorage.html23
-rw-r--r--devtools/client/storage/test/storage-unsecured-iframe.html19
-rw-r--r--devtools/client/storage/test/storage-updates.html64
-rw-r--r--devtools/client/storage/ui.js1073
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 ]));
+ },
+};