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 191351 0dd61eada6c925e48f8949c6858915ed7f8709d5
parent 191350 b5fec3059a699719ca69a37dfb9bbefb1841f7f6
child 191352 163c21c788e593ea10d194616b88f4992f8ff020
push id474
push userasasaki@mozilla.com
push dateMon, 02 Jun 2014 21:01:02 +0000
treeherdermozilla-release@967f4cf1b31c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjwalker
bugs965872
milestone30.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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>