summaryrefslogtreecommitdiff
path: root/browser/devtools/scratchpad
diff options
context:
space:
mode:
authorwolfbeast <mcwerewolf@gmail.com>2014-05-21 11:38:25 +0200
committerwolfbeast <mcwerewolf@gmail.com>2014-05-21 11:38:25 +0200
commitd25ba7d760b017b038e5aa6c0a605b4a330eb68d (patch)
tree16ec27edc7d5f83986f16236d3a36a2682a0f37e /browser/devtools/scratchpad
parenta942906574671868daf122284a9c4689e6924f74 (diff)
downloadpalemoon-gre-d25ba7d760b017b038e5aa6c0a605b4a330eb68d.tar.gz
Recommit working copy to repo with proper line endings.
Diffstat (limited to 'browser/devtools/scratchpad')
-rw-r--r--browser/devtools/scratchpad/CmdScratchpad.jsm22
-rw-r--r--browser/devtools/scratchpad/Makefile.in16
-rw-r--r--browser/devtools/scratchpad/moz.build7
-rw-r--r--browser/devtools/scratchpad/scratchpad-manager.jsm166
-rw-r--r--browser/devtools/scratchpad/scratchpad.js1650
-rw-r--r--browser/devtools/scratchpad/scratchpad.xul301
-rw-r--r--browser/devtools/scratchpad/test/Makefile.in44
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_bug650345_find_ui.js97
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_bug684546_reset_undo.js158
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_bug690552_display_outputs_errors.js56
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_bug714942_goto_line_ui.js45
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_bug740948_reload_and_run.js73
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_bug756681_display_non_error_exceptions.js107
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_bug_644413_modeline.js92
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_bug_646070_chrome_context_pref.js51
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_bug_650760_help_key.js60
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_bug_651942_recent_files.js355
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_bug_653427_confirm_close.js227
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_bug_660560_tab.js81
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_bug_661762_wrong_window_focus.js94
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_bug_669612_unsaved.js120
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_bug_679467_falsy.js68
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_bug_699130_edit_ui_updates.js187
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_bug_751744_revert_to_saved.js137
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_contexts.js174
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_execute_print.js138
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_files.js118
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_initialization.js48
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_inspect.js55
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_open.js76
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_restore.js98
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_tab_switch.js103
-rw-r--r--browser/devtools/scratchpad/test/browser_scratchpad_ui.js70
-rw-r--r--browser/devtools/scratchpad/test/head.js197
-rw-r--r--browser/devtools/scratchpad/test/moz.build6
35 files changed, 5297 insertions, 0 deletions
diff --git a/browser/devtools/scratchpad/CmdScratchpad.jsm b/browser/devtools/scratchpad/CmdScratchpad.jsm
new file mode 100644
index 000000000..74e20d7e0
--- /dev/null
+++ b/browser/devtools/scratchpad/CmdScratchpad.jsm
@@ -0,0 +1,22 @@
+/* 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/. */
+
+this.EXPORTED_SYMBOLS = [ ];
+
+Components.utils.import("resource://gre/modules/devtools/gcli.jsm");
+
+/**
+ * 'scratchpad' command
+ */
+gcli.addCommand({
+ name: "scratchpad",
+ buttonId: "command-button-scratchpad",
+ buttonClass: "command-button",
+ tooltipText: gcli.lookup("scratchpadOpenTooltip"),
+ hidden: true,
+ exec: function(args, context) {
+ let chromeWindow = context.environment.chromeDocument.defaultView;
+ chromeWindow.Scratchpad.ScratchpadManager.openScratchpad();
+ }
+});
diff --git a/browser/devtools/scratchpad/Makefile.in b/browser/devtools/scratchpad/Makefile.in
new file mode 100644
index 000000000..8890154f7
--- /dev/null
+++ b/browser/devtools/scratchpad/Makefile.in
@@ -0,0 +1,16 @@
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DEPTH = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+include $(topsrcdir)/config/rules.mk
+
+libs::
+ $(NSINSTALL) $(srcdir)/*.jsm $(FINAL_TARGET)/modules/devtools
diff --git a/browser/devtools/scratchpad/moz.build b/browser/devtools/scratchpad/moz.build
new file mode 100644
index 000000000..86ec46748
--- /dev/null
+++ b/browser/devtools/scratchpad/moz.build
@@ -0,0 +1,7 @@
+# -*- 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/.
+
+TEST_DIRS += ['test']
diff --git a/browser/devtools/scratchpad/scratchpad-manager.jsm b/browser/devtools/scratchpad/scratchpad-manager.jsm
new file mode 100644
index 000000000..c49e642dd
--- /dev/null
+++ b/browser/devtools/scratchpad/scratchpad-manager.jsm
@@ -0,0 +1,166 @@
+/* vim:set ts=2 sw=2 sts=2 et tw=80:
+ * 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";
+
+this.EXPORTED_SYMBOLS = ["ScratchpadManager"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+const SCRATCHPAD_WINDOW_URL = "chrome://browser/content/devtools/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.
+ */
+this.ScratchpadManager = {
+
+ _nextUid: 1,
+ _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 = [];
+
+ function clone(src) {
+ let dest = {};
+
+ for (let key in src) {
+ if (src.hasOwnProperty(key)) {
+ dest[key] = src[key];
+ }
+ }
+
+ return dest;
+ }
+
+ // We need to clone objects we get from Scratchpad instances
+ // because such (cross-window) objects have a property 'parent'
+ // that holds on to a ChromeWindow instance. This means that
+ // such objects are not primitive-values-only anymore so they
+ // can leak.
+
+ let enumerator = Services.wm.getEnumerator("devtools:scratchpad");
+ while (enumerator.hasMoreElements()) {
+ let win = enumerator.getNext();
+ if (!win.closed && win.Scratchpad.initialized) {
+ this._scratchpads.push(clone(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 = Cc["@mozilla.org/embedcomp/dialogparam;1"]
+ .createInstance(Ci.nsIDialogParamBlock);
+
+ params.SetNumberStrings(2);
+ params.SetString(0, JSON.stringify(this._nextUid++));
+
+ if (aState) {
+ if (typeof aState != 'object') {
+ return;
+ }
+
+ params.SetString(1, JSON.stringify(aState));
+ }
+
+ let win = Services.ww.openWindow(null, SCRATCHPAD_WINDOW_URL, "_blank",
+ SCRATCHPAD_WINDOW_FEATURES, params);
+
+ // Only add the 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");
+ }
+}; \ No newline at end of file
diff --git a/browser/devtools/scratchpad/scratchpad.js b/browser/devtools/scratchpad/scratchpad.js
new file mode 100644
index 000000000..08a0bc2a5
--- /dev/null
+++ b/browser/devtools/scratchpad/scratchpad.js
@@ -0,0 +1,1650 @@
+/* vim:set ts=2 sw=2 sts=2 et:
+ * 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/. */
+
+/*
+ * Original version history can be found here:
+ * https://github.com/mozilla/workspace
+ *
+ * Copied and relicensed from the Public Domain.
+ * See bug 653934 for details.
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=653934
+ */
+
+"use strict";
+
+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/source-editor.jsm");
+Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
+Cu.import("resource:///modules/devtools/scratchpad-manager.jsm");
+Cu.import("resource://gre/modules/jsdebugger.jsm");
+Cu.import("resource:///modules/devtools/gDevTools.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
+
+XPCOMUtils.defineLazyModuleGetter(this, "VariablesView",
+ "resource:///modules/devtools/VariablesView.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "devtools",
+ "resource://gre/modules/devtools/Loader.jsm");
+
+let Telemetry = devtools.require("devtools/shared/telemetry");
+
+const SCRATCHPAD_CONTEXT_CONTENT = 1;
+const SCRATCHPAD_CONTEXT_BROWSER = 2;
+const SCRATCHPAD_L10N = "chrome://browser/locale/devtools/scratchpad.properties";
+const DEVTOOLS_CHROME_ENABLED = "devtools.chrome.enabled";
+const PREF_RECENT_FILES_MAX = "devtools.scratchpad.recentFilesMax";
+const BUTTON_POSITION_SAVE = 0;
+const BUTTON_POSITION_CANCEL = 1;
+const BUTTON_POSITION_DONT_SAVE = 2;
+const BUTTON_POSITION_REVERT = 0;
+const VARIABLES_VIEW_URL = "chrome://browser/content/devtools/widgets/VariablesView.xul";
+
+// Because we have no constructor / destructor where we can log metrics we need
+// to do so here.
+let telemetry = new Telemetry();
+telemetry.toolOpened("scratchpad");
+
+/**
+ * The scratchpad object handles the Scratchpad window functionality.
+ */
+var Scratchpad = {
+ _instanceId: null,
+ _initialWindowTitle: document.title,
+
+ /**
+ * Check if provided string is a mode-line and, if it is, return an
+ * object with its values.
+ *
+ * @param string aLine
+ * @return string
+ */
+ _scanModeLine: function SP__scanModeLine(aLine="")
+ {
+ aLine = aLine.trim();
+
+ let obj = {};
+ let ch1 = aLine.charAt(0);
+ let ch2 = aLine.charAt(1);
+
+ if (ch1 !== "/" || (ch2 !== "*" && ch2 !== "/")) {
+ return obj;
+ }
+
+ aLine = aLine
+ .replace(/^\/\//, "")
+ .replace(/^\/\*/, "")
+ .replace(/\*\/$/, "");
+
+ aLine.split(",").forEach(pair => {
+ let [key, val] = pair.split(":");
+
+ if (key && val) {
+ obj[key.trim()] = val.trim();
+ }
+ });
+
+ return obj;
+ },
+
+ /**
+ * The script execution context. This tells Scratchpad in which context the
+ * script shall execute.
+ *
+ * Possible values:
+ * - SCRATCHPAD_CONTEXT_CONTENT to execute code in the context of the current
+ * tab content window object.
+ * - SCRATCHPAD_CONTEXT_BROWSER to execute code in the context of the
+ * currently active chrome window object.
+ */
+ executionContext: SCRATCHPAD_CONTEXT_CONTENT,
+
+ /**
+ * Tells if this Scratchpad is initialized and ready for use.
+ * @boolean
+ * @see addObserver
+ */
+ initialized: false,
+
+ /**
+ * Retrieve the xul:notificationbox DOM element. It notifies the user when
+ * the current code execution context is SCRATCHPAD_CONTEXT_BROWSER.
+ */
+ get notificationBox() document.getElementById("scratchpad-notificationbox"),
+
+ /**
+ * Get the selected text from the editor.
+ *
+ * @return string
+ * The selected text.
+ */
+ get selectedText() this.editor.getSelectedText(),
+
+ /**
+ * Get the editor content, in the given range. If no range is given you get
+ * the entire editor content.
+ *
+ * @param number [aStart=0]
+ * Optional, start from the given offset.
+ * @param number [aEnd=content char count]
+ * Optional, end offset for the text you want. If this parameter is not
+ * given, then the text returned goes until the end of the editor
+ * content.
+ * @return string
+ * The text in the given range.
+ */
+ getText: function SP_getText(aStart, aEnd)
+ {
+ return this.editor.getText(aStart, aEnd);
+ },
+
+ /**
+ * Replace text in the source editor with the given text, in the given range.
+ *
+ * @param string aText
+ * The text you want to put into the editor.
+ * @param number [aStart=0]
+ * Optional, the start offset, zero based, from where you want to start
+ * replacing text in the editor.
+ * @param number [aEnd=char count]
+ * Optional, the end offset, zero based, where you want to stop
+ * 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)
+ {
+ this.filename = aFilename;
+ this._updateTitle();
+ },
+
+ /**
+ * Update the Scratchpad window title based on the current state.
+ * @private
+ */
+ _updateTitle: function SP__updateTitle()
+ {
+ let title = this.filename || this._initialWindowTitle;
+
+ if (this.editor && this.editor.dirty) {
+ title = "*" + title;
+ }
+
+ document.title = title;
+ },
+
+ /**
+ * 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,
+ saved: !this.editor.dirty,
+ };
+ },
+
+ /**
+ * 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_setState(aState)
+ {
+ if (aState.filename) {
+ this.setFilename(aState.filename);
+ }
+ if (this.editor) {
+ this.editor.dirty = !aState.saved;
+ }
+
+ 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.
+ */
+ _previousWindow: null,
+
+ /**
+ * Get the gBrowser object of the most recent browser window.
+ */
+ get gBrowser()
+ {
+ let recentWin = this.browserWindow;
+ return recentWin ? recentWin.gBrowser : null;
+ },
+
+ /**
+ * Cached Cu.Sandbox object for the active tab content window object.
+ */
+ _contentSandbox: null,
+
+ /**
+ * Unique name for the current Scratchpad instance. Used to distinguish
+ * Scratchpad windows between each other. See bug 661762.
+ */
+ get uniqueName()
+ {
+ return "Scratchpad/" + this._instanceId;
+ },
+
+
+ /**
+ * Sidebar that contains the VariablesView for object inspection.
+ */
+ get sidebar()
+ {
+ if (!this._sidebar) {
+ this._sidebar = new ScratchpadSidebar(this);
+ }
+ return this._sidebar;
+ },
+
+ /**
+ * Get the Cu.Sandbox object for the active tab content window object. Note
+ * that the returned object is cached for later reuse. The cached object is
+ * kept only for the current location in the current tab of the current
+ * browser window and it is reset for each context switch,
+ * navigator:browser window switch, tab switch or navigation.
+ */
+ get contentSandbox()
+ {
+ if (!this.browserWindow) {
+ Cu.reportError(this.strings.
+ GetStringFromName("browserWindow.unavailable"));
+ return;
+ }
+
+ if (!this._contentSandbox ||
+ this.browserWindow != this._previousBrowserWindow ||
+ this._previousBrowser != this.gBrowser.selectedBrowser ||
+ this._previousLocation != this.gBrowser.contentWindow.location.href) {
+ let contentWindow = this.gBrowser.selectedBrowser.contentWindow;
+ this._contentSandbox = new Cu.Sandbox(contentWindow,
+ { sandboxPrototype: contentWindow, wantXrays: false,
+ sandboxName: 'scratchpad-content'});
+ this._contentSandbox.__SCRATCHPAD__ = this;
+
+ this._previousBrowserWindow = this.browserWindow;
+ this._previousBrowser = this.gBrowser.selectedBrowser;
+ this._previousLocation = contentWindow.location.href;
+ }
+
+ return this._contentSandbox;
+ },
+
+ /**
+ * Cached Cu.Sandbox object for the most recently active navigator:browser
+ * chrome window object.
+ */
+ _chromeSandbox: null,
+
+ /**
+ * Get the Cu.Sandbox object for the most recently active navigator:browser
+ * chrome window object. Note that the returned object is cached for later
+ * reuse. The cached object is kept only for the current browser window and it
+ * is reset for each context switch or navigator:browser window switch.
+ */
+ get chromeSandbox()
+ {
+ if (!this.browserWindow) {
+ Cu.reportError(this.strings.
+ GetStringFromName("browserWindow.unavailable"));
+ return;
+ }
+
+ if (!this._chromeSandbox ||
+ this.browserWindow != this._previousBrowserWindow) {
+ this._chromeSandbox = new Cu.Sandbox(this.browserWindow,
+ { sandboxPrototype: this.browserWindow, wantXrays: false,
+ sandboxName: 'scratchpad-chrome'});
+ this._chromeSandbox.__SCRATCHPAD__ = this;
+ addDebuggerToGlobal(this._chromeSandbox);
+
+ this._previousBrowserWindow = this.browserWindow;
+ }
+
+ return this._chromeSandbox;
+ },
+
+ /**
+ * Drop the editor selection.
+ */
+ deselect: function SP_deselect()
+ {
+ this.editor.dropSelection();
+ },
+
+ /**
+ * Select a specific range in the Scratchpad editor.
+ *
+ * @param number aStart
+ * Selection range start.
+ * @param number aEnd
+ * Selection range end.
+ */
+ selectRange: function SP_selectRange(aStart, aEnd)
+ {
+ this.editor.setSelection(aStart, aEnd);
+ },
+
+ /**
+ * Get the current selection range.
+ *
+ * @return object
+ * An object with two properties, start and end, that give the
+ * selection range (zero based offsets).
+ */
+ getSelectionRange: function SP_getSelection()
+ {
+ return this.editor.getSelection();
+ },
+
+ /**
+ * Evaluate a string in the currently desired context, that is either the
+ * chrome window or the tab content window object.
+ *
+ * @param string aString
+ * The script you want to evaluate.
+ * @return Promise
+ * The promise for the script evaluation result.
+ */
+ evalForContext: function SP_evaluateForContext(aString)
+ {
+ let deferred = Promise.defer();
+
+ // This setTimeout is temporary and will be replaced by DebuggerClient
+ // execution in a future patch (bug 825039). The purpose for using
+ // setTimeout is to ensure there is no accidental dependency on the
+ // promise being resolved synchronously, which can cause subtle bugs.
+ setTimeout(() => {
+ let chrome = this.executionContext != SCRATCHPAD_CONTEXT_CONTENT;
+ let sandbox = chrome ? this.chromeSandbox : this.contentSandbox;
+ let name = this.uniqueName;
+
+ try {
+ let result = Cu.evalInSandbox(aString, sandbox, "1.8", name, 1);
+ deferred.resolve([aString, undefined, result]);
+ }
+ catch (ex) {
+ deferred.resolve([aString, ex]);
+ }
+ }, 0);
+
+ return deferred.promise;
+ },
+
+ /**
+ * Execute the selected text (if any) or the entire editor content in the
+ * current context.
+ *
+ * @return Promise
+ * The promise for the script evaluation result.
+ */
+ execute: function SP_execute()
+ {
+ let selection = this.selectedText || this.getText();
+ return this.evalForContext(selection);
+ },
+
+ /**
+ * Execute the selected text (if any) or the entire editor content in the
+ * current context.
+ *
+ * @return Promise
+ * The promise for the script evaluation result.
+ */
+ run: function SP_run()
+ {
+ let promise = this.execute();
+ promise.then(([, aError, ]) => {
+ if (aError) {
+ this.writeAsErrorComment(aError);
+ }
+ else {
+ this.deselect();
+ }
+ });
+ return promise;
+ },
+
+ /**
+ * Execute the selected text (if any) or the entire editor content in the
+ * current context. If the result is primitive then it is written as a
+ * comment. Otherwise, the resulting object is inspected up in the sidebar.
+ *
+ * @return Promise
+ * The promise for the script evaluation result.
+ */
+ inspect: function SP_inspect()
+ {
+ let deferred = Promise.defer();
+ let reject = aReason => deferred.reject(aReason);
+
+ this.execute().then(([aString, aError, aResult]) => {
+ let resolve = () => deferred.resolve([aString, aError, aResult]);
+
+ if (aError) {
+ this.writeAsErrorComment(aError);
+ resolve();
+ }
+ else if (!isObject(aResult)) {
+ this.writeAsComment(aResult);
+ resolve();
+ }
+ else {
+ this.deselect();
+ this.sidebar.open(aString, aResult).then(resolve, reject);
+ }
+ }, reject);
+
+ return deferred.promise;
+ },
+
+ /**
+ * Reload the current page and execute the entire editor content when
+ * the page finishes loading. Note that this operation should be available
+ * only in the content context.
+ *
+ * @return Promise
+ * The promise for the script evaluation result.
+ */
+ reloadAndRun: function SP_reloadAndRun()
+ {
+ let deferred = Promise.defer();
+
+ if (this.executionContext !== SCRATCHPAD_CONTEXT_CONTENT) {
+ Cu.reportError(this.strings.
+ GetStringFromName("scratchpadContext.invalid"));
+ return;
+ }
+
+ let browser = this.gBrowser.selectedBrowser;
+
+ this._reloadAndRunEvent = evt => {
+ if (evt.target !== browser.contentDocument) {
+ return;
+ }
+
+ browser.removeEventListener("load", this._reloadAndRunEvent, true);
+
+ this.run().then(aResults => deferred.resolve(aResults));
+ };
+
+ browser.addEventListener("load", this._reloadAndRunEvent, true);
+ browser.contentWindow.location.reload();
+
+ return deferred.promise;
+ },
+
+ /**
+ * Execute the selected text (if any) or the entire editor content in the
+ * current context. The evaluation result is inserted into the editor after
+ * the selected text, or at the end of the editor content if there is no
+ * selected text.
+ *
+ * @return Promise
+ * The promise for the script evaluation result.
+ */
+ display: function SP_display()
+ {
+ let promise = this.execute();
+ promise.then(([aString, aError, aResult]) => {
+ if (aError) {
+ this.writeAsErrorComment(aError);
+ }
+ else {
+ this.writeAsComment(aResult);
+ }
+ });
+ return promise;
+ },
+
+ /**
+ * Write out a value at the next line from the current insertion point.
+ * The comment block will always be preceded by a newline character.
+ * @param object aValue
+ * The Object to write out as a string
+ */
+ writeAsComment: function SP_writeAsComment(aValue)
+ {
+ let selection = this.getSelectionRange();
+ let insertionPoint = selection.start != selection.end ?
+ selection.end : // after selected text
+ this.editor.getCharCount(); // after text end
+
+ let newComment = "\n/*\n" + aValue + "\n*/";
+
+ this.setText(newComment, insertionPoint, insertionPoint);
+
+ // Select the new comment.
+ this.selectRange(insertionPoint, insertionPoint + newComment.length);
+ },
+
+ /**
+ * Write out an error at the current insertion point as a block comment
+ * @param object aValue
+ * The Error object to write out the message and stack trace
+ */
+ writeAsErrorComment: function SP_writeAsErrorComment(aError)
+ {
+ let stack = "";
+ if (aError.stack) {
+ stack = aError.stack;
+ }
+ else if (aError.fileName) {
+ if (aError.lineNumber) {
+ stack = "@" + aError.fileName + ":" + aError.lineNumber;
+ }
+ else {
+ stack = "@" + aError.fileName;
+ }
+ }
+ else if (aError.lineNumber) {
+ stack = "@" + aError.lineNumber;
+ }
+
+ let newComment = "Exception: " + ( aError.message || aError) + ( stack == "" ? stack : "\n" + stack.replace(/\n$/, "") );
+
+ this.writeAsComment(newComment);
+ },
+
+ // Menu Operations
+
+ /**
+ * Open a new Scratchpad window.
+ *
+ * @return nsIWindow
+ */
+ openScratchpad: function SP_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
+ * If the file already exists, ask for confirmation?
+ * @param boolean aSilentError
+ * True if you do not want to display an error when file save fails,
+ * false otherwise.
+ * @param function aCallback
+ * Optional function you want to call when file save completes. It will
+ * get the following arguments:
+ * 1) the nsresult status code for the export operation.
+ */
+ exportToFile: function SP_exportToFile(aFile, aNoConfirmation, aSilentError,
+ aCallback)
+ {
+ if (!aNoConfirmation && aFile.exists() &&
+ !window.confirm(this.strings.
+ GetStringFromName("export.fileOverwriteConfirmation"))) {
+ return;
+ }
+
+ let encoder = new TextEncoder();
+ let buffer = encoder.encode(this.getText());
+ let promise = OS.File.writeAtomic(aFile.path, buffer,{tmpPath: aFile.path + ".tmp"});
+ promise.then(value => {
+ if (aCallback) {
+ aCallback.call(this, Components.results.NS_OK);
+ }
+ }, reason => {
+ if (!aSilentError) {
+ window.alert(this.strings.GetStringFromName("saveFile.failed"));
+ }
+ if (aCallback) {
+ aCallback.call(this, Components.results.NS_ERROR_UNEXPECTED);
+ }
+ });
+
+ },
+
+ /**
+ * Read the content of a file and put it into the textbox.
+ *
+ * @param nsILocalFile aFile
+ * The file you want to save the textbox content into.
+ * @param boolean aSilentError
+ * True if you do not want to display an error when file load fails,
+ * false otherwise.
+ * @param function aCallback
+ * Optional function you want to call when file load completes. It will
+ * get the following arguments:
+ * 1) the nsresult status code for the import operation.
+ * 2) the data that was read from the file, if any.
+ */
+ importFromFile: function SP_importFromFile(aFile, aSilentError, aCallback)
+ {
+ // Prevent file type detection.
+ let channel = NetUtil.newChannel(aFile);
+ channel.contentType = "application/javascript";
+
+ NetUtil.asyncFetch(channel, (aInputStream, aStatus) => {
+ let content = null;
+
+ if (Components.isSuccessCode(aStatus)) {
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
+ createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ content = NetUtil.readInputStreamToString(aInputStream,
+ aInputStream.available());
+ content = converter.ConvertToUnicode(content);
+
+ // Check to see if the first line is a mode-line comment.
+ let line = content.split("\n")[0];
+ let modeline = this._scanModeLine(line);
+ let chrome = Services.prefs.getBoolPref(DEVTOOLS_CHROME_ENABLED);
+
+ if (chrome && modeline["-sp-context"] === "browser") {
+ this.setBrowserContext();
+ }
+
+ this.setText(content);
+ this.editor.resetUndo();
+ }
+ else if (!aSilentError) {
+ window.alert(this.strings.GetStringFromName("openFile.failed"));
+ }
+
+ if (aCallback) {
+ aCallback.call(this, aStatus, content);
+ }
+ });
+ },
+
+ /**
+ * Open a file to edit in the Scratchpad.
+ *
+ * @param integer aIndex
+ * Optional integer: clicked menuitem in the 'Open Recent'-menu.
+ */
+ openFile: function SP_openFile(aIndex)
+ {
+ let promptCallback = aFile => {
+ this.promptSave((aCloseFile, aSaved, aStatus) => {
+ let shouldOpen = aCloseFile;
+ if (aSaved && !Components.isSuccessCode(aStatus)) {
+ shouldOpen = false;
+ }
+
+ if (shouldOpen) {
+ let file;
+ if (aFile) {
+ file = aFile;
+ } else {
+ file = Components.classes["@mozilla.org/file/local;1"].
+ createInstance(Components.interfaces.nsILocalFile);
+ let filePath = this.getRecentFiles()[aIndex];
+ file.initWithPath(filePath);
+ }
+
+ if (!file.exists()) {
+ this.notificationBox.appendNotification(
+ this.strings.GetStringFromName("fileNoLongerExists.notification"),
+ "file-no-longer-exists",
+ null,
+ this.notificationBox.PRIORITY_WARNING_HIGH,
+ null);
+
+ this.clearFiles(aIndex, 1);
+ return;
+ }
+
+ this.setFilename(file.path);
+ this.importFromFile(file, false);
+ this.setRecentFile(file);
+ }
+ });
+ };
+
+ if (aIndex > -1) {
+ promptCallback();
+ } else {
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ fp.init(window, this.strings.GetStringFromName("openFile.title"),
+ Ci.nsIFilePicker.modeOpen);
+ fp.defaultString = "";
+ fp.open(aResult => {
+ if (aResult != Ci.nsIFilePicker.returnCancel) {
+ promptCallback(fp.file);
+ }
+ });
+ }
+ },
+
+ /**
+ * Get recent files.
+ *
+ * @return Array
+ * File paths.
+ */
+ getRecentFiles: function SP_getRecentFiles()
+ {
+ let branch = Services.prefs.getBranch("devtools.scratchpad.");
+ let filePaths = [];
+
+ // WARNING: Do not use getCharPref here, it doesn't play nicely with
+ // Unicode strings.
+
+ if (branch.prefHasUserValue("recentFilePaths")) {
+ let data = branch.getComplexValue("recentFilePaths",
+ Ci.nsISupportsString).data;
+ filePaths = JSON.parse(data);
+ }
+
+ return filePaths;
+ },
+
+ /**
+ * Save a recent file in a JSON parsable string.
+ *
+ * @param nsILocalFile aFile
+ * The nsILocalFile we want to save as a recent file.
+ */
+ setRecentFile: function SP_setRecentFile(aFile)
+ {
+ let maxRecent = Services.prefs.getIntPref(PREF_RECENT_FILES_MAX);
+ if (maxRecent < 1) {
+ return;
+ }
+
+ let filePaths = this.getRecentFiles();
+ let filesCount = filePaths.length;
+ let pathIndex = filePaths.indexOf(aFile.path);
+
+ // We are already storing this file in the list of recent files.
+ if (pathIndex > -1) {
+ // If it's already the most recent file, we don't have to do anything.
+ if (pathIndex === (filesCount - 1)) {
+ // Updating the menu to clear the disabled state from the wrong menuitem
+ // in rare cases when two or more Scratchpad windows are open and the
+ // same file has been opened in two or more windows.
+ this.populateRecentFilesMenu();
+ return;
+ }
+
+ // It is not the most recent file. Remove it from the list, we add it as
+ // the most recent farther down.
+ filePaths.splice(pathIndex, 1);
+ }
+ // If we are not storing the file and the 'recent files'-list is full,
+ // remove the oldest file from the list.
+ else if (filesCount === maxRecent) {
+ filePaths.shift();
+ }
+
+ filePaths.push(aFile.path);
+
+ // WARNING: Do not use setCharPref here, it doesn't play nicely with
+ // Unicode strings.
+
+ let str = Cc["@mozilla.org/supports-string;1"]
+ .createInstance(Ci.nsISupportsString);
+ str.data = JSON.stringify(filePaths);
+
+ let branch = Services.prefs.getBranch("devtools.scratchpad.");
+ branch.setComplexValue("recentFilePaths",
+ Ci.nsISupportsString, str);
+ },
+
+ /**
+ * Populates the 'Open Recent'-menu.
+ */
+ populateRecentFilesMenu: function SP_populateRecentFilesMenu()
+ {
+ let maxRecent = Services.prefs.getIntPref(PREF_RECENT_FILES_MAX);
+ let recentFilesMenu = document.getElementById("sp-open_recent-menu");
+
+ if (maxRecent < 1) {
+ recentFilesMenu.setAttribute("hidden", true);
+ return;
+ }
+
+ let recentFilesPopup = recentFilesMenu.firstChild;
+ let filePaths = this.getRecentFiles();
+ let filename = this.getState().filename;
+
+ recentFilesMenu.setAttribute("disabled", true);
+ while (recentFilesPopup.hasChildNodes()) {
+ recentFilesPopup.removeChild(recentFilesPopup.firstChild);
+ }
+
+ if (filePaths.length > 0) {
+ recentFilesMenu.removeAttribute("disabled");
+
+ // Print out menuitems with the most recent file first.
+ for (let i = filePaths.length - 1; i >= 0; --i) {
+ let menuitem = document.createElement("menuitem");
+ menuitem.setAttribute("type", "radio");
+ menuitem.setAttribute("label", filePaths[i]);
+
+ if (filePaths[i] === filename) {
+ menuitem.setAttribute("checked", true);
+ menuitem.setAttribute("disabled", true);
+ }
+
+ menuitem.setAttribute("oncommand", "Scratchpad.openFile(" + i + ");");
+ recentFilesPopup.appendChild(menuitem);
+ }
+
+ recentFilesPopup.appendChild(document.createElement("menuseparator"));
+ let clearItems = document.createElement("menuitem");
+ clearItems.setAttribute("id", "sp-menu-clear_recent");
+ clearItems.setAttribute("label",
+ this.strings.
+ GetStringFromName("clearRecentMenuItems.label"));
+ clearItems.setAttribute("command", "sp-cmd-clearRecentFiles");
+ recentFilesPopup.appendChild(clearItems);
+ }
+ },
+
+ /**
+ * Clear a range of files from the list.
+ *
+ * @param integer aIndex
+ * Index of file in menu to remove.
+ * @param integer aLength
+ * Number of files from the index 'aIndex' to remove.
+ */
+ clearFiles: function SP_clearFile(aIndex, aLength)
+ {
+ let filePaths = this.getRecentFiles();
+ filePaths.splice(aIndex, aLength);
+
+ // WARNING: Do not use setCharPref here, it doesn't play nicely with
+ // Unicode strings.
+
+ let str = Cc["@mozilla.org/supports-string;1"]
+ .createInstance(Ci.nsISupportsString);
+ str.data = JSON.stringify(filePaths);
+
+ let branch = Services.prefs.getBranch("devtools.scratchpad.");
+ branch.setComplexValue("recentFilePaths",
+ Ci.nsISupportsString, str);
+ },
+
+ /**
+ * Clear all recent files.
+ */
+ clearRecentFiles: function SP_clearRecentFiles()
+ {
+ Services.prefs.clearUserPref("devtools.scratchpad.recentFilePaths");
+ },
+
+ /**
+ * Handle changes to the 'PREF_RECENT_FILES_MAX'-preference.
+ */
+ handleRecentFileMaxChange: function SP_handleRecentFileMaxChange()
+ {
+ let maxRecent = Services.prefs.getIntPref(PREF_RECENT_FILES_MAX);
+ let menu = document.getElementById("sp-open_recent-menu");
+
+ // Hide the menu if the 'PREF_RECENT_FILES_MAX'-pref is set to zero or less.
+ if (maxRecent < 1) {
+ menu.setAttribute("hidden", true);
+ } else {
+ if (menu.hasAttribute("hidden")) {
+ if (!menu.firstChild.hasChildNodes()) {
+ this.populateRecentFilesMenu();
+ }
+
+ menu.removeAttribute("hidden");
+ }
+
+ let filePaths = this.getRecentFiles();
+ if (maxRecent < filePaths.length) {
+ let diff = filePaths.length - maxRecent;
+ this.clearFiles(0, diff);
+ }
+ }
+ },
+ /**
+ * Save the textbox content to the currently open file.
+ *
+ * @param function aCallback
+ * Optional function you want to call when file is saved
+ */
+ saveFile: function SP_saveFile(aCallback)
+ {
+ if (!this.filename) {
+ return this.saveFileAs(aCallback);
+ }
+
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
+ file.initWithPath(this.filename);
+
+ this.exportToFile(file, true, false, aStatus => {
+ if (Components.isSuccessCode(aStatus)) {
+ this.editor.dirty = false;
+ this.setRecentFile(file);
+ }
+ if (aCallback) {
+ aCallback(aStatus);
+ }
+ });
+ },
+
+ /**
+ * Save the textbox content to a new file.
+ *
+ * @param function aCallback
+ * Optional function you want to call when file is saved
+ */
+ saveFileAs: function SP_saveFileAs(aCallback)
+ {
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ let fpCallback = aResult => {
+ if (aResult != Ci.nsIFilePicker.returnCancel) {
+ this.setFilename(fp.file.path);
+ this.exportToFile(fp.file, true, false, aStatus => {
+ if (Components.isSuccessCode(aStatus)) {
+ this.editor.dirty = false;
+ this.setRecentFile(fp.file);
+ }
+ if (aCallback) {
+ aCallback(aStatus);
+ }
+ });
+ }
+ };
+
+ fp.init(window, this.strings.GetStringFromName("saveFileAs"),
+ Ci.nsIFilePicker.modeSave);
+ fp.defaultString = "scratchpad.js";
+ fp.open(fpCallback);
+ },
+
+ /**
+ * Restore content from saved version of current file.
+ *
+ * @param function aCallback
+ * Optional function you want to call when file is saved
+ */
+ revertFile: function SP_revertFile(aCallback)
+ {
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
+ file.initWithPath(this.filename);
+
+ if (!file.exists()) {
+ return;
+ }
+
+ this.importFromFile(file, false, (aStatus, aContent) => {
+ if (aCallback) {
+ aCallback(aStatus);
+ }
+ });
+ },
+
+ /**
+ * Prompt to revert scratchpad if it has unsaved changes.
+ *
+ * @param function aCallback
+ * Optional function you want to call when file is saved. The callback
+ * receives three arguments:
+ * - aRevert (boolean) - tells if the file has been reverted.
+ * - status (number) - the file revert status result (if the file was
+ * saved).
+ */
+ promptRevert: function SP_promptRervert(aCallback)
+ {
+ if (this.filename) {
+ let ps = Services.prompt;
+ let flags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_REVERT +
+ ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL;
+
+ let button = ps.confirmEx(window,
+ this.strings.GetStringFromName("confirmRevert.title"),
+ this.strings.GetStringFromName("confirmRevert"),
+ flags, null, null, null, null, {});
+ if (button == BUTTON_POSITION_CANCEL) {
+ if (aCallback) {
+ aCallback(false);
+ }
+
+ return;
+ }
+ if (button == BUTTON_POSITION_REVERT) {
+ this.revertFile(aStatus => {
+ if (aCallback) {
+ aCallback(true, aStatus);
+ }
+ });
+
+ return;
+ }
+ }
+ if (aCallback) {
+ aCallback(false);
+ }
+ },
+
+ /**
+ * Open the Error Console.
+ */
+ openErrorConsole: function SP_openErrorConsole()
+ {
+ this.browserWindow.HUDConsoleUI.toggleBrowserConsole();
+ },
+
+ /**
+ * Open the Web Console.
+ */
+ openWebConsole: function SP_openWebConsole()
+ {
+ let target = devtools.TargetFactory.forTab(this.gBrowser.selectedTab);
+ gDevTools.showToolbox(target, "webconsole");
+ this.browserWindow.focus();
+ },
+
+ /**
+ * Set the current execution context to be the active tab content window.
+ */
+ setContentContext: function SP_setContentContext()
+ {
+ if (this.executionContext == SCRATCHPAD_CONTEXT_CONTENT) {
+ return;
+ }
+
+ let content = document.getElementById("sp-menu-content");
+ document.getElementById("sp-menu-browser").removeAttribute("checked");
+ document.getElementById("sp-cmd-reloadAndRun").removeAttribute("disabled");
+ content.setAttribute("checked", true);
+ this.executionContext = SCRATCHPAD_CONTEXT_CONTENT;
+ this.notificationBox.removeAllNotifications(false);
+ this.resetContext();
+ },
+
+ /**
+ * Set the current execution context to be the most recent chrome window.
+ */
+ setBrowserContext: function SP_setBrowserContext()
+ {
+ if (this.executionContext == SCRATCHPAD_CONTEXT_BROWSER) {
+ return;
+ }
+
+ let browser = document.getElementById("sp-menu-browser");
+ let reloadAndRun = document.getElementById("sp-cmd-reloadAndRun");
+
+ document.getElementById("sp-menu-content").removeAttribute("checked");
+ reloadAndRun.setAttribute("disabled", true);
+ browser.setAttribute("checked", true);
+
+ this.executionContext = SCRATCHPAD_CONTEXT_BROWSER;
+ this.notificationBox.appendNotification(
+ this.strings.GetStringFromName("browserContext.notification"),
+ SCRATCHPAD_CONTEXT_BROWSER,
+ null,
+ this.notificationBox.PRIORITY_WARNING_HIGH,
+ null);
+ this.resetContext();
+ },
+
+ /**
+ * Reset the cached Cu.Sandbox object for the current context.
+ */
+ resetContext: function SP_resetContext()
+ {
+ this._chromeSandbox = null;
+ this._contentSandbox = null;
+ this._previousWindow = null;
+ this._previousBrowser = null;
+ this._previousLocation = null;
+ },
+
+ /**
+ * Gets the ID of the inner window of the given DOM window object.
+ *
+ * @param nsIDOMWindow aWindow
+ * @return integer
+ * the inner window ID
+ */
+ getInnerWindowId: function SP_getInnerWindowId(aWindow)
+ {
+ return aWindow.QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
+ },
+
+ /**
+ * The Scratchpad window load event handler. This method
+ * initializes the Scratchpad window and source editor.
+ *
+ * @param nsIDOMEvent aEvent
+ */
+ onLoad: function SP_onLoad(aEvent)
+ {
+ if (aEvent.target != document) {
+ return;
+ }
+
+ let chrome = Services.prefs.getBoolPref(DEVTOOLS_CHROME_ENABLED);
+ if (chrome) {
+ 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.formatStringFromName(
+ "scratchpadIntro1",
+ [LayoutHelpers.prettyKey(document.getElementById("sp-key-run")),
+ LayoutHelpers.prettyKey(document.getElementById("sp-key-inspect")),
+ LayoutHelpers.prettyKey(document.getElementById("sp-key-display"))],
+ 3);
+
+ let args = window.arguments;
+
+ if (args && args[0] instanceof Ci.nsIDialogParamBlock) {
+ args = args[0];
+ } else {
+ // If this Scratchpad window doesn't have any arguments, horrible
+ // things might happen so we need to report an error.
+ Cu.reportError(this.strings. GetStringFromName("scratchpad.noargs"));
+ }
+
+ this._instanceId = args.GetString(0);
+
+ let state = args.GetString(1) || null;
+ if (state) {
+ state = JSON.parse(state);
+ this.setState(state);
+ initialText = state.text;
+ }
+
+ this.editor = new SourceEditor();
+
+ let config = {
+ mode: SourceEditor.MODES.JAVASCRIPT,
+ showLineNumbers: true,
+ initialText: initialText,
+ contextMenu: "scratchpad-text-popup",
+ };
+
+ let editorPlaceholder = document.getElementById("scratchpad-editor");
+ this.editor.init(editorPlaceholder, config,
+ this._onEditorLoad.bind(this, state));
+ },
+
+ /**
+ * The load event handler for the source editor. This method does post-load
+ * editor initialization.
+ *
+ * @private
+ * @param object aState
+ * The initial Scratchpad state object.
+ */
+ _onEditorLoad: function SP__onEditorLoad(aState)
+ {
+ this.editor.addEventListener(SourceEditor.EVENTS.DIRTY_CHANGED,
+ this._onDirtyChanged);
+ this.editor.focus();
+ this.editor.setCaretOffset(this.editor.getCharCount());
+ if (aState) {
+ this.editor.dirty = !aState.saved;
+ }
+
+ this.initialized = true;
+
+ this._triggerObservers("Ready");
+
+ this.populateRecentFilesMenu();
+ PreferenceObserver.init();
+ },
+
+ /**
+ * Insert text at the current caret location.
+ *
+ * @param string aText
+ * The text you want to insert.
+ */
+ insertTextAtCaret: function SP_insertTextAtCaret(aText)
+ {
+ let caretOffset = this.editor.getCaretOffset();
+ this.setText(aText, caretOffset, caretOffset);
+ this.editor.setCaretOffset(caretOffset + aText.length);
+ },
+
+ /**
+ * The Source Editor DirtyChanged event handler. This function updates the
+ * Scratchpad window title to show an asterisk when there are unsaved changes.
+ *
+ * @private
+ * @see SourceEditor.EVENTS.DIRTY_CHANGED
+ * @param object aEvent
+ * The DirtyChanged event object.
+ */
+ _onDirtyChanged: function SP__onDirtyChanged(aEvent)
+ {
+ Scratchpad._updateTitle();
+ if (Scratchpad.filename) {
+ if (Scratchpad.editor.dirty) {
+ document.getElementById("sp-cmd-revert").removeAttribute("disabled");
+ }
+ else {
+ document.getElementById("sp-cmd-revert").setAttribute("disabled", true);
+ }
+ }
+ },
+
+ /**
+ * Undo the last action of the user.
+ */
+ undo: function SP_undo()
+ {
+ this.editor.undo();
+ },
+
+ /**
+ * Redo the previously undone action.
+ */
+ redo: function SP_redo()
+ {
+ this.editor.redo();
+ },
+
+ /**
+ * The Scratchpad window unload event handler. This method unloads/destroys
+ * the source editor.
+ *
+ * @param nsIDOMEvent aEvent
+ */
+ onUnload: function SP_onUnload(aEvent)
+ {
+ if (aEvent.target != document) {
+ return;
+ }
+
+ this.resetContext();
+
+ // This event is created only after user uses 'reload and run' feature.
+ if (this._reloadAndRunEvent) {
+ this.gBrowser.selectedBrowser.removeEventListener("load",
+ this._reloadAndRunEvent, true);
+ }
+
+ this.editor.removeEventListener(SourceEditor.EVENTS.DIRTY_CHANGED,
+ this._onDirtyChanged);
+ PreferenceObserver.uninit();
+
+ this.editor.destroy();
+ this.editor = null;
+ this.initialized = false;
+ },
+
+ /**
+ * Prompt to save scratchpad if it has unsaved changes.
+ *
+ * @param function aCallback
+ * Optional function you want to call when file is saved. The callback
+ * receives three arguments:
+ * - toClose (boolean) - tells if the window should be closed.
+ * - saved (boolen) - tells if the file has been saved.
+ * - status (number) - the file save status result (if the file was
+ * saved).
+ * @return boolean
+ * Whether the window should be closed
+ */
+ promptSave: function SP_promptSave(aCallback)
+ {
+ if (this.editor.dirty) {
+ let ps = Services.prompt;
+ let flags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_SAVE +
+ ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL +
+ ps.BUTTON_POS_2 * ps.BUTTON_TITLE_DONT_SAVE;
+
+ let button = ps.confirmEx(window,
+ this.strings.GetStringFromName("confirmClose.title"),
+ this.strings.GetStringFromName("confirmClose"),
+ flags, null, null, null, null, {});
+
+ if (button == BUTTON_POSITION_CANCEL) {
+ if (aCallback) {
+ aCallback(false, false);
+ }
+ return false;
+ }
+
+ if (button == BUTTON_POSITION_SAVE) {
+ this.saveFile(aStatus => {
+ if (aCallback) {
+ aCallback(true, true, aStatus);
+ }
+ });
+ return true;
+ }
+ }
+
+ if (aCallback) {
+ aCallback(true, false);
+ }
+ return true;
+ },
+
+ /**
+ * Handler for window close event. Prompts to save scratchpad if
+ * there are unsaved changes.
+ *
+ * @param nsIDOMEvent aEvent
+ * @param function aCallback
+ * Optional function you want to call when file is saved/closed.
+ * Used mainly for tests.
+ */
+ onClose: function SP_onClose(aEvent, aCallback)
+ {
+ aEvent.preventDefault();
+ this.close(aCallback);
+ },
+
+ /**
+ * Close the scratchpad window. Prompts before closing if the scratchpad
+ * has unsaved changes.
+ *
+ * @param function aCallback
+ * Optional function you want to call when file is saved
+ */
+ close: function SP_close(aCallback)
+ {
+ this.promptSave((aShouldClose, aSaved, aStatus) => {
+ let shouldClose = aShouldClose;
+ if (aSaved && !Components.isSuccessCode(aStatus)) {
+ shouldClose = false;
+ }
+
+ if (shouldClose) {
+ telemetry.toolClosed("scratchpad");
+ window.close();
+ }
+ if (aCallback) {
+ aCallback();
+ }
+ });
+ },
+
+ _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);
+ }
+ }
+ },
+
+ openDocumentationPage: function SP_openDocumentationPage()
+ {
+ let url = this.strings.GetStringFromName("help.openDocumentationPage");
+ let newTab = this.gBrowser.addTab(url);
+ this.browserWindow.focus();
+ this.gBrowser.selectedTab = newTab;
+ },
+};
+
+
+/**
+ * Encapsulates management of the sidebar containing the VariablesView for
+ * object inspection.
+ */
+function ScratchpadSidebar(aScratchpad)
+{
+ let ToolSidebar = devtools.require("devtools/framework/sidebar").ToolSidebar;
+ let tabbox = document.querySelector("#scratchpad-sidebar");
+ this._sidebar = new ToolSidebar(tabbox, this, "scratchpad");
+ this._scratchpad = aScratchpad;
+}
+
+ScratchpadSidebar.prototype = {
+ /*
+ * The ToolSidebar for this sidebar.
+ */
+ _sidebar: null,
+
+ /*
+ * The VariablesView for this sidebar.
+ */
+ variablesView: null,
+
+ /*
+ * Whether the sidebar is currently shown.
+ */
+ visible: false,
+
+ /**
+ * Open the sidebar, if not open already, and populate it with the properties
+ * of the given object.
+ *
+ * @param string aString
+ * The string that was evaluated.
+ * @param object aObject
+ * The object to inspect, which is the aEvalString evaluation result.
+ * @return Promise
+ * A promise that will resolve once the sidebar is open.
+ */
+ open: function SS_open(aEvalString, aObject)
+ {
+ this.show();
+
+ let deferred = Promise.defer();
+
+ let onTabReady = () => {
+ if (!this.variablesView) {
+ let window = this._sidebar.getWindowForTab("variablesview");
+ let container = window.document.querySelector("#variables");
+ this.variablesView = new VariablesView(container, {
+ searchEnabled: true,
+ searchPlaceholder: this._scratchpad.strings
+ .GetStringFromName("propertiesFilterPlaceholder")
+ });
+ }
+ this._update(aObject).then(() => deferred.resolve());
+ };
+
+ if (this._sidebar.getCurrentTabID() == "variablesview") {
+ onTabReady();
+ }
+ else {
+ this._sidebar.once("variablesview-ready", onTabReady);
+ this._sidebar.addTab("variablesview", VARIABLES_VIEW_URL, true);
+ }
+
+ return deferred.promise;
+ },
+
+ /**
+ * Show the sidebar.
+ */
+ show: function SS_show()
+ {
+ if (!this.visible) {
+ this.visible = true;
+ this._sidebar.show();
+ }
+ },
+
+ /**
+ * Hide the sidebar.
+ */
+ hide: function SS_hide()
+ {
+ if (this.visible) {
+ this.visible = false;
+ this._sidebar.hide();
+ }
+ },
+
+ /**
+ * Update the object currently inspected by the sidebar.
+ *
+ * @param object aObject
+ * The object to inspect in the sidebar.
+ * @return Promise
+ * A promise that resolves when the update completes.
+ */
+ _update: function SS__update(aObject)
+ {
+ let deferred = Promise.defer();
+
+ this.variablesView.rawObject = aObject;
+
+ // In the future this will work on remote values (bug 825039).
+ setTimeout(() => deferred.resolve(), 0);
+ return deferred.promise;
+ }
+};
+
+
+/**
+ * Check whether a value is non-primitive.
+ */
+function isObject(aValue)
+{
+ let type = typeof aValue;
+ return type == "object" ? aValue != null : type == "function";
+}
+
+
+/**
+ * The PreferenceObserver listens for preference changes while Scratchpad is
+ * running.
+ */
+var PreferenceObserver = {
+ _initialized: false,
+
+ init: function PO_init()
+ {
+ if (this._initialized) {
+ return;
+ }
+
+ this.branch = Services.prefs.getBranch("devtools.scratchpad.");
+ this.branch.addObserver("", this, false);
+ this._initialized = true;
+ },
+
+ observe: function PO_observe(aMessage, aTopic, aData)
+ {
+ if (aTopic != "nsPref:changed") {
+ return;
+ }
+
+ if (aData == "recentFilesMax") {
+ Scratchpad.handleRecentFileMaxChange();
+ }
+ else if (aData == "recentFilePaths") {
+ Scratchpad.populateRecentFilesMenu();
+ }
+ },
+
+ uninit: function PO_uninit () {
+ if (!this.branch) {
+ return;
+ }
+
+ this.branch.removeObserver("", this);
+ this.branch = null;
+ }
+};
+
+XPCOMUtils.defineLazyGetter(Scratchpad, "strings", function () {
+ return Services.strings.createBundle(SCRATCHPAD_L10N);
+});
+
+addEventListener("load", Scratchpad.onLoad.bind(Scratchpad), false);
+addEventListener("unload", Scratchpad.onUnload.bind(Scratchpad), false);
+addEventListener("close", Scratchpad.onClose.bind(Scratchpad), false);
diff --git a/browser/devtools/scratchpad/scratchpad.xul b/browser/devtools/scratchpad/scratchpad.xul
new file mode 100644
index 000000000..af66b0832
--- /dev/null
+++ b/browser/devtools/scratchpad/scratchpad.xul
@@ -0,0 +1,301 @@
+<?xml version="1.0"?>
+#ifdef 0
+<!-- 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/. -->
+#endif
+<!DOCTYPE window [
+<!ENTITY % scratchpadDTD SYSTEM "chrome://browser/locale/devtools/scratchpad.dtd" >
+ %scratchpadDTD;
+]>
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/common.css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/scratchpad.css"?>
+<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
+<?xul-overlay href="chrome://browser/content/devtools/source-editor-overlay.xul"?>
+
+<window id="main-window"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="&window.title;"
+ windowtype="devtools:scratchpad"
+ macanimationtype="document"
+ fullscreenbutton="true"
+ screenX="4" screenY="4"
+ width="640" height="480"
+ persist="screenX screenY width height sizemode">
+
+<script type="application/javascript" src="chrome://global/content/globalOverlay.js"/>
+<script type="application/javascript" src="chrome://browser/content/devtools/scratchpad.js"/>
+
+<commandset id="editMenuCommands"/>
+<commandset id="sourceEditorCommands"/>
+
+<commandset id="sp-commandset">
+ <command id="sp-cmd-newWindow" oncommand="Scratchpad.openScratchpad();"/>
+ <command id="sp-cmd-openFile" oncommand="Scratchpad.openFile();"/>
+ <command id="sp-cmd-clearRecentFiles" oncommand="Scratchpad.clearRecentFiles();"/>
+ <command id="sp-cmd-save" oncommand="Scratchpad.saveFile();"/>
+ <command id="sp-cmd-saveas" oncommand="Scratchpad.saveFileAs();"/>
+ <command id="sp-cmd-revert" oncommand="Scratchpad.promptRevert();" disabled="true"/>
+
+ <!-- TODO: bug 650340 - implement printFile()
+ <command id="sp-cmd-printFile" oncommand="Scratchpad.printFile();" disabled="true"/>
+ -->
+
+ <command id="sp-cmd-close" oncommand="Scratchpad.close();"/>
+ <command id="sp-cmd-run" oncommand="Scratchpad.run();"/>
+ <command id="sp-cmd-inspect" oncommand="Scratchpad.inspect();"/>
+ <command id="sp-cmd-display" oncommand="Scratchpad.display();"/>
+ <command id="sp-cmd-contentContext" oncommand="Scratchpad.setContentContext();"/>
+ <command id="sp-cmd-browserContext" oncommand="Scratchpad.setBrowserContext();" disabled="true"/>
+ <command id="sp-cmd-reloadAndRun" oncommand="Scratchpad.reloadAndRun();"/>
+ <command id="sp-cmd-resetContext" oncommand="Scratchpad.resetContext();"/>
+ <command id="sp-cmd-errorConsole" oncommand="Scratchpad.openErrorConsole();" disabled="true"/>
+ <command id="sp-cmd-webConsole" oncommand="Scratchpad.openWebConsole();"/>
+ <command id="sp-cmd-documentationLink" oncommand="Scratchpad.openDocumentationPage();"/>
+ <command id="sp-cmd-hideSidebar" oncommand="Scratchpad.sidebar.hide();"/>
+</commandset>
+
+<keyset id="sourceEditorKeys"/>
+
+<keyset id="sp-keyset">
+ <key id="sp-key-window"
+ key="&newWindowCmd.commandkey;"
+ command="sp-cmd-newWindow"
+ modifiers="accel"/>
+ <key id="sp-key-open"
+ key="&openFileCmd.commandkey;"
+ command="sp-cmd-openFile"
+ modifiers="accel"/>
+ <key id="sp-key-save"
+ key="&saveFileCmd.commandkey;"
+ command="sp-cmd-save"
+ modifiers="accel"/>
+ <key id="sp-key-close"
+ key="&closeCmd.key;"
+ command="sp-cmd-close"
+ modifiers="accel"/>
+
+ <!-- TODO: bug 650340 - implement printFile
+ <key id="sp-key-printFile"
+ key="&printCmd.commandkey;"
+ command="sp-cmd-printFile"
+ modifiers="accel"/>
+ -->
+
+ <key id="sp-key-run"
+ key="&run.key;"
+ command="sp-cmd-run"
+ modifiers="accel"/>
+ <key id="sp-key-inspect"
+ key="&inspect.key;"
+ command="sp-cmd-inspect"
+ modifiers="accel"/>
+ <key id="sp-key-display"
+ key="&display.key;"
+ command="sp-cmd-display"
+ modifiers="accel"/>
+ <key id="sp-key-reloadAndRun"
+ key="&reloadAndRun.key;"
+ command="sp-cmd-reloadAndRun"
+ modifiers="accel,shift"/>
+ <key id="sp-key-errorConsole"
+ key="&errorConsoleCmd.commandkey;"
+ command="sp-cmd-errorConsole"
+ modifiers="accel,shift"/>
+ <key id="sp-key-hideSidebar"
+ keycode="VK_ESCAPE"
+ command="sp-cmd-hideSidebar"/>
+ <key id="key_openHelp"
+ keycode="VK_F1"
+ command="sp-cmd-documentationLink"/>
+</keyset>
+
+
+<menubar id="sp-menubar">
+ <menu id="sp-file-menu" label="&fileMenu.label;"
+ accesskey="&fileMenu.accesskey;">
+ <menupopup id="sp-menu-filepopup">
+ <menuitem id="sp-menu-newscratchpad"
+ label="&newWindowCmd.label;"
+ accesskey="&newWindowCmd.accesskey;"
+ key="sp-key-window"
+ command="sp-cmd-newWindow"/>
+ <menuseparator/>
+ <menuitem id="sp-menu-open"
+ label="&openFileCmd.label;"
+ command="sp-cmd-openFile"
+ key="sp-key-open"
+ accesskey="&openFileCmd.accesskey;"/>
+ <menu id="sp-open_recent-menu" label="&openRecentMenu.label;"
+ accesskey="&openRecentMenu.accesskey;"
+ disabled="true">
+ <menupopup id="sp-menu-open_recentPopup"/>
+ </menu>
+ <menuitem id="sp-menu-save"
+ label="&saveFileCmd.label;"
+ accesskey="&saveFileCmd.accesskey;"
+ key="sp-key-save"
+ command="sp-cmd-save"/>
+ <menuitem id="sp-menu-saveas"
+ label="&saveFileAsCmd.label;"
+ accesskey="&saveFileAsCmd.accesskey;"
+ command="sp-cmd-saveas"/>
+ <menuitem id="sp-menu-revert"
+ label="&revertCmd.label;"
+ accesskey="&revertCmd.accesskey;"
+ command="sp-cmd-revert"/>
+ <menuseparator/>
+
+ <!-- TODO: bug 650340 - implement printFile
+ <menuitem id="sp-menu-print"
+ label="&printCmd.label;"
+ accesskey="&printCmd.accesskey;"
+ command="sp-cmd-printFile"/>
+ <menuseparator/>
+ -->
+
+ <menuitem id="sp-menu-close"
+ label="&closeCmd.label;"
+ key="sp-key-close"
+ accesskey="&closeCmd.accesskey;"
+ command="sp-cmd-close"/>
+ </menupopup>
+ </menu>
+
+ <menu id="sp-edit-menu" label="&editMenu.label;"
+ accesskey="&editMenu.accesskey;">
+ <menupopup id="sp-menu_editpopup"
+ onpopupshowing="goUpdateSourceEditorMenuItems()">
+ <menuitem id="se-menu-undo"/>
+ <menuitem id="se-menu-redo"/>
+ <menuseparator/>
+ <menuitem id="se-menu-cut"/>
+ <menuitem id="se-menu-copy"/>
+ <menuitem id="se-menu-paste"/>
+ <menuseparator/>
+ <menuitem id="se-menu-selectAll"/>
+ <menuseparator/>
+ <menuitem id="se-menu-find"/>
+ <menuitem id="se-menu-findAgain"/>
+ <menuseparator/>
+ <menuitem id="se-menu-gotoLine"/>
+ </menupopup>
+ </menu>
+
+ <menu id="sp-execute-menu" label="&executeMenu.label;"
+ accesskey="&executeMenu.accesskey;">
+ <menupopup id="sp-menu_executepopup">
+ <menuitem id="sp-text-run"
+ label="&run.label;"
+ accesskey="&run.accesskey;"
+ key="sp-key-run"
+ command="sp-cmd-run"/>
+ <menuitem id="sp-text-inspect"
+ label="&inspect.label;"
+ accesskey="&inspect.accesskey;"
+ key="sp-key-inspect"
+ command="sp-cmd-inspect"/>
+ <menuitem id="sp-text-display"
+ label="&display.label;"
+ accesskey="&display.accesskey;"
+ key="sp-key-display"
+ command="sp-cmd-display"/>
+ <menuseparator/>
+ <menuitem id="sp-text-reloadAndRun"
+ label="&reloadAndRun.label;"
+ key="sp-key-reloadAndRun"
+ accesskey="&reloadAndRun.accesskey;"
+ command="sp-cmd-reloadAndRun"/>
+ <menuitem id="sp-text-resetContext"
+ label="&resetContext2.label;"
+ accesskey="&resetContext2.accesskey;"
+ command="sp-cmd-resetContext"/>
+ </menupopup>
+ </menu>
+
+ <menu id="sp-environment-menu"
+ label="&environmentMenu.label;"
+ accesskey="&environmentMenu.accesskey;"
+ hidden="true">
+ <menupopup id="sp-menu-environment">
+ <menuitem id="sp-menu-content"
+ label="&contentContext.label;"
+ accesskey="&contentContext.accesskey;"
+ command="sp-cmd-contentContext"
+ checked="true"
+ type="radio"/>
+ <menuitem id="sp-menu-browser"
+ command="sp-cmd-browserContext"
+ label="&browserContext.label;"
+ accesskey="&browserContext.accesskey;"
+ type="radio"/>
+ </menupopup>
+ </menu>
+
+#ifdef XP_WIN
+ <menu id="sp-help-menu"
+ label="&helpMenu.label;"
+ accesskey="&helpMenuWin.accesskey;">
+#else
+ <menu id="sp-help-menu"
+ label="&helpMenu.label;"
+ accesskey="&helpMenu.accesskey;">
+#endif
+ <menupopup id="sp-menu-help">
+ <menuitem id="sp-menu-documentation"
+ label="&documentationLink.label;"
+ accesskey="&documentationLink.accesskey;"
+ command="sp-cmd-documentationLink"
+ key="key_openHelp"/>
+ </menupopup>
+ </menu>
+</menubar>
+
+<popupset id="scratchpad-popups">
+ <menupopup id="scratchpad-text-popup"
+ onpopupshowing="goUpdateSourceEditorMenuItems()">
+ <menuitem id="se-cMenu-cut"/>
+ <menuitem id="se-cMenu-copy"/>
+ <menuitem id="se-cMenu-paste"/>
+ <menuitem id="se-cMenu-delete"/>
+ <menuseparator/>
+ <menuitem id="se-cMenu-selectAll"/>
+ <menuseparator/>
+ <menuitem id="sp-text-run"
+ label="&run.label;"
+ accesskey="&run.accesskey;"
+ key="sp-key-run"
+ command="sp-cmd-run"/>
+ <menuitem id="sp-text-inspect"
+ label="&inspect.label;"
+ accesskey="&inspect.accesskey;"
+ key="sp-key-inspect"
+ command="sp-cmd-inspect"/>
+ <menuitem id="sp-text-display"
+ label="&display.label;"
+ accesskey="&display.accesskey;"
+ key="sp-key-display"
+ command="sp-cmd-display"/>
+ <menuseparator/>
+ <menuitem id="sp-text-resetContext"
+ label="&resetContext2.label;"
+ accesskey="&resetContext2.accesskey;"
+ command="sp-cmd-resetContext"/>
+ </menupopup>
+</popupset>
+
+<notificationbox id="scratchpad-notificationbox" flex="1">
+ <hbox flex="1">
+ <vbox id="scratchpad-editor" flex="1"/>
+ <splitter class="devtools-side-splitter"/>
+ <tabbox id="scratchpad-sidebar" class="devtools-sidebar-tabs"
+ width="300"
+ hidden="true">
+ <tabs/>
+ <tabpanels flex="1"/>
+ </tabbox>
+ </hbox>
+</notificationbox>
+
+</window>
diff --git a/browser/devtools/scratchpad/test/Makefile.in b/browser/devtools/scratchpad/test/Makefile.in
new file mode 100644
index 000000000..e74af132a
--- /dev/null
+++ b/browser/devtools/scratchpad/test/Makefile.in
@@ -0,0 +1,44 @@
+# 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/.
+
+DEPTH = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+relativesrcdir = @relativesrcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+MOCHITEST_BROWSER_FILES = \
+ browser_scratchpad_initialization.js \
+ 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 \
+ browser_scratchpad_bug_679467_falsy.js \
+ browser_scratchpad_bug_699130_edit_ui_updates.js \
+ browser_scratchpad_bug_669612_unsaved.js \
+ browser_scratchpad_bug684546_reset_undo.js \
+ browser_scratchpad_bug690552_display_outputs_errors.js \
+ browser_scratchpad_bug650345_find_ui.js \
+ browser_scratchpad_bug714942_goto_line_ui.js \
+ browser_scratchpad_bug_650760_help_key.js \
+ browser_scratchpad_bug_651942_recent_files.js \
+ browser_scratchpad_bug756681_display_non_error_exceptions.js \
+ browser_scratchpad_bug_751744_revert_to_saved.js \
+ browser_scratchpad_bug740948_reload_and_run.js \
+ browser_scratchpad_bug_661762_wrong_window_focus.js \
+ browser_scratchpad_bug_644413_modeline.js \
+ head.js \
+
+# Disable test due to bug 807234 becoming basically permanent
+# browser_scratchpad_bug_653427_confirm_close.js \
+
+include $(topsrcdir)/config/rules.mk
diff --git a/browser/devtools/scratchpad/test/browser_scratchpad_bug650345_find_ui.js b/browser/devtools/scratchpad/test/browser_scratchpad_bug650345_find_ui.js
new file mode 100644
index 000000000..81e45aeef
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug650345_find_ui.js
@@ -0,0 +1,97 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function browserLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", browserLoad, true);
+ openScratchpad(runTests);
+ }, true);
+
+ content.location = "data:text/html,<p>test the Find feature in Scratchpad";
+}
+
+function runTests(aWindow, aScratchpad)
+{
+ let editor = aScratchpad.editor;
+ let text = "foobar bug650345\nBug650345 bazbaz\nfoobar omg\ntest";
+ editor.setText(text);
+
+ let needle = "foobar";
+ editor.setSelection(0, needle.length);
+
+ let oldPrompt = Services.prompt;
+ Services.prompt = {
+ prompt: function() { return true; },
+ };
+
+ let findKey = "F";
+ info("test Ctrl/Cmd-" + findKey + " (find)");
+ EventUtils.synthesizeKey(findKey, {accelKey: true}, aWindow);
+ let selection = editor.getSelection();
+ let newIndex = text.indexOf(needle, needle.length);
+ is(selection.start, newIndex, "selection.start is correct");
+ is(selection.end, newIndex + needle.length, "selection.end is correct");
+
+ info("test cmd_find");
+ aWindow.goDoCommand("cmd_find");
+ selection = editor.getSelection();
+ is(selection.start, 0, "selection.start is correct");
+ is(selection.end, needle.length, "selection.end is correct");
+
+ let findNextKey = Services.appinfo.OS == "Darwin" ? "G" : "VK_F3";
+ let findNextKeyOptions = Services.appinfo.OS == "Darwin" ?
+ {accelKey: true} : {};
+
+ info("test " + findNextKey + " (findNext)");
+ EventUtils.synthesizeKey(findNextKey, findNextKeyOptions, aWindow);
+ selection = editor.getSelection();
+ is(selection.start, newIndex, "selection.start is correct");
+ is(selection.end, newIndex + needle.length, "selection.end is correct");
+
+ info("test cmd_findAgain");
+ aWindow.goDoCommand("cmd_findAgain");
+ selection = editor.getSelection();
+ is(selection.start, 0, "selection.start is correct");
+ is(selection.end, needle.length, "selection.end is correct");
+
+ let findPreviousKey = Services.appinfo.OS == "Darwin" ? "G" : "VK_F3";
+ let findPreviousKeyOptions = Services.appinfo.OS == "Darwin" ?
+ {accelKey: true, shiftKey: true} : {shiftKey: true};
+
+ info("test " + findPreviousKey + " (findPrevious)");
+ EventUtils.synthesizeKey(findPreviousKey, findPreviousKeyOptions, aWindow);
+ selection = editor.getSelection();
+ is(selection.start, newIndex, "selection.start is correct");
+ is(selection.end, newIndex + needle.length, "selection.end is correct");
+
+ info("test cmd_findPrevious");
+ aWindow.goDoCommand("cmd_findPrevious");
+ selection = editor.getSelection();
+ is(selection.start, 0, "selection.start is correct");
+ is(selection.end, needle.length, "selection.end is correct");
+
+ needle = "BAZbaz";
+ newIndex = text.toLowerCase().indexOf(needle.toLowerCase());
+
+ Services.prompt = {
+ prompt: function(aWindow, aTitle, aMessage, aValue) {
+ aValue.value = needle;
+ return true;
+ },
+ };
+
+ info("test Ctrl/Cmd-" + findKey + " (find) with a custom value");
+ EventUtils.synthesizeKey(findKey, {accelKey: true}, aWindow);
+ selection = editor.getSelection();
+ is(selection.start, newIndex, "selection.start is correct");
+ is(selection.end, newIndex + needle.length, "selection.end is correct");
+
+ Services.prompt = oldPrompt;
+
+ finish();
+}
+
diff --git a/browser/devtools/scratchpad/test/browser_scratchpad_bug684546_reset_undo.js b/browser/devtools/scratchpad/test/browser_scratchpad_bug684546_reset_undo.js
new file mode 100644
index 000000000..d4ec88012
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug684546_reset_undo.js
@@ -0,0 +1,158 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let tempScope = {};
+Cu.import("resource://gre/modules/NetUtil.jsm", tempScope);
+Cu.import("resource://gre/modules/FileUtils.jsm", tempScope);
+let NetUtil = tempScope.NetUtil;
+let FileUtils = tempScope.FileUtils;
+
+// Reference to the Scratchpad chrome window object.
+let gScratchpadWindow;
+
+// Reference to the Scratchpad object.
+let gScratchpad;
+
+// Reference to the temporary nsIFile we will work with.
+let gFileA;
+let gFileB;
+
+// The temporary file content.
+let gFileAContent = "// File A ** Hello World!";
+let gFileBContent = "// File B ** Goodbye All";
+
+// Help track if one or both files are saved
+let gFirstFileSaved = false;
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function browserLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", browserLoad, true);
+ openScratchpad(runTests);
+ }, true);
+
+ content.location = "data:text/html,<p>test that undo get's reset after file load in Scratchpad";
+}
+
+function runTests()
+{
+ gScratchpad = gScratchpadWindow.Scratchpad;
+
+ // Create a temporary file.
+ gFileA = FileUtils.getFile("TmpD", ["fileAForBug684546.tmp"]);
+ gFileA.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0666);
+
+ gFileB = FileUtils.getFile("TmpD", ["fileBForBug684546.tmp"]);
+ gFileB.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0666);
+
+ // Write the temporary file.
+ let foutA = Cc["@mozilla.org/network/file-output-stream;1"].
+ createInstance(Ci.nsIFileOutputStream);
+ foutA.init(gFileA.QueryInterface(Ci.nsILocalFile), 0x02 | 0x08 | 0x20,
+ 0644, foutA.DEFER_OPEN);
+
+ let foutB = Cc["@mozilla.org/network/file-output-stream;1"].
+ createInstance(Ci.nsIFileOutputStream);
+ foutB.init(gFileB.QueryInterface(Ci.nsILocalFile), 0x02 | 0x08 | 0x20,
+ 0644, foutB.DEFER_OPEN);
+
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
+ createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ let fileContentStreamA = converter.convertToInputStream(gFileAContent);
+ let fileContentStreamB = converter.convertToInputStream(gFileBContent);
+
+ NetUtil.asyncCopy(fileContentStreamA, foutA, tempFileSaved);
+ NetUtil.asyncCopy(fileContentStreamB, foutB, tempFileSaved);
+}
+
+function tempFileSaved(aStatus)
+{
+ let success = Components.isSuccessCode(aStatus);
+
+ ok(success, "a temporary file was saved successfully");
+
+ if (!success)
+ {
+ finish();
+ return;
+ }
+
+ if (gFirstFileSaved && success)
+ {
+ ok((gFirstFileSaved && success), "Both files loaded");
+ // Import the file A into Scratchpad.
+ gScratchpad.importFromFile(gFileA.QueryInterface(Ci.nsILocalFile), true,
+ fileAImported);
+ }
+ gFirstFileSaved = success;
+}
+
+function fileAImported(aStatus, aFileContent)
+{
+ ok(Components.isSuccessCode(aStatus),
+ "the temporary file A was imported successfully with Scratchpad");
+
+ is(aFileContent, gFileAContent, "received data is correct");
+
+ is(gScratchpad.getText(), gFileAContent, "the editor content is correct");
+
+ gScratchpad.setText("new text", gScratchpad.getText().length);
+
+ is(gScratchpad.getText(), gFileAContent + "new text", "text updated correctly");
+ gScratchpad.undo();
+ is(gScratchpad.getText(), gFileAContent, "undo works");
+ gScratchpad.redo();
+ is(gScratchpad.getText(), gFileAContent + "new text", "redo works");
+
+ // Import the file B into Scratchpad.
+ gScratchpad.importFromFile(gFileB.QueryInterface(Ci.nsILocalFile), true,
+ fileBImported);
+}
+
+function fileBImported(aStatus, aFileContent)
+{
+ ok(Components.isSuccessCode(aStatus),
+ "the temporary file B was imported successfully with Scratchpad");
+
+ is(aFileContent, gFileBContent, "received data is correct");
+
+ is(gScratchpad.getText(), gFileBContent, "the editor content is correct");
+
+ ok(!gScratchpad.editor.canUndo(), "editor cannot undo after load");
+
+ gScratchpad.undo();
+ is(gScratchpad.getText(), gFileBContent,
+ "the editor content is still correct after undo");
+
+ gScratchpad.setText("new text", gScratchpad.getText().length);
+ is(gScratchpad.getText(), gFileBContent + "new text", "text updated correctly");
+
+ gScratchpad.undo();
+ is(gScratchpad.getText(), gFileBContent, "undo works");
+ ok(!gScratchpad.editor.canUndo(), "editor cannot undo after load (again)");
+
+ gScratchpad.redo();
+ is(gScratchpad.getText(), gFileBContent + "new text", "redo works");
+
+ // Done!
+ finish();
+}
+
+registerCleanupFunction(function() {
+ if (gFileA && gFileA.exists())
+ {
+ gFileA.remove(false);
+ gFileA = null;
+ }
+ if (gFileB && gFileB.exists())
+ {
+ gFileB.remove(false);
+ gFileB = null;
+ }
+ gScratchpad = null;
+});
diff --git a/browser/devtools/scratchpad/test/browser_scratchpad_bug690552_display_outputs_errors.js b/browser/devtools/scratchpad/test/browser_scratchpad_bug690552_display_outputs_errors.js
new file mode 100644
index 000000000..637af088e
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug690552_display_outputs_errors.js
@@ -0,0 +1,56 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function browserLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", browserLoad, true);
+ openScratchpad(runTests, {"state":{"text":""}});
+ }, true);
+
+ content.location = "data:text/html,<p>test that exceptions our output as " +
+ "comments for 'display' and not sent to the console in Scratchpad";
+}
+
+function runTests()
+{
+ let scratchpad = gScratchpadWindow.Scratchpad;
+
+ let message = "\"Hello World!\""
+ let openComment = "\n/*\n";
+ let closeComment = "\n*/";
+ let error = "throw new Error(\"Ouch!\")";
+
+ let tests = [{
+ method: "display",
+ code: message,
+ result: message + openComment + "Hello World!" + closeComment,
+ label: "message display output"
+ },
+ {
+ method: "display",
+ code: error,
+ result: error + openComment + "Exception: Ouch!\n@" +
+ scratchpad.uniqueName + ":1" + closeComment,
+ label: "error display output",
+ },
+ {
+ method: "run",
+ code: message,
+ result: message,
+ label: "message run output",
+ },
+ {
+ method: "run",
+ code: error,
+ result: error + openComment + "Exception: Ouch!\n@" +
+ scratchpad.uniqueName + ":1" + closeComment,
+ label: "error run output",
+ }];
+
+ runAsyncTests(scratchpad, tests).then(finish);
+}
diff --git a/browser/devtools/scratchpad/test/browser_scratchpad_bug714942_goto_line_ui.js b/browser/devtools/scratchpad/test/browser_scratchpad_bug714942_goto_line_ui.js
new file mode 100644
index 000000000..5cbc344db
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug714942_goto_line_ui.js
@@ -0,0 +1,45 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function browserLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", browserLoad, true);
+ openScratchpad(runTests);
+ }, true);
+
+ content.location = "data:text/html,<p>test the 'Jump to line' feature in Scratchpad";
+}
+
+function runTests(aWindow, aScratchpad)
+{
+ let editor = aScratchpad.editor;
+ let text = "foobar bug650345\nBug650345 bazbaz\nfoobar omg\ntest";
+ editor.setText(text);
+ editor.setCaretOffset(0);
+
+ let oldPrompt = Services.prompt;
+ let desiredValue = null;
+ Services.prompt = {
+ prompt: function(aWindow, aTitle, aMessage, aValue) {
+ aValue.value = desiredValue;
+ return true;
+ },
+ };
+
+ desiredValue = 3;
+ EventUtils.synthesizeKey("J", {accelKey: true}, aWindow);
+ is(editor.getCaretOffset(), 34, "caret offset is correct");
+
+ desiredValue = 2;
+ aWindow.goDoCommand("cmd_gotoLine")
+ is(editor.getCaretOffset(), 17, "caret offset is correct (again)");
+
+ Services.prompt = oldPrompt;
+
+ finish();
+}
diff --git a/browser/devtools/scratchpad/test/browser_scratchpad_bug740948_reload_and_run.js b/browser/devtools/scratchpad/test/browser_scratchpad_bug740948_reload_and_run.js
new file mode 100644
index 000000000..354ccbf75
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug740948_reload_and_run.js
@@ -0,0 +1,73 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let DEVTOOLS_CHROME_ENABLED = "devtools.chrome.enabled";
+let EDITOR_TEXT = [
+ "var evt = new CustomEvent('foo', { bubbles: true });",
+ "document.body.innerHTML = 'Modified text';",
+ "window.dispatchEvent(evt);"
+].join("\n");
+
+function test()
+{
+ waitForExplicitFinish();
+ Services.prefs.setBoolPref(DEVTOOLS_CHROME_ENABLED, true);
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ openScratchpad(runTests);
+ }, true);
+
+ content.location = "data:text/html,Scratchpad test for bug 740948";
+}
+
+function runTests()
+{
+ let sp = gScratchpadWindow.Scratchpad;
+ ok(sp, "Scratchpad object exists in new window");
+
+ // Test that Reload And Run command is enabled in the content
+ // context and disabled in the browser context.
+
+ let reloadAndRun = gScratchpadWindow.document.
+ getElementById("sp-cmd-reloadAndRun");
+ ok(reloadAndRun, "Reload And Run command exists");
+ ok(!reloadAndRun.hasAttribute("disabled"),
+ "Reload And Run command is enabled");
+
+ sp.setBrowserContext();
+ ok(reloadAndRun.hasAttribute("disabled"),
+ "Reload And Run command is disabled in the browser context.");
+
+ // Switch back to the content context and run our predefined
+ // code. This code modifies the body of our document and dispatches
+ // a custom event 'foo'. We listen to that event and check the
+ // body to make sure that the page has been reloaded and Scratchpad
+ // code has been executed.
+
+ sp.setContentContext();
+ sp.setText(EDITOR_TEXT);
+
+ let browser = gBrowser.selectedBrowser;
+
+ browser.addEventListener("DOMWindowCreated", function onWindowCreated() {
+ browser.removeEventListener("DOMWindowCreated", onWindowCreated, true);
+
+ browser.contentWindow.addEventListener("foo", function onFoo() {
+ browser.contentWindow.removeEventListener("foo", onFoo, true);
+
+ is(browser.contentWindow.document.body.innerHTML, "Modified text",
+ "After reloading, HTML is different.");
+
+ Services.prefs.clearUserPref(DEVTOOLS_CHROME_ENABLED);
+ finish();
+ }, true);
+ }, true);
+
+ ok(browser.contentWindow.document.body.innerHTML !== "Modified text",
+ "Before reloading, HTML is intact.");
+ sp.reloadAndRun();
+}
+
diff --git a/browser/devtools/scratchpad/test/browser_scratchpad_bug756681_display_non_error_exceptions.js b/browser/devtools/scratchpad/test/browser_scratchpad_bug756681_display_non_error_exceptions.js
new file mode 100644
index 000000000..894af5ffd
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug756681_display_non_error_exceptions.js
@@ -0,0 +1,107 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function browserLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", browserLoad, true);
+ openScratchpad(runTests, {"state":{"text":""}});
+ }, true);
+
+ content.location = "data:text/html, test that exceptions are output as " +
+ "comments correctly in Scratchpad";
+}
+
+function runTests()
+{
+ var scratchpad = gScratchpadWindow.Scratchpad;
+
+ var message = "\"Hello World!\""
+ var openComment = "\n/*\n";
+ var closeComment = "\n*/";
+ var error1 = "throw new Error(\"Ouch!\")";
+ var error2 = "throw \"A thrown string\"";
+ var error3 = "throw {}";
+ var error4 = "document.body.appendChild(document.body)";
+
+ let tests = [{
+ // Display message
+ method: "display",
+ code: message,
+ result: message + openComment + "Hello World!" + closeComment,
+ label: "message display output"
+ },
+ {
+ // Display error1, throw new Error("Ouch")
+ method: "display",
+ code: error1,
+ result: error1 + openComment +
+ "Exception: Ouch!\n@" + scratchpad.uniqueName + ":1" + closeComment,
+ label: "error display output"
+ },
+ {
+ // Display error2, throw "A thrown string"
+ method: "display",
+ code: error2,
+ result: error2 + openComment + "Exception: A thrown string" + closeComment,
+ label: "thrown string display output"
+ },
+ {
+ // Display error3, throw {}
+ method: "display",
+ code: error3,
+ result: error3 + openComment + "Exception: [object Object]" + closeComment,
+ label: "thrown object display output"
+ },
+ {
+ // Display error4, document.body.appendChild(document.body)
+ method: "display",
+ code: error4,
+ result: error4 + openComment + "Exception: Node cannot be inserted " +
+ "at the specified point in the hierarchy\n@1" + closeComment,
+ label: "Alternative format error display output"
+ },
+ {
+ // Run message
+ method: "run",
+ code: message,
+ result: message,
+ label: "message run output"
+ },
+ {
+ // Run error1, throw new Error("Ouch")
+ method: "run",
+ code: error1,
+ result: error1 + openComment +
+ "Exception: Ouch!\n@" + scratchpad.uniqueName + ":1" + closeComment,
+ label: "error run output"
+ },
+ {
+ // Run error2, throw "A thrown string"
+ method: "run",
+ code: error2,
+ result: error2 + openComment + "Exception: A thrown string" + closeComment,
+ label: "thrown string run output"
+ },
+ {
+ // Run error3, throw {}
+ method: "run",
+ code: error3,
+ result: error3 + openComment + "Exception: [object Object]" + closeComment,
+ label: "thrown object run output"
+ },
+ {
+ // Run error4, document.body.appendChild(document.body)
+ method: "run",
+ code: error4,
+ result: error4 + openComment + "Exception: Node cannot be inserted " +
+ "at the specified point in the hierarchy\n@1" + closeComment,
+ label: "Alternative format error run output"
+ }];
+
+ runAsyncTests(scratchpad, tests).then(finish);
+} \ No newline at end of file
diff --git a/browser/devtools/scratchpad/test/browser_scratchpad_bug_644413_modeline.js b/browser/devtools/scratchpad/test/browser_scratchpad_bug_644413_modeline.js
new file mode 100644
index 000000000..54d5e835a
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug_644413_modeline.js
@@ -0,0 +1,92 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let tempScope = {};
+Cu.import("resource://gre/modules/NetUtil.jsm", tempScope);
+Cu.import("resource://gre/modules/FileUtils.jsm", tempScope);
+let NetUtil = tempScope.NetUtil;
+let FileUtils = tempScope.FileUtils;
+
+
+let gScratchpad; // Reference to the Scratchpad object.
+let gFile; // Reference to the temporary nsIFile we will work with.
+let DEVTOOLS_CHROME_ENABLED = "devtools.chrome.enabled";
+
+// The temporary file content.
+let gFileContent = "function main() { return 0; }";
+
+function test() {
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ openScratchpad(runTests);
+ }, true);
+
+ content.location = "data:text/html,<p>test file open and save in Scratchpad";
+}
+
+function runTests() {
+ gScratchpad = gScratchpadWindow.Scratchpad;
+ function size(obj) { return Object.keys(obj).length; }
+
+ // Test Scratchpad._scanModeLine method.
+ let obj = gScratchpad._scanModeLine();
+ is(size(obj), 0, "Mode-line object has no properties");
+
+ obj = gScratchpad._scanModeLine("/* This is not a mode-line comment */");
+ is(size(obj), 0, "Mode-line object has no properties");
+
+ obj = gScratchpad._scanModeLine("/* -sp-context:browser */");
+ is(size(obj), 1, "Mode-line object has one property");
+ is(obj["-sp-context"], "browser");
+
+ obj = gScratchpad._scanModeLine("/* -sp-context: browser */");
+ is(size(obj), 1, "Mode-line object has one property");
+ is(obj["-sp-context"], "browser");
+
+ obj = gScratchpad._scanModeLine("// -sp-context: browser");
+ is(size(obj), 1, "Mode-line object has one property");
+ is(obj["-sp-context"], "browser");
+
+ obj = gScratchpad._scanModeLine("/* -sp-context:browser, other:true */");
+ is(size(obj), 2, "Mode-line object has two properties");
+ is(obj["-sp-context"], "browser");
+ is(obj["other"], "true");
+
+ // Test importing files with a mode-line in them.
+ let content = "/* -sp-context:browser */\n" + gFileContent;
+ createTempFile("fileForBug644413.tmp", content, function(aStatus, aFile) {
+ ok(Components.isSuccessCode(aStatus), "File was saved successfully");
+
+ gFile = aFile;
+ gScratchpad.importFromFile(gFile.QueryInterface(Ci.nsILocalFile), true, fileImported);
+ });
+}
+
+function fileImported(status, content) {
+ ok(Components.isSuccessCode(status), "File was imported successfully");
+
+ // Since devtools.chrome.enabled is off, Scratchpad should still be in
+ // the content context.
+ is(gScratchpad.executionContext, gScratchpadWindow.SCRATCHPAD_CONTEXT_CONTENT);
+
+ // Set the pref and try again.
+ Services.prefs.setBoolPref(DEVTOOLS_CHROME_ENABLED, true);
+
+ gScratchpad.importFromFile(gFile.QueryInterface(Ci.nsILocalFile), true, function(status, content) {
+ ok(Components.isSuccessCode(status), "File was imported successfully");
+ is(gScratchpad.executionContext, gScratchpadWindow.SCRATCHPAD_CONTEXT_BROWSER);
+
+ gFile.remove(false);
+ gFile = null;
+ gScratchpad = null;
+ finish();
+ });
+}
+
+registerCleanupFunction(function () {
+ Services.prefs.clearUserPref(DEVTOOLS_CHROME_ENABLED);
+}); \ No newline at end of file
diff --git 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
new file mode 100644
index 000000000..28f6b08fe
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug_646070_chrome_context_pref.js
@@ -0,0 +1,51 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let DEVTOOLS_CHROME_ENABLED = "devtools.chrome.enabled";
+
+function test()
+{
+ waitForExplicitFinish();
+
+ Services.prefs.setBoolPref(DEVTOOLS_CHROME_ENABLED, true);
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+
+ ok(window.Scratchpad, "Scratchpad variable exists");
+
+ openScratchpad(runTests);
+ }, true);
+
+ content.location = "data:text/html,Scratchpad test for bug 646070 - chrome context preference";
+}
+
+function runTests()
+{
+ let sp = gScratchpadWindow.Scratchpad;
+ ok(sp, "Scratchpad object exists in new window");
+
+ let environmentMenu = gScratchpadWindow.document.
+ getElementById("sp-environment-menu");
+ ok(environmentMenu, "Environment menu element exists");
+ ok(!environmentMenu.hasAttribute("hidden"),
+ "Environment menu is visible");
+
+ let errorConsoleCommand = gScratchpadWindow.document.
+ getElementById("sp-cmd-errorConsole");
+ ok(errorConsoleCommand, "Error console command element exists");
+ ok(!errorConsoleCommand.hasAttribute("disabled"),
+ "Error console command is enabled");
+
+ 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.clearUserPref(DEVTOOLS_CHROME_ENABLED);
+
+ finish();
+}
diff --git a/browser/devtools/scratchpad/test/browser_scratchpad_bug_650760_help_key.js b/browser/devtools/scratchpad/test/browser_scratchpad_bug_650760_help_key.js
new file mode 100644
index 000000000..faf5d2994
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug_650760_help_key.js
@@ -0,0 +1,60 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ content.location = "data:text/html,Test keybindings for opening Scratchpad MDN Documentation, bug 650760";
+ gBrowser.selectedBrowser.addEventListener("load", function onTabLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onTabLoad, true);
+
+ ok(window.Scratchpad, "Scratchpad variable exists");
+
+ openScratchpad(runTest);
+ }, true);
+}
+
+function runTest()
+{
+ let sp = gScratchpadWindow.Scratchpad;
+ ok(sp, "Scratchpad object exists in new window");
+ ok(sp.editor.hasFocus(), "the editor has focus");
+
+ let keyid = gScratchpadWindow.document.getElementById("key_openHelp");
+ let modifiers = keyid.getAttribute("modifiers");
+
+ let key = null;
+ if (keyid.getAttribute("keycode"))
+ key = keyid.getAttribute("keycode");
+
+ else if (keyid.getAttribute("key"))
+ key = keyid.getAttribute("key");
+
+ isnot(key, null, "Successfully retrieved keycode/key");
+
+ var aEvent = {
+ shiftKey: modifiers.match("shift"),
+ ctrlKey: modifiers.match("ctrl"),
+ altKey: modifiers.match("alt"),
+ metaKey: modifiers.match("meta"),
+ accelKey: modifiers.match("accel")
+ }
+
+ info("check that the MDN page is opened on \"F1\"");
+ let linkClicked = false;
+ sp.openDocumentationPage = function(event) { linkClicked = true; };
+
+ EventUtils.synthesizeKey(key, aEvent, gScratchpadWindow);
+
+ is(linkClicked, true, "MDN page will open");
+ finishTest();
+}
+
+function finishTest()
+{
+ gScratchpadWindow.close();
+ finish();
+}
diff --git a/browser/devtools/scratchpad/test/browser_scratchpad_bug_651942_recent_files.js b/browser/devtools/scratchpad/test/browser_scratchpad_bug_651942_recent_files.js
new file mode 100644
index 000000000..3ab397650
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug_651942_recent_files.js
@@ -0,0 +1,355 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let tempScope = {};
+Cu.import("resource://gre/modules/NetUtil.jsm", tempScope);
+Cu.import("resource://gre/modules/FileUtils.jsm", tempScope);
+let NetUtil = tempScope.NetUtil;
+let FileUtils = tempScope.FileUtils;
+
+// Reference to the Scratchpad object.
+let gScratchpad;
+
+// References to the temporary nsIFiles.
+let gFile01;
+let gFile02;
+let gFile03;
+let gFile04;
+
+// lists of recent files.
+var lists = {
+ recentFiles01: null,
+ recentFiles02: null,
+ recentFiles03: null,
+ recentFiles04: null,
+};
+
+// Temporary file names.
+let gFileName01 = "file01_ForBug651942.tmp"
+let gFileName02 = "☕" // See bug 783858 for more information
+let gFileName03 = "file03_ForBug651942.tmp"
+let gFileName04 = "file04_ForBug651942.tmp"
+
+// Content for the temporary files.
+let gFileContent;
+let gFileContent01 = "hello.world.01('bug651942');";
+let gFileContent02 = "hello.world.02('bug651942');";
+let gFileContent03 = "hello.world.03('bug651942');";
+let gFileContent04 = "hello.world.04('bug651942');";
+
+function startTest()
+{
+ gScratchpad = gScratchpadWindow.Scratchpad;
+
+ gFile01 = createAndLoadTemporaryFile(gFile01, gFileName01, gFileContent01);
+ gFile02 = createAndLoadTemporaryFile(gFile02, gFileName02, gFileContent02);
+ gFile03 = createAndLoadTemporaryFile(gFile03, gFileName03, gFileContent03);
+}
+
+// Test to see if the three files we created in the 'startTest()'-method have
+// been added to the list of recent files.
+function testAddedToRecent()
+{
+ lists.recentFiles01 = gScratchpad.getRecentFiles();
+
+ is(lists.recentFiles01.length, 3,
+ "Temporary files created successfully and added to list of recent files.");
+
+ // Create a 4th file, this should clear the oldest file.
+ gFile04 = createAndLoadTemporaryFile(gFile04, gFileName04, gFileContent04);
+}
+
+// We have opened a 4th file. Test to see if the oldest recent file was removed,
+// and that the other files were reordered successfully.
+function testOverwriteRecent()
+{
+ lists.recentFiles02 = gScratchpad.getRecentFiles();
+
+ is(lists.recentFiles02[0], lists.recentFiles01[1],
+ "File02 was reordered successfully in the 'recent files'-list.");
+ is(lists.recentFiles02[1], lists.recentFiles01[2],
+ "File03 was reordered successfully in the 'recent files'-list.");
+ isnot(lists.recentFiles02[2], lists.recentFiles01[2],
+ "File04: was added successfully.");
+
+ // Open the oldest recent file.
+ gScratchpad.openFile(0);
+}
+
+// We have opened the "oldest"-recent file. Test to see if it is now the most
+// recent file, and that the other files were reordered successfully.
+function testOpenOldestRecent()
+{
+ lists.recentFiles03 = gScratchpad.getRecentFiles();
+
+ is(lists.recentFiles02[0], lists.recentFiles03[2],
+ "File04 was reordered successfully in the 'recent files'-list.");
+ is(lists.recentFiles02[1], lists.recentFiles03[0],
+ "File03 was reordered successfully in the 'recent files'-list.");
+ is(lists.recentFiles02[2], lists.recentFiles03[1],
+ "File02 was reordered successfully in the 'recent files'-list.");
+
+ Services.prefs.setIntPref("devtools.scratchpad.recentFilesMax", 0);
+}
+
+// The "devtools.scratchpad.recentFilesMax"-preference was set to zero (0).
+// This should disable the "Open Recent"-menu by hiding it (this should not
+// remove any files from the list). Test to see if it's been hidden.
+function testHideMenu()
+{
+ let menu = gScratchpadWindow.document.getElementById("sp-open_recent-menu");
+ ok(menu.hasAttribute("hidden"), "The menu was hidden successfully.");
+
+ Services.prefs.setIntPref("devtools.scratchpad.recentFilesMax", 2);
+}
+
+// We have set the recentFilesMax-pref to one (1), this enables the feature,
+// removes the two oldest files, rebuilds the menu and removes the
+// "hidden"-attribute from it. Test to see if this works.
+function testChangedMaxRecent()
+{
+ let menu = gScratchpadWindow.document.getElementById("sp-open_recent-menu");
+ ok(!menu.hasAttribute("hidden"), "The menu is visible. \\o/");
+
+ lists.recentFiles04 = gScratchpad.getRecentFiles();
+
+ is(lists.recentFiles04.length, 2,
+ "Two recent files were successfully removed from the 'recent files'-list");
+
+ let doc = gScratchpadWindow.document;
+ let popup = doc.getElementById("sp-menu-open_recentPopup");
+
+ let menuitemLabel = popup.children[0].getAttribute("label");
+ let correctMenuItem = false;
+ if (menuitemLabel === lists.recentFiles03[2] &&
+ menuitemLabel === lists.recentFiles04[1]) {
+ correctMenuItem = true;
+ }
+
+ is(correctMenuItem, true,
+ "Two recent files were successfully removed from the 'Open Recent'-menu");
+
+ // We now remove one file from the harddrive and use the recent-menuitem for
+ // it to make sure the user is notified that the file no longer exists.
+ // This is tested in testOpenDeletedFile().
+ gFile04.remove(false);
+
+ // Make sure the file has been deleted before continuing to avoid
+ // intermittent oranges.
+ waitForFileDeletion();
+}
+
+function waitForFileDeletion() {
+ if (gFile04.exists()) {
+ executeSoon(waitForFileDeletion);
+ return;
+ }
+
+ gFile04 = null;
+ gScratchpad.openFile(0);
+}
+
+// By now we should have two recent files stored in the list but one of the
+// files should be missing on the harddrive.
+function testOpenDeletedFile() {
+ let doc = gScratchpadWindow.document;
+ let popup = doc.getElementById("sp-menu-open_recentPopup");
+
+ is(gScratchpad.getRecentFiles().length, 1,
+ "The missing file was successfully removed from the list.");
+ // The number of recent files stored, plus the separator and the
+ // clearRecentMenuItems-item.
+ is(popup.children.length, 3,
+ "The missing file was successfully removed from the menu.");
+ ok(gScratchpad.notificationBox.currentNotification,
+ "The notification was successfully displayed.");
+ is(gScratchpad.notificationBox.currentNotification.label,
+ gScratchpad.strings.GetStringFromName("fileNoLongerExists.notification"),
+ "The notification label is correct.");
+
+ gScratchpad.clearRecentFiles();
+}
+
+// We have cleared the last file. Test to see if the last file was removed,
+// the menu is empty and was disabled successfully.
+function testClearedAll()
+{
+ let doc = gScratchpadWindow.document;
+ let menu = doc.getElementById("sp-open_recent-menu");
+ let popup = doc.getElementById("sp-menu-open_recentPopup");
+
+ is(gScratchpad.getRecentFiles().length, 0,
+ "All recent files removed successfully.");
+ is(popup.children.length, 0, "All menuitems removed successfully.");
+ ok(menu.hasAttribute("disabled"),
+ "No files in the menu, it was disabled successfully.");
+
+ finishTest();
+}
+
+function createAndLoadTemporaryFile(aFile, aFileName, aFileContent)
+{
+ // Create a temporary file.
+ aFile = FileUtils.getFile("TmpD", [aFileName]);
+ aFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0666);
+
+ // Write the temporary file.
+ let fout = Cc["@mozilla.org/network/file-output-stream;1"].
+ createInstance(Ci.nsIFileOutputStream);
+ fout.init(aFile.QueryInterface(Ci.nsILocalFile), 0x02 | 0x08 | 0x20,
+ 0644, fout.DEFER_OPEN);
+
+ gScratchpad.setFilename(aFile.path);
+ gScratchpad.importFromFile(aFile.QueryInterface(Ci.nsILocalFile), true,
+ fileImported);
+ gScratchpad.saveFile(fileSaved);
+
+ return aFile;
+}
+
+function fileImported(aStatus)
+{
+ ok(Components.isSuccessCode(aStatus),
+ "the temporary file was imported successfully with Scratchpad");
+}
+
+function fileSaved(aStatus)
+{
+ ok(Components.isSuccessCode(aStatus),
+ "the temporary file was saved successfully with Scratchpad");
+
+ checkIfMenuIsPopulated();
+}
+
+function checkIfMenuIsPopulated()
+{
+ let doc = gScratchpadWindow.document;
+ let expectedMenuitemCount = doc.getElementById("sp-menu-open_recentPopup").
+ children.length;
+ // The number of recent files stored, plus the separator and the
+ // clearRecentMenuItems-item.
+ let recentFilesPlusExtra = gScratchpad.getRecentFiles().length + 2;
+
+ if (expectedMenuitemCount > 2) {
+ is(expectedMenuitemCount, recentFilesPlusExtra,
+ "the recent files menu was populated successfully.");
+ }
+}
+
+/**
+ * The PreferenceObserver listens for preference changes while Scratchpad is
+ * running.
+ */
+var PreferenceObserver = {
+ _initialized: false,
+
+ _timesFired: 0,
+ set timesFired(aNewValue) {
+ this._timesFired = aNewValue;
+ },
+ get timesFired() {
+ return this._timesFired;
+ },
+
+ init: function PO_init()
+ {
+ if (this._initialized) {
+ return;
+ }
+
+ this.branch = Services.prefs.getBranch("devtools.scratchpad.");
+ this.branch.addObserver("", this, false);
+ this._initialized = true;
+ },
+
+ observe: function PO_observe(aMessage, aTopic, aData)
+ {
+ if (aTopic != "nsPref:changed") {
+ return;
+ }
+
+ switch (this.timesFired) {
+ case 0:
+ this.timesFired = 1;
+ break;
+ case 1:
+ this.timesFired = 2;
+ break;
+ case 2:
+ this.timesFired = 3;
+ testAddedToRecent();
+ break;
+ case 3:
+ this.timesFired = 4;
+ testOverwriteRecent();
+ break;
+ case 4:
+ this.timesFired = 5;
+ testOpenOldestRecent();
+ break;
+ case 5:
+ this.timesFired = 6;
+ testHideMenu();
+ break;
+ case 6:
+ this.timesFired = 7;
+ testChangedMaxRecent();
+ break;
+ case 7:
+ this.timesFired = 8;
+ testOpenDeletedFile();
+ break;
+ case 8:
+ this.timesFired = 9;
+ testClearedAll();
+ break;
+ }
+ },
+
+ uninit: function PO_uninit () {
+ this.branch.removeObserver("", this);
+ }
+};
+
+function test()
+{
+ waitForExplicitFinish();
+
+ registerCleanupFunction(function () {
+ gFile01.remove(false);
+ gFile01 = null;
+ gFile02.remove(false);
+ gFile02 = null;
+ gFile03.remove(false);
+ gFile03 = null;
+ // gFile04 was removed earlier.
+ lists.recentFiles01 = null;
+ lists.recentFiles02 = null;
+ lists.recentFiles03 = null;
+ lists.recentFiles04 = null;
+ gScratchpad = null;
+
+ PreferenceObserver.uninit();
+ Services.prefs.clearUserPref("devtools.scratchpad.recentFilesMax");
+ });
+
+ Services.prefs.setIntPref("devtools.scratchpad.recentFilesMax", 3);
+
+ // Initiate the preference observer after we have set the temporary recent
+ // files max for this test.
+ PreferenceObserver.init();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ openScratchpad(startTest);
+ }, true);
+
+ content.location = "data:text/html,<p>test recent files in Scratchpad";
+}
+
+function finishTest()
+{
+ finish();
+}
diff --git a/browser/devtools/scratchpad/test/browser_scratchpad_bug_653427_confirm_close.js b/browser/devtools/scratchpad/test/browser_scratchpad_bug_653427_confirm_close.js
new file mode 100644
index 000000000..cbcaf0ddf
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug_653427_confirm_close.js
@@ -0,0 +1,227 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let tempScope = {};
+Cu.import("resource://gre/modules/NetUtil.jsm", tempScope);
+Cu.import("resource://gre/modules/FileUtils.jsm", tempScope);
+let NetUtil = tempScope.NetUtil;
+let FileUtils = tempScope.FileUtils;
+
+// only finish() when correct number of tests are done
+const expected = 9;
+var count = 0;
+function done()
+{
+ if (++count == expected) {
+ cleanup();
+ finish();
+ }
+}
+
+var gFile;
+
+var oldPrompt = Services.prompt;
+var promptButton = -1;
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gFile = createTempFile("fileForBug653427.tmp");
+ writeFile(gFile, "text", testUnsaved.call(this));
+
+ Services.prompt = {
+ confirmEx: function() {
+ return promptButton;
+ }
+ };
+
+ testNew();
+ testSavedFile();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ content.location = "data:text/html,<p>test scratchpad save file prompt on closing";
+}
+
+function testNew()
+{
+ openScratchpad(function(win) {
+ win.Scratchpad.close(function() {
+ ok(win.closed, "new scratchpad window should close without prompting")
+ done();
+ });
+ }, {noFocus: true});
+}
+
+function testSavedFile()
+{
+ openScratchpad(function(win) {
+ win.Scratchpad.filename = "test.js";
+ win.Scratchpad.editor.dirty = false;
+ win.Scratchpad.close(function() {
+ ok(win.closed, "scratchpad from file with no changes should close")
+ done();
+ });
+ }, {noFocus: true});
+}
+
+function testUnsaved()
+{
+ function setFilename(aScratchpad, aFile) {
+ aScratchpad.setFilename(aFile);
+ }
+
+ testUnsavedFileCancel(setFilename);
+ testUnsavedFileSave(setFilename);
+ testUnsavedFileDontSave(setFilename);
+ testCancelAfterLoad();
+
+ function mockSaveFile(aScratchpad) {
+ let SaveFileStub = function (aCallback) {
+ /*
+ * An argument for aCallback must pass Components.isSuccessCode
+ *
+ * A version of isSuccessCode in JavaScript:
+ * function isSuccessCode(returnCode) {
+ * return (returnCode & 0x80000000) == 0;
+ * }
+ */
+ aCallback(1);
+ };
+
+ aScratchpad.saveFile = SaveFileStub;
+ }
+
+ // Run these tests again but this time without setting a filename to
+ // test that Scratchpad always asks for confirmation on dirty editor.
+ testUnsavedFileCancel(mockSaveFile);
+ testUnsavedFileSave(mockSaveFile);
+ testUnsavedFileDontSave();
+}
+
+function testUnsavedFileCancel(aCallback=function () {})
+{
+ openScratchpad(function(win) {
+ aCallback(win.Scratchpad, "test.js");
+ win.Scratchpad.editor.dirty = true;
+
+ promptButton = win.BUTTON_POSITION_CANCEL;
+
+ win.Scratchpad.close(function() {
+ ok(!win.closed, "cancelling dialog shouldn't close scratchpad");
+ win.close();
+ done();
+ });
+ }, {noFocus: true});
+}
+
+// Test a regression where our confirmation dialog wasn't appearing
+// after openFile calls. See bug 801982.
+function testCancelAfterLoad()
+{
+ openScratchpad(function(win) {
+ win.Scratchpad.setRecentFile(gFile);
+ win.Scratchpad.openFile(0);
+ win.Scratchpad.editor.dirty = true;
+ promptButton = win.BUTTON_POSITION_CANCEL;
+
+ let EventStub = {
+ called: false,
+ preventDefault: function() {
+ EventStub.called = true;
+ }
+ };
+
+ win.Scratchpad.onClose(EventStub, function() {
+ ok(!win.closed, "cancelling dialog shouldn't close scratchpad");
+ ok(EventStub.called, "aEvent.preventDefault was called");
+
+ win.Scratchpad.editor.dirty = false;
+ win.close();
+ done();
+ });
+ }, {noFocus: true});
+}
+
+function testUnsavedFileSave(aCallback=function () {})
+{
+ openScratchpad(function(win) {
+ win.Scratchpad.importFromFile(gFile, true, function(status, content) {
+ aCallback(win.Scratchpad, gFile.path);
+
+ let text = "new text";
+ win.Scratchpad.setText(text);
+
+ promptButton = win.BUTTON_POSITION_SAVE;
+
+ win.Scratchpad.close(function() {
+ ok(win.closed, 'pressing "Save" in dialog should close scratchpad');
+ readFile(gFile, function(savedContent) {
+ is(savedContent, text, 'prompted "Save" worked when closing scratchpad');
+ done();
+ });
+ });
+ });
+ }, {noFocus: true});
+}
+
+function testUnsavedFileDontSave(aCallback=function () {})
+{
+ openScratchpad(function(win) {
+ aCallback(win.Scratchpad, gFile.path);
+ win.Scratchpad.editor.dirty = true;
+
+ promptButton = win.BUTTON_POSITION_DONT_SAVE;
+
+ win.Scratchpad.close(function() {
+ ok(win.closed, 'pressing "Don\'t Save" in dialog should close scratchpad');
+ done();
+ });
+ }, {noFocus: true});
+}
+
+function cleanup()
+{
+ Services.prompt = oldPrompt;
+ gFile.remove(false);
+ gFile = null;
+}
+
+function createTempFile(name)
+{
+ let file = FileUtils.getFile("TmpD", [name]);
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0666);
+ file.QueryInterface(Ci.nsILocalFile)
+ return file;
+}
+
+function writeFile(file, content, callback)
+{
+ let fout = Cc["@mozilla.org/network/file-output-stream;1"].
+ createInstance(Ci.nsIFileOutputStream);
+ fout.init(file.QueryInterface(Ci.nsILocalFile), 0x02 | 0x08 | 0x20,
+ 0644, fout.DEFER_OPEN);
+
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
+ createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ let fileContentStream = converter.convertToInputStream(content);
+
+ NetUtil.asyncCopy(fileContentStream, fout, callback);
+}
+
+function readFile(file, callback)
+{
+ let channel = NetUtil.newChannel(file);
+ channel.contentType = "application/javascript";
+
+ NetUtil.asyncFetch(channel, function(inputStream, status) {
+ ok(Components.isSuccessCode(status),
+ "file was read successfully");
+
+ let content = NetUtil.readInputStreamToString(inputStream,
+ inputStream.available());
+ callback(content);
+ });
+}
diff --git a/browser/devtools/scratchpad/test/browser_scratchpad_bug_660560_tab.js b/browser/devtools/scratchpad/test/browser_scratchpad_bug_660560_tab.js
new file mode 100644
index 000000000..3687c8173
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug_660560_tab.js
@@ -0,0 +1,81 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ 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);
+
+ openScratchpad(runTests);
+ }, true);
+
+ content.location = "data:text/html,Scratchpad test for the Tab key, bug 660560";
+}
+
+function runTests()
+{
+ let sp = gScratchpadWindow.Scratchpad;
+ ok(sp, "Scratchpad object exists in new window");
+
+ 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");
+
+ is(sp.editor.getCaretOffset(), 5, "caret location is correct");
+
+ sp.editor.setCaretOffset(6);
+
+ EventUtils.synthesizeKey("VK_TAB", {}, gScratchpadWindow);
+
+ is(sp.getText(), " w indow.foo;",
+ "Tab key added 4 spaces");
+
+ is(sp.editor.getCaretOffset(), 10, "caret location is correct");
+
+ // Test the new insertTextAtCaret() method.
+
+ sp.insertTextAtCaret("omg");
+
+ 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);
+
+ openScratchpad(runTests2);
+}
+
+function runTests2()
+{
+ let sp = gScratchpadWindow.Scratchpad;
+
+ 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");
+
+ finish();
+}
diff --git a/browser/devtools/scratchpad/test/browser_scratchpad_bug_661762_wrong_window_focus.js b/browser/devtools/scratchpad/test/browser_scratchpad_bug_661762_wrong_window_focus.js
new file mode 100644
index 000000000..94342b048
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug_661762_wrong_window_focus.js
@@ -0,0 +1,94 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let tempScope = {};
+Cu.import("resource:///modules/HUDService.jsm", tempScope);
+let HUDService = tempScope.HUDService;
+
+function test()
+{
+ waitForExplicitFinish();
+
+ // To test for this bug we open a Scratchpad window, save its
+ // reference and then open another one. This way the first window
+ // loses its focus.
+ //
+ // Then we open a web console and execute a console.log statement
+ // from the first Scratch window (that's why we needed to save its
+ // reference).
+ //
+ // Then we wait for our message to appear in the console and click
+ // on the location link. After that we check which Scratchpad window
+ // is currently active (it should be the older one).
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+
+ openScratchpad(function () {
+ let sw = gScratchpadWindow;
+
+ openScratchpad(function () {
+ function onWebConsoleOpen(subj) {
+ Services.obs.removeObserver(onWebConsoleOpen,
+ "web-console-created");
+ subj.QueryInterface(Ci.nsISupportsString);
+
+ let hud = HUDService.getHudReferenceById(subj.data);
+ hud.jsterm.clearOutput(true);
+ executeSoon(testFocus.bind(null, sw, hud));
+ }
+
+ Services.obs.
+ addObserver(onWebConsoleOpen, "web-console-created", false);
+
+ HUDService.consoleUI.toggleHUD();
+ });
+ });
+ }, true);
+
+ content.location = "data:text/html;charset=utf8,<p>test window focus for Scratchpad.";
+}
+
+function testFocus(sw, hud) {
+ let sp = sw.Scratchpad;
+
+ function onMessage(subj) {
+ Services.obs.removeObserver(onMessage, "web-console-message-created");
+
+ var loc = hud.jsterm.outputNode.querySelector(".webconsole-location");
+ ok(loc, "location element exists");
+ is(loc.value, sw.Scratchpad.uniqueName + ":1",
+ "location value is correct");
+
+ sw.addEventListener("focus", function onFocus() {
+ sw.removeEventListener("focus", onFocus, true);
+
+ let win = Services.wm.getMostRecentWindow("devtools:scratchpad");
+
+ ok(win, "there are active Scratchpad windows");
+ is(win.Scratchpad.uniqueName, sw.Scratchpad.uniqueName,
+ "correct window is in focus");
+
+ // gScratchpadWindow will be closed automatically but we need to
+ // close the second window ourselves.
+ sw.close();
+ finish();
+ }, true);
+
+ // Simulate a click on the "Scratchpad/N:1" link.
+ EventUtils.synthesizeMouse(loc, 2, 2, {}, hud.iframeWindow);
+ }
+
+ // Sending messages to web console is an asynchronous operation. That's
+ // why we have to setup an observer here.
+ Services.obs.addObserver(onMessage, "web-console-message-created", false);
+
+ sp.setText("console.log('foo');");
+ sp.run().then(function ([selection, error, result]) {
+ is(selection, "console.log('foo');", "selection is correct");
+ is(error, undefined, "error is correct");
+ is(result, undefined, "result is correct");
+ });
+}
diff --git a/browser/devtools/scratchpad/test/browser_scratchpad_bug_669612_unsaved.js b/browser/devtools/scratchpad/test/browser_scratchpad_bug_669612_unsaved.js
new file mode 100644
index 000000000..15d4bb615
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug_669612_unsaved.js
@@ -0,0 +1,120 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// only finish() when correct number of tests are done
+const expected = 4;
+var count = 0;
+function done()
+{
+ if (++count == expected) {
+ finish();
+ }
+}
+
+var ScratchpadManager = Scratchpad.ScratchpadManager;
+
+
+function test()
+{
+ waitForExplicitFinish();
+
+ testListeners();
+ testRestoreNotFromFile();
+ testRestoreFromFileSaved();
+ testRestoreFromFileUnsaved();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ content.location = "data:text/html,<p>test star* UI for unsaved file changes";
+}
+
+function testListeners()
+{
+ openScratchpad(function(aWin, aScratchpad) {
+ aScratchpad.setText("new text");
+ ok(isStar(aWin), "show start if scratchpad text changes");
+
+ aScratchpad.editor.dirty = false;
+ ok(!isStar(aWin), "no star before changing text");
+
+ aScratchpad.setFilename("foo.js");
+ aScratchpad.setText("new text2");
+ ok(isStar(aWin), "shows star if scratchpad text changes");
+
+ aScratchpad.editor.dirty = false;
+ ok(!isStar(aWin), "no star if scratchpad was just saved");
+
+ aScratchpad.setText("new text3");
+ ok(isStar(aWin), "shows star if scratchpad has more changes");
+
+ aScratchpad.undo();
+ ok(!isStar(aWin), "no star if scratchpad undo to save point");
+
+ aScratchpad.undo();
+ ok(isStar(aWin), "star if scratchpad undo past save point");
+
+ aWin.close();
+ done();
+ }, {noFocus: true});
+}
+
+function testRestoreNotFromFile()
+{
+ let session = [{
+ text: "test1",
+ executionContext: 1
+ }];
+
+ let [win] = ScratchpadManager.restoreSession(session);
+ openScratchpad(function(aWin, aScratchpad) {
+ aScratchpad.setText("new text");
+ ok(isStar(win), "show star if restored scratchpad isn't from a file");
+
+ win.close();
+ done();
+ }, {window: win, noFocus: true});
+}
+
+function testRestoreFromFileSaved()
+{
+ let session = [{
+ filename: "test.js",
+ text: "test1",
+ executionContext: 1,
+ saved: true
+ }];
+
+ let [win] = ScratchpadManager.restoreSession(session);
+ openScratchpad(function(aWin, aScratchpad) {
+ ok(!isStar(win), "no star before changing text in scratchpad restored from file");
+
+ aScratchpad.setText("new text");
+ ok(isStar(win), "star when text changed from scratchpad restored from file");
+
+ win.close();
+ done();
+ }, {window: win, noFocus: true});
+}
+
+function testRestoreFromFileUnsaved()
+{
+ let session = [{
+ filename: "test.js",
+ text: "test1",
+ executionContext: 1,
+ saved: false
+ }];
+
+ let [win] = ScratchpadManager.restoreSession(session);
+ openScratchpad(function() {
+ ok(isStar(win), "star with scratchpad restored with unsaved text");
+
+ win.close();
+ done();
+ }, {window: win, noFocus: true});
+}
+
+function isStar(win)
+{
+ return win.document.title.match(/^\*[^\*]/);
+}
diff --git a/browser/devtools/scratchpad/test/browser_scratchpad_bug_679467_falsy.js b/browser/devtools/scratchpad/test/browser_scratchpad_bug_679467_falsy.js
new file mode 100644
index 000000000..08785a76b
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug_679467_falsy.js
@@ -0,0 +1,68 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ openScratchpad(testFalsy);
+ }, true);
+
+ content.location = "data:text/html,<p>test falsy display() values in Scratchpad";
+}
+
+function testFalsy()
+{
+ let scratchpad = gScratchpadWindow.Scratchpad;
+ verifyFalsies(scratchpad).then(function() {
+ scratchpad.setBrowserContext();
+ verifyFalsies(scratchpad).then(finish);
+ });
+}
+
+
+function verifyFalsies(scratchpad)
+{
+ let tests = [{
+ method: "display",
+ code: "undefined",
+ result: "undefined\n/*\nundefined\n*/",
+ label: "undefined is displayed"
+ },
+ {
+ method: "display",
+ code: "false",
+ result: "false\n/*\nfalse\n*/",
+ label: "false is displayed"
+ },
+ {
+ method: "display",
+ code: "0",
+ result: "0\n/*\n0\n*/",
+ label: "0 is displayed"
+ },
+ {
+ method: "display",
+ code: "null",
+ result: "null\n/*\nnull\n*/",
+ label: "null is displayed"
+ },
+ {
+ method: "display",
+ code: "NaN",
+ result: "NaN\n/*\nNaN\n*/",
+ label: "NaN is displayed"
+ },
+ {
+ method: "display",
+ code: "''",
+ result: "''\n/*\n\n*/",
+ label: "the empty string is displayed"
+ }];
+
+ return runAsyncTests(scratchpad, tests);
+}
diff --git 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
new file mode 100644
index 000000000..4befa8d69
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug_699130_edit_ui_updates.js
@@ -0,0 +1,187 @@
+/* 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 tempScope = {};
+Cu.import("resource:///modules/source-editor.jsm", tempScope);
+let SourceEditor = tempScope.SourceEditor;
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ openScratchpad(runTests);
+ }, true);
+
+ content.location = "data:text/html,test Edit menu updates Scratchpad - bug 699130";
+}
+
+function runTests()
+{
+ let sp = gScratchpadWindow.Scratchpad;
+ let doc = gScratchpadWindow.document;
+ let winUtils = gScratchpadWindow.QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIDOMWindowUtils);
+ let OS = Services.appinfo.OS;
+
+ info("will test the Edit menu");
+
+ let pass = 0;
+
+ sp.setText("bug 699130: hello world! (edit menu)");
+
+ let editMenu = doc.getElementById("sp-edit-menu");
+ ok(editMenu, "the Edit menu");
+ let menubar = editMenu.parentNode;
+ ok(menubar, "menubar found");
+
+ let editMenuIndex = -1;
+ for (let i = 0; i < menubar.children.length; i++) {
+ if (menubar.children[i] === editMenu) {
+ editMenuIndex = i;
+ break;
+ }
+ }
+ isnot(editMenuIndex, -1, "Edit menu index is correct");
+
+ let menuPopup = editMenu.menupopup;
+ ok(menuPopup, "the Edit menupopup");
+ let cutItem = doc.getElementById("se-menu-cut");
+ ok(cutItem, "the Cut menuitem");
+ let pasteItem = doc.getElementById("se-menu-paste");
+ ok(pasteItem, "the Paste menuitem");
+
+ let anchor = doc.documentElement;
+ let isContextMenu = false;
+
+ let openMenu = function(aX, aY, aCallback) {
+ if (!editMenu || OS != "Darwin") {
+ menuPopup.addEventListener("popupshown", function onPopupShown() {
+ menuPopup.removeEventListener("popupshown", onPopupShown, false);
+ executeSoon(aCallback);
+ }, false);
+ }
+
+ executeSoon(function() {
+ if (editMenu) {
+ if (OS == "Darwin") {
+ winUtils.forceUpdateNativeMenuAt(editMenuIndex);
+ executeSoon(aCallback);
+ } else {
+ editMenu.open = true;
+ }
+ } else {
+ menuPopup.openPopup(anchor, "overlap", aX, aY, isContextMenu, false);
+ }
+ });
+ };
+
+ let closeMenu = function(aCallback) {
+ if (!editMenu || OS != "Darwin") {
+ menuPopup.addEventListener("popuphidden", function onPopupHidden() {
+ menuPopup.removeEventListener("popuphidden", onPopupHidden, false);
+ executeSoon(aCallback);
+ }, false);
+ }
+
+ executeSoon(function() {
+ if (editMenu) {
+ if (OS == "Darwin") {
+ winUtils.forceUpdateNativeMenuAt(editMenuIndex);
+ executeSoon(aCallback);
+ } else {
+ editMenu.open = false;
+ }
+ } else {
+ menuPopup.hidePopup();
+ }
+ });
+ };
+
+ let firstShow = function() {
+ ok(cutItem.hasAttribute("disabled"), "cut menuitem is disabled");
+ closeMenu(firstHide);
+ };
+
+ let firstHide = function() {
+ sp.selectRange(0, 10);
+ openMenu(11, 11, showAfterSelect);
+ };
+
+ 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);
+ 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);
+ 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() {
+ ok(cutItem.hasAttribute("disabled"), "cut menuitem is disabled after paste");
+ ok(!pasteItem.hasAttribute("disabled"), "paste menuitem is enabled after paste");
+ closeMenu(hideAfterPaste);
+ };
+
+ let hideAfterPaste = function() {
+ if (pass == 0) {
+ pass++;
+ testContextMenu();
+ } else {
+ finish();
+ }
+ };
+
+ let testContextMenu = function() {
+ info("will test the context menu");
+
+ editMenu = null;
+ isContextMenu = true;
+
+ menuPopup = doc.getElementById("scratchpad-text-popup");
+ ok(menuPopup, "the context menupopup");
+ cutItem = doc.getElementById("se-cMenu-cut");
+ ok(cutItem, "the Cut menuitem");
+ pasteItem = doc.getElementById("se-cMenu-paste");
+ ok(pasteItem, "the Paste menuitem");
+
+ sp.setText("bug 699130: hello world! (context menu)");
+ openMenu(10, 10, firstShow);
+ };
+
+ openMenu(10, 10, firstShow);
+}
diff --git a/browser/devtools/scratchpad/test/browser_scratchpad_bug_751744_revert_to_saved.js b/browser/devtools/scratchpad/test/browser_scratchpad_bug_751744_revert_to_saved.js
new file mode 100644
index 000000000..9af0ad4f8
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug_751744_revert_to_saved.js
@@ -0,0 +1,137 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let tempScope = {};
+Cu.import("resource://gre/modules/NetUtil.jsm", tempScope);
+Cu.import("resource://gre/modules/FileUtils.jsm", tempScope);
+let NetUtil = tempScope.NetUtil;
+let FileUtils = tempScope.FileUtils;
+
+// Reference to the Scratchpad object.
+let gScratchpad;
+
+// Reference to the temporary nsIFiles.
+let gFile;
+
+// Temporary file name.
+let gFileName = "testFileForBug751744.tmp"
+
+
+// Content for the temporary file.
+let gFileContent = "/* this file is already saved */\n" +
+ "function foo() { alert('bar') }";
+let gLength = gFileContent.length;
+
+// Reference to the menu entry.
+let menu;
+
+function startTest()
+{
+ gScratchpad = gScratchpadWindow.Scratchpad;
+ menu = gScratchpadWindow.document.getElementById("sp-menu-revert");
+ createAndLoadTemporaryFile();
+}
+
+function testAfterSaved() {
+ // Check if the revert menu is disabled as the file is at saved state.
+ ok(menu.hasAttribute("disabled"), "The revert menu entry is disabled.");
+
+ // chancging the text in the file
+ gScratchpad.setText("\nfoo();", gLength, gLength);
+ // Checking the text got changed
+ is(gScratchpad.getText(), gFileContent + "\nfoo();",
+ "The text changed the first time.");
+
+ // Revert menu now should be enabled.
+ ok(!menu.hasAttribute("disabled"),
+ "The revert menu entry is enabled after changing text first time");
+
+ // reverting back to last saved state.
+ gScratchpad.revertFile(testAfterRevert);
+}
+
+function testAfterRevert() {
+ // Check if the file's text got reverted
+ is(gScratchpad.getText(), gFileContent,
+ "The text reverted back to original text.");
+ // The revert menu should be disabled again.
+ ok(menu.hasAttribute("disabled"),
+ "The revert menu entry is disabled after reverting.");
+
+ // chancging the text in the file again
+ gScratchpad.setText("\nalert(foo.toSource());", gLength, gLength);
+ // Saving the file.
+ gScratchpad.saveFile(testAfterSecondSave);
+}
+
+function testAfterSecondSave() {
+ // revert menu entry should be disabled.
+ ok(menu.hasAttribute("disabled"),
+ "The revert menu entry is disabled after saving.");
+
+ // changing the text.
+ gScratchpad.setText("\nfoo();", gLength + 23, gLength + 23);
+
+ // revert menu entry should get enabled yet again.
+ ok(!menu.hasAttribute("disabled"),
+ "The revert menu entry is enabled after changing text third time");
+
+ // reverting back to last saved state.
+ gScratchpad.revertFile(testAfterSecondRevert);
+}
+
+function testAfterSecondRevert() {
+ // Check if the file's text got reverted
+ is(gScratchpad.getText(), gFileContent + "\nalert(foo.toSource());",
+ "The text reverted back to the changed saved text.");
+ // The revert menu should be disabled again.
+ ok(menu.hasAttribute("disabled"),
+ "Revert menu entry is disabled after reverting to changed saved state.");
+ gFile.remove(false);
+ gFile = null;
+ gScratchpad = null;
+}
+
+function createAndLoadTemporaryFile()
+{
+ // Create a temporary file.
+ gFile = FileUtils.getFile("TmpD", [gFileName]);
+ gFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0666);
+
+ // Write the temporary file.
+ let fout = Cc["@mozilla.org/network/file-output-stream;1"].
+ createInstance(Ci.nsIFileOutputStream);
+ fout.init(gFile.QueryInterface(Ci.nsILocalFile), 0x02 | 0x08 | 0x20,
+ 0644, fout.DEFER_OPEN);
+
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
+ createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ let fileContentStream = converter.convertToInputStream(gFileContent);
+
+ NetUtil.asyncCopy(fileContentStream, fout, tempFileSaved);
+}
+
+function tempFileSaved(aStatus)
+{
+ ok(Components.isSuccessCode(aStatus),
+ "the temporary file was saved successfully");
+
+ // Import the file into Scratchpad.
+ gScratchpad.setFilename(gFile.path);
+ gScratchpad.importFromFile(gFile.QueryInterface(Ci.nsILocalFile), true,
+ testAfterSaved);
+}
+
+function test()
+{
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ openScratchpad(startTest);
+ }, true);
+
+ content.location = "data:text/html,<p>test reverting to last saved state of" +
+ " a file </p>";
+}
diff --git a/browser/devtools/scratchpad/test/browser_scratchpad_contexts.js b/browser/devtools/scratchpad/test/browser_scratchpad_contexts.js
new file mode 100644
index 000000000..6c0c684ae
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_contexts.js
@@ -0,0 +1,174 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ openScratchpad(runTests);
+ }, true);
+
+ content.location = "data:text/html,test context switch in Scratchpad";
+}
+
+
+function runTests()
+{
+ let sp = gScratchpadWindow.Scratchpad;
+ let contentMenu = gScratchpadWindow.document.getElementById("sp-menu-content");
+ let chromeMenu = gScratchpadWindow.document.getElementById("sp-menu-browser");
+ let notificationBox = sp.notificationBox;
+
+ ok(contentMenu, "found #sp-menu-content");
+ ok(chromeMenu, "found #sp-menu-browser");
+ ok(notificationBox, "found Scratchpad.notificationBox");
+
+ let tests = [{
+ method: "run",
+ prepare: function() {
+ sp.setContentContext();
+
+ is(sp.executionContext, gScratchpadWindow.SCRATCHPAD_CONTEXT_CONTENT,
+ "executionContext is content");
+
+ is(contentMenu.getAttribute("checked"), "true",
+ "content menuitem is checked");
+
+ isnot(chromeMenu.getAttribute("checked"), "true",
+ "chrome menuitem is not checked");
+
+ ok(!notificationBox.currentNotification,
+ "there is no notification in content context");
+
+ let dsp = sp.contentSandbox.__SCRATCHPAD__;
+
+ ok(sp.contentSandbox.__SCRATCHPAD__,
+ "there is a variable named __SCRATCHPAD__");
+
+ ok(sp.contentSandbox.__SCRATCHPAD__.editor,
+ "scratchpad is actually an instance of Scratchpad");
+
+ sp.setText("window.foobarBug636725 = 'aloha';");
+
+ ok(!content.wrappedJSObject.foobarBug636725,
+ "no content.foobarBug636725");
+ },
+ then: function() {
+ is(content.wrappedJSObject.foobarBug636725, "aloha",
+ "content.foobarBug636725 has been set");
+ }
+ },
+ {
+ method: "run",
+ prepare: function() {
+ sp.setBrowserContext();
+
+ is(sp.executionContext, gScratchpadWindow.SCRATCHPAD_CONTEXT_BROWSER,
+ "executionContext is chrome");
+
+ is(chromeMenu.getAttribute("checked"), "true",
+ "chrome menuitem is checked");
+
+ isnot(contentMenu.getAttribute("checked"), "true",
+ "content menuitem is not checked");
+
+ ok(sp.chromeSandbox.__SCRATCHPAD__,
+ "there is a variable named __SCRATCHPAD__");
+
+ ok(sp.chromeSandbox.__SCRATCHPAD__.editor,
+ "scratchpad is actually an instance of Scratchpad");
+
+ ok(notificationBox.currentNotification,
+ "there is a notification in browser context");
+
+ sp.setText("2'", 31, 32);
+
+ is(sp.getText(), "window.foobarBug636725 = 'aloha2';",
+ "setText() worked");
+ },
+ then: function() {
+ is(window.foobarBug636725, "aloha2",
+ "window.foobarBug636725 has been set");
+
+ delete window.foobarBug636725;
+ ok(!window.foobarBug636725, "no window.foobarBug636725");
+ }
+ },
+ {
+ method: "run",
+ prepare: function() {
+ sp.setText("gBrowser", 7);
+
+ is(sp.getText(), "window.gBrowser",
+ "setText() worked with no end for the replace range");
+ },
+ then: function([, , result]) {
+ is(typeof result.addTab, "function",
+ "chrome context has access to chrome objects");
+ }
+ },
+ {
+ method: "run",
+ prepare: function() {
+ // Check that the sandbox is cached.
+ sp.setText("typeof foobarBug636725cache;");
+ },
+ then: function([, , result]) {
+ is(result, "undefined", "global variable does not exist");
+ }
+ },
+ {
+ method: "run",
+ prepare: function() {
+ sp.setText("var foobarBug636725cache = 'foo';" +
+ "typeof foobarBug636725cache;");
+ },
+ then: function([, , result]) {
+ is(result, "string",
+ "global variable exists across two different executions");
+ }
+ },
+ {
+ method: "run",
+ prepare: function() {
+ sp.resetContext();
+ sp.setText("typeof foobarBug636725cache;");
+ },
+ then: function([, , result]) {
+ is(result, "undefined",
+ "global variable no longer exists after calling resetContext()");
+ }
+ },
+ {
+ method: "run",
+ prepare: function() {
+ sp.setText("var foobarBug636725cache2 = 'foo';" +
+ "typeof foobarBug636725cache2;");
+ },
+ then: function([, , result]) {
+ is(result, "string",
+ "global variable exists across two different executions");
+ }
+ },
+ {
+ method: "run",
+ prepare: function() {
+ sp.setContentContext();
+
+ is(sp.executionContext, gScratchpadWindow.SCRATCHPAD_CONTEXT_CONTENT,
+ "executionContext is content");
+
+ sp.setText("typeof foobarBug636725cache2;");
+ },
+ then: function([, , result]) {
+ is(result, "undefined",
+ "global variable no longer exists after changing the context");
+ }
+ }];
+
+ runAsyncCallbackTests(sp, tests).then(finish);
+} \ No newline at end of file
diff --git a/browser/devtools/scratchpad/test/browser_scratchpad_execute_print.js b/browser/devtools/scratchpad/test/browser_scratchpad_execute_print.js
new file mode 100644
index 000000000..a2c43c3ca
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_execute_print.js
@@ -0,0 +1,138 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onTabLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onTabLoad, true);
+ openScratchpad(runTests);
+ }, true);
+
+ content.location = "data:text/html,<p>test run() and display() in Scratchpad";
+}
+
+
+function runTests()
+{
+ let sp = gScratchpadWindow.Scratchpad;
+ let tests = [{
+ method: "run",
+ prepare: function() {
+ content.wrappedJSObject.foobarBug636725 = 1;
+ sp.setText("++window.foobarBug636725");
+ },
+ then: function([code, , result]) {
+ is(code, sp.getText(), "code is correct");
+ is(result, content.wrappedJSObject.foobarBug636725,
+ "result is correct");
+
+ is(sp.getText(), "++window.foobarBug636725",
+ "run() does not change the editor content");
+
+ is(content.wrappedJSObject.foobarBug636725, 2,
+ "run() updated window.foobarBug636725");
+ }
+ },
+ {
+ method: "display",
+ prepare: function() {},
+ then: function() {
+ is(content.wrappedJSObject.foobarBug636725, 3,
+ "display() updated window.foobarBug636725");
+
+ is(sp.getText(), "++window.foobarBug636725\n/*\n3\n*/",
+ "display() shows evaluation result in the textbox");
+
+ is(sp.selectedText, "\n/*\n3\n*/", "selectedText is correct");
+ }
+ },
+ {
+ method: "run",
+ prepare: function() {
+ let selection = sp.getSelectionRange();
+ is(selection.start, 24, "selection.start is correct");
+ is(selection.end, 32, "selection.end is correct");
+
+ // Test selection run() and display().
+
+ sp.setText("window.foobarBug636725 = 'a';\n" +
+ "window.foobarBug636725 = 'b';");
+
+ sp.selectRange(1, 2);
+
+ selection = sp.getSelectionRange();
+
+ is(selection.start, 1, "selection.start is 1");
+ is(selection.end, 2, "selection.end is 2");
+
+ sp.selectRange(0, 29);
+
+ selection = sp.getSelectionRange();
+
+ is(selection.start, 0, "selection.start is 0");
+ is(selection.end, 29, "selection.end is 29");
+ },
+ then: function([code, , result]) {
+ is(code, "window.foobarBug636725 = 'a';", "code is correct");
+ is(result, "a", "result is correct");
+
+ is(sp.getText(), "window.foobarBug636725 = 'a';\n" +
+ "window.foobarBug636725 = 'b';",
+ "run() does not change the textbox value");
+
+ is(content.wrappedJSObject.foobarBug636725, "a",
+ "run() worked for the selected range");
+ }
+ },
+ {
+ method: "display",
+ prepare: function() {
+ sp.setText("window.foobarBug636725 = 'c';\n" +
+ "window.foobarBug636725 = 'b';");
+
+ sp.selectRange(0, 22);
+ },
+ then: function() {
+ is(content.wrappedJSObject.foobarBug636725, "a",
+ "display() worked for the selected range");
+
+ is(sp.getText(), "window.foobarBug636725" +
+ "\n/*\na\n*/" +
+ " = 'c';\n" +
+ "window.foobarBug636725 = 'b';",
+ "display() shows evaluation result in the textbox");
+
+ is(sp.selectedText, "\n/*\na\n*/", "selectedText is correct");
+ }
+ }]
+
+
+ runAsyncCallbackTests(sp, tests).then(function() {
+ let selection = sp.getSelectionRange();
+ is(selection.start, 22, "selection.start is correct");
+ is(selection.end, 30, "selection.end is correct");
+
+ sp.deselect();
+
+ ok(!sp.selectedText, "selectedText is empty");
+
+ selection = sp.getSelectionRange();
+ is(selection.start, selection.end, "deselect() works");
+
+ // Test undo/redo.
+
+ 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");
+
+ finish();
+ });
+}
diff --git a/browser/devtools/scratchpad/test/browser_scratchpad_files.js b/browser/devtools/scratchpad/test/browser_scratchpad_files.js
new file mode 100644
index 000000000..e31af7946
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_files.js
@@ -0,0 +1,118 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let tempScope = {};
+Cu.import("resource://gre/modules/NetUtil.jsm", tempScope);
+let NetUtil = tempScope.NetUtil;
+
+// Reference to the Scratchpad object.
+let gScratchpad;
+
+// Reference to the temporary nsIFile we will work with.
+let gFile;
+
+// The temporary file content.
+let gFileContent = "hello.world('bug636725');";
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ openScratchpad(runTests);
+ }, true);
+
+ content.location = "data:text/html,<p>test file open and save in Scratchpad";
+}
+
+function runTests()
+{
+ gScratchpad = gScratchpadWindow.Scratchpad;
+
+ createTempFile("fileForBug636725.tmp", gFileContent, function(aStatus, aFile) {
+ ok(Components.isSuccessCode(aStatus),
+ "The temporary file was saved successfully");
+
+ gFile = aFile;
+ gScratchpad.importFromFile(gFile.QueryInterface(Ci.nsILocalFile), true,
+ fileImported);
+ });
+}
+
+function fileImported(aStatus, aFileContent)
+{
+ ok(Components.isSuccessCode(aStatus),
+ "the temporary file was imported successfully with Scratchpad");
+
+ is(aFileContent, gFileContent,
+ "received data is correct");
+
+ is(gScratchpad.getText(), gFileContent,
+ "the editor content is correct");
+
+ // Save the file after changes.
+ gFileContent += "// omg, saved!";
+ gScratchpad.setText(gFileContent);
+
+ gScratchpad.exportToFile(gFile.QueryInterface(Ci.nsILocalFile), true, true,
+ fileExported);
+}
+
+function fileExported(aStatus)
+{
+ ok(Components.isSuccessCode(aStatus),
+ "the temporary file was exported successfully with Scratchpad");
+
+ let oldContent = gFileContent;
+
+ // Attempt another file save, with confirmation which returns false.
+ gFileContent += "// omg, saved twice!";
+ gScratchpad.setText(gFileContent);
+
+ let oldConfirm = gScratchpadWindow.confirm;
+ let askedConfirmation = false;
+ gScratchpadWindow.confirm = function() {
+ askedConfirmation = true;
+ return false;
+ };
+
+ gScratchpad.exportToFile(gFile.QueryInterface(Ci.nsILocalFile), false, true,
+ fileExported2);
+
+ gScratchpadWindow.confirm = oldConfirm;
+
+ ok(askedConfirmation, "exportToFile() asked for overwrite confirmation");
+
+ gFileContent = oldContent;
+
+ let channel = NetUtil.newChannel(gFile);
+ channel.contentType = "application/javascript";
+
+ // Read back the temporary file.
+ NetUtil.asyncFetch(channel, fileRead);
+}
+
+function fileExported2()
+{
+ ok(false, "exportToFile() did not cancel file overwrite");
+}
+
+function fileRead(aInputStream, aStatus)
+{
+ ok(Components.isSuccessCode(aStatus),
+ "the temporary file was read back successfully");
+
+ let updatedContent =
+ NetUtil.readInputStreamToString(aInputStream, aInputStream.available());;
+
+ is(updatedContent, gFileContent, "file properly updated");
+
+ // Done!
+ gFile.remove(false);
+ gFile = null;
+ gScratchpad = null;
+ finish();
+}
diff --git a/browser/devtools/scratchpad/test/browser_scratchpad_initialization.js b/browser/devtools/scratchpad/test/browser_scratchpad_initialization.js
new file mode 100644
index 000000000..67bca826a
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_initialization.js
@@ -0,0 +1,48 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+
+ ok(window.Scratchpad, "Scratchpad variable exists");
+
+ openScratchpad(runTests);
+ }, true);
+
+ content.location = "data:text/html,initialization test for Scratchpad";
+}
+
+function runTests()
+{
+ let sp = gScratchpadWindow.Scratchpad;
+ ok(sp, "Scratchpad object exists in new window");
+ is(typeof sp.run, "function", "Scratchpad.run() exists");
+ is(typeof sp.inspect, "function", "Scratchpad.inspect() exists");
+ is(typeof sp.display, "function", "Scratchpad.display() exists");
+
+ let environmentMenu = gScratchpadWindow.document.
+ getElementById("sp-environment-menu");
+ ok(environmentMenu, "Environment menu element exists");
+ ok(environmentMenu.hasAttribute("hidden"),
+ "Environment menu is not visible");
+
+ let errorConsoleCommand = gScratchpadWindow.document.
+ getElementById("sp-cmd-errorConsole");
+ ok(errorConsoleCommand, "Error console command element exists");
+ is(errorConsoleCommand.getAttribute("disabled"), "true",
+ "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");
+
+ finish();
+}
diff --git a/browser/devtools/scratchpad/test/browser_scratchpad_inspect.js b/browser/devtools/scratchpad/test/browser_scratchpad_inspect.js
new file mode 100644
index 000000000..b4e81a991
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_inspect.js
@@ -0,0 +1,55 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ openScratchpad(runTests);
+ }, true);
+
+ content.location = "data:text/html;charset=utf8,<p>test inspect() in Scratchpad</p>";
+}
+
+function runTests()
+{
+ let sp = gScratchpadWindow.Scratchpad;
+
+ sp.setText("({ a: 'foobarBug636725' })");
+
+ sp.inspect().then(function() {
+ let sidebar = sp.sidebar;
+ ok(sidebar.visible, "sidebar is open");
+
+
+ let found = false;
+
+ outer: for (let scope in sidebar.variablesView) {
+ for (let [, obj] in scope) {
+ for (let [, prop] in obj) {
+ if (prop.name == "a" && prop.value == "foobarBug636725") {
+ found = true;
+ break outer;
+ }
+ }
+ }
+ }
+
+ ok(found, "found the property");
+
+ let tabbox = sidebar._sidebar._tabbox;
+ is(tabbox.width, 300, "Scratchpad sidebar width is correct");
+ ok(!tabbox.hasAttribute("hidden"), "Scratchpad sidebar visible");
+ sidebar.hide();
+ ok(tabbox.hasAttribute("hidden"), "Scratchpad sidebar hidden");
+ sp.inspect().then(function() {
+ is(tabbox.width, 300, "Scratchpad sidebar width is still correct");
+ ok(!tabbox.hasAttribute("hidden"), "Scratchpad sidebar visible again");
+ finish();
+ });
+ });
+}
diff --git a/browser/devtools/scratchpad/test/browser_scratchpad_open.js b/browser/devtools/scratchpad/test/browser_scratchpad_open.js
new file mode 100644
index 000000000..462d9ad2d
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_open.js
@@ -0,0 +1,76 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// only finish() when correct number of tests are done
+const expected = 3;
+var count = 0;
+var lastUniqueName = null;
+
+function done()
+{
+ if (++count == expected) {
+ finish();
+ }
+}
+
+function test()
+{
+ waitForExplicitFinish();
+ testOpen();
+ testOpenWithState();
+ testOpenInvalidState();
+}
+
+function testUniqueName(name)
+{
+ ok(name, "Scratchpad has a uniqueName");
+
+ if (lastUniqueName === null) {
+ lastUniqueName = name;
+ return;
+ }
+
+ ok(name !== lastUniqueName,
+ "Unique name for this instance differs from the last one.");
+}
+
+function testOpen()
+{
+ openScratchpad(function(win) {
+ is(win.Scratchpad.filename, undefined, "Default filename is undefined");
+ isnot(win.Scratchpad.getText(), null, "Default text should not be null");
+ is(win.Scratchpad.executionContext, win.SCRATCHPAD_CONTEXT_CONTENT,
+ "Default execution context is content");
+ testUniqueName(win.Scratchpad.uniqueName);
+
+ win.close();
+ done();
+ }, {noFocus: true});
+}
+
+function testOpenWithState()
+{
+ let state = {
+ filename: "testfile",
+ executionContext: 2,
+ text: "test text"
+ };
+
+ openScratchpad(function(win) {
+ 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");
+ testUniqueName(win.Scratchpad.uniqueName);
+
+ win.close();
+ done();
+ }, {state: state, noFocus: true});
+}
+
+function testOpenInvalidState()
+{
+ let win = openScratchpad(null, {state: 7});
+ ok(!win, "no scratchpad opened if state is not an object");
+ done();
+}
diff --git a/browser/devtools/scratchpad/test/browser_scratchpad_restore.js b/browser/devtools/scratchpad/test/browser_scratchpad_restore.js
new file mode 100644
index 000000000..a83c4213c
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_restore.js
@@ -0,0 +1,98 @@
+/* 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
+ openScratchpad(done, {state: state, noFocus: true});
+ }, 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) {
+ openScratchpad(function(aWin) {
+ let state = aWin.Scratchpad.getState();
+ aWin.close();
+ done(state);
+ }, {window: restoredWin, noFocus: true});
+ }, 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;
+ })
+ });
+}
diff --git a/browser/devtools/scratchpad/test/browser_scratchpad_tab_switch.js b/browser/devtools/scratchpad/test/browser_scratchpad_tab_switch.js
new file mode 100644
index 000000000..68314b930
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_tab_switch.js
@@ -0,0 +1,103 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let tab1;
+let tab2;
+let sp;
+
+function test()
+{
+ waitForExplicitFinish();
+
+ tab1 = gBrowser.addTab();
+ gBrowser.selectedTab = tab1;
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad1() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad1, true);
+
+ tab2 = gBrowser.addTab();
+ gBrowser.selectedTab = tab2;
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad2() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad2, true);
+ openScratchpad(runTests);
+ }, true);
+ content.location = "data:text/html,test context switch in Scratchpad tab 2";
+ }, true);
+
+ content.location = "data:text/html,test context switch in Scratchpad tab 1";
+}
+
+function runTests()
+{
+ sp = gScratchpadWindow.Scratchpad;
+
+ let contentMenu = gScratchpadWindow.document.getElementById("sp-menu-content");
+ let browserMenu = gScratchpadWindow.document.getElementById("sp-menu-browser");
+ let notificationBox = sp.notificationBox;
+
+ ok(contentMenu, "found #sp-menu-content");
+ ok(browserMenu, "found #sp-menu-browser");
+ ok(notificationBox, "found Scratchpad.notificationBox");
+
+ sp.setContentContext();
+
+ is(sp.executionContext, gScratchpadWindow.SCRATCHPAD_CONTEXT_CONTENT,
+ "executionContext is content");
+
+ is(contentMenu.getAttribute("checked"), "true",
+ "content menuitem is checked");
+
+ isnot(browserMenu.getAttribute("checked"), "true",
+ "chrome menuitem is not checked");
+
+ is(notificationBox.currentNotification, null,
+ "there is no notification currently shown for content context");
+
+ sp.setText("window.foosbug653108 = 'aloha';");
+
+ ok(!content.wrappedJSObject.foosbug653108,
+ "no content.foosbug653108");
+
+ sp.run().then(function() {
+ is(content.wrappedJSObject.foosbug653108, "aloha",
+ "content.foosbug653108 has been set");
+
+ gBrowser.tabContainer.addEventListener("TabSelect", runTests2, true);
+ gBrowser.selectedTab = tab1;
+ });
+}
+
+function runTests2() {
+ gBrowser.tabContainer.removeEventListener("TabSelect", runTests2, true);
+
+ ok(!window.foosbug653108, "no window.foosbug653108");
+
+ sp.setText("window.foosbug653108");
+ sp.run().then(function([, , result]) {
+ isnot(result, "aloha", "window.foosbug653108 is not aloha");
+
+ sp.setText("window.foosbug653108 = 'ahoyhoy';");
+ sp.run().then(function() {
+ is(content.wrappedJSObject.foosbug653108, "ahoyhoy",
+ "content.foosbug653108 has been set 2");
+
+ gBrowser.selectedBrowser.addEventListener("load", runTests3, true);
+ content.location = "data:text/html,test context switch in Scratchpad location 2";
+ });
+ });
+}
+
+function runTests3() {
+ gBrowser.selectedBrowser.removeEventListener("load", runTests3, true);
+ // Check that the sandbox is not cached.
+
+ sp.setText("typeof foosbug653108;");
+ sp.run().then(function([, , result]) {
+ is(result, "undefined", "global variable does not exist");
+
+ tab1 = null;
+ tab2 = null;
+ sp = null;
+ finish();
+ });
+}
diff --git a/browser/devtools/scratchpad/test/browser_scratchpad_ui.js b/browser/devtools/scratchpad/test/browser_scratchpad_ui.js
new file mode 100644
index 000000000..a41aea149
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_ui.js
@@ -0,0 +1,70 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ openScratchpad(runTests);
+ }, true);
+
+ content.location = "data:text/html,<title>foobarBug636725</title>" +
+ "<p>test inspect() in Scratchpad";
+}
+
+function runTests()
+{
+ let sp = gScratchpadWindow.Scratchpad;
+ let doc = gScratchpadWindow.document;
+
+ let methodsAndItems = {
+ "sp-menu-newscratchpad": "openScratchpad",
+ "sp-menu-open": "openFile",
+ "sp-menu-save": "saveFile",
+ "sp-menu-saveas": "saveFileAs",
+ "sp-text-run": "run",
+ "sp-text-inspect": "inspect",
+ "sp-text-display": "display",
+ "sp-text-resetContext": "resetContext",
+ "sp-menu-content": "setContentContext",
+ "sp-menu-browser": "setBrowserContext",
+ };
+
+ let lastMethodCalled = null;
+ sp.__noSuchMethod__ = function(aMethodName) {
+ lastMethodCalled = aMethodName;
+ };
+
+ for (let id in methodsAndItems) {
+ lastMethodCalled = null;
+
+ let methodName = methodsAndItems[id];
+ let oldMethod = sp[methodName];
+ ok(oldMethod, "found method " + methodName + " in Scratchpad object");
+
+ delete sp[methodName];
+
+ let menu = doc.getElementById(id);
+ ok(menu, "found menuitem #" + id);
+
+ try {
+ menu.doCommand();
+ }
+ catch (ex) {
+ ok(false, "exception thrown while executing the command of menuitem #" + id);
+ }
+
+ ok(lastMethodCalled == methodName,
+ "method " + methodName + " invoked by the associated menuitem");
+
+ sp[methodName] = oldMethod;
+ }
+
+ delete sp.__noSuchMethod__;
+
+ finish();
+}
diff --git a/browser/devtools/scratchpad/test/head.js b/browser/devtools/scratchpad/test/head.js
new file mode 100644
index 000000000..07dfd0239
--- /dev/null
+++ b/browser/devtools/scratchpad/test/head.js
@@ -0,0 +1,197 @@
+/* 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 tempScope = {};
+
+Cu.import("resource://gre/modules/NetUtil.jsm", tempScope);
+Cu.import("resource://gre/modules/FileUtils.jsm", tempScope);
+Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", tempScope);
+
+
+let NetUtil = tempScope.NetUtil;
+let FileUtils = tempScope.FileUtils;
+let Promise = tempScope.Promise;
+
+let gScratchpadWindow; // Reference to the Scratchpad chrome window object
+
+/**
+ * Open a Scratchpad window.
+ *
+ * @param function aReadyCallback
+ * Optional. The function you want invoked when the Scratchpad instance
+ * is ready.
+ * @param object aOptions
+ * Optional. Options for opening the scratchpad:
+ * - window
+ * Provide this if there's already a Scratchpad window you want to wait
+ * loading for.
+ * - state
+ * Scratchpad state object. This is used when Scratchpad is open.
+ * - noFocus
+ * Boolean that tells you do not want the opened window to receive
+ * focus.
+ * @return nsIDOMWindow
+ * The new window object that holds Scratchpad. Note that the
+ * gScratchpadWindow global is also updated to reference the new window
+ * object.
+ */
+function openScratchpad(aReadyCallback, aOptions)
+{
+ aOptions = aOptions || {};
+
+ let win = aOptions.window ||
+ Scratchpad.ScratchpadManager.openScratchpad(aOptions.state);
+ if (!win) {
+ return;
+ }
+
+ let onLoad = function() {
+ win.removeEventListener("load", onLoad, false);
+
+ win.Scratchpad.addObserver({
+ onReady: function(aScratchpad) {
+ aScratchpad.removeObserver(this);
+
+ if (aOptions.noFocus) {
+ aReadyCallback(win, aScratchpad);
+ } else {
+ waitForFocus(aReadyCallback.bind(null, win, aScratchpad), win);
+ }
+ }
+ });
+ };
+
+ if (aReadyCallback) {
+ win.addEventListener("load", onLoad, false);
+ }
+
+ gScratchpadWindow = win;
+ return gScratchpadWindow;
+}
+
+/**
+ * Create a temporary file, write to it and call a callback
+ * when done.
+ *
+ * @param string aName
+ * Name of your temporary file.
+ * @param string aContent
+ * Temporary file's contents.
+ * @param function aCallback
+ * Optional callback to be called when we're done writing
+ * to the file. It will receive two parameters: status code
+ * and a file object.
+ */
+function createTempFile(aName, aContent, aCallback=function(){})
+{
+ // Create a temporary file.
+ let file = FileUtils.getFile("TmpD", [aName]);
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("666", 8));
+
+ // Write the temporary file.
+ let fout = Cc["@mozilla.org/network/file-output-stream;1"].
+ createInstance(Ci.nsIFileOutputStream);
+ fout.init(file.QueryInterface(Ci.nsILocalFile), 0x02 | 0x08 | 0x20,
+ parseInt("644", 8), fout.DEFER_OPEN);
+
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
+ createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ let fileContentStream = converter.convertToInputStream(aContent);
+
+ NetUtil.asyncCopy(fileContentStream, fout, function (aStatus) {
+ aCallback(aStatus, file);
+ });
+}
+
+/**
+ * Run a set of asychronous tests sequentially defined by input and output.
+ *
+ * @param Scratchpad aScratchpad
+ * The scratchpad to use in running the tests.
+ * @param array aTests
+ * An array of test objects, each with the following properties:
+ * - method
+ * Scratchpad method to use, one of "run", "display", or "inspect".
+ * - code
+ * Code to run in the scratchpad.
+ * - result
+ * Expected code that will be in the scratchpad upon completion.
+ * - label
+ * The tests label which will be logged in the test runner output.
+ * @return Promise
+ * The promise that will be resolved when all tests are finished.
+ */
+function runAsyncTests(aScratchpad, aTests)
+{
+ let deferred = Promise.defer();
+
+ (function runTest() {
+ if (aTests.length) {
+ let test = aTests.shift();
+ aScratchpad.setText(test.code);
+ aScratchpad[test.method]().then(function success() {
+ is(aScratchpad.getText(), test.result, test.label);
+ runTest();
+ }, function failure(error) {
+ ok(false, error.stack + " " + test.label);
+ runTest();
+ });
+ } else {
+ deferred.resolve();
+ }
+ })();
+
+ return deferred.promise;
+}
+
+/**
+ * Run a set of asychronous tests sequentially with callbacks to prepare each
+ * test and to be called when the test result is ready.
+ *
+ * @param Scratchpad aScratchpad
+ * The scratchpad to use in running the tests.
+ * @param array aTests
+ * An array of test objects, each with the following properties:
+ * - method
+ * Scratchpad method to use, one of "run", "display", or "inspect".
+ * - prepare
+ * The callback to run just prior to executing the scratchpad method.
+ * - then
+ * The callback to run when the scratchpad execution promise resolves.
+ * @return Promise
+ * The promise that will be resolved when all tests are finished.
+ */
+function runAsyncCallbackTests(aScratchpad, aTests)
+{
+ let deferred = Promise.defer();
+
+ (function runTest() {
+ if (aTests.length) {
+ let test = aTests.shift();
+ test.prepare();
+ aScratchpad[test.method]().then(test.then.bind(test)).then(runTest);
+ } else {
+ deferred.resolve();
+ }
+ })();
+
+ return deferred.promise;
+}
+
+
+function cleanup()
+{
+ if (gScratchpadWindow) {
+ gScratchpadWindow.close();
+ gScratchpadWindow = null;
+ }
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+}
+
+registerCleanupFunction(cleanup);
diff --git a/browser/devtools/scratchpad/test/moz.build b/browser/devtools/scratchpad/test/moz.build
new file mode 100644
index 000000000..895d11993
--- /dev/null
+++ b/browser/devtools/scratchpad/test/moz.build
@@ -0,0 +1,6 @@
+# -*- 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/.
+