Bug 965872 - Storage Inspector - actor for cookies, local storage and session storage, r=jwalker
authorGirish Sharma <scrapmachines@gmail.com>
Wed, 12 Mar 2014 03:36:43 +0530
changeset 173135 0dd61eada6c925e48f8949c6858915ed7f8709d5
parent 173134 b5fec3059a699719ca69a37dfb9bbefb1841f7f6
child 173136 163c21c788e593ea10d194616b88f4992f8ff020
push idunknown
push userunknown
push dateunknown
reviewersjwalker
bugs965872
milestone30.0a1
Bug 965872 - Storage Inspector - actor for cookies, local storage and session storage, r=jwalker
browser/devtools/commandline/BuiltinCommands.jsm
toolkit/devtools/server/actors/root.js
toolkit/devtools/server/actors/storage.js
toolkit/devtools/server/main.js
toolkit/devtools/server/moz.build
toolkit/devtools/server/tests/browser/browser.ini
toolkit/devtools/server/tests/browser/browser_storage_dynamic_windows.js
toolkit/devtools/server/tests/browser/browser_storage_listings.js
toolkit/devtools/server/tests/browser/browser_storage_updates.js
toolkit/devtools/server/tests/browser/head.js
toolkit/devtools/server/tests/browser/storage-dynamic-windows.html
toolkit/devtools/server/tests/browser/storage-listings.html
toolkit/devtools/server/tests/browser/storage-secured-iframe.html
toolkit/devtools/server/tests/browser/storage-unsecured-iframe.html
toolkit/devtools/server/tests/browser/storage-updates.html
--- a/browser/devtools/commandline/BuiltinCommands.jsm
+++ b/browser/devtools/commandline/BuiltinCommands.jsm
@@ -952,17 +952,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
   /**
    * Check if a given cookie matches a given host
    */
   function isCookieAtHost(cookie, host) {
     if (cookie.host == null) {
       return host == null;
     }
     if (cookie.host.startsWith(".")) {
-      return cookie.host === "." + host;
+      return host.endsWith(cookie.host);
     }
     else {
       return cookie.host == host;
     }
   }
 
   /**
    * 'cookie' command
--- a/toolkit/devtools/server/actors/root.js
+++ b/toolkit/devtools/server/actors/root.js
@@ -180,16 +180,20 @@ RootActor.prototype = {
         // Wether the server-side highlighter actor exists and can be used to
         // remotely highlight nodes (see server/actors/highlighter.js)
         highlightable: true,
         // Wether the inspector actor implements the getImageDataFromURL
         // method that returns data-uris for image URLs. This is used for image
         // tooltips for instance
         urlToImageDataResolver: true,
         networkMonitor: true,
+        // Wether the storage inspector actor to inspect cookies, etc.
+        storageInspector: true,
+        // Wether storage inspector is read only
+        storageInspectorReadOnly: true,
       }
     };
   },
 
   /**
    * This is true for the root actor only, used by some child actors
    */
   get isRootActor() true,
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/actors/storage.js
@@ -0,0 +1,1085 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {Cu, Cc, Ci} = require("chrome");
+const events = require("sdk/event/core");
+const protocol = require("devtools/server/protocol");
+const {Arg, Option, method, RetVal, types} = protocol;
+const {LongStringActor, ShortLongString} = require("devtools/server/actors/string");
+Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+exports.register = function(handle) {
+  handle.addTabActor(StorageActor, "storageActor");
+};
+
+exports.unregister = function(handle) {
+  handle.removeTabActor(StorageActor);
+};
+
+// Maximum number of cookies/local storage key-value-pairs that can be sent
+// over the wire to the client in one request.
+const MAX_STORE_OBJECT_COUNT = 30;
+// Interval for the batch job that sends the accumilated update packets to the
+// client.
+const UPDATE_INTERVAL = 500; // ms
+
+// Holder for all the registered storage actors.
+let storageTypePool = new Map();
+
+/**
+ * Gets an accumulated list of all storage actors registered to be used to
+ * create a RetVal to define the return type of StorageActor.listStores method.
+ */
+function getRegisteredTypes() {
+  let registeredTypes = {};
+  for (let store of storageTypePool.keys()) {
+    registeredTypes[store] = store;
+  }
+  return registeredTypes;
+}
+
+// Cookies store object
+types.addDictType("cookieobject", {
+  name: "string",
+  value: "longstring",
+  path: "nullable:string",
+  host: "string",
+  isDomain: "boolean",
+  isSecure: "boolean",
+  isHttpOnly: "boolean",
+  creationTime: "number",
+  lastAccessed: "number",
+  expires: "number"
+});
+
+// Array of cookie store objects
+types.addDictType("cookiestoreobject", {
+  total: "number",
+  offset: "number",
+  data: "array:nullable:cookieobject"
+});
+
+// Local Storage / Session Storage store object
+types.addDictType("storageobject", {
+  name: "string",
+  value: "longstring"
+});
+
+// Array of Local Storage / Session Storage store objects
+types.addDictType("storagestoreobject", {
+  total: "number",
+  offset: "number",
+  data: "array:nullable:storageobject"
+});
+
+// Update notification object
+types.addDictType("storeUpdateObject", {
+  changed: "nullable:json",
+  deleted: "nullable:json",
+  added: "nullable:json"
+});
+
+// Helper methods to create a storage actor.
+let StorageActors = {};
+
+/**
+ * Creates a default object with the common methods required by all storage
+ * actors.
+ *
+ * This default object is missing a couple of required methods that should be
+ * implemented seperately for each actor. They are namely:
+ *   - observe : Method which gets triggered on the notificaiton of the watched
+ *               topic.
+ *   - getNamesForHost : Given a host, get list of all known store names.
+ *   - getValuesForHost : Given a host (and optianally a name) get all known
+ *                        store objects.
+ *   - toStoreObject : Given a store object, convert it to the required format
+ *                     so that it can be transferred over wire.
+ *   - populateStoresForHost : Given a host, populate the map of all store
+ *                             objects for it
+ *
+ * @param {string} typeName
+ *        The typeName of the actor.
+ * @param {string} observationTopic
+ *        The topic which this actor listens to via Notification Observers.
+ * @param {string} storeObjectType
+ *        The RetVal type of the store object of this actor.
+ */
+StorageActors.defaults = function(typeName, observationTopic, storeObjectType) {
+  return {
+    typeName: typeName,
+
+    get conn() {
+      return this.storageActor.conn;
+    },
+
+    /**
+     * Returns a list of currently knwon hosts for the target window. This list
+     * contains unique hosts from the window + all inner windows.
+     */
+    get hosts() {
+      let hosts = new Set();
+      for (let {location} of this.storageActor.windows) {
+        hosts.add(this.getHostName(location));
+      }
+      return hosts;
+    },
+
+    /**
+     * Returns all the windows present on the page. Includes main window + inner
+     * iframe windows.
+     */
+    get windows() {
+      return this.storageActor.windows;
+    },
+
+    /**
+     * Converts the window.location object into host.
+     */
+    getHostName: function(location) {
+      return location.hostname || location.href;
+    },
+
+    initialize: function(storageActor) {
+      protocol.Actor.prototype.initialize.call(this, null);
+
+      this.storageActor = storageActor;
+
+      this.populateStoresForHosts();
+      Services.obs.addObserver(this, observationTopic, false);
+      this.onWindowReady = this.onWindowReady.bind(this);
+      this.onWindowDestroyed = this.onWindowDestroyed.bind(this);
+      events.on(this.storageActor, "window-ready", this.onWindowReady);
+      events.on(this.storageActor, "window-destroyed", this.onWindowDestroyed);
+    },
+
+    destroy: function() {
+      this.hostVsStores = null;
+      Services.obs.removeObserver(this, observationTopic, false);
+      events.off(this.storageActor, "window-ready", this.onWindowReady);
+      events.off(this.storageActor, "window-destroyed", this.onWindowDestroyed);
+    },
+
+    /**
+     * When a new window is added to the page. This generally means that a new
+     * iframe is created, or the current window is completely reloaded.
+     *
+     * @param {window} window
+     *        The window which was added.
+     */
+    onWindowReady: function(window) {
+      let host = this.getHostName(window.location);
+      if (!this.hostVsStores.has(host)) {
+        this.populateStoresForHost(host, window);
+        let data = {};
+        data[host] = this.getNamesForHost(host);
+        this.storageActor.update("added", typeName, data);
+      }
+    },
+
+    /**
+     * When a window is removed from the page. This generally means that an
+     * iframe was removed, or the current window reload is triggered.
+     *
+     * @param {window} window
+     *        The window which was removed.
+     */
+    onWindowDestroyed: function(window) {
+      let host = this.getHostName(window.location);
+      if (!this.hosts.has(host)) {
+        this.hostVsStores.delete(host);
+        let data = {};
+        data[host] = [];
+        this.storageActor.update("deleted", typeName, data);
+      }
+    },
+
+    form: function(form, detail) {
+      if (detail === "actorid") {
+        return this.actorID;
+      }
+
+      let hosts = {};
+      for (let host of this.hosts) {
+        hosts[host] = [];
+      }
+
+      return {
+        actor: this.actorID,
+        hosts: hosts
+      };
+    },
+
+    /**
+     * Populates a map of known hosts vs a map of stores vs value.
+     */
+    populateStoresForHosts: function() {
+      this.hostVsStores = new Map();
+      for (let host of this.hosts) {
+        this.populateStoresForHost(host);
+      }
+    },
+
+    /**
+     * Returns a list of requested store objects. Maximum values returned are
+     * MAX_STORE_OBJECT_COUNT. This method returns paginated values whose
+     * starting index and total size can be controlled via the options object
+     *
+     * @param {string} host
+     *        The host name for which the store values are required.
+     * @param {array:string} names
+     *        Array containing the names of required store objects. Empty if all
+     *        items are required.
+     * @param {object} options
+     *        Additional options for the request containing following properties:
+     *         - offset {number} : The begin index of the returned array amongst
+     *                  the total values
+     *         - size {number} : The number of values required.
+     *         - sortOn {string} : The values should be sorted on this property.
+     *
+     * @return {object} An object containing following properties:
+     *          - offset - The actual offset of the returned array. This might
+     *                     be different from the requested offset if that was
+     *                     invalid
+     *          - size - The actual size of the returned array.
+     *          - data - The requested values.
+     */
+    getStoreObjects: method(function(host, names, options = {}) {
+      let offset = options.offset || 0;
+      let size = options.offset || MAX_STORE_OBJECT_COUNT;
+      let sortOn = options.sortOn || "name";
+
+      let toReturn = {
+        offset: offset,
+        total: -1,
+        data: []
+      };
+
+      if (names) {
+        for (let name of names) {
+          toReturn.data.push(
+            this.toStoreObject(this.getValuesForHost(host, name))
+          );
+        }
+      }
+      else {
+        let total = this.getValuesForHost(host);
+        toReturn.total = total.length;
+        if (offset > toReturn.total) {
+          toReturn.offset = offset = 0;
+        }
+
+        toReturn.data = total.sort((a,b) => {
+          return a[sortOn] - b[sortOn];
+        }).slice(offset, size).map(object => this.toStoreObject(object));
+      }
+
+      return toReturn;
+    }, {
+      request: {
+        host: Arg(0),
+        names: Arg(1, "nullable:array:string"),
+        options: Arg(2, "nullable:json")
+      },
+      response: RetVal(storeObjectType)
+    })
+  }
+};
+
+/**
+ * Creates an actor and its corresponding front and registers it to the Storage
+ * Actor.
+ *
+ * @See StorageActors.defaults()
+ *
+ * @param {object} options
+ *        Options required by StorageActors.defaults method which are :
+ *         - 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
+ *        StorageActors.defaults method plus the required ones described there.
+ */
+StorageActors.createActor = function(options = {}, overrides = {}) {
+  let actorObject = StorageActors.defaults(
+    options.typeName,
+    options.observationTopic,
+    options.storeObjectType
+  );
+  for (let key in overrides) {
+    actorObject[key] = overrides[key];
+  }
+
+  let actor = protocol.ActorClass(actorObject);
+  let front = protocol.FrontClass(actor, {
+    form: function(form, detail) {
+      if (detail === "actorid") {
+        this.actorID = form;
+        return null;
+      }
+
+      this.actorID = form.actor;
+      this.hosts = form.hosts;
+      return null;
+    }
+  });
+  storageTypePool.set(actorObject.typeName, actor);
+}
+
+/**
+ * The Cookies actor and front.
+ */
+StorageActors.createActor({
+  typeName: "cookies",
+  observationTopic: "cookie-changed",
+  storeObjectType: "cookiestoreobject"
+}, {
+  initialize: function(storageActor) {
+    protocol.Actor.prototype.initialize.call(this, null);
+
+    this.storageActor = storageActor;
+
+    this.populateStoresForHosts();
+    Services.obs.addObserver(this, "cookie-changed", false);
+    Services.obs.addObserver(this, "http-on-response-set-cookie", false);
+    this.onWindowReady = this.onWindowReady.bind(this);
+    this.onWindowDestroyed = this.onWindowDestroyed.bind(this);
+    events.on(this.storageActor, "window-ready", this.onWindowReady);
+    events.on(this.storageActor, "window-destroyed", this.onWindowDestroyed);
+  },
+
+  destroy: function() {
+    this.hostVsStores = null;
+    Services.obs.removeObserver(this, "cookie-changed", false);
+    Services.obs.removeObserver(this, "http-on-response-set-cookie", false);
+    events.off(this.storageActor, "window-ready", this.onWindowReady);
+    events.off(this.storageActor, "window-destroyed", this.onWindowDestroyed);
+  },
+
+  getNamesForHost: function(host) {
+    return [...this.hostVsStores.get(host).keys()];
+  },
+
+  getValuesForHost: function(host, name) {
+    if (name) {
+      return this.hostVsStores.get(host).get(name);
+    }
+    return [...this.hostVsStores.get(host).values()];
+  },
+
+  /**
+   * Given a cookie object, figure out all the matching hosts from the page that
+   * the cookie belong to.
+   */
+  getMatchingHosts: function(cookies) {
+    if (!cookies.length) {
+      cookies = [cookies];
+    }
+    let hosts = new Set();
+    for (let host of this.hosts) {
+      for (let cookie of cookies) {
+        if (this.isCookieAtHost(cookie, host)) {
+          hosts.add(host);
+        }
+      }
+    }
+    return [...hosts];
+  },
+
+  /**
+   * Given a cookie object and a host, figure out if the cookie is valid for
+   * that host.
+   */
+  isCookieAtHost: function(cookie, host) {
+    try {
+      cookie = cookie.QueryInterface(Ci.nsICookie)
+                     .QueryInterface(Ci.nsICookie2);
+    } catch(ex) {
+      return false;
+    }
+    if (cookie.host == null) {
+      return host == null;
+    }
+    if (cookie.host.startsWith(".")) {
+      return host.endsWith(cookie.host);
+    }
+    else {
+      return cookie.host == host;
+    }
+  },
+
+  toStoreObject: function(cookie) {
+    if (!cookie) {
+      return null;
+    }
+
+    return {
+      name: cookie.name,
+      path: cookie.path || "",
+      host: cookie.host || "",
+      expires: (cookie.expires || 0) * 1000, // because expires is in seconds
+      creationTime: cookie.creationTime / 1000, // because it is in micro seconds
+      lastAccessed: cookie.lastAccessed / 1000, // - do -
+      value: new LongStringActor(this.conn, cookie.value || ""),
+      isDomain: cookie.isDomain,
+      isSecure: cookie.isSecure,
+      isHttpOnly: cookie.isHttpOnly
+    }
+  },
+
+  populateStoresForHost: function(host) {
+    this.hostVsStores.set(host, new Map());
+    let cookies = Services.cookies.getCookiesFromHost(host);
+    while (cookies.hasMoreElements()) {
+      let cookie = cookies.getNext().QueryInterface(Ci.nsICookie)
+                          .QueryInterface(Ci.nsICookie2);
+      if (this.isCookieAtHost(cookie, host)) {
+        this.hostVsStores.get(host).set(cookie.name, cookie);
+      }
+    }
+  },
+
+  /**
+   * Converts the raw cookie string returned in http request's response header
+   * to a nsICookie compatible object.
+   *
+   * @param {string} cookieString
+   *        The raw cookie string coming from response header.
+   * @param {string} domain
+   *        The domain of the url of the nsiChannel the cookie corresponds to.
+   *        This will be used when the cookie string does not have a domain.
+   *
+   * @returns {[object]}
+   *          An array of nsICookie like objects representing the cookies.
+   */
+  parseCookieString: function(cookieString, domain) {
+    /**
+     * Takes a date string present in raw cookie string coming from http
+     * request's response headers and returns the number of milliseconds elapsed
+     * since epoch. If the date string is undefined, its probably a session
+     * cookie so return 0.
+     */
+    let parseDateString = dateString => {
+      return dateString ? new Date(dateString.replace(/-/g, " ")).getTime(): 0;
+    };
+
+    let cookies = [];
+    for (let string of cookieString.split("\n")) {
+      let keyVals = {}, name = null;
+      for (let keyVal of string.split(/;\s*/)) {
+        let tokens = keyVal.split(/\s*=\s*/);
+        if (!name) {
+          name = tokens[0];
+        }
+        else {
+          tokens[0] = tokens[0].toLowerCase();
+        }
+        keyVals[tokens.splice(0, 1)[0]] = tokens.join("=");
+      }
+      let expiresTime = parseDateString(keyVals.expires);
+      keyVals.domain = keyVals.domain || domain;
+      cookies.push({
+        name: name,
+        value: keyVals[name] || "",
+        path: keyVals.path,
+        host: keyVals.domain,
+        expires: expiresTime/1000, // seconds, to match with nsiCookie.expires
+        lastAccessed: expiresTime * 1000,
+        // microseconds, to match with nsiCookie.lastAccessed
+        creationTime: expiresTime * 1000,
+        // microseconds, to match with nsiCookie.creationTime
+        isHttpOnly: true,
+        isSecure: keyVals.secure != null,
+        isDomain: keyVals.domain.startsWith("."),
+      });
+    }
+    return cookies;
+  },
+
+  /**
+   * Notification observer for topics "http-on-response-set-cookie" and
+   * "cookie-change".
+   *
+   * @param subject
+   *        {nsiChannel} The channel associated to the SET-COOKIE response
+   *        header in case of "http-on-response-set-cookie" topic.
+   *        {nsiCookie|[nsiCookie]} A single nsiCookie object or a list of it
+   *        depending on the action. Array is only in case of "batch-deleted"
+   *        action.
+   * @param {string} topic
+   *        The topic of the notification.
+   * @param {string} action
+   *        Additional data associated with the notification. Its the type of
+   *        cookie change in case of "cookie-change" topic and the cookie string
+   *        in case of "http-on-response-set-cookie" topic.
+   */
+  observe: function(subject, topic, action) {
+    if (topic == "http-on-response-set-cookie") {
+      // Some cookies got created as a result of http response header SET-COOKIE
+      // subject here is an nsIChannel object referring to the http request.
+      // We get the requestor of this channel and thus the content window.
+      let channel = subject.QueryInterface(Ci.nsIChannel);
+      let requestor = channel.notificationCallbacks ||
+                      channel.loadGroup.notificationCallbacks;
+      // requester can be null sometimes.
+      let window = requestor ? requestor.getInterface(Ci.nsIDOMWindow): null;
+      // Proceed only if this window is present on the currently targetted tab
+      if (window && this.storageActor.isIncludedInTopLevelWindow(window)) {
+        let host = this.getHostName(window.location);
+        if (this.hostVsStores.has(host)) {
+          let cookies = this.parseCookieString(action, channel.URI.host);
+          let data = {};
+          data[host] =  [];
+          for (let cookie of cookies) {
+            if (this.hostVsStores.get(host).has(cookie.name)) {
+              continue;
+            }
+            this.hostVsStores.get(host).set(cookie.name, cookie);
+            data[host].push(cookie.name);
+          }
+          if (data[host]) {
+            this.storageActor.update("added", "cookies", data);
+          }
+        }
+      }
+      return null;
+    }
+
+    if (topic != "cookie-changed") {
+      return null;
+    }
+
+    let hosts = this.getMatchingHosts(subject);
+    let data = {};
+
+    switch(action) {
+      case "added":
+      case "changed":
+        if (hosts.length) {
+          subject = subject.QueryInterface(Ci.nsICookie)
+                           .QueryInterface(Ci.nsICookie2);
+          for (let host of hosts) {
+            this.hostVsStores.get(host).set(subject.name, subject);
+            data[host] = [subject.name];
+          }
+          this.storageActor.update(action, "cookies", data);
+        }
+        break;
+
+      case "deleted":
+        if (hosts.length) {
+          subject = subject.QueryInterface(Ci.nsICookie)
+                           .QueryInterface(Ci.nsICookie2);
+          for (let host of hosts) {
+            this.hostVsStores.get(host).delete(subject.name);
+            data[host] = [subject.name];
+          }
+          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) {
+              cookie = cookie.QueryInterface(Ci.nsICookie)
+                             .QueryInterface(Ci.nsICookie2);
+              this.hostVsStores.get(host).delete(cookie.name);
+              stores.push(cookie.name);
+            }
+            data[host] = stores;
+          }
+          this.storageActor.update("deleted", "cookies", data);
+        }
+        break;
+
+      case "cleared":
+        this.storageActor.update("cleared", "cookies", hosts);
+        break;
+
+      case "reload":
+        this.storageActor.update("reloaded", "cookies", hosts);
+        break;
+    }
+    return null;
+  },
+});
+
+
+/**
+ * Helper method to create the overriden object required in
+ * StorageActors.createActor for Local Storage and Session Storage.
+ * This method exists as both Local Storage and Session Storage have almost
+ * identical actors.
+ */
+function getObjectForLocalOrSessionStorage(type) {
+  return {
+    getNamesForHost: function(host) {
+      let storage = this.hostVsStores.get(host);
+      return [key for (key in storage)];
+    },
+
+    getValuesForHost: function(host, name) {
+      let storage = this.hostVsStores.get(host);
+      if (name) {
+        return {name: name, value: storage.getItem(name)};
+      }
+      return [{name: name, value: storage.getItem(name)} for (name in storage)];
+    },
+
+    getHostName: function(location) {
+      if (!location.host) {
+        return location.href;
+      }
+      return location.protocol + "//" + location.host;
+    },
+
+    populateStoresForHost: function(host, window) {
+      try {
+        this.hostVsStores.set(host, window[type]);
+      } catch(ex) {
+        // Exceptions happen when local or session storage is inaccessible
+      }
+      return null;
+    },
+
+    populateStoresForHosts: function() {
+      this.hostVsStores = new Map();
+      try {
+        for (let window of this.windows) {
+          this.hostVsStores.set(this.getHostName(window.location), window[type]);
+        }
+      } catch(ex) {
+        // Exceptions happen when local or session storage is inaccessible
+      }
+      return null;
+    },
+
+    observe: function(subject, topic, data) {
+      if (topic != "dom-storage2-changed" || data != type) {
+        return null;
+      }
+
+      let host = this.getSchemaAndHost(subject.url);
+
+      if (!this.hostVsStores.has(host)) {
+        return null;
+      }
+
+      let action = "changed";
+      if (subject.key == null) {
+        return this.storageActor.update("cleared", type, [host]);
+      }
+      else if (subject.oldValue == null) {
+        action = "added";
+      }
+      else if (subject.newValue == null) {
+        action = "deleted";
+      }
+      let updateData = {};
+      updateData[host] = [subject.key];
+      return this.storageActor.update(action, type, updateData);
+    },
+
+    /**
+     * Given a url, correctly determine its protocol + hostname part.
+     */
+    getSchemaAndHost: function(url) {
+      let uri = Services.io.newURI(url, null, null);
+      return uri.scheme + "://" + uri.hostPort;
+    },
+
+    toStoreObject: function(item) {
+      if (!item) {
+        return null;
+      }
+
+      return {
+        name: item.name,
+        value: new LongStringActor(this.conn, item.value || "")
+      };
+    },
+  }
+};
+
+/**
+ * The Local Storage actor and front.
+ */
+StorageActors.createActor({
+  typeName: "localStorage",
+  observationTopic: "dom-storage2-changed",
+  storeObjectType: "storagestoreobject"
+}, getObjectForLocalOrSessionStorage("localStorage"));
+
+/**
+ * The Session Storage actor and front.
+ */
+StorageActors.createActor({
+  typeName: "sessionStorage",
+  observationTopic: "dom-storage2-changed",
+  storeObjectType: "storagestoreobject"
+}, getObjectForLocalOrSessionStorage("sessionStorage"));
+
+
+/**
+ * The main Storage Actor.
+ */
+let StorageActor = exports.StorageActor = protocol.ActorClass({
+  typeName: "storage",
+
+  get window() {
+    return this.parentActor.window;
+  },
+
+  get document() {
+    return this.parentActor.window.document;
+  },
+
+  get windows() {
+    return this.childWindowPool;
+  },
+
+  /**
+   * List of event notifications that the server can send to the client.
+   *
+   *  - stores-update : When any store object in any storage type changes.
+   *  - stores-cleared : When all the store objects are removed.
+   *  - stores-reloaded : When all stores are reloaded. This generally mean that
+   *                      we should refetch everything again.
+   */
+  events: {
+    "stores-update": {
+      type: "storesUpdate",
+      data: Arg(0, "storeUpdateObject")
+    },
+    "stores-cleared": {
+      type: "storesCleared",
+      data: Arg(0, "json")
+    },
+    "stores-reloaded": {
+      type: "storesRelaoded",
+      data: Arg(0, "json")
+    }
+  },
+
+  initialize: function (conn, tabActor) {
+    protocol.Actor.prototype.initialize.call(this, null);
+
+    this.conn = conn;
+    this.parentActor = tabActor;
+
+    this.childActorPool = new Map();
+    this.childWindowPool = new Set();
+
+    // Get the top level content docshell for the tab we are targetting.
+    this.topDocshell = tabActor.window
+      .QueryInterface(Ci.nsIInterfaceRequestor)
+      .getInterface(Ci.nsIWebNavigation)
+      .QueryInterface(Ci.nsIDocShell)
+      .QueryInterface(Ci.nsIDocShellTreeItem);
+    // Fetch all the inner iframe windows in this tab.
+    this.fetchChildWindows(this.topDocshell);
+
+    // Initialize the registered store types
+    for (let [store, actor] of storageTypePool) {
+      this.childActorPool.set(store, new actor(this));
+    }
+
+    // Notifications that help us keep track of newly added windows and windows
+    // that got removed
+    Services.obs.addObserver(this, "content-document-global-created", false);
+    Services.obs.addObserver(this, "inner-window-destroyed", false);
+    this.onPageChange = this.onPageChange.bind(this);
+    tabActor.browser.addEventListener("pageshow", this.onPageChange, true);
+    tabActor.browser.addEventListener("pagehide", this.onPageChange, true);
+
+    this.boundUpdate = {};
+    // The time which periodically flushes and transfers the updated store
+    // objects.
+    this.updateTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+    this.updateTimer.initWithCallback(this , UPDATE_INTERVAL,
+      Ci.nsITimer.TYPE_REPEATING_PRECISE_CAN_SKIP);
+
+    // Layout helper for window.parent and window.top helper methods that work
+    // accross devices.
+    this.layoutHelper = new LayoutHelpers(this.window);
+  },
+
+  destroy: function() {
+    this.updateTimer.cancel();
+    this.updateTimer = null;
+    // Remove observers
+    Services.obs.removeObserver(this, "content-document-global-created", false);
+    Services.obs.removeObserver(this, "inner-window-destroyed", false);
+    if (this.parentActor.browser) {
+      this.parentActor.browser.removeEventListener(
+        "pageshow", this.onPageChange, true);
+      this.parentActor.browser.removeEventListener(
+        "pagehide", this.onPageChange, true);
+    }
+    // Destroy the registered store types
+    for (let actor of this.childActorPool.values()) {
+      actor.destroy();
+    }
+    this.childActorPool.clear();
+    this.topDocshell = null;
+  },
+
+  /**
+   * Given a docshell, recursively find otu all the child windows from it.
+   *
+   * @param {nsIDocShell} item
+   *        The docshell from which all inner windows need to be extracted.
+   */
+  fetchChildWindows: function(item) {
+    let docShell = item.QueryInterface(Ci.nsIDocShell)
+                       .QueryInterface(Ci.nsIDocShellTreeItem);
+    if (!docShell.contentViewer) {
+      return null;
+    }
+    let window = docShell.contentViewer.DOMDocument.defaultView;
+    if (window.location.href == "about:blank") {
+      // Skip out about:blank windows as Gecko creates them multiple times while
+      // creating any global.
+      return null;
+    }
+    this.childWindowPool.add(window);
+    for (let i = 0; i < docShell.childCount; i++) {
+      let child = docShell.getChildAt(i);
+      this.fetchChildWindows(child);
+    }
+    return null;
+  },
+
+  isIncludedInTopLevelWindow: function(window) {
+    return this.layoutHelper.isIncludedInTopLevelWindow(window);
+  },
+
+  getWindowFromInnerWindowID: function(innerID) {
+    innerID = innerID.QueryInterface(Ci.nsISupportsPRUint64).data;
+    for (let win of this.childWindowPool.values()) {
+      let id = win.QueryInterface(Ci.nsIInterfaceRequestor)
+                   .getInterface(Ci.nsIDOMWindowUtils)
+                   .currentInnerWindowID;
+      if (id == innerID) {
+        return win;
+      }
+    }
+    return null;
+  },
+
+  /**
+   * Event handler for any docshell update. This lets us figure out whenever
+   * any new window is added, or an existing window is removed.
+   */
+  observe: function(subject, topic, data) {
+    if (subject.location &&
+        (!subject.location.href || subject.location.href == "about:blank")) {
+      return null;
+    }
+    if (topic == "content-document-global-created" &&
+        this.isIncludedInTopLevelWindow(subject)) {
+      this.childWindowPool.add(subject);
+      events.emit(this, "window-ready", subject);
+    }
+    else if (topic == "inner-window-destroyed") {
+      let window = this.getWindowFromInnerWindowID(subject);
+      if (window) {
+        this.childActorPool.delete(window);
+        events.emit(this, "window-destroyed", window);
+      }
+    }
+    return null;
+  },
+
+  onPageChange: function({target, type}) {
+    let window = target.defaultView;
+    if (type == "pagehide" && this.childWindowPool.delete(window)) {
+      events.emit(this, "window-destroyed", window)
+    }
+    else if (type == "pageshow" && window.location.href &&
+             window.location.href != "about:blank" &&
+             this.isIncludedInTopLevelWindow(window)) {
+      this.childWindowPool.add(window);
+      events.emit(this, "window-ready", window);
+    }
+  },
+
+  /**
+   * Lists the availabel hosts for all the registered storage types.
+   *
+   * @returns {object} An object containing with the following structure:
+   *  - <storageType> : [{
+   *      actor: <actorId>,
+   *      host: <hostname>
+   *    }]
+   */
+  listStores: method(function() {
+    // Explictly recalculate the window-tree
+    this.childWindowPool.clear();
+    this.fetchChildWindows(this.topDocshell);
+
+    let toReturn = {};
+    for (let [name, value] of this.childActorPool) {
+      toReturn[name] = value;
+    }
+    return toReturn;
+  }, {
+    response: RetVal(types.addDictType("storelist", getRegisteredTypes()))
+  }),
+
+  /**
+   * Notifies the client front with the updates in stores at regular intervals.
+   */
+  notify: function() {
+    if (!this.updatePending || this.updatingUpdateObject) {
+      return null;
+    }
+    events.emit(this, "stores-update", this.boundUpdate);
+    this.boundUpdate = {};
+    this.updatePending = false;
+    return null;
+  },
+
+  /**
+   * This method is called by the registered storage types so as to tell the
+   * Storage Actor that there are some changes in the stores. Storage Actor then
+   * notifies the client front about these changes at regular (UPDATE_INTERVAL)
+   * interval.
+   *
+   * @param {string} action
+   *        The type of change. One of "added", "changed" or "deleted"
+   * @param {string} storeType
+   *        The storage actor in which this change has occurred.
+   * @param {object} data
+   *        The update object. This object is of the following format:
+   *         - {
+   *             <host1>: [<store_names1>, <store_name2>...],
+   *             <host2>: [<store_names34>...],
+   *           }
+   *           Where host1, host2 are the host in which this change happened and
+   *           [<store_namesX] is an array of the names of the changed store
+   *           objects. Leave it empty if the host was completely removed.
+   *        When the action is "reloaded" or "cleared", `data` is an array of
+   *        hosts for which the stores were cleared or reloaded.
+   */
+  update: function(action, storeType, data) {
+    if (action == "cleared" || action == "reloaded") {
+      let toSend = {};
+      toSend[storeType] = data
+      events.emit(this, "stores-" + action, toSend);
+      return null;
+    }
+
+    this.updatingUpdateObject = true;
+    if (!this.boundUpdate[action]) {
+      this.boundUpdate[action] = {};
+    }
+    if (!this.boundUpdate[action][storeType]) {
+      this.boundUpdate[action][storeType] = {};
+    }
+    this.updatePending = true;
+    for (let host in data) {
+      if (!this.boundUpdate[action][storeType][host] || action == "deleted") {
+        this.boundUpdate[action][storeType][host] = data[host];
+      }
+      else {
+        this.boundUpdate[action][storeType][host] =
+        this.boundUpdate[action][storeType][host].concat(data[host]);
+      }
+    }
+    if (action == "added") {
+      // If the same store name was previously deleted or changed, but now is
+      // added somehow, dont send the deleted or changed update.
+      this.removeNamesFromUpdateList("deleted", storeType, data);
+      this.removeNamesFromUpdateList("changed", storeType, data);
+    }
+    else if (action == "changed" && this.boundUpdate.added &&
+             this.boundUpdate.added[storeType]) {
+      // If something got added and changed at the same time, then remove those
+      // 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] &&
+            this.boundUpdate.changed[storeType][host]) {
+          delete this.boundUpdate.changed[storeType][host];
+        }
+      }
+    }
+    this.updatingUpdateObject = false;
+    return null;
+  },
+
+  /**
+   * This method removes data from the this.boundUpdate object in the same
+   * manner like this.update() adds data to it.
+   *
+   * @param {string} action
+   *        The type of change. One of "added", "changed" or "deleted"
+   * @param {string} storeType
+   *        The storage actor for which you want to remove the updates data.
+   * @param {object} data
+   *        The update object. This object is of the following format:
+   *         - {
+   *             <host1>: [<store_names1>, <store_name2>...],
+   *             <host2>: [<store_names34>...],
+   *           }
+   *           Where host1, host2 are the hosts which you want to remove and
+   *           [<store_namesX] is an array of the names of the store objects.
+   */
+  removeNamesFromUpdateList: function(action, storeType, data) {
+    for (let host in data) {
+      if (this.boundUpdate[action] && this.boundUpdate[action][storeType] &&
+          this.boundUpdate[action][storeType][host]) {
+        for (let name in data[host]) {
+          let index = this.boundUpdate[action][storeType][host].indexOf(name);
+          if (index > -1) {
+            this.boundUpdate[action][storeType][host].splice(index, 1);
+          }
+        }
+        if (!this.boundUpdate[action][storeType][host].length) {
+          delete this.boundUpdate[action][storeType][host];
+        }
+      }
+    }
+    return null;
+  }
+});
+
+/**
+ * Front for the Storage Actor.
+ */
+let StorageFront = exports.StorageFront = protocol.FrontClass(StorageActor, {
+  initialize: function(client, tabForm) {
+    protocol.Front.prototype.initialize.call(this, client);
+    this.actorID = tabForm.storageActor;
+
+    client.addActorPool(this);
+    this.manage(this);
+  }
+});
--- a/toolkit/devtools/server/main.js
+++ b/toolkit/devtools/server/main.js
@@ -386,16 +386,17 @@ var DebuggerServer = {
    */
   addTabActors: function() {
     this.addActors("resource://gre/modules/devtools/server/actors/script.js");
     this.addActors("resource://gre/modules/devtools/server/actors/webconsole.js");
     this.registerModule("devtools/server/actors/inspector");
     this.registerModule("devtools/server/actors/webgl");
     this.registerModule("devtools/server/actors/stylesheets");
     this.registerModule("devtools/server/actors/styleeditor");
+    this.registerModule("devtools/server/actors/storage");
     this.registerModule("devtools/server/actors/gcli");
     this.registerModule("devtools/server/actors/tracer");
     this.registerModule("devtools/server/actors/memory");
     this.registerModule("devtools/server/actors/eventlooplag");
     if ("nsIProfiler" in Ci)
       this.addActors("resource://gre/modules/devtools/server/actors/profiler.js");
   },
 
--- a/toolkit/devtools/server/moz.build
+++ b/toolkit/devtools/server/moz.build
@@ -1,14 +1,15 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
+BROWSER_CHROME_MANIFESTS += ['tests/browser/browser.ini']
 MOCHITEST_CHROME_MANIFESTS += ['tests/mochitest/chrome.ini']
 XPCSHELL_TESTS_MANIFESTS += ['tests/unit/xpcshell.ini']
 
 XPIDL_SOURCES += [
     'nsIJSInspector.idl',
 ]
 
 XPIDL_MODULE = 'jsinspector'
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/browser/browser.ini
@@ -0,0 +1,12 @@
+[DEFAULT]
+support-files =
+  head.js
+  storage-dynamic-windows.html
+  storage-listings.html
+  storage-unsecured-iframe.html
+  storage-updates.html
+  storage-secured-iframe.html
+
+[browser_storage_dynamic_windows.js]
+[browser_storage_listings.js]
+[browser_storage_updates.js]
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/browser/browser_storage_dynamic_windows.js
@@ -0,0 +1,289 @@
+const Cu = Components.utils;
+Cu.import("resource://gre/modules/Services.jsm");
+let tempScope = {};
+Cu.import("resource://gre/modules/devtools/dbg-client.jsm", tempScope);
+Cu.import("resource://gre/modules/devtools/dbg-server.jsm", tempScope);
+Cu.import("resource://gre/modules/Promise.jsm", tempScope);
+let {DebuggerServer, DebuggerClient, Promise} = tempScope;
+tempScope = null;
+
+const {StorageFront} = require("devtools/server/actors/storage");
+let gFront;
+
+const beforeReload = {
+  cookies: {
+    "test1.example.org": ["c1", "cs2", "c3", "uc1"],
+    "sectest1.example.org": ["uc1", "cs2"]
+  },
+  localStorage: {
+    "http://test1.example.org": ["ls1", "ls2"],
+    "http://sectest1.example.org": ["iframe-u-ls1"]
+  },
+  sessionStorage: {
+    "http://test1.example.org": ["ss1"],
+    "http://sectest1.example.org": ["iframe-u-ss1", "iframe-u-ss2"]
+  }
+};
+
+// Always log packets when running tests.
+Services.prefs.setBoolPref("devtools.debugger.log", true);
+registerCleanupFunction(function() {
+  Services.prefs.clearUserPref("devtools.debugger.log");
+});
+
+function finishTests(client) {
+  client.close(() => {
+    DebuggerServer.destroy();
+    DebuggerClient = DebuggerServer = gFront = null;
+    finish();
+  });
+}
+
+function testStores(data, client) {
+  testWindowsBeforeReload(data);
+  testReload().then(() =>
+  testAddIframe()).then(() =>
+  testRemoveIframe()).then(() =>
+  finishTests(client));
+}
+
+function testWindowsBeforeReload(data) {
+  for (let storageType in beforeReload) {
+    ok(data[storageType], storageType + " storage actor is present");
+    is(Object.keys(data[storageType].hosts).length,
+       Object.keys(beforeReload[storageType]).length,
+       "Number of hosts for " + storageType + "match");
+    for (let host in beforeReload[storageType]) {
+      ok(data[storageType].hosts[host], "Host " + host + " is present");
+    }
+  }
+}
+
+function markOutMatched(toBeEmptied, data, deleted) {
+  if (!Object.keys(toBeEmptied).length) {
+    info("Object empty")
+    return;
+  }
+  ok(Object.keys(data).length,
+     "Atleast some storage types should be present in deleted");
+  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);
+          }
+        }
+        if (!toBeEmptied[storageType][host].length) {
+          delete toBeEmptied[storageType][host];
+        }
+      }
+      else {
+        delete toBeEmptied[storageType][host];
+      }
+    }
+    if (!Object.keys(toBeEmptied[storageType]).length) {
+      delete toBeEmptied[storageType];
+    }
+  }
+}
+
+function testReload() {
+  info("Testing if reload works properly");
+
+  let shouldBeEmptyFirst = Cu.cloneInto(beforeReload,  {});
+  let shouldBeEmptyLast = Cu.cloneInto(beforeReload,  {});
+  let reloaded = Promise.defer();
+
+  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
+    if (data.deleted) {
+      markOutMatched(shouldBeEmptyFirst, data.deleted, true);
+    }
+
+    if (!Object.keys(shouldBeEmptyFirst).length) {
+      info("shouldBeEmptyFirst is empty now");
+    }
+    else if (!data.added || !Object.keys(data.added).length) {
+      ok(false, "deleted object should have something if an added object " +
+         "does not have anything");
+      endTestReloaded();
+    }
+
+    // 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 = () => {
+    gFront.off("stores-update", onStoresUpdate);
+    reloaded.resolve();
+  };
+
+  gFront.on("stores-update", onStoresUpdate);
+
+  content.location.reload();
+  return reloaded.promise;
+}
+
+function testAddIframe() {
+  info("Testing if new iframe addition works properly");
+  let reloaded = Promise.defer();
+
+  let shouldBeEmpty = {
+    localStorage: {
+      "https://sectest1.example.org": ["iframe-s-ls1"]
+    },
+    sessionStorage: {
+      "https://sectest1.example.org": ["iframe-s-ss1"]
+    },
+    cookies: {
+      "sectest1.example.org": ["sc1"]
+    }
+  };
+
+  let onStoresUpdate = data => {
+    info("checking if the hosts list is correct for this iframe addition");
+
+    markOutMatched(shouldBeEmpty, data.added);
+
+    ok(!data.changed || !data.changed.cookies ||
+       !data.changed.cookies["https://sectest1.example.org"],
+       "Nothing got changed for cookies");
+    ok(!data.changed || !data.changed.localStorage ||
+       !data.changed.localStorage["https://sectest1.example.org"],
+       "Nothing got changed for local storage");
+    ok(!data.changed || !data.changed.sessionStorage ||
+       !data.changed.sessionStorage["https://sectest1.example.org"],
+       "Nothing got changed for session storage");
+
+    ok(!data.deleted || !data.deleted.cookies ||
+       !data.deleted.cookies["https://sectest1.example.org"],
+       "Nothing got deleted for cookies");
+    ok(!data.deleted || !data.deleted.localStorage ||
+       !data.deleted.localStorage["https://sectest1.example.org"],
+       "Nothing got deleted for local storage");
+    ok(!data.deleted || !data.deleted.sessionStorage ||
+       !data.deleted.sessionStorage["https://sectest1.example.org"],
+       "Nothing got deleted for session storage");
+
+    if (!Object.keys(shouldBeEmpty).length) {
+      info("Everything to be received is received.");
+      endTestReloaded();
+    }
+  };
+
+  let endTestReloaded = () => {
+    gFront.off("stores-update", onStoresUpdate);
+    reloaded.resolve();
+  };
+
+  gFront.on("stores-update", onStoresUpdate);
+
+  let iframe = content.document.createElement("iframe");
+  iframe.src = ALT_DOMAIN_SECURED + "storage-secured-iframe.html";
+  content.document.querySelector("body").appendChild(iframe);
+  return reloaded.promise;
+}
+
+function testRemoveIframe() {
+  info("Testing if iframe removal works properly");
+  let reloaded = Promise.defer();
+
+  let shouldBeEmpty = {
+    localStorage: {
+      "http://sectest1.example.org": []
+    },
+    sessionStorage: {
+      "http://sectest1.example.org": []
+    }
+  };
+
+  let onStoresUpdate = data => {
+    info("checking if the hosts list is correct for this iframe deletion");
+
+    markOutMatched(shouldBeEmpty, data.deleted, true);
+
+    ok(!data.deleted.cookies || !data.deleted.cookies["sectest1.example.org"],
+       "Nothing got deleted for Cookies as the same hostname is still present");
+
+    ok(!data.changed || !data.changed.cookies ||
+       !data.changed.cookies["http://sectest1.example.org"],
+       "Nothing got changed for cookies");
+    ok(!data.changed || !data.changed.localStorage ||
+       !data.changed.localStorage["http://sectest1.example.org"],
+       "Nothing got changed for local storage");
+    ok(!data.changed || !data.changed.sessionStorage ||
+       !data.changed.sessionStorage["http://sectest1.example.org"],
+       "Nothing got changed for session storage");
+
+    ok(!data.added || !data.added.cookies ||
+       !data.added.cookies["http://sectest1.example.org"],
+       "Nothing got added for cookies");
+    ok(!data.added || !data.added.localStorage ||
+       !data.added.localStorage["http://sectest1.example.org"],
+       "Nothing got added for local storage");
+    ok(!data.added || !data.added.sessionStorage ||
+       !data.added.sessionStorage["http://sectest1.example.org"],
+       "Nothing got added for session storage");
+
+    if (!Object.keys(shouldBeEmpty).length) {
+      info("Everything to be received is received.");
+      endTestReloaded();
+    }
+  };
+
+  let endTestReloaded = () => {
+    gFront.off("stores-update", onStoresUpdate);
+    reloaded.resolve();
+  };
+
+  gFront.on("stores-update", onStoresUpdate);
+
+  for (let iframe of content.document.querySelectorAll("iframe")) {
+    if (iframe.src.startsWith("http:")) {
+      iframe.remove();
+      break;
+    }
+  }
+  return reloaded.promise;
+}
+
+function test() {
+  waitForExplicitFinish();
+  addTab(MAIN_DOMAIN + "storage-dynamic-windows.html", function() {
+    try {
+      // Sometimes debugger server does not get destroyed correctly by previous
+      // tests.
+      DebuggerServer.destroy();
+    } catch (ex) { }
+    DebuggerServer.init(function () { return true; });
+    DebuggerServer.addBrowserActors();
+
+    let client = new DebuggerClient(DebuggerServer.connectPipe());
+    client.connect(function onConnect() {
+      client.listTabs(function onListTabs(aResponse) {
+        let form = aResponse.tabs[aResponse.selected];
+        gFront = StorageFront(client, form);
+
+        gFront.listStores().then(data => testStores(data, client));
+      });
+    });
+  })
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/browser/browser_storage_listings.js
@@ -0,0 +1,291 @@
+const Cu = Components.utils;
+Cu.import("resource://gre/modules/Services.jsm");
+let tempScope = {};
+Cu.import("resource://gre/modules/devtools/dbg-client.jsm", tempScope);
+Cu.import("resource://gre/modules/devtools/dbg-server.jsm", tempScope);
+let {DebuggerServer, DebuggerClient} = tempScope;
+tempScope = null;
+
+const {StorageFront} = require("devtools/server/actors/storage");
+
+// Always log packets when running tests.
+Services.prefs.setBoolPref("devtools.debugger.log", true);
+registerCleanupFunction(function() {
+  Services.prefs.clearUserPref("devtools.debugger.log");
+});
+
+const storeMap = {
+  cookies: {
+    "test1.example.org": [
+      {
+        name: "c1",
+        value: "foobar",
+        expires: 2000000000000,
+        path: "/browser",
+        host: "test1.example.org",
+        isDomain: false,
+        isSecure: false,
+      },
+      {
+        name: "cs2",
+        value: "sessionCookie",
+        path: "/",
+        host: ".example.org",
+        expires: 0,
+        isDomain: true,
+        isSecure: false,
+      },
+      {
+        name: "c3",
+        value: "foobar-2",
+        expires: 2000000001000,
+        path: "/",
+        host: "test1.example.org",
+        isDomain: false,
+        isSecure: true,
+      },
+      {
+        name: "uc1",
+        value: "foobar",
+        host: ".example.org",
+        path: "/",
+        expires: 0,
+        isDomain: true,
+        isSecure: true,
+      }
+    ],
+    "sectest1.example.org": [
+      {
+        name: "uc1",
+        value: "foobar",
+        host: ".example.org",
+        path: "/",
+        expires: 0,
+        isDomain: true,
+        isSecure: true,
+      },
+      {
+        name: "cs2",
+        value: "sessionCookie",
+        path: "/",
+        host: ".example.org",
+        expires: 0,
+        isDomain: true,
+        isSecure: false,
+      },
+      {
+        name: "sc1",
+        value: "foobar",
+        path: "/browser/toolkit/devtools/server/tests/browser/",
+        host: "sectest1.example.org",
+        expires: 0,
+        isDomain: false,
+        isSecure: false,
+      }
+    ]
+  },
+  localStorage: {
+    "http://test1.example.org": [
+      {
+        name: "ls1",
+        value: "foobar"
+      },
+      {
+        name: "ls2",
+        value: "foobar-2"
+      }
+    ],
+    "http://sectest1.example.org": [
+      {
+        name: "iframe-u-ls1",
+        value: "foobar"
+      }
+    ],
+    "https://sectest1.example.org": [
+      {
+        name: "iframe-s-ls1",
+        value: "foobar"
+      }
+    ]
+  },
+  sessionStorage: {
+    "http://test1.example.org": [
+      {
+        name: "ss1",
+        value: "foobar-3"
+      }
+    ],
+    "http://sectest1.example.org": [
+      {
+        name: "iframe-u-ss1",
+        value: "foobar1"
+      },
+      {
+        name: "iframe-u-ss2",
+        value: "foobar2"
+      }
+    ],
+    "https://sectest1.example.org": [
+      {
+        name: "iframe-s-ss1",
+        value: "foobar-2"
+      }
+    ]
+  }
+};
+
+function finishTests(client) {
+  client.close(() => {
+    DebuggerServer.destroy();
+    DebuggerClient = DebuggerServer = null;
+    finish();
+  });
+}
+
+function testStores(data, client) {
+  ok(data.cookies, "Cookies storage actor is present");
+  ok(data.localStorage, "Local Storage storage actor is present");
+  ok(data.sessionStorage, "Session Storage storage actor is present");
+  testCookies(data.cookies).then(() =>
+  testLocalStorage(data.localStorage)).then(() =>
+  testSessionStorage(data.sessionStorage)).then(() =>
+  finishTests(client));
+}
+
+function testCookies(cookiesActor) {
+  is(Object.keys(cookiesActor.hosts).length, 2, "Correct number of host entries for cookies");
+  return testCookiesObjects(0, cookiesActor.hosts, cookiesActor);
+}
+
+function testCookiesObjects(index, hosts, cookiesActor) {
+  let host = Object.keys(hosts)[index];
+  let matchItems = data => {
+    is(data.total, storeMap.cookies[host].length,
+       "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) {
+          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;
+        }
+      }
+      if (!found) {
+        ok(false, "cookie " + item.name + " should not exist in response;");
+      }
+    }
+  };
+
+  ok(!!storeMap.cookies[host], "Host is present in the list : " + host);
+  if (index == Object.keys(hosts).length - 1) {
+    return cookiesActor.getStoreObjects(host).then(matchItems);
+  }
+  return cookiesActor.getStoreObjects(host).then(matchItems).then(() => {
+    return testCookiesObjects(++index, hosts, cookiesActor);
+  });
+}
+
+function testLocalStorage(localStorageActor) {
+  is(Object.keys(localStorageActor.hosts).length, 3,
+     "Correct number of host entries for local storage");
+  return testLocalStorageObjects(0, localStorageActor.hosts, localStorageActor);
+}
+
+function testLocalStorageObjects(index, hosts, localStorageActor) {
+  let host = Object.keys(hosts)[index];
+  let matchItems = data => {
+    is(data.total, storeMap.localStorage[host].length,
+       "Number of local storage items in host " + host + " matches");
+    for (let item of data.data) {
+      let found = false;
+      for (let toMatch of storeMap.localStorage[host]) {
+        if (item.name == toMatch.name) {
+          found = true;
+          ok(true, "Found local storage item " + item.name + " in response");
+          is(item.value.str, toMatch.value, "The value matches.");
+          break;
+        }
+      }
+      if (!found) {
+        ok(false, "local storage item " + item.name +
+                  " should not exist in response;");
+      }
+    }
+  };
+
+  ok(!!storeMap.localStorage[host], "Host is present in the list : " + host);
+  if (index == Object.keys(hosts).length - 1) {
+    return localStorageActor.getStoreObjects(host).then(matchItems);
+  }
+  return localStorageActor.getStoreObjects(host).then(matchItems).then(() => {
+    return testLocalStorageObjects(++index, hosts, localStorageActor);
+  });
+}
+
+function testSessionStorage(sessionStorageActor) {
+  is(Object.keys(sessionStorageActor.hosts).length, 3,
+     "Correct number of host entries for session storage");
+  return testSessionStorageObjects(0, sessionStorageActor.hosts,
+                                   sessionStorageActor);
+}
+
+function testSessionStorageObjects(index, hosts, sessionStorageActor) {
+  let host = Object.keys(hosts)[index];
+  let matchItems = data => {
+    is(data.total, storeMap.sessionStorage[host].length,
+       "Number of session storage items in host " + host + " matches");
+    for (let item of data.data) {
+      let found = false;
+      for (let toMatch of storeMap.sessionStorage[host]) {
+        if (item.name == toMatch.name) {
+          found = true;
+          ok(true, "Found session storage item " + item.name + " in response");
+          is(item.value.str, toMatch.value, "The value matches.");
+          break;
+        }
+      }
+      if (!found) {
+        ok(false, "session storage item " + item.name +
+                  " should not exist in response;");
+      }
+    }
+  };
+
+  ok(!!storeMap.sessionStorage[host], "Host is present in the list : " + host);
+  if (index == Object.keys(hosts).length - 1) {
+    return sessionStorageActor.getStoreObjects(host).then(matchItems);
+  }
+  return sessionStorageActor.getStoreObjects(host).then(matchItems).then(() => {
+    return testSessionStorageObjects(++index, hosts, sessionStorageActor);
+  });
+}
+
+function test() {
+  waitForExplicitFinish();
+  addTab(MAIN_DOMAIN + "storage-listings.html", function() {
+    try {
+      // Sometimes debugger server does not get destroyed correctly by previous
+      // tests.
+      DebuggerServer.destroy();
+    } catch (ex) { }
+    DebuggerServer.init(function () { return true; });
+    DebuggerServer.addBrowserActors();
+
+    let client = new DebuggerClient(DebuggerServer.connectPipe());
+    client.connect(function onConnect() {
+      client.listTabs(function onListTabs(aResponse) {
+        let form = aResponse.tabs[aResponse.selected];
+        let front = StorageFront(client, form);
+
+        front.listStores().then(data => testStores(data, client));
+      });
+    });
+  })
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/browser/browser_storage_updates.js
@@ -0,0 +1,258 @@
+const Cu = Components.utils;
+Cu.import("resource://gre/modules/Services.jsm");
+let tempScope = {};
+Cu.import("resource://gre/modules/devtools/dbg-client.jsm", tempScope);
+Cu.import("resource://gre/modules/devtools/dbg-server.jsm", tempScope);
+Cu.import("resource://gre/modules/Promise.jsm", tempScope);
+let {DebuggerServer, DebuggerClient, Promise} = tempScope;
+tempScope = null;
+
+const {StorageFront} = require("devtools/server/actors/storage");
+let gTests;
+let gExpected;
+let index = 0;
+
+const beforeReload = {
+  cookies: ["test1.example.org", "sectest1.example.org"],
+  localStorage: ["http://test1.example.org", "http://sectest1.example.org"],
+  sessionStorage: ["http://test1.example.org", "http://sectest1.example.org"],
+};
+
+// Always log packets when running tests.
+Services.prefs.setBoolPref("devtools.debugger.log", true);
+registerCleanupFunction(function() {
+  Services.prefs.clearUserPref("devtools.debugger.log");
+});
+
+function finishTests(client) {
+  client.close(() => {
+    DebuggerServer.destroy();
+    DebuggerClient = DebuggerServer = gTests = null;
+    finish();
+  });
+}
+
+function markOutMatched(toBeEmptied, data, deleted) {
+  if (!Object.keys(toBeEmptied).length) {
+    info("Object empty")
+    return;
+  }
+  ok(Object.keys(data).length,
+     "Atleast some storage types should be present in deleted");
+  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");
+
+      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);
+        }
+      }
+      if (!toBeEmptied[storageType][host].length) {
+        delete toBeEmptied[storageType][host];
+      }
+    }
+    if (!Object.keys(toBeEmptied[storageType]).length) {
+      delete toBeEmptied[storageType];
+    }
+  }
+}
+
+function onStoresCleared(data) {
+  if (data.sessionStorage || data.localStorage) {
+    let hosts = data.sessionStorage || data.localStorage;
+    info("Stores cleared required for session storage");
+    is(hosts.length, 1, "number of hosts is 1");
+    is(hosts[0], "http://test1.example.org",
+       "host matches for " + Object.keys(data)[0]);
+    gTests.next();
+  }
+  else {
+    ok(false, "Stores cleared should only be for local and sesion storage");
+  }
+
+}
+
+function onStoresUpdate({added, changed, deleted}) {
+  info("inside stores update for index " + index);
+
+  // Here, added, changed and deleted might be null even if they are required as
+  // per gExpected. This is fine as they might come in the next stores-update
+  // call or have already come in the previous one.
+  if (added) {
+    info("matching added object for index " + index);
+    markOutMatched(gExpected.added, added);
+  }
+  if (changed) {
+    info("matching changed object for index " + index);
+    markOutMatched(gExpected.changed, changed);
+  }
+  if (deleted) {
+    info("matching deleted object for index " + index);
+    markOutMatched(gExpected.deleted, deleted);
+  }
+
+  if ((!gExpected.added || !Object.keys(gExpected.added).length) &&
+      (!gExpected.changed || !Object.keys(gExpected.changed).length) &&
+      (!gExpected.deleted || !Object.keys(gExpected.deleted).length)) {
+    info("Everything expected has been received for index " + index);
+    index++;
+    gTests.next();
+  }
+  else {
+    info("Still some updates pending for index " + index);
+  }
+}
+
+function* UpdateTests(front, win, client) {
+  front.on("stores-update", onStoresUpdate);
+
+  // index 0
+  gExpected = {
+    added: {
+      cookies: {
+        "test1.example.org": ["c1", "c2"]
+      },
+      localStorage: {
+        "http://test1.example.org": ["l1"]
+      }
+    }
+  };
+  win.addCookie("c1", "foobar1");
+  win.addCookie("c2", "foobar2");
+  win.localStorage.setItem("l1", "foobar1");
+  yield undefined;
+
+  // index 1
+  gExpected = {
+    changed: {
+      cookies: {
+        "test1.example.org": ["c1"]
+      }
+    },
+    added: {
+      localStorage: {
+        "http://test1.example.org": ["l2"]
+      }
+    }
+  };
+  win.addCookie("c1", "new_foobar1");
+  win.localStorage.setItem("l2", "foobar2");
+  yield undefined;
+
+  // index 2
+  gExpected = {
+    deleted: {
+      cookies: {
+        "test1.example.org": ["c2"]
+      },
+      localStorage: {
+        "http://test1.example.org": ["l1"]
+      }
+    },
+    added: {
+      localStorage: {
+        "http://test1.example.org": ["l3"]
+      }
+    }
+  };
+  win.removeCookie("c2");
+  win.localStorage.removeItem("l1");
+  win.localStorage.setItem("l3", "foobar3");
+  yield undefined;
+
+  // index 3
+  gExpected = {
+    added: {
+      cookies: {
+        "test1.example.org": ["c3"]
+      },
+      sessionStorage: {
+        "http://test1.example.org": ["s1", "s2"]
+      }
+    },
+    changed: {
+      localStorage: {
+        "http://test1.example.org": ["l3"]
+      }
+    },
+    deleted: {
+      cookies: {
+        "test1.example.org": ["c1"]
+      },
+      localStorage: {
+        "http://test1.example.org": ["l2"]
+      }
+    }
+  };
+  win.removeCookie("c1");
+  win.addCookie("c3", "foobar3");
+  win.localStorage.removeItem("l2");
+  win.sessionStorage.setItem("s1", "foobar1");
+  win.sessionStorage.setItem("s2", "foobar2");
+  win.localStorage.setItem("l3", "new_foobar3");
+  yield undefined;
+
+  // index 4
+  gExpected = {
+    deleted: {
+      sessionStorage: {
+        "http://test1.example.org": ["s1"]
+      }
+    }
+  };
+  win.sessionStorage.removeItem("s1");
+  yield undefined;
+
+  // index 5
+  gExpected = {
+    deleted: {
+      cookies: {
+        "test1.example.org": ["c3"]
+      }
+    }
+  };
+  front.on("stores-cleared", onStoresCleared);
+  win.clear();
+  yield undefined;
+  // Another 2 more yield undefined s so as to wait for the "stores-cleared" to
+  // fire. One for Local Storage and other for Session Storage
+  yield undefined;
+  yield undefined;
+
+  front.off("stores-cleared", onStoresCleared);
+  front.off("stores-update", onStoresUpdate);
+  finishTests(client);
+}
+
+
+function test() {
+  waitForExplicitFinish();
+  addTab(MAIN_DOMAIN + "storage-updates.html", function(doc) {
+    try {
+      // Sometimes debugger server does not get destroyed correctly by previous
+      // tests.
+      DebuggerServer.destroy();
+    } catch (ex) { }
+    DebuggerServer.init(function () { return true; });
+    DebuggerServer.addBrowserActors();
+
+    let client = new DebuggerClient(DebuggerServer.connectPipe());
+    client.connect(function onConnect() {
+      client.listTabs(function onListTabs(aResponse) {
+        let form = aResponse.tabs[aResponse.selected];
+        let front = StorageFront(client, form);
+        gTests = UpdateTests(front, doc.defaultView.wrappedJSObject,
+                             client);
+        // Make an initial call to initialize the actor
+        front.listStores().then(() => gTests.next());
+      });
+    });
+  })
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/browser/head.js
@@ -0,0 +1,43 @@
+/* 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/. */
+let tempScope = {};
+Cu.import("resource://gre/modules/devtools/Loader.jsm", tempScope);
+Cu.import("resource://gre/modules/devtools/Console.jsm", tempScope);
+const require = tempScope.devtools.require;
+const console = tempScope.console;
+tempScope = null;
+const PATH = "browser/toolkit/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;
+
+/**
+ * Open a new tab at a URL and call a callback on load
+ */
+function addTab(aURL, aCallback)
+{
+  waitForExplicitFinish();
+
+  gBrowser.selectedTab = gBrowser.addTab();
+  content.location = aURL;
+
+  let tab = gBrowser.selectedTab;
+  let browser = gBrowser.getBrowserForTab(tab);
+
+  function onTabLoad(event) {
+    if (event.originalTarget.location.href != aURL) {
+      return;
+    }
+    browser.removeEventListener("load", onTabLoad, true);
+    aCallback(browser.contentDocument);
+  }
+
+  browser.addEventListener("load", onTabLoad, true);
+}
+
+registerCleanupFunction(function tearDown() {
+  while (gBrowser.tabs.length > 1) {
+    gBrowser.removeCurrentTab();
+  }
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/browser/storage-dynamic-windows.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 965872 - Storage inspector actor with cookies, local storage and session storage.
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Storage inspector test for listing hosts and storages</title>
+</head>
+<body>
+<iframe src="http://sectest1.example.org/browser/toolkit/devtools/server/tests/browser/storage-unsecured-iframe.html"></iframe>
+<script>
+
+var partialHostname = location.hostname.match(/^[^.]+(\..*)$/)[1];
+var cookieExpiresTime1 = 2000000000000;
+var cookieExpiresTime2 = 2000000001000;
+// Setting up some cookies to eat.
+document.cookie = "c1=foobar; expires=" +
+  new Date(cookieExpiresTime1).toGMTString() + "; path=/browser";
+document.cookie = "cs2=sessionCookie; path=/; domain=" + partialHostname;
+document.cookie = "c3=foobar-2; expires=" +
+  new Date(cookieExpiresTime2).toGMTString() + "; path=/";
+// ... and some local storage items ..
+localStorage.setItem("ls1", "foobar");
+localStorage.setItem("ls2", "foobar-2");
+// ... and finally some session storage items too
+sessionStorage.setItem("ss1", "foobar-3");
+console.log("added cookies and stuff from main page");
+
+window.onunload = function() {
+  document.cookie = "c1=; expires=Thu, 01 Jan 1970 00:00:00 GMT";
+  document.cookie = "c3=; expires=Thu, 01 Jan 1970 00:00:00 GMT";
+  localStorage.clear();
+  sessionStorage.clear();
+  console.log("removed cookies and stuff from main page");
+}
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/browser/storage-listings.html
@@ -0,0 +1,40 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 965872 - Storage inspector actor with cookies, local storage and session storage.
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Storage inspector test for listing hosts and storages</title>
+</head>
+<body>
+<iframe src="http://sectest1.example.org/browser/toolkit/devtools/server/tests/browser/storage-unsecured-iframe.html"></iframe>
+<iframe src="https://sectest1.example.org:443/browser/toolkit/devtools/server/tests/browser/storage-secured-iframe.html"></iframe>
+<script>
+
+var partialHostname = location.hostname.match(/^[^.]+(\..*)$/)[1];
+var cookieExpiresTime1 = 2000000000000;
+var cookieExpiresTime2 = 2000000001000;
+// Setting up some cookies to eat.
+document.cookie = "c1=foobar; expires=" +
+  new Date(cookieExpiresTime1).toGMTString() + "; path=/browser";
+document.cookie = "cs2=sessionCookie; path=/; domain=" + partialHostname;
+document.cookie = "c3=foobar-2; secure=true; expires=" +
+  new Date(cookieExpiresTime2).toGMTString() + "; path=/";
+// ... and some local storage items ..
+localStorage.setItem("ls1", "foobar");
+localStorage.setItem("ls2", "foobar-2");
+// ... and finally some session storage items too
+sessionStorage.setItem("ss1", "foobar-3");
+console.log("added cookies and stuff from main page");
+
+window.onunload = function() {
+  document.cookie = "c1=; expires=Thu, 01 Jan 1970 00:00:00 GMT";
+  document.cookie = "c3=; expires=Thu, 01 Jan 1970 00:00:00 GMT";
+  localStorage.clear();
+  sessionStorage.clear();
+  console.log("removed cookies and stuff from main page");
+}
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/browser/storage-secured-iframe.html
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Iframe for testing multiple host detetion in storage actor
+-->
+<head>
+  <meta charset="utf-8">
+</head>
+<body>
+<script>
+
+document.cookie = "sc1=foobar;";
+localStorage.setItem("iframe-s-ls1", "foobar");
+sessionStorage.setItem("iframe-s-ss1", "foobar-2");
+console.log("added cookies and stuff from secured iframe");
+
+window.onunload = function() {
+  localStorage.clear();
+  sessionStorage.clear();
+  console.log("removed cookies and stuff from secured iframe");
+}
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/browser/storage-unsecured-iframe.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Iframe for testing multiple host detetion in storage actor
+-->
+<head>
+  <meta charset="utf-8">
+</head>
+<body>
+<script>
+
+document.cookie = "uc1=foobar; domain=.example.org; path=/; secure=true";
+localStorage.setItem("iframe-u-ls1", "foobar");
+sessionStorage.setItem("iframe-u-ss1", "foobar1");
+sessionStorage.setItem("iframe-u-ss2", "foobar2");
+console.log("added cookies and stuff from unsecured iframe");
+
+window.onunload = function() {
+  localStorage.clear();
+  sessionStorage.clear();
+  console.log("removed cookies and stuff from unsecured iframe");
+}
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/browser/storage-updates.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 965872 - Storage inspector actor with cookies, local storage and session storage.
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Storage inspector blank html for tests</title>
+</head>
+<body>
+<script>
+
+window.addCookie = function(name, value, path, domain, expires, secure) {
+  var cookieString = name + "=" + value + ";";
+  if (path) {
+    cookieString += "path=" + path + ";";
+  }
+  if (domain) {
+    cookieString += "domain=" + domain + ";";
+  }
+  if (expires) {
+    cookieString += "expires=" + expires + ";";
+  }
+  if (secure) {
+    cookieString += "secure=true;";
+  }
+  document.cookie = cookieString;
+};
+
+window.removeCookie = function(name) {
+  document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT";
+}
+
+window.clear = function() {
+  var cookies = document.cookie;
+  for (var cookie of cookies.split(";")) {
+    removeCookie(cookie.split("=")[0]);
+  }
+  localStorage.clear();
+  sessionStorage.clear();
+}
+</script>
+</body>
+</html>