Bug 644409 - Make scratchpads save their state across restarts
☠☠ backed out by 0d039fef4b8e ☠ ☠
authorHeather Arthur <fayearthur@gmail.com>
Wed, 02 Nov 2011 12:14:30 -0700
changeset 80948 3b63add3a404020f6f5fb051465d3e71a5b4c785
parent 80876 185e913a7061366831528d6160466385cb552c1c
child 80949 0d039fef4b8e65c275dc9b4abbf856ad223fb584
push idunknown
push userunknown
push dateunknown
bugs644409
milestone10.0a1
Bug 644409 - Make scratchpads save their state across restarts
browser/base/content/browser.js
browser/components/sessionstore/src/nsSessionStore.js
browser/components/sessionstore/test/browser/Makefile.in
browser/components/sessionstore/test/browser/browser_644409-scratchpads.js
browser/devtools/Makefile.in
browser/devtools/scratchpad/Makefile.in
browser/devtools/scratchpad/scratchpad-manager.jsm
browser/devtools/scratchpad/scratchpad.js
browser/devtools/scratchpad/test/Makefile.in
browser/devtools/scratchpad/test/browser_scratchpad_open.js
browser/devtools/scratchpad/test/browser_scratchpad_restore.js
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -8895,24 +8895,26 @@ function toggleAddonBar() {
   let addonBar = document.getElementById("addon-bar");
   setToolbarVisibility(addonBar, addonBar.collapsed);
 }
 
 var Scratchpad = {
   prefEnabledName: "devtools.scratchpad.enabled",
 
   openScratchpad: function SP_openScratchpad() {
-    const SCRATCHPAD_WINDOW_URL = "chrome://browser/content/scratchpad.xul";
-    const SCRATCHPAD_WINDOW_FEATURES = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
-
-    return Services.ww.openWindow(null, SCRATCHPAD_WINDOW_URL, "_blank",
-                                  SCRATCHPAD_WINDOW_FEATURES, null);
-  },
+    return this.ScratchpadManager.openScratchpad();
+  }
 };
 
+XPCOMUtils.defineLazyGetter(Scratchpad, "ScratchpadManager", function() {
+  let tmp = {};
+  Cu.import("resource:///modules/devtools/scratchpad-manager.jsm", tmp);
+  return tmp.ScratchpadManager;
+});
+
 
 XPCOMUtils.defineLazyGetter(window, "gShowPageResizers", function () {
 #ifdef XP_WIN
   // Only show resizers on Windows 2000 and XP
   let sysInfo = Components.classes["@mozilla.org/system-info;1"]
                           .getService(Components.interfaces.nsIPropertyBag2);
   return parseFloat(sysInfo.getProperty("version")) < 6;
 #else
--- a/browser/components/sessionstore/src/nsSessionStore.js
+++ b/browser/components/sessionstore/src/nsSessionStore.js
@@ -135,16 +135,21 @@ Cu.import("resource://gre/modules/Servic
 // debug.js adds NS_ASSERT. cf. bug 669196
 Cu.import("resource://gre/modules/debug.js");
 
 XPCOMUtils.defineLazyGetter(this, "NetUtil", function() {
   Cu.import("resource://gre/modules/NetUtil.jsm");
   return NetUtil;
 });
 
+XPCOMUtils.defineLazyGetter(this, "ScratchpadManager", function() {
+  Cu.import("resource:///modules/devtools/scratchpad-manager.jsm");
+  return ScratchpadManager;
+});
+
 XPCOMUtils.defineLazyServiceGetter(this, "CookieSvc",
   "@mozilla.org/cookiemanager;1", "nsICookieManager2");
 
 #ifdef MOZ_CRASHREPORTER
 XPCOMUtils.defineLazyServiceGetter(this, "CrashReporter",
   "@mozilla.org/xre/app-info;1", "nsICrashReporter");
 #endif
 
@@ -1577,16 +1582,20 @@ SessionStoreService.prototype = {
     }
 
     // Merge closed windows from this session with ones from last session
     if (lastSessionState._closedWindows) {
       this._closedWindows = this._closedWindows.concat(lastSessionState._closedWindows);
       this._capClosedWindows();
     }
 
+    if (lastSessionState.scratchpads) {
+      ScratchpadManager.restoreSession(lastSessionState.scratchpads);
+    }
+
     // Set data that persists between sessions
     this._recentCrashes = lastSessionState.session &&
                           lastSessionState.session.recentCrashes || 0;
     this._sessionStartTime = lastSessionState.session &&
                              lastSessionState.session.startTime ||
                              this._sessionStartTime;
 
     this._lastSessionState = null;
@@ -2482,22 +2491,26 @@ SessionStoreService.prototype = {
       ix = -1;
 
     let session = {
       state: this._loadState == STATE_RUNNING ? STATE_RUNNING_STR : STATE_STOPPED_STR,
       lastUpdate: Date.now(),
       startTime: this._sessionStartTime,
       recentCrashes: this._recentCrashes
     };
+    
+    // get open Scratchpad window states too
+    var scratchpads = ScratchpadManager.getSessionState();
 
     return {
       windows: total,
       selectedWindow: ix + 1,
       _closedWindows: lastClosedWindowsCopy,
-      session: session
+      session: session,
+      scratchpads: scratchpads
     };
   },
 
   /**
    * serialize session data for a window 
    * @param aWindow
    *        Window reference
    * @returns string
@@ -2695,16 +2708,20 @@ SessionStoreService.prototype = {
     }
     if (aOverwriteTabs || root._firstTabs) {
       this._windows[aWindow.__SSi]._closedTabs = winData._closedTabs || [];
     }
     
     this.restoreHistoryPrecursor(aWindow, tabs, winData.tabs,
       (aOverwriteTabs ? (parseInt(winData.selected) || 1) : 0), 0, 0);
 
+    if (aState.scratchpads) {
+      ScratchpadManager.restoreSession(aState.scratchpads);
+    }
+
     // This will force the keypress listener that Panorama has to attach if it
     // isn't already. This will be the case if tab view wasn't entered or there
     // were only visible tabs when TabView.init was first called.
     aWindow.TabView.init();
 
     // set smoothScroll back to the original value
     tabstrip.smoothScroll = smoothScroll;
 
--- a/browser/components/sessionstore/test/browser/Makefile.in
+++ b/browser/components/sessionstore/test/browser/Makefile.in
@@ -144,16 +144,17 @@ include $(topsrcdir)/config/rules.mk
 	browser_615394-SSWindowState_events.js \
 	browser_618151.js \
 	browser_623779.js \
 	browser_624727.js \
 	browser_625257.js \
 	browser_628270.js \
 	browser_635418.js \
 	browser_636279.js \
+	browser_644409-scratchpads.js \
 	browser_645428.js \
 	browser_659591.js \
 	browser_662812.js \
 	browser_665702-state_session.js \
 	browser_682507.js \
 	browser_694378.js \
 	$(NULL)
 
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/browser/browser_644409-scratchpads.js
@@ -0,0 +1,59 @@
+ /* Any copyright is dedicated to the Public Domain.
+    http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const testState = {
+  windows: [{
+    tabs: [
+      { entries: [{ url: "about:blank" }] },
+    ]
+  }],
+  scratchpads: [
+    { text: "text1", executionContext: 1 },
+    { text: "", executionContext: 2, filename: "test.js" }
+  ]
+};
+
+// only finish() when correct number of windows opened
+var restored = [];
+function addState(state) {
+  restored.push(state);
+
+  if (restored.length == testState.scratchpads.length) {
+    ok(statesMatch(restored, testState.scratchpads),
+      "Two scratchpad windows restored");
+
+    Services.ww.unregisterNotification(windowObserver);
+    finish();
+  }
+}
+
+function test() {
+  waitForExplicitFinish();
+
+  Services.ww.registerNotification(windowObserver);
+
+  ss.setBrowserState(JSON.stringify(testState));
+}
+
+function windowObserver(aSubject, aTopic, aData) {
+  if (aTopic == "domwindowopened") {     
+    let win = aSubject.QueryInterface(Ci.nsIDOMWindow);
+    win.addEventListener("load", function() {
+      if (win.Scratchpad) {
+        let state = win.Scratchpad.getState();
+        win.close();
+        addState(state);
+      }
+    }, false);
+  }
+}
+
+function statesMatch(restored, states) {
+  return states.every(function(state) {
+    return restored.some(function(restoredState) {
+      return state.filename == restoredState.filename &&
+             state.text == restoredState.text &&
+             state.executionContext == restoredState.executionContext;
+    })
+  });
+}
\ No newline at end of file
--- a/browser/devtools/Makefile.in
+++ b/browser/devtools/Makefile.in
@@ -46,16 +46,13 @@ include $(DEPTH)/config/autoconf.mk
 
 include $(topsrcdir)/config/config.mk
 
 DIRS = \
   highlighter \
   webconsole \
   sourceeditor \
   styleinspector \
+  scratchpad \
   shared \
   $(NULL)
 
-ifdef ENABLE_TESTS
-DIRS += scratchpad/test
-endif
-
 include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/browser/devtools/scratchpad/Makefile.in
@@ -0,0 +1,53 @@
+#
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is Scratchpad Build Code.
+#
+# The Initial Developer of the Original Code is The Mozilla Foundation.
+#
+# Portions created by the Initial Developer are Copyright (C) 2011
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#   Rob Campbell <rcampbell@mozilla.com>
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either the GNU General Public License Version 2 or later (the "GPL"), or
+# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+
+DEPTH		= ../../..
+topsrcdir	= @top_srcdir@
+srcdir		= @srcdir@
+VPATH		= @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+ifdef ENABLE_TESTS
+	DIRS += test
+endif
+
+include $(topsrcdir)/config/rules.mk
+
+libs::
+	$(NSINSTALL) $(srcdir)/*.jsm $(FINAL_TARGET)/modules/devtools
new file mode 100644
--- /dev/null
+++ b/browser/devtools/scratchpad/scratchpad-manager.jsm
@@ -0,0 +1,174 @@
+/* vim:set ts=2 sw=2 sts=2 et tw=80:
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Scratchpad
+ *
+ * The Initial Developer of the Original Code is
+ * The Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Heather Arthur <fayearthur@gmail.com> (original author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK *****/
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["ScratchpadManager"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+const SCRATCHPAD_WINDOW_URL = "chrome://browser/content/scratchpad.xul";
+const SCRATCHPAD_WINDOW_FEATURES = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+/**
+ * The ScratchpadManager object opens new Scratchpad windows and manages the state
+ * of open scratchpads for session restore. There's only one ScratchpadManager in
+ * the life of the browser.
+ */
+var ScratchpadManager = {
+
+  _scratchpads: [],
+
+  /**
+   * Get the saved states of open scratchpad windows. Called by
+   * session restore.
+   *
+   * @return array
+   *         The array of scratchpad states.
+   */
+  getSessionState: function SPM_getSessionState()
+  {
+    return this._scratchpads;
+  },
+
+  /**
+   * Restore scratchpad windows from the scratchpad session store file.
+   * Called by session restore.
+   *
+   * @param function aSession
+   *        The session object with scratchpad states.
+   *
+   * @return array
+   *         The restored scratchpad windows.
+   */
+  restoreSession: function SPM_restoreSession(aSession)
+  {
+    if (!Array.isArray(aSession)) {
+      return [];
+    }
+
+    let wins = [];
+    aSession.forEach(function(state) {
+      let win = this.openScratchpad(state);
+      wins.push(win);
+    }, this);
+
+    return wins;
+  },
+
+  /**
+   * Iterate through open scratchpad windows and save their states.
+   */
+  saveOpenWindows: function SPM_saveOpenWindows() {
+    this._scratchpads = [];
+
+    let enumerator = Services.wm.getEnumerator("devtools:scratchpad");
+    while (enumerator.hasMoreElements()) {
+      let win = enumerator.getNext();
+      if (!win.closed) {
+        this._scratchpads.push(win.Scratchpad.getState());
+      }
+    }
+  },
+
+  /**
+   * Open a new scratchpad window with an optional initial state.
+   *
+   * @param object aState
+   *        Optional. The initial state of the scratchpad, an object
+   *        with properties filename, text, and executionContext.
+   *
+   * @return nsIDomWindow
+   *         The opened scratchpad window.
+   */
+  openScratchpad: function SPM_openScratchpad(aState)
+  {
+    let params = null;
+    if (aState) {
+      if (typeof aState != 'object') {
+        return;
+      }
+      params = Cc["@mozilla.org/embedcomp/dialogparam;1"]
+               .createInstance(Ci.nsIDialogParamBlock);
+      params.SetNumberStrings(1);
+      params.SetString(0, JSON.stringify(aState));
+    }
+    let win = Services.ww.openWindow(null, SCRATCHPAD_WINDOW_URL, "_blank",
+                                     SCRATCHPAD_WINDOW_FEATURES, params);
+    // Only add shutdown observer if we've opened a scratchpad window
+    ShutdownObserver.init();
+
+    return win;
+  }
+};
+
+
+/**
+ * The ShutdownObserver listens for app shutdown and saves the current state
+ * of the scratchpads for session restore.
+ */
+var ShutdownObserver = {
+  _initialized: false,
+
+  init: function SDO_init()
+  {
+    if (this._initialized) {
+      return;
+    }
+
+    Services.obs.addObserver(this, "quit-application-granted", false);
+    this._initialized = true;
+  },
+
+  observe: function SDO_observe(aMessage, aTopic, aData)
+  {
+    if (aTopic == "quit-application-granted") {
+      ScratchpadManager.saveOpenWindows();
+      this.uninit();
+    }
+  },
+
+  uninit: function SDO_uninit()
+  {
+    Services.obs.removeObserver(this, "quit-application-granted");
+  }
+};
--- a/browser/devtools/scratchpad/scratchpad.js
+++ b/browser/devtools/scratchpad/scratchpad.js
@@ -54,22 +54,22 @@ const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
 Cu.import("resource:///modules/PropertyPanel.jsm");
 Cu.import("resource:///modules/source-editor.jsm");
+Cu.import("resource:///modules/devtools/scratchpad-manager.jsm");
+
 
 const SCRATCHPAD_CONTEXT_CONTENT = 1;
 const SCRATCHPAD_CONTEXT_BROWSER = 2;
-const SCRATCHPAD_WINDOW_URL = "chrome://browser/content/scratchpad.xul";
 const SCRATCHPAD_L10N = "chrome://browser/locale/devtools/scratchpad.properties";
-const SCRATCHPAD_WINDOW_FEATURES = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
 const DEVTOOLS_CHROME_ENABLED = "devtools.chrome.enabled";
 
 /**
  * The scratchpad object handles the Scratchpad window functionality.
  */
 var Scratchpad = {
   /**
    * The script execution context. This tells Scratchpad in which context the
@@ -128,16 +128,65 @@ var Scratchpad = {
    *        replacing text in the editor.
    */
   setText: function SP_setText(aText, aStart, aEnd)
   {
     this.editor.setText(aText, aStart, aEnd);
   },
 
   /**
+   * Set the filename in the scratchpad UI and object
+   *
+   * @param string aFilename
+   *        The new filename
+   */
+  setFilename: function SP_setFilename(aFilename)
+  {
+    document.title = this.filename = aFilename;
+  },
+
+  /**
+   * Get the current state of the scratchpad. Called by the
+   * Scratchpad Manager for session storing.
+   *
+   * @return object
+   *        An object with 3 properties: filename, text, and
+   *        executionContext.
+   */
+  getState: function SP_getState()
+  {
+    return {
+      filename: this.filename,
+      text: this.getText(),
+      executionContext: this.executionContext
+    };
+  },
+
+  /**
+   * Set the filename and execution context using the given state. Called
+   * when scratchpad is being restored from a previous session.
+   *
+   * @param object aState
+   *        An object with filename and executionContext properties.
+   */
+  setState: function SP_getState(aState)
+  {
+    if (aState.filename) {
+      this.setFilename(aState.filename);
+    }
+
+    if (aState.executionContext == SCRATCHPAD_CONTEXT_BROWSER) {
+      this.setBrowserContext();
+    }
+    else {
+      this.setContentContext();
+    }
+  },
+
+  /**
    * Get the most recent chrome window of type navigator:browser.
    */
   get browserWindow() Services.wm.getMostRecentWindow("navigator:browser"),
 
   /**
    * Reference to the last chrome window of type navigator:browser. We use this
    * to check if the chrome window changed since the last code evaluation.
    */
@@ -437,18 +486,17 @@ var Scratchpad = {
 
   // Menu Operations
 
   /**
    * Open a new Scratchpad window.
    */
   openScratchpad: function SP_openScratchpad()
   {
-    Services.ww.openWindow(null, SCRATCHPAD_WINDOW_URL, "_blank",
-                           SCRATCHPAD_WINDOW_FEATURES, null);
+    ScratchpadManager.openScratchpad();
   },
 
   /**
    * Export the textbox content to a file.
    *
    * @param nsILocalFile aFile
    *        The file where you want to save the textbox content.
    * @param boolean aNoConfirmation
@@ -536,17 +584,17 @@ var Scratchpad = {
    */
   openFile: function SP_openFile()
   {
     let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
     fp.init(window, this.strings.GetStringFromName("openFile.title"),
             Ci.nsIFilePicker.modeOpen);
     fp.defaultString = "";
     if (fp.show() != Ci.nsIFilePicker.returnCancel) {
-      document.title = this.filename = fp.file.path;
+      this.setFilename(fp.file.path);
       this.importFromFile(fp.file);
     }
   },
 
   /**
    * Save the textbox content to the currently open file.
    */
   saveFile: function SP_saveFile()
@@ -675,22 +723,30 @@ var Scratchpad = {
       let environmentMenu = document.getElementById("sp-environment-menu");
       let errorConsoleCommand = document.getElementById("sp-cmd-errorConsole");
       let chromeContextCommand = document.getElementById("sp-cmd-browserContext");
       environmentMenu.removeAttribute("hidden");
       chromeContextCommand.removeAttribute("disabled");
       errorConsoleCommand.removeAttribute("disabled");
     }
 
+    let initialText = this.strings.GetStringFromName("scratchpadIntro");
+    if ("arguments" in window &&
+         window.arguments[0] instanceof Ci.nsIDialogParamBlock) {
+      let state = JSON.parse(window.arguments[0].GetString(0));
+      this.setState(state);
+      initialText = state.text;
+    }
+
     this.editor = new SourceEditor();
 
     let config = {
       mode: SourceEditor.MODES.JAVASCRIPT,
       showLineNumbers: true,
-      placeholderText: this.strings.GetStringFromName("scratchpadIntro"),
+      placeholderText: initialText
     };
 
     let editorPlaceholder = document.getElementById("scratchpad-editor");
     this.editor.init(editorPlaceholder, config, this.onEditorLoad.bind(this));
   },
 
   /**
    * The load event handler for the source editor. This method does post-load
--- a/browser/devtools/scratchpad/test/Makefile.in
+++ b/browser/devtools/scratchpad/test/Makefile.in
@@ -48,11 +48,13 @@ include $(topsrcdir)/config/rules.mk
 		browser_scratchpad_contexts.js \
 		browser_scratchpad_tab_switch.js \
 		browser_scratchpad_execute_print.js \
 		browser_scratchpad_inspect.js \
 		browser_scratchpad_files.js \
 		browser_scratchpad_ui.js \
 		browser_scratchpad_bug_646070_chrome_context_pref.js \
 		browser_scratchpad_bug_660560_tab.js \
+		browser_scratchpad_open.js \
+		browser_scratchpad_restore.js \
 
 libs:: $(_BROWSER_TEST_FILES)
 	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir)
new file mode 100644
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_open.js
@@ -0,0 +1,71 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var ScratchpadManager = Scratchpad.ScratchpadManager;
+
+// only finish() when correct number of tests are done
+const expected = 3;
+var count = 0;
+
+function done()
+{
+  if (++count == expected) {
+    finish();
+  }
+}
+
+
+function test()
+{
+  waitForExplicitFinish();
+  testOpen();
+  testOpenWithState();
+  testOpenInvalidState();
+}
+
+function testOpen()
+{
+  let win = ScratchpadManager.openScratchpad();
+
+  win.addEventListener("load", function() {
+    is(win.Scratchpad.filename, undefined, "Default filename is undefined");
+    is(win.Scratchpad.getText(),
+       win.Scratchpad.strings.GetStringFromName("scratchpadIntro"),
+       "Default text is loaded")
+    is(win.Scratchpad.executionContext, win.SCRATCHPAD_CONTEXT_CONTENT,
+      "Default execution context is content");
+
+    win.close();
+    done();
+  });
+}
+
+function testOpenWithState()
+{
+  let state = {
+    filename: "testfile",
+    executionContext: 2,
+    text: "test text"
+  };
+
+  let win = ScratchpadManager.openScratchpad(state);
+
+  win.addEventListener("load", function() {
+    is(win.Scratchpad.filename, state.filename, "Filename loaded from state");
+    is(win.Scratchpad.executionContext, state.executionContext, "Execution context loaded from state");
+    is(win.Scratchpad.getText(), state.text, "Content loaded from state");
+
+    win.close();
+    done();
+  });
+}
+
+function testOpenInvalidState()
+{
+  let state = 7;
+
+  let win = ScratchpadManager.openScratchpad(state);
+  ok(!win, "no scratchpad opened if state is not an object");
+  done();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_restore.js
@@ -0,0 +1,101 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var ScratchpadManager = Scratchpad.ScratchpadManager;
+
+/* Call the iterator for each item in the list,
+   calling the final callback with all the results
+   after every iterator call has sent its result */
+function asyncMap(items, iterator, callback)
+{
+  let expected = items.length;
+  let results = [];
+
+  items.forEach(function(item) {
+    iterator(item, function(result) {
+      results.push(result);
+      if (results.length == expected) {
+        callback(results);
+      }
+    });
+  });
+}
+
+function test()
+{
+  waitForExplicitFinish();
+  testRestore();
+}
+
+function testRestore()
+{
+  let states = [
+    {
+      filename: "testfile",
+      text: "test1",
+      executionContext: 2
+    },
+    {
+      text: "text2",
+      executionContext: 1
+    },
+    {
+      text: "text3",
+      executionContext: 1
+    }
+  ];
+
+  asyncMap(states, function(state, done) {
+    // Open some scratchpad windows
+    let win = ScratchpadManager.openScratchpad(state);
+    win.addEventListener("load", function() {
+      done(win);
+    })
+  }, function(wins) {
+    // Then save the windows to session store
+    ScratchpadManager.saveOpenWindows();
+
+    // Then get their states
+    let session = ScratchpadManager.getSessionState();
+
+    // Then close them
+    wins.forEach(function(win) {
+      win.close();
+    });
+
+    // Clear out session state for next tests
+    ScratchpadManager.saveOpenWindows();
+
+    // Then restore them
+    let restoredWins = ScratchpadManager.restoreSession(session);
+
+    is(restoredWins.length, 3, "Three scratchad windows restored");
+
+    asyncMap(restoredWins, function(restoredWin, done) {
+      restoredWin.addEventListener("load", function() {
+        let state = restoredWin.Scratchpad.getState();
+        restoredWin.close();
+        done(state);
+      });
+    }, function(restoredStates) {
+      // Then make sure they were restored with the right states
+      ok(statesMatch(restoredStates, states),
+        "All scratchpad window states restored correctly");
+
+      // Yay, we're done!
+      finish();
+    });
+  });
+}
+
+function statesMatch(restoredStates, states)
+{
+  return states.every(function(state) {
+    return restoredStates.some(function(restoredState) {
+      return state.filename == restoredState.filename
+        && state.text == restoredState.text
+        && state.executionContext == restoredState.executionContext;
+    })
+  });
+}