Bug 583041 - Style Editor integration; part 1; r=rcampbell
authorCedric Vivier <cedricv@neonux.com>
Tue, 15 Nov 2011 16:41:37 +0800
changeset 80300 00e06ace79460572b3ce4eb4327c3b55d185eeec
parent 80299 a43d24581da011cd7cdfeb28cf45a74caaa763ad
child 80301 69cd4124a49a44d338dd10938d57448c88cd3405
push id330
push userrcampbell@mozilla.com
push dateThu, 17 Nov 2011 22:33:35 +0000
treeherderfx-team@1cac8411fdc6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrcampbell
bugs583041
milestone11.0a1
Bug 583041 - Style Editor integration; part 1; r=rcampbell
browser/devtools/scratchpad/scratchpad.js
browser/devtools/scratchpad/test/Makefile.in
browser/devtools/scratchpad/test/browser_scratchpad_bug_646070_chrome_context_pref.js
browser/devtools/scratchpad/test/browser_scratchpad_bug_660560_tab.js
browser/devtools/scratchpad/test/browser_scratchpad_bug_669612_unsaved.js
browser/devtools/scratchpad/test/browser_scratchpad_bug_679467_falsy.js
browser/devtools/scratchpad/test/browser_scratchpad_bug_699130_edit_ui_updates.js
browser/devtools/scratchpad/test/browser_scratchpad_contexts.js
browser/devtools/scratchpad/test/browser_scratchpad_execute_print.js
browser/devtools/scratchpad/test/browser_scratchpad_files.js
browser/devtools/scratchpad/test/browser_scratchpad_initialization.js
browser/devtools/scratchpad/test/browser_scratchpad_inspect.js
browser/devtools/scratchpad/test/browser_scratchpad_open.js
browser/devtools/scratchpad/test/browser_scratchpad_restore.js
browser/devtools/scratchpad/test/browser_scratchpad_tab_switch.js
browser/devtools/scratchpad/test/browser_scratchpad_ui.js
browser/devtools/scratchpad/test/head.js
--- a/browser/devtools/scratchpad/scratchpad.js
+++ b/browser/devtools/scratchpad/scratchpad.js
@@ -486,20 +486,22 @@ var Scratchpad = {
 
     return propPanel;
   },
 
   // Menu Operations
 
   /**
    * Open a new Scratchpad window.
+   *
+   * @return nsIWindow
    */
   openScratchpad: function SP_openScratchpad()
   {
-    ScratchpadManager.openScratchpad();
+    return 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
@@ -763,16 +765,18 @@ var Scratchpad = {
     this.editor.setCaretOffset(this.editor.getCharCount());
     
     if (this.filename && !this.saved) {
       this.onTextChanged();
     }
     else if (this.filename && this.saved) {
       this.onTextSaved();
     }
+
+    this._triggerObservers("Ready");
   },
 
   /**
    * Insert text at the current caret location.
    *
    * @param string aText
    *        The text you want to insert.
    */
@@ -872,16 +876,78 @@ var Scratchpad = {
     }
 
     this.resetContext();
     this.editor.removeEventListener(SourceEditor.EVENTS.CONTEXT_MENU,
                                     this.onContextMenu);
     this.editor.destroy();
     this.editor = null;
   },
+
+  _observers: [],
+
+  /**
+   * Add an observer for Scratchpad events.
+   *
+   * The observer implements IScratchpadObserver := {
+   *   onReady:      Called when the Scratchpad and its SourceEditor are ready.
+   *                 Arguments: (Scratchpad aScratchpad)
+   * }
+   *
+   * All observer handlers are optional.
+   *
+   * @param IScratchpadObserver aObserver
+   * @see removeObserver
+   */
+  addObserver: function SP_addObserver(aObserver)
+  {
+    this._observers.push(aObserver);
+  },
+
+  /**
+   * Remove an observer for Scratchpad events.
+   *
+   * @param IScratchpadObserver aObserver
+   * @see addObserver
+   */
+  removeObserver: function SP_removeObserver(aObserver)
+  {
+    let index = this._observers.indexOf(aObserver);
+    if (index != -1) {
+      this._observers.splice(index, 1);
+    }
+  },
+
+  /**
+   * Trigger named handlers in Scratchpad observers.
+   *
+   * @param string aName
+   *        Name of the handler to trigger.
+   * @param Array aArgs
+   *        Optional array of arguments to pass to the observer(s).
+   * @see addObserver
+   */
+  _triggerObservers: function SP_triggerObservers(aName, aArgs)
+  {
+    // insert this Scratchpad instance as the first argument
+    if (!aArgs) {
+      aArgs = [this];
+    } else {
+      aArgs.unshift(this);
+    }
+
+    // trigger all observers that implement this named handler
+    for (let i = 0; i < this._observers.length; ++i) {
+      let observer = this._observers[i];
+      let handler = observer["on" + aName];
+      if (handler) {
+        handler.apply(observer, aArgs);
+      }
+    }
+  }
 };
 
 XPCOMUtils.defineLazyGetter(Scratchpad, "strings", function () {
   return Services.strings.createBundle(SCRATCHPAD_L10N);
 });
 
 addEventListener("DOMContentLoaded", Scratchpad.onLoad.bind(Scratchpad), false);
 addEventListener("unload", Scratchpad.onUnload.bind(Scratchpad), false);
--- a/browser/devtools/scratchpad/test/Makefile.in
+++ b/browser/devtools/scratchpad/test/Makefile.in
@@ -53,11 +53,12 @@ include $(topsrcdir)/config/rules.mk
 		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 \
 		browser_scratchpad_bug_679467_falsy.js \
 		browser_scratchpad_bug_699130_edit_ui_updates.js \
 		browser_scratchpad_bug_669612_unsaved.js \
+		head.js \
 
 libs:: $(_BROWSER_TEST_FILES)
 	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir)
--- a/browser/devtools/scratchpad/test/browser_scratchpad_bug_646070_chrome_context_pref.js
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug_646070_chrome_context_pref.js
@@ -1,15 +1,12 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-// Reference to the Scratchpad chrome window object.
-let gScratchpadWindow;
-
 let gOldPref;
 let DEVTOOLS_CHROME_ENABLED = "devtools.chrome.enabled";
 
 function test()
 {
   waitForExplicitFinish();
 
   gOldPref = Services.prefs.getBoolPref(DEVTOOLS_CHROME_ENABLED);
@@ -50,13 +47,10 @@ function runTests()
   let chromeContextCommand = gScratchpadWindow.document.
                             getElementById("sp-cmd-browserContext");
   ok(chromeContextCommand, "Chrome context command element exists");
   ok(!chromeContextCommand.hasAttribute("disabled"),
      "Chrome context command is disabled");
 
   Services.prefs.setBoolPref(DEVTOOLS_CHROME_ENABLED, gOldPref);
 
-  gScratchpadWindow.close();
-  gScratchpadWindow = null;
-  gBrowser.removeCurrentTab();
   finish();
 }
--- a/browser/devtools/scratchpad/test/browser_scratchpad_bug_660560_tab.js
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug_660560_tab.js
@@ -1,41 +1,47 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-// Reference to the Scratchpad chrome window object.
-let gScratchpadWindow;
+var ScratchpadManager = Scratchpad.ScratchpadManager;
 
 function test()
 {
   waitForExplicitFinish();
 
   gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.selectedBrowser.addEventListener("load", function() {
-    gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
+  gBrowser.selectedBrowser.addEventListener("load", function onTabLoad() {
+    gBrowser.selectedBrowser.removeEventListener("load", onTabLoad, true);
 
     ok(window.Scratchpad, "Scratchpad variable exists");
 
     Services.prefs.setIntPref("devtools.editor.tabsize", 5);
 
     gScratchpadWindow = Scratchpad.openScratchpad();
-    gScratchpadWindow.addEventListener("load", runTests, false);
+    gScratchpadWindow.addEventListener("load", function onScratchpadLoad() {
+      gScratchpadWindow.removeEventListener("load", onScratchpadLoad, false);
+
+      gScratchpadWindow.Scratchpad.addObserver({
+        onReady: runTests
+      });
+    }, false);
   }, true);
 
   content.location = "data:text/html,Scratchpad test for the Tab key, bug 660560";
 }
 
 function runTests()
 {
-  gScratchpadWindow.removeEventListener("load", arguments.callee, false);
-
   let sp = gScratchpadWindow.Scratchpad;
   ok(sp, "Scratchpad object exists in new window");
 
+  is(this.onReady, runTests, "the handler runs in the context of the observer");
+  sp.removeObserver(this);
+
   ok(sp.editor.hasFocus(), "the editor has focus");
 
   sp.setText("window.foo;");
   sp.editor.setCaretOffset(0);
 
   EventUtils.synthesizeKey("VK_TAB", {}, gScratchpadWindow);
 
   is(sp.getText(), "     window.foo;", "Tab key added 5 spaces");
@@ -58,36 +64,37 @@ function runTests()
   is(sp.getText(), "     w    omgindow.foo;", "insertTextAtCaret() works");
 
   is(sp.editor.getCaretOffset(), 13, "caret location is correct after update");
 
   gScratchpadWindow.close();
 
   Services.prefs.setIntPref("devtools.editor.tabsize", 6);
   Services.prefs.setBoolPref("devtools.editor.expandtab", false);
+
   gScratchpadWindow = Scratchpad.openScratchpad();
-  gScratchpadWindow.addEventListener("load", runTests2, false);
+  gScratchpadWindow.addEventListener("load", function onScratchpadLoad() {
+    gScratchpadWindow.removeEventListener("load", onScratchpadLoad, false);
+    gScratchpadWindow.Scratchpad.addObserver({
+      onReady: runTests2
+    });
+  }, false);
 }
 
 function runTests2()
 {
-  gScratchpadWindow.removeEventListener("load", arguments.callee, false);
-
   let sp = gScratchpadWindow.Scratchpad;
+  sp.removeObserver(this);
 
   sp.setText("window.foo;");
   sp.editor.setCaretOffset(0);
 
   EventUtils.synthesizeKey("VK_TAB", {}, gScratchpadWindow);
 
   is(sp.getText(), "\twindow.foo;", "Tab key added the tab character");
 
   is(sp.editor.getCaretOffset(), 1, "caret location is correct");
 
   Services.prefs.clearUserPref("devtools.editor.tabsize");
   Services.prefs.clearUserPref("devtools.editor.expandtab");
 
-  gScratchpadWindow.close();
-  gScratchpadWindow = null;
-
-  gBrowser.removeCurrentTab();
   finish();
 }
--- a/browser/devtools/scratchpad/test/browser_scratchpad_bug_669612_unsaved.js
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug_669612_unsaved.js
@@ -27,114 +27,144 @@ function test()
 
   content.location = "data:text/html,<p>test star* UI for unsaved file changes";
 }
 
 function testListeners()
 {
   let win = ScratchpadManager.openScratchpad();
 
-  win.addEventListener("load", function() {
-    let scratchpad = win.Scratchpad;
+  win.addEventListener("load", function onScratchpadLoad() {
+    win.removeEventListener("load", onScratchpadLoad, false);
 
-    scratchpad.setText("new text");
-    ok(!isStar(win), "no star if scratchpad isn't from a file");
+    win.Scratchpad.addObserver({
+      onReady: function (aScratchpad) {
+        aScratchpad.removeObserver(this);
 
-    scratchpad.onTextSaved(); 
-    ok(!isStar(win), "no star before changing text");
+        aScratchpad.setText("new text");
+        ok(!isStar(win), "no star if scratchpad isn't from a file");
+
+        aScratchpad.onTextSaved();
+        ok(!isStar(win), "no star before changing text");
 
-    scratchpad.setText("new text2");
-    ok(isStar(win), "shows star if scratchpad text changes");
+        aScratchpad.setText("new text2");
+        ok(isStar(win), "shows star if scratchpad text changes");
+
+        aScratchpad.onTextSaved();
+        ok(!isStar(win), "no star if scratchpad was just saved");
 
-    scratchpad.onTextSaved();
-    ok(!isStar(win), "no star if scratchpad was just saved");
-    
-    scratchpad.undo();
-    ok(isStar(win), "star if scratchpad undo");
+        aScratchpad.undo();
+        ok(isStar(win), "star if scratchpad undo");
 
-    win.close();
-    done();
-  });
+        win.close();
+        done();
+      }
+    });
+  }, false);
 }
 
 function testErrorStatus()
 {
   let win = ScratchpadManager.openScratchpad();
 
-  win.addEventListener("load", function() {
-    let scratchpad = win.Scratchpad;
+  win.addEventListener("load", function onScratchpadLoad() {
+    win.removeEventListener("load", onScratchpadLoad, false);
+
+    win.Scratchpad.addObserver({
+      onReady: function (aScratchpad) {
+        aScratchpad.removeObserver(this);
 
-    scratchpad.onTextSaved(Components.results.NS_ERROR_FAILURE);
-    scratchpad.setText("new text");
-    ok(!isStar(win), "no star if file save failed");
+        aScratchpad.onTextSaved(Components.results.NS_ERROR_FAILURE);
+        aScratchpad.setText("new text");
+        ok(!isStar(win), "no star if file save failed");
 
-    win.close();
-    done();
-  });
+        win.close();
+        done();
+      }
+    });
+  }, false);
 }
 
 
 function testRestoreNotFromFile()
 {
   let session = [{
     text: "test1",
     executionContext: 1
   }];
 
   let [win] = ScratchpadManager.restoreSession(session);
-  win.addEventListener("load", function() {
-    let scratchpad = win.Scratchpad;
+  win.addEventListener("load", function onScratchpadLoad() {
+    win.removeEventListener("load", onScratchpadLoad, false);
+
+    win.Scratchpad.addObserver({
+      onReady: function (aScratchpad) {
+        aScratchpad.removeObserver(this);
 
-    scratchpad.setText("new text");
-    ok(!isStar(win), "no star if restored scratchpad isn't from a file");
-    
-    win.close();
-    done();
-  });
+        aScratchpad.setText("new text");
+        ok(!isStar(win), "no star if restored scratchpad isn't from a file");
+
+        win.close();
+        done();
+      }
+    });
+  }, false);
 }
 
 function testRestoreFromFileSaved()
 {
   let session = [{
     filename: "test.js",
     text: "test1",
     executionContext: 1,
     saved: true
   }];
 
   let [win] = ScratchpadManager.restoreSession(session);
-  win.addEventListener("load", function() {
-    let scratchpad = win.Scratchpad;
+  win.addEventListener("load", function onScratchpadLoad() {
+    win.removeEventListener("load", onScratchpadLoad, false);
 
-    ok(!isStar(win), "no star before changing text in scratchpad restored from file");
+    win.Scratchpad.addObserver({
+      onReady: function (aScratchpad) {
+        aScratchpad.removeObserver(this);
+
+        ok(!isStar(win), "no star before changing text in scratchpad restored from file");
 
-    scratchpad.setText("new text");
-    ok(isStar(win), "star when text changed from scratchpad restored from file");
+        aScratchpad.setText("new text");
+        ok(isStar(win), "star when text changed from scratchpad restored from file");
 
-    win.close();
-    done();
-  });
+        win.close();
+        done();
+      }
+    });
+  }, false);
 }
 
 function testRestoreFromFileUnsaved()
 {
   let session = [{
     filename: "test.js",
     text: "test1",
     executionContext: 1,
     saved: false
   }];
 
   let [win] = ScratchpadManager.restoreSession(session);
-  win.addEventListener("load", function() {
-    let scratchpad = win.Scratchpad;
+  win.addEventListener("load", function onScratchpadLoad() {
+    win.removeEventListener("load", onScratchpadLoad, false);
+
+    win.Scratchpad.addObserver({
+      onReady: function (aScratchpad) {
+        aScratchpad.removeObserver(this);
 
-    ok(isStar(win), "star with scratchpad restored with unsaved text");
+        ok(isStar(win), "star with scratchpad restored with unsaved text");
 
-    win.close();
-    done();
-  });
+        win.close();
+        done();
+      }
+    });
+  }, false);
 }
 
 function isStar(win)
 {
   return win.document.title.match(/^\*[^\*]/);
 }
--- a/browser/devtools/scratchpad/test/browser_scratchpad_bug_679467_falsy.js
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug_679467_falsy.js
@@ -25,19 +25,16 @@ function testFalsy(sp)
   gScratchpadWindow.removeEventListener("load", testFalsy, false);
 
   let sp = gScratchpadWindow.Scratchpad;
   verifyFalsies(sp);
   
   sp.setBrowserContext();
   verifyFalsies(sp);
 
-  gScratchpadWindow.close();
-  gScratchpadWindow = null;
-  gBrowser.removeCurrentTab();
   finish();
 }
 
 function verifyFalsies(sp)
 {
   sp.setText("undefined");
   sp.display();
   is(sp.selectedText, "/*\nundefined\n*/", "'undefined' is displayed");
--- a/browser/devtools/scratchpad/test/browser_scratchpad_bug_699130_edit_ui_updates.js
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug_699130_edit_ui_updates.js
@@ -1,39 +1,35 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 Cu.import("resource:///modules/source-editor.jsm");
 
-// Reference to the Scratchpad chrome window object.
-let gScratchpadWindow;
-
 function test()
 {
   waitForExplicitFinish();
 
   gBrowser.selectedTab = gBrowser.addTab();
   gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
     gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
 
     gScratchpadWindow = Scratchpad.openScratchpad();
-    gScratchpadWindow.addEventListener("load", function onScratchpadLoad() {
-      gScratchpadWindow.removeEventListener("load", onScratchpadLoad, false);
-      waitForFocus(runTests, gScratchpadWindow);
-    }, false);
+    gScratchpadWindow.addEventListener("load", runTests, false);
   }, true);
 
   content.location = "data:text/html,test Edit menu updates Scratchpad - bug 699130";
 }
 
 function runTests()
 {
+  gScratchpadWindow.removeEventListener("load", runTests, false);
+
   let sp = gScratchpadWindow.Scratchpad;
   let doc = gScratchpadWindow.document;
   let winUtils = gScratchpadWindow.QueryInterface(Ci.nsIInterfaceRequestor).
                  getInterface(Ci.nsIDOMWindowUtils);
   let OS = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS;
 
   info("will test the Edit menu");
 
@@ -121,33 +117,40 @@ function runTests()
 
   let showAfterSelect = function() {
     ok(!cutItem.hasAttribute("disabled"), "cut menuitem is enabled after select");
     closeMenu(hideAfterSelect);
   };
 
   let hideAfterSelect = function() {
     sp.editor.addEventListener(SourceEditor.EVENTS.TEXT_CHANGED, onCut);
-    EventUtils.synthesizeKey("x", {accelKey: true}, gScratchpadWindow);
+    waitForFocus(function () {
+      let selectedText = sp.editor.getSelectedText();
+      ok(selectedText.length > 0, "non-empty selected text will be cut");
+
+      EventUtils.synthesizeKey("x", {accelKey: true}, gScratchpadWindow);
+    }, gScratchpadWindow);
   };
 
   let onCut = function() {
     sp.editor.removeEventListener(SourceEditor.EVENTS.TEXT_CHANGED, onCut);
     openMenu(12, 12, showAfterCut);
   };
 
   let showAfterCut = function() {
     ok(cutItem.hasAttribute("disabled"), "cut menuitem is disabled after cut");
     ok(!pasteItem.hasAttribute("disabled"), "paste menuitem is enabled after cut");
     closeMenu(hideAfterCut);
   };
 
   let hideAfterCut = function() {
     sp.editor.addEventListener(SourceEditor.EVENTS.TEXT_CHANGED, onPaste);
-    EventUtils.synthesizeKey("v", {accelKey: true}, gScratchpadWindow);
+    waitForFocus(function () {
+      EventUtils.synthesizeKey("v", {accelKey: true}, gScratchpadWindow);
+    }, gScratchpadWindow);
   };
 
   let onPaste = function() {
     sp.editor.removeEventListener(SourceEditor.EVENTS.TEXT_CHANGED, onPaste);
     openMenu(13, 13, showAfterPaste);
   };
 
   let showAfterPaste = function() {
@@ -156,17 +159,17 @@ function runTests()
     closeMenu(hideAfterPaste);
   };
 
   let hideAfterPaste = function() {
     if (pass == 0) {
       pass++;
       testContextMenu();
     } else {
-      finishTest();
+      finish();
     }
   };
 
   let testContextMenu = function() {
     info("will test the context menu");
 
     editMenu = null;
     isContextMenu = true;
@@ -177,17 +180,10 @@ function runTests()
     ok(cutItem, "the Cut menuitem");
     pasteItem = doc.getElementById("menu_paste");
     ok(pasteItem, "the Paste menuitem");
 
     sp.setText("bug 699130: hello world! (context menu)");
     openMenu(10, 10, firstShow);
   };
 
-  let finishTest = function() {
-    gScratchpadWindow.close();
-    gScratchpadWindow = null;
-    gBrowser.removeCurrentTab();
-    finish();
-  };
-
   openMenu(10, 10, firstShow);
 }
--- a/browser/devtools/scratchpad/test/browser_scratchpad_contexts.js
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_contexts.js
@@ -118,13 +118,10 @@ function runTests()
   sp.setContentContext();
 
   is(sp.executionContext, gScratchpadWindow.SCRATCHPAD_CONTEXT_CONTENT,
      "executionContext is content");
 
   is(sp.run()[2], "undefined",
      "global variable no longer exists after changing the context");
 
-  gScratchpadWindow.close();
-  gScratchpadWindow = null;
-  gBrowser.removeCurrentTab();
   finish();
 }
--- a/browser/devtools/scratchpad/test/browser_scratchpad_execute_print.js
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_execute_print.js
@@ -1,15 +1,12 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-// Reference to the Scratchpad chrome window object.
-let gScratchpadWindow;
-
 function test()
 {
   waitForExplicitFinish();
 
   gBrowser.selectedTab = gBrowser.addTab();
   gBrowser.selectedBrowser.addEventListener("load", function() {
     gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
 
@@ -124,13 +121,10 @@ function runTests()
   sp.setText("foo1");
   sp.setText("foo2");
   is(sp.getText(), "foo2", "editor content updated");
   sp.undo();
   is(sp.getText(), "foo1", "undo() works");
   sp.redo();
   is(sp.getText(), "foo2", "redo() works");
 
-  gScratchpadWindow.close();
-  gScratchpadWindow = null;
-  gBrowser.removeCurrentTab();
   finish();
 }
--- a/browser/devtools/scratchpad/test/browser_scratchpad_files.js
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_files.js
@@ -133,13 +133,10 @@ function fileRead(aInputStream, aStatus)
     NetUtil.readInputStreamToString(aInputStream, aInputStream.available());;
 
   is(updatedContent, gFileContent, "file properly updated");
 
   // Done!
   gFile.remove(false);
   gFile = null;
   gScratchpad = null;
-  gScratchpadWindow.close();
-  gScratchpadWindow = null;
-  gBrowser.removeCurrentTab();
   finish();
 }
--- a/browser/devtools/scratchpad/test/browser_scratchpad_initialization.js
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_initialization.js
@@ -45,13 +45,10 @@ function runTests()
      "Error console command is disabled");
 
   let chromeContextCommand = gScratchpadWindow.document.
                             getElementById("sp-cmd-browserContext");
   ok(chromeContextCommand, "Chrome context command element exists");
   is(chromeContextCommand.getAttribute("disabled"), "true",
      "Chrome context command is disabled");
 
-  gScratchpadWindow.close();
-  gScratchpadWindow = null;
-  gBrowser.removeCurrentTab();
   finish();
 }
--- a/browser/devtools/scratchpad/test/browser_scratchpad_inspect.js
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_inspect.js
@@ -50,15 +50,12 @@ function runTests()
         break;
       }
     }
     ok(found, "found the document.title property");
 
     executeSoon(function() {
       propPanel.hidePopup();
 
-      gScratchpadWindow.close();
-      gScratchpadWindow = null;
-      gBrowser.removeCurrentTab();
       finish();
     });
   }, false);
 }
--- a/browser/devtools/scratchpad/test/browser_scratchpad_open.js
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_open.js
@@ -23,47 +23,51 @@ function test()
   testOpenWithState();
   testOpenInvalidState();
 }
 
 function testOpen()
 {
   let win = ScratchpadManager.openScratchpad();
 
-  win.addEventListener("load", function() {
+  win.addEventListener("load", function onScratchpadLoad() {
+    win.removeEventListener("load", onScratchpadLoad, false);
+
     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();
-  });
+  }, false);
 }
 
 function testOpenWithState()
 {
   let state = {
     filename: "testfile",
     executionContext: 2,
     text: "test text"
   };
 
   let win = ScratchpadManager.openScratchpad(state);
 
-  win.addEventListener("load", function() {
+  win.addEventListener("load", function onScratchpadLoad() {
+    win.removeEventListener("load", onScratchpadLoad, false);
+
     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();
-  });
+  }, false);
 }
 
 function testOpenInvalidState()
 {
   let state = 7;
 
   let win = ScratchpadManager.openScratchpad(state);
   ok(!win, "no scratchpad opened if state is not an object");
--- a/browser/devtools/scratchpad/test/browser_scratchpad_restore.js
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_restore.js
@@ -44,19 +44,20 @@ function testRestore()
       text: "text3",
       executionContext: 1
     }
   ];
 
   asyncMap(states, function(state, done) {
     // Open some scratchpad windows
     let win = ScratchpadManager.openScratchpad(state);
-    win.addEventListener("load", function() {
+    win.addEventListener("load", function onScratchpadLoad() {
+      removeEventListener("load", onScratchpadLoad, false);
       done(win);
-    })
+    }, false)
   }, function(wins) {
     // Then save the windows to session store
     ScratchpadManager.saveOpenWindows();
 
     // Then get their states
     let session = ScratchpadManager.getSessionState();
 
     // Then close them
@@ -68,21 +69,22 @@ function testRestore()
     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() {
+      restoredWin.addEventListener("load", function onScratchpadLoad() {
+        restoredWin.removeEventListener("load", onScratchpadLoad, false);
         let state = restoredWin.Scratchpad.getState();
         restoredWin.close();
         done(state);
-      });
+      }, false);
     }, 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();
     });
--- a/browser/devtools/scratchpad/test/browser_scratchpad_tab_switch.js
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_tab_switch.js
@@ -94,17 +94,13 @@ function runTests2() {
 
 function runTests3() {
   gBrowser.selectedBrowser.removeEventListener("load", runTests3, true);
   // Check that the sandbox is not cached.
 
   sp.setText("typeof foosbug653108;");
   is(sp.run()[2], "undefined", "global variable does not exist");
 
-  gScratchpadWindow.close();
-  gScratchpadWindow = null;
   tab1 = null;
   tab2 = null;
   sp = null;
-  gBrowser.removeCurrentTab();
-  gBrowser.removeCurrentTab();
   finish();
 }
--- a/browser/devtools/scratchpad/test/browser_scratchpad_ui.js
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_ui.js
@@ -70,13 +70,10 @@ function runTests()
     ok(lastMethodCalled == methodName,
        "method " + methodName + " invoked by the associated menuitem");
 
     sp[methodName] = oldMethod;
   }
 
   delete sp.__noSuchMethod__;
 
-  gScratchpadWindow.close();
-  gScratchpadWindow = null;
-  gBrowser.removeCurrentTab();
   finish();
 }
new file mode 100644
--- /dev/null
+++ b/browser/devtools/scratchpad/test/head.js
@@ -0,0 +1,20 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let gScratchpadWindow; // Reference to the Scratchpad chrome window object
+
+function cleanup()
+{
+  if (gScratchpadWindow) {
+    gScratchpadWindow.close();
+    gScratchpadWindow = null;
+  }
+  while (gBrowser.tabs.length > 1) {
+    gBrowser.removeCurrentTab();
+  }
+}
+
+registerCleanupFunction(cleanup);