diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /devtools/client/scratchpad | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'devtools/client/scratchpad')
49 files changed, 7023 insertions, 0 deletions
diff --git a/devtools/client/scratchpad/moz.build b/devtools/client/scratchpad/moz.build new file mode 100644 index 000000000..da8257c11 --- /dev/null +++ b/devtools/client/scratchpad/moz.build @@ -0,0 +1,13 @@ +# -*- Mode: python; 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/. + +DevToolsModules( + 'scratchpad-commands.js', + 'scratchpad-manager.jsm', + 'scratchpad-panel.js', +) + +BROWSER_CHROME_MANIFESTS += ['test/browser.ini'] diff --git a/devtools/client/scratchpad/scratchpad-commands.js b/devtools/client/scratchpad/scratchpad-commands.js new file mode 100644 index 000000000..8ae1fc4da --- /dev/null +++ b/devtools/client/scratchpad/scratchpad-commands.js @@ -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/. */ + +"use strict"; + +const l10n = require("gcli/l10n"); +const {Cu} = require("chrome"); + +exports.items = [{ + item: "command", + runAt: "client", + name: "scratchpad", + buttonId: "command-button-scratchpad", + buttonClass: "command-button command-button-invertable", + tooltipText: l10n.lookup("scratchpadOpenTooltip"), + hidden: true, + exec: function (args, context) { + const {ScratchpadManager} = Cu.import("resource://devtools/client/scratchpad/scratchpad-manager.jsm", {}); + ScratchpadManager.openScratchpad(); + } +}]; diff --git a/devtools/client/scratchpad/scratchpad-manager.jsm b/devtools/client/scratchpad/scratchpad-manager.jsm new file mode 100644 index 000000000..5b4b3bd0a --- /dev/null +++ b/devtools/client/scratchpad/scratchpad-manager.jsm @@ -0,0 +1,185 @@ +/* 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://devtools/content/scratchpad/scratchpad.xul"; +const SCRATCHPAD_WINDOW_FEATURES = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no"; + +const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {}); +const Services = require("Services"); +const Telemetry = require("devtools/client/shared/telemetry"); + + +/** + * 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: [], + + _telemetry: new Telemetry(), + + /** + * 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, this.createUid()); + + 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); + + this._telemetry.toolOpened("scratchpad-window"); + let onClose = () => { + this._telemetry.toolClosed("scratchpad-window"); + }; + win.addEventListener("unload", onClose); + + // Only add the shutdown observer if we've opened a scratchpad window. + ShutdownObserver.init(); + + return win; + }, + + /** + * Create a unique ID for a new Scratchpad. + */ + createUid: function SPM_createUid() + { + return JSON.stringify(this._nextUid++); + } +}; + + +/** + * 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"); + } +}; diff --git a/devtools/client/scratchpad/scratchpad-panel.js b/devtools/client/scratchpad/scratchpad-panel.js new file mode 100644 index 000000000..6f92585f7 --- /dev/null +++ b/devtools/client/scratchpad/scratchpad-panel.js @@ -0,0 +1,56 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 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"; + +const {Cu} = require("chrome"); +const EventEmitter = require("devtools/shared/event-emitter"); +const promise = require("promise"); + + +function ScratchpadPanel(iframeWindow, toolbox) { + let { Scratchpad } = iframeWindow; + this._toolbox = toolbox; + this.panelWin = iframeWindow; + this.scratchpad = Scratchpad; + + Scratchpad.target = this.target; + Scratchpad.hideMenu(); + + let deferred = promise.defer(); + this._readyObserver = deferred.promise; + Scratchpad.addObserver({ + onReady: function () { + Scratchpad.removeObserver(this); + deferred.resolve(); + } + }); + + EventEmitter.decorate(this); +} +exports.ScratchpadPanel = ScratchpadPanel; + +ScratchpadPanel.prototype = { + /** + * Open is effectively an asynchronous constructor. For the ScratchpadPanel, + * by the time this is called, the Scratchpad will already be ready. + */ + open: function () { + return this._readyObserver.then(() => { + this.isReady = true; + this.emit("ready"); + return this; + }); + }, + + get target() { + return this._toolbox.target; + }, + + destroy: function () { + this.emit("destroyed"); + return promise.resolve(); + } +}; diff --git a/devtools/client/scratchpad/scratchpad.js b/devtools/client/scratchpad/scratchpad.js new file mode 100644 index 000000000..306b635df --- /dev/null +++ b/devtools/client/scratchpad/scratchpad.js @@ -0,0 +1,2480 @@ +/* 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"; + +var Cu = Components.utils; +var Cc = Components.classes; +var Ci = Components.interfaces; + +const SCRATCHPAD_CONTEXT_CONTENT = 1; +const SCRATCHPAD_CONTEXT_BROWSER = 2; +const BUTTON_POSITION_SAVE = 0; +const BUTTON_POSITION_CANCEL = 1; +const BUTTON_POSITION_DONT_SAVE = 2; +const BUTTON_POSITION_REVERT = 0; +const EVAL_FUNCTION_TIMEOUT = 1000; // milliseconds + +const MAXIMUM_FONT_SIZE = 96; +const MINIMUM_FONT_SIZE = 6; +const NORMAL_FONT_SIZE = 12; + +const SCRATCHPAD_L10N = "chrome://devtools/locale/scratchpad.properties"; +const DEVTOOLS_CHROME_ENABLED = "devtools.chrome.enabled"; +const PREF_RECENT_FILES_MAX = "devtools.scratchpad.recentFilesMax"; +const SHOW_LINE_NUMBERS = "devtools.scratchpad.lineNumbers"; +const WRAP_TEXT = "devtools.scratchpad.wrapText"; +const SHOW_TRAILING_SPACE = "devtools.scratchpad.showTrailingSpace"; +const EDITOR_FONT_SIZE = "devtools.scratchpad.editorFontSize"; +const ENABLE_AUTOCOMPLETION = "devtools.scratchpad.enableAutocompletion"; +const TAB_SIZE = "devtools.editor.tabsize"; +const FALLBACK_CHARSET_LIST = "intl.fallbackCharsetList.ISO-8859-1"; + +const VARIABLES_VIEW_URL = "chrome://devtools/content/shared/widgets/VariablesView.xul"; + +const {require, loader} = Cu.import("resource://devtools/shared/Loader.jsm", {}); + +const Editor = require("devtools/client/sourceeditor/editor"); +const TargetFactory = require("devtools/client/framework/target").TargetFactory; +const EventEmitter = require("devtools/shared/event-emitter"); +const {DevToolsWorker} = require("devtools/shared/worker/worker"); +const DevToolsUtils = require("devtools/shared/DevToolsUtils"); +const flags = require("devtools/shared/flags"); +const promise = require("promise"); +const Services = require("Services"); +const {gDevTools} = require("devtools/client/framework/devtools"); +const {Heritage} = require("devtools/client/shared/widgets/view-helpers"); + +const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm"); +const {NetUtil} = require("resource://gre/modules/NetUtil.jsm"); +const {ScratchpadManager} = require("resource://devtools/client/scratchpad/scratchpad-manager.jsm"); +const {addDebuggerToGlobal} = require("resource://gre/modules/jsdebugger.jsm"); +const {OS} = require("resource://gre/modules/osfile.jsm"); +const {Reflect} = require("resource://gre/modules/reflect.jsm"); + +XPCOMUtils.defineConstant(this, "SCRATCHPAD_CONTEXT_CONTENT", SCRATCHPAD_CONTEXT_CONTENT); +XPCOMUtils.defineConstant(this, "SCRATCHPAD_CONTEXT_BROWSER", SCRATCHPAD_CONTEXT_BROWSER); +XPCOMUtils.defineConstant(this, "BUTTON_POSITION_SAVE", BUTTON_POSITION_SAVE); +XPCOMUtils.defineConstant(this, "BUTTON_POSITION_CANCEL", BUTTON_POSITION_CANCEL); +XPCOMUtils.defineConstant(this, "BUTTON_POSITION_DONT_SAVE", BUTTON_POSITION_DONT_SAVE); +XPCOMUtils.defineConstant(this, "BUTTON_POSITION_REVERT", BUTTON_POSITION_REVERT); + +XPCOMUtils.defineLazyModuleGetter(this, "VariablesView", + "resource://devtools/client/shared/widgets/VariablesView.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "VariablesViewController", + "resource://devtools/client/shared/widgets/VariablesViewController.jsm"); + +loader.lazyRequireGetter(this, "DebuggerServer", "devtools/server/main", true); + +loader.lazyRequireGetter(this, "DebuggerClient", "devtools/shared/client/main", true); +loader.lazyRequireGetter(this, "EnvironmentClient", "devtools/shared/client/main", true); +loader.lazyRequireGetter(this, "ObjectClient", "devtools/shared/client/main", true); +loader.lazyRequireGetter(this, "HUDService", "devtools/client/webconsole/hudservice"); + +XPCOMUtils.defineLazyGetter(this, "REMOTE_TIMEOUT", () => + Services.prefs.getIntPref("devtools.debugger.remote-timeout")); + +XPCOMUtils.defineLazyModuleGetter(this, "ShortcutUtils", + "resource://gre/modules/ShortcutUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "Reflect", + "resource://gre/modules/reflect.jsm"); + +var WebConsoleUtils = require("devtools/client/webconsole/utils").Utils; + +/** + * The scratchpad object handles the Scratchpad window functionality. + */ +var Scratchpad = { + _instanceId: null, + _initialWindowTitle: document.title, + _dirty: false, + + /** + * 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; + }, + + /** + * Add the event listeners for popupshowing events. + */ + _setupPopupShowingListeners: function SP_setupPopupShowing() { + let elementIDs = ["sp-menu_editpopup", "scratchpad-text-popup"]; + + for (let elementID of elementIDs) { + let elem = document.getElementById(elementID); + if (elem) { + elem.addEventListener("popupshowing", function () { + goUpdateGlobalEditMenuItems(); + let commands = ["cmd_undo", "cmd_redo", "cmd_delete", "cmd_findAgain"]; + commands.forEach(goUpdateCommand); + }); + } + } + }, + + /** + * Add the event event listeners for command events. + */ + _setupCommandListeners: function SP_setupCommands() { + let commands = { + "cmd_find": () => { + goDoCommand("cmd_find"); + }, + "cmd_findAgain": () => { + goDoCommand("cmd_findAgain"); + }, + "cmd_gotoLine": () => { + goDoCommand("cmd_gotoLine"); + }, + "sp-cmd-newWindow": () => { + Scratchpad.openScratchpad(); + }, + "sp-cmd-openFile": () => { + Scratchpad.openFile(); + }, + "sp-cmd-clearRecentFiles": () => { + Scratchpad.clearRecentFiles(); + }, + "sp-cmd-save": () => { + Scratchpad.saveFile(); + }, + "sp-cmd-saveas": () => { + Scratchpad.saveFileAs(); + }, + "sp-cmd-revert": () => { + Scratchpad.promptRevert(); + }, + "sp-cmd-close": () => { + Scratchpad.close(); + }, + "sp-cmd-run": () => { + Scratchpad.run(); + }, + "sp-cmd-inspect": () => { + Scratchpad.inspect(); + }, + "sp-cmd-display": () => { + Scratchpad.display(); + }, + "sp-cmd-pprint": () => { + Scratchpad.prettyPrint(); + }, + "sp-cmd-contentContext": () => { + Scratchpad.setContentContext(); + }, + "sp-cmd-browserContext": () => { + Scratchpad.setBrowserContext(); + }, + "sp-cmd-reloadAndRun": () => { + Scratchpad.reloadAndRun(); + }, + "sp-cmd-evalFunction": () => { + Scratchpad.evalTopLevelFunction(); + }, + "sp-cmd-errorConsole": () => { + Scratchpad.openErrorConsole(); + }, + "sp-cmd-webConsole": () => { + Scratchpad.openWebConsole(); + }, + "sp-cmd-documentationLink": () => { + Scratchpad.openDocumentationPage(); + }, + "sp-cmd-hideSidebar": () => { + Scratchpad.sidebar.hide(); + }, + "sp-cmd-line-numbers": () => { + Scratchpad.toggleEditorOption("lineNumbers", SHOW_LINE_NUMBERS); + }, + "sp-cmd-wrap-text": () => { + Scratchpad.toggleEditorOption("lineWrapping", WRAP_TEXT); + }, + "sp-cmd-highlight-trailing-space": () => { + Scratchpad.toggleEditorOption("showTrailingSpace", SHOW_TRAILING_SPACE); + }, + "sp-cmd-larger-font": () => { + Scratchpad.increaseFontSize(); + }, + "sp-cmd-smaller-font": () => { + Scratchpad.decreaseFontSize(); + }, + "sp-cmd-normal-font": () => { + Scratchpad.normalFontSize(); + }, + }; + + for (let command in commands) { + let elem = document.getElementById(command); + if (elem) { + elem.addEventListener("command", commands[command]); + } + } + }, + + /** + * Check or uncheck view menu items according to stored preferences. + */ + _updateViewMenuItems: function SP_updateViewMenuItems() { + this._updateViewMenuItem(SHOW_LINE_NUMBERS, "sp-menu-line-numbers"); + this._updateViewMenuItem(WRAP_TEXT, "sp-menu-word-wrap"); + this._updateViewMenuItem(SHOW_TRAILING_SPACE, "sp-menu-highlight-trailing-space"); + this._updateViewFontMenuItem(MINIMUM_FONT_SIZE, "sp-cmd-smaller-font"); + this._updateViewFontMenuItem(MAXIMUM_FONT_SIZE, "sp-cmd-larger-font"); + }, + + /** + * Check or uncheck view menu item according to stored preferences. + */ + _updateViewMenuItem: function SP_updateViewMenuItem(preferenceName, menuId) { + let checked = Services.prefs.getBoolPref(preferenceName); + if (checked) { + document.getElementById(menuId).setAttribute("checked", true); + } else { + document.getElementById(menuId).removeAttribute("checked"); + } + }, + + /** + * Disable view menu item if the stored font size is equals to the given one. + */ + _updateViewFontMenuItem: function SP_updateViewFontMenuItem(fontSize, commandId) { + let prefFontSize = Services.prefs.getIntPref(EDITOR_FONT_SIZE); + if (prefFontSize === fontSize) { + document.getElementById(commandId).setAttribute("disabled", true); + } + }, + + /** + * 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, + + /** + * Returns the 'dirty' state of this Scratchpad. + */ + get dirty() + { + let clean = this.editor && this.editor.isClean(); + return this._dirty || !clean; + }, + + /** + * Sets the 'dirty' state of this Scratchpad. + */ + set dirty(aValue) + { + this._dirty = aValue; + if (!aValue && this.editor) + this.editor.setClean(); + this._updateTitle(); + }, + + /** + * Retrieve the xul:notificationbox DOM element. It notifies the user when + * the current code execution context is SCRATCHPAD_CONTEXT_BROWSER. + */ + get notificationBox() + { + return document.getElementById("scratchpad-notificationbox"); + }, + + /** + * Hide the menu bar. + */ + hideMenu: function SP_hideMenu() + { + document.getElementById("sp-menubar").style.display = "none"; + }, + + /** + * Show the menu bar. + */ + showMenu: function SP_showMenu() + { + document.getElementById("sp-menubar").style.display = ""; + }, + + /** + * 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) + { + var value = this.editor.getText(); + return value.slice(aStart || 0, aEnd || value.length); + }, + + /** + * 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.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.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); + + this.dirty = !aState.saved; + + if (aState.executionContext == SCRATCHPAD_CONTEXT_BROWSER) + this.setBrowserContext(); + else + this.setContentContext(); + }, + + /** + * Get the most recent main chrome browser window + */ + get browserWindow() + { + return Services.wm.getMostRecentWindow(gDevTools.chromeWindowType); + }, + + /** + * Get the gBrowser object of the most recent browser window. + */ + get gBrowser() + { + let recentWin = this.browserWindow; + return recentWin ? recentWin.gBrowser : 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; + }, + + /** + * Replaces context of an editor with provided value (a string). + * Note: this method is simply a shortcut to editor.setText. + */ + setText: function SP_setText(value) + { + return this.editor.setText(value); + }, + + /** + * 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. + */ + evaluate: function SP_evaluate(aString) + { + let connection; + if (this.target) { + connection = ScratchpadTarget.consoleFor(this.target); + } + else if (this.executionContext == SCRATCHPAD_CONTEXT_CONTENT) { + connection = ScratchpadTab.consoleFor(this.gBrowser.selectedTab); + } + else { + connection = ScratchpadWindow.consoleFor(this.browserWindow); + } + + let evalOptions = { url: this.uniqueName }; + + return connection.then(({ debuggerClient, webConsoleClient }) => { + let deferred = promise.defer(); + + webConsoleClient.evaluateJSAsync(aString, aResponse => { + this.debuggerClient = debuggerClient; + this.webConsoleClient = webConsoleClient; + if (aResponse.error) { + deferred.reject(aResponse); + } + else if (aResponse.exception !== null) { + deferred.resolve([aString, aResponse]); + } + else { + deferred.resolve([aString, undefined, aResponse.result]); + } + }, evalOptions); + + 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() + { + WebConsoleUtils.usageCount++; + let selection = this.editor.getSelection() || this.getText(); + return this.evaluate(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 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).then(resolve, reject); + } + else { + this.editor.dropSelection(); + resolve(); + } + }, reject); + + return deferred.promise; + }, + + /** + * Execute the selected text (if any) or the entire editor content in the + * current context. 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).then(resolve, reject); + } + else { + this.editor.dropSelection(); + 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) { + console.error(this.strings. + GetStringFromName("scratchpadContext.invalid")); + return; + } + + let target = TargetFactory.forTab(this.gBrowser.selectedTab); + target.once("navigate", () => { + this.run().then(results => deferred.resolve(results)); + }); + target.makeRemote().then(() => target.activeTab.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 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).then(resolve, reject); + } + else if (VariablesView.isPrimitive({ value: aResult })) { + this._writePrimitiveAsComment(aResult).then(resolve, reject); + } + else { + let objectClient = new ObjectClient(this.debuggerClient, aResult); + objectClient.getDisplayString(aResponse => { + if (aResponse.error) { + reportError("display", aResponse); + reject(aResponse); + } + else { + this.writeAsComment(aResponse.displayString); + resolve(); + } + }); + } + }, reject); + + return deferred.promise; + }, + + _prettyPrintWorker: null, + + /** + * Get or create the worker that handles pretty printing. + */ + get prettyPrintWorker() { + if (!this._prettyPrintWorker) { + this._prettyPrintWorker = new DevToolsWorker( + "resource://devtools/server/actors/pretty-print-worker.js", + { name: "pretty-print", + verbose: flags.wantLogging } + ); + } + return this._prettyPrintWorker; + }, + + /** + * Pretty print the source text inside the scratchpad. + * + * @return Promise + * A promise resolved with the pretty printed code, or rejected with + * an error. + */ + prettyPrint: function SP_prettyPrint() { + const uglyText = this.getText(); + const tabsize = Services.prefs.getIntPref(TAB_SIZE); + + return this.prettyPrintWorker.performTask("pretty-print", { + url: "(scratchpad)", + indent: tabsize, + source: uglyText + }).then(data => { + this.editor.setText(data.code); + }).then(null, error => { + this.writeAsErrorComment({ exception: error }); + throw error; + }); + }, + + /** + * Parse the text and return an AST. If we can't parse it, write an error + * comment and return false. + */ + _parseText: function SP__parseText(aText) { + try { + return Reflect.parse(aText); + } catch (e) { + this.writeAsErrorComment({ exception: DevToolsUtils.safeErrorString(e) }); + return false; + } + }, + + /** + * Determine if the given AST node location contains the given cursor + * position. + * + * @returns Boolean + */ + _containsCursor: function (aLoc, aCursorPos) { + // Our line numbers are 1-based, while CodeMirror's are 0-based. + const lineNumber = aCursorPos.line + 1; + const columnNumber = aCursorPos.ch; + + if (aLoc.start.line <= lineNumber && aLoc.end.line >= lineNumber) { + if (aLoc.start.line === aLoc.end.line) { + return aLoc.start.column <= columnNumber + && aLoc.end.column >= columnNumber; + } + + if (aLoc.start.line == lineNumber) { + return columnNumber >= aLoc.start.column; + } + + if (aLoc.end.line == lineNumber) { + return columnNumber <= aLoc.end.column; + } + + return true; + } + + return false; + }, + + /** + * Find the top level function AST node that the cursor is within. + * + * @returns Object|null + */ + _findTopLevelFunction: function SP__findTopLevelFunction(aAst, aCursorPos) { + for (let statement of aAst.body) { + switch (statement.type) { + case "FunctionDeclaration": + if (this._containsCursor(statement.loc, aCursorPos)) { + return statement; + } + break; + + case "VariableDeclaration": + for (let decl of statement.declarations) { + if (!decl.init) { + continue; + } + if ((decl.init.type == "FunctionExpression" + || decl.init.type == "ArrowFunctionExpression") + && this._containsCursor(decl.loc, aCursorPos)) { + return decl; + } + } + break; + } + } + + return null; + }, + + /** + * Get the source text associated with the given function statement. + * + * @param Object aFunction + * @param String aFullText + * @returns String + */ + _getFunctionText: function SP__getFunctionText(aFunction, aFullText) { + let functionText = ""; + // Initially set to 0, but incremented first thing in the loop below because + // line numbers are 1 based, not 0 based. + let lineNumber = 0; + const { start, end } = aFunction.loc; + const singleLine = start.line === end.line; + + for (let line of aFullText.split(/\n/g)) { + lineNumber++; + + if (singleLine && start.line === lineNumber) { + functionText = line.slice(start.column, end.column); + break; + } + + if (start.line === lineNumber) { + functionText += line.slice(start.column) + "\n"; + continue; + } + + if (end.line === lineNumber) { + functionText += line.slice(0, end.column); + break; + } + + if (start.line < lineNumber && end.line > lineNumber) { + functionText += line + "\n"; + } + } + + return functionText; + }, + + /** + * Evaluate the top level function that the cursor is resting in. + * + * @returns Promise [text, error, result] + */ + evalTopLevelFunction: function SP_evalTopLevelFunction() { + const text = this.getText(); + const ast = this._parseText(text); + if (!ast) { + return promise.resolve([text, undefined, undefined]); + } + + const cursorPos = this.editor.getCursor(); + const funcStatement = this._findTopLevelFunction(ast, cursorPos); + if (!funcStatement) { + return promise.resolve([text, undefined, undefined]); + } + + let functionText = this._getFunctionText(funcStatement, text); + + // TODO: This is a work around for bug 940086. It should be removed when + // that is fixed. + if (funcStatement.type == "FunctionDeclaration" + && !functionText.startsWith("function ")) { + functionText = "function " + functionText; + funcStatement.loc.start.column -= 9; + } + + // The decrement by one is because our line numbers are 1-based, while + // CodeMirror's are 0-based. + const from = { + line: funcStatement.loc.start.line - 1, + ch: funcStatement.loc.start.column + }; + const to = { + line: funcStatement.loc.end.line - 1, + ch: funcStatement.loc.end.column + }; + + const marker = this.editor.markText(from, to, "eval-text"); + setTimeout(() => marker.clear(), EVAL_FUNCTION_TIMEOUT); + + return this.evaluate(functionText); + }, + + /** + * Writes out a primitive value as a comment. This handles values which are + * to be printed directly (number, string) as well as grips to values + * (null, undefined, longString). + * + * @param any aValue + * The value to print. + * @return Promise + * The promise that resolves after the value has been printed. + */ + _writePrimitiveAsComment: function SP__writePrimitiveAsComment(aValue) + { + let deferred = promise.defer(); + + if (aValue.type == "longString") { + let client = this.webConsoleClient; + client.longString(aValue).substring(0, aValue.length, aResponse => { + if (aResponse.error) { + reportError("display", aResponse); + deferred.reject(aResponse); + } + else { + deferred.resolve(aResponse.substring); + } + }); + } + else { + deferred.resolve(aValue.type || aValue); + } + + return deferred.promise.then(aComment => { + this.writeAsComment(aComment); + }); + }, + + /** + * 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 value = "\n/*\n" + aValue + "\n*/"; + + if (this.editor.somethingSelected()) { + let from = this.editor.getCursor("end"); + this.editor.replaceSelection(this.editor.getSelection() + value); + let to = this.editor.getPosition(this.editor.getOffset(from) + value.length); + this.editor.setSelection(from, to); + return; + } + + let text = this.editor.getText(); + this.editor.setText(text + value); + + let [ from, to ] = this.editor.getPosition(text.length, (text + value).length); + this.editor.setSelection(from, to); + }, + + /** + * 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. It must + * contain an |exception| property with the actual error thrown, but it + * will often be the entire response of an evaluateJS request. + * @return Promise + * The promise that indicates when writing the comment completes. + */ + writeAsErrorComment: function SP_writeAsErrorComment(aError) + { + let deferred = promise.defer(); + + if (VariablesView.isPrimitive({ value: aError.exception })) { + let error = aError.exception; + let type = error.type; + if (type == "undefined" || + type == "null" || + type == "Infinity" || + type == "-Infinity" || + type == "NaN" || + type == "-0") { + deferred.resolve(type); + } + else if (type == "longString") { + deferred.resolve(error.initial + "\u2026"); + } + else { + deferred.resolve(error); + } + } else if ("preview" in aError.exception) { + let error = aError.exception; + let stack = this._constructErrorStack(error.preview); + if (typeof aError.exceptionMessage == "string") { + deferred.resolve(aError.exceptionMessage + stack); + } else { + deferred.resolve(stack); + } + } else { + // If there is no preview information, we need to ask the server for more. + let objectClient = new ObjectClient(this.debuggerClient, aError.exception); + objectClient.getPrototypeAndProperties(aResponse => { + if (aResponse.error) { + deferred.reject(aResponse); + return; + } + + let { ownProperties, safeGetterValues } = aResponse; + let error = Object.create(null); + + // Combine all the property descriptor/getter values into one object. + for (let key of Object.keys(safeGetterValues)) { + error[key] = safeGetterValues[key].getterValue; + } + + for (let key of Object.keys(ownProperties)) { + error[key] = ownProperties[key].value; + } + + let stack = this._constructErrorStack(error); + + if (typeof error.message == "string") { + deferred.resolve(error.message + stack); + } + else { + objectClient.getDisplayString(aResponse => { + if (aResponse.error) { + deferred.reject(aResponse); + } + else if (typeof aResponse.displayString == "string") { + deferred.resolve(aResponse.displayString + stack); + } + else { + deferred.resolve(stack); + } + }); + } + }); + } + + return deferred.promise.then(aMessage => { + console.error(aMessage); + this.writeAsComment("Exception: " + aMessage); + }); + }, + + /** + * Assembles the best possible stack from the properties of the provided + * error. + */ + _constructErrorStack(error) { + let stack; + if (typeof error.stack == "string" && error.stack) { + stack = error.stack; + } else if (typeof error.fileName == "string") { + stack = "@" + error.fileName; + if (typeof error.lineNumber == "number") { + stack += ":" + error.lineNumber; + } + } else if (typeof error.filename == "string") { + stack = "@" + error.filename; + if (typeof error.lineNumber == "number") { + stack += ":" + error.lineNumber; + if (typeof error.columnNumber == "number") { + stack += ":" + error.columnNumber; + } + } + } else if (typeof error.lineNumber == "number") { + stack = "@" + error.lineNumber; + if (typeof error.columnNumber == "number") { + stack += ":" + error.columnNumber; + } + } + + return stack ? "\n" + stack.replace(/\n$/, "") : ""; + }, + + // 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 writePromise = OS.File.writeAtomic(aFile.path, buffer, {tmpPath: aFile.path + ".tmp"}); + writePromise.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); + } + }); + + }, + + /** + * Get a list of applicable charsets. + * The best charset, defaulting to "UTF-8" + * + * @param string aBestCharset + * @return array of strings + */ + _getApplicableCharsets: function SP__getApplicableCharsets(aBestCharset = "UTF-8") { + let charsets = Services.prefs.getCharPref( + FALLBACK_CHARSET_LIST).split(",").filter(function (value) { + return value.length; + }); + charsets.unshift(aBestCharset); + return charsets; + }, + + /** + * Get content converted to unicode, using a list of input charset to try. + * + * @param string aContent + * @param array of string aCharsetArray + * @return string + */ + _getUnicodeContent: function SP__getUnicodeContent(aContent, aCharsetArray) { + let content = null, + converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].createInstance(Ci.nsIScriptableUnicodeConverter), + success = aCharsetArray.some(charset => { + try { + converter.charset = charset; + content = converter.ConvertToUnicode(aContent); + return true; + } catch (e) { + this.notificationBox.appendNotification( + this.strings.formatStringFromName("importFromFile.convert.failed", + [ charset ], 1), + "file-import-convert-failed", + null, + this.notificationBox.PRIORITY_WARNING_HIGH, + null); + } + }); + return content; + }, + + /** + * 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({ + uri: NetUtil.newURI(aFile), + loadingNode: window.document, + securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_INHERITS, + contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER}); + channel.contentType = "application/javascript"; + + this.notificationBox.removeAllNotifications(false); + + NetUtil.asyncFetch(channel, (aInputStream, aStatus) => { + let content = null; + + if (Components.isSuccessCode(aStatus)) { + let charsets = this._getApplicableCharsets(); + content = NetUtil.readInputStreamToString(aInputStream, + aInputStream.available()); + content = this._getUnicodeContent(content, charsets); + if (!content) { + let message = this.strings.formatStringFromName( + "importFromFile.convert.failed", + [ charsets.join(", ") ], + 1); + this.notificationBox.appendNotification( + message, + "file-import-convert-failed", + null, + this.notificationBox.PRIORITY_CRITICAL_MEDIUM, + null); + if (aCallback) { + aCallback.call(this, aStatus, content); + } + return; + } + // 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.editor.setText(content); + this.editor.clearHistory(); + this.dirty = false; + document.getElementById("sp-cmd-revert").setAttribute("disabled", true); + } + else if (!aSilentError) { + window.alert(this.strings.GetStringFromName("openFile.failed")); + } + this.setFilename(aFile.path); + this.setRecentFile(aFile); + 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.importFromFile(file, false); + } + }); + }; + + 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.appendFilter("JavaScript Files", "*.js; *.jsm; *.json"); + fp.appendFilter("All Files", "*.*"); + 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.addEventListener("command", Scratchpad.openFile.bind(Scratchpad, 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.dirty = false; + document.getElementById("sp-cmd-revert").setAttribute("disabled", true); + 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.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.appendFilter("JavaScript Files", "*.js; *.jsm; *.json"); + fp.appendFilter("All Files", "*.*"); + 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() + { + HUDService.toggleBrowserConsole(); + }, + + /** + * Open the Web Console. + */ + openWebConsole: function SP_openWebConsole() + { + let target = 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); + }, + + /** + * 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); + }, + + /** + * 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; + }, + + updateStatusBar: function SP_updateStatusBar(aEventType) + { + var statusBarField = document.getElementById("statusbar-line-col"); + let { line, ch } = this.editor.getCursor(); + statusBarField.textContent = this.strings.formatStringFromName( + "scratchpad.statusBarLineCol", [ line + 1, ch + 1], 2); + }, + + /** + * 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", + [ShortcutUtils.prettifyShortcut(document.getElementById("sp-key-run"), true), + ShortcutUtils.prettifyShortcut(document.getElementById("sp-key-inspect"), true), + ShortcutUtils.prettifyShortcut(document.getElementById("sp-key-display"), true)], + 3); + + let args = window.arguments; + let state = null; + + if (args && args[0] instanceof Ci.nsIDialogParamBlock) { + args = args[0]; + this._instanceId = args.GetString(0); + + state = args.GetString(1) || null; + if (state) { + state = JSON.parse(state); + this.setState(state); + if ("text" in state) { + initialText = state.text; + } + } + } else { + this._instanceId = ScratchpadManager.createUid(); + } + + let config = { + mode: Editor.modes.js, + value: initialText, + lineNumbers: Services.prefs.getBoolPref(SHOW_LINE_NUMBERS), + contextMenu: "scratchpad-text-popup", + showTrailingSpace: Services.prefs.getBoolPref(SHOW_TRAILING_SPACE), + autocomplete: Services.prefs.getBoolPref(ENABLE_AUTOCOMPLETION), + lineWrapping: Services.prefs.getBoolPref(WRAP_TEXT), + }; + + this.editor = new Editor(config); + let editorElement = document.querySelector("#scratchpad-editor"); + this.editor.appendTo(editorElement).then(() => { + var lines = initialText.split("\n"); + + this.editor.setFontSize(Services.prefs.getIntPref(EDITOR_FONT_SIZE)); + + this.editor.on("change", this._onChanged); + // Keep a reference to the bound version for use in onUnload. + this.updateStatusBar = Scratchpad.updateStatusBar.bind(this); + this.editor.on("cursorActivity", this.updateStatusBar); + let okstring = this.strings.GetStringFromName("selfxss.okstring"); + let msg = this.strings.formatStringFromName("selfxss.msg", [okstring], 1); + this._onPaste = WebConsoleUtils.pasteHandlerGen(this.editor.container.contentDocument.body, + document.querySelector("#scratchpad-notificationbox"), + msg, okstring); + editorElement.addEventListener("paste", this._onPaste, true); + editorElement.addEventListener("drop", this._onPaste); + this.editor.on("saveRequested", () => this.saveFile()); + this.editor.focus(); + this.editor.setCursor({ line: lines.length, ch: lines.pop().length }); + + if (state) + this.dirty = !state.saved; + + this.initialized = true; + this._triggerObservers("Ready"); + this.populateRecentFilesMenu(); + PreferenceObserver.init(); + CloseObserver.init(); + }).then(null, (err) => console.error(err)); + this._setupCommandListeners(); + this._updateViewMenuItems(); + this._setupPopupShowingListeners(); + }, + + /** + * The Source Editor "change" event handler. This function updates the + * Scratchpad window title to show an asterisk when there are unsaved changes. + * + * @private + */ + _onChanged: function SP__onChanged() + { + Scratchpad._updateTitle(); + + if (Scratchpad.filename) { + if (Scratchpad.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 event is created only after user uses 'reload and run' feature. + if (this._reloadAndRunEvent && this.gBrowser) { + this.gBrowser.selectedBrowser.removeEventListener("load", + this._reloadAndRunEvent, true); + } + + PreferenceObserver.uninit(); + CloseObserver.uninit(); + if (this._onPaste) { + let editorElement = document.querySelector("#scratchpad-editor"); + editorElement.removeEventListener("paste", this._onPaste, true); + editorElement.removeEventListener("drop", this._onPaste); + this._onPaste = null; + } + this.editor.off("change", this._onChanged); + this.editor.off("cursorActivity", this.updateStatusBar); + this.editor.destroy(); + this.editor = null; + + if (this._sidebar) { + this._sidebar.destroy(); + this._sidebar = null; + } + + if (this._prettyPrintWorker) { + this._prettyPrintWorker.destroy(); + this._prettyPrintWorker = null; + } + + scratchpadTargets = null; + this.webConsoleClient = null; + this.debuggerClient = 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.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) + { + let shouldClose; + + this.promptSave((aShouldClose, aSaved, aStatus) => { + shouldClose = aShouldClose; + if (aSaved && !Components.isSuccessCode(aStatus)) { + shouldClose = false; + } + + if (shouldClose) { + window.close(); + } + + if (aCallback) { + aCallback(shouldClose); + } + }); + + return shouldClose; + }, + + /** + * Toggle a editor's boolean option. + */ + toggleEditorOption: function SP_toggleEditorOption(optionName, optionPreference) + { + let newOptionValue = !this.editor.getOption(optionName); + this.editor.setOption(optionName, newOptionValue); + Services.prefs.setBoolPref(optionPreference, newOptionValue); + }, + + /** + * Increase the editor's font size by 1 px. + */ + increaseFontSize: function SP_increaseFontSize() + { + let size = this.editor.getFontSize(); + + if (size < MAXIMUM_FONT_SIZE) { + let newFontSize = size + 1; + this.editor.setFontSize(newFontSize); + Services.prefs.setIntPref(EDITOR_FONT_SIZE, newFontSize); + + if (newFontSize === MAXIMUM_FONT_SIZE) { + document.getElementById("sp-cmd-larger-font").setAttribute("disabled", true); + } + + document.getElementById("sp-cmd-smaller-font").removeAttribute("disabled"); + } + }, + + /** + * Decrease the editor's font size by 1 px. + */ + decreaseFontSize: function SP_decreaseFontSize() + { + let size = this.editor.getFontSize(); + + if (size > MINIMUM_FONT_SIZE) { + let newFontSize = size - 1; + this.editor.setFontSize(newFontSize); + Services.prefs.setIntPref(EDITOR_FONT_SIZE, newFontSize); + + if (newFontSize === MINIMUM_FONT_SIZE) { + document.getElementById("sp-cmd-smaller-font").setAttribute("disabled", true); + } + } + + document.getElementById("sp-cmd-larger-font").removeAttribute("disabled"); + }, + + /** + * Restore the editor's original font size. + */ + normalFontSize: function SP_normalFontSize() + { + this.editor.setFontSize(NORMAL_FONT_SIZE); + Services.prefs.setIntPref(EDITOR_FONT_SIZE, NORMAL_FONT_SIZE); + + document.getElementById("sp-cmd-larger-font").removeAttribute("disabled"); + document.getElementById("sp-cmd-smaller-font").removeAttribute("disabled"); + }, + + _observers: [], + + /** + * Add an observer for Scratchpad events. + * + * The observer implements IScratchpadObserver := { + * onReady: Called when the Scratchpad and its Editor 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); + } + } + }, + + /** + * Opens the MDN documentation page for Scratchpad. + */ + openDocumentationPage: function SP_openDocumentationPage() + { + let url = this.strings.GetStringFromName("help.openDocumentationPage"); + this.browserWindow.openUILinkIn(url,"tab"); + this.browserWindow.focus(); + }, +}; + + +/** + * Represents the DebuggerClient connection to a specific tab as used by the + * Scratchpad. + * + * @param object aTab + * The tab to connect to. + */ +function ScratchpadTab(aTab) +{ + this._tab = aTab; +} + +var scratchpadTargets = new WeakMap(); + +/** + * Returns the object containing the DebuggerClient and WebConsoleClient for a + * given tab or window. + * + * @param object aSubject + * The tab or window to obtain the connection for. + * @return Promise + * The promise for the connection information. + */ +ScratchpadTab.consoleFor = function consoleFor(aSubject) +{ + if (!scratchpadTargets.has(aSubject)) { + scratchpadTargets.set(aSubject, new this(aSubject)); + } + return scratchpadTargets.get(aSubject).connect(aSubject); +}; + + +ScratchpadTab.prototype = { + /** + * The promise for the connection. + */ + _connector: null, + + /** + * Initialize a debugger client and connect it to the debugger server. + * + * @param object aSubject + * The tab or window to obtain the connection for. + * @return Promise + * The promise for the result of connecting to this tab or window. + */ + connect: function ST_connect(aSubject) + { + if (this._connector) { + return this._connector; + } + + let deferred = promise.defer(); + this._connector = deferred.promise; + + let connectTimer = setTimeout(() => { + deferred.reject({ + error: "timeout", + message: Scratchpad.strings.GetStringFromName("connectionTimeout"), + }); + }, REMOTE_TIMEOUT); + + deferred.promise.then(() => clearTimeout(connectTimer)); + + this._attach(aSubject).then(aTarget => { + let consoleActor = aTarget.form.consoleActor; + let client = aTarget.client; + client.attachConsole(consoleActor, [], (aResponse, aWebConsoleClient) => { + if (aResponse.error) { + reportError("attachConsole", aResponse); + deferred.reject(aResponse); + } + else { + deferred.resolve({ + webConsoleClient: aWebConsoleClient, + debuggerClient: client + }); + } + }); + }); + + return deferred.promise; + }, + + /** + * Attach to this tab. + * + * @param object aSubject + * The tab or window to obtain the connection for. + * @return Promise + * The promise for the TabTarget for this tab. + */ + _attach: function ST__attach(aSubject) + { + let target = TargetFactory.forTab(this._tab); + target.once("close", () => { + if (scratchpadTargets) { + scratchpadTargets.delete(aSubject); + } + }); + return target.makeRemote().then(() => target); + }, +}; + + +/** + * Represents the DebuggerClient connection to a specific window as used by the + * Scratchpad. + */ +function ScratchpadWindow() {} + +ScratchpadWindow.consoleFor = ScratchpadTab.consoleFor; + +ScratchpadWindow.prototype = Heritage.extend(ScratchpadTab.prototype, { + /** + * Attach to this window. + * + * @return Promise + * The promise for the target for this window. + */ + _attach: function SW__attach() + { + if (!DebuggerServer.initialized) { + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + } + DebuggerServer.allowChromeProcess = true; + + let client = new DebuggerClient(DebuggerServer.connectPipe()); + return client.connect() + .then(() => client.getProcess()) + .then(aResponse => { + return { form: aResponse.form, client: client }; + }); + } +}); + + +function ScratchpadTarget(aTarget) +{ + this._target = aTarget; +} + +ScratchpadTarget.consoleFor = ScratchpadTab.consoleFor; + +ScratchpadTarget.prototype = Heritage.extend(ScratchpadTab.prototype, { + _attach: function ST__attach() + { + if (this._target.isRemote) { + return promise.resolve(this._target); + } + return this._target.makeRemote().then(() => this._target); + } +}); + + +/** + * Encapsulates management of the sidebar containing the VariablesView for + * object inspection. + */ +function ScratchpadSidebar(aScratchpad) +{ + // Make sure to decorate this object. ToolSidebar requires the parent + // panel to support event (emit) API. + EventEmitter.decorate(this); + + let ToolSidebar = require("devtools/client/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) { + this.variablesView.controller.releaseActors(); + } + else { + 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") + }); + + VariablesViewController.attach(this.variablesView, { + getEnvironmentClient: aGrip => { + return new EnvironmentClient(this._scratchpad.debuggerClient, aGrip); + }, + getObjectClient: aGrip => { + return new ObjectClient(this._scratchpad.debuggerClient, aGrip); + }, + getLongStringClient: aActor => { + return this._scratchpad.webConsoleClient.longString(aActor); + }, + releaseActor: aActor => { + this._scratchpad.debuggerClient.release(aActor); + } + }); + } + 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, {selected: 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(); + } + }, + + /** + * Destroy the sidebar. + * + * @return Promise + * The promise that resolves when the sidebar is destroyed. + */ + destroy: function SS_destroy() + { + if (this.variablesView) { + this.variablesView.controller.releaseActors(); + this.variablesView = null; + } + return this._sidebar.destroy(); + }, + + /** + * Update the object currently inspected by the sidebar. + * + * @param any aValue + * The JS value to inspect in the sidebar. + * @return Promise + * A promise that resolves when the update completes. + */ + _update: function SS__update(aValue) + { + let options, onlyEnumVisible; + if (VariablesView.isPrimitive({ value: aValue })) { + options = { rawObject: { value: aValue } }; + onlyEnumVisible = true; + } else { + options = { objectActor: aValue }; + onlyEnumVisible = false; + } + let view = this.variablesView; + view.onlyEnumVisible = onlyEnumVisible; + view.empty(); + return view.controller.setSingleVariable(options).expanded; + } +}; + + +/** + * Report an error coming over the remote debugger protocol. + * + * @param string aAction + * The name of the action or method that failed. + * @param object aResponse + * The response packet that contains the error. + */ +function reportError(aAction, aResponse) +{ + console.error(aAction + " failed: " + aResponse.error + " " + + aResponse.message); +} + + +/** + * 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; + } +}; + + +/** + * The CloseObserver listens for the last browser window closing and attempts to + * close the Scratchpad. + */ +var CloseObserver = { + init: function CO_init() + { + Services.obs.addObserver(this, "browser-lastwindow-close-requested", false); + }, + + observe: function CO_observe(aSubject) + { + if (Scratchpad.close()) { + this.uninit(); + } + else { + aSubject.QueryInterface(Ci.nsISupportsPRBool); + aSubject.data = true; + } + }, + + uninit: function CO_uninit() + { + // Will throw exception if removeObserver is called twice. + if (this._uninited) { + return; + } + + this._uninited = true; + Services.obs.removeObserver(this, "browser-lastwindow-close-requested", + false); + }, +}; + +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/devtools/client/scratchpad/scratchpad.xul b/devtools/client/scratchpad/scratchpad.xul new file mode 100644 index 000000000..0603fa95e --- /dev/null +++ b/devtools/client/scratchpad/scratchpad.xul @@ -0,0 +1,412 @@ +<?xml version="1.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/. --> + +<!DOCTYPE window [ +<!ENTITY % scratchpadDTD SYSTEM "chrome://devtools/locale/scratchpad.dtd" > + %scratchpadDTD; +<!ENTITY % editMenuStrings SYSTEM "chrome://global/locale/editMenuOverlay.dtd"> +%editMenuStrings; +<!ENTITY % sourceEditorStrings SYSTEM "chrome://devtools/locale/sourceeditor.dtd"> +%sourceEditorStrings; +]> + +<?xml-stylesheet href="chrome://global/skin/global.css"?> +<?xml-stylesheet href="chrome://devtools/skin/scratchpad.css"?> +<?xul-overlay href="chrome://global/content/editMenuOverlay.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;version=1.8" + src="chrome://devtools/content/shared/theme-switching.js"/> +<script type="application/javascript" src="chrome://global/content/globalOverlay.js"/> +<script type="application/javascript" src="chrome://devtools/content/scratchpad/scratchpad.js"/> + +<commandset id="editMenuCommands"/> + +<commandset id="sourceEditorCommands"> + <command id="cmd_find" oncommand=";"/> + <command id="cmd_findAgain" oncommand=";"/> + <command id="cmd_gotoLine" oncommand=";"/> +</commandset> + +<commandset id="sp-commandset"> + <command id="sp-cmd-newWindow" oncommand=";"/> + <command id="sp-cmd-openFile" oncommand=";"/> + <command id="sp-cmd-clearRecentFiles" oncommand=";"/> + <command id="sp-cmd-save" oncommand=";"/> + <command id="sp-cmd-saveas" oncommand=";"/> + <command id="sp-cmd-revert" oncommand=";" disabled="true"/> + <command id="sp-cmd-close" oncommand=";"/> + <command id="sp-cmd-line-numbers" oncommand=";"/> + <command id="sp-cmd-wrap-text" oncommand=";"/> + <command id="sp-cmd-highlight-trailing-space" oncommand=";"/> + <command id="sp-cmd-larger-font" oncommand=";"/> + <command id="sp-cmd-smaller-font" oncommand=";"/> + <command id="sp-cmd-normal-font" oncommand=";"/> + <command id="sp-cmd-run" oncommand=";"/> + <command id="sp-cmd-inspect" oncommand=";"/> + <command id="sp-cmd-display" oncommand=";"/> + <command id="sp-cmd-pprint" oncommand=";"/> + <command id="sp-cmd-contentContext" oncommand=";"/> + <command id="sp-cmd-browserContext" oncommand=";" disabled="true"/> + <command id="sp-cmd-reloadAndRun" oncommand=";"/> + <command id="sp-cmd-evalFunction" oncommand=";"/> + <command id="sp-cmd-errorConsole" oncommand=";" disabled="true"/> + <command id="sp-cmd-webConsole" oncommand=";"/> + <command id="sp-cmd-documentationLink" oncommand=";"/> + <command id="sp-cmd-hideSidebar" oncommand=";"/> +</commandset> + +<keyset id="editMenuKeys"/> + +<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"/> + <key id="sp-key-larger-font" + key="&largerFont.commandkey;" + command="sp-cmd-larger-font" + modifiers="accel"/> + <key key="&largerFont.commandkey2;" + command="sp-cmd-larger-font" + modifiers="accel"/> + <key id="sp-key-smaller-font" + key="&smallerFont.commandkey;" + command="sp-cmd-smaller-font" + modifiers="accel"/> + <key id="sp-key-normal-size-font" + key="&normalSize.commandkey;" + command="sp-cmd-normal-font" + 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-pprint" + key="&pprint.key;" + command="sp-cmd-pprint" + modifiers="accel"/> + <key id="sp-key-reloadAndRun" + key="&reloadAndRun.key;" + command="sp-cmd-reloadAndRun" + modifiers="accel,shift"/> + <key id="sp-key-evalFunction" + key="&evalFunction.key;" + command="sp-cmd-evalFunction" + modifiers="accel"/> + <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"/> + <key id="key_gotoLine" + key="&gotoLineCmd.key;" + command="key_gotoLine" + modifiers="accel"/> + +</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/> + + <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"> + <menuitem id="menu_undo"/> + <menuitem id="menu_redo"/> + <menuseparator/> + <menuitem id="menu_cut"/> + <menuitem id="menu_copy"/> + <menuitem id="menu_paste"/> + <menuseparator/> + <menuitem id="menu_selectAll"/> + <menuseparator/> + <menuitem id="menu_find"/> + <menuitem id="menu_findAgain"/> + <menuseparator/> + <menuitem id="se-menu-gotoLine" + label="&gotoLineCmd.label;" + accesskey="&gotoLineCmd.accesskey;" + key="key_gotoLine" + command="cmd_gotoLine"/> + <menuitem id="sp-menu-pprint" + label="&pprint.label;" + accesskey="&pprint.accesskey;" + key="sp-key-pprint" + command="sp-cmd-pprint"/> + </menupopup> + </menu> + + <menu id="sp-view-menu" label="&viewMenu.label;" accesskey="&viewMenu.accesskey;"> + <menupopup id="sp-menu-viewpopup"> + <menuitem id="sp-menu-line-numbers" + label="&lineNumbers.label;" + accesskey="&lineNumbers.accesskey;" + type="checkbox" + command="sp-cmd-line-numbers"/> + <menuitem id="sp-menu-word-wrap" + label="&wordWrap.label;" + accesskey="&wordWrap.accesskey;" + type="checkbox" + command="sp-cmd-wrap-text"/> + <menuitem id="sp-menu-highlight-trailing-space" + label="&highlightTrailingSpace.label;" + accesskey="&highlightTrailingSpace.accesskey;" + type="checkbox" + command="sp-cmd-highlight-trailing-space"/> + <menuseparator/> + <menuitem id="sp-menu-larger-font" + label="&largerFont.label;" + key="sp-key-larger-font" + accesskey="&largerFont.accesskey;" + command="sp-cmd-larger-font"/> + <menuitem id="sp-menu-smaller-font" + label="&smallerFont.label;" + key="sp-key-smaller-font" + accesskey="&smallerFont.accesskey;" + command="sp-cmd-smaller-font"/> + <menuitem id="sp-menu-normal-size-font" + label="&normalSize.label;" + key="sp-menu-normal-font" + accesskey="&normalSize.accesskey;" + command="sp-cmd-normal-font"/> + </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-evalFunction" + label="&evalFunction.label;" + key="sp-key-evalFunction" + accesskey="&evalFunction.accesskey;" + command="sp-cmd-evalFunction"/> + </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> + +<toolbar id="sp-toolbar" + class="devtools-toolbar"> + <toolbarbutton id="sp-toolbar-open" + class="devtools-toolbarbutton" + label="&openFileCmd.label;" + command="sp-cmd-openFile"/> + <toolbarbutton id="sp-toolbar-save" + class="devtools-toolbarbutton" + label="&saveFileCmd.label;" + command="sp-cmd-save"/> + <toolbarbutton id="sp-toolbar-saveAs" + class="devtools-toolbarbutton" + label="&saveFileAsCmd.label;" + command="sp-cmd-saveas"/> + <toolbarspacer/> + <toolbarbutton id="sp-toolbar-run" + class="devtools-toolbarbutton" + label="&run.label;" + command="sp-cmd-run"/> + <toolbarbutton id="sp-toolbar-inspect" + class="devtools-toolbarbutton" + label="&inspect.label;" + command="sp-cmd-inspect"/> + <toolbarbutton id="sp-toolbar-display" + class="devtools-toolbarbutton" + label="&display.label;" + command="sp-cmd-display"/> + <toolbarspacer/> + <toolbarbutton id="sp-toolbar-pprint" + class="devtools-toolbarbutton" + label="&pprint.label;" + command="sp-cmd-pprint"/> +</toolbar> + + +<popupset id="scratchpad-popups"> + <menupopup id="scratchpad-text-popup"> + <menuitem id="cMenu_cut"/> + <menuitem id="cMenu_copy"/> + <menuitem id="cMenu_paste"/> + <menuitem id="cMenu_delete"/> + <menuseparator/> + <menuitem id="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"/> + <menuitem id="sp-text-evalFunction" + label="&evalFunction.label;" + key="sp-key-evalFunction" + accesskey="&evalFunction.accesskey;" + command="sp-cmd-evalFunction"/> + <menuseparator/> + <menuitem id="sp-text-reloadAndRun" + label="&reloadAndRun.label;" + key="sp-key-reloadAndRun" + accesskey="&reloadAndRun.accesskey;" + command="sp-cmd-reloadAndRun"/> + </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> + <toolbar id="statusbar-line-col" class="devtools-toolbar"/> +</notificationbox> + +</window> diff --git a/devtools/client/scratchpad/test/.eslintrc.js b/devtools/client/scratchpad/test/.eslintrc.js new file mode 100644 index 000000000..8d15a76d9 --- /dev/null +++ b/devtools/client/scratchpad/test/.eslintrc.js @@ -0,0 +1,6 @@ +"use strict"; + +module.exports = { + // Extend from the shared list of defined globals for mochitests. + "extends": "../../../.eslintrc.mochitests.js" +}; diff --git a/devtools/client/scratchpad/test/NS_ERROR_ILLEGAL_INPUT.txt b/devtools/client/scratchpad/test/NS_ERROR_ILLEGAL_INPUT.txt new file mode 100644 index 000000000..031c0597b --- /dev/null +++ b/devtools/client/scratchpad/test/NS_ERROR_ILLEGAL_INPUT.txt @@ -0,0 +1,2 @@ +Typ Datum Uhrzeit Quelle Kategorie Ereignis Benutzer Computer
+Informationen 10.08.2012 16:07:11 MSDTC Datenträger 2444 Nicht zutreffend
diff --git a/devtools/client/scratchpad/test/browser.ini b/devtools/client/scratchpad/test/browser.ini new file mode 100644 index 000000000..cc67ce1ab --- /dev/null +++ b/devtools/client/scratchpad/test/browser.ini @@ -0,0 +1,46 @@ +[DEFAULT] +tags = devtools +subsuite = devtools +support-files = head.js + +[browser_scratchpad_autocomplete.js] +[browser_scratchpad_browser_last_window_closing.js] +[browser_scratchpad_reset_undo.js] +[browser_scratchpad_display_outputs_errors.js] +[browser_scratchpad_eval_func.js] +[browser_scratchpad_goto_line_ui.js] +[browser_scratchpad_reload_and_run.js] +[browser_scratchpad_display_non_error_exceptions.js] +[browser_scratchpad_modeline.js] +[browser_scratchpad_chrome_context_pref.js] +[browser_scratchpad_help_key.js] +[browser_scratchpad_recent_files.js] +# [browser_scratchpad_confirm_close.js] +# Disable test due to bug 807234 becoming basically permanent +[browser_scratchpad_tab.js] +[browser_scratchpad_wrong_window_focus.js] +[browser_scratchpad_unsaved.js] +[browser_scratchpad_falsy.js] +[browser_scratchpad_edit_ui_updates.js] +[browser_scratchpad_revert_to_saved.js] +[browser_scratchpad_run_error_goto_line.js] +[browser_scratchpad_contexts.js] +[browser_scratchpad_execute_print.js] +[browser_scratchpad_files.js] +[browser_scratchpad_initialization.js] +[browser_scratchpad_inspect.js] +[browser_scratchpad_inspect_primitives.js] +[browser_scratchpad_long_string.js] +[browser_scratchpad_open.js] +support-files = NS_ERROR_ILLEGAL_INPUT.txt +[browser_scratchpad_open_error_console.js] +[browser_scratchpad_throw_output.js] +[browser_scratchpad_pprint-02.js] +[browser_scratchpad_pprint.js] +[browser_scratchpad_pprint_error_goto_line.js] +[browser_scratchpad_restore.js] +[browser_scratchpad_tab_switch.js] +[browser_scratchpad_ui.js] +[browser_scratchpad_close_toolbox.js] +[browser_scratchpad_remember_view_options.js] +[browser_scratchpad_disable_view_menu_items.js] diff --git a/devtools/client/scratchpad/test/browser_scratchpad_autocomplete.js b/devtools/client/scratchpad/test/browser_scratchpad_autocomplete.js new file mode 100644 index 000000000..3a6eef8b4 --- /dev/null +++ b/devtools/client/scratchpad/test/browser_scratchpad_autocomplete.js @@ -0,0 +1,66 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* Bug 968896 */ + +// Test the completions using numbers. +const source = "0x1."; +const completions = ["toExponential", "toFixed", "toString"]; +const { Task } = require("devtools/shared/task"); + +function test() { + const options = { tabContent: "test scratchpad autocomplete" }; + openTabAndScratchpad(options) + .then(Task.async(runTests)) + .then(finish, console.error); +} + + +function* runTests([win, sp]) { + const {editor} = sp; + const editorWin = editor.container.contentWindow; + + // Show the completions popup. + sp.setText(source); + sp.editor.setCursor({ line: 0, ch: source.length }); + yield keyOnce("suggestion-entered", " ", { ctrlKey: true }); + + // Get the hints popup container. + const hints = editorWin.document.querySelector(".CodeMirror-hints"); + + ok(hints, + "The hint container should exist."); + is(hints.childNodes.length, 3, + "The hint container should have the completions."); + + let i = 0; + for (let completion of completions) { + let active = hints.querySelector(".CodeMirror-hint-active"); + is(active.textContent, completion, + "Check that completion " + i++ + " is what is expected."); + yield keyOnce("suggestion-entered", "VK_DOWN"); + } + + // We should have looped around to the first suggestion again. Accept it. + yield keyOnce("after-suggest", "VK_RETURN"); + + is(sp.getText(), source + completions[0], + "Autocompletion should work and select the right element."); + + // Check that the information tooltips work. + sp.setText("5"); + yield keyOnce("show-information", " ", { ctrlKey: true, shiftKey: true }); + + // Get the information container. + const info = editorWin.document.querySelector(".CodeMirror-Tern-information"); + ok(info, + "Info tooltip should appear."); + is(info.textContent.slice(0, 6), "number", + "Info tooltip should have expected contents."); + + function keyOnce(event, key, opts = {}) { + const p = editor.once(event); + EventUtils.synthesizeKey(key, opts, editorWin); + return p; + } +} diff --git a/devtools/client/scratchpad/test/browser_scratchpad_browser_last_window_closing.js b/devtools/client/scratchpad/test/browser_scratchpad_browser_last_window_closing.js new file mode 100644 index 000000000..3a8316059 --- /dev/null +++ b/devtools/client/scratchpad/test/browser_scratchpad_browser_last_window_closing.js @@ -0,0 +1,79 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const BUTTON_POSITION_CANCEL = 1; +const BUTTON_POSITION_DONT_SAVE = 2; + + +function test() +{ + waitForExplicitFinish(); + + // Observer must be attached *before* Scratchpad is opened. + CloseObserver.init(); + + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("load", function onTabLoad() { + gBrowser.selectedBrowser.removeEventListener("load", onTabLoad, true); + openScratchpad(runTests); + }, true); + + content.location = "data:text/html;charset=utf8,<p>test browser last window closing</p>"; +} + + + +function runTests({ Scratchpad }) +{ + let browser = Services.wm.getEnumerator("navigator:browser").getNext(); + let oldPrompt = Services.prompt; + let button; + + Services.prompt = { + confirmEx: () => button + }; + + + Scratchpad.dirty = true; + + // Test canceling close. + button = BUTTON_POSITION_CANCEL; + CloseObserver.expectedValue = true; + browser.BrowserTryToCloseWindow(); + + // Test accepting close. + button = BUTTON_POSITION_DONT_SAVE; + CloseObserver.expectedValue = false; + browser.BrowserTryToCloseWindow(); + + // Test closing without prompt needed. + Scratchpad.dirty = false; + browser.BrowserTryToCloseWindow(); + + Services.prompt = oldPrompt; + CloseObserver.uninit(); + finish(); +} + + +var CloseObserver = { + expectedValue: null, + init: function () + { + Services.obs.addObserver(this, "browser-lastwindow-close-requested", false); + }, + + observe: function (aSubject) + { + aSubject.QueryInterface(Ci.nsISupportsPRBool); + let message = this.expectedValue ? "close" : "stay open"; + ok(this.expectedValue === aSubject.data, "Expected browser to " + message); + aSubject.data = true; + }, + + uninit: function () + { + Services.obs.removeObserver(this, "browser-lastwindow-close-requested", false); + }, +}; diff --git a/devtools/client/scratchpad/test/browser_scratchpad_chrome_context_pref.js b/devtools/client/scratchpad/test/browser_scratchpad_chrome_context_pref.js new file mode 100644 index 000000000..08528d2c2 --- /dev/null +++ b/devtools/client/scratchpad/test/browser_scratchpad_chrome_context_pref.js @@ -0,0 +1,50 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* Bug 646070 */ + +var 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); + + 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/devtools/client/scratchpad/test/browser_scratchpad_close_toolbox.js b/devtools/client/scratchpad/test/browser_scratchpad_close_toolbox.js new file mode 100644 index 000000000..fd1126fd4 --- /dev/null +++ b/devtools/client/scratchpad/test/browser_scratchpad_close_toolbox.js @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that closing the toolbox after having opened a scratchpad leaves the +// latter in a functioning state. + +var {Task} = require("devtools/shared/task"); +var {TargetFactory} = require("devtools/client/framework/target"); + +function test() { + const options = { + tabContent: "test closing toolbox and then reusing scratchpad" + }; + openTabAndScratchpad(options) + .then(Task.async(runTests)) + .then(finish, console.error); +} + +function* runTests([win, sp]) { + // Use the scratchpad before opening the toolbox. + const source = "window.foobar = 7;"; + sp.setText(source); + let [,, result] = yield sp.display(); + is(result, 7, "Display produced the expected output."); + + // Now open the toolbox and close it again. + let target = TargetFactory.forTab(gBrowser.selectedTab); + let toolbox = yield gDevTools.showToolbox(target, "webconsole"); + ok(toolbox, "Toolbox was opened."); + let closed = yield gDevTools.closeToolbox(target); + is(closed, true, "Toolbox was closed."); + + // Now see if using the scratcphad works as expected. + sp.setText(source); + let [,, result2] = yield sp.display(); + is(result2, 7, + "Display produced the expected output after the toolbox was gone."); +} diff --git a/devtools/client/scratchpad/test/browser_scratchpad_confirm_close.js b/devtools/client/scratchpad/test/browser_scratchpad_confirm_close.js new file mode 100644 index 000000000..a6318fa75 --- /dev/null +++ b/devtools/client/scratchpad/test/browser_scratchpad_confirm_close.js @@ -0,0 +1,230 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* Bug 653427 */ + +var tempScope = {}; +Cu.import("resource://gre/modules/NetUtil.jsm", tempScope); +Cu.import("resource://gre/modules/FileUtils.jsm", tempScope); +var NetUtil = tempScope.NetUtil; +var 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, 0o666); + 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, + 0o644, 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({ + uri: NetUtil.newURI(file), + loadUsingSystemPrincipal: true}); + 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/devtools/client/scratchpad/test/browser_scratchpad_contexts.js b/devtools/client/scratchpad/test/browser_scratchpad_contexts.js new file mode 100644 index 000000000..ae1933b4d --- /dev/null +++ b/devtools/client/scratchpad/test/browser_scratchpad_contexts.js @@ -0,0 +1,149 @@ +/* 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"; + +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"); + + sp.editor.setText("window.foobarBug636725 = 'aloha';"); + + let pageResult = yield inContent(function* () { + return content.wrappedJSObject.foobarBug636725; + }); + ok(!pageResult, "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(notificationBox.currentNotification, + "there is a notification in browser context"); + + let [ from, to ] = sp.editor.getPosition(31, 32); + sp.editor.replaceText("2'", from, to); + + 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.editor.replaceText("gBrowser", sp.editor.getPosition(7)); + + is(sp.getText(), "window.gBrowser", + "setText() worked with no end for the replace range"); + }, + then: function* ([, , result]) { + is(result.class, "XULElement", + "chrome context has access to chrome objects"); + } + }, { + method: "run", + prepare: function* () { + // Check that the sandbox is cached. + sp.editor.setText("typeof foobarBug636725cache;"); + }, + then: function* ([, , result]) { + is(result, "undefined", "global variable does not exist"); + } + }, { + method: "run", + prepare: function* () { + sp.editor.setText("window.foobarBug636725cache = 'foo';" + + "typeof foobarBug636725cache;"); + }, + then: function* ([, , result]) { + is(result, "string", + "global variable exists across two different executions"); + } + }, { + method: "run", + prepare: function* () { + sp.editor.setText("window.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.editor.setText("typeof foobarBug636725cache2;"); + }, + then: function* ([, , result]) { + is(result, "undefined", + "global variable no longer exists after changing the context"); + } + }]; + + runAsyncCallbackTests(sp, tests).then(() => { + sp.setBrowserContext(); + sp.editor.setText("delete foobarBug636725cache;" + + "delete foobarBug636725cache2;"); + sp.run().then(finish); + }); +} diff --git a/devtools/client/scratchpad/test/browser_scratchpad_disable_view_menu_items.js b/devtools/client/scratchpad/test/browser_scratchpad_disable_view_menu_items.js new file mode 100644 index 000000000..ed501ce2d --- /dev/null +++ b/devtools/client/scratchpad/test/browser_scratchpad_disable_view_menu_items.js @@ -0,0 +1,66 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test if the view menu items "Larger Font" and "Smaller Font" are disabled +// when the font size reaches the maximum/minimum values. + +var {Task} = require("devtools/shared/task"); + +function test() { + const options = { + tabContent: 'test if view menu items "Larger Font" and "Smaller Font" are enabled/disabled.' + }; + openTabAndScratchpad(options) + .then(Task.async(runTests)) + .then(finish, console.error); +} + +function* runTests([win, sp]) { + yield testMaximumFontSize(win, sp); + + yield testMinimumFontSize(win, sp); +} + +const MAXIMUM_FONT_SIZE = 96; +const MINIMUM_FONT_SIZE = 6; +const NORMAL_FONT_SIZE = 12; + +var testMaximumFontSize = Task.async(function* (win, sp) { + let doc = win.document; + + Services.prefs.clearUserPref("devtools.scratchpad.editorFontSize"); + + let menu = doc.getElementById("sp-menu-larger-font"); + + for (let i = NORMAL_FONT_SIZE; i <= MAXIMUM_FONT_SIZE; i++) { + menu.doCommand(); + } + + let cmd = doc.getElementById("sp-cmd-larger-font"); + ok(cmd.getAttribute("disabled") === "true", 'Command "sp-cmd-larger-font" is disabled.'); + + menu = doc.getElementById("sp-menu-smaller-font"); + menu.doCommand(); + + ok(cmd.hasAttribute("disabled") === false, 'Command "sp-cmd-larger-font" is enabled.'); +}); + +var testMinimumFontSize = Task.async(function* (win, sp) { + let doc = win.document; + + let menu = doc.getElementById("sp-menu-smaller-font"); + + for (let i = MAXIMUM_FONT_SIZE; i >= MINIMUM_FONT_SIZE; i--) { + menu.doCommand(); + } + + let cmd = doc.getElementById("sp-cmd-smaller-font"); + ok(cmd.getAttribute("disabled") === "true", 'Command "sp-cmd-smaller-font" is disabled.'); + + menu = doc.getElementById("sp-menu-larger-font"); + menu.doCommand(); + + ok(cmd.hasAttribute("disabled") === false, 'Command "sp-cmd-smaller-font" is enabled.'); + + Services.prefs.clearUserPref("devtools.scratchpad.editorFontSize"); +}); diff --git a/devtools/client/scratchpad/test/browser_scratchpad_display_non_error_exceptions.js b/devtools/client/scratchpad/test/browser_scratchpad_display_non_error_exceptions.js new file mode 100644 index 000000000..d1f2cb513 --- /dev/null +++ b/devtools/client/scratchpad/test/browser_scratchpad_display_non_error_exceptions.js @@ -0,0 +1,110 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* Bug 756681 */ + +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: Error: Ouch!\n@" + scratchpad.uniqueName + ":1:7" + 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: HierarchyRequestError: Node cannot be inserted " + + "at the specified point in the hierarchy\n@" + + scratchpad.uniqueName + ":1:0" + 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: Error: Ouch!\n@" + scratchpad.uniqueName + ":1:7" + 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: HierarchyRequestError: Node cannot be inserted " + + "at the specified point in the hierarchy\n@" + + scratchpad.uniqueName + ":1:0" + closeComment, + label: "Alternative format error run output" + }]; + + runAsyncTests(scratchpad, tests).then(finish); +} diff --git a/devtools/client/scratchpad/test/browser_scratchpad_display_outputs_errors.js b/devtools/client/scratchpad/test/browser_scratchpad_display_outputs_errors.js new file mode 100644 index 000000000..3855a873d --- /dev/null +++ b/devtools/client/scratchpad/test/browser_scratchpad_display_outputs_errors.js @@ -0,0 +1,72 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* Bug 690552 */ + +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 are 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 syntaxError = "("; + + let tests = [{ + method: "display", + code: message, + result: message + openComment + "Hello World!" + closeComment, + label: "message display output" + }, + { + method: "display", + code: error, + result: error + openComment + "Exception: Error: Ouch!\n@" + + scratchpad.uniqueName + ":1:7" + closeComment, + label: "error display output", + }, + { + method: "display", + code: syntaxError, + result: syntaxError + openComment + "Exception: SyntaxError: expected expression, got end of script\n@" + + scratchpad.uniqueName + ":1" + closeComment, + label: "syntaxError display output", + }, + { + method: "run", + code: message, + result: message, + label: "message run output", + }, + { + method: "run", + code: error, + result: error + openComment + "Exception: Error: Ouch!\n@" + + scratchpad.uniqueName + ":1:7" + closeComment, + label: "error run output", + }, + { + method: "run", + code: syntaxError, + result: syntaxError + openComment + "Exception: SyntaxError: expected expression, got end of script\n@" + + scratchpad.uniqueName + ":1" + closeComment, + label: "syntaxError run output", + }]; + + runAsyncTests(scratchpad, tests).then(finish); +} diff --git a/devtools/client/scratchpad/test/browser_scratchpad_edit_ui_updates.js b/devtools/client/scratchpad/test/browser_scratchpad_edit_ui_updates.js new file mode 100644 index 000000000..ade87eaac --- /dev/null +++ b/devtools/client/scratchpad/test/browser_scratchpad_edit_ui_updates.js @@ -0,0 +1,206 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* Bug 699130 */ + +"use strict"; + +var WebConsoleUtils = require("devtools/client/webconsole/utils").Utils; +var DEVTOOLS_CHROME_ENABLED = "devtools.chrome.enabled"; + +function test() +{ + waitForExplicitFinish(); + gBrowser.selectedTab = gBrowser.addTab(); + Services.prefs.setBoolPref(DEVTOOLS_CHROME_ENABLED, false); + 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("menu_cut"); + ok(cutItem, "the Cut menuitem"); + let pasteItem = doc.getElementById("menu_paste"); + ok(pasteItem, "the Paste menuitem"); + + let anchor = doc.documentElement; + let isContextMenu = false; + + let oldVal = sp.editor.getText(); + + let testSelfXss = function (oldVal) { + // Self xss prevention tests (bug 994134) + info("Self xss paste tests"); + is(WebConsoleUtils.usageCount, 0, "Test for usage count getter"); + let notificationbox = doc.getElementById("scratchpad-notificationbox"); + let notification = notificationbox.getNotificationWithValue("selfxss-notification"); + ok(notification, "Self-xss notification shown"); + is(oldVal, sp.editor.getText(), "Paste blocked by self-xss prevention"); + Services.prefs.setIntPref("devtools.selfxss.count", 10); + notificationbox.removeAllNotifications(true); + openMenu(10, 10, firstShow); + }; + + 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 enabled"); + closeMenu(firstHide); + }; + + let firstHide = function () { + sp.editor.setSelection({ line: 0, ch: 0 }, { line: 0, ch: 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.on("change", onCut); + waitForFocus(function () { + let selectedText = sp.editor.getSelection(); + ok(selectedText.length > 0, "non-empty selected text will be cut"); + + EventUtils.synthesizeKey("x", {accelKey: true}, gScratchpadWindow); + }, gScratchpadWindow); + }; + + let onCut = function () { + sp.editor.off("change", onCut); + openMenu(12, 12, showAfterCut); + }; + + let showAfterCut = function () { + ok(!cutItem.hasAttribute("disabled"), "cut menuitem is enabled after cut"); + ok(!pasteItem.hasAttribute("disabled"), "paste menuitem is enabled after cut"); + closeMenu(hideAfterCut); + }; + + let hideAfterCut = function () { + waitForFocus(function () { + sp.editor.on("change", onPaste); + EventUtils.synthesizeKey("v", {accelKey: true}, gScratchpadWindow); + }, gScratchpadWindow); + }; + + let onPaste = function () { + sp.editor.off("change", onPaste); + openMenu(13, 13, showAfterPaste); + }; + + let showAfterPaste = function () { + ok(!cutItem.hasAttribute("disabled"), "cut menuitem is enabled after paste"); + ok(!pasteItem.hasAttribute("disabled"), "paste menuitem is enabled after paste"); + closeMenu(hideAfterPaste); + }; + + let hideAfterPaste = function () { + if (pass == 0) { + pass++; + testContextMenu(); + } else { + Services.prefs.clearUserPref(DEVTOOLS_CHROME_ENABLED); + 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("cMenu_cut"); + ok(cutItem, "the Cut menuitem"); + pasteItem = doc.getElementById("cMenu_paste"); + ok(pasteItem, "the Paste menuitem"); + + sp.setText("bug 699130: hello world! (context menu)"); + openMenu(10, 10, firstShow); + }; + waitForFocus(function () { + WebConsoleUtils.usageCount = 0; + EventUtils.synthesizeKey("v", {accelKey: true}, gScratchpadWindow); + testSelfXss(oldVal); + }, gScratchpadWindow); +} diff --git a/devtools/client/scratchpad/test/browser_scratchpad_eval_func.js b/devtools/client/scratchpad/test/browser_scratchpad_eval_func.js new file mode 100644 index 000000000..3753b5a35 --- /dev/null +++ b/devtools/client/scratchpad/test/browser_scratchpad_eval_func.js @@ -0,0 +1,86 @@ +/* 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,test Scratchpad eval function."; +} + +function reportErrorAndQuit(error) { + DevToolsUtils.reportException("browser_scratchpad_eval_func.js", error); + ok(false); + finish(); +} + +function runTests(sw) +{ + const sp = sw.Scratchpad; + + let foo = "" + function main() { console.log(1); }; + let bar = "var bar = " + (() => { console.log(2); }); + + const fullText = + foo + "\n" + + "\n" + + bar + "\n"; + + sp.setText(fullText); + + // On the function declaration. + sp.editor.setCursor({ line: 0, ch: 18 }); + sp.evalTopLevelFunction() + .then(([text, error, result]) => { + is(text, foo, "Should re-eval foo."); + ok(!error, "Should not have got an error."); + ok(result, "Should have got a result."); + }) + + // On the arrow function. + .then(() => { + sp.editor.setCursor({ line: 2, ch: 18 }); + return sp.evalTopLevelFunction(); + }) + .then(([text, error, result]) => { + is(text, bar.replace("var ", ""), "Should re-eval bar."); + ok(!error, "Should not have got an error."); + ok(result, "Should have got a result."); + }) + + // On the empty line. + .then(() => { + sp.editor.setCursor({ line: 1, ch: 0 }); + return sp.evalTopLevelFunction(); + }) + .then(([text, error, result]) => { + is(text, fullText, + "Should get full text back since we didn't find a specific function."); + ok(!error, "Should not have got an error."); + ok(!result, "Should not have got a result."); + }) + + // Syntax error. + .then(() => { + sp.setText("function {}"); + sp.editor.setCursor({ line: 0, ch: 9 }); + return sp.evalTopLevelFunction(); + }) + .then(([text, error, result]) => { + is(text, "function {}", + "Should get the full text back since there was a parse error."); + ok(!error, "Should not have got an error"); + ok(!result, "Should not have got a result"); + ok(sp.getText().includes("SyntaxError"), + "We should have written the syntax error to the scratchpad."); + }) + + .then(finish, reportErrorAndQuit); +} diff --git a/devtools/client/scratchpad/test/browser_scratchpad_execute_print.js b/devtools/client/scratchpad/test/browser_scratchpad_execute_print.js new file mode 100644 index 000000000..029916507 --- /dev/null +++ b/devtools/client/scratchpad/test/browser_scratchpad_execute_print.js @@ -0,0 +1,116 @@ +/* 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"; + +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* () { + yield inContent(function* () { + content.wrappedJSObject.foobarBug636725 = 1; + }); + sp.editor.setText("++window.foobarBug636725"); + }, + then: function* ([code, , result]) { + is(code, sp.getText(), "code is correct"); + + let pageResult = yield inContent(function* () { + return content.wrappedJSObject.foobarBug636725; + }); + is(result, pageResult, + "result is correct"); + + is(sp.getText(), "++window.foobarBug636725", + "run() does not change the editor content"); + + is(pageResult, 2, "run() updated window.foobarBug636725"); + } + }, { + method: "display", + prepare: function* () {}, + then: function* () { + let pageResult = yield inContent(function* () { + return content.wrappedJSObject.foobarBug636725; + }); + is(pageResult, 3, "display() updated window.foobarBug636725"); + + is(sp.getText(), "++window.foobarBug636725\n/*\n3\n*/", + "display() shows evaluation result in the textbox"); + + is(sp.editor.getSelection(), "\n/*\n3\n*/", "getSelection is correct"); + } + }, { + method: "run", + prepare: function* () { + sp.editor.setText("window.foobarBug636725 = 'a';\n" + + "window.foobarBug636725 = 'b';"); + sp.editor.setSelection({ line: 0, ch: 0 }, { line: 0, ch: 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"); + + let pageResult = yield inContent(function* () { + return content.wrappedJSObject.foobarBug636725; + }); + is(pageResult, "a", "run() worked for the selected range"); + } + }, { + method: "display", + prepare: function* () { + sp.editor.setText("window.foobarBug636725 = 'c';\n" + + "window.foobarBug636725 = 'b';"); + sp.editor.setSelection({ line: 0, ch: 0 }, { line: 0, ch: 22 }); + }, + then: function* () { + let pageResult = yield inContent(function* () { + return content.wrappedJSObject.foobarBug636725; + }); + is(pageResult, "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.editor.getSelection(), "\n/*\na\n*/", "getSelection is correct"); + } + }]; + + runAsyncCallbackTests(sp, tests).then(function () { + ok(sp.editor.somethingSelected(), "something is selected"); + sp.editor.dropSelection(); + ok(!sp.editor.somethingSelected(), "something is no longer selected"); + ok(!sp.editor.getSelection(), "getSelection is empty"); + + // Test undo/redo. + sp.editor.setText("foo1"); + sp.editor.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/devtools/client/scratchpad/test/browser_scratchpad_falsy.js b/devtools/client/scratchpad/test/browser_scratchpad_falsy.js new file mode 100644 index 000000000..9eb7efb94 --- /dev/null +++ b/devtools/client/scratchpad/test/browser_scratchpad_falsy.js @@ -0,0 +1,69 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* Bug 679467 */ + +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/devtools/client/scratchpad/test/browser_scratchpad_files.js b/devtools/client/scratchpad/test/browser_scratchpad_files.js new file mode 100644 index 000000000..d52718a81 --- /dev/null +++ b/devtools/client/scratchpad/test/browser_scratchpad_files.js @@ -0,0 +1,119 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Reference to the Scratchpad object. +var gScratchpad; + +// Reference to the temporary nsIFile we will work with. +var gFile; + +// The temporary file content. +var 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"); + + is(gScratchpad.dirty, false, + "the editor marks imported file as saved"); + + // Save the file after changes. + gFileContent += "// omg, saved!"; + gScratchpad.editor.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.editor.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({ + uri: NetUtil.newURI(gFile), + loadUsingSystemPrincipal: true}); + 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/devtools/client/scratchpad/test/browser_scratchpad_goto_line_ui.js b/devtools/client/scratchpad/test/browser_scratchpad_goto_line_ui.js new file mode 100644 index 000000000..34c71ac0c --- /dev/null +++ b/devtools/client/scratchpad/test/browser_scratchpad_goto_line_ui.js @@ -0,0 +1,43 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* Bug 714942 */ + +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.setCursor({ line: 0, ch: 0 }); + + let oldPrompt = editor.openDialog; + let desiredValue; + + editor.openDialog = function (text, cb) { + cb(desiredValue); + }; + + desiredValue = 3; + EventUtils.synthesizeKey("J", {accelKey: true}, aWindow); + is(editor.getCursor().line, 2, "line is correct"); + + desiredValue = 2; + EventUtils.synthesizeKey("J", {accelKey: true}, aWindow); + is(editor.getCursor().line, 1, "line is correct (again)"); + + editor.openDialog = oldPrompt; + finish(); +} diff --git a/devtools/client/scratchpad/test/browser_scratchpad_help_key.js b/devtools/client/scratchpad/test/browser_scratchpad_help_key.js new file mode 100644 index 000000000..d5383db57 --- /dev/null +++ b/devtools/client/scratchpad/test/browser_scratchpad_help_key.js @@ -0,0 +1,59 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* Bug 650760 */ + +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); + + 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/devtools/client/scratchpad/test/browser_scratchpad_initialization.js b/devtools/client/scratchpad/test/browser_scratchpad_initialization.js new file mode 100644 index 000000000..387bcb08c --- /dev/null +++ b/devtools/client/scratchpad/test/browser_scratchpad_initialization.js @@ -0,0 +1,50 @@ +/* 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 DEVTOOLS_CHROME_ENABLED = "devtools.chrome.enabled"; + +function test() +{ + waitForExplicitFinish(); + + Services.prefs.setBoolPref(DEVTOOLS_CHROME_ENABLED, false); + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("load", function onLoad() { + gBrowser.selectedBrowser.removeEventListener("load", onLoad, true); + + 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"); + + Services.prefs.clearUserPref(DEVTOOLS_CHROME_ENABLED); + finish(); +} diff --git a/devtools/client/scratchpad/test/browser_scratchpad_inspect.js b/devtools/client/scratchpad/test/browser_scratchpad_inspect.js new file mode 100644 index 000000000..23194f05e --- /dev/null +++ b/devtools/client/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 of sidebar.variablesView) { + for (let [, obj] of scope) { + for (let [, prop] of 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/devtools/client/scratchpad/test/browser_scratchpad_inspect_primitives.js b/devtools/client/scratchpad/test/browser_scratchpad_inspect_primitives.js new file mode 100644 index 000000000..914f0a6d8 --- /dev/null +++ b/devtools/client/scratchpad/test/browser_scratchpad_inspect_primitives.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that inspecting primitive values uses the object inspector, not an +// inline comment. + +var {Task} = require("devtools/shared/task"); + +function test() { + const options = { + tabContent: "test inspecting primitive values" + }; + openTabAndScratchpad(options) + .then(Task.async(runTests)) + .then(finish, console.error); +} + +function* runTests([win, sp]) { + // Inspect a number. + yield checkResults(sp, 7); + + // Inspect a string. + yield checkResults(sp, "foobar", true); + + // Inspect a boolean. + yield checkResults(sp, true); +} + +// Helper function that does the actual testing. +var checkResults = Task.async(function* (sp, value, isString = false) { + let sourceValue = value; + if (isString) { + sourceValue = '"' + value + '"'; + } + let source = "var foobar = " + sourceValue + "; foobar"; + sp.setText(source); + yield sp.inspect(); + + let sidebar = sp.sidebar; + ok(sidebar.visible, "sidebar is open"); + + let found = false; + + outer: for (let scope of sidebar.variablesView) { + for (let [, obj] of scope) { + for (let [, prop] of obj) { + if (prop.name == "value" && prop.value == value) { + found = true; + break outer; + } + } + } + } + + ok(found, "found the value of " + value); + + let tabbox = sidebar._sidebar._tabbox; + ok(!tabbox.hasAttribute("hidden"), "Scratchpad sidebar visible"); + sidebar.hide(); + ok(tabbox.hasAttribute("hidden"), "Scratchpad sidebar hidden"); +}); diff --git a/devtools/client/scratchpad/test/browser_scratchpad_long_string.js b/devtools/client/scratchpad/test/browser_scratchpad_long_string.js new file mode 100644 index 000000000..d85a7df1c --- /dev/null +++ b/devtools/client/scratchpad/test/browser_scratchpad_long_string.js @@ -0,0 +1,30 @@ +/* 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 long string in Scratchpad</p>"; +} + +function runTests() +{ + let sp = gScratchpadWindow.Scratchpad; + + sp.setText("'0'.repeat(10000)"); + + sp.display().then(() => { + is(sp.getText(), "'0'.repeat(10000)\n" + + "/*\n" + "0".repeat(10000) + "\n*/", + "display()ing a long string works"); + finish(); + }); +} diff --git a/devtools/client/scratchpad/test/browser_scratchpad_modeline.js b/devtools/client/scratchpad/test/browser_scratchpad_modeline.js new file mode 100644 index 000000000..20a4e8449 --- /dev/null +++ b/devtools/client/scratchpad/test/browser_scratchpad_modeline.js @@ -0,0 +1,87 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* Bug 644413 */ + +var gScratchpad; // Reference to the Scratchpad object. +var gFile; // Reference to the temporary nsIFile we will work with. +var DEVTOOLS_CHROME_ENABLED = "devtools.chrome.enabled"; + +// The temporary file content. +var gFileContent = "function main() { return 0; }"; + +function test() { + waitForExplicitFinish(); + + Services.prefs.setBoolPref(DEVTOOLS_CHROME_ENABLED, false); + 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); +}); diff --git a/devtools/client/scratchpad/test/browser_scratchpad_open.js b/devtools/client/scratchpad/test/browser_scratchpad_open.js new file mode 100644 index 000000000..ec55f0101 --- /dev/null +++ b/devtools/client/scratchpad/test/browser_scratchpad_open.js @@ -0,0 +1,101 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// only finish() when correct number of tests are done +const expected = 4; +var count = 0; +var lastUniqueName = null; + +function done() +{ + if (++count == expected) { + finish(); + } +} + +function test() +{ + waitForExplicitFinish(); + testOpen(); + testOpenWithState(); + testOpenInvalidState(); + testOpenTestFile(); +} + +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(); +} + +function testOpenTestFile() +{ + let win = openScratchpad(function (win) { + ok(win, "scratchpad opened for file open"); + try { + win.Scratchpad.importFromFile( + "http://example.com/browser/devtools/client/scratchpad/test/NS_ERROR_ILLEGAL_INPUT.txt", + "silent", + function (aStatus, content) { + let nb = win.document.querySelector("#scratchpad-notificationbox"); + is(nb.querySelectorAll("notification").length, 1, "There is just one notification"); + let cn = nb.currentNotification; + is(cn.priority, nb.PRIORITY_WARNING_HIGH, "notification priority is correct"); + is(cn.value, "file-import-convert-failed", "notification value is corrent"); + is(cn.type, "warning", "notification type is correct"); + done(); + }); + ok(true, "importFromFile does not cause exception"); + } catch (exception) { + ok(false, "importFromFile causes exception " + DevToolsUtils.safeErrorString(exception)); + } + }, {noFocus: true}); +} diff --git a/devtools/client/scratchpad/test/browser_scratchpad_open_error_console.js b/devtools/client/scratchpad/test/browser_scratchpad_open_error_console.js new file mode 100644 index 000000000..4da2a2daf --- /dev/null +++ b/devtools/client/scratchpad/test/browser_scratchpad_open_error_console.js @@ -0,0 +1,39 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const HUDService = require("devtools/client/webconsole/hudservice"); + +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,test Scratchpad." + + "openErrorConsole()"; +} + +function runTests() +{ + Services.obs.addObserver(function observer(aSubject) { + Services.obs.removeObserver(observer, "web-console-created"); + aSubject.QueryInterface(Ci.nsISupportsString); + + let hud = HUDService.getBrowserConsole(); + ok(hud, "browser console is open"); + is(aSubject.data, hud.hudId, "notification hudId is correct"); + + HUDService.toggleBrowserConsole().then(finish); + }, "web-console-created", false); + + let hud = HUDService.getBrowserConsole(); + ok(!hud, "browser console is not open"); + info("wait for the browser console to open from Scratchpad"); + + gScratchpadWindow.Scratchpad.openErrorConsole(); +} diff --git a/devtools/client/scratchpad/test/browser_scratchpad_pprint-02.js b/devtools/client/scratchpad/test/browser_scratchpad_pprint-02.js new file mode 100644 index 000000000..c7cd2927e --- /dev/null +++ b/devtools/client/scratchpad/test/browser_scratchpad_pprint-02.js @@ -0,0 +1,40 @@ +/* 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,test Scratchpad pretty print."; +} + +var gTabsize; + +function runTests(sw) +{ + gTabsize = Services.prefs.getIntPref("devtools.editor.tabsize"); + Services.prefs.setIntPref("devtools.editor.tabsize", 6); + const space = " ".repeat(6); + + const sp = sw.Scratchpad; + sp.setText("function main() { console.log(5); }"); + sp.prettyPrint().then(() => { + const prettyText = sp.getText(); + ok(prettyText.includes(space)); + finish(); + }).then(null, error => { + ok(false, error); + }); +} + +registerCleanupFunction(function () { + Services.prefs.setIntPref("devtools.editor.tabsize", gTabsize); + gTabsize = null; +}); diff --git a/devtools/client/scratchpad/test/browser_scratchpad_pprint.js b/devtools/client/scratchpad/test/browser_scratchpad_pprint.js new file mode 100644 index 000000000..1ba9a2cda --- /dev/null +++ b/devtools/client/scratchpad/test/browser_scratchpad_pprint.js @@ -0,0 +1,29 @@ +/* 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,test Scratchpad pretty print."; +} + +function runTests(sw) +{ + const sp = sw.Scratchpad; + sp.setText("function main() { console.log(5); }"); + sp.prettyPrint().then(() => { + const prettyText = sp.getText(); + ok(prettyText.includes("\n")); + finish(); + }).then(null, error => { + ok(false, error); + }); +} diff --git a/devtools/client/scratchpad/test/browser_scratchpad_pprint_error_goto_line.js b/devtools/client/scratchpad/test/browser_scratchpad_pprint_error_goto_line.js new file mode 100644 index 000000000..21f266f61 --- /dev/null +++ b/devtools/client/scratchpad/test/browser_scratchpad_pprint_error_goto_line.js @@ -0,0 +1,78 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2; fill-column: 80 -*- */ +/* 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"; + +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," + + "test Scratchpad pretty print error goto line."; +} + +function testJumpToPrettyPrintError(sp, error, remark) { + info("will test jumpToLine after prettyPrint error" + remark); + + // CodeMirror lines and columns are 0-based, Scratchpad UI and error + // stack are 1-based. + is(/Invalid regular expression flag \(3:10\)/.test(error), true, + "prettyPrint expects error in editor text:\n" + error); + + sp.editor.jumpToLine(); + + const editorDoc = sp.editor.container.contentDocument; + const lineInput = editorDoc.querySelector("input"); + const errorLocation = lineInput.value; + const [ inputLine, inputColumn ] = errorLocation.split(":"); + const errorLine = 3, errorColumn = 10; + + is(inputLine, errorLine, + "jumpToLine input field is set from editor selection (line)"); + is(inputColumn, errorColumn, + "jumpToLine input field is set from editor selection (column)"); + + EventUtils.synthesizeKey("VK_RETURN", { }, editorDoc.defaultView); + + // CodeMirror lines and columns are 0-based, Scratchpad UI and error + // stack are 1-based. + const cursor = sp.editor.getCursor(); + is(inputLine, cursor.line + 1, "jumpToLine goto error location (line)"); + is(inputColumn, cursor.ch + 1, "jumpToLine goto error location (column)"); +} + +function runTests(sw, sp) +{ + sp.setText([ + "// line 1", + "// line 2", + "var re = /a bad /regexp/; // line 3 is an obvious syntax error!", + "// line 4", + "// line 5", + "" + ].join("\n")); + + sp.prettyPrint().then(aFulfill => { + ok(false, "Expecting Invalid regexp flag (3:10)"); + finish(); + }, error => { + testJumpToPrettyPrintError(sp, error, " (Bug 1005471, first time)"); + }); + + sp.prettyPrint().then(aFulfill => { + ok(false, "Expecting Invalid regexp flag (3:10)"); + finish(); + }, error => { + // Second time verifies bug in earlier implementation fixed. + testJumpToPrettyPrintError(sp, error, " (second time)"); + finish(); + }); +} diff --git a/devtools/client/scratchpad/test/browser_scratchpad_recent_files.js b/devtools/client/scratchpad/test/browser_scratchpad_recent_files.js new file mode 100644 index 000000000..66a4e7cd1 --- /dev/null +++ b/devtools/client/scratchpad/test/browser_scratchpad_recent_files.js @@ -0,0 +1,350 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* Bug 651942 */ + +// Reference to the Scratchpad object. +var gScratchpad; + +// References to the temporary nsIFiles. +var gFile01; +var gFile02; +var gFile03; +var gFile04; + +// lists of recent files. +var lists = { + recentFiles01: null, + recentFiles02: null, + recentFiles03: null, + recentFiles04: null, +}; + +// Temporary file names. +var gFileName01 = "file01_ForBug651942.tmp"; +var gFileName02 = "☕"; // See bug 783858 for more information +var gFileName03 = "file03_ForBug651942.tmp"; +var gFileName04 = "file04_ForBug651942.tmp"; + +// Content for the temporary files. +var gFileContent; +var gFileContent01 = "hello.world.01('bug651942');"; +var gFileContent02 = "hello.world.02('bug651942');"; +var gFileContent03 = "hello.world.03('bug651942');"; +var 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, 0o666); + + // 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, + 0o644, 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/devtools/client/scratchpad/test/browser_scratchpad_reload_and_run.js b/devtools/client/scratchpad/test/browser_scratchpad_reload_and_run.js new file mode 100644 index 000000000..19e360b20 --- /dev/null +++ b/devtools/client/scratchpad/test/browser_scratchpad_reload_and_run.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/ */ +/* Bug 740948 */ + +var DEVTOOLS_CHROME_ENABLED = "devtools.chrome.enabled"; +var EDITOR_TEXT = [ + "var evt = new CustomEvent('foo', { bubbles: true });", + "document.body.innerHTML = 'Modified text';", + "window.dispatchEvent(evt);" +].join("\n"); + +function test() +{ + requestLongerTimeout(2); + 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; + + let deferred = promise.defer(); + 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); + deferred.resolve(); + }, true); + }, true); + + ok(browser.contentWindow.document.body.innerHTML !== "Modified text", + "Before reloading, HTML is intact."); + sp.reloadAndRun().then(deferred.promise).then(finish); +} + diff --git a/devtools/client/scratchpad/test/browser_scratchpad_remember_view_options.js b/devtools/client/scratchpad/test/browser_scratchpad_remember_view_options.js new file mode 100644 index 000000000..67073c52f --- /dev/null +++ b/devtools/client/scratchpad/test/browser_scratchpad_remember_view_options.js @@ -0,0 +1,65 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* Bug 1140839 */ + +// Test that view menu items are remembered across scratchpad invocations. +function test() +{ + waitForExplicitFinish(); + + // To test for this bug we open a Scratchpad window and change all + // view menu options. After each change we compare the correspondent + // preference value with the expected value. + + 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>Bug 1140839</title>" + + "<p>test Scratchpad should remember View options"; +} + +function runTests() +{ + let sp = gScratchpadWindow.Scratchpad; + let doc = gScratchpadWindow.document; + + let testData = [ + {itemMenuId: "sp-menu-line-numbers", prefId: "devtools.scratchpad.lineNumbers", expectedVal: false}, + {itemMenuId: "sp-menu-word-wrap", prefId: "devtools.scratchpad.wrapText", expectedVal: true}, + {itemMenuId: "sp-menu-highlight-trailing-space", prefId: "devtools.scratchpad.showTrailingSpace", expectedVal: true}, + {itemMenuId: "sp-menu-larger-font", prefId: "devtools.scratchpad.editorFontSize", expectedVal: 13}, + {itemMenuId: "sp-menu-normal-size-font", prefId: "devtools.scratchpad.editorFontSize", expectedVal: 12}, + {itemMenuId: "sp-menu-smaller-font", prefId: "devtools.scratchpad.editorFontSize", expectedVal: 11}, + ]; + + testData.forEach(function (data) { + let getPref = getPrefFunction(data.prefId); + + try { + let menu = doc.getElementById(data.itemMenuId); + menu.doCommand(); + let newPreferenceValue = getPref(data.prefId); + ok(newPreferenceValue === data.expectedVal, newPreferenceValue + " !== " + data.expectedVal); + Services.prefs.clearUserPref(data.prefId); + } + catch (exception) { + ok(false, "Exception thrown while executing the command of menuitem #" + data.itemMenuId); + } + }); + + finish(); +} + +function getPrefFunction(preferenceId) +{ + let preferenceType = Services.prefs.getPrefType(preferenceId); + if (preferenceType === Services.prefs.PREF_INT) { + return Services.prefs.getIntPref; + } else if (preferenceType === Services.prefs.PREF_BOOL) { + return Services.prefs.getBoolPref; + } +} diff --git a/devtools/client/scratchpad/test/browser_scratchpad_reset_undo.js b/devtools/client/scratchpad/test/browser_scratchpad_reset_undo.js new file mode 100644 index 000000000..a1b60cd33 --- /dev/null +++ b/devtools/client/scratchpad/test/browser_scratchpad_reset_undo.js @@ -0,0 +1,155 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* Bug 684546 */ + +// Reference to the Scratchpad chrome window object. +var gScratchpadWindow; + +// Reference to the Scratchpad object. +var gScratchpad; + +// Reference to the temporary nsIFile we will work with. +var gFileA; +var gFileB; + +// The temporary file content. +var gFileAContent = "// File A ** Hello World!"; +var gFileBContent = "// File B ** Goodbye All"; + +// Help track if one or both files are saved +var 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, 0o666); + + gFileB = FileUtils.getFile("TmpD", ["fileBForBug684546.tmp"]); + gFileB.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666); + + // 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, + 0o644, 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, + 0o644, 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.editor.replaceText("new text", + gScratchpad.editor.getPosition(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.editor.replaceText("new text", + gScratchpad.editor.getPosition(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/devtools/client/scratchpad/test/browser_scratchpad_restore.js b/devtools/client/scratchpad/test/browser_scratchpad_restore.js new file mode 100644 index 000000000..5890e954f --- /dev/null +++ b/devtools/client/scratchpad/test/browser_scratchpad_restore.js @@ -0,0 +1,96 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* 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/devtools/client/scratchpad/test/browser_scratchpad_revert_to_saved.js b/devtools/client/scratchpad/test/browser_scratchpad_revert_to_saved.js new file mode 100644 index 000000000..92c6c3720 --- /dev/null +++ b/devtools/client/scratchpad/test/browser_scratchpad_revert_to_saved.js @@ -0,0 +1,134 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* Bug 751744 */ + +// Reference to the Scratchpad object. +var gScratchpad; + +// Reference to the temporary nsIFiles. +var gFile; + +// Temporary file name. +var gFileName = "testFileForBug751744.tmp"; + + +// Content for the temporary file. +var gFileContent = "/* this file is already saved */\n" + + "function foo() { alert('bar') }"; +var gLength = gFileContent.length; + +// Reference to the menu entry. +var menu; + +function startTest() +{ + gScratchpad = gScratchpadWindow.Scratchpad; + menu = gScratchpadWindow.document.getElementById("sp-cmd-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(gScratchpad.getText() + "\nfoo();"); + // 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(gScratchpad.getText() + "\nalert(foo.toSource());"); + // 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(gScratchpad.getText() + "\nfoo();"); + + // 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 = gScratchpad = menu = null; + finish(); +} + +function createAndLoadTemporaryFile() +{ + // Create a temporary file. + gFile = FileUtils.getFile("TmpD", [gFileName]); + gFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666); + + // 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, + 0o644, 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() +{ + waitForExplicitFinish(); + + 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/devtools/client/scratchpad/test/browser_scratchpad_run_error_goto_line.js b/devtools/client/scratchpad/test/browser_scratchpad_run_error_goto_line.js new file mode 100644 index 000000000..a5d3d5163 --- /dev/null +++ b/devtools/client/scratchpad/test/browser_scratchpad_run_error_goto_line.js @@ -0,0 +1,60 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2; fill-column: 80 -*- */ +/* 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"; + +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,test Scratchpad pretty print."; +} + +function runTests(sw) +{ + const sp = sw.Scratchpad; + sp.setText([ + "// line 1", + "// line 2", + "var re = /a bad /regexp/; // line 3 is an obvious syntax error!", + "// line 4", + "// line 5", + "" + ].join("\n")); + sp.run().then(() => { + // CodeMirror lines and columns are 0-based, Scratchpad UI and error + // stack are 1-based. + let errorLine = 3; + let editorDoc = sp.editor.container.contentDocument; + sp.editor.jumpToLine(); + let lineInput = editorDoc.querySelector("input"); + let inputLine = lineInput.value; + is(inputLine, errorLine, "jumpToLine input field is set from editor selection"); + EventUtils.synthesizeKey("VK_RETURN", { }, editorDoc.defaultView); + // CodeMirror lines and columns are 0-based, Scratchpad UI and error + // stack are 1-based. + let cursor = sp.editor.getCursor(); + is(cursor.line + 1, inputLine, "jumpToLine goto error location (line)"); + is(cursor.ch + 1, 1, "jumpToLine goto error location (column)"); + }, error => { + ok(false, error); + finish(); + }).then(() => { + var statusBarField = sp.editor.container.ownerDocument.querySelector("#statusbar-line-col"); + let { line, ch } = sp.editor.getCursor(); + is(statusBarField.textContent, sp.strings.formatStringFromName( + "scratchpad.statusBarLineCol", [ line + 1, ch + 1], 2), "statusbar text is correct (" + statusBarField.textContent + ")"); + finish(); + }, error => { + ok(false, error); + finish(); + }); +} diff --git a/devtools/client/scratchpad/test/browser_scratchpad_tab.js b/devtools/client/scratchpad/test/browser_scratchpad_tab.js new file mode 100644 index 000000000..67057f206 --- /dev/null +++ b/devtools/client/scratchpad/test/browser_scratchpad_tab.js @@ -0,0 +1,75 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* Bug 660560 */ + +function test() +{ + waitForExplicitFinish(); + + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("load", function onTabLoad() { + gBrowser.selectedBrowser.removeEventListener("load", onTabLoad, true); + + 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.setCursor({ line: 0, ch: 0 }); + + EventUtils.synthesizeKey("VK_TAB", {}, gScratchpadWindow); + + is(sp.getText(), " window.foo;", "Tab key added 5 spaces"); + + is(sp.editor.getCursor().line, 0, "line is correct"); + is(sp.editor.getCursor().ch, 5, "character is correct"); + + sp.editor.setCursor({ line: 0, ch: 6 }); + + EventUtils.synthesizeKey("VK_TAB", {}, gScratchpadWindow); + + is(sp.getText(), " w indow.foo;", + "Tab key added 4 spaces"); + + is(sp.editor.getCursor().line, 0, "line is correct"); + is(sp.editor.getCursor().ch, 10, "character is correct"); + + 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.setCursor({ line: 0, ch: 0 }); + + EventUtils.synthesizeKey("VK_TAB", {}, gScratchpadWindow); + + is(sp.getText(), "\twindow.foo;", "Tab key added the tab character"); + + is(sp.editor.getCursor().line, 0, "line is correct"); + is(sp.editor.getCursor().ch, 1, "character is correct"); + + Services.prefs.clearUserPref("devtools.editor.tabsize"); + Services.prefs.clearUserPref("devtools.editor.expandtab"); + + finish(); +} diff --git a/devtools/client/scratchpad/test/browser_scratchpad_tab_switch.js b/devtools/client/scratchpad/test/browser_scratchpad_tab_switch.js new file mode 100644 index 000000000..c2419a1e1 --- /dev/null +++ b/devtools/client/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/ */ + +var tab1; +var tab2; +var 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/devtools/client/scratchpad/test/browser_scratchpad_throw_output.js b/devtools/client/scratchpad/test/browser_scratchpad_throw_output.js new file mode 100644 index 000000000..cfcd5e049 --- /dev/null +++ b/devtools/client/scratchpad/test/browser_scratchpad_throw_output.js @@ -0,0 +1,52 @@ +/* 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(testThrowOutput); + }, true); + + content.location = "data:text/html;charset=utf8,<p>Test throw outputs in Scratchpad</p>"; +} + +function testThrowOutput() +{ + let scratchpad = gScratchpadWindow.Scratchpad, tests = []; + + let falsyValues = ["false", "0", "-0", "null", "undefined", "Infinity", + "-Infinity", "NaN"]; + falsyValues.forEach(function (value) { + tests.push({ + method: "display", + code: "throw " + value + ";", + result: "throw " + value + ";\n/*\nException: " + value + "\n*/", + label: "Correct exception message for '" + value + "' is shown" + }); + }); + + let { DebuggerServer } = require("devtools/server/main"); + + let longLength = DebuggerServer.LONG_STRING_LENGTH + 1; + let longString = new Array(longLength).join("a"); + let shortedString = longString.substring(0, + DebuggerServer.LONG_STRING_INITIAL_LENGTH) + "\u2026"; + + tests.push({ + method: "display", + code: "throw (new Array(" + longLength + ").join('a'));", + result: "throw (new Array(" + longLength + ").join('a'));\n" + + "/*\nException: " + shortedString + "\n*/", + label: "Correct exception message for a longString is shown" + }); + + runAsyncTests(scratchpad, tests).then(function () { + finish(); + }); +} diff --git a/devtools/client/scratchpad/test/browser_scratchpad_ui.js b/devtools/client/scratchpad/test/browser_scratchpad_ui.js new file mode 100644 index 000000000..5d8af1068 --- /dev/null +++ b/devtools/client/scratchpad/test/browser_scratchpad_ui.js @@ -0,0 +1,74 @@ +/* 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-reloadAndRun": "reloadAndRun", + "sp-menu-content": "setContentContext", + "sp-menu-browser": "setBrowserContext", + "sp-menu-pprint": "prettyPrint", + "sp-menu-line-numbers": "toggleEditorOption", + "sp-menu-word-wrap": "toggleEditorOption", + "sp-menu-highlight-trailing-space": "toggleEditorOption", + "sp-menu-larger-font": "increaseFontSize", + "sp-menu-smaller-font": "decreaseFontSize", + "sp-menu-normal-size-font": "normalFontSize", + }; + + let lastMethodCalled = null; + + for (let id in methodsAndItems) { + lastMethodCalled = null; + + let methodName = methodsAndItems[id]; + let oldMethod = sp[methodName]; + ok(oldMethod, "found method " + methodName + " in Scratchpad object"); + + sp[methodName] = () => { + lastMethodCalled = 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; + } + + finish(); +} diff --git a/devtools/client/scratchpad/test/browser_scratchpad_unsaved.js b/devtools/client/scratchpad/test/browser_scratchpad_unsaved.js new file mode 100644 index 000000000..54b97b75b --- /dev/null +++ b/devtools/client/scratchpad/test/browser_scratchpad_unsaved.js @@ -0,0 +1,119 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* Bug 669612 */ + +// only finish() when correct number of tests are done +const expected = 4; +var count = 0; +function done() +{ + if (++count == expected) { + finish(); + } +} + + +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 star if scratchpad text changes"); + + aScratchpad.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.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/devtools/client/scratchpad/test/browser_scratchpad_wrong_window_focus.js b/devtools/client/scratchpad/test/browser_scratchpad_wrong_window_focus.js new file mode 100644 index 000000000..0d094ba98 --- /dev/null +++ b/devtools/client/scratchpad/test/browser_scratchpad_wrong_window_focus.js @@ -0,0 +1,93 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* Bug 661762 */ + +// Use the old webconsole since scratchpad focus isn't working on new one (Bug 1304794) +Services.prefs.setBoolPref("devtools.webconsole.new-frontend-enabled", false); +registerCleanupFunction(function* () { + Services.prefs.clearUserPref("devtools.webconsole.new-frontend-enabled"); +}); + +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; + let {require} = Cu.import("resource://devtools/shared/Loader.jsm", {}); + let {TargetFactory} = require("devtools/client/framework/target"); + + openScratchpad(function () { + let target = TargetFactory.forTab(gBrowser.selectedTab); + gDevTools.showToolbox(target, "webconsole").then((toolbox) => { + let hud = toolbox.getCurrentPanel().hud; + hud.jsterm.clearOutput(true); + testFocus(sw, hud); + }); + }); + }); + }, true); + + content.location = "data:text/html;charset=utf8,<p>test window focus for Scratchpad."; +} + +function testFocus(sw, hud) { + let sp = sw.Scratchpad; + + function onMessage(event, messages) { + let msg = [...messages][0]; + let node = msg.node; + + var loc = node.querySelector(".frame-link"); + ok(loc, "location element exists"); + is(loc.getAttribute("data-url"), sw.Scratchpad.uniqueName, "location value is correct"); + is(loc.getAttribute("data-line"), "1", "line value is correct"); + is(loc.getAttribute("data-column"), "1", "column 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. + hud.ui.once("new-messages", onMessage); + + 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.type, "undefined", "result is correct"); + }); +} diff --git a/devtools/client/scratchpad/test/head.js b/devtools/client/scratchpad/test/head.js new file mode 100644 index 000000000..15619a169 --- /dev/null +++ b/devtools/client/scratchpad/test/head.js @@ -0,0 +1,221 @@ +/* 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"; + +const {NetUtil} = Cu.import("resource://gre/modules/NetUtil.jsm", {}); +const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm", {}); +const {console} = Cu.import("resource://gre/modules/Console.jsm", {}); +const {ScratchpadManager} = Cu.import("resource://devtools/client/scratchpad/scratchpad-manager.jsm", {}); +const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {}); +const Services = require("Services"); +const DevToolsUtils = require("devtools/shared/DevToolsUtils"); +const flags = require("devtools/shared/flags"); +const promise = require("promise"); + + +var gScratchpadWindow; // Reference to the Scratchpad chrome window object + +flags.testing = true; +SimpleTest.registerCleanupFunction(() => { + flags.testing = false; +}); + +/** + * 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 = {}) +{ + let win = aOptions.window || + 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; +} + +/** + * Open a new tab and then open a scratchpad. + * @param object aOptions + * Optional. Options for opening the tab and scratchpad. In addition + * to the options supported by openScratchpad, the following options + * are supported: + * - tabContent + * A string providing the html content of the tab. + * @return Promise + */ +function openTabAndScratchpad(aOptions = {}) +{ + waitForExplicitFinish(); + return new promise(resolve => { + gBrowser.selectedTab = gBrowser.addTab(); + let {selectedBrowser} = gBrowser; + selectedBrowser.addEventListener("load", function onLoad() { + selectedBrowser.removeEventListener("load", onLoad, true); + openScratchpad((win, sp) => resolve([win, sp]), aOptions); + }, true); + content.location = "data:text/html;charset=utf8," + (aOptions.tabContent || ""); + }); +} + +/** + * 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. + */ +var runAsyncCallbackTests = Task.async(function* (aScratchpad, aTests) { + for (let {prepare, method, then} of aTests) { + yield prepare(); + let res = yield aScratchpad[method](); + yield then(res); + } +}); + +/** + * A simple wrapper for ContentTask.spawn for more compact code. + */ +function inContent(generator) { + return ContentTask.spawn(gBrowser.selectedBrowser, {}, generator); +} + +function cleanup() +{ + if (gScratchpadWindow) { + gScratchpadWindow.close(); + gScratchpadWindow = null; + } + while (gBrowser.tabs.length > 1) { + gBrowser.removeCurrentTab(); + } +} + +registerCleanupFunction(cleanup); |