diff options
author | wolfbeast <mcwerewolf@gmail.com> | 2014-05-21 11:38:25 +0200 |
---|---|---|
committer | wolfbeast <mcwerewolf@gmail.com> | 2014-05-21 11:38:25 +0200 |
commit | d25ba7d760b017b038e5aa6c0a605b4a330eb68d (patch) | |
tree | 16ec27edc7d5f83986f16236d3a36a2682a0f37e /browser/devtools/scratchpad | |
parent | a942906574671868daf122284a9c4689e6924f74 (diff) | |
download | palemoon-gre-d25ba7d760b017b038e5aa6c0a605b4a330eb68d.tar.gz |
Recommit working copy to repo with proper line endings.
Diffstat (limited to 'browser/devtools/scratchpad')
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/. + |