--- a/devtools/client/storage/storage.xul
+++ b/devtools/client/storage/storage.xul
@@ -21,17 +21,17 @@
<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-database"/>
+ <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>
--- a/devtools/client/storage/test/browser.ini
+++ b/devtools/client/storage/test/browser.ini
@@ -14,16 +14,17 @@ support-files =
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]
new file mode 100644
--- /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();
+});
--- a/devtools/client/storage/test/browser_storage_delete.js
+++ b/devtools/client/storage/test/browser_storage_delete.js
@@ -11,17 +11,19 @@
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"]
+ 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");
@@ -34,18 +36,19 @@ add_task(function* () {
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();
- ok(menuDeleteItem.getAttribute("label").includes(rowName),
- `Context menu item label contains '${rowName}'`);
+ 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`);
}
--- a/devtools/client/storage/test/browser_storage_delete_all.js
+++ b/devtools/client/storage/test/browser_storage_delete_all.js
@@ -26,35 +26,39 @@ add_task(function* () {
[["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"],
- [["sessionStorage", "https://sectest1.example.org"], "iframe-s-ss1"],
- [["indexedDB", "http://test1.example.org", "idb1", "obj1"], 1],
+ [["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] of deleteHosts) {
+ 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).name;
+ let cell = getRowCells(rowName)[cellToClick];
yield waitForContextMenu(contextMenu, cell, () => {
info(`Opened context menu in ${storeName}, row '${rowName}'`);
menuDeleteAllItem.click();
});
yield eventWait;
}
@@ -71,14 +75,16 @@ add_task(function* () {
[["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();
});
--- a/devtools/client/storage/test/browser_storage_delete_tree.js
+++ b/devtools/client/storage/test/browser_storage_delete_tree.js
@@ -16,24 +16,27 @@ add_task(function* () {
"#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-" +
@@ -52,12 +55,13 @@ add_task(function* () {
}
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();
});
--- a/devtools/client/storage/test/browser_storage_indexeddb_delete.js
+++ b/devtools/client/storage/test/browser_storage_indexeddb_delete.js
@@ -7,18 +7,17 @@
"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-database");
+ 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"];
--- a/devtools/client/storage/ui.js
+++ b/devtools/client/storage/ui.js
@@ -62,16 +62,23 @@ const COOKIE_KEY_MAP = {
};
// 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.
@@ -152,17 +159,17 @@ function StorageUI(front, target, panelW
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.onRemoveDatabase = this.onRemoveDatabase.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",
@@ -171,20 +178,18 @@ function StorageUI(front, target, panelW
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._treePopupDeleteDatabase = this._panelDoc.getElementById(
- "storage-tree-popup-delete-database");
- this._treePopupDeleteDatabase.addEventListener("command",
- this.onRemoveDatabase);
+ this._treePopupDelete = this._panelDoc.getElementById("storage-tree-popup-delete");
+ this._treePopupDelete.addEventListener("command", this.onRemoveTreeItem);
}
exports.StorageUI = StorageUI;
StorageUI.prototype = {
storageTypes: null,
shouldLoadMoreItems: true,
@@ -200,31 +205,24 @@ StorageUI.prototype = {
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._treePopupDeleteDatabase.removeEventListener("command",
- this.onRemoveDatabase);
+ 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);
+ 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;
@@ -939,34 +937,49 @@ StorageUI.prototype = {
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) and for the whole host (level 2 of tree) of other storage
- // types (cookies, localStorage, ...).
- let showDeleteAll = actor.removeAll &&
- (selectedItem.length === (type === "indexedDB" ? 4 : 2));
+ // (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 action to delete database is available for IndexedDB databases, i.e.,
- // at level 3 of the tree.
- let showDeleteDb = actor.removeDatabase && selectedItem.length === 3;
- this._treePopupDeleteDatabase.hidden = !showDeleteDb;
- if (showDeleteDb) {
- let dbName = addEllipsis(selectedItem[2]);
- this._treePopupDeleteDatabase.setAttribute("label",
- L10N.getFormatStr("storage.popupMenu.deleteLabel", dbName));
+ // 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 || showDeleteDb;
+ showMenu = showDeleteAll || showDelete;
}
if (!showMenu) {
event.preventDefault();
}
},
/**
@@ -1005,31 +1018,46 @@ StorageUI.prototype = {
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);
},
- onRemoveDatabase: function () {
- let [type, host, name] = this.tree.selectedItem;
- let actor = this.storageTypes[type];
+ onRemoveTreeItem: function () {
+ let [type, host, ...path] = this.tree.selectedItem;
- actor.removeDatabase(host, name).then(result => {
+ 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", name),
+ 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", name),
+ 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 ]));
+ },
};
--- a/devtools/server/actors/storage.js
+++ b/devtools/server/actors/storage.js
@@ -1245,16 +1245,71 @@ StorageActors.createActor({
getSchemaAndHost(url) {
let uri = Services.io.newURI(url, null, null);
return uri.scheme + "://" + uri.hostPort;
},
toStoreObject(item) {
return item;
},
+
+ removeItem: Task.async(function* (host, name) {
+ const cacheMap = this.hostVsStores.get(host);
+ if (!cacheMap) {
+ return;
+ }
+
+ const parsedName = JSON.parse(name);
+
+ if (parsedName.length == 1) {
+ // Delete the whole Cache object
+ const [ cacheName ] = parsedName;
+ cacheMap.delete(cacheName);
+ const cacheStorage = yield this.getCachesForHost(host);
+ yield cacheStorage.delete(cacheName);
+ this.onItemUpdated("deleted", host, [ cacheName ]);
+ } else if (parsedName.length == 2) {
+ // Delete one cached request
+ const [ cacheName, url ] = parsedName;
+ const cache = cacheMap.get(cacheName);
+ if (cache) {
+ yield cache.delete(url);
+ this.onItemUpdated("deleted", host, [ cacheName, url ]);
+ }
+ }
+ }),
+
+ removeAll: Task.async(function* (host, name) {
+ const cacheMap = this.hostVsStores.get(host);
+ if (!cacheMap) {
+ return;
+ }
+
+ const parsedName = JSON.parse(name);
+
+ // Only a Cache object is a valid object to clear
+ if (parsedName.length == 1) {
+ const [ cacheName ] = parsedName;
+ const cache = cacheMap.get(cacheName);
+ if (cache) {
+ let keys = yield cache.keys();
+ yield promise.all(keys.map(key => cache.delete(key)));
+ this.onItemUpdated("cleared", host, [ cacheName ]);
+ }
+ }
+ }),
+
+ /**
+ * CacheStorage API doesn't support any notifications, we must fake them
+ */
+ onItemUpdated(action, host, path) {
+ this.storageActor.update(action, "Cache", {
+ [host]: [ JSON.stringify(path) ]
+ });
+ },
});
/**
* Code related to the Indexed DB actor and front
*/
// Metadata holder objects for various components of Indexed DB
--- a/devtools/shared/specs/storage.js
+++ b/devtools/shared/specs/storage.js
@@ -148,17 +148,33 @@ types.addDictType("cachestoreobject", {
total: "number",
offset: "number",
data: "array:nullable:cacheobject"
});
// Cache storage spec
createStorageSpec({
typeName: "Cache",
- storeObjectType: "cachestoreobject"
+ storeObjectType: "cachestoreobject",
+ methods: {
+ removeAll: {
+ request: {
+ host: Arg(0, "string"),
+ name: Arg(1, "string"),
+ },
+ response: {}
+ },
+ removeItem: {
+ request: {
+ host: Arg(0, "string"),
+ name: Arg(1, "string"),
+ },
+ response: {}
+ },
+ }
});
// Indexed DB store object
// This is a union on idb object, db metadata object and object store metadata
// object
types.addDictType("idbobject", {
name: "nullable:string",
db: "nullable:string",