Bug 1146194 - Multiple cookies with the same name not shown r?gl draft
authorMichael Ratcliffe <mratcliffe@mozilla.com>
Fri, 11 Nov 2016 15:57:51 +0000
changeset 442367 af8dae45ecc6948940a514c6ee28854b04bf872b
parent 441209 a103e1713a2eda3b4e88d71df82d140487c8db87
child 537786 27d7a67816f345c58ff96601465e9fef9fbc9eaf
push id36689
push userbmo:mratcliffe@mozilla.com
push dateTue, 22 Nov 2016 11:25:00 +0000
reviewersgl
bugs1146194
milestone53.0a1
Bug 1146194 - Multiple cookies with the same name not shown r?gl Stubborn debugger statement. MozReview-Commit-ID: F2qgWofemHG
devtools/client/locales/en-US/storage.properties
devtools/client/shared/widgets/TableWidget.js
devtools/client/storage/test/browser.ini
devtools/client/storage/test/browser_storage_basic.js
devtools/client/storage/test/browser_storage_cookies_delete_all.js
devtools/client/storage/test/browser_storage_cookies_domain.js
devtools/client/storage/test/browser_storage_cookies_edit.js
devtools/client/storage/test/browser_storage_cookies_edit_keyboard.js
devtools/client/storage/test/browser_storage_cookies_tab_navigation.js
devtools/client/storage/test/browser_storage_delete.js
devtools/client/storage/test/browser_storage_delete_tree.js
devtools/client/storage/test/browser_storage_dynamic_updates.js
devtools/client/storage/test/browser_storage_dynamic_updates_cookies.js
devtools/client/storage/test/browser_storage_dynamic_updates_localStorage.js
devtools/client/storage/test/browser_storage_dynamic_updates_sessionStorage.js
devtools/client/storage/test/browser_storage_sidebar.js
devtools/client/storage/test/browser_storage_values.js
devtools/client/storage/test/head.js
devtools/client/storage/test/storage-updates.html
devtools/client/storage/ui.js
devtools/server/actors/storage.js
devtools/server/tests/browser/browser.ini
devtools/server/tests/browser/browser_storage_cookies-duplicate-names.js
devtools/server/tests/browser/browser_storage_dynamic_windows.js
devtools/server/tests/browser/browser_storage_listings.js
devtools/server/tests/browser/browser_storage_updates.js
devtools/server/tests/browser/head.js
devtools/server/tests/browser/storage-cookies-same-name.html
--- a/devtools/client/locales/en-US/storage.properties
+++ b/devtools/client/locales/en-US/storage.properties
@@ -30,16 +30,17 @@ tree.labels.cookies=Cookies
 tree.labels.localStorage=Local Storage
 tree.labels.sessionStorage=Session Storage
 tree.labels.indexedDB=Indexed DB
 tree.labels.Cache=Cache Storage
 
 # LOCALIZATION NOTE (table.headers.*.*):
 # These strings are the header names of the columns in the Storage Table for
 # each type of storage available through the Storage Tree to the side.
+table.headers.cookies.uniqueKey=Unique key
 table.headers.cookies.name=Name
 table.headers.cookies.path=Path
 table.headers.cookies.host=Domain
 table.headers.cookies.expires=Expires on
 table.headers.cookies.value=Value
 table.headers.cookies.lastAccessed=Last accessed on
 table.headers.cookies.creationTime=Created on
 
--- a/devtools/client/shared/widgets/TableWidget.js
+++ b/devtools/client/shared/widgets/TableWidget.js
@@ -610,27 +610,36 @@ TableWidget.prototype = {
     this.menupopup.addEventListener("command", this.onPopupCommand);
     popupset.appendChild(this.menupopup);
     this.populateMenuPopup();
   },
 
   /**
    * Populates the header context menu with the names of the columns along with
    * displaying which columns are hidden or visible.
+   *
+   * @param {Array} privateColumns=[]
+   *        An array of column names that should never appear in the table. This
+   *        allows us to e.g. have an invisible compound primary key for a
+   *        table's rows.
    */
-  populateMenuPopup: function () {
+  populateMenuPopup: function (privateColumns = []) {
     if (!this.menupopup) {
       return;
     }
 
     while (this.menupopup.firstChild) {
       this.menupopup.firstChild.remove();
     }
 
     for (let column of this.columns.values()) {
+      if (privateColumns.includes(column.id)) {
+        continue;
+      }
+
       let menuitem = this.document.createElementNS(XUL_NS, "menuitem");
       menuitem.setAttribute("label", column.header.getAttribute("value"));
       menuitem.setAttribute("data-id", column.id);
       menuitem.setAttribute("type", "checkbox");
       menuitem.setAttribute("checked", !column.wrapper.getAttribute("hidden"));
       if (column.id == this.uniqueId) {
         menuitem.setAttribute("disabled", "true");
       }
@@ -658,26 +667,31 @@ TableWidget.prototype = {
       disabled[disabled.length - 1].removeAttribute("disabled");
     }
   },
 
   /**
    * Creates the columns in the table. Without calling this method, data cannot
    * be inserted into the table unless `initialColumns` was supplied.
    *
-   * @param {object} columns
+   * @param {Object} columns
    *        A key value pair representing the columns of the table. Where the
    *        key represents the id of the column and the value is the displayed
    *        label in the header of the column.
-   * @param {string} sortOn
+   * @param {String} sortOn
    *        The id of the column on which the table will be initially sorted on.
-   * @param {array} hiddenColumns
+   * @param {Array} hiddenColumns
    *        Ids of all the columns that are hidden by default.
+   * @param {Array} privateColumns=[]
+   *        An array of column names that should never appear in the table. This
+   *        allows us to e.g. have an invisible compound primary key for a
+   *        table's rows.
    */
-  setColumns: function (columns, sortOn = this.sortedOn, hiddenColumns = []) {
+  setColumns: function (columns, sortOn = this.sortedOn, hiddenColumns = [],
+                        privateColumns = []) {
     for (let column of this.columns.values()) {
       column.destroy();
     }
 
     this.columns.clear();
 
     if (!(sortOn in columns)) {
       sortOn = null;
@@ -697,23 +711,24 @@ TableWidget.prototype = {
         sortOn = id;
       }
 
       if (this.firstColumn && id == this.firstColumn) {
         continue;
       }
 
       this.columns.set(id, new Column(this, id, columns[id]));
-      if (hiddenColumns.indexOf(id) > -1) {
+      if (hiddenColumns.includes(id) || privateColumns.includes(id)) {
+        // Hide the column.
         this.columns.get(id).toggleColumn();
       }
     }
     this.sortedOn = sortOn;
     this.sortBy(this.sortedOn);
-    this.populateMenuPopup();
+    this.populateMenuPopup(privateColumns);
   },
 
   /**
    * Returns true if the passed string or the row json object corresponds to the
    * selected item in the table.
    */
   isSelected: function (item) {
     if (typeof item == "object") {
@@ -773,16 +788,21 @@ TableWidget.prototype = {
       return;
     }
 
     if (this.items.has(item[this.uniqueId])) {
       this.update(item);
       return;
     }
 
+    if (this.editBookmark && !this.items.has(this.editBookmark)) {
+      // Key has been updated... update bookmark.
+      this.editBookmark = item[this.uniqueId];
+    }
+
     let index = this.columns.get(this.sortedOn).push(item);
     for (let [key, column] of this.columns) {
       if (key != this.sortedOn) {
         column.insertAt(item, index);
       }
       column.updateZebra();
     }
     this.items.set(item[this.uniqueId], item);
--- a/devtools/client/storage/test/browser.ini
+++ b/devtools/client/storage/test/browser.ini
@@ -24,17 +24,19 @@ support-files =
 [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_dynamic_updates_cookies.js]
+[browser_storage_dynamic_updates_localStorage.js]
+[browser_storage_dynamic_updates_sessionStorage.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]
--- a/devtools/client/storage/test/browser_storage_basic.js
+++ b/devtools/client/storage/test/browser_storage_basic.js
@@ -1,12 +1,14 @@
 /* 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 head.js */
+
 // 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
@@ -16,20 +18,33 @@
 // ]
 // 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"]],
+  [
+    ["cookies", "test1.example.org"],
+    [
+      getCookieId("c1", "test1.example.org", "/browser"),
+      getCookieId("cs2", ".example.org", "/"),
+      getCookieId("c3", "test1.example.org", "/"),
+      getCookieId("uc1", ".example.org", "/")
+    ]
+  ],
+  [
+    ["cookies", "sectest1.example.org"],
+    [
+      getCookieId("uc1", ".example.org", "/"),
+      getCookieId("cs2", ".example.org", "/"),
+      getCookieId("sc1", "sectest1.example.org", "/browser/devtools/client/storage/test/")
+    ]
+  ],
   [["localStorage", "http://test1.example.org"],
    ["ls1", "ls2"]],
   [["localStorage", "http://sectest1.example.org"],
    ["iframe-u-ls1"]],
   [["localStorage", "https://sectest1.example.org"],
    ["iframe-s-ls1"]],
   [["sessionStorage", "http://test1.example.org"],
    ["ss1"]],
--- a/devtools/client/storage/test/browser_storage_cookies_delete_all.js
+++ b/devtools/client/storage/test/browser_storage_cookies_delete_all.js
@@ -16,18 +16,18 @@ function* performDelete(store, rowName, 
   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, true);
 
-  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),
@@ -38,34 +38,64 @@ function* performDelete(store, rowName, 
   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"]],
+    [
+      ["cookies", "test1.example.org"], [
+        getCookieId("c1", "test1.example.org", "/browser"),
+        getCookieId("c3", "test1.example.org", "/"),
+        getCookieId("cs2", ".example.org", "/"),
+        getCookieId("uc1", ".example.org", "/")
+      ]
+    ],
+    [
+      ["cookies", "sectest1.example.org"], [
+        getCookieId("cs2", ".example.org", "/"),
+        getCookieId("sc1", "sectest1.example.org",
+                    "/browser/devtools/client/storage/test/"),
+        getCookieId("uc1", ".example.org", "/")
+      ]
+    ],
   ]);
 
   info("delete all from domain");
   // delete only cookies that match the host exactly
-  yield performDelete(["cookies", "test1.example.org"], "c1", false);
+  let id = getCookieId("c1", "test1.example.org", "/browser");
+  yield performDelete(["cookies", "test1.example.org"], id, 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"]],
+    [
+      ["cookies", "test1.example.org"],
+      [
+        getCookieId("cs2", ".example.org", "/"),
+        getCookieId("uc1", ".example.org", "/")
+      ]
+    ],
+    [
+      ["cookies", "sectest1.example.org"],
+      [
+        getCookieId("cs2", ".example.org", "/"),
+        getCookieId("uc1", ".example.org", "/"),
+        getCookieId("sc1", "sectest1.example.org",
+                    "/browser/devtools/client/storage/test/"),
+      ]
+    ],
   ]);
 
   info("delete all");
   // delete all cookies for host, including domain cookies
-  yield performDelete(["cookies", "sectest1.example.org"], "uc1", true);
+  id = getCookieId("uc1", ".example.org", "/");
+  yield performDelete(["cookies", "sectest1.example.org"], id, 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"], []],
   ]);
--- a/devtools/client/storage/test/browser_storage_cookies_domain.js
+++ b/devtools/client/storage/test/browser_storage_cookies_domain.js
@@ -8,14 +8,22 @@
 
 // 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"]],
+    [
+      ["cookies", "test1.example.org"],
+      [
+        getCookieId("test1", ".test1.example.org", "/browser"),
+        getCookieId("test2", "test1.example.org", "/browser"),
+        getCookieId("test3", ".test1.example.org", "/browser"),
+        getCookieId("test4", "test1.example.org", "/browser"),
+        getCookieId("test5", ".test1.example.org", "/browser")
+      ]
+    ],
   ]);
 
   yield finishTests();
 });
--- a/devtools/client/storage/test/browser_storage_cookies_edit.js
+++ b/devtools/client/storage/test/browser_storage_cookies_edit.js
@@ -5,18 +5,25 @@
 // 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");
+  let id = getCookieId("test3", ".test1.example.org", "/browser");
+  yield editCell(id, "name", "newTest3");
+
+  id = getCookieId("newTest3", ".test1.example.org", "/browser");
+  yield editCell(id, "host", "test1.example.org");
+
+  id = getCookieId("newTest3", "test1.example.org", "/browser");
+  yield editCell(id, "path", "/");
+
+  id = getCookieId("newTest3", "test1.example.org", "/");
+  yield editCell(id, "expires", "Tue, 14 Feb 2040 17:41:14 GMT");
+  yield editCell(id, "value", "newValue3");
+  yield editCell(id, "isSecure", "true");
+  yield editCell(id, "isHttpOnly", "true");
 
   yield finishTests();
 });
--- a/devtools/client/storage/test/browser_storage_cookies_edit_keyboard.js
+++ b/devtools/client/storage/test/browser_storage_cookies_edit_keyboard.js
@@ -5,19 +5,20 @@
 // 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");
+  let id = getCookieId("test4", "test1.example.org", "/browser");
+  yield startCellEdit(id, "name");
   yield typeWithTerminator("test6", "VK_TAB");
+  yield typeWithTerminator(".example.org", "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();
 });
--- a/devtools/client/storage/test/browser_storage_cookies_tab_navigation.js
+++ b/devtools/client/storage/test/browser_storage_cookies_tab_navigation.js
@@ -5,17 +5,18 @@
 // 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");
+  let id = getCookieId("test1", ".test1.example.org", "/browser");
+  yield startCellEdit(id, "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.");
--- a/devtools/client/storage/test/browser_storage_delete.js
+++ b/devtools/client/storage/test/browser_storage_delete.js
@@ -8,18 +8,20 @@
 
 // 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"],
+  [
+    ["cookies", "test1.example.org"],
+    getCookieId("c1", "test1.example.org", "/browser"), "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");
@@ -36,17 +38,17 @@ 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();
-      let truncatedRowName = String(rowName).substr(0, 16);
+      let truncatedRowName = String(rowName).replace(SEPARATOR_GUID, "-").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_tree.js
+++ b/devtools/client/storage/test/browser_storage_delete_tree.js
@@ -12,17 +12,25 @@ 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"]],
+    [
+      ["cookies", "test1.example.org"],
+      [
+        getCookieId("c1", "test1.example.org", "/browser"),
+        getCookieId("cs2", ".example.org", "/"),
+        getCookieId("c3", "test1.example.org", "/"),
+        getCookieId("uc1", ".example.org", "/")
+      ]
+    ],
     [["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");
new file mode 100644
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_dynamic_updates_cookies.js
@@ -0,0 +1,188 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Test dynamic updates in the storage inspector for cookies.
+
+add_task(function* () {
+  yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-updates.html");
+
+  gUI.tree.expandAll();
+
+  ok(gUI.sidebar.hidden, "Sidebar is initially hidden");
+  let c1id = getCookieId("c1", "test1.example.org", "/browser");
+  yield selectTableItem(c1id);
+
+  // 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
+
+  yield checkState([
+    [
+      ["cookies", "test1.example.org"],
+      [
+        getCookieId("c1", "test1.example.org", "/browser"),
+        getCookieId("c2", "test1.example.org", "/browser")
+      ]
+    ],
+  ]);
+  checkCell(c1id, "value", "1.2.3.4.5.6.7");
+
+  gWindow.addCookie("c1", '{"foo": 4,"bar":6}', "/browser");
+  yield gUI.once("sidebar-updated");
+
+  yield findVariableViewProperties(finalValue[0], false);
+  yield findVariableViewProperties(finalValue[1], true);
+
+  yield checkState([
+    [
+      ["cookies", "test1.example.org"],
+      [
+        getCookieId("c1", "test1.example.org", "/browser"),
+        getCookieId("c2", "test1.example.org", "/browser")
+      ]
+    ],
+  ]);
+  checkCell(c1id, "value", '{"foo": 4,"bar":6}');
+
+  // Add a new entry
+  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");
+
+  yield checkState([
+    [
+      ["cookies", "test1.example.org"],
+      [
+        getCookieId("c1", "test1.example.org", "/browser"),
+        getCookieId("c2", "test1.example.org", "/browser"),
+        getCookieId("c3", "test1.example.org",
+                    "/browser/devtools/client/storage/test/")
+      ]
+    ],
+  ]);
+  let c3id = getCookieId("c3", "test1.example.org",
+                         "/browser/devtools/client/storage/test/");
+  checkCell(c3id, "value", "booyeah");
+
+  // 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");
+
+  yield checkState([
+    [
+      ["cookies", "test1.example.org"],
+      [
+        getCookieId("c1", "test1.example.org", "/browser"),
+        getCookieId("c2", "test1.example.org", "/browser"),
+        getCookieId("c3", "test1.example.org",
+                    "/browser/devtools/client/storage/test/"),
+        getCookieId("c4", "test1.example.org",
+                    "/browser/devtools/client/storage/test/")
+      ]
+    ],
+  ]);
+  let c4id = getCookieId("c4", "test1.example.org",
+                         "/browser/devtools/client/storage/test/");
+  checkCell(c4id, "value", "booyeah");
+
+  // Removing cookies
+  gWindow.removeCookie("c1", "/browser");
+
+  yield gUI.once("sidebar-updated");
+
+  yield checkState([
+    [
+      ["cookies", "test1.example.org"],
+      [
+        getCookieId("c2", "test1.example.org", "/browser"),
+        getCookieId("c3", "test1.example.org",
+                    "/browser/devtools/client/storage/test/"),
+        getCookieId("c4", "test1.example.org",
+                    "/browser/devtools/client/storage/test/")
+      ]
+    ],
+  ]);
+
+  ok(!gUI.sidebar.hidden, "Sidebar still visible for next row");
+
+  // Check if next element's value is visible in sidebar
+  yield findVariableViewProperties([{name: "c2", value: "foobar"}]);
+
+  // Keep deleting till no rows
+  gWindow.removeCookie("c3");
+
+  yield gUI.once("store-objects-updated");
+
+  yield checkState([
+    [
+      ["cookies", "test1.example.org"],
+      [
+        getCookieId("c2", "test1.example.org", "/browser"),
+        getCookieId("c4", "test1.example.org",
+                    "/browser/devtools/client/storage/test/")
+      ]
+    ],
+  ]);
+
+  // Check if next element's value is visible in sidebar
+  yield findVariableViewProperties([{name: "c2", value: "foobar"}]);
+
+  gWindow.removeCookie("c2", "/browser");
+
+  yield gUI.once("sidebar-updated");
+
+  yield checkState([
+    [
+      ["cookies", "test1.example.org"],
+      [
+        getCookieId("c4", "test1.example.org",
+                    "/browser/devtools/client/storage/test/")
+      ]
+    ],
+  ]);
+
+  // Check if next element's value is visible in sidebar
+  yield findVariableViewProperties([{name: "c4", value: "booyeah"}]);
+
+  gWindow.removeCookie("c4");
+
+  yield gUI.once("store-objects-updated");
+
+  yield checkState([
+    [["cookies", "test1.example.org"], [ ]],
+  ]);
+
+  ok(gUI.sidebar.hidden, "Sidebar is hidden when no rows");
+
+  yield finishTests();
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_dynamic_updates_localStorage.js
@@ -0,0 +1,70 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Test dynamic updates in the storage inspector for localStorage.
+
+add_task(function* () {
+  yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-updates.html");
+
+  gUI.tree.expandAll();
+
+  ok(gUI.sidebar.hidden, "Sidebar is initially hidden");
+
+  yield checkState([
+    [
+      ["localStorage", "http://test1.example.org"],
+      ["ls1", "ls2", "ls3", "ls4", "ls5", "ls6", "ls7"]
+    ],
+  ]);
+
+  gWindow.localStorage.removeItem("ls4");
+
+  yield gUI.once("store-objects-updated");
+
+  yield checkState([
+    [
+      ["localStorage", "http://test1.example.org"],
+      ["ls1", "ls2", "ls3", "ls5", "ls6", "ls7"]
+    ],
+  ]);
+
+  gWindow.localStorage.setItem("ls4", "again");
+
+  yield gUI.once("store-objects-updated");
+  yield gUI.once("store-objects-updated");
+
+  yield checkState([
+    [
+      ["localStorage", "http://test1.example.org"],
+      ["ls1", "ls2", "ls3", "ls4", "ls5", "ls6", "ls7"]
+    ],
+  ]);
+  // Updating a row
+  gWindow.localStorage.setItem("ls2", "ls2-changed");
+
+  yield gUI.once("store-objects-updated");
+  yield gUI.once("store-objects-updated");
+
+  checkCell("ls2", "value", "ls2-changed");
+
+  // 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");
+
+  yield checkState([
+    [
+      ["localStorage", "http://test1.example.org"],
+      [ ]
+    ],
+  ]);
+
+  yield finishTests();
+});
rename from devtools/client/storage/test/browser_storage_dynamic_updates.js
rename to devtools/client/storage/test/browser_storage_dynamic_updates_sessionStorage.js
--- a/devtools/client/storage/test/browser_storage_dynamic_updates.js
+++ b/devtools/client/storage/test/browser_storage_dynamic_updates_sessionStorage.js
@@ -1,213 +1,84 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
+// Test dynamic updates in the storage inspector for sessionStorage.
+
 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");
+  yield checkState([
+    [
+      ["sessionStorage", "http://test1.example.org"],
+      ["ss1", "ss2", "ss3"]
+    ],
+  ]);
 
   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");
+  yield checkState([
+    [
+      ["sessionStorage", "http://test1.example.org"],
+      ["ss1", "ss2", "ss3", "ss4"]
+    ],
+  ]);
 
   // 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 checkState([
+    [
+      ["sessionStorage", "http://test1.example.org"],
+      ["ss2", "ss4"]
+    ],
+  ]);
 
   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");
+  checkCell("ss2", "value", "changed=ss2");
 
   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 checkState([
+    [
+      ["sessionStorage", "http://test1.example.org"],
+      [ ]
+    ],
+  ]);
 
   yield finishTests();
 });
--- a/devtools/client/storage/test/browser_storage_sidebar.js
+++ b/devtools/client/storage/test/browser_storage_sidebar.js
@@ -15,32 +15,32 @@
 "use strict";
 
 const testCases = [
   {
     location: ["cookies", "sectest1.example.org"],
     sidebarHidden: true
   },
   {
-    location: "cs2",
+    location: getCookieId("cs2", ".example.org", "/"),
     sidebarHidden: false
   },
   {
     sendEscape: true
   },
   {
-    location: "cs2",
+    location: getCookieId("cs2", ".example.org", "/"),
     sidebarHidden: false
   },
   {
-    location: "uc1",
+    location: getCookieId("uc1", ".example.org", "/"),
     sidebarHidden: false
   },
   {
-    location: "uc1",
+    location: getCookieId("uc1", ".example.org", "/"),
     sidebarHidden: false
   },
 
   {
     location: ["localStorage", "http://sectest1.example.org"],
     sidebarHidden: true
   },
   {
--- a/devtools/client/storage/test/browser_storage_values.js
+++ b/devtools/client/storage/test/browser_storage_values.js
@@ -12,44 +12,48 @@
 //   true if the check is to be made in the parsed value section
 // ]
 
 "use strict";
 
 const LONG_WORD = "a".repeat(1000);
 
 const testCases = [
-  ["cs2", [
+  [getCookieId("cs2", ".example.org", "/"), [
     {name: "cs2", value: "sessionCookie"},
     {name: "cs2.Path", value: "/"},
     {name: "cs2.HostOnly", value: "false"},
     {name: "cs2.HttpOnly", value: "false"},
     {name: "cs2.Domain", value: ".example.org"},
     {name: "cs2.Expires", value: "Session"},
     {name: "cs2.Secure", value: "false"},
   ]],
-  ["c1", [
+  [getCookieId("c1", "test1.example.org", "/browser"), [
     {name: "c1", value: JSON.stringify(["foo", "Bar", {foo: "Bar"}])},
     {name: "c1.Path", value: "/browser"},
     {name: "c1.HostOnly", value: "true"},
     {name: "c1.HttpOnly", value: "false"},
     {name: "c1.Domain", value: "test1.example.org"},
     {name: "c1.Expires", 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"}}))}
-  ]],
+  [
+    getCookieId("c_encoded", "test1.example.org",
+                "/browser/devtools/client/storage/test/"),
+    [
+      {name: "c_encoded", value: encodeURIComponent(JSON.stringify({foo: {foo1: "bar"}}))}
+    ]
+  ],
   [null, [
     {name: "c_encoded", value: "Object"},
     {name: "c_encoded.foo", value: "Object"},
     {name: "c_encoded.foo.foo1", value: "bar"}
   ], true],
   [["localStorage", "http://test1.example.org"]],
   ["ls2", [
     {name: "ls2", value: "foobar-2"}
--- a/devtools/client/storage/test/head.js
+++ b/devtools/client/storage/test/head.js
@@ -19,16 +19,21 @@ const DUMPEMIT_PREF = "devtools.dump.emi
 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;
 
+// GUID to be used as a separator in compound keys. This must match the same
+// constant in devtools/server/actors/storage.js,
+// devtools/client/storage/ui.js and devtools/server/tests/browser/head.js
+const SEPARATOR_GUID = "{9d414cc5-8319-0a04-0586-c0a6ae01670a}";
+
 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(() => {
@@ -500,20 +505,27 @@ function* selectTreeItem(ids) {
 
 /**
  * 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 table = gUI.table;
+  let selector = ".table-widget-column#" + table.uniqueId +
+                 " .table-widget-cell[value='" + id + "']";
   let target = gPanelWindow.document.querySelector(selector);
+
   ok(target, "table item found with ids " + id);
 
+  if (!target) {
+    showAvailableIds();
+  }
+
   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
@@ -581,32 +593,49 @@ function getRowValues(id, includeHidden 
 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");
+
+    showAvailableIds();
   }
 
-  let index = table.columns.get(table.uniqueId).visibleCellNodes.indexOf(item);
+  let index = table.columns.get(table.uniqueId).cellNodes.indexOf(item);
   let cells = {};
 
   for (let [name, column] of [...table.columns]) {
     if (!includeHidden && column.column.parentNode.hidden) {
       continue;
     }
-    cells[name] = column.visibleCellNodes[index];
+    cells[name] = column.cellNodes[index];
   }
 
   return cells;
 }
 
 /**
+ * Show available ids.
+ */
+function showAvailableIds() {
+  let doc = gPanelWindow.document;
+  let table = gUI.table;
+
+  info("Available ids:");
+  let cells = doc.querySelectorAll(".table-widget-column#" + table.uniqueId +
+                                   " .table-widget-cell");
+  for (let cell of cells) {
+    info("  - " + cell.getAttribute("value"));
+  }
+}
+
+/**
  * Get a cell value.
  *
  * @param {String} id
  *        The uniqueId of the row.
  * @param {String} column
  *        The id of the column
  *
  * @yield {String}
@@ -793,19 +822,28 @@ function* checkState(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}`);
+
+    if (names.length === 0) {
+      showAvailableIds();
+    }
+
     for (let name of names) {
       ok(items.has(name),
         `There is item with name '${name}' in ${storeName}`);
+
+      if (!items.has(name)) {
+        showAvailableIds();
+      }
     }
   }
 }
 
 /**
  * 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
@@ -833,8 +871,12 @@ var focusSearchBoxUsingShortcut = Task.a
   synthesizeKeyShortcut(strings.GetStringFromName("storage.filter.key"));
 
   yield focused;
 
   if (callback) {
     callback();
   }
 });
+
+function getCookieId(name, domain, path) {
+  return `${name}${SEPARATOR_GUID}${domain}${SEPARATOR_GUID}${path}`;
+}
--- a/devtools/client/storage/test/storage-updates.html
+++ b/devtools/client/storage/test/storage-updates.html
@@ -33,18 +33,20 @@ window.removeCookie = function(name, pat
 };
 
 /**
  * We keep this method here even though these items are automatically cleared
  * after the test is complete. this is so that the store-objects-cleared event
  * can be tested.
  */
 window.clear = function*() {
+  localStorage.clear();
+  dump("removed localStorage from " + document.location + "\n");
+
   sessionStorage.clear();
-
   dump("removed sessionStorage from " + document.location + "\n");
 };
 
 window.onload = function() {
   addCookie("c1", "1.2.3.4.5.6.7", "/browser");
   addCookie("c2", "foobar", "/browser");
 
   localStorage.setItem("ls1", "testing");
--- a/devtools/client/storage/ui.js
+++ b/devtools/client/storage/ui.js
@@ -7,16 +7,22 @@
 
 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");
 
+// GUID to be used as a separator in compound keys. This must match the same
+// constant in devtools/server/actors/storage.js,
+// devtools/client/storage/test/head.js and
+// devtools/server/tests/browser/head.js
+const SEPARATOR_GUID = "{9d414cc5-8319-0a04-0586-c0a6ae01670a}";
+
 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");
@@ -31,23 +37,16 @@ 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 = {
@@ -781,43 +780,53 @@ StorageUI.prototype = {
    */
   resetColumns: function* (type, host, subtype) {
     this.table.host = host;
     this.table.datatype = type;
 
     let uniqueKey = null;
     let columns = {};
     let editableFields = [];
+    let hiddenFields = [];
+    let privateFields = [];
     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);
       }
 
+      if (f.hidden) {
+        hiddenFields.push(f.name);
+      }
+
+      if (f.private) {
+        privateFields.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.table.setColumns(columns, null, hiddenFields, privateFields);
     this.hideSidebar();
 
     yield this.makeFieldsEditable(editableFields);
   },
 
   /**
    * Populates or updates the rows in the storage table.
    *
@@ -922,20 +931,23 @@ StorageUI.prototype = {
     // 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]);
+    let name = data[this.table.uniqueId];
+
+    let separatorRegex = new RegExp(SEPARATOR_GUID, "g");
+    let label = addEllipsis((name + "").replace(separatorRegex, "-"));
 
     this._tablePopupDelete.setAttribute("label",
-      L10N.getFormatStr("storage.popupMenu.deleteLabel", name));
+      L10N.getFormatStr("storage.popupMenu.deleteLabel", label));
 
     if (type === "cookies") {
       let host = addEllipsis(data.host);
 
       this._tablePopupDeleteAllFrom.hidden = false;
       this._tablePopupDeleteAllFrom.setAttribute("label",
         L10N.getFormatStr("storage.popupMenu.deleteAllFromLabel", host));
     } else {
--- a/devtools/server/actors/storage.js
+++ b/devtools/server/actors/storage.js
@@ -10,16 +10,22 @@ const protocol = require("devtools/share
 const {LongStringActor} = require("devtools/server/actors/string");
 const {DebuggerServer} = require("devtools/server/main");
 const Services = require("Services");
 const promise = require("promise");
 const {isWindowIncluded} = require("devtools/shared/layout/utils");
 const specs = require("devtools/shared/specs/storage");
 const { Task } = require("devtools/shared/task");
 
+// GUID to be used as a separator in compound keys. This must match the same
+// constant in devtools/client/storage/ui.js,
+// devtools/client/storage/test/head.js and
+// devtools/server/tests/browser/head.js
+const SEPARATOR_GUID = "{9d414cc5-8319-0a04-0586-c0a6ae01670a}";
+
 loader.lazyImporter(this, "OS", "resource://gre/modules/osfile.jsm");
 loader.lazyImporter(this, "Sqlite", "resource://gre/modules/Sqlite.jsm");
 
 // We give this a funny name to avoid confusion with the global
 // indexedDB.
 loader.lazyGetter(this, "indexedDBForStorage", () => {
   // On xpcshell, we can't instantiate indexedDB without crashing
   try {
@@ -462,24 +468,26 @@ StorageActors.createActor({
   },
 
   toStoreObject(cookie) {
     if (!cookie) {
       return null;
     }
 
     return {
+      uniqueKey: `${cookie.name}${SEPARATOR_GUID}${cookie.host}` +
+                 `${SEPARATOR_GUID}${cookie.path}`,
       name: cookie.name,
+      host: cookie.host || "",
       path: cookie.path || "",
-      host: cookie.host || "",
 
       // because expires is in seconds
       expires: (cookie.expires || 0) * 1000,
 
-      // because it is in micro seconds
+      // because creationTime is in micro seconds
       creationTime: cookie.creationTime / 1000,
 
       // - do -
       lastAccessed: cookie.lastAccessed / 1000,
       value: new LongStringActor(this.conn, cookie.value || ""),
       isDomain: cookie.isDomain,
       isSecure: cookie.isSecure,
       isHttpOnly: cookie.isHttpOnly
@@ -490,17 +498,20 @@ StorageActors.createActor({
     this.hostVsStores.set(host, new Map());
     let doc = this.storageActor.document;
 
     let cookies = this.getCookiesFromHost(host, doc.nodePrincipal
                                                    .originAttributes);
 
     for (let cookie of cookies) {
       if (this.isCookieAtHost(cookie, host)) {
-        this.hostVsStores.get(host).set(cookie.name, cookie);
+        let uniqueKey = `${cookie.name}${SEPARATOR_GUID}${cookie.host}` +
+                        `${SEPARATOR_GUID}${cookie.path}`;
+
+        this.hostVsStores.get(host).set(uniqueKey, cookie);
       }
     }
   },
 
   /**
    * Notification observer for "cookie-change".
    *
    * @param subject
@@ -523,40 +534,49 @@ StorageActors.createActor({
     let hosts = this.getMatchingHosts(subject);
     let data = {};
 
     switch (action) {
       case "added":
       case "changed":
         if (hosts.length) {
           for (let host of hosts) {
-            this.hostVsStores.get(host).set(subject.name, subject);
-            data[host] = [subject.name];
+            let uniqueKey = `${subject.name}${SEPARATOR_GUID}${subject.host}` +
+                            `${SEPARATOR_GUID}${subject.path}`;
+
+            this.hostVsStores.get(host).set(uniqueKey, subject);
+            data[host] = [uniqueKey];
           }
           this.storageActor.update(action, "cookies", data);
         }
         break;
 
       case "deleted":
         if (hosts.length) {
           for (let host of hosts) {
-            this.hostVsStores.get(host).delete(subject.name);
-            data[host] = [subject.name];
+            let uniqueKey = `${subject.name}${SEPARATOR_GUID}${subject.host}` +
+                            `${SEPARATOR_GUID}${subject.path}`;
+
+            this.hostVsStores.get(host).delete(uniqueKey);
+            data[host] = [uniqueKey];
           }
           this.storageActor.update("deleted", "cookies", data);
         }
         break;
 
       case "batch-deleted":
         if (hosts.length) {
           for (let host of hosts) {
             let stores = [];
             for (let cookie of subject) {
-              this.hostVsStores.get(host).delete(cookie.name);
-              stores.push(cookie.name);
+              let uniqueKey = `${cookie.name}${SEPARATOR_GUID}${cookie.host}` +
+                              `${SEPARATOR_GUID}${cookie.path}`;
+
+              this.hostVsStores.get(host).delete(uniqueKey);
+              stores.push(uniqueKey);
             }
             data[host] = stores;
           }
           this.storageActor.update("deleted", "cookies", data);
         }
         break;
 
       case "cleared":
@@ -568,25 +588,27 @@ StorageActors.createActor({
         }
         break;
     }
     return null;
   },
 
   getFields: Task.async(function* () {
     return [
-      { name: "name", editable: 1},
-      { name: "path", editable: 1},
-      { name: "host", editable: 1},
-      { name: "expires", editable: 1},
-      { name: "lastAccessed", editable: 0},
-      { name: "value", editable: 1},
-      { name: "isDomain", editable: 0},
-      { name: "isSecure", editable: 1},
-      { name: "isHttpOnly", editable: 1}
+      { name: "uniqueKey", editable: false, private: true },
+      { name: "name", editable: true, hidden: false },
+      { name: "host", editable: true, hidden: false },
+      { name: "path", editable: true, hidden: false },
+      { name: "expires", editable: true, hidden: false },
+      { name: "lastAccessed", editable: false, hidden: false },
+      { name: "creationTime", editable: false, hidden: true },
+      { name: "value", editable: true, hidden: false },
+      { name: "isDomain", editable: false, hidden: true },
+      { name: "isSecure", editable: true, hidden: true },
+      { name: "isHttpOnly", editable: true, hidden: false }
     ];
   }),
 
   /**
    * Pass the editItem command from the content to the chrome process.
    *
    * @param {Object} data
    *        See editCookie() for format details.
@@ -698,17 +720,17 @@ var cookieHelpers = {
   /**
    * Apply the results of a cookie edit.
    *
    * @param {Object} data
    *        An object in the following format:
    *        {
    *          host: "http://www.mozilla.org",
    *          field: "value",
-   *          key: "name",
+   *          editCookie: "name",
    *          oldValue: "%7BHello%7D",
    *          newValue: "%7BHelloo%7D",
    *          items: {
    *            name: "optimizelyBuckets",
    *            path: "/",
    *            host: ".mozilla.org",
    *            expires: "Mon, 02 Jun 2025 12:37:37 GMT",
    *            creationTime: "Tue, 18 Nov 2014 16:21:18 GMT",
@@ -722,20 +744,23 @@ var cookieHelpers = {
    */
   editCookie(data) {
     let {field, oldValue, newValue} = data;
     let origName = field === "name" ? oldValue : data.items.name;
     let origHost = field === "host" ? oldValue : data.items.host;
     let origPath = field === "path" ? oldValue : data.items.path;
     let cookie = null;
 
-    let enumerator = Services.cookies.getCookiesFromHost(origHost, data.originAttributes || {});
+    let enumerator =
+      Services.cookies.getCookiesFromHost(origHost, data.originAttributes || {});
     while (enumerator.hasMoreElements()) {
       let nsiCookie = enumerator.getNext().QueryInterface(Ci.nsICookie2);
-      if (nsiCookie.name === origName && nsiCookie.host === origHost) {
+      if (nsiCookie.name === origName &&
+          nsiCookie.host === origHost &&
+          nsiCookie.path === origPath) {
         cookie = {
           host: nsiCookie.host,
           path: nsiCookie.path,
           name: nsiCookie.name,
           value: nsiCookie.value,
           isSecure: nsiCookie.isSecure,
           isHttpOnly: nsiCookie.isHttpOnly,
           isSession: nsiCookie.isSession,
@@ -745,17 +770,17 @@ var cookieHelpers = {
         break;
       }
     }
 
     if (!cookie) {
       return;
     }
 
-    // If the date is expired set it for 1 minute in the future.
+    // If the date is expired set it for 10 seconds in the future.
     let now = new Date();
     if (!cookie.isSession && (cookie.expires * 1000) <= now) {
       let tenSecondsFromNow = (now.getTime() + 10 * 1000) / 1000;
 
       cookie.expires = tenSecondsFromNow;
     }
 
     switch (field) {
@@ -799,32 +824,44 @@ var cookieHelpers = {
       cookie.isHttpOnly,
       cookie.isSession,
       cookie.isSession ? MAX_COOKIE_EXPIRY : cookie.expires,
       cookie.originAttributes
     );
   },
 
   _removeCookies(host, opts = {}) {
+    // We use a uniqueId to emulate compound keys for cookies. We need to
+    // extract the cookie name to remove the correct cookie.
+    if (opts.name) {
+      let split = opts.name.split(SEPARATOR_GUID);
+
+      opts.name = split[0];
+      opts.path = split[2];
+    }
+
     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, opts.originAttributes || {});
+    let enumerator =
+      Services.cookies.getCookiesFromHost(host, opts.originAttributes || {});
+
     while (enumerator.hasMoreElements()) {
       let cookie = enumerator.getNext().QueryInterface(Ci.nsICookie2);
       if (hostMatches(cookie.host, host) &&
           (!opts.name || cookie.name === opts.name) &&
-          (!opts.domain || cookie.host === opts.domain)) {
+          (!opts.domain || cookie.host === opts.domain) &&
+          (!opts.path || cookie.path === opts.path)) {
         Services.cookies.remove(
           cookie.host,
           cookie.name,
           cookie.path,
           false,
           cookie.originAttributes
         );
       }
@@ -1026,18 +1063,18 @@ function getObjectForLocalOrSessionStora
       this.hostVsStores = new Map();
       for (let window of this.windows) {
         this.populateStoresForHost(this.getHostName(window.location), window);
       }
     },
 
     getFields: Task.async(function* () {
       return [
-        { name: "name", editable: 1},
-        { name: "value", editable: 1}
+        { name: "name", editable: true },
+        { name: "value", editable: true }
       ];
     }),
 
     /**
      * Edit localStorage or sessionStorage fields.
      *
      * @param {Object} data
      *        See editCookie() for format details.
@@ -1207,18 +1244,18 @@ StorageActors.createActor({
     return {
       url: String(request.url),
       status: String(response.statusText),
     };
   }),
 
   getFields: Task.async(function* () {
     return [
-      { name: "url", editable: 0 },
-      { name: "status", editable: 0 }
+      { name: "url", editable: false },
+      { name: "status", editable: false }
     ];
   }),
 
   getHostName(location) {
     if (!location.host) {
       return location.href;
     }
     return location.protocol + "//" + location.host;
@@ -1731,36 +1768,36 @@ StorageActors.createActor({
     }
   },
 
   getFields: Task.async(function* (subType) {
     switch (subType) {
       // Detail of database
       case "database":
         return [
-          { name: "objectStore", editable: 0 },
-          { name: "keyPath", editable: 0 },
-          { name: "autoIncrement", editable: 0 },
-          { name: "indexes", editable: 0 },
+          { name: "objectStore", editable: false },
+          { name: "keyPath", editable: false },
+          { name: "autoIncrement", editable: false },
+          { name: "indexes", editable: false },
         ];
 
       // Detail of object store
       case "object store":
         return [
-          { name: "name", editable: 0 },
-          { name: "value", editable: 0 }
+          { name: "name", editable: false },
+          { name: "value", editable: false }
         ];
 
       // Detail of indexedDB for one origin
       default:
         return [
-          { name: "db", editable: 0 },
-          { name: "origin", editable: 0 },
-          { name: "version", editable: 0 },
-          { name: "objectStores", editable: 0 },
+          { name: "db", editable: false },
+          { name: "origin", editable: false },
+          { name: "version", editable: false },
+          { name: "objectStores", editable: false },
         ];
     }
   })
 });
 
 var indexedDBHelpers = {
   backToChild(...args) {
     let mm = Cc["@mozilla.org/globalmessagemanager;1"]
@@ -2489,16 +2526,17 @@ let StorageActor = protocol.ActorClassWi
       // items from changed instead.
       this.removeNamesFromUpdateList("changed", storeType,
                                      this.boundUpdate.added[storeType]);
     } else if (action == "deleted") {
       // If any item got delete, or a host got delete, no point in sending
       // added or changed update
       this.removeNamesFromUpdateList("added", storeType, data);
       this.removeNamesFromUpdateList("changed", storeType, data);
+
       for (let host in data) {
         if (data[host].length == 0 && this.boundUpdate.added &&
             this.boundUpdate.added[storeType] &&
             this.boundUpdate.added[storeType][host]) {
           delete this.boundUpdate.added[storeType][host];
         }
         if (data[host].length == 0 && this.boundUpdate.changed &&
             this.boundUpdate.changed[storeType] &&
--- a/devtools/server/tests/browser/browser.ini
+++ b/devtools/server/tests/browser/browser.ini
@@ -6,16 +6,17 @@ support-files =
   animation.html
   doc_allocations.html
   doc_force_cc.html
   doc_force_gc.html
   doc_innerHTML.html
   doc_perf.html
   navigate-first.html
   navigate-second.html
+  storage-cookies-same-name.html
   storage-dynamic-windows.html
   storage-listings.html
   storage-unsecured-iframe.html
   storage-updates.html
   storage-secured-iframe.html
   stylesheets-nested-iframes.html
   timeline-iframe-child.html
   timeline-iframe-parent.html
@@ -75,16 +76,17 @@ skip-if = true # Needs to be updated for
 skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S
 [browser_perf-recording-actor-02.js]
 skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S
 [browser_perf-samples-01.js]
 [browser_perf-samples-02.js]
 #[browser_perf-front-profiler-01.js] bug 1077464
 #[browser_perf-front-profiler-05.js] bug 1077464
 #[browser_perf-front-profiler-06.js]
+[browser_storage_cookies-duplicate-names.js]
 [browser_storage_dynamic_windows.js]
 [browser_storage_listings.js]
 [browser_storage_updates.js]
 [browser_stylesheets_getTextEmpty.js]
 [browser_stylesheets_nested-iframes.js]
 [browser_timeline.js]
 [browser_timeline_actors.js]
 [browser_timeline_iframes.js]
new file mode 100644
--- /dev/null
+++ b/devtools/server/tests/browser/browser_storage_cookies-duplicate-names.js
@@ -0,0 +1,105 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the storage panel is able to display multiple cookies with the same
+// name (and different paths).
+
+const {StorageFront} = require("devtools/shared/fronts/storage");
+Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/server/tests/browser/storage-helpers.js", this);
+
+const TESTDATA = {
+  "test1.example.org": [
+    {
+      name: "name",
+      value: "value1",
+      expires: 0,
+      path: "/",
+      host: "test1.example.org",
+      isDomain: false,
+      isSecure: false,
+    },
+    {
+      name: "name",
+      value: "value2",
+      expires: 0,
+      path: "/path2/",
+      host: "test1.example.org",
+      isDomain: false,
+      isSecure: false,
+    },
+    {
+      name: "name",
+      value: "value3",
+      expires: 0,
+      path: "/path3/",
+      host: "test1.example.org",
+      isDomain: false,
+      isSecure: false,
+    }
+  ]
+};
+
+add_task(function* () {
+  yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-cookies-same-name.html");
+
+  initDebuggerServer();
+  let client = new DebuggerClient(DebuggerServer.connectPipe());
+  let form = yield connectDebuggerClient(client);
+  let front = StorageFront(client, form);
+  let data = yield front.listStores();
+
+  ok(data.cookies, "Cookies storage actor is present");
+
+  yield testCookies(data.cookies);
+  yield clearStorage();
+
+  // Forcing GC/CC to get rid of docshells and windows created by this test.
+  forceCollections();
+  yield client.close();
+  forceCollections();
+  DebuggerServer.destroy();
+  forceCollections();
+});
+
+function testCookies(cookiesActor) {
+  let numHosts = Object.keys(cookiesActor.hosts).length;
+  is(numHosts, 1, "Correct number of host entries for cookies");
+  return testCookiesObjects(0, cookiesActor.hosts, cookiesActor);
+}
+
+var testCookiesObjects = Task.async(function* (index, hosts, cookiesActor) {
+  let host = Object.keys(hosts)[index];
+  let matchItems = data => {
+    is(data.total, TESTDATA[host].length,
+       "Number of cookies in host " + host + " matches");
+    for (let item of data.data) {
+      let found = false;
+      for (let toMatch of TESTDATA[host]) {
+        if (item.name === toMatch.name &&
+            item.host === toMatch.host &&
+            item.path === toMatch.path) {
+          found = true;
+          ok(true, "Found cookie " + item.name + " in response");
+          is(item.value.str, toMatch.value, "The value matches.");
+          is(item.expires, toMatch.expires, "The expiry time matches.");
+          is(item.path, toMatch.path, "The path matches.");
+          is(item.host, toMatch.host, "The host matches.");
+          is(item.isSecure, toMatch.isSecure, "The isSecure value matches.");
+          is(item.isDomain, toMatch.isDomain, "The isDomain value matches.");
+          break;
+        }
+      }
+      ok(found, "cookie " + item.name + " should exist in response");
+    }
+  };
+
+  ok(!!TESTDATA[host], "Host is present in the list : " + host);
+  matchItems(yield cookiesActor.getStoreObjects(host));
+  if (index == Object.keys(hosts).length - 1) {
+    return;
+  }
+  yield testCookiesObjects(++index, hosts, cookiesActor);
+});
--- a/devtools/server/tests/browser/browser_storage_dynamic_windows.js
+++ b/devtools/server/tests/browser/browser_storage_dynamic_windows.js
@@ -61,16 +61,17 @@ function markOutMatched(toBeEmptied, dat
      "At least one storage type should be present");
   for (let storageType in toBeEmptied) {
     if (!data[storageType]) {
       continue;
     }
     info("Testing for " + storageType);
     for (let host in data[storageType]) {
       ok(toBeEmptied[storageType][host], "Host " + host + " found");
+
       if (!deleted) {
         for (let item of data[storageType][host]) {
           let index = toBeEmptied[storageType][host].indexOf(item);
           ok(index > -1, "Item found - " + item);
           if (index > -1) {
             toBeEmptied[storageType][host].splice(index, 1);
           }
         }
@@ -82,80 +83,39 @@ function markOutMatched(toBeEmptied, dat
       }
     }
     if (!Object.keys(toBeEmptied[storageType]).length) {
       delete toBeEmptied[storageType];
     }
   }
 }
 
-// function testReload(front) {
-//   info("Testing if reload works properly");
-
-//   let shouldBeEmptyFirst = Cu.cloneInto(beforeReload,  {});
-//   let shouldBeEmptyLast = Cu.cloneInto(beforeReload,  {});
-//   return new Promise(resolve => {
-
-//     let onStoresUpdate = data => {
-//       info("in stores update of testReload");
-//       // This might be second time stores update is happening, in which case,
-//       // data.deleted will be null.
-//       // OR.. This might be the first time on a super slow machine where both
-//       // data.deleted and data.added is missing in the first update.
-//       if (data.deleted) {
-//         markOutMatched(shouldBeEmptyFirst, data.deleted, true);
-//       }
-
-//       if (!Object.keys(shouldBeEmptyFirst).length) {
-//         info("shouldBeEmptyFirst is empty now");
-//       }
-
-//       // stores-update call might not have data.added for the first time on
-//       // slow machines, in which case, data.added will be null
-//       if (data.added) {
-//         markOutMatched(shouldBeEmptyLast, data.added);
-//       }
-
-//       if (!Object.keys(shouldBeEmptyLast).length) {
-//         info("Everything to be received is received.");
-//         endTestReloaded();
-//       }
-//     };
-
-//     let endTestReloaded = () => {
-//       front.off("stores-update", onStoresUpdate);
-//       resolve();
-//     };
-
-//     front.on("stores-update", onStoresUpdate);
-
-//     content.location.reload();
-//   });
-// }
-
 function testAddIframe(front) {
   info("Testing if new iframe addition works properly");
   return new Promise(resolve => {
     let shouldBeEmpty = {
       localStorage: {
         "https://sectest1.example.org": ["iframe-s-ls1"]
       },
       sessionStorage: {
         "https://sectest1.example.org": ["iframe-s-ss1"]
       },
       cookies: {
-        "sectest1.example.org": ["sc1"]
+        "sectest1.example.org": [
+          getCookieId("sc1", "sectest1.example.org",
+                      "/browser/devtools/server/tests/browser/")
+        ]
       },
       indexedDB: {
         // empty because indexed db creation happens after the page load, so at
         // the time of window-ready, there was no indexed db present.
         "https://sectest1.example.org": []
       },
       Cache: {
-        "https://sectest1.example.org":[]
+        "https://sectest1.example.org": []
       }
     };
 
     let onStoresUpdate = data => {
       info("checking if the hosts list is correct for this iframe addition");
 
       markOutMatched(shouldBeEmpty, data.added);
 
--- a/devtools/server/tests/browser/browser_storage_listings.js
+++ b/devtools/server/tests/browser/browser_storage_listings.js
@@ -30,25 +30,16 @@ const storeMap = {
       },
       {
         name: "c3",
         value: "foobar-2",
         expires: 2000000001000,
         path: "/",
         host: "test1.example.org",
         isDomain: false,
-        isSecure: false,
-      },
-      {
-        name: "uc1",
-        value: "foobar",
-        host: ".example.org",
-        path: "/",
-        expires: 0,
-        isDomain: true,
         isSecure: true,
       }
     ],
     "sectest1.example.org": [
       {
         name: "uc1",
         value: "foobar",
         host: ".example.org",
@@ -332,28 +323,29 @@ function* testStores(data) {
   ok(data.indexedDB, "Indexed DB storage actor is present");
   yield testCookies(data.cookies);
   yield testLocalStorage(data.localStorage);
   yield testSessionStorage(data.sessionStorage);
   yield testIndexedDB(data.indexedDB);
 }
 
 function testCookies(cookiesActor) {
-  is(Object.keys(cookiesActor.hosts).length, 2, "Correct number of host entries for cookies");
+  is(Object.keys(cookiesActor.hosts).length, 2,
+                 "Correct number of host entries for cookies");
   return testCookiesObjects(0, cookiesActor.hosts, cookiesActor);
 }
 
 var testCookiesObjects = Task.async(function* (index, hosts, cookiesActor) {
   let host = Object.keys(hosts)[index];
   let matchItems = data => {
     let cookiesLength = 0;
     for (let secureCookie of storeMap.cookies[host]) {
-       if (secureCookie.isSecure) {
-          ++cookiesLength;
-       }
+      if (secureCookie.isSecure) {
+        ++cookiesLength;
+      }
     }
     // Any secure cookies did not get stored in the database.
     is(data.total, storeMap.cookies[host].length - cookiesLength,
        "Number of cookies in host " + host + " matches");
     for (let item of data.data) {
       let found = false;
       for (let toMatch of storeMap.cookies[host]) {
         if (item.name == toMatch.name) {
--- a/devtools/server/tests/browser/browser_storage_updates.js
+++ b/devtools/server/tests/browser/browser_storage_updates.js
@@ -22,17 +22,22 @@ const TESTS = [
       win.addCookie("c2", "foobar2");
 
       info('win.localStorage.setItem("l1", "foobar1")');
       win.localStorage.setItem("l1", "foobar1");
     },
     expected: {
       added: {
         cookies: {
-          "test1.example.org": ["c1", "c2"]
+          "test1.example.org": [
+            getCookieId("c1", "test1.example.org",
+                        "/browser/devtools/server/tests/browser/"),
+            getCookieId("c2", "test1.example.org",
+                        "/browser/devtools/server/tests/browser/")
+          ]
         },
         localStorage: {
           "http://test1.example.org": ["l1"]
         }
       }
     }
   },
 
@@ -43,17 +48,20 @@ const TESTS = [
       win.addCookie("c1", "new_foobar1");
 
       info('win.localStorage.setItem("l2", "foobar2")');
       win.localStorage.setItem("l2", "foobar2");
     },
     expected: {
       changed: {
         cookies: {
-          "test1.example.org": ["c1"]
+          "test1.example.org": [
+            getCookieId("c1", "test1.example.org",
+                        "/browser/devtools/server/tests/browser/"),
+          ]
         }
       },
       added: {
         localStorage: {
           "http://test1.example.org": ["l2"]
         }
       }
     }
@@ -69,17 +77,20 @@ const TESTS = [
       win.localStorage.removeItem("l1");
 
       info('win.localStorage.setItem("l3", "foobar3")');
       win.localStorage.setItem("l3", "foobar3");
     },
     expected: {
       deleted: {
         cookies: {
-          "test1.example.org": ["c2"]
+          "test1.example.org": [
+            getCookieId("c2", "test1.example.org",
+                        "/browser/devtools/server/tests/browser/"),
+          ]
         },
         localStorage: {
           "http://test1.example.org": ["l1"]
         }
       },
       added: {
         localStorage: {
           "http://test1.example.org": ["l3"]
@@ -107,30 +118,36 @@ const TESTS = [
       win.sessionStorage.setItem("s2", "foobar2");
 
       info('win.localStorage.setItem("l3", "new_foobar3")');
       win.localStorage.setItem("l3", "new_foobar3");
     },
     expected: {
       added: {
         cookies: {
-          "test1.example.org": ["c3"]
+          "test1.example.org": [
+            getCookieId("c3", "test1.example.org",
+                        "/browser/devtools/server/tests/browser/"),
+          ]
         },
         sessionStorage: {
           "http://test1.example.org": ["s1", "s2"]
         }
       },
       changed: {
         localStorage: {
           "http://test1.example.org": ["l3"]
         }
       },
       deleted: {
         cookies: {
-          "test1.example.org": ["c1"]
+          "test1.example.org": [
+            getCookieId("c1", "test1.example.org",
+                        "/browser/devtools/server/tests/browser/"),
+          ]
         },
         localStorage: {
           "http://test1.example.org": ["l2"]
         }
       }
     }
   },
 
@@ -153,17 +170,20 @@ const TESTS = [
   {
     action: function (win) {
       info("win.clearCookies()");
       win.clearCookies();
     },
     expected: {
       deleted: {
         cookies: {
-          "test1.example.org": ["c3"]
+          "test1.example.org": [
+            getCookieId("c3", "test1.example.org",
+                        "/browser/devtools/server/tests/browser/"),
+          ]
         }
       }
     }
   }
 ];
 
 function markOutMatched(toBeEmptied, data) {
   if (!Object.keys(toBeEmptied).length) {
--- a/devtools/server/tests/browser/head.js
+++ b/devtools/server/tests/browser/head.js
@@ -1,12 +1,16 @@
 /* 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"}] */
+
 var Cc = Components.classes;
 var Ci = Components.interfaces;
 var Cu = Components.utils;
 
 const {console} = Cu.import("resource://gre/modules/Console.jsm", {});
 const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
 const {DebuggerClient} = require("devtools/shared/client/main");
 const {DebuggerServer} = require("devtools/server/main");
@@ -14,16 +18,21 @@ const {defer} = require("promise");
 const DevToolsUtils = require("devtools/shared/DevToolsUtils");
 const Services = require("Services");
 
 const PATH = "browser/devtools/server/tests/browser/";
 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;
 
+// GUID to be used as a separator in compound keys. This must match the same
+// constant in devtools/server/actors/storage.js,
+// devtools/client/storage/ui.js and devtools/client/storage/test/head.js
+const SEPARATOR_GUID = "{9d414cc5-8319-0a04-0586-c0a6ae01670a}";
+
 // All tests are asynchronous.
 waitForExplicitFinish();
 
 /**
  * Add a new test tab in the browser and load the given url.
  * @param {String} url The url to be loaded in the new tab
  * @return a promise that resolves to the new browser that the document
  *         is loaded in. Note that we cannot return the document
@@ -89,17 +98,16 @@ function connectDebuggerClient(client) {
  * @param {String} eventName
  * @param {Boolean} useCapture Optional, for addEventListener/removeEventListener
  * @return A promise that resolves when the event has been handled
  */
 function once(target, eventName, useCapture = false) {
   info("Waiting for event: '" + eventName + "' on " + target + ".");
 
   return new Promise(resolve => {
-
     for (let [add, remove] of [
       ["addEventListener", "removeEventListener"],
       ["addListener", "removeListener"],
       ["on", "off"]
     ]) {
       if ((add in target) && (remove in target)) {
         target[add](eventName, function onEvent(...aArgs) {
           info("Got event: '" + eventName + "' on " + target + ".");
@@ -132,29 +140,34 @@ function forceCollections() {
 function getMockTabActor(win) {
   return {
     window: win,
     isRootActor: true
   };
 }
 
 registerCleanupFunction(function tearDown() {
+  Services.cookies.removeAll();
+
   while (gBrowser.tabs.length > 1) {
     gBrowser.removeCurrentTab();
   }
 });
 
 function idleWait(time) {
   return DevToolsUtils.waitForTime(time);
 }
 
 function busyWait(time) {
   let start = Date.now();
+  // eslint-disable-next-line
   let stack;
-  while (Date.now() - start < time) { stack = Components.stack; }
+  while (Date.now() - start < time) {
+    stack = Components.stack;
+  }
 }
 
 /**
  * Waits until a predicate returns true.
  *
  * @param function predicate
  *        Invoked once in a while until it returns true.
  * @param number interval [optional]
@@ -167,37 +180,44 @@ function waitUntil(predicate, interval =
   return new Promise(resolve => {
     setTimeout(function () {
       waitUntil(predicate).then(() => resolve(true));
     }, interval);
   });
 }
 
 function waitForMarkerType(front, types, predicate,
-  unpackFun = (name, data) => data.markers,
-  eventName = "timeline-data")
-{
+                           unpackFun = (name, data) => data.markers,
+                           eventName = "timeline-data") {
   types = [].concat(types);
-  predicate = predicate || function () { return true; };
+  predicate = predicate || function () {
+    return true;
+  };
   let filteredMarkers = [];
   let { promise, resolve } = defer();
 
   info("Waiting for markers of type: " + types);
 
   function handler(name, data) {
     if (typeof name === "string" && name !== "markers") {
       return;
     }
 
     let markers = unpackFun(name, data);
     info("Got markers: " + JSON.stringify(markers, null, 2));
 
-    filteredMarkers = filteredMarkers.concat(markers.filter(m => types.indexOf(m.name) !== -1));
+    filteredMarkers = filteredMarkers.concat(
+      markers.filter(m => types.indexOf(m.name) !== -1));
 
-    if (types.every(t => filteredMarkers.some(m => m.name === t)) && predicate(filteredMarkers)) {
+    if (types.every(t => filteredMarkers.some(m => m.name === t)) &&
+        predicate(filteredMarkers)) {
       front.off(eventName, handler);
       resolve(filteredMarkers);
     }
   }
   front.on(eventName, handler);
 
   return promise;
 }
+
+function getCookieId(name, domain, path) {
+  return `${name}${SEPARATOR_GUID}${domain}${SEPARATOR_GUID}${path}`;
+}
new file mode 100644
--- /dev/null
+++ b/devtools/server/tests/browser/storage-cookies-same-name.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Storage inspector cookies with duplicate names</title>
+</head>
+<body onload="createCookies()">
+<script type="application/javascript;version=1.7">
+"use strict";
+function createCookies() {
+  document.cookie = "name=value1;path=/;";
+  document.cookie = "name=value2;path=/path2/;";
+  document.cookie = "name=value3;path=/path3/;";
+}
+
+window.removeCookie = function (name) {
+  document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT";
+};
+
+window.clearCookies = function () {
+  let cookies = document.cookie;
+  for (let cookie of cookies.split(";")) {
+    removeCookie(cookie.split("=")[0]);
+  }
+};
+</script>
+</body>
+</html>