Bug 1231437 - Storage Inspector: context menu to remove cookie/storage item r=mratcliffe a=kwierso
☠☠ backed out by b5300f1eae93 ☠ ☠
authorJarda Snajdr <jsnajdr@gmail.com>
Thu, 31 Mar 2016 16:14:15 -0700
changeset 291294 66f61a6565710232618266a9c8099cdb85287701
parent 291293 6ecf26c604a3f6a80e6757b9107a2deb1a689937
child 291295 27104628616c5550fc56b060e727248c5f3a1c2c
push id19656
push usergwagner@mozilla.com
push dateMon, 04 Apr 2016 13:43:23 +0000
treeherderb2g-inbound@e99061fde28a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmratcliffe, kwierso
bugs1231437
milestone48.0a1
Bug 1231437 - Storage Inspector: context menu to remove cookie/storage item r=mratcliffe a=kwierso MozReview-Commit-ID: ETmjp8jjQ6z
devtools/client/framework/test/shared-head.js
devtools/client/locales/en-US/storage.properties
devtools/client/shared/widgets/TableWidget.js
devtools/client/storage/storage.xul
devtools/client/storage/test/browser.ini
devtools/client/storage/test/browser_storage_delete.js
devtools/client/storage/test/head.js
devtools/client/storage/ui.js
devtools/client/webconsole/test/head.js
devtools/server/actors/storage.js
--- a/devtools/client/framework/test/shared-head.js
+++ b/devtools/client/framework/test/shared-head.js
@@ -301,8 +301,55 @@ function evalInDebuggee (mm, script) {
       }
 
       info(`Successfully evaled in debuggee: ${script}`);
       mm.removeMessageListener("devtools:test:eval:response", handler);
       resolve(data.value);
     }
   });
 }
+
+/**
+ * Wait for a context menu popup to open.
+ *
+ * @param nsIDOMElement popup
+ *        The XUL popup you expect to open.
+ * @param nsIDOMElement button
+ *        The button/element that receives the contextmenu event. This is
+ *        expected to open the popup.
+ * @param function onShown
+ *        Function to invoke on popupshown event.
+ * @param function onHidden
+ *        Function to invoke on popuphidden event.
+ * @return object
+ *         A Promise object that is resolved after the popuphidden event
+ *         callback is invoked.
+ */
+function waitForContextMenu(popup, button, onShown, onHidden) {
+  let deferred = promise.defer();
+
+  function onPopupShown() {
+    info("onPopupShown");
+    popup.removeEventListener("popupshown", onPopupShown);
+
+    onShown && onShown();
+
+    // Use executeSoon() to get out of the popupshown event.
+    popup.addEventListener("popuphidden", onPopupHidden);
+    executeSoon(() => popup.hidePopup());
+  }
+  function onPopupHidden() {
+    info("onPopupHidden");
+    popup.removeEventListener("popuphidden", onPopupHidden);
+
+    onHidden && onHidden();
+
+    deferred.resolve(popup);
+  }
+
+  popup.addEventListener("popupshown", onPopupShown);
+
+  info("wait for the context menu to open");
+  let eventDetails = {type: "contextmenu", button: 2};
+  EventUtils.synthesizeMouse(button, 2, 2, eventDetails,
+                             button.ownerDocument.defaultView);
+  return deferred.promise;
+}
--- a/devtools/client/locales/en-US/storage.properties
+++ b/devtools/client/locales/en-US/storage.properties
@@ -110,8 +110,12 @@ storage.search.placeholder=Filter values
 
 # LOCALIZATION NOTE (storage.data.label):
 # This is the heading displayed over the item value in the sidebar
 storage.data.label=Data
 
 # LOCALIZATION NOTE (storage.parsedValue.label):
 # This is the heading displayed over the item parsed value in the sidebar
 storage.parsedValue.label=Parsed Value
+
+# LOCALIZATION NOTE (storage.popupMenu.deleteLabel):
+# Label of popup menu action to delete storage item.
+storage.popupMenu.deleteLabel=Delete "%S"
--- a/devtools/client/shared/widgets/TableWidget.js
+++ b/devtools/client/shared/widgets/TableWidget.js
@@ -52,31 +52,34 @@ const MAX_VISIBLE_STRING_SIZE = 100;
  *                          the table. See @setupColumns for more info.
  *        - uniqueId: the column which will be the unique identifier of each
  *                    entry in the table. Default: name.
  *        - emptyText: text to display when no entries in the table to display.
  *        - highlightUpdated: true to highlight the changed/added row.
  *        - removableColumns: Whether columns are removeable. If set to false,
  *                            the context menu in the headers will not appear.
  *        - firstColumn: key of the first column that should appear.
+ *        - cellContextMenuId: ID of a <menupopup> element to be set as a
+ *                             context menu of every cell.
  */
 function TableWidget(node, options = {}) {
   EventEmitter.decorate(this);
 
   this.document = node.ownerDocument;
   this.window = this.document.defaultView;
   this._parent = node;
 
   let {initialColumns, emptyText, uniqueId, highlightUpdated, removableColumns,
-       firstColumn} = options;
+       firstColumn, cellContextMenuId} = options;
   this.emptyText = emptyText || "";
   this.uniqueId = uniqueId || "name";
   this.firstColumn = firstColumn || "";
   this.highlightUpdated = highlightUpdated || false;
   this.removableColumns = removableColumns !== false;
+  this.cellContextMenuId = cellContextMenuId;
 
   this.tbody = this.document.createElementNS(XUL_NS, "hbox");
   this.tbody.className = "table-widget-body theme-body";
   this.tbody.setAttribute("flex", "1");
   this.tbody.setAttribute("tabindex", "0");
   this._parent.appendChild(this.tbody);
   this.afterScroll = this.afterScroll.bind(this);
   this.tbody.addEventListener("scroll", this.onScroll.bind(this));
@@ -801,16 +804,17 @@ TableWidget.prototype = {
     }
     let removed = this.items.delete(item[this.uniqueId]);
 
     if (!removed) {
       return;
     }
     for (let column of this.columns.values()) {
       column.remove(item);
+      column.updateZebra();
     }
     if (this.items.size == 0) {
       this.tbody.setAttribute("empty", "empty");
     }
 
     this.emit(EVENTS.ROW_REMOVED, item);
   },
 
@@ -1443,16 +1447,25 @@ function Cell(column, item, nextCell) {
   this.label.className = "plain table-widget-cell";
 
   if (nextCell) {
     column.column.insertBefore(this.label, nextCell.label);
   } else {
     column.column.appendChild(this.label);
   }
 
+  if (column.table.cellContextMenuId) {
+    this.label.setAttribute("context", column.table.cellContextMenuId);
+    this.label.addEventListener("contextmenu", (event) => {
+      // Make the ID of the clicked cell available as a property on the table.
+      // It's then available for the popupshowing or command handler.
+      column.table.contextMenuRowId = this.id;
+    }, false);
+  }
+
   this.value = item[column.id];
   this.id = item[column.uniqueId];
 }
 
 Cell.prototype = {
 
   set id(value) {
     this._id = value;
--- a/devtools/client/storage/storage.xul
+++ b/devtools/client/storage/storage.xul
@@ -17,16 +17,22 @@
 <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-table-popup">
+      <menuitem id="storage-table-popup-delete"/>
+    </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-searchinput"
                  type="search"
--- a/devtools/client/storage/test/browser.ini
+++ b/devtools/client/storage/test/browser.ini
@@ -12,12 +12,13 @@ support-files =
   storage-updates.html
   head.js
 
 [browser_storage_basic.js]
 [browser_storage_dynamic_updates.js]
 [browser_storage_cookies_edit.js]
 [browser_storage_cookies_edit_keyboard.js]
 [browser_storage_cookies_tab_navigation.js]
+[browser_storage_delete.js]
 [browser_storage_overflow.js]
 [browser_storage_search.js]
 [browser_storage_sidebar.js]
 [browser_storage_values.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_delete.js
@@ -0,0 +1,48 @@
+/* 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"]
+];
+
+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 [ [store, host], rowName, cellToClick] of TEST_CASES) {
+    info(`Selecting tree item ${store} > ${host}`);
+    yield selectTreeItem([store, host]);
+
+    let row = getRowCells(rowName);
+
+    ok(gUI.table.items.has(rowName),
+      `There is a row '${rowName}' in ${store} > ${host}`);
+
+    yield waitForContextMenu(contextMenu, row[cellToClick], () => {
+      info(`Opened context menu in ${store} > ${host}, row '${rowName}'`);
+      menuDeleteItem.click();
+      ok(menuDeleteItem.getAttribute("label").includes(rowName),
+        `Context menu item label contains '${rowName}'`);
+    });
+
+    yield gUI.once("store-objects-updated");
+
+    ok(!gUI.table.items.has(rowName),
+      `There is no row '${rowName}' in ${store} > ${host} after deletion`);
+  }
+
+  yield finishTests();
+});
--- a/devtools/client/storage/test/head.js
+++ b/devtools/client/storage/test/head.js
@@ -1,35 +1,32 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 /* eslint no-unused-vars: [2, {"vars": "local"}] */
+/* import-globals-from ../../framework/test/shared-head.js */
 
-var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
-var { TargetFactory } = require("devtools/client/framework/target");
-var promise = require("promise");
-var DevToolsUtils = require("devtools/shared/DevToolsUtils");
+// 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;
 
-waitForExplicitFinish();
-
 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);
 DevToolsUtils.testing = true;
--- a/devtools/client/storage/ui.js
+++ b/devtools/client/storage/ui.js
@@ -42,16 +42,20 @@ const HIDDEN_COLUMNS = [
 
 const REASON = {
   NEW_ROW: "new-row",
   NEXT_50_ITEMS: "next-50-items",
   POPULATE: "populate",
   UPDATE: "update"
 };
 
+// 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;
+
 /**
  * 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
@@ -69,16 +73,17 @@ var StorageUI = this.StorageUI = functio
   this.tree = new TreeWidget(treeNode, {defaultType: "dir"});
   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);
 
@@ -100,16 +105,25 @@ var StorageUI = this.StorageUI = functio
 
   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.onPopupShowing = this.onPopupShowing.bind(this);
+  this._tablePopup = this._panelDoc.getElementById("storage-table-popup");
+  this._tablePopup.addEventListener("popupshowing", this.onPopupShowing, false);
+
+  this.onRemoveItem = this.onRemoveItem.bind(this);
+  this._tablePopupDelete = this._panelDoc.getElementById(
+    "storage-table-popup-delete");
+  this._tablePopupDelete.addEventListener("command", this.onRemoveItem, false);
 };
 
 exports.StorageUI = StorageUI;
 
 StorageUI.prototype = {
 
   storageTypes: null,
   shouldResetColumns: true,
@@ -125,16 +139,19 @@ StorageUI.prototype = {
     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._tablePopup.removeEventListener("popupshowing", this.onPopupShowing);
+    this._tablePopupDelete.removeEventListener("command", this.onRemoveItem);
   },
 
   /**
    * Empties and hides the object viewer sidebar
    */
   hideSidebar: function() {
     this.view.empty();
     this.sidebar.hidden = true;
@@ -725,10 +742,46 @@ StorageUI.prototype = {
 
     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 current storage actor doesn't support removing items, prevent
+   * showing the menu.
+   */
+  onPopupShowing: function(event) {
+    if (!this.getCurrentActor().removeItem) {
+      event.preventDefault();
+      return;
+    }
+
+    let rowId = this.table.contextMenuRowId;
+    let data = this.table.items.get(rowId);
+    let name = data[this.table.uniqueId];
+
+    const maxLen = ITEM_NAME_MAX_LENGTH;
+    if (name.length > maxLen) {
+      name = name.substr(0, maxLen) + L10N.ellipsis;
+    }
+
+    this._tablePopupDelete.setAttribute("label",
+      L10N.getFormatStr("storage.popupMenu.deleteLabel", name));
+  },
+
+  /**
+   * Handles removing an item from the storage
+   */
+  onRemoveItem: function() {
+    let [, host] = this.tree.selectedItem;
+    let actor = this.getCurrentActor();
+    let rowId = this.table.contextMenuRowId;
+    let data = this.table.items.get(rowId);
+
+    actor.removeItem(host, data[this.table.uniqueId]);
+  },
 };
--- a/devtools/client/webconsole/test/head.js
+++ b/devtools/client/webconsole/test/head.js
@@ -237,63 +237,16 @@ var closeConsole = Task.async(function* 
   let target = TargetFactory.forTab(tab || gBrowser.selectedTab);
   let toolbox = gDevTools.getToolbox(target);
   if (toolbox) {
     yield toolbox.destroy();
   }
 });
 
 /**
- * Wait for a context menu popup to open.
- *
- * @param nsIDOMElement popup
- *        The XUL popup you expect to open.
- * @param nsIDOMElement button
- *        The button/element that receives the contextmenu event. This is
- *        expected to open the popup.
- * @param function onShown
- *        Function to invoke on popupshown event.
- * @param function onHidden
- *        Function to invoke on popuphidden event.
- * @return object
- *         A Promise object that is resolved after the popuphidden event
- *         callback is invoked.
- */
-function waitForContextMenu(popup, button, onShown, onHidden) {
-  let deferred = promise.defer();
-
-  function onPopupShown() {
-    info("onPopupShown");
-    popup.removeEventListener("popupshown", onPopupShown);
-
-    onShown && onShown();
-
-    // Use executeSoon() to get out of the popupshown event.
-    popup.addEventListener("popuphidden", onPopupHidden);
-    executeSoon(() => popup.hidePopup());
-  }
-  function onPopupHidden() {
-    info("onPopupHidden");
-    popup.removeEventListener("popuphidden", onPopupHidden);
-
-    onHidden && onHidden();
-
-    deferred.resolve(popup);
-  }
-
-  popup.addEventListener("popupshown", onPopupShown);
-
-  info("wait for the context menu to open");
-  let eventDetails = {type: "contextmenu", button: 2};
-  EventUtils.synthesizeMouse(button, 2, 2, eventDetails,
-                             button.ownerDocument.defaultView);
-  return deferred.promise;
-}
-
-/**
  * Listen for a new tab to open and return a promise that resolves when one
  * does and completes the load event.
  * @return a promise that resolves to the tab object
  */
 var waitForTab = Task.async(function*() {
   info("Waiting for a tab to open");
   yield once(gBrowser.tabContainer, "TabOpen");
   let tab = gBrowser.selectedTab;
--- a/devtools/server/actors/storage.js
+++ b/devtools/server/actors/storage.js
@@ -421,17 +421,17 @@ StorageActors.defaults = function(typeNa
  *         - typeName {string}
  *                    The typeName of the actor.
  *         - observationTopic {string}
  *                            The topic which this actor listens to via
  *                            Notification Observers.
  *         - storeObjectType {string}
  *                           The RetVal type of the store object of this actor.
  * @param {object} overrides
- *        All the methods which you want to be differnt from the ones in
+ *        All the methods which you want to be different from the ones in
  *        StorageActors.defaults method plus the required ones described there.
  */
 StorageActors.createActor = function(options = {}, overrides = {}) {
   let actorObject = StorageActors.defaults(
     options.typeName,
     options.observationTopic || null,
     options.storeObjectType
   );
@@ -669,24 +669,35 @@ StorageActors.createActor({
     this.editCookie(data);
   }), {
     request: {
       data: Arg(0, "json"),
     },
     response: {}
   }),
 
+  removeItem: method(Task.async(function*(host, name) {
+    this.removeCookie(host, name);
+  }), {
+    request: {
+      host: Arg(0),
+      name: Arg(1),
+    },
+    response: {}
+  }),
+
   maybeSetupChildProcess: function() {
     cookieHelpers.onCookieChanged = this.onCookieChanged.bind(this);
 
     if (!DebuggerServer.isInChildProcess) {
       this.getCookiesFromHost = cookieHelpers.getCookiesFromHost;
       this.addCookieObservers = cookieHelpers.addCookieObservers;
       this.removeCookieObservers = cookieHelpers.removeCookieObservers;
       this.editCookie = cookieHelpers.editCookie;
+      this.removeCookie = cookieHelpers.removeCookie;
       return;
     }
 
     const { sendSyncMessage, addMessageListener } =
       this.conn.parentMessageManager;
 
     this.conn.setupInParent({
       module: "devtools/server/actors/storage",
@@ -696,16 +707,18 @@ StorageActors.createActor({
     this.getCookiesFromHost =
       callParentProcess.bind(null, "getCookiesFromHost");
     this.addCookieObservers =
       callParentProcess.bind(null, "addCookieObservers");
     this.removeCookieObservers =
       callParentProcess.bind(null, "removeCookieObservers");
     this.editCookie =
       callParentProcess.bind(null, "editCookie");
+    this.removeCookie =
+      callParentProcess.bind(null, "removeCookie");
 
     addMessageListener("storage:storage-cookie-request-child",
                        cookieHelpers.handleParentRequest);
 
     function callParentProcess(methodName, ...args) {
       let reply = sendSyncMessage("storage:storage-cookie-request-parent", {
         method: methodName,
         args: args
@@ -848,16 +861,42 @@ var cookieHelpers = {
       cookie.value,
       cookie.isSecure,
       cookie.isHttpOnly,
       cookie.isSession,
       cookie.isSession ? MAX_COOKIE_EXPIRY : cookie.expires
     );
   },
 
+  removeCookie: function(host, name) {
+    function hostMatches(cookieHost, matchHost) {
+      if (cookieHost == null) {
+        return matchHost == null;
+      }
+      if (cookieHost.startsWith(".")) {
+        return matchHost.endsWith(cookieHost);
+      }
+      return cookieHost == host;
+    }
+
+    let enumerator = Services.cookies.getCookiesFromHost(host);
+    while (enumerator.hasMoreElements()) {
+      let cookie = enumerator.getNext().QueryInterface(Ci.nsICookie2);
+      if (hostMatches(cookie.host, host) && cookie.name === name) {
+        Services.cookies.remove(
+          cookie.host,
+          cookie.name,
+          cookie.path,
+          cookie.originAttributes,
+          false
+        );
+      }
+    }
+  },
+
   addCookieObservers: function() {
     Services.obs.addObserver(cookieHelpers, "cookie-changed", false);
     return null;
   },
 
   removeCookieObservers: function() {
     Services.obs.removeObserver(cookieHelpers, "cookie-changed", false);
     return null;
@@ -896,27 +935,36 @@ var cookieHelpers = {
         cookie = JSON.parse(cookie);
         cookieHelpers.onCookieChanged(cookie, topic, data);
         break;
     }
   },
 
   handleChildRequest: function(msg) {
     switch (msg.json.method) {
-      case "getCookiesFromHost":
+      case "getCookiesFromHost": {
         let host = msg.data.args[0];
         let cookies = cookieHelpers.getCookiesFromHost(host);
         return JSON.stringify(cookies);
-      case "addCookieObservers":
+      }
+      case "addCookieObservers": {
         return cookieHelpers.addCookieObservers();
-      case "removeCookieObservers":
+      }
+      case "removeCookieObservers": {
         return cookieHelpers.removeCookieObservers();
-      case "editCookie":
+      }
+      case "editCookie": {
         let rowdata = msg.data.args[0];
         return cookieHelpers.editCookie(rowdata);
+      }
+      case "removeCookie": {
+        let host = msg.data.args[0];
+        let name = msg.data.args[1];
+        return cookieHelpers.removeCookie(host, name);
+      }
       default:
         console.error("ERR_DIRECTOR_PARENT_UNKNOWN_METHOD", msg.json.method);
         throw new Error("ERR_DIRECTOR_PARENT_UNKNOWN_METHOD");
     }
   },
 };
 
 /**
@@ -1068,16 +1116,27 @@ function getObjectForLocalOrSessionStora
         return null;
       }
 
       return {
         name: item.name,
         value: new LongStringActor(this.conn, item.value || "")
       };
     },
+
+    removeItem: method(Task.async(function*(host, name) {
+      let storage = this.hostVsStores.get(host);
+      storage.removeItem(name);
+    }), {
+      request: {
+        host: Arg(0),
+        name: Arg(1),
+      },
+      response: {}
+    }),
   };
 }
 
 /**
  * The Local Storage actor and front.
  */
 StorageActors.createActor({
   typeName: "localStorage",