diff options
author | Matt A. Tobin <email@mattatobin.com> | 2016-10-16 19:34:53 -0400 |
---|---|---|
committer | Matt A. Tobin <email@mattatobin.com> | 2016-10-16 19:34:53 -0400 |
commit | 81805ce3f63e2e4a799bd54f174083c58a9b5640 (patch) | |
tree | 6e13374b213ac9b2ae74c25d8aac875faf71fdd0 /toolkit/devtools/debugger | |
parent | 28c8da71bf521bb3ee76f27b8a241919e24b7cd5 (diff) | |
download | palemoon-gre-81805ce3f63e2e4a799bd54f174083c58a9b5640.tar.gz |
Move Mozilla DevTools to Platform - Part 3: Merge the browser/devtools and toolkit/devtools adjusting for directory collisions
Diffstat (limited to 'toolkit/devtools/debugger')
358 files changed, 40437 insertions, 0 deletions
diff --git a/toolkit/devtools/debugger/debugger-commands.js b/toolkit/devtools/debugger/debugger-commands.js new file mode 100644 index 000000000..72824c446 --- /dev/null +++ b/toolkit/devtools/debugger/debugger-commands.js @@ -0,0 +1,604 @@ +/* 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 { Cc, Ci, Cu } = require("chrome"); +const gcli = require("gcli/index"); + +loader.lazyImporter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm"); + +/** + * The commands and converters that are exported to GCLI + */ +exports.items = []; + +/** + * Utility to get access to the current breakpoint list. + * + * @param DebuggerPanel dbg + * The debugger panel. + * @return array + * An array of objects, one for each breakpoint, where each breakpoint + * object has the following properties: + * - url: the URL of the source file. + * - label: a unique string identifier designed to be user visible. + * - lineNumber: the line number of the breakpoint in the source file. + * - lineText: the text of the line at the breakpoint. + * - truncatedLineText: lineText truncated to MAX_LINE_TEXT_LENGTH. + */ +function getAllBreakpoints(dbg) { + let breakpoints = []; + let sources = dbg._view.Sources; + let { trimUrlLength: trim } = dbg.panelWin.SourceUtils; + + for (let source of sources) { + for (let { attachment: breakpoint } of source) { + breakpoints.push({ + url: source.attachment.source.url, + label: source.attachment.label + ":" + breakpoint.line, + lineNumber: breakpoint.line, + lineText: breakpoint.text, + truncatedLineText: trim(breakpoint.text, MAX_LINE_TEXT_LENGTH, "end") + }); + } + } + + return breakpoints; +} + +function getAllSources(dbg) { + if (!dbg) { + return []; + } + + let items = dbg._view.Sources.items; + return items + .filter(item => !!item.attachment.source.url) + .map(item => ({ + name: item.attachment.source.url, + value: item.attachment.source.actor + })); +} + +/** + * 'break' command + */ +exports.items.push({ + name: "break", + description: gcli.lookup("breakDesc"), + manual: gcli.lookup("breakManual") +}); + +/** + * 'break list' command + */ +exports.items.push({ + name: "break list", + description: gcli.lookup("breaklistDesc"), + returnType: "breakpoints", + exec: function(args, context) { + let dbg = getPanel(context, "jsdebugger", { ensureOpened: true }); + return dbg.then(getAllBreakpoints); + } +}); + +exports.items.push({ + item: "converter", + from: "breakpoints", + to: "view", + exec: function(breakpoints, context) { + let dbg = getPanel(context, "jsdebugger"); + if (dbg && breakpoints.length) { + return context.createView({ + html: breakListHtml, + data: { + breakpoints: breakpoints, + onclick: context.update, + ondblclick: context.updateExec + } + }); + } else { + return context.createView({ + html: "<p>${message}</p>", + data: { message: gcli.lookup("breaklistNone") } + }); + } + } +}); + +var breakListHtml = "" + + "<table>" + + " <thead>" + + " <th>Source</th>" + + " <th>Line</th>" + + " <th>Actions</th>" + + " </thead>" + + " <tbody>" + + " <tr foreach='breakpoint in ${breakpoints}'>" + + " <td class='gcli-breakpoint-label'>${breakpoint.label}</td>" + + " <td class='gcli-breakpoint-lineText'>" + + " ${breakpoint.truncatedLineText}" + + " </td>" + + " <td>" + + " <span class='gcli-out-shortcut'" + + " data-command='break del ${breakpoint.label}'" + + " onclick='${onclick}'" + + " ondblclick='${ondblclick}'>" + + " " + gcli.lookup("breaklistOutRemove") + "</span>" + + " </td>" + + " </tr>" + + " </tbody>" + + "</table>" + + ""; + +var MAX_LINE_TEXT_LENGTH = 30; +var MAX_LABEL_LENGTH = 20; + +/** + * 'break add' command + */ +exports.items.push({ + name: "break add", + description: gcli.lookup("breakaddDesc"), + manual: gcli.lookup("breakaddManual") +}); + +/** + * 'break add line' command + */ +exports.items.push({ + name: "break add line", + description: gcli.lookup("breakaddlineDesc"), + params: [ + { + name: "file", + type: { + name: "selection", + lookup: function(context) { + return getAllSources(getPanel(context, "jsdebugger")); + } + }, + description: gcli.lookup("breakaddlineFileDesc") + }, + { + name: "line", + type: { name: "number", min: 1, step: 10 }, + description: gcli.lookup("breakaddlineLineDesc") + } + ], + returnType: "string", + exec: function(args, context) { + let dbg = getPanel(context, "jsdebugger"); + if (!dbg) { + return gcli.lookup("debuggerStopped"); + } + + let deferred = context.defer(); + let item = dbg._view.Sources.getItemForAttachment(a => { + return a.source && a.source.actor === args.file; + }) + let position = { actor: item.value, line: args.line }; + + dbg.addBreakpoint(position).then(() => { + deferred.resolve(gcli.lookup("breakaddAdded")); + }, aError => { + deferred.resolve(gcli.lookupFormat("breakaddFailed", [aError])); + }); + + return deferred.promise; + } +}); + +/** + * 'break del' command + */ +exports.items.push({ + name: "break del", + description: gcli.lookup("breakdelDesc"), + params: [ + { + name: "breakpoint", + type: { + name: "selection", + lookup: function(context) { + let dbg = getPanel(context, "jsdebugger"); + if (!dbg) { + return []; + } + return getAllBreakpoints(dbg).map(breakpoint => ({ + name: breakpoint.label, + value: breakpoint, + description: breakpoint.truncatedLineText + })); + } + }, + description: gcli.lookup("breakdelBreakidDesc") + } + ], + returnType: "string", + exec: function(args, context) { + let dbg = getPanel(context, "jsdebugger"); + if (!dbg) { + return gcli.lookup("debuggerStopped"); + } + + let source = dbg._view.Sources.getItemForAttachment(a => { + return a.source && a.source.url === args.breakpoint.url + }); + + let deferred = context.defer(); + let position = { actor: source.attachment.source.actor, + line: args.breakpoint.lineNumber }; + + dbg.removeBreakpoint(position).then(() => { + deferred.resolve(gcli.lookup("breakdelRemoved")); + }, () => { + deferred.resolve(gcli.lookup("breakNotFound")); + }); + + return deferred.promise; + } +}); + +/** + * 'dbg' command + */ +exports.items.push({ + name: "dbg", + description: gcli.lookup("dbgDesc"), + manual: gcli.lookup("dbgManual") +}); + +/** + * 'dbg open' command + */ +exports.items.push({ + name: "dbg open", + description: gcli.lookup("dbgOpen"), + params: [], + exec: function(args, context) { + let target = context.environment.target; + return gDevTools.showToolbox(target, "jsdebugger").then(() => null); + } +}); + +/** + * 'dbg close' command + */ +exports.items.push({ + name: "dbg close", + description: gcli.lookup("dbgClose"), + params: [], + exec: function(args, context) { + if (!getPanel(context, "jsdebugger")) { + return; + } + let target = context.environment.target; + return gDevTools.closeToolbox(target).then(() => null); + } +}); + +/** + * 'dbg interrupt' command + */ +exports.items.push({ + name: "dbg interrupt", + description: gcli.lookup("dbgInterrupt"), + params: [], + exec: function(args, context) { + let dbg = getPanel(context, "jsdebugger"); + if (!dbg) { + return gcli.lookup("debuggerStopped"); + } + + let controller = dbg._controller; + let thread = controller.activeThread; + if (!thread.paused) { + thread.interrupt(); + } + } +}); + +/** + * 'dbg continue' command + */ +exports.items.push({ + name: "dbg continue", + description: gcli.lookup("dbgContinue"), + params: [], + exec: function(args, context) { + let dbg = getPanel(context, "jsdebugger"); + if (!dbg) { + return gcli.lookup("debuggerStopped"); + } + + let controller = dbg._controller; + let thread = controller.activeThread; + if (thread.paused) { + thread.resume(); + } + } +}); + +/** + * 'dbg step' command + */ +exports.items.push({ + name: "dbg step", + description: gcli.lookup("dbgStepDesc"), + manual: gcli.lookup("dbgStepManual") +}); + +/** + * 'dbg step over' command + */ +exports.items.push({ + name: "dbg step over", + description: gcli.lookup("dbgStepOverDesc"), + params: [], + exec: function(args, context) { + let dbg = getPanel(context, "jsdebugger"); + if (!dbg) { + return gcli.lookup("debuggerStopped"); + } + + let controller = dbg._controller; + let thread = controller.activeThread; + if (thread.paused) { + thread.stepOver(); + } + } +}); + +/** + * 'dbg step in' command + */ +exports.items.push({ + name: 'dbg step in', + description: gcli.lookup("dbgStepInDesc"), + params: [], + exec: function(args, context) { + let dbg = getPanel(context, "jsdebugger"); + if (!dbg) { + return gcli.lookup("debuggerStopped"); + } + + let controller = dbg._controller; + let thread = controller.activeThread; + if (thread.paused) { + thread.stepIn(); + } + } +}); + +/** + * 'dbg step over' command + */ +exports.items.push({ + name: 'dbg step out', + description: gcli.lookup("dbgStepOutDesc"), + params: [], + exec: function(args, context) { + let dbg = getPanel(context, "jsdebugger"); + if (!dbg) { + return gcli.lookup("debuggerStopped"); + } + + let controller = dbg._controller; + let thread = controller.activeThread; + if (thread.paused) { + thread.stepOut(); + } + } +}); + +/** + * 'dbg list' command + */ +exports.items.push({ + name: "dbg list", + description: gcli.lookup("dbgListSourcesDesc"), + params: [], + returnType: "dom", + exec: function(args, context) { + let dbg = getPanel(context, "jsdebugger"); + if (!dbg) { + return gcli.lookup("debuggerClosed"); + } + + let sources = getAllSources(dbg); + let doc = context.environment.chromeDocument; + let div = createXHTMLElement(doc, "div"); + let ol = createXHTMLElement(doc, "ol"); + + sources.forEach(source => { + let li = createXHTMLElement(doc, "li"); + li.textContent = source.name; + ol.appendChild(li); + }); + div.appendChild(ol); + + return div; + } +}); + +/** + * Define the 'dbg blackbox' and 'dbg unblackbox' commands. + */ +[ + { + name: "blackbox", + clientMethod: "blackBox", + l10nPrefix: "dbgBlackBox" + }, + { + name: "unblackbox", + clientMethod: "unblackBox", + l10nPrefix: "dbgUnBlackBox" + } +].forEach(function(cmd) { + const lookup = function(id) { + return gcli.lookup(cmd.l10nPrefix + id); + }; + + exports.items.push({ + name: "dbg " + cmd.name, + description: lookup("Desc"), + params: [ + { + name: "source", + type: { + name: "selection", + lookup: function(context) { + return getAllSources(getPanel(context, "jsdebugger")); + } + }, + description: lookup("SourceDesc"), + defaultValue: null + }, + { + name: "glob", + type: "string", + description: lookup("GlobDesc"), + defaultValue: null + }, + { + name: "invert", + type: "boolean", + description: lookup("InvertDesc") + } + ], + returnType: "dom", + exec: function(args, context) { + const dbg = getPanel(context, "jsdebugger"); + const doc = context.environment.chromeDocument; + if (!dbg) { + throw new Error(gcli.lookup("debuggerClosed")); + } + + const { promise, resolve, reject } = context.defer(); + const { activeThread } = dbg._controller; + const globRegExp = args.glob ? globToRegExp(args.glob) : null; + + // Filter the sources down to those that we will need to black box. + + function shouldBlackBox(source) { + var value = globRegExp && globRegExp.test(source.url) + || args.source && source.actor == args.source; + return args.invert ? !value : value; + } + + const toBlackBox = [s.attachment.source + for (s of dbg._view.Sources.items) + if (shouldBlackBox(s.attachment.source))]; + + // If we aren't black boxing any sources, bail out now. + + if (toBlackBox.length === 0) { + const empty = createXHTMLElement(doc, "div"); + empty.textContent = lookup("EmptyDesc"); + return void resolve(empty); + } + + // Send the black box request to each source we are black boxing. As we + // get responses, accumulate the results in `blackBoxed`. + + const blackBoxed = []; + + for (let source of toBlackBox) { + activeThread.source(source)[cmd.clientMethod](function({ error }) { + if (error) { + blackBoxed.push(lookup("ErrorDesc") + " " + source.url); + } else { + blackBoxed.push(source.url); + } + + if (toBlackBox.length === blackBoxed.length) { + displayResults(); + } + }); + } + + // List the results for the user. + + function displayResults() { + const results = doc.createElement("div"); + results.textContent = lookup("NonEmptyDesc"); + + const list = createXHTMLElement(doc, "ul"); + results.appendChild(list); + + for (let result of blackBoxed) { + const item = createXHTMLElement(doc, "li"); + item.textContent = result; + list.appendChild(item); + } + resolve(results); + } + + return promise; + } + }); +}); + +/** + * A helper to create xhtml namespaced elements. + */ +function createXHTMLElement(document, tagname) { + return document.createElementNS("http://www.w3.org/1999/xhtml", tagname); +} + +/** + * A helper to go from a command context to a debugger panel. + */ +function getPanel(context, id, options = {}) { + if (!context) { + return undefined; + } + + let target = context.environment.target; + + if (options.ensureOpened) { + return gDevTools.showToolbox(target, id).then(toolbox => { + return toolbox.getPanel(id); + }); + } else { + let toolbox = gDevTools.getToolbox(target); + if (toolbox) { + return toolbox.getPanel(id); + } else { + return undefined; + } + } +} + +/** + * Converts a glob to a regular expression. + */ +function globToRegExp(glob) { + const reStr = glob + // Escape existing regular expression syntax. + .replace(/\\/g, "\\\\") + .replace(/\//g, "\\/") + .replace(/\^/g, "\\^") + .replace(/\$/g, "\\$") + .replace(/\+/g, "\\+") + .replace(/\?/g, "\\?") + .replace(/\./g, "\\.") + .replace(/\(/g, "\\(") + .replace(/\)/g, "\\)") + .replace(/\=/g, "\\=") + .replace(/\!/g, "\\!") + .replace(/\|/g, "\\|") + .replace(/\{/g, "\\{") + .replace(/\}/g, "\\}") + .replace(/\,/g, "\\,") + .replace(/\[/g, "\\[") + .replace(/\]/g, "\\]") + .replace(/\-/g, "\\-") + // Turn * into the match everything wildcard. + .replace(/\*/g, ".*") + return new RegExp("^" + reStr + "$"); +} diff --git a/toolkit/devtools/debugger/debugger-controller.js b/toolkit/devtools/debugger/debugger-controller.js new file mode 100644 index 000000000..531d7df1a --- /dev/null +++ b/toolkit/devtools/debugger/debugger-controller.js @@ -0,0 +1,2427 @@ +/* -*- 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 { classes: Cc, interfaces: Ci, utils: Cu } = Components; + +const DBG_STRINGS_URI = "chrome://browser/locale/devtools/debugger.properties"; +const NEW_SOURCE_IGNORED_URLS = ["debugger eval code", "XStringBundle"]; +const NEW_SOURCE_DISPLAY_DELAY = 200; // ms +const FETCH_SOURCE_RESPONSE_DELAY = 200; // ms +const FETCH_EVENT_LISTENERS_DELAY = 200; // ms +const FRAME_STEP_CLEAR_DELAY = 100; // ms +const CALL_STACK_PAGE_SIZE = 25; // frames + +// The panel's window global is an EventEmitter firing the following events: +const EVENTS = { + // When the debugger's source editor instance finishes loading or unloading. + EDITOR_LOADED: "Debugger:EditorLoaded", + EDITOR_UNLOADED: "Debugger:EditorUnoaded", + + // When new sources are received from the debugger server. + NEW_SOURCE: "Debugger:NewSource", + SOURCES_ADDED: "Debugger:SourcesAdded", + + // When a source is shown in the source editor. + SOURCE_SHOWN: "Debugger:EditorSourceShown", + SOURCE_ERROR_SHOWN: "Debugger:EditorSourceErrorShown", + + // When the editor has shown a source and set the line / column position + EDITOR_LOCATION_SET: "Debugger:EditorLocationSet", + + // When scopes, variables, properties and watch expressions are fetched and + // displayed in the variables view. + FETCHED_SCOPES: "Debugger:FetchedScopes", + FETCHED_VARIABLES: "Debugger:FetchedVariables", + FETCHED_PROPERTIES: "Debugger:FetchedProperties", + FETCHED_BUBBLE_PROPERTIES: "Debugger:FetchedBubbleProperties", + FETCHED_WATCH_EXPRESSIONS: "Debugger:FetchedWatchExpressions", + + // When a breakpoint has been added or removed on the debugger server. + BREAKPOINT_ADDED: "Debugger:BreakpointAdded", + BREAKPOINT_REMOVED: "Debugger:BreakpointRemoved", + + // When a breakpoint has been shown or hidden in the source editor + // or the pane. + BREAKPOINT_SHOWN_IN_EDITOR: "Debugger:BreakpointShownInEditor", + BREAKPOINT_SHOWN_IN_PANE: "Debugger:BreakpointShownInPane", + BREAKPOINT_HIDDEN_IN_EDITOR: "Debugger:BreakpointHiddenInEditor", + BREAKPOINT_HIDDEN_IN_PANE: "Debugger:BreakpointHiddenInPane", + + // When a conditional breakpoint's popup is showing or hiding. + CONDITIONAL_BREAKPOINT_POPUP_SHOWING: "Debugger:ConditionalBreakpointPopupShowing", + CONDITIONAL_BREAKPOINT_POPUP_HIDING: "Debugger:ConditionalBreakpointPopupHiding", + + // When event listeners are fetched or event breakpoints are updated. + EVENT_LISTENERS_FETCHED: "Debugger:EventListenersFetched", + EVENT_BREAKPOINTS_UPDATED: "Debugger:EventBreakpointsUpdated", + + // When a file search was performed. + FILE_SEARCH_MATCH_FOUND: "Debugger:FileSearch:MatchFound", + FILE_SEARCH_MATCH_NOT_FOUND: "Debugger:FileSearch:MatchNotFound", + + // When a function search was performed. + FUNCTION_SEARCH_MATCH_FOUND: "Debugger:FunctionSearch:MatchFound", + FUNCTION_SEARCH_MATCH_NOT_FOUND: "Debugger:FunctionSearch:MatchNotFound", + + // When a global text search was performed. + GLOBAL_SEARCH_MATCH_FOUND: "Debugger:GlobalSearch:MatchFound", + GLOBAL_SEARCH_MATCH_NOT_FOUND: "Debugger:GlobalSearch:MatchNotFound", + + // After the the StackFrames object has been filled with frames + AFTER_FRAMES_REFILLED: "Debugger:AfterFramesRefilled", + + // After the stackframes are cleared and debugger won't pause anymore. + AFTER_FRAMES_CLEARED: "Debugger:AfterFramesCleared", + + // When the options popup is showing or hiding. + OPTIONS_POPUP_SHOWING: "Debugger:OptionsPopupShowing", + OPTIONS_POPUP_HIDDEN: "Debugger:OptionsPopupHidden", + + // When the widgets layout has been changed. + LAYOUT_CHANGED: "Debugger:LayoutChanged" +}; + +// Descriptions for what a stack frame represents after the debugger pauses. +const FRAME_TYPE = { + NORMAL: 0, + CONDITIONAL_BREAKPOINT_EVAL: 1, + WATCH_EXPRESSIONS_EVAL: 2, + PUBLIC_CLIENT_EVAL: 3 +}; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/devtools/event-emitter.js"); +Cu.import("resource:///modules/devtools/SimpleListWidget.jsm"); +Cu.import("resource:///modules/devtools/BreadcrumbsWidget.jsm"); +Cu.import("resource:///modules/devtools/SideMenuWidget.jsm"); +Cu.import("resource:///modules/devtools/VariablesView.jsm"); +Cu.import("resource:///modules/devtools/VariablesViewController.jsm"); +Cu.import("resource:///modules/devtools/ViewHelpers.jsm"); + +const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require; +const promise = require("devtools/toolkit/deprecated-sync-thenables"); +const Editor = require("devtools/sourceeditor/editor"); +const DebuggerEditor = require("devtools/sourceeditor/debugger.js"); +const {Tooltip} = require("devtools/shared/widgets/Tooltip"); +const FastListWidget = require("devtools/shared/widgets/FastListWidget"); + +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "Parser", + "resource:///modules/devtools/Parser.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "devtools", + "resource://gre/modules/devtools/Loader.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "DevToolsUtils", + "resource://gre/modules/devtools/DevToolsUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "ShortcutUtils", + "resource://gre/modules/ShortcutUtils.jsm"); + +Object.defineProperty(this, "NetworkHelper", { + get: function() { + return devtools.require("devtools/toolkit/webconsole/network-helper"); + }, + configurable: true, + enumerable: true +}); + +/** + * Object defining the debugger controller components. + */ +let DebuggerController = { + /** + * Initializes the debugger controller. + */ + initialize: function() { + dumpn("Initializing the DebuggerController"); + + this.startupDebugger = this.startupDebugger.bind(this); + this.shutdownDebugger = this.shutdownDebugger.bind(this); + this._onTabNavigated = this._onTabNavigated.bind(this); + this._onTabDetached = this._onTabDetached.bind(this); + }, + + /** + * Initializes the view. + * + * @return object + * A promise that is resolved when the debugger finishes startup. + */ + startupDebugger: Task.async(function*() { + if (this._startup) { + return; + } + + yield DebuggerView.initialize(); + this._startup = true; + }), + + /** + * Destroys the view and disconnects the debugger client from the server. + * + * @return object + * A promise that is resolved when the debugger finishes shutdown. + */ + shutdownDebugger: Task.async(function*() { + if (this._shutdown) { + return; + } + + yield DebuggerView.destroy(); + this.SourceScripts.disconnect(); + this.StackFrames.disconnect(); + this.ThreadState.disconnect(); + this.Tracer.disconnect(); + this.disconnect(); + + this._shutdown = true; + }), + + /** + * Initiates remote debugging based on the current target, wiring event + * handlers as necessary. + * + * @return object + * A promise that is resolved when the debugger finishes connecting. + */ + connect: Task.async(function*() { + if (this._connected) { + return; + } + + let target = this._target; + let { client, form: { chromeDebugger, traceActor, actor } } = target; + target.on("close", this._onTabDetached); + target.on("navigate", this._onTabNavigated); + target.on("will-navigate", this._onTabNavigated); + this.client = client; + + if (target.isAddon) { + yield this._startAddonDebugging(actor); + } else if (target.chrome) { + yield this._startChromeDebugging(chromeDebugger); + } else { + yield this._startDebuggingTab(); + + if (Prefs.tracerEnabled && traceActor) { + yield this._startTracingTab(traceActor); + } + } + + this._hideUnsupportedFeatures(); + }), + + /** + * Disconnects the debugger client and removes event handlers as necessary. + */ + disconnect: function() { + // Return early if the client didn't even have a chance to instantiate. + if (!this.client) { + return; + } + + this._connected = false; + this.client = null; + this.activeThread = null; + }, + + _hideUnsupportedFeatures: function() { + if (this.client.mainRoot.traits.noPrettyPrinting) { + DebuggerView.Sources.hidePrettyPrinting(); + } + + if (this.client.mainRoot.traits.noBlackBoxing) { + DebuggerView.Sources.hideBlackBoxing(); + } + }, + + /** + * Called for each location change in the debugged tab. + * + * @param string aType + * Packet type. + * @param object aPacket + * Packet received from the server. + */ + _onTabNavigated: function(aType, aPacket) { + switch (aType) { + case "will-navigate": { + // Reset UI. + DebuggerView.handleTabNavigation(); + + // Discard all the cached sources *before* the target starts navigating. + // Sources may be fetched during navigation, in which case we don't + // want to hang on to the old source contents. + DebuggerController.SourceScripts.clearCache(); + DebuggerController.Parser.clearCache(); + SourceUtils.clearCache(); + + // Prevent performing any actions that were scheduled before navigation. + clearNamedTimeout("new-source"); + clearNamedTimeout("event-breakpoints-update"); + clearNamedTimeout("event-listeners-fetch"); + break; + } + case "navigate": { + this.ThreadState.handleTabNavigation(); + this.StackFrames.handleTabNavigation(); + this.SourceScripts.handleTabNavigation(); + break; + } + } + }, + + /** + * Called when the debugged tab is closed. + */ + _onTabDetached: function() { + this.shutdownDebugger(); + }, + + /** + * Warn if resuming execution produced a wrongOrder error. + */ + _ensureResumptionOrder: function(aResponse) { + if (aResponse.error == "wrongOrder") { + DebuggerView.Toolbar.showResumeWarning(aResponse.lastPausedUrl); + } + }, + + /** + * Sets up a debugging session. + * + * @return object + * A promise resolved once the client attaches to the active thread. + */ + _startDebuggingTab: function() { + let deferred = promise.defer(); + let threadOptions = { + useSourceMaps: Prefs.sourceMapsEnabled, + autoBlackBox: Prefs.autoBlackBox + }; + + this._target.activeTab.attachThread(threadOptions, (aResponse, aThreadClient) => { + if (!aThreadClient) { + deferred.reject(new Error("Couldn't attach to thread: " + aResponse.error)); + return; + } + this.activeThread = aThreadClient; + this.ThreadState.connect(); + this.StackFrames.connect(); + this.SourceScripts.connect(); + + if (aThreadClient.paused) { + aThreadClient.resume(this._ensureResumptionOrder); + } + + deferred.resolve(); + }); + + return deferred.promise; + }, + + /** + * Sets up an addon debugging session. + * + * @param object aAddonActor + * The actor for the addon that is being debugged. + * @return object + * A promise resolved once the client attaches to the active thread. + */ + _startAddonDebugging: function(aAddonActor) { + let deferred = promise.defer(); + + this.client.attachAddon(aAddonActor, aResponse => { + this._startChromeDebugging(aResponse.threadActor).then(deferred.resolve); + }); + + return deferred.promise; + }, + + /** + * Sets up a chrome debugging session. + * + * @param object aChromeDebugger + * The remote protocol grip of the chrome debugger. + * @return object + * A promise resolved once the client attaches to the active thread. + */ + _startChromeDebugging: function(aChromeDebugger) { + let deferred = promise.defer(); + let threadOptions = { + useSourceMaps: Prefs.sourceMapsEnabled, + autoBlackBox: Prefs.autoBlackBox + }; + + this.client.attachThread(aChromeDebugger, (aResponse, aThreadClient) => { + if (!aThreadClient) { + deferred.reject(new Error("Couldn't attach to thread: " + aResponse.error)); + return; + } + this.activeThread = aThreadClient; + this.ThreadState.connect(); + this.StackFrames.connect(); + this.SourceScripts.connect(); + + if (aThreadClient.paused) { + aThreadClient.resume(this._ensureResumptionOrder); + } + + deferred.resolve(); + }, threadOptions); + + return deferred.promise; + }, + + /** + * Sets up an execution tracing session. + * + * @param object aTraceActor + * The remote protocol grip of the trace actor. + * @return object + * A promise resolved once the client attaches to the tracer. + */ + _startTracingTab: function(aTraceActor) { + let deferred = promise.defer(); + + this.client.attachTracer(aTraceActor, (response, traceClient) => { + if (!traceClient) { + deferred.reject(new Error("Failed to attach to tracing actor.")); + return; + } + this.traceClient = traceClient; + this.Tracer.connect(); + + deferred.resolve(); + }); + + return deferred.promise; + }, + + /** + * Detach and reattach to the thread actor with useSourceMaps true, blow + * away old sources and get them again. + */ + reconfigureThread: function({ useSourceMaps, autoBlackBox }) { + this.activeThread.reconfigure({ + useSourceMaps: useSourceMaps, + autoBlackBox: autoBlackBox + }, aResponse => { + if (aResponse.error) { + let msg = "Couldn't reconfigure thread: " + aResponse.message; + Cu.reportError(msg); + dumpn(msg); + return; + } + + // Reset the view and fetch all the sources again. + DebuggerView.handleTabNavigation(); + this.SourceScripts.handleTabNavigation(); + + // Update the stack frame list. + if (this.activeThread.paused) { + this.activeThread._clearFrames(); + this.activeThread.fillFrames(CALL_STACK_PAGE_SIZE); + } + }); + }, + + _startup: false, + _shutdown: false, + _connected: false, + client: null, + activeThread: null +}; + +/** + * ThreadState keeps the UI up to date with the state of the + * thread (paused/attached/etc.). + */ +function ThreadState() { + this._update = this._update.bind(this); + this.interruptedByResumeButton = false; +} + +ThreadState.prototype = { + get activeThread() DebuggerController.activeThread, + + /** + * Connect to the current thread client. + */ + connect: function() { + dumpn("ThreadState is connecting..."); + this.activeThread.addListener("paused", this._update); + this.activeThread.addListener("resumed", this._update); + this.activeThread.pauseOnExceptions(Prefs.pauseOnExceptions, + Prefs.ignoreCaughtExceptions); + }, + + /** + * Disconnect from the client. + */ + disconnect: function() { + if (!this.activeThread) { + return; + } + dumpn("ThreadState is disconnecting..."); + this.activeThread.removeListener("paused", this._update); + this.activeThread.removeListener("resumed", this._update); + }, + + /** + * Handles any initialization on a tab navigation event issued by the client. + */ + handleTabNavigation: function() { + if (!this.activeThread) { + return; + } + dumpn("Handling tab navigation in the ThreadState"); + this._update(); + }, + + /** + * Update the UI after a thread state change. + */ + _update: function(aEvent, aPacket) { + // Ignore "interrupted" events, to avoid UI flicker. These are generated + // by the slow script dialog and internal events such as setting + // breakpoints. Pressing the resume button does need to be shown, though. + if (aEvent == "paused" && + aPacket.why.type == "interrupted" && + !this.interruptedByResumeButton) { + return; + } + + this.interruptedByResumeButton = false; + DebuggerView.Toolbar.toggleResumeButtonState(this.activeThread.state); + + if (gTarget && (aEvent == "paused" || aEvent == "resumed")) { + gTarget.emit("thread-" + aEvent); + } + } +}; + +/** + * Keeps the stack frame list up-to-date, using the thread client's + * stack frame cache. + */ +function StackFrames() { + this._onPaused = this._onPaused.bind(this); + this._onResumed = this._onResumed.bind(this); + this._onFrames = this._onFrames.bind(this); + this._onFramesCleared = this._onFramesCleared.bind(this); + this._onBlackBoxChange = this._onBlackBoxChange.bind(this); + this._onPrettyPrintChange = this._onPrettyPrintChange.bind(this); + this._afterFramesCleared = this._afterFramesCleared.bind(this); + this.evaluate = this.evaluate.bind(this); +} + +StackFrames.prototype = { + get activeThread() DebuggerController.activeThread, + currentFrameDepth: -1, + _currentFrameDescription: FRAME_TYPE.NORMAL, + _syncedWatchExpressions: null, + _currentWatchExpressions: null, + _currentBreakpointLocation: null, + _currentEvaluation: null, + _currentException: null, + _currentReturnedValue: null, + + /** + * Connect to the current thread client. + */ + connect: function() { + dumpn("StackFrames is connecting..."); + this.activeThread.addListener("paused", this._onPaused); + this.activeThread.addListener("resumed", this._onResumed); + this.activeThread.addListener("framesadded", this._onFrames); + this.activeThread.addListener("framescleared", this._onFramesCleared); + this.activeThread.addListener("blackboxchange", this._onBlackBoxChange); + this.activeThread.addListener("prettyprintchange", this._onPrettyPrintChange); + this.handleTabNavigation(); + }, + + /** + * Disconnect from the client. + */ + disconnect: function() { + if (!this.activeThread) { + return; + } + dumpn("StackFrames is disconnecting..."); + this.activeThread.removeListener("paused", this._onPaused); + this.activeThread.removeListener("resumed", this._onResumed); + this.activeThread.removeListener("framesadded", this._onFrames); + this.activeThread.removeListener("framescleared", this._onFramesCleared); + this.activeThread.removeListener("blackboxchange", this._onBlackBoxChange); + this.activeThread.removeListener("prettyprintchange", this._onPrettyPrintChange); + clearNamedTimeout("frames-cleared"); + }, + + /** + * Handles any initialization on a tab navigation event issued by the client. + */ + handleTabNavigation: function() { + dumpn("Handling tab navigation in the StackFrames"); + // Nothing to do here yet. + }, + + /** + * Handler for the thread client's paused notification. + * + * @param string aEvent + * The name of the notification ("paused" in this case). + * @param object aPacket + * The response packet. + */ + _onPaused: function(aEvent, aPacket) { + switch (aPacket.why.type) { + // If paused by a breakpoint, store the breakpoint location. + case "breakpoint": + this._currentBreakpointLocation = aPacket.frame.where; + break; + // If paused by a client evaluation, store the evaluated value. + case "clientEvaluated": + this._currentEvaluation = aPacket.why.frameFinished; + break; + // If paused by an exception, store the exception value. + case "exception": + this._currentException = aPacket.why.exception; + break; + // If paused while stepping out of a frame, store the returned value or + // thrown exception. + case "resumeLimit": + if (!aPacket.why.frameFinished) { + break; + } else if (aPacket.why.frameFinished.throw) { + this._currentException = aPacket.why.frameFinished.throw; + } else if (aPacket.why.frameFinished.return) { + this._currentReturnedValue = aPacket.why.frameFinished.return; + } + break; + // If paused by an explicit interrupt, which are generated by the slow + // script dialog and internal events such as setting breakpoints, ignore + // the event to avoid UI flicker. + case "interrupted": + return; + } + + this.activeThread.fillFrames(CALL_STACK_PAGE_SIZE); + DebuggerView.editor.focus(); + }, + + /** + * Handler for the thread client's resumed notification. + */ + _onResumed: function() { + // Prepare the watch expression evaluation string for the next pause. + if (this._currentFrameDescription != FRAME_TYPE.WATCH_EXPRESSIONS_EVAL) { + this._currentWatchExpressions = this._syncedWatchExpressions; + } + }, + + /** + * Handler for the thread client's framesadded notification. + */ + _onFrames: Task.async(function*() { + // Ignore useless notifications. + if (!this.activeThread || !this.activeThread.cachedFrames.length) { + return; + } + if (this._currentFrameDescription != FRAME_TYPE.NORMAL && + this._currentFrameDescription != FRAME_TYPE.PUBLIC_CLIENT_EVAL) { + return; + } + + // TODO: remove all of this deprecated code: Bug 990137. + yield this._handleConditionalBreakpoint(); + + // TODO: handle all of this server-side: Bug 832470, comment 14. + yield this._handleWatchExpressions(); + + // Make sure the debugger view panes are visible, then refill the frames. + DebuggerView.showInstrumentsPane(); + this._refillFrames(); + + // No additional processing is necessary for this stack frame. + if (this._currentFrameDescription != FRAME_TYPE.NORMAL) { + this._currentFrameDescription = FRAME_TYPE.NORMAL; + } + }), + + /** + * Fill the StackFrames view with the frames we have in the cache, compressing + * frames which have black boxed sources into single frames. + */ + _refillFrames: function() { + // Make sure all the previous stackframes are removed before re-adding them. + DebuggerView.StackFrames.empty(); + + for (let frame of this.activeThread.cachedFrames) { + let { depth, source, where: { line } } = frame; + + let isBlackBoxed = source ? this.activeThread.source(source).isBlackBoxed : false; + let location = NetworkHelper.convertToUnicode(unescape(source.url || source.introductionUrl)); + let title = StackFrameUtils.getFrameTitle(frame); + DebuggerView.StackFrames.addFrame(title, location, line, depth, isBlackBoxed); + } + + DebuggerView.StackFrames.selectedDepth = Math.max(this.currentFrameDepth, 0); + DebuggerView.StackFrames.dirty = this.activeThread.moreFrames; + + window.emit(EVENTS.AFTER_FRAMES_REFILLED); + }, + + /** + * Handler for the thread client's framescleared notification. + */ + _onFramesCleared: function() { + switch (this._currentFrameDescription) { + case FRAME_TYPE.NORMAL: + this._currentEvaluation = null; + this._currentException = null; + this._currentReturnedValue = null; + break; + case FRAME_TYPE.CONDITIONAL_BREAKPOINT_EVAL: + this._currentBreakpointLocation = null; + break; + case FRAME_TYPE.WATCH_EXPRESSIONS_EVAL: + this._currentWatchExpressions = null; + break; + } + + // After each frame step (in, over, out), framescleared is fired, which + // forces the UI to be emptied and rebuilt on framesadded. Most of the times + // this is not necessary, and will result in a brief redraw flicker. + // To avoid it, invalidate the UI only after a short time if necessary. + setNamedTimeout("frames-cleared", FRAME_STEP_CLEAR_DELAY, this._afterFramesCleared); + }, + + /** + * Handler for the debugger's blackboxchange notification. + */ + _onBlackBoxChange: function() { + if (this.activeThread.state == "paused") { + // Hack to avoid selecting the topmost frame after blackboxing a source. + this.currentFrameDepth = NaN; + this._refillFrames(); + } + }, + + /** + * Handler for the debugger's prettyprintchange notification. + */ + _onPrettyPrintChange: function() { + if (this.activeThread.state != "paused") { + return; + } + // Makes sure the selected source remains selected + // after the fillFrames is called. + const source = DebuggerView.Sources.selectedValue; + + this.activeThread.fillFrames(CALL_STACK_PAGE_SIZE, () => { + DebuggerView.Sources.selectedValue = source; + }); + }, + + /** + * Called soon after the thread client's framescleared notification. + */ + _afterFramesCleared: function() { + // Ignore useless notifications. + if (this.activeThread.cachedFrames.length) { + return; + } + DebuggerView.editor.clearDebugLocation(); + DebuggerView.StackFrames.empty(); + DebuggerView.Sources.unhighlightBreakpoint(); + DebuggerView.WatchExpressions.toggleContents(true); + DebuggerView.Variables.empty(0); + + window.emit(EVENTS.AFTER_FRAMES_CLEARED); + }, + + /** + * Marks the stack frame at the specified depth as selected and updates the + * properties view with the stack frame's data. + * + * @param number aDepth + * The depth of the frame in the stack. + */ + selectFrame: function(aDepth) { + // Make sure the frame at the specified depth exists first. + let frame = this.activeThread.cachedFrames[this.currentFrameDepth = aDepth]; + if (!frame) { + return; + } + + // Check if the frame does not represent the evaluation of debuggee code. + let { environment, where, source } = frame; + if (!environment) { + return; + } + + // Don't change the editor's location if the execution was paused by a + // public client evaluation. This is useful for adding overlays on + // top of the editor, like a variable inspection popup. + let isClientEval = this._currentFrameDescription == FRAME_TYPE.PUBLIC_CLIENT_EVAL; + let isPopupShown = DebuggerView.VariableBubble.contentsShown(); + if (!isClientEval && !isPopupShown) { + // Move the editor's caret to the proper url and line. + DebuggerView.setEditorLocation(source.actor, where.line); + } else { + // Highlight the line where the execution is paused in the editor. + DebuggerView.setEditorLocation(source.actor, where.line, { noCaret: true }); + } + + // Highlight the breakpoint at the line and column if it exists. + DebuggerView.Sources.highlightBreakpointAtCursor(); + + // Don't display the watch expressions textbox inputs in the pane. + DebuggerView.WatchExpressions.toggleContents(false); + + // Start recording any added variables or properties in any scope and + // clear existing scopes to create each one dynamically. + DebuggerView.Variables.empty(); + + // If watch expressions evaluation results are available, create a scope + // to contain all the values. + if (this._syncedWatchExpressions && aDepth == 0) { + let label = L10N.getStr("watchExpressionsScopeLabel"); + let scope = DebuggerView.Variables.addScope(label); + + // Customize the scope for holding watch expressions evaluations. + scope.descriptorTooltip = false; + scope.contextMenuId = "debuggerWatchExpressionsContextMenu"; + scope.separatorStr = L10N.getStr("watchExpressionsSeparatorLabel"); + scope.switch = DebuggerView.WatchExpressions.switchExpression; + scope.delete = DebuggerView.WatchExpressions.deleteExpression; + + // The evaluation hasn't thrown, so fetch and add the returned results. + this._fetchWatchExpressions(scope, this._currentEvaluation.return); + + // The watch expressions scope is always automatically expanded. + scope.expand(); + } + + do { + // Create a scope to contain all the inspected variables in the + // current environment. + let label = StackFrameUtils.getScopeLabel(environment); + let scope = DebuggerView.Variables.addScope(label); + let innermost = environment == frame.environment; + + // Handle special additions to the innermost scope. + if (innermost) { + this._insertScopeFrameReferences(scope, frame); + } + + // Handle the expansion of the scope, lazily populating it with the + // variables in the current environment. + DebuggerView.Variables.controller.addExpander(scope, environment); + + // The innermost scope is always automatically expanded, because it + // contains the variables in the current stack frame which are likely to + // be inspected. The previously expanded scopes are also reexpanded here. + if (innermost || DebuggerView.Variables.wasExpanded(scope)) { + scope.expand(); + } + } while ((environment = environment.parent)); + + // Signal that scope environments have been shown. + window.emit(EVENTS.FETCHED_SCOPES); + }, + + /** + * Loads more stack frames from the debugger server cache. + */ + addMoreFrames: function() { + this.activeThread.fillFrames( + this.activeThread.cachedFrames.length + CALL_STACK_PAGE_SIZE); + }, + + /** + * Evaluate an expression in the context of the selected frame. + * + * @param string aExpression + * The expression to evaluate. + * @param object aOptions [optional] + * Additional options for this client evaluation: + * - depth: the frame depth used for evaluation, 0 being the topmost. + * - meta: some meta-description for what this evaluation represents. + * @return object + * A promise that is resolved when the evaluation finishes, + * or rejected if there was no stack frame available or some + * other error occurred. + */ + evaluate: function(aExpression, aOptions = {}) { + let depth = "depth" in aOptions ? aOptions.depth : this.currentFrameDepth; + let frame = this.activeThread.cachedFrames[depth]; + if (frame == null) { + return promise.reject(new Error("No stack frame available.")); + } + + let deferred = promise.defer(); + + this.activeThread.addOneTimeListener("paused", (aEvent, aPacket) => { + let { type, frameFinished } = aPacket.why; + if (type == "clientEvaluated") { + deferred.resolve(frameFinished); + } else { + deferred.reject(new Error("Active thread paused unexpectedly.")); + } + }); + + let meta = "meta" in aOptions ? aOptions.meta : FRAME_TYPE.PUBLIC_CLIENT_EVAL; + this._currentFrameDescription = meta; + this.activeThread.eval(frame.actor, aExpression); + + return deferred.promise; + }, + + /** + * Add nodes for special frame references in the innermost scope. + * + * @param Scope aScope + * The scope where the references will be placed into. + * @param object aFrame + * The frame to get some references from. + */ + _insertScopeFrameReferences: function(aScope, aFrame) { + // Add any thrown exception. + if (this._currentException) { + let excRef = aScope.addItem("<exception>", { value: this._currentException }); + DebuggerView.Variables.controller.addExpander(excRef, this._currentException); + } + // Add any returned value. + if (this._currentReturnedValue) { + let retRef = aScope.addItem("<return>", { value: this._currentReturnedValue }); + DebuggerView.Variables.controller.addExpander(retRef, this._currentReturnedValue); + } + // Add "this". + if (aFrame.this) { + let thisRef = aScope.addItem("this", { value: aFrame.this }); + DebuggerView.Variables.controller.addExpander(thisRef, aFrame.this); + } + }, + + /** + * Handles conditional breakpoints when the debugger pauses and the + * stackframes are received. + * + * We moved conditional breakpoint handling to the server, but + * need to support it in the client for a while until most of the + * server code in production is updated with it. + * TODO: remove all of this deprecated code: Bug 990137. + * + * @return object + * A promise that is resolved after a potential breakpoint's + * conditional expression is evaluated. If there's no breakpoint + * where the debugger is paused, the promise is resolved immediately. + */ + _handleConditionalBreakpoint: Task.async(function*() { + if (gClient.mainRoot.traits.conditionalBreakpoints) { + return; + } + let breakLocation = this._currentBreakpointLocation; + if (!breakLocation) { + return; + } + + let breakpointPromise = DebuggerController.Breakpoints._getAdded(breakLocation); + if (!breakpointPromise) { + return; + } + let breakpointClient = yield breakpointPromise; + let conditionalExpression = breakpointClient.conditionalExpression; + if (!conditionalExpression) { + return; + } + + // Evaluating the current breakpoint's conditional expression will + // cause the stack frames to be cleared and active thread to pause, + // sending a 'clientEvaluated' packed and adding the frames again. + let evaluationOptions = { depth: 0, meta: FRAME_TYPE.CONDITIONAL_BREAKPOINT_EVAL }; + yield this.evaluate(conditionalExpression, evaluationOptions); + this._currentFrameDescription = FRAME_TYPE.NORMAL; + + // If the breakpoint's conditional expression evaluation is falsy, + // automatically resume execution. + if (VariablesView.isFalsy({ value: this._currentEvaluation.return })) { + this.activeThread.resume(DebuggerController._ensureResumptionOrder); + } + }), + + /** + * Handles watch expressions when the debugger pauses and the stackframes + * are received. + * + * @return object + * A promise that is resolved after the potential watch expressions + * are evaluated. If there are no watch expressions where the debugger + * is paused, the promise is resolved immediately. + */ + _handleWatchExpressions: Task.async(function*() { + // Ignore useless notifications. + if (!this.activeThread || !this.activeThread.cachedFrames.length) { + return; + } + + let watchExpressions = this._currentWatchExpressions; + if (!watchExpressions) { + return; + } + + // Evaluation causes the stack frames to be cleared and active thread to + // pause, sending a 'clientEvaluated' packet and adding the frames again. + let evaluationOptions = { depth: 0, meta: FRAME_TYPE.WATCH_EXPRESSIONS_EVAL }; + yield this.evaluate(watchExpressions, evaluationOptions); + this._currentFrameDescription = FRAME_TYPE.NORMAL; + + // If an error was thrown during the evaluation of the watch expressions + // or the evaluation was terminated from the slow script dialog, then at + // least one expression evaluation could not be performed. So remove the + // most recent watch expression and try again. + if (this._currentEvaluation.throw || this._currentEvaluation.terminated) { + DebuggerView.WatchExpressions.removeAt(0); + yield DebuggerController.StackFrames.syncWatchExpressions(); + } + }), + + /** + * Adds the watch expressions evaluation results to a scope in the view. + * + * @param Scope aScope + * The scope where the watch expressions will be placed into. + * @param object aExp + * The grip of the evaluation results. + */ + _fetchWatchExpressions: function(aScope, aExp) { + // Fetch the expressions only once. + if (aScope._fetched) { + return; + } + aScope._fetched = true; + + // Add nodes for every watch expression in scope. + this.activeThread.pauseGrip(aExp).getPrototypeAndProperties(aResponse => { + let ownProperties = aResponse.ownProperties; + let totalExpressions = DebuggerView.WatchExpressions.itemCount; + + for (let i = 0; i < totalExpressions; i++) { + let name = DebuggerView.WatchExpressions.getString(i); + let expVal = ownProperties[i].value; + let expRef = aScope.addItem(name, ownProperties[i]); + DebuggerView.Variables.controller.addExpander(expRef, expVal); + + // Revert some of the custom watch expressions scope presentation flags, + // so that they don't propagate to child items. + expRef.switch = null; + expRef.delete = null; + expRef.descriptorTooltip = true; + expRef.separatorStr = L10N.getStr("variablesSeparatorLabel"); + } + + // Signal that watch expressions have been fetched. + window.emit(EVENTS.FETCHED_WATCH_EXPRESSIONS); + }); + }, + + /** + * Updates a list of watch expressions to evaluate on each pause. + * TODO: handle all of this server-side: Bug 832470, comment 14. + */ + syncWatchExpressions: function() { + let list = DebuggerView.WatchExpressions.getAllStrings(); + + // Sanity check all watch expressions before syncing them. To avoid + // having the whole watch expressions array throw because of a single + // faulty expression, simply convert it to a string describing the error. + // There's no other information necessary to be offered in such cases. + let sanitizedExpressions = list.map(aString => { + // Reflect.parse throws when it encounters a syntax error. + try { + Parser.reflectionAPI.parse(aString); + return aString; // Watch expression can be executed safely. + } catch (e) { + return "\"" + e.name + ": " + e.message + "\""; // Syntax error. + } + }); + + if (!sanitizedExpressions.length) { + this._currentWatchExpressions = null; + this._syncedWatchExpressions = null; + } else { + this._syncedWatchExpressions = + this._currentWatchExpressions = "[" + + sanitizedExpressions.map(aString => + "eval(\"" + + "try {" + + // Make sure all quotes are escaped in the expression's syntax, + // and add a newline after the statement to avoid comments + // breaking the code integrity inside the eval block. + aString.replace(/"/g, "\\$&") + "\" + " + "'\\n'" + " + \"" + + "} catch (e) {" + + "e.name + ': ' + e.message;" + // TODO: Bug 812765, 812764. + "}" + + "\")" + ).join(",") + + "]"; + } + + this.currentFrameDepth = -1; + return this._onFrames(); + } +}; + +/** + * Keeps the source script list up-to-date, using the thread client's + * source script cache. + */ +function SourceScripts() { + this._onNewGlobal = this._onNewGlobal.bind(this); + this._onNewSource = this._onNewSource.bind(this); + this._onSourcesAdded = this._onSourcesAdded.bind(this); + this._onBlackBoxChange = this._onBlackBoxChange.bind(this); + this._onPrettyPrintChange = this._onPrettyPrintChange.bind(this); +} + +SourceScripts.prototype = { + get activeThread() DebuggerController.activeThread, + get debuggerClient() DebuggerController.client, + _cache: new Map(), + + /** + * Connect to the current thread client. + */ + connect: function() { + dumpn("SourceScripts is connecting..."); + this.debuggerClient.addListener("newGlobal", this._onNewGlobal); + this.debuggerClient.addListener("newSource", this._onNewSource); + this.activeThread.addListener("blackboxchange", this._onBlackBoxChange); + this.activeThread.addListener("prettyprintchange", this._onPrettyPrintChange); + this.handleTabNavigation(); + }, + + /** + * Disconnect from the client. + */ + disconnect: function() { + if (!this.activeThread) { + return; + } + dumpn("SourceScripts is disconnecting..."); + this.debuggerClient.removeListener("newGlobal", this._onNewGlobal); + this.debuggerClient.removeListener("newSource", this._onNewSource); + this.activeThread.removeListener("blackboxchange", this._onBlackBoxChange); + this.activeThread.addListener("prettyprintchange", this._onPrettyPrintChange); + }, + + /** + * Clears all the cached source contents. + */ + clearCache: function() { + this._cache.clear(); + }, + + /** + * Handles any initialization on a tab navigation event issued by the client. + */ + handleTabNavigation: function() { + if (!this.activeThread) { + return; + } + dumpn("Handling tab navigation in the SourceScripts"); + + // Retrieve the list of script sources known to the server from before + // the client was ready to handle "newSource" notifications. + this.activeThread.getSources(this._onSourcesAdded); + }, + + /** + * Handler for the debugger client's unsolicited newGlobal notification. + */ + _onNewGlobal: function(aNotification, aPacket) { + // TODO: bug 806775, update the globals list using aPacket.hostAnnotations + // from bug 801084. + }, + + /** + * Handler for the debugger client's unsolicited newSource notification. + */ + _onNewSource: function(aNotification, aPacket) { + // Ignore bogus scripts, e.g. generated from 'clientEvaluate' packets. + if (NEW_SOURCE_IGNORED_URLS.indexOf(aPacket.source.url) != -1) { + return; + } + + // Add the source in the debugger view sources container. + DebuggerView.Sources.addSource(aPacket.source, { staged: false }); + + // Select this source if it's the preferred one. + let preferredValue = DebuggerView.Sources.preferredValue; + if (aPacket.source.url == preferredValue) { + DebuggerView.Sources.selectedValue = preferredValue; + } + // ..or the first entry if there's none selected yet after a while + else { + setNamedTimeout("new-source", NEW_SOURCE_DISPLAY_DELAY, () => { + // If after a certain delay the preferred source still wasn't received, + // just give up on waiting and display the first entry. + if (!DebuggerView.Sources.selectedValue) { + DebuggerView.Sources.selectedIndex = 0; + } + }); + } + + // If there are any stored breakpoints for this source, display them again, + // both in the editor and the breakpoints pane. + DebuggerController.Breakpoints.updatePaneBreakpoints(); + DebuggerController.Breakpoints.updateEditorBreakpoints(); + DebuggerController.HitCounts.updateEditorHitCounts(); + + // Make sure the events listeners are up to date. + if (DebuggerView.instrumentsPaneTab == "events-tab") { + DebuggerController.Breakpoints.DOM.scheduleEventListenersFetch(); + } + + // Signal that a new source has been added. + window.emit(EVENTS.NEW_SOURCE); + }, + + /** + * Callback for the debugger's active thread getSources() method. + */ + _onSourcesAdded: function(aResponse) { + if (aResponse.error) { + let msg = "Error getting sources: " + aResponse.message; + Cu.reportError(msg); + dumpn(msg); + return; + } + + if (aResponse.sources.length === 0) { + DebuggerView.Sources.emptyText = L10N.getStr("noSourcesText"); + window.emit(EVENTS.SOURCES_ADDED); + return; + } + + // Add all the sources in the debugger view sources container. + for (let source of aResponse.sources) { + // Ignore bogus scripts, e.g. generated from 'clientEvaluate' packets. + if (NEW_SOURCE_IGNORED_URLS.indexOf(source.url) == -1) { + DebuggerView.Sources.addSource(source, { staged: true }); + } + } + + // Flushes all the prepared sources into the sources container. + DebuggerView.Sources.commit({ sorted: true }); + + // Select the preferred source if it exists and was part of the response. + let preferredValue = DebuggerView.Sources.preferredValue; + if (DebuggerView.Sources.containsValue(preferredValue)) { + DebuggerView.Sources.selectedValue = preferredValue; + } + // ..or the first entry if there's no one selected yet. + else if (!DebuggerView.Sources.selectedValue) { + DebuggerView.Sources.selectedIndex = 0; + } + + // If there are any stored breakpoints for the sources, display them again, + // both in the editor and the breakpoints pane. + DebuggerController.Breakpoints.updatePaneBreakpoints(); + DebuggerController.Breakpoints.updateEditorBreakpoints(); + DebuggerController.HitCounts.updateEditorHitCounts(); + + // Signal that sources have been added. + window.emit(EVENTS.SOURCES_ADDED); + }, + + /** + * Handler for the debugger client's 'blackboxchange' notification. + */ + _onBlackBoxChange: function (aEvent, { actor, isBlackBoxed }) { + const item = DebuggerView.Sources.getItemByValue(actor); + if (item) { + item.prebuiltNode.classList.toggle("black-boxed", isBlackBoxed); + } + DebuggerView.Sources.updateToolbarButtonsState(); + DebuggerView.maybeShowBlackBoxMessage(); + }, + + /** + * Set the black boxed status of the given source. + * + * @param Object aSource + * The source form. + * @param bool aBlackBoxFlag + * True to black box the source, false to un-black box it. + * @returns Promise + * A promize that resolves to [aSource, isBlackBoxed] or rejects to + * [aSource, error]. + */ + setBlackBoxing: function(aSource, aBlackBoxFlag) { + const sourceClient = this.activeThread.source(aSource); + const deferred = promise.defer(); + + sourceClient[aBlackBoxFlag ? "blackBox" : "unblackBox"](aPacket => { + const { error, message } = aPacket; + if (error) { + let msg = "Couldn't toggle black boxing for " + aSource.url + ": " + message; + dumpn(msg); + Cu.reportError(msg); + deferred.reject([aSource, msg]); + } else { + deferred.resolve([aSource, sourceClient.isBlackBoxed]); + } + }); + + return deferred.promise; + }, + + /** + * Toggle the pretty printing of a source's text. All subsequent calls to + * |getText| will return the pretty-toggled text. Nothing will happen for + * non-javascript files. + * + * @param Object aSource + * The source form from the RDP. + * @returns Promise + * A promise that resolves to [aSource, prettyText] or rejects to + * [aSource, error]. + */ + togglePrettyPrint: function(aSource) { + // Only attempt to pretty print JavaScript sources. + if (!SourceUtils.isJavaScript(aSource.url, aSource.contentType)) { + return promise.reject([aSource, "Can't prettify non-javascript files."]); + } + + const sourceClient = this.activeThread.source(aSource); + const wantPretty = !sourceClient.isPrettyPrinted; + + // Only use the existing promise if it is pretty printed. + let textPromise = this._cache.get(aSource.url); + if (textPromise && textPromise.pretty === wantPretty) { + return textPromise; + } + + const deferred = promise.defer(); + deferred.promise.pretty = wantPretty; + this._cache.set(aSource.actor, deferred.promise); + + const afterToggle = ({ error, message, source: text, contentType }) => { + if (error) { + // Revert the rejected promise from the cache, so that the original + // source's text may be shown when the source is selected. + this._cache.set(aSource.actor, textPromise); + deferred.reject([aSource, message || error]); + return; + } + deferred.resolve([aSource, text, contentType]); + }; + + if (wantPretty) { + sourceClient.prettyPrint(Prefs.editorTabSize, afterToggle); + } else { + sourceClient.disablePrettyPrint(afterToggle); + } + + return deferred.promise; + }, + + /** + * Handler for the debugger's prettyprintchange notification. + */ + _onPrettyPrintChange: function(aEvent, { url }) { + // Remove the cached source AST from the Parser, to avoid getting + // wrong locations when searching for functions. + DebuggerController.Parser.clearSource(url); + }, + + /** + * Gets a specified source's text. + * + * @param object aSource + * The source object coming from the active thread. + * @param function aOnTimeout [optional] + * Function called when the source text takes a long time to fetch, + * but not necessarily failing. Long fetch times don't cause the + * rejection of the returned promise. + * @param number aDelay [optional] + * The amount of time it takes to consider a source slow to fetch. + * If unspecified, it defaults to a predefined value. + * @return object + * A promise that is resolved after the source text has been fetched. + */ + getText: function(aSource, aOnTimeout, aDelay = FETCH_SOURCE_RESPONSE_DELAY) { + // Fetch the source text only once. + let textPromise = this._cache.get(aSource.actor); + if (textPromise) { + return textPromise; + } + + let deferred = promise.defer(); + this._cache.set(aSource.actor, deferred.promise); + + // If the source text takes a long time to fetch, invoke a callback. + if (aOnTimeout) { + var fetchTimeout = window.setTimeout(() => aOnTimeout(aSource), aDelay); + } + + // Get the source text from the active thread. + this.activeThread.source(aSource).source(({ error, source: text, contentType }) => { + if (aOnTimeout) { + window.clearTimeout(fetchTimeout); + } + if (error) { + deferred.reject([aSource, error]); + } else { + deferred.resolve([aSource, text, contentType]); + } + }); + + return deferred.promise; + }, + + /** + * Starts fetching all the sources, silently. + * + * @param array aUrls + * The urls for the sources to fetch. If fetching a source's text + * takes too long, it will be discarded. + * @return object + * A promise that is resolved after source texts have been fetched. + */ + getTextForSources: function(aActors) { + let deferred = promise.defer(); + let pending = new Set(aActors); + let fetched = []; + + // Can't use promise.all, because if one fetch operation is rejected, then + // everything is considered rejected, thus no other subsequent source will + // be getting fetched. We don't want that. Something like Q's allSettled + // would work like a charm here. + + // Try to fetch as many sources as possible. + for (let actor of aActors) { + let sourceItem = DebuggerView.Sources.getItemByValue(actor); + let sourceForm = sourceItem.attachment.source; + this.getText(sourceForm, onTimeout).then(onFetch, onError); + } + + /* Called if fetching a source takes too long. */ + function onTimeout(aSource) { + onError([aSource]); + } + + /* Called if fetching a source finishes successfully. */ + function onFetch([aSource, aText, aContentType]) { + // If fetching the source has previously timed out, discard it this time. + if (!pending.has(aSource.actor)) { + return; + } + pending.delete(aSource.actor); + fetched.push([aSource.actor, aText, aContentType]); + maybeFinish(); + } + + /* Called if fetching a source failed because of an error. */ + function onError([aSource, aError]) { + pending.delete(aSource.actor); + maybeFinish(); + } + + /* Called every time something interesting happens while fetching sources. */ + function maybeFinish() { + if (pending.size == 0) { + // Sort the fetched sources alphabetically by their url. + deferred.resolve(fetched.sort(([aFirst], [aSecond]) => aFirst > aSecond)); + } + } + + return deferred.promise; + } +}; + +/** + * Tracer update the UI according to the messages exchanged with the tracer + * actor. + */ +function Tracer() { + this._trace = null; + this._idCounter = 0; + this.onTraces = this.onTraces.bind(this); +} + +Tracer.prototype = { + get client() { + return DebuggerController.client; + }, + + get traceClient() { + return DebuggerController.traceClient; + }, + + get tracing() { + return !!this._trace; + }, + + /** + * Hooks up the debugger controller with the tracer client. + */ + connect: function() { + this._stack = []; + this.client.addListener("traces", this.onTraces); + }, + + /** + * Disconnects the debugger controller from the tracer client. Any further + * communcation with the tracer actor will not have any effect on the UI. + */ + disconnect: function() { + this._stack = null; + this.client.removeListener("traces", this.onTraces); + }, + + /** + * Instructs the tracer actor to start tracing. + */ + startTracing: function(aCallback = () => {}) { + if (this.tracing) { + return; + } + + DebuggerView.Tracer.selectTab(); + + let id = this._trace = "dbg.trace" + Math.random(); + let fields = [ + "name", + "location", + "hitCount", + "parameterNames", + "depth", + "arguments", + "return", + "throw", + "yield" + ]; + + this.traceClient.startTrace(fields, id, aResponse => { + const { error } = aResponse; + if (error) { + DevToolsUtils.reportException("Tracer.prototype.startTracing", error); + this._trace = null; + } + + aCallback(aResponse); + }); + }, + + /** + * Instructs the tracer actor to stop tracing. + */ + stopTracing: function(aCallback = () => {}) { + if (!this.tracing) { + return; + } + this.traceClient.stopTrace(this._trace, aResponse => { + const { error } = aResponse; + if (error) { + DevToolsUtils.reportException("Tracer.prototype.stopTracing", error); + } + + this._trace = null; + DebuggerController.HitCounts.clear(); + aCallback(aResponse); + }); + }, + + onTraces: function (aEvent, { traces }) { + const tracesLength = traces.length; + let tracesToShow; + + // Update hit counts. + for (let t of traces) { + if (t.type == "enteredFrame") { + DebuggerController.HitCounts.set(t.location, t.hitCount); + } + } + DebuggerController.HitCounts.updateEditorHitCounts(); + + // Limit number of traces to be shown in the log. + if (tracesLength > TracerView.MAX_TRACES) { + tracesToShow = traces.slice(tracesLength - TracerView.MAX_TRACES, tracesLength); + this._stack.splice(0, this._stack.length); + DebuggerView.Tracer.empty(); + } else { + tracesToShow = traces; + } + + // Show traces in the log. + for (let t of tracesToShow) { + if (t.type == "enteredFrame") { + this._onCall(t); + } else { + this._onReturn(t); + } + } + DebuggerView.Tracer.commit(); + }, + + /** + * Callback for handling a new call frame. + */ + _onCall: function({ name, location, blackBoxed, parameterNames, depth, arguments: args }) { + const item = { + name: name, + location: location, + id: this._idCounter++, + blackBoxed + }; + + this._stack.push(item); + DebuggerView.Tracer.addTrace({ + type: "call", + name: name, + location: location, + depth: depth, + parameterNames: parameterNames, + arguments: args, + frameId: item.id, + blackBoxed + }); + }, + + /** + * Callback for handling an exited frame. + */ + _onReturn: function(aPacket) { + if (!this._stack.length) { + return; + } + + const { name, id, location, blackBoxed } = this._stack.pop(); + DebuggerView.Tracer.addTrace({ + type: aPacket.why, + name: name, + location: location, + depth: aPacket.depth, + frameId: id, + returnVal: aPacket.return || aPacket.throw || aPacket.yield, + blackBoxed + }); + }, + + /** + * Create an object which has the same interface as a normal object client, + * but since we already have all the information for an object that we will + * ever get (the server doesn't create actors when tracing, just firehoses + * data and forgets about it) just return the data immdiately. + * + * @param Object aObject + * The tracer object "grip" (more like a limited snapshot). + * @returns Object + * The synchronous client object. + */ + syncGripClient: function(aObject) { + return { + get isFrozen() { return aObject.frozen; }, + get isSealed() { return aObject.sealed; }, + get isExtensible() { return aObject.extensible; }, + + get ownProperties() { return aObject.ownProperties; }, + get prototype() { return null; }, + + getParameterNames: callback => callback(aObject), + getPrototypeAndProperties: callback => callback(aObject), + getPrototype: callback => callback(aObject), + + getOwnPropertyNames: (callback) => { + callback({ + ownPropertyNames: aObject.ownProperties + ? Object.keys(aObject.ownProperties) + : [] + }); + }, + + getProperty: (property, callback) => { + callback({ + descriptor: aObject.ownProperties + ? aObject.ownProperties[property] + : null + }); + }, + + getDisplayString: callback => callback("[object " + aObject.class + "]"), + + getScope: callback => callback({ + error: "scopeNotAvailable", + message: "Cannot get scopes for traced objects" + }) + }; + }, + + /** + * Wraps object snapshots received from the tracer server so that we can + * differentiate them from long living object grips from the debugger server + * in the variables view. + * + * @param Object aObject + * The object snapshot from the tracer actor. + */ + WrappedObject: function(aObject) { + this.object = aObject; + } +}; + +/** + * Handles breaking on event listeners in the currently debugged target. + */ +function EventListeners() { +} + +EventListeners.prototype = { + /** + * A list of event names on which the debuggee will automatically pause + * when invoked. + */ + activeEventNames: [], + + /** + * Updates the list of events types with listeners that, when invoked, + * will automatically pause the debuggee. The respective events are + * retrieved from the UI. + */ + scheduleEventBreakpointsUpdate: function() { + // Make sure we're not sending a batch of closely repeated requests. + // This can easily happen when toggling all events of a certain type. + setNamedTimeout("event-breakpoints-update", 0, () => { + this.activeEventNames = DebuggerView.EventListeners.getCheckedEvents(); + gThreadClient.pauseOnDOMEvents(this.activeEventNames); + + // Notify that event breakpoints were added/removed on the server. + window.emit(EVENTS.EVENT_BREAKPOINTS_UPDATED); + }); + }, + + /** + * Schedules fetching the currently attached event listeners from the debugee. + */ + scheduleEventListenersFetch: function() { + // Make sure we're not sending a batch of closely repeated requests. + // This can easily happen whenever new sources are fetched. + setNamedTimeout("event-listeners-fetch", FETCH_EVENT_LISTENERS_DELAY, () => { + if (gThreadClient.state != "paused") { + gThreadClient.interrupt(() => this._getListeners(() => gThreadClient.resume())); + } else { + this._getListeners(); + } + }); + }, + + /** + * Fetches the currently attached event listeners from the debugee. + * The thread client state is assumed to be "paused". + * + * @param function aCallback + * Invoked once the event listeners are fetched and displayed. + */ + _getListeners: function(aCallback) { + gThreadClient.eventListeners(Task.async(function*(aResponse) { + if (aResponse.error) { + throw "Error getting event listeners: " + aResponse.message; + } + + // Make sure all the listeners are sorted by the event type, since + // they're not guaranteed to be clustered together. + aResponse.listeners.sort((a, b) => a.type > b.type ? 1 : -1); + + // Add all the listeners in the debugger view event linsteners container. + for (let listener of aResponse.listeners) { + let definitionSite; + if (listener.function.class == "Function") { + definitionSite = yield this._getDefinitionSite(listener.function); + } + listener.function.url = definitionSite; + DebuggerView.EventListeners.addListener(listener, { staged: true }); + } + + // Flushes all the prepared events into the event listeners container. + DebuggerView.EventListeners.commit(); + + // Notify that event listeners were fetched and shown in the view, + // and callback to resume the active thread if necessary. + window.emit(EVENTS.EVENT_LISTENERS_FETCHED); + aCallback && aCallback(); + }.bind(this))); + }, + + /** + * Gets a function's source-mapped definiton site. + * + * @param object aFunction + * The grip of the function to get the definition site for. + * @return object + * A promise that is resolved with the function's owner source url. + */ + _getDefinitionSite: function(aFunction) { + let deferred = promise.defer(); + + gThreadClient.pauseGrip(aFunction).getDefinitionSite(aResponse => { + if (aResponse.error) { + // Don't make this error fatal, because it would break the entire events pane. + const msg = "Error getting function definition site: " + aResponse.message; + DevToolsUtils.reportException("_getDefinitionSite", msg); + } + deferred.resolve(aResponse.source.url); + }); + + return deferred.promise; + } +}; + +/** + * Handles all the breakpoints in the current debugger. + */ +function Breakpoints() { + this._onEditorBreakpointAdd = this._onEditorBreakpointAdd.bind(this); + this._onEditorBreakpointRemove = this._onEditorBreakpointRemove.bind(this); + this.addBreakpoint = this.addBreakpoint.bind(this); + this.removeBreakpoint = this.removeBreakpoint.bind(this); +} + +Breakpoints.prototype = { + /** + * A map of breakpoint promises as tracked by the debugger frontend. + * The keys consist of a string representation of the breakpoint location. + */ + _added: new Map(), + _removing: new Map(), + _disabled: new Map(), + + /** + * Adds the source editor breakpoint handlers. + * + * @return object + * A promise that is resolved when the breakpoints finishes initializing. + */ + initialize: function() { + DebuggerView.editor.on("breakpointAdded", this._onEditorBreakpointAdd); + DebuggerView.editor.on("breakpointRemoved", this._onEditorBreakpointRemove); + + // Initialization is synchronous, for now. + return promise.resolve(null); + }, + + /** + * Removes the source editor breakpoint handlers & all the added breakpoints. + * + * @return object + * A promise that is resolved when the breakpoints finishes destroying. + */ + destroy: function() { + DebuggerView.editor.off("breakpointAdded", this._onEditorBreakpointAdd); + DebuggerView.editor.off("breakpointRemoved", this._onEditorBreakpointRemove); + + return this.removeAllBreakpoints(); + }, + + /** + * Event handler for new breakpoints that come from the editor. + * + * @param number aLine + * Line number where breakpoint was set. + */ + _onEditorBreakpointAdd: Task.async(function*(_, aLine) { + let actor = DebuggerView.Sources.selectedValue; + let location = { actor: actor, line: aLine + 1 }; + + // Initialize the breakpoint, but don't update the editor, since this + // callback is invoked because a breakpoint was added in the + // editor itself. + let breakpointClient = yield this.addBreakpoint(location, { noEditorUpdate: true }); + + // If the breakpoint client has a "requestedLocation" attached, then + // the original requested placement for the breakpoint wasn't accepted. + // In this case, we need to update the editor with the new location. + if (breakpointClient.requestedLocation) { + DebuggerView.editor.moveBreakpoint( + breakpointClient.requestedLocation.line - 1, + breakpointClient.location.line - 1 + ); + } + + // Notify that we've shown a breakpoint in the source editor. + window.emit(EVENTS.BREAKPOINT_SHOWN_IN_EDITOR); + }), + + /** + * Event handler for breakpoints that are removed from the editor. + * + * @param number aLine + * Line number where breakpoint was removed. + */ + _onEditorBreakpointRemove: Task.async(function*(_, aLine) { + let actor = DebuggerView.Sources.selectedValue; + let location = { actor: actor, line: aLine + 1 }; + yield this.removeBreakpoint(location, { noEditorUpdate: true }); + + // Notify that we've hidden a breakpoint in the source editor. + window.emit(EVENTS.BREAKPOINT_HIDDEN_IN_EDITOR); + }), + + /** + * Update the breakpoints in the editor view. This function takes the list of + * breakpoints in the debugger and adds them back into the editor view. + * This is invoked when the selected script is changed, or when new sources + * are received via the _onNewSource and _onSourcesAdded event listeners. + */ + updateEditorBreakpoints: Task.async(function*() { + for (let breakpointPromise of this._addedOrDisabled) { + let breakpointClient = yield breakpointPromise; + let location = breakpointClient.location; + let currentSourceActor = DebuggerView.Sources.selectedValue; + let sourceActor = DebuggerView.Sources.getActorForLocation(location); + + // Update the view only if the breakpoint is in the currently + // shown source. + if (currentSourceActor === sourceActor) { + yield this._showBreakpoint(breakpointClient, { noPaneUpdate: true }); + } + } + }), + + /** + * Update the breakpoints in the pane view. This function takes the list of + * breakpoints in the debugger and adds them back into the breakpoints pane. + * This is invoked when new sources are received via the _onNewSource and + * _onSourcesAdded event listeners. + */ + updatePaneBreakpoints: Task.async(function*() { + for (let breakpointPromise of this._addedOrDisabled) { + let breakpointClient = yield breakpointPromise; + let container = DebuggerView.Sources; + let sourceActor = breakpointClient.location.actor; + + // Update the view only if the breakpoint exists in a known source. + if (container.containsValue(sourceActor)) { + yield this._showBreakpoint(breakpointClient, { noEditorUpdate: true }); + } + } + }), + + /** + * Add a breakpoint. + * + * @param object aLocation + * The location where you want the breakpoint. + * This object must have two properties: + * - url: the breakpoint's source location. + * - line: the breakpoint's line number. + * It can also have the following optional properties: + * - condition: only pause if this condition evaluates truthy + * @param object aOptions [optional] + * Additional options or flags supported by this operation: + * - openPopup: tells if the expression popup should be shown. + * - noEditorUpdate: tells if you want to skip editor updates. + * - noPaneUpdate: tells if you want to skip breakpoint pane updates. + * @return object + * A promise that is resolved after the breakpoint is added, or + * rejected if there was an error. + */ + addBreakpoint: Task.async(function*(aLocation, aOptions = {}) { + // Make sure a proper location is available. + if (!aLocation) { + throw new Error("Invalid breakpoint location."); + } + let addedPromise, removingPromise; + + // If the breakpoint was already added, or is currently being added at the + // specified location, then return that promise immediately. + if ((addedPromise = this._getAdded(aLocation))) { + return addedPromise; + } + + // If the breakpoint is currently being removed from the specified location, + // then wait for that to finish. + if ((removingPromise = this._getRemoving(aLocation))) { + yield removingPromise; + } + + let deferred = promise.defer(); + + // Remember the breakpoint initialization promise in the store. + let identifier = this.getIdentifier(aLocation); + this._added.set(identifier, deferred.promise); + + let source = gThreadClient.source( + DebuggerView.Sources.getItemByValue(aLocation.actor).attachment.source + ); + + source.setBreakpoint(aLocation, Task.async(function*(aResponse, aBreakpointClient) { + // If the breakpoint response has an "actualLocation" attached, then + // the original requested placement for the breakpoint wasn't accepted. + if (aResponse.actualLocation) { + // Remember the initialization promise for the new location instead. + let oldIdentifier = identifier; + let newIdentifier = identifier = this.getIdentifier(aResponse.actualLocation); + this._added.delete(oldIdentifier); + this._added.set(newIdentifier, deferred.promise); + } + + // By default, new breakpoints are always enabled. Disabled breakpoints + // are, in fact, removed from the server but preserved in the frontend, + // so that they may not be forgotten across target navigations. + let disabledPromise = this._disabled.get(identifier); + if (disabledPromise) { + let aPrevBreakpointClient = yield disabledPromise; + let condition = aPrevBreakpointClient.getCondition(); + this._disabled.delete(identifier); + + if (condition) { + aBreakpointClient = yield aBreakpointClient.setCondition( + gThreadClient, + condition + ); + } + } + + if (aResponse.actualLocation) { + // Store the originally requested location in case it's ever needed + // and update the breakpoint client with the actual location. + let actualLoc = aResponse.actualLocation; + aBreakpointClient.requestedLocation = aLocation; + aBreakpointClient.location = actualLoc; + aBreakpointClient.location.actor = actualLoc.source ? actualLoc.source.actor : null; + } + + // Preserve information about the breakpoint's line text, to display it + // in the sources pane without requiring fetching the source (for example, + // after the target navigated). Note that this will get out of sync + // if the source text contents change. + let line = aBreakpointClient.location.line - 1; + aBreakpointClient.text = DebuggerView.editor.getText(line).trim(); + + // Show the breakpoint in the editor and breakpoints pane, and + // resolve. + yield this._showBreakpoint(aBreakpointClient, aOptions); + + // Notify that we've added a breakpoint. + window.emit(EVENTS.BREAKPOINT_ADDED, aBreakpointClient); + deferred.resolve(aBreakpointClient); + }.bind(this))); + + return deferred.promise; + }), + + /** + * Remove a breakpoint. + * + * @param object aLocation + * @see DebuggerController.Breakpoints.addBreakpoint + * @param object aOptions [optional] + * @see DebuggerController.Breakpoints.addBreakpoint + * @return object + * A promise that is resolved after the breakpoint is removed, or + * rejected if there was an error. + */ + removeBreakpoint: function(aLocation, aOptions = {}) { + // Make sure a proper location is available. + if (!aLocation) { + return promise.reject(new Error("Invalid breakpoint location.")); + } + + // If the breakpoint was already removed, or has never even been added, + // then return a resolved promise immediately. + let addedPromise = this._getAdded(aLocation); + if (!addedPromise) { + return promise.resolve(aLocation); + } + + // If the breakpoint is currently being removed from the specified location, + // then return that promise immediately. + let removingPromise = this._getRemoving(aLocation); + if (removingPromise) { + return removingPromise; + } + + let deferred = promise.defer(); + + // Remember the breakpoint removal promise in the store. + let identifier = this.getIdentifier(aLocation); + this._removing.set(identifier, deferred.promise); + + // Retrieve the corresponding breakpoint client first. + addedPromise.then(aBreakpointClient => { + // Try removing the breakpoint. + aBreakpointClient.remove(aResponse => { + // If there was an error removing the breakpoint, reject the promise + // and forget about it that the breakpoint may be re-removed later. + if (aResponse.error) { + deferred.reject(aResponse); + return void this._removing.delete(identifier); + } + + // When a breakpoint is removed, the frontend may wish to preserve some + // details about it, so that it can be easily re-added later. In such + // cases, breakpoints are marked and stored as disabled, so that they + // may not be forgotten across target navigations. + if (aOptions.rememberDisabled) { + aBreakpointClient.disabled = true; + this._disabled.set(identifier, promise.resolve(aBreakpointClient)); + } + + // Forget both the initialization and removal promises from the store. + this._added.delete(identifier); + this._removing.delete(identifier); + + // Hide the breakpoint from the editor and breakpoints pane, and resolve. + this._hideBreakpoint(aLocation, aOptions); + + // Notify that we've removed a breakpoint. + window.emit(EVENTS.BREAKPOINT_REMOVED, aLocation); + deferred.resolve(aLocation); + }); + }); + + return deferred.promise; + }, + + /** + * Removes all the currently enabled breakpoints. + * + * @return object + * A promise that is resolved after all breakpoints are removed, or + * rejected if there was an error. + */ + removeAllBreakpoints: function() { + /* Gets an array of all the existing breakpoints promises. */ + let getActiveBreakpoints = (aPromises, aStore = []) => { + for (let [, breakpointPromise] of aPromises) { + aStore.push(breakpointPromise); + } + return aStore; + } + + /* Gets an array of all the removed breakpoints promises. */ + let getRemovedBreakpoints = (aClients, aStore = []) => { + for (let breakpointClient of aClients) { + aStore.push(this.removeBreakpoint(breakpointClient.location)); + } + return aStore; + } + + // First, populate an array of all the currently added breakpoints promises. + // Then, once all the breakpoints clients are retrieved, populate an array + // of all the removed breakpoints promises and wait for their fulfillment. + return promise.all(getActiveBreakpoints(this._added)).then(aBreakpointClients => { + return promise.all(getRemovedBreakpoints(aBreakpointClients)); + }); + }, + + /** + * Update the condition of a breakpoint. + * + * @param object aLocation + * @see DebuggerController.Breakpoints.addBreakpoint + * @param string aClients + * The condition to set on the breakpoint + * @return object + * A promise that will be resolved with the breakpoint client + */ + updateCondition: Task.async(function*(aLocation, aCondition) { + let addedPromise = this._getAdded(aLocation); + if (!addedPromise) { + throw new Error("Breakpoint does not exist at the specified location"); + } + let breakpointClient = yield addedPromise; + let promise = breakpointClient.setCondition(gThreadClient, aCondition); + + // `setCondition` returns a new breakpoint that has the condition, + // so we need to update the store + this._added.set(this.getIdentifier(aLocation), promise); + return promise; + }), + + /** + * Update the editor and breakpoints pane to show a specified breakpoint. + * + * @param object aBreakpointClient + * A BreakpointClient instance. + * This object has additional properties dynamically added by + * our code: + * - disabled: the breakpoint's disabled state, boolean + * - text: the breakpoint's line text to be displayed + * @param object aOptions [optional] + * @see DebuggerController.Breakpoints.addBreakpoint + */ + _showBreakpoint: function(aBreakpointClient, aOptions = {}) { + let tasks = []; + let currentSourceActor = DebuggerView.Sources.selectedValue; + let location = aBreakpointClient.location; + let actor = DebuggerView.Sources.getActorForLocation(location); + + // Update the editor if required. + if (!aOptions.noEditorUpdate && !aBreakpointClient.disabled) { + if (currentSourceActor === actor) { + tasks.push(DebuggerView.editor.addBreakpoint(location.line - 1)); + } + } + + // Update the breakpoints pane if required. + if (!aOptions.noPaneUpdate) { + DebuggerView.Sources.addBreakpoint(aBreakpointClient, aOptions); + } + + return promise.all(tasks); + }, + + /** + * Update the editor and breakpoints pane to hide a specified breakpoint. + * + * @param object aLocation + * @see DebuggerController.Breakpoints.addBreakpoint + * @param object aOptions [optional] + * @see DebuggerController.Breakpoints.addBreakpoint + */ + _hideBreakpoint: function(aLocation, aOptions = {}) { + let currentSourceActor = DebuggerView.Sources.selectedValue; + let actor = DebuggerView.Sources.getActorForLocation(aLocation); + + // Update the editor if required. + if (!aOptions.noEditorUpdate) { + if (currentSourceActor === actor) { + DebuggerView.editor.removeBreakpoint(aLocation.line - 1); + } + } + + // Update the breakpoints pane if required. + if (!aOptions.noPaneUpdate) { + DebuggerView.Sources.removeBreakpoint(aLocation); + } + }, + + /** + * Get a Promise for the BreakpointActor client object which is already added + * or currently being added at the given location. + * + * @param object aLocation + * @see DebuggerController.Breakpoints.addBreakpoint + * @return object | null + * A promise that is resolved after the breakpoint is added, or + * null if no breakpoint was found. + */ + _getAdded: function(aLocation) { + return this._added.get(this.getIdentifier(aLocation)); + }, + + /** + * Get a Promise for the BreakpointActor client object which is currently + * being removed from the given location. + * + * @param object aLocation + * @see DebuggerController.Breakpoints.addBreakpoint + * @return object | null + * A promise that is resolved after the breakpoint is removed, or + * null if no breakpoint was found. + */ + _getRemoving: function(aLocation) { + return this._removing.get(this.getIdentifier(aLocation)); + }, + + /** + * Get an identifier string for a given location. Breakpoint promises are + * identified in the store by a string representation of their location. + * + * @param object aLocation + * The location to serialize to a string. + * @return string + * The identifier string. + */ + getIdentifier: function(aLocation) { + return (aLocation.source ? aLocation.source.actor : aLocation.actor) + + ":" + aLocation.line; + } +}; + +/** + * Gets all Promises for the BreakpointActor client objects that are + * either enabled (added to the server) or disabled (removed from the server, + * but for which some details are preserved). + */ +Object.defineProperty(Breakpoints.prototype, "_addedOrDisabled", { + get: function* () { + yield* this._added.values(); + yield* this._disabled.values(); + } +}); + +/** + * Handles Tracer's hit counts. + */ +function HitCounts() { + /** + * Storage of hit counts for every location + * hitCount = _locations[url][line][column] + */ + this._hitCounts = Object.create(null); +} + +HitCounts.prototype = { + set: function({url, line, column}, aHitCount) { + if (url) { + if (!this._hitCounts[url]) { + this._hitCounts[url] = Object.create(null); + } + if (!this._hitCounts[url][line]) { + this._hitCounts[url][line] = Object.create(null); + } + this._hitCounts[url][line][column] = aHitCount; + } + }, + + /** + * Update all the hit counts in the editor view. This is invoked when the + * selected script is changed, or when new sources are received via the + * _onNewSource and _onSourcesAdded event listeners. + */ + updateEditorHitCounts: function() { + // First, remove all hit counters. + DebuggerView.editor.removeAllMarkers("hit-counts"); + + // Then, add new hit counts, just for the current source. + for (let url in this._hitCounts) { + for (let line in this._hitCounts[url]) { + for (let column in this._hitCounts[url][line]) { + this._updateEditorHitCount({url, line, column}); + } + } + } + }, + + /** + * Update a hit counter on a certain line. + */ + _updateEditorHitCount: function({url, line, column}) { + // Editor must be initialized. + if (!DebuggerView.editor) { + return; + } + + // No need to do anything if the counter's source is not being shown in the + // editor. + if (url && + DebuggerView.Sources.selectedItem.attachment.source.url != url) { + return; + } + + // There might be more counters on the same line. We need to combine them + // into one. + let content = Object.keys(this._hitCounts[url][line]) + .sort() // Sort by key (column). + .map(a => this._hitCounts[url][line][a]) // Extract values. + .map(a => a + "\u00D7") // Format hit count (e.g. 146×). + .join("|"); + + // CodeMirror's lines are indexed from 0, while traces start from 1 + DebuggerView.editor.addContentMarker(line - 1, "hit-counts", "hit-count", + content); + }, + + /** + * Remove all hit couters and clear the storage + */ + clear: function() { + DebuggerView.editor.removeAllMarkers("hit-counts"); + this._hitCounts = Object.create(null); + } +} + +/** + * Localization convenience methods. + */ +let L10N = new ViewHelpers.L10N(DBG_STRINGS_URI); + +/** + * Shortcuts for accessing various debugger preferences. + */ +let Prefs = new ViewHelpers.Prefs("devtools", { + sourcesWidth: ["Int", "debugger.ui.panes-sources-width"], + instrumentsWidth: ["Int", "debugger.ui.panes-instruments-width"], + panesVisibleOnStartup: ["Bool", "debugger.ui.panes-visible-on-startup"], + variablesSortingEnabled: ["Bool", "debugger.ui.variables-sorting-enabled"], + variablesOnlyEnumVisible: ["Bool", "debugger.ui.variables-only-enum-visible"], + variablesSearchboxVisible: ["Bool", "debugger.ui.variables-searchbox-visible"], + pauseOnExceptions: ["Bool", "debugger.pause-on-exceptions"], + ignoreCaughtExceptions: ["Bool", "debugger.ignore-caught-exceptions"], + sourceMapsEnabled: ["Bool", "debugger.source-maps-enabled"], + prettyPrintEnabled: ["Bool", "debugger.pretty-print-enabled"], + autoPrettyPrint: ["Bool", "debugger.auto-pretty-print"], + tracerEnabled: ["Bool", "debugger.tracer"], + editorTabSize: ["Int", "editor.tabsize"], + autoBlackBox: ["Bool", "debugger.auto-black-box"] +}); + +/** + * Convenient way of emitting events from the panel window. + */ +EventEmitter.decorate(this); + +/** + * Preliminary setup for the DebuggerController object. + */ +DebuggerController.initialize(); +DebuggerController.Parser = new Parser(); +DebuggerController.ThreadState = new ThreadState(); +DebuggerController.StackFrames = new StackFrames(); +DebuggerController.SourceScripts = new SourceScripts(); +DebuggerController.Breakpoints = new Breakpoints(); +DebuggerController.Breakpoints.DOM = new EventListeners(); +DebuggerController.Tracer = new Tracer(); +DebuggerController.HitCounts = new HitCounts(); + +/** + * Export some properties to the global scope for easier access. + */ +Object.defineProperties(window, { + "gTarget": { + get: function() DebuggerController._target, + configurable: true + }, + "gHostType": { + get: function() DebuggerView._hostType, + configurable: true + }, + "gClient": { + get: function() DebuggerController.client, + configurable: true + }, + "gThreadClient": { + get: function() DebuggerController.activeThread, + configurable: true + }, + "gCallStackPageSize": { + get: function() CALL_STACK_PAGE_SIZE, + configurable: true + } +}); + +/** + * Helper method for debugging. + * @param string + */ +function dumpn(str) { + if (wantLogging) { + dump("DBG-FRONTEND: " + str + "\n"); + } +} + +let wantLogging = Services.prefs.getBoolPref("devtools.debugger.log"); diff --git a/toolkit/devtools/debugger/debugger-panes.js b/toolkit/devtools/debugger/debugger-panes.js new file mode 100644 index 000000000..b01d4a32e --- /dev/null +++ b/toolkit/devtools/debugger/debugger-panes.js @@ -0,0 +1,3420 @@ +/* -*- 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"; + +// Used to detect minification for automatic pretty printing +const SAMPLE_SIZE = 50; // no of lines +const INDENT_COUNT_THRESHOLD = 5; // percentage +const CHARACTER_LIMIT = 250; // line character limit + +// Maps known URLs to friendly source group names and put them at the +// bottom of source list. +const KNOWN_SOURCE_GROUPS = { + "Add-on SDK": "resource://gre/modules/commonjs/", +}; + +KNOWN_SOURCE_GROUPS[L10N.getStr("evalGroupLabel")] = "eval"; + +/** + * Functions handling the sources UI. + */ +function SourcesView() { + dumpn("SourcesView was instantiated"); + + this.togglePrettyPrint = this.togglePrettyPrint.bind(this); + this.toggleBlackBoxing = this.toggleBlackBoxing.bind(this); + this.toggleBreakpoints = this.toggleBreakpoints.bind(this); + + this._onEditorLoad = this._onEditorLoad.bind(this); + this._onEditorUnload = this._onEditorUnload.bind(this); + this._onEditorCursorActivity = this._onEditorCursorActivity.bind(this); + this._onSourceSelect = this._onSourceSelect.bind(this); + this._onStopBlackBoxing = this._onStopBlackBoxing.bind(this); + this._onBreakpointRemoved = this._onBreakpointRemoved.bind(this); + this._onBreakpointClick = this._onBreakpointClick.bind(this); + this._onBreakpointCheckboxClick = this._onBreakpointCheckboxClick.bind(this); + this._onConditionalPopupShowing = this._onConditionalPopupShowing.bind(this); + this._onConditionalPopupShown = this._onConditionalPopupShown.bind(this); + this._onConditionalPopupHiding = this._onConditionalPopupHiding.bind(this); + this._onConditionalTextboxKeyPress = this._onConditionalTextboxKeyPress.bind(this); +} + +SourcesView.prototype = Heritage.extend(WidgetMethods, { + /** + * Initialization function, called when the debugger is started. + */ + initialize: function() { + dumpn("Initializing the SourcesView"); + + this.widget = new SideMenuWidget(document.getElementById("sources"), { + showArrows: true + }); + + this.emptyText = L10N.getStr("noSourcesText"); + this._blackBoxCheckboxTooltip = L10N.getStr("blackBoxCheckboxTooltip"); + + this._commandset = document.getElementById("debuggerCommands"); + this._popupset = document.getElementById("debuggerPopupset"); + this._cmPopup = document.getElementById("sourceEditorContextMenu"); + this._cbPanel = document.getElementById("conditional-breakpoint-panel"); + this._cbTextbox = document.getElementById("conditional-breakpoint-panel-textbox"); + this._blackBoxButton = document.getElementById("black-box"); + this._stopBlackBoxButton = document.getElementById("black-boxed-message-button"); + this._prettyPrintButton = document.getElementById("pretty-print"); + this._toggleBreakpointsButton = document.getElementById("toggle-breakpoints"); + + if (Prefs.prettyPrintEnabled) { + this._prettyPrintButton.removeAttribute("hidden"); + } + + window.on(EVENTS.EDITOR_LOADED, this._onEditorLoad, false); + window.on(EVENTS.EDITOR_UNLOADED, this._onEditorUnload, false); + this.widget.addEventListener("select", this._onSourceSelect, false); + this._stopBlackBoxButton.addEventListener("click", this._onStopBlackBoxing, false); + this._cbPanel.addEventListener("popupshowing", this._onConditionalPopupShowing, false); + this._cbPanel.addEventListener("popupshown", this._onConditionalPopupShown, false); + this._cbPanel.addEventListener("popuphiding", this._onConditionalPopupHiding, false); + this._cbTextbox.addEventListener("keypress", this._onConditionalTextboxKeyPress, false); + + this.autoFocusOnSelection = false; + + // Sort the contents by the displayed label. + this.sortContents((aFirst, aSecond) => { + return +(aFirst.attachment.label.toLowerCase() > + aSecond.attachment.label.toLowerCase()); + }); + + // Sort known source groups towards the end of the list + this.widget.groupSortPredicate = function(a, b) { + if ((a in KNOWN_SOURCE_GROUPS) == (b in KNOWN_SOURCE_GROUPS)) { + return a.localeCompare(b); + } + return (a in KNOWN_SOURCE_GROUPS) ? 1 : -1; + }; + }, + + /** + * Destruction function, called when the debugger is closed. + */ + destroy: function() { + dumpn("Destroying the SourcesView"); + + window.off(EVENTS.EDITOR_LOADED, this._onEditorLoad, false); + window.off(EVENTS.EDITOR_UNLOADED, this._onEditorUnload, false); + this.widget.removeEventListener("select", this._onSourceSelect, false); + this._stopBlackBoxButton.removeEventListener("click", this._onStopBlackBoxing, false); + this._cbPanel.removeEventListener("popupshowing", this._onConditionalPopupShowing, false); + this._cbPanel.removeEventListener("popupshowing", this._onConditionalPopupShown, false); + this._cbPanel.removeEventListener("popuphiding", this._onConditionalPopupHiding, false); + this._cbTextbox.removeEventListener("keypress", this._onConditionalTextboxKeyPress, false); + }, + + /** + * Sets the preferred location to be selected in this sources container. + * @param string aUrl + */ + set preferredSource(aUrl) { + this._preferredValue = aUrl; + + // Selects the element with the specified value in this sources container, + // if already inserted. + if (this.containsValue(aUrl)) { + this.selectedValue = aUrl; + } + }, + + /** + * Adds a source to this sources container. + * + * @param object aSource + * The source object coming from the active thread. + * @param object aOptions [optional] + * Additional options for adding the source. Supported options: + * - staged: true to stage the item to be appended later + */ + addSource: function(aSource, aOptions = {}) { + if (!aSource.url) { + // We don't show any unnamed eval scripts yet (see bug 1124106) + return; + } + + let { label, group, unicodeUrl } = this._parseUrl(aSource); + + let contents = document.createElement("label"); + contents.className = "plain dbg-source-item"; + contents.setAttribute("value", label); + contents.setAttribute("crop", "start"); + contents.setAttribute("flex", "1"); + contents.setAttribute("tooltiptext", unicodeUrl); + + // If the source is blackboxed, apply the appropriate style. + if (gThreadClient.source(aSource).isBlackBoxed) { + contents.classList.add("black-boxed"); + } + + // Append a source item to this container. + this.push([contents, aSource.actor], { + staged: aOptions.staged, /* stage the item to be appended later? */ + attachment: { + label: label, + group: group, + checkboxState: !aSource.isBlackBoxed, + checkboxTooltip: this._blackBoxCheckboxTooltip, + source: aSource + } + }); + }, + + _parseUrl: function(aSource) { + let fullUrl = aSource.url; + let url = fullUrl.split(" -> ").pop(); + let label = aSource.addonPath ? aSource.addonPath : SourceUtils.getSourceLabel(url); + let group = aSource.addonID ? aSource.addonID : SourceUtils.getSourceGroup(url); + + return { + label: label, + group: group, + unicodeUrl: NetworkHelper.convertToUnicode(unescape(fullUrl)) + }; + }, + + /** + * Adds a breakpoint to this sources container. + * + * @param object aBreakpointClient + * See Breakpoints.prototype._showBreakpoint + * @param object aOptions [optional] + * @see DebuggerController.Breakpoints.addBreakpoint + */ + addBreakpoint: function(aBreakpointClient, aOptions = {}) { + let { location, disabled } = aBreakpointClient; + + // Make sure we're not duplicating anything. If a breakpoint at the + // specified source url and line already exists, just toggle it. + if (this.getBreakpoint(location)) { + this[disabled ? "disableBreakpoint" : "enableBreakpoint"](location); + return; + } + + // Get the source item to which the breakpoint should be attached. + let sourceItem = this.getItemByValue(this.getActorForLocation(location)); + + // Create the element node and menu popup for the breakpoint item. + let breakpointArgs = Heritage.extend(aBreakpointClient, aOptions); + let breakpointView = this._createBreakpointView.call(this, breakpointArgs); + let contextMenu = this._createContextMenu.call(this, breakpointArgs); + + // Append a breakpoint child item to the corresponding source item. + sourceItem.append(breakpointView.container, { + attachment: Heritage.extend(breakpointArgs, { + actor: location.actor, + line: location.line, + view: breakpointView, + popup: contextMenu + }), + attributes: [ + ["contextmenu", contextMenu.menupopupId] + ], + // Make sure that when the breakpoint item is removed, the corresponding + // menupopup and commandset are also destroyed. + finalize: this._onBreakpointRemoved + }); + + // Highlight the newly appended breakpoint child item if + // necessary. + if (aOptions.openPopup || !aOptions.noEditorUpdate) { + this.highlightBreakpoint(location, aOptions); + } + + window.emit(EVENTS.BREAKPOINT_SHOWN_IN_PANE); + }, + + /** + * Removes a breakpoint from this sources container. + * It does not also remove the breakpoint from the controller. Be careful. + * + * @param object aLocation + * @see DebuggerController.Breakpoints.addBreakpoint + */ + removeBreakpoint: function(aLocation) { + // When a parent source item is removed, all the child breakpoint items are + // also automagically removed. + let sourceItem = this.getItemByValue(aLocation.actor); + if (!sourceItem) { + return; + } + let breakpointItem = this.getBreakpoint(aLocation); + if (!breakpointItem) { + return; + } + + // Clear the breakpoint view. + sourceItem.remove(breakpointItem); + + window.emit(EVENTS.BREAKPOINT_HIDDEN_IN_PANE); + }, + + /** + * Returns the breakpoint at the specified source url and line. + * + * @param object aLocation + * @see DebuggerController.Breakpoints.addBreakpoint + * @return object + * The corresponding breakpoint item if found, null otherwise. + */ + getBreakpoint: function(aLocation) { + return this.getItemForPredicate(aItem => + aItem.attachment.actor == aLocation.actor && + aItem.attachment.line == aLocation.line); + }, + + /** + * Returns all breakpoints for all sources. + * + * @return array + * The breakpoints for all sources if any, an empty array otherwise. + */ + getAllBreakpoints: function(aStore = []) { + return this.getOtherBreakpoints(undefined, aStore); + }, + + /** + * Returns all breakpoints which are not at the specified source url and line. + * + * @param object aLocation [optional] + * @see DebuggerController.Breakpoints.addBreakpoint + * @param array aStore [optional] + * A list in which to store the corresponding breakpoints. + * @return array + * The corresponding breakpoints if found, an empty array otherwise. + */ + getOtherBreakpoints: function(aLocation = {}, aStore = []) { + for (let source of this) { + for (let breakpointItem of source) { + let { actor, line } = breakpointItem.attachment; + if (actor != aLocation.actor || line != aLocation.line) { + aStore.push(breakpointItem); + } + } + } + return aStore; + }, + + /** + * Enables a breakpoint. + * + * @param object aLocation + * @see DebuggerController.Breakpoints.addBreakpoint + * @param object aOptions [optional] + * Additional options or flags supported by this operation: + * - silent: pass true to not update the checkbox checked state; + * this is usually necessary when the checked state will + * be updated automatically (e.g: on a checkbox click). + * @return object + * A promise that is resolved after the breakpoint is enabled, or + * rejected if no breakpoint was found at the specified location. + */ + enableBreakpoint: function(aLocation, aOptions = {}) { + let breakpointItem = this.getBreakpoint(aLocation); + if (!breakpointItem) { + return promise.reject(new Error("No breakpoint found.")); + } + + // Breakpoint will now be enabled. + let attachment = breakpointItem.attachment; + attachment.disabled = false; + + // Update the corresponding menu items to reflect the enabled state. + let prefix = "bp-cMenu-"; // "breakpoints context menu" + let identifier = DebuggerController.Breakpoints.getIdentifier(attachment); + let enableSelfId = prefix + "enableSelf-" + identifier + "-menuitem"; + let disableSelfId = prefix + "disableSelf-" + identifier + "-menuitem"; + document.getElementById(enableSelfId).setAttribute("hidden", "true"); + document.getElementById(disableSelfId).removeAttribute("hidden"); + + // Update the breakpoint toggle button checked state. + this._toggleBreakpointsButton.removeAttribute("checked"); + + // Update the checkbox state if necessary. + if (!aOptions.silent) { + attachment.view.checkbox.setAttribute("checked", "true"); + } + + return DebuggerController.Breakpoints.addBreakpoint(aLocation, { + // No need to update the pane, since this method is invoked because + // a breakpoint's view was interacted with. + noPaneUpdate: true + }); + }, + + /** + * Disables a breakpoint. + * + * @param object aLocation + * @see DebuggerController.Breakpoints.addBreakpoint + * @param object aOptions [optional] + * Additional options or flags supported by this operation: + * - silent: pass true to not update the checkbox checked state; + * this is usually necessary when the checked state will + * be updated automatically (e.g: on a checkbox click). + * @return object + * A promise that is resolved after the breakpoint is disabled, or + * rejected if no breakpoint was found at the specified location. + */ + disableBreakpoint: function(aLocation, aOptions = {}) { + let breakpointItem = this.getBreakpoint(aLocation); + if (!breakpointItem) { + return promise.reject(new Error("No breakpoint found.")); + } + + // Breakpoint will now be disabled. + let attachment = breakpointItem.attachment; + attachment.disabled = true; + + // Update the corresponding menu items to reflect the disabled state. + let prefix = "bp-cMenu-"; // "breakpoints context menu" + let identifier = DebuggerController.Breakpoints.getIdentifier(attachment); + let enableSelfId = prefix + "enableSelf-" + identifier + "-menuitem"; + let disableSelfId = prefix + "disableSelf-" + identifier + "-menuitem"; + document.getElementById(enableSelfId).removeAttribute("hidden"); + document.getElementById(disableSelfId).setAttribute("hidden", "true"); + + // Update the checkbox state if necessary. + if (!aOptions.silent) { + attachment.view.checkbox.removeAttribute("checked"); + } + + return DebuggerController.Breakpoints.removeBreakpoint(aLocation, { + // No need to update this pane, since this method is invoked because + // a breakpoint's view was interacted with. + noPaneUpdate: true, + // Mark this breakpoint as being "disabled", not completely removed. + // This makes sure it will not be forgotten across target navigations. + rememberDisabled: true + }); + }, + + /** + * Highlights a breakpoint in this sources container. + * + * @param object aLocation + * @see DebuggerController.Breakpoints.addBreakpoint + * @param object aOptions [optional] + * An object containing some of the following boolean properties: + * - openPopup: tells if the expression popup should be shown. + * - noEditorUpdate: tells if you want to skip editor updates. + */ + highlightBreakpoint: function(aLocation, aOptions = {}) { + let breakpointItem = this.getBreakpoint(aLocation); + if (!breakpointItem) { + return; + } + + // Breakpoint will now be selected. + this._selectBreakpoint(breakpointItem); + + // Update the editor location if necessary. + if (!aOptions.noEditorUpdate) { + DebuggerView.setEditorLocation(aLocation.actor, aLocation.line, { noDebug: true }); + } + + // If the breakpoint requires a new conditional expression, display + // the panel to input the corresponding expression. + if (aOptions.openPopup) { + this._openConditionalPopup(); + } else { + this._hideConditionalPopup(); + } + }, + + /** + * Highlight the breakpoint on the current currently focused line/column + * if it exists. + */ + highlightBreakpointAtCursor: function() { + let actor = DebuggerView.Sources.selectedValue; + let line = DebuggerView.editor.getCursor().line + 1; + + let location = { actor: actor, line: line }; + this.highlightBreakpoint(location, { noEditorUpdate: true }); + }, + + /** + * Unhighlights the current breakpoint in this sources container. + */ + unhighlightBreakpoint: function() { + this._hideConditionalPopup(); + this._unselectBreakpoint(); + }, + + /** + * Update the checked/unchecked and enabled/disabled states of the buttons in + * the sources toolbar based on the currently selected source's state. + */ + updateToolbarButtonsState: function() { + const { source } = this.selectedItem.attachment; + const sourceClient = gThreadClient.source(source); + + if (sourceClient.isBlackBoxed) { + this._prettyPrintButton.setAttribute("disabled", true); + this._blackBoxButton.setAttribute("checked", true); + } else { + this._prettyPrintButton.removeAttribute("disabled"); + this._blackBoxButton.removeAttribute("checked"); + } + + if (sourceClient.isPrettyPrinted) { + this._prettyPrintButton.setAttribute("checked", true); + } else { + this._prettyPrintButton.removeAttribute("checked"); + } + }, + + /** + * Toggle the pretty printing of the selected source. + */ + togglePrettyPrint: Task.async(function*() { + if (this._prettyPrintButton.hasAttribute("disabled")) { + return; + } + + const resetEditor = ([{ actor }]) => { + // Only set the text when the source is still selected. + if (actor == this.selectedValue) { + DebuggerView.setEditorLocation(actor, 0, { force: true }); + } + }; + + const printError = ([{ url }, error]) => { + DevToolsUtils.reportException("togglePrettyPrint", error); + }; + + DebuggerView.showProgressBar(); + const { source } = this.selectedItem.attachment; + const sourceClient = gThreadClient.source(source); + const shouldPrettyPrint = !sourceClient.isPrettyPrinted; + + if (shouldPrettyPrint) { + this._prettyPrintButton.setAttribute("checked", true); + } else { + this._prettyPrintButton.removeAttribute("checked"); + } + + try { + let resolution = yield DebuggerController.SourceScripts.togglePrettyPrint(source); + resetEditor(resolution); + } catch (rejection) { + printError(rejection); + } + + DebuggerView.showEditor(); + this.updateToolbarButtonsState(); + }), + + /** + * Toggle the black boxed state of the selected source. + */ + toggleBlackBoxing: Task.async(function*() { + const { source } = this.selectedItem.attachment; + const sourceClient = gThreadClient.source(source); + const shouldBlackBox = !sourceClient.isBlackBoxed; + + // Be optimistic that the (un-)black boxing will succeed, so enable/disable + // the pretty print button and check/uncheck the black box button immediately. + // Then, once we actually get the results from the server, make sure that + // it is in the correct state again by calling `updateToolbarButtonsState`. + + if (shouldBlackBox) { + this._prettyPrintButton.setAttribute("disabled", true); + this._blackBoxButton.setAttribute("checked", true); + } else { + this._prettyPrintButton.removeAttribute("disabled"); + this._blackBoxButton.removeAttribute("checked"); + } + + try { + yield DebuggerController.SourceScripts.setBlackBoxing(source, shouldBlackBox); + } catch (e) { + // Continue execution in this task even if blackboxing failed. + } + + this.updateToolbarButtonsState(); + }), + + /** + * Toggles all breakpoints enabled/disabled. + */ + toggleBreakpoints: function() { + let breakpoints = this.getAllBreakpoints(); + let hasBreakpoints = breakpoints.length > 0; + let hasEnabledBreakpoints = breakpoints.some(e => !e.attachment.disabled); + + if (hasBreakpoints && hasEnabledBreakpoints) { + this._toggleBreakpointsButton.setAttribute("checked", true); + this._onDisableAll(); + } else { + this._toggleBreakpointsButton.removeAttribute("checked"); + this._onEnableAll(); + } + }, + + hidePrettyPrinting: function() { + this._prettyPrintButton.style.display = 'none'; + + if (this._blackBoxButton.style.display === 'none') { + let sep = document.querySelector('#sources-toolbar .devtools-separator'); + sep.style.display = 'none'; + } + }, + + hideBlackBoxing: function() { + this._blackBoxButton.style.display = 'none'; + + if (this._prettyPrintButton.style.display === 'none') { + let sep = document.querySelector('#sources-toolbar .devtools-separator'); + sep.style.display = 'none'; + } + }, + + /** + * Look up a source actor id for a location. This is necessary for + * backwards compatibility; otherwise we could just use the `actor` + * property. Older servers don't use the same actor ids for sources + * across reloads, so we resolve a url to the current actor if a url + * exists. + * + * @param object aLocation + * An object with the following properties: + * - actor: the source actor id + * - url: a url (might be null) + */ + getActorForLocation: function(aLocation) { + if (aLocation.url) { + for (var item of this) { + let source = item.attachment.source; + + if (aLocation.url === source.url) { + return source.actor; + } + } + } + return aLocation.actor; + }, + + /** + * Marks a breakpoint as selected in this sources container. + * + * @param object aItem + * The breakpoint item to select. + */ + _selectBreakpoint: function(aItem) { + if (this._selectedBreakpointItem == aItem) { + return; + } + this._unselectBreakpoint(); + + this._selectedBreakpointItem = aItem; + this._selectedBreakpointItem.target.classList.add("selected"); + + // Ensure the currently selected breakpoint is visible. + this.widget.ensureElementIsVisible(aItem.target); + }, + + /** + * Marks the current breakpoint as unselected in this sources container. + */ + _unselectBreakpoint: function() { + if (!this._selectedBreakpointItem) { + return; + } + this._selectedBreakpointItem.target.classList.remove("selected"); + this._selectedBreakpointItem = null; + }, + + /** + * Opens a conditional breakpoint's expression input popup. + */ + _openConditionalPopup: function() { + let breakpointItem = this._selectedBreakpointItem; + let attachment = breakpointItem.attachment; + // Check if this is an enabled conditional breakpoint, and if so, + // retrieve the current conditional epression. + let breakpointPromise = DebuggerController.Breakpoints._getAdded(attachment); + if (breakpointPromise) { + breakpointPromise.then(aBreakpointClient => { + let isConditionalBreakpoint = aBreakpointClient.hasCondition(); + let condition = aBreakpointClient.getCondition(); + doOpen.call(this, isConditionalBreakpoint ? condition : "") + }); + } else { + doOpen.call(this, "") + } + + function doOpen(aConditionalExpression) { + // Update the conditional expression textbox. If no expression was + // previously set, revert to using an empty string by default. + this._cbTextbox.value = aConditionalExpression; + + // Show the conditional expression panel. The popup arrow should be pointing + // at the line number node in the breakpoint item view. + this._cbPanel.hidden = false; + this._cbPanel.openPopup(breakpointItem.attachment.view.lineNumber, + BREAKPOINT_CONDITIONAL_POPUP_POSITION, + BREAKPOINT_CONDITIONAL_POPUP_OFFSET_X, + BREAKPOINT_CONDITIONAL_POPUP_OFFSET_Y); + } + }, + + /** + * Hides a conditional breakpoint's expression input popup. + */ + _hideConditionalPopup: function() { + this._cbPanel.hidden = true; + + // Sometimes this._cbPanel doesn't have hidePopup method which doesn't + // break anything but simply outputs an exception to the console. + if (this._cbPanel.hidePopup) { + this._cbPanel.hidePopup(); + } + }, + + /** + * Customization function for creating a breakpoint item's UI. + * + * @param object aOptions + * A couple of options or flags supported by this operation: + * - location: the breakpoint's source location and line number + * - disabled: the breakpoint's disabled state, boolean + * - text: the breakpoint's line text to be displayed + * @return object + * An object containing the breakpoint container, checkbox, + * line number and line text nodes. + */ + _createBreakpointView: function(aOptions) { + let { location, disabled, text } = aOptions; + let identifier = DebuggerController.Breakpoints.getIdentifier(location); + + let checkbox = document.createElement("checkbox"); + checkbox.setAttribute("checked", !disabled); + checkbox.className = "dbg-breakpoint-checkbox"; + + let lineNumberNode = document.createElement("label"); + lineNumberNode.className = "plain dbg-breakpoint-line"; + lineNumberNode.setAttribute("value", location.line); + + let lineTextNode = document.createElement("label"); + lineTextNode.className = "plain dbg-breakpoint-text"; + lineTextNode.setAttribute("value", text); + lineTextNode.setAttribute("crop", "end"); + lineTextNode.setAttribute("flex", "1"); + + let tooltip = text.substr(0, BREAKPOINT_LINE_TOOLTIP_MAX_LENGTH); + lineTextNode.setAttribute("tooltiptext", tooltip); + + let container = document.createElement("hbox"); + container.id = "breakpoint-" + identifier; + container.className = "dbg-breakpoint side-menu-widget-item-other"; + container.classList.add("devtools-monospace"); + container.setAttribute("align", "center"); + container.setAttribute("flex", "1"); + + container.addEventListener("click", this._onBreakpointClick, false); + checkbox.addEventListener("click", this._onBreakpointCheckboxClick, false); + + container.appendChild(checkbox); + container.appendChild(lineNumberNode); + container.appendChild(lineTextNode); + + return { + container: container, + checkbox: checkbox, + lineNumber: lineNumberNode, + lineText: lineTextNode + }; + }, + + /** + * Creates a context menu for a breakpoint element. + * + * @param object aOptions + * A couple of options or flags supported by this operation: + * - location: the breakpoint's source location and line number + * - disabled: the breakpoint's disabled state, boolean + * @return object + * An object containing the breakpoint commandset and menu popup ids. + */ + _createContextMenu: function(aOptions) { + let { location, disabled } = aOptions; + let identifier = DebuggerController.Breakpoints.getIdentifier(location); + + let commandset = document.createElement("commandset"); + let menupopup = document.createElement("menupopup"); + commandset.id = "bp-cSet-" + identifier; + menupopup.id = "bp-mPop-" + identifier; + + createMenuItem.call(this, "enableSelf", !disabled); + createMenuItem.call(this, "disableSelf", disabled); + createMenuItem.call(this, "deleteSelf"); + createMenuSeparator(); + createMenuItem.call(this, "setConditional"); + createMenuSeparator(); + createMenuItem.call(this, "enableOthers"); + createMenuItem.call(this, "disableOthers"); + createMenuItem.call(this, "deleteOthers"); + createMenuSeparator(); + createMenuItem.call(this, "enableAll"); + createMenuItem.call(this, "disableAll"); + createMenuSeparator(); + createMenuItem.call(this, "deleteAll"); + + this._popupset.appendChild(menupopup); + this._commandset.appendChild(commandset); + + return { + commandsetId: commandset.id, + menupopupId: menupopup.id + }; + + /** + * Creates a menu item specified by a name with the appropriate attributes + * (label and handler). + * + * @param string aName + * A global identifier for the menu item. + * @param boolean aHiddenFlag + * True if this menuitem should be hidden. + */ + function createMenuItem(aName, aHiddenFlag) { + let menuitem = document.createElement("menuitem"); + let command = document.createElement("command"); + + let prefix = "bp-cMenu-"; // "breakpoints context menu" + let commandId = prefix + aName + "-" + identifier + "-command"; + let menuitemId = prefix + aName + "-" + identifier + "-menuitem"; + + let label = L10N.getStr("breakpointMenuItem." + aName); + let func = "_on" + aName.charAt(0).toUpperCase() + aName.slice(1); + + command.id = commandId; + command.setAttribute("label", label); + command.addEventListener("command", () => this[func](location), false); + + menuitem.id = menuitemId; + menuitem.setAttribute("command", commandId); + aHiddenFlag && menuitem.setAttribute("hidden", "true"); + + commandset.appendChild(command); + menupopup.appendChild(menuitem); + } + + /** + * Creates a simple menu separator element and appends it to the current + * menupopup hierarchy. + */ + function createMenuSeparator() { + let menuseparator = document.createElement("menuseparator"); + menupopup.appendChild(menuseparator); + } + }, + + /** + * Function called each time a breakpoint item is removed. + * + * @param object aItem + * The corresponding item. + */ + _onBreakpointRemoved: function(aItem) { + dumpn("Finalizing breakpoint item: " + aItem.stringify()); + + // Destroy the context menu for the breakpoint. + let contextMenu = aItem.attachment.popup; + document.getElementById(contextMenu.commandsetId).remove(); + document.getElementById(contextMenu.menupopupId).remove(); + + // Clear the breakpoint selection. + if (this._selectedBreakpointItem == aItem) { + this._selectedBreakpointItem = null; + } + }, + + /** + * The load listener for the source editor. + */ + _onEditorLoad: function(aName, aEditor) { + aEditor.on("cursorActivity", this._onEditorCursorActivity); + }, + + /** + * The unload listener for the source editor. + */ + _onEditorUnload: function(aName, aEditor) { + aEditor.off("cursorActivity", this._onEditorCursorActivity); + }, + + /** + * The selection listener for the source editor. + */ + _onEditorCursorActivity: function(e) { + let editor = DebuggerView.editor; + let start = editor.getCursor("start").line + 1; + let end = editor.getCursor().line + 1; + let actor = this.selectedValue; + + let location = { actor: actor, line: start }; + + if (this.getBreakpoint(location) && start == end) { + this.highlightBreakpoint(location, { noEditorUpdate: true }); + } else { + this.unhighlightBreakpoint(); + } + }, + + /** + * The select listener for the sources container. + */ + _onSourceSelect: Task.async(function*({ detail: sourceItem }) { + if (!sourceItem) { + return; + } + const { source } = sourceItem.attachment; + const sourceClient = gThreadClient.source(source); + + // The container is not empty and an actual item was selected. + DebuggerView.setEditorLocation(sourceItem.value); + + // Attempt to automatically pretty print minified source code. + if (Prefs.autoPrettyPrint && !sourceClient.isPrettyPrinted) { + let isMinified = yield SourceUtils.isMinified(sourceClient); + if (isMinified) { + this.togglePrettyPrint(); + } + } + + // Set window title. No need to split the url by " -> " here, because it was + // already sanitized when the source was added. + document.title = L10N.getFormatStr("DebuggerWindowScriptTitle", + sourceItem.attachment.source.url); + + DebuggerView.maybeShowBlackBoxMessage(); + this.updateToolbarButtonsState(); + }), + + /** + * The click listener for the "stop black boxing" button. + */ + _onStopBlackBoxing: Task.async(function*() { + const { source } = this.selectedItem.attachment; + + try { + yield DebuggerController.SourceScripts.setBlackBoxing(source, false); + } catch (e) { + // Continue execution in this task even if blackboxing failed. + } + + this.updateToolbarButtonsState(); + }), + + /** + * The click listener for a breakpoint container. + */ + _onBreakpointClick: function(e) { + let sourceItem = this.getItemForElement(e.target); + let breakpointItem = this.getItemForElement.call(sourceItem, e.target); + let attachment = breakpointItem.attachment; + + // Check if this is an enabled conditional breakpoint. + let breakpointPromise = DebuggerController.Breakpoints._getAdded(attachment); + if (breakpointPromise) { + breakpointPromise.then(aBreakpointClient => { + doHighlight.call(this, aBreakpointClient.hasCondition()); + }); + } else { + doHighlight.call(this, false); + } + + function doHighlight(aConditionalBreakpointFlag) { + // Highlight the breakpoint in this pane and in the editor. + this.highlightBreakpoint(attachment, { + // Don't show the conditional expression popup if this is not a + // conditional breakpoint, or the right mouse button was pressed (to + // avoid clashing the popup with the context menu). + openPopup: aConditionalBreakpointFlag && e.button == 0 + }); + } + }, + + /** + * The click listener for a breakpoint checkbox. + */ + _onBreakpointCheckboxClick: function(e) { + let sourceItem = this.getItemForElement(e.target); + let breakpointItem = this.getItemForElement.call(sourceItem, e.target); + let attachment = breakpointItem.attachment; + + // Toggle the breakpoint enabled or disabled. + this[attachment.disabled ? "enableBreakpoint" : "disableBreakpoint"](attachment, { + // Do this silently (don't update the checkbox checked state), since + // this listener is triggered because a checkbox was already clicked. + silent: true + }); + + // Don't update the editor location (avoid propagating into _onBreakpointClick). + e.preventDefault(); + e.stopPropagation(); + }, + + /** + * The popup showing listener for the breakpoints conditional expression panel. + */ + _onConditionalPopupShowing: function() { + this._conditionalPopupVisible = true; // Used in tests. + window.emit(EVENTS.CONDITIONAL_BREAKPOINT_POPUP_SHOWING); + }, + + /** + * The popup shown listener for the breakpoints conditional expression panel. + */ + _onConditionalPopupShown: function() { + this._cbTextbox.focus(); + this._cbTextbox.select(); + }, + + /** + * The popup hiding listener for the breakpoints conditional expression panel. + */ + _onConditionalPopupHiding: Task.async(function*() { + this._conditionalPopupVisible = false; // Used in tests. + + let breakpointItem = this._selectedBreakpointItem; + let attachment = breakpointItem.attachment; + + // Check if this is an enabled conditional breakpoint, and if so, + // save the current conditional epression. + let breakpointPromise = DebuggerController.Breakpoints._getAdded(attachment); + if (breakpointPromise) { + let { location } = yield breakpointPromise; + let condition = this._cbTextbox.value; + yield DebuggerController.Breakpoints.updateCondition(location, condition); + } + + window.emit(EVENTS.CONDITIONAL_BREAKPOINT_POPUP_HIDING); + }), + + /** + * The keypress listener for the breakpoints conditional expression textbox. + */ + _onConditionalTextboxKeyPress: function(e) { + if (e.keyCode == e.DOM_VK_RETURN) { + this._hideConditionalPopup(); + } + }, + + /** + * Called when the add breakpoint key sequence was pressed. + */ + _onCmdAddBreakpoint: function(e) { + let actor = DebuggerView.Sources.selectedValue; + let line = (e && e.sourceEvent.target.tagName == 'menuitem' ? + DebuggerView.clickedLine + 1 : + DebuggerView.editor.getCursor().line + 1); + let location = { actor, line }; + let breakpointItem = this.getBreakpoint(location); + + // If a breakpoint already existed, remove it now. + if (breakpointItem) { + DebuggerController.Breakpoints.removeBreakpoint(location); + } + // No breakpoint existed at the required location, add one now. + else { + DebuggerController.Breakpoints.addBreakpoint(location); + } + }, + + /** + * Called when the add conditional breakpoint key sequence was pressed. + */ + _onCmdAddConditionalBreakpoint: function(e) { + let actor = DebuggerView.Sources.selectedValue; + let line = (e && e.sourceEvent.target.tagName == 'menuitem' ? + DebuggerView.clickedLine + 1 : + DebuggerView.editor.getCursor().line + 1); + let location = { actor, line }; + let breakpointItem = this.getBreakpoint(location); + + // If a breakpoint already existed or wasn't a conditional, morph it now. + if (breakpointItem) { + this.highlightBreakpoint(location, { openPopup: true }); + } + // No breakpoint existed at the required location, add one now. + else { + DebuggerController.Breakpoints.addBreakpoint(location, { openPopup: true }); + } + }, + + /** + * Function invoked on the "setConditional" menuitem command. + * + * @param object aLocation + * @see DebuggerController.Breakpoints.addBreakpoint + */ + _onSetConditional: function(aLocation) { + // Highlight the breakpoint and show a conditional expression popup. + this.highlightBreakpoint(aLocation, { openPopup: true }); + }, + + /** + * Function invoked on the "enableSelf" menuitem command. + * + * @param object aLocation + * @see DebuggerController.Breakpoints.addBreakpoint + */ + _onEnableSelf: function(aLocation) { + // Enable the breakpoint, in this container and the controller store. + this.enableBreakpoint(aLocation); + }, + + /** + * Function invoked on the "disableSelf" menuitem command. + * + * @param object aLocation + * @see DebuggerController.Breakpoints.addBreakpoint + */ + _onDisableSelf: function(aLocation) { + // Disable the breakpoint, in this container and the controller store. + this.disableBreakpoint(aLocation); + }, + + /** + * Function invoked on the "deleteSelf" menuitem command. + * + * @param object aLocation + * @see DebuggerController.Breakpoints.addBreakpoint + */ + _onDeleteSelf: function(aLocation) { + // Remove the breakpoint, from this container and the controller store. + this.removeBreakpoint(aLocation); + DebuggerController.Breakpoints.removeBreakpoint(aLocation); + }, + + /** + * Function invoked on the "enableOthers" menuitem command. + * + * @param object aLocation + * @see DebuggerController.Breakpoints.addBreakpoint + */ + _onEnableOthers: function(aLocation) { + let enableOthers = aCallback => { + let other = this.getOtherBreakpoints(aLocation); + let outstanding = other.map(e => this.enableBreakpoint(e.attachment)); + promise.all(outstanding).then(aCallback); + } + + // Breakpoints can only be set while the debuggee is paused. To avoid + // an avalanche of pause/resume interrupts of the main thread, simply + // pause it beforehand if it's not already. + if (gThreadClient.state != "paused") { + gThreadClient.interrupt(() => enableOthers(() => gThreadClient.resume())); + } else { + enableOthers(); + } + }, + + /** + * Function invoked on the "disableOthers" menuitem command. + * + * @param object aLocation + * @see DebuggerController.Breakpoints.addBreakpoint + */ + _onDisableOthers: function(aLocation) { + let other = this.getOtherBreakpoints(aLocation); + other.forEach(e => this._onDisableSelf(e.attachment)); + }, + + /** + * Function invoked on the "deleteOthers" menuitem command. + * + * @param object aLocation + * @see DebuggerController.Breakpoints.addBreakpoint + */ + _onDeleteOthers: function(aLocation) { + let other = this.getOtherBreakpoints(aLocation); + other.forEach(e => this._onDeleteSelf(e.attachment)); + }, + + /** + * Function invoked on the "enableAll" menuitem command. + */ + _onEnableAll: function() { + this._onEnableOthers(undefined); + }, + + /** + * Function invoked on the "disableAll" menuitem command. + */ + _onDisableAll: function() { + this._onDisableOthers(undefined); + }, + + /** + * Function invoked on the "deleteAll" menuitem command. + */ + _onDeleteAll: function() { + this._onDeleteOthers(undefined); + }, + + _commandset: null, + _popupset: null, + _cmPopup: null, + _cbPanel: null, + _cbTextbox: null, + _selectedBreakpointItem: null, + _conditionalPopupVisible: false +}); + +/** + * Functions handling the traces UI. + */ +function TracerView() { + this._selectedItem = null; + this._matchingItems = null; + this.widget = null; + + this._highlightItem = this._highlightItem.bind(this); + this._isNotSelectedItem = this._isNotSelectedItem.bind(this); + + this._unhighlightMatchingItems = + DevToolsUtils.makeInfallible(this._unhighlightMatchingItems.bind(this)); + this._onToggleTracing = + DevToolsUtils.makeInfallible(this._onToggleTracing.bind(this)); + this._onStartTracing = + DevToolsUtils.makeInfallible(this._onStartTracing.bind(this)); + this._onClear = + DevToolsUtils.makeInfallible(this._onClear.bind(this)); + this._onSelect = + DevToolsUtils.makeInfallible(this._onSelect.bind(this)); + this._onMouseOver = + DevToolsUtils.makeInfallible(this._onMouseOver.bind(this)); + this._onSearch = + DevToolsUtils.makeInfallible(this._onSearch.bind(this)); +} + +TracerView.MAX_TRACES = 200; + +TracerView.prototype = Heritage.extend(WidgetMethods, { + /** + * Initialization function, called when the debugger is started. + */ + initialize: function() { + dumpn("Initializing the TracerView"); + + this._traceButton = document.getElementById("trace"); + this._tracerTab = document.getElementById("tracer-tab"); + + // Remove tracer related elements from the dom and tear everything down if + // the tracer isn't enabled. + if (!Prefs.tracerEnabled) { + this._traceButton.remove(); + this._traceButton = null; + this._tracerTab.remove(); + this._tracerTab = null; + return; + } + + this.widget = new FastListWidget(document.getElementById("tracer-traces")); + this._traceButton.removeAttribute("hidden"); + this._tracerTab.removeAttribute("hidden"); + + this._search = document.getElementById("tracer-search"); + this._template = document.getElementsByClassName("trace-item-template")[0]; + this._templateItem = this._template.getElementsByClassName("trace-item")[0]; + this._templateTypeIcon = this._template.getElementsByClassName("trace-type")[0]; + this._templateNameNode = this._template.getElementsByClassName("trace-name")[0]; + + this.widget.addEventListener("select", this._onSelect, false); + this.widget.addEventListener("mouseover", this._onMouseOver, false); + this.widget.addEventListener("mouseout", this._unhighlightMatchingItems, false); + this._search.addEventListener("input", this._onSearch, false); + + this._startTooltip = L10N.getStr("startTracingTooltip"); + this._stopTooltip = L10N.getStr("stopTracingTooltip"); + this._tracingNotStartedString = L10N.getStr("tracingNotStartedText"); + this._noFunctionCallsString = L10N.getStr("noFunctionCallsText"); + + this._traceButton.setAttribute("tooltiptext", this._startTooltip); + this.emptyText = this._tracingNotStartedString; + }, + + /** + * Destruction function, called when the debugger is closed. + */ + destroy: function() { + dumpn("Destroying the TracerView"); + + if (!this.widget) { + return; + } + + this.widget.removeEventListener("select", this._onSelect, false); + this.widget.removeEventListener("mouseover", this._onMouseOver, false); + this.widget.removeEventListener("mouseout", this._unhighlightMatchingItems, false); + this._search.removeEventListener("input", this._onSearch, false); + }, + + /** + * Function invoked by the "toggleTracing" command to switch the tracer state. + */ + _onToggleTracing: function() { + if (DebuggerController.Tracer.tracing) { + this._onStopTracing(); + } else { + this._onStartTracing(); + } + }, + + /** + * Function invoked either by the "startTracing" command or by + * _onToggleTracing to start execution tracing in the backend. + * + * @return object + * A promise resolved once the tracing has successfully started. + */ + _onStartTracing: function() { + this._traceButton.setAttribute("checked", true); + this._traceButton.setAttribute("tooltiptext", this._stopTooltip); + + this.empty(); + this.emptyText = this._noFunctionCallsString; + + let deferred = promise.defer(); + DebuggerController.Tracer.startTracing(deferred.resolve); + return deferred.promise; + }, + + /** + * Function invoked by _onToggleTracing to stop execution tracing in the + * backend. + * + * @return object + * A promise resolved once the tracing has successfully stopped. + */ + _onStopTracing: function() { + this._traceButton.removeAttribute("checked"); + this._traceButton.setAttribute("tooltiptext", this._startTooltip); + + this.emptyText = this._tracingNotStartedString; + + let deferred = promise.defer(); + DebuggerController.Tracer.stopTracing(deferred.resolve); + return deferred.promise; + }, + + /** + * Function invoked by the "clearTraces" command to empty the traces pane. + */ + _onClear: function() { + this.empty(); + }, + + /** + * Populate the given parent scope with the variable with the provided name + * and value. + * + * @param String aName + * The name of the variable. + * @param Object aParent + * The parent scope. + * @param Object aValue + * The value of the variable. + */ + _populateVariable: function(aName, aParent, aValue) { + let item = aParent.addItem(aName, { value: aValue }); + + if (aValue) { + let wrappedValue = new DebuggerController.Tracer.WrappedObject(aValue); + DebuggerView.Variables.controller.populate(item, wrappedValue); + item.expand(); + item.twisty = false; + } + }, + + /** + * Handler for the widget's "select" event. Displays parameters, exception, or + * return value depending on whether the selected trace is a call, throw, or + * return respectively. + * + * @param Object traceItem + * The selected trace item. + */ + _onSelect: function _onSelect({ detail: traceItem }) { + if (!traceItem) { + return; + } + + const data = traceItem.attachment.trace; + const { location: { url, line } } = data; + DebuggerView.setEditorLocation( + DebuggerView.Sources.getActorForLocation({ url }), + line, + { noDebug: true } + ); + + DebuggerView.Variables.empty(); + const scope = DebuggerView.Variables.addScope(); + + if (data.type == "call") { + const params = DevToolsUtils.zip(data.parameterNames, data.arguments); + for (let [name, val] of params) { + if (val === undefined) { + scope.addItem(name, { value: "<value not available>" }); + } else { + this._populateVariable(name, scope, val); + } + } + } else { + const varName = "<" + (data.type == "throw" ? "exception" : data.type) + ">"; + this._populateVariable(varName, scope, data.returnVal); + } + + scope.expand(); + DebuggerView.showInstrumentsPane(); + }, + + /** + * Add the hover frame enter/exit highlighting to a given item. + */ + _highlightItem: function(aItem) { + if (!aItem || !aItem.target) { + return; + } + const trace = aItem.target.querySelector(".trace-item"); + trace.classList.add("selected-matching"); + }, + + /** + * Remove the hover frame enter/exit highlighting to a given item. + */ + _unhighlightItem: function(aItem) { + if (!aItem || !aItem.target) { + return; + } + const match = aItem.target.querySelector(".selected-matching"); + if (match) { + match.classList.remove("selected-matching"); + } + }, + + /** + * Remove the frame enter/exit pair highlighting we do when hovering. + */ + _unhighlightMatchingItems: function() { + if (this._matchingItems) { + this._matchingItems.forEach(this._unhighlightItem); + this._matchingItems = null; + } + }, + + /** + * Returns true if the given item is not the selected item. + */ + _isNotSelectedItem: function(aItem) { + return aItem !== this.selectedItem; + }, + + /** + * Highlight the frame enter/exit pair of items for the given item. + */ + _highlightMatchingItems: function(aItem) { + const frameId = aItem.attachment.trace.frameId; + const predicate = e => e.attachment.trace.frameId == frameId; + + this._unhighlightMatchingItems(); + this._matchingItems = this.items.filter(predicate); + this._matchingItems + .filter(this._isNotSelectedItem) + .forEach(this._highlightItem); + }, + + /** + * Listener for the mouseover event. + */ + _onMouseOver: function({ target }) { + const traceItem = this.getItemForElement(target); + if (traceItem) { + this._highlightMatchingItems(traceItem); + } + }, + + /** + * Listener for typing in the search box. + */ + _onSearch: function() { + const query = this._search.value.trim().toLowerCase(); + const predicate = name => name.toLowerCase().contains(query); + this.filterContents(item => predicate(item.attachment.trace.name)); + }, + + /** + * Select the traces tab in the sidebar. + */ + selectTab: function() { + const tabs = this._tracerTab.parentElement; + tabs.selectedIndex = Array.indexOf(tabs.children, this._tracerTab); + }, + + /** + * Commit all staged items to the widget. Overridden so that we can call + * |FastListWidget.prototype.flush|. + */ + commit: function() { + WidgetMethods.commit.call(this); + // TODO: Accessing non-standard widget properties. Figure out what's the + // best way to expose such things. Bug 895514. + this.widget.flush(); + }, + + /** + * Adds the trace record provided as an argument to the view. + * + * @param object aTrace + * The trace record coming from the tracer actor. + */ + addTrace: function(aTrace) { + // Create the element node for the trace item. + let view = this._createView(aTrace); + + // Append a source item to this container. + this.push([view], { + staged: true, + attachment: { + trace: aTrace + } + }); + }, + + /** + * Customization function for creating an item's UI. + * + * @return nsIDOMNode + * The network request view. + */ + _createView: function(aTrace) { + let { type, name, location, blackBoxed, depth, frameId } = aTrace; + let { parameterNames, returnVal, arguments: args } = aTrace; + let fragment = document.createDocumentFragment(); + + this._templateItem.classList.toggle("black-boxed", blackBoxed); + this._templateItem.setAttribute("tooltiptext", SourceUtils.trimUrl(location.url)); + this._templateItem.style.MozPaddingStart = depth + "em"; + + const TYPES = ["call", "yield", "return", "throw"]; + for (let t of TYPES) { + this._templateTypeIcon.classList.toggle("trace-" + t, t == type); + } + this._templateTypeIcon.setAttribute("value", { + call: "\u2192", + yield: "Y", + return: "\u2190", + throw: "E", + terminated: "TERMINATED" + }[type]); + + this._templateNameNode.setAttribute("value", name); + + // All extra syntax and parameter nodes added. + const addedNodes = []; + + if (parameterNames) { + const syntax = (p) => { + const el = document.createElement("label"); + el.setAttribute("value", p); + el.classList.add("trace-syntax"); + el.classList.add("plain"); + addedNodes.push(el); + return el; + }; + + this._templateItem.appendChild(syntax("(")); + + for (let i = 0, n = parameterNames.length; i < n; i++) { + let param = document.createElement("label"); + param.setAttribute("value", parameterNames[i]); + param.classList.add("trace-param"); + param.classList.add("plain"); + addedNodes.push(param); + this._templateItem.appendChild(param); + + if (i + 1 !== n) { + this._templateItem.appendChild(syntax(", ")); + } + } + + this._templateItem.appendChild(syntax(")")); + } + + // Flatten the DOM by removing one redundant box (the template container). + for (let node of this._template.childNodes) { + fragment.appendChild(node.cloneNode(true)); + } + + // Remove any added nodes from the template. + for (let node of addedNodes) { + this._templateItem.removeChild(node); + } + + return fragment; + } +}); + +/** + * Utility functions for handling sources. + */ +let SourceUtils = { + _labelsCache: new Map(), // Can't use WeakMaps because keys are strings. + _groupsCache: new Map(), + _minifiedCache: new WeakMap(), + + /** + * Returns true if the specified url and/or content type are specific to + * javascript files. + * + * @return boolean + * True if the source is likely javascript. + */ + isJavaScript: function(aUrl, aContentType = "") { + return (aUrl && /\.jsm?$/.test(this.trimUrlQuery(aUrl))) || + aContentType.contains("javascript"); + }, + + /** + * Determines if the source text is minified by using + * the percentage indented of a subset of lines + * + * @return object + * A promise that resolves to true if source text is minified. + */ + isMinified: Task.async(function*(sourceClient) { + if (this._minifiedCache.has(sourceClient)) { + return this._minifiedCache.get(sourceClient); + } + + let [, text] = yield DebuggerController.SourceScripts.getText(sourceClient); + let isMinified; + let lineEndIndex = 0; + let lineStartIndex = 0; + let lines = 0; + let indentCount = 0; + let overCharLimit = false; + + // Strip comments. + text = text.replace(/\/\*[\S\s]*?\*\/|\/\/(.+|\n)/g, ""); + + while (lines++ < SAMPLE_SIZE) { + lineEndIndex = text.indexOf("\n", lineStartIndex); + if (lineEndIndex == -1) { + break; + } + if (/^\s+/.test(text.slice(lineStartIndex, lineEndIndex))) { + indentCount++; + } + // For files with no indents but are not minified. + if ((lineEndIndex - lineStartIndex) > CHARACTER_LIMIT) { + overCharLimit = true; + break; + } + lineStartIndex = lineEndIndex + 1; + } + + isMinified = + ((indentCount / lines) * 100) < INDENT_COUNT_THRESHOLD || overCharLimit; + + this._minifiedCache.set(sourceClient, isMinified); + return isMinified; + }), + + /** + * Clears the labels, groups and minify cache, populated by methods like + * SourceUtils.getSourceLabel or Source Utils.getSourceGroup. + * This should be done every time the content location changes. + */ + clearCache: function() { + this._labelsCache.clear(); + this._groupsCache.clear(); + this._minifiedCache.clear(); + }, + + /** + * Gets a unique, simplified label from a source url. + * + * @param string aUrl + * The source url. + * @return string + * The simplified label. + */ + getSourceLabel: function(aUrl) { + let cachedLabel = this._labelsCache.get(aUrl); + if (cachedLabel) { + return cachedLabel; + } + + let sourceLabel = null; + + for (let name of Object.keys(KNOWN_SOURCE_GROUPS)) { + if (aUrl.startsWith(KNOWN_SOURCE_GROUPS[name])) { + sourceLabel = aUrl.substring(KNOWN_SOURCE_GROUPS[name].length); + } + } + + if (!sourceLabel) { + sourceLabel = this.trimUrl(aUrl); + } + + let unicodeLabel = NetworkHelper.convertToUnicode(unescape(sourceLabel)); + this._labelsCache.set(aUrl, unicodeLabel); + return unicodeLabel; + }, + + /** + * Gets as much information as possible about the hostname and directory paths + * of an url to create a short url group identifier. + * + * @param string aUrl + * The source url. + * @return string + * The simplified group. + */ + getSourceGroup: function(aUrl) { + let cachedGroup = this._groupsCache.get(aUrl); + if (cachedGroup) { + return cachedGroup; + } + + try { + // Use an nsIURL to parse all the url path parts. + var uri = Services.io.newURI(aUrl, null, null).QueryInterface(Ci.nsIURL); + } catch (e) { + // This doesn't look like a url, or nsIURL can't handle it. + return ""; + } + + let groupLabel = uri.prePath; + + for (let name of Object.keys(KNOWN_SOURCE_GROUPS)) { + if (aUrl.startsWith(KNOWN_SOURCE_GROUPS[name])) { + groupLabel = name; + } + } + + let unicodeLabel = NetworkHelper.convertToUnicode(unescape(groupLabel)); + this._groupsCache.set(aUrl, unicodeLabel) + return unicodeLabel; + }, + + /** + * Trims the url by shortening it if it exceeds a certain length, adding an + * ellipsis at the end. + * + * @param string aUrl + * The source url. + * @param number aLength [optional] + * The expected source url length. + * @param number aSection [optional] + * The section to trim. Supported values: "start", "center", "end" + * @return string + * The shortened url. + */ + trimUrlLength: function(aUrl, aLength, aSection) { + aLength = aLength || SOURCE_URL_DEFAULT_MAX_LENGTH; + aSection = aSection || "end"; + + if (aUrl.length > aLength) { + switch (aSection) { + case "start": + return L10N.ellipsis + aUrl.slice(-aLength); + break; + case "center": + return aUrl.substr(0, aLength / 2 - 1) + L10N.ellipsis + aUrl.slice(-aLength / 2 + 1); + break; + case "end": + return aUrl.substr(0, aLength) + L10N.ellipsis; + break; + } + } + return aUrl; + }, + + /** + * Trims the query part or reference identifier of a url string, if necessary. + * + * @param string aUrl + * The source url. + * @return string + * The shortened url. + */ + trimUrlQuery: function(aUrl) { + let length = aUrl.length; + let q1 = aUrl.indexOf('?'); + let q2 = aUrl.indexOf('&'); + let q3 = aUrl.indexOf('#'); + let q = Math.min(q1 != -1 ? q1 : length, + q2 != -1 ? q2 : length, + q3 != -1 ? q3 : length); + + return aUrl.slice(0, q); + }, + + /** + * Trims as much as possible from a url, while keeping the label unique + * in the sources container. + * + * @param string | nsIURL aUrl + * The source url. + * @param string aLabel [optional] + * The resulting label at each step. + * @param number aSeq [optional] + * The current iteration step. + * @return string + * The resulting label at the final step. + */ + trimUrl: function(aUrl, aLabel, aSeq) { + if (!(aUrl instanceof Ci.nsIURL)) { + try { + // Use an nsIURL to parse all the url path parts. + aUrl = Services.io.newURI(aUrl, null, null).QueryInterface(Ci.nsIURL); + } catch (e) { + // This doesn't look like a url, or nsIURL can't handle it. + return aUrl; + } + } + if (!aSeq) { + let name = aUrl.fileName; + if (name) { + // This is a regular file url, get only the file name (contains the + // base name and extension if available). + + // If this url contains an invalid query, unfortunately nsIURL thinks + // it's part of the file extension. It must be removed. + aLabel = aUrl.fileName.replace(/\&.*/, ""); + } else { + // This is not a file url, hence there is no base name, nor extension. + // Proceed using other available information. + aLabel = ""; + } + aSeq = 1; + } + + // If we have a label and it doesn't only contain a query... + if (aLabel && aLabel.indexOf("?") != 0) { + // A page may contain multiple requests to the same url but with different + // queries. It is *not* redundant to show each one. + if (!DebuggerView.Sources.getItemForAttachment(e => e.label == aLabel)) { + return aLabel; + } + } + + // Append the url query. + if (aSeq == 1) { + let query = aUrl.query; + if (query) { + return this.trimUrl(aUrl, aLabel + "?" + query, aSeq + 1); + } + aSeq++; + } + // Append the url reference. + if (aSeq == 2) { + let ref = aUrl.ref; + if (ref) { + return this.trimUrl(aUrl, aLabel + "#" + aUrl.ref, aSeq + 1); + } + aSeq++; + } + // Prepend the url directory. + if (aSeq == 3) { + let dir = aUrl.directory; + if (dir) { + return this.trimUrl(aUrl, dir.replace(/^\//, "") + aLabel, aSeq + 1); + } + aSeq++; + } + // Prepend the hostname and port number. + if (aSeq == 4) { + let host = aUrl.hostPort; + if (host) { + return this.trimUrl(aUrl, host + "/" + aLabel, aSeq + 1); + } + aSeq++; + } + // Use the whole url spec but ignoring the reference. + if (aSeq == 5) { + return this.trimUrl(aUrl, aUrl.specIgnoringRef, aSeq + 1); + } + // Give up. + return aUrl.spec; + } +}; + +/** + * Functions handling the variables bubble UI. + */ +function VariableBubbleView() { + dumpn("VariableBubbleView was instantiated"); + + this._onMouseMove = this._onMouseMove.bind(this); + this._onMouseOut = this._onMouseOut.bind(this); + this._onPopupHiding = this._onPopupHiding.bind(this); +} + +VariableBubbleView.prototype = { + /** + * Initialization function, called when the debugger is started. + */ + initialize: function() { + dumpn("Initializing the VariableBubbleView"); + + this._editorContainer = document.getElementById("editor"); + this._editorContainer.addEventListener("mousemove", this._onMouseMove, false); + this._editorContainer.addEventListener("mouseout", this._onMouseOut, false); + + this._tooltip = new Tooltip(document, { + closeOnEvents: [{ + emitter: DebuggerController._toolbox, + event: "select" + }, { + emitter: this._editorContainer, + event: "scroll", + useCapture: true + }] + }); + this._tooltip.defaultPosition = EDITOR_VARIABLE_POPUP_POSITION; + this._tooltip.defaultShowDelay = EDITOR_VARIABLE_HOVER_DELAY; + this._tooltip.panel.addEventListener("popuphiding", this._onPopupHiding); + }, + + /** + * Destruction function, called when the debugger is closed. + */ + destroy: function() { + dumpn("Destroying the VariableBubbleView"); + + this._tooltip.panel.removeEventListener("popuphiding", this._onPopupHiding); + this._editorContainer.removeEventListener("mousemove", this._onMouseMove, false); + this._editorContainer.removeEventListener("mouseout", this._onMouseOut, false); + }, + + /** + * Specifies whether literals can be (redundantly) inspected in a popup. + * This behavior is deprecated, but still tested in a few places. + */ + _ignoreLiterals: true, + + /** + * Searches for an identifier underneath the specified position in the + * source editor, and if found, opens a VariablesView inspection popup. + * + * @param number x, y + * The left/top coordinates where to look for an identifier. + */ + _findIdentifier: function(x, y) { + let editor = DebuggerView.editor; + + // Calculate the editor's line and column at the current x and y coords. + let hoveredPos = editor.getPositionFromCoords({ left: x, top: y }); + let hoveredOffset = editor.getOffset(hoveredPos); + let hoveredLine = hoveredPos.line; + let hoveredColumn = hoveredPos.ch; + + // A source contains multiple scripts. Find the start index of the script + // containing the specified offset relative to its parent source. + let contents = editor.getText(); + let location = DebuggerView.Sources.selectedValue; + let parsedSource = DebuggerController.Parser.get(contents, location); + let scriptInfo = parsedSource.getScriptInfo(hoveredOffset); + + // If the script length is negative, we're not hovering JS source code. + if (scriptInfo.length == -1) { + return; + } + + // Using the script offset, determine the actual line and column inside the + // script, to use when finding identifiers. + let scriptStart = editor.getPosition(scriptInfo.start); + let scriptLineOffset = scriptStart.line; + let scriptColumnOffset = (hoveredLine == scriptStart.line ? scriptStart.ch : 0); + + let scriptLine = hoveredLine - scriptLineOffset; + let scriptColumn = hoveredColumn - scriptColumnOffset; + let identifierInfo = parsedSource.getIdentifierAt({ + line: scriptLine + 1, + column: scriptColumn, + scriptIndex: scriptInfo.index, + ignoreLiterals: this._ignoreLiterals + }); + + // If the info is null, we're not hovering any identifier. + if (!identifierInfo) { + return; + } + + // Transform the line and column relative to the parsed script back + // to the context of the parent source. + let { start: identifierStart, end: identifierEnd } = identifierInfo.location; + let identifierCoords = { + line: identifierStart.line + scriptLineOffset, + column: identifierStart.column + scriptColumnOffset, + length: identifierEnd.column - identifierStart.column + }; + + // Evaluate the identifier in the current stack frame and show the + // results in a VariablesView inspection popup. + DebuggerController.StackFrames.evaluate(identifierInfo.evalString) + .then(frameFinished => { + if ("return" in frameFinished) { + this.showContents({ + coords: identifierCoords, + evalPrefix: identifierInfo.evalString, + objectActor: frameFinished.return + }); + } else { + let msg = "Evaluation has thrown for: " + identifierInfo.evalString; + console.warn(msg); + dumpn(msg); + } + }) + .then(null, err => { + let msg = "Couldn't evaluate: " + err.message; + console.error(msg); + dumpn(msg); + }); + }, + + /** + * Shows an inspection popup for a specified object actor grip. + * + * @param string object + * An object containing the following properties: + * - coords: the inspected identifier coordinates in the editor, + * containing the { line, column, length } properties. + * - evalPrefix: a prefix for the variables view evaluation macros. + * - objectActor: the value grip for the object actor. + */ + showContents: function({ coords, evalPrefix, objectActor }) { + let editor = DebuggerView.editor; + let { line, column, length } = coords; + + // Highlight the function found at the mouse position. + this._markedText = editor.markText( + { line: line - 1, ch: column }, + { line: line - 1, ch: column + length }); + + // If the grip represents a primitive value, use a more lightweight + // machinery to display it. + if (VariablesView.isPrimitive({ value: objectActor })) { + let className = VariablesView.getClass(objectActor); + let textContent = VariablesView.getString(objectActor); + this._tooltip.setTextContent({ + messages: [textContent], + messagesClass: className, + containerClass: "plain" + }, [{ + label: L10N.getStr('addWatchExpressionButton'), + className: "dbg-expression-button", + command: () => { + DebuggerView.VariableBubble.hideContents(); + DebuggerView.WatchExpressions.addExpression(evalPrefix, true); + } + }]); + } else { + this._tooltip.setVariableContent(objectActor, { + searchPlaceholder: L10N.getStr("emptyPropertiesFilterText"), + searchEnabled: Prefs.variablesSearchboxVisible, + eval: (variable, value) => { + let string = variable.evaluationMacro(variable, value); + DebuggerController.StackFrames.evaluate(string); + DebuggerView.VariableBubble.hideContents(); + } + }, { + getEnvironmentClient: aObject => gThreadClient.environment(aObject), + getObjectClient: aObject => gThreadClient.pauseGrip(aObject), + simpleValueEvalMacro: this._getSimpleValueEvalMacro(evalPrefix), + getterOrSetterEvalMacro: this._getGetterOrSetterEvalMacro(evalPrefix), + overrideValueEvalMacro: this._getOverrideValueEvalMacro(evalPrefix) + }, { + fetched: (aEvent, aType) => { + if (aType == "properties") { + window.emit(EVENTS.FETCHED_BUBBLE_PROPERTIES); + } + } + }, [{ + label: L10N.getStr("addWatchExpressionButton"), + className: "dbg-expression-button", + command: () => { + DebuggerView.VariableBubble.hideContents(); + DebuggerView.WatchExpressions.addExpression(evalPrefix, true); + } + }], DebuggerController._toolbox); + } + + this._tooltip.show(this._markedText.anchor); + }, + + /** + * Hides the inspection popup. + */ + hideContents: function() { + clearNamedTimeout("editor-mouse-move"); + this._tooltip.hide(); + }, + + /** + * Checks whether the inspection popup is shown. + * + * @return boolean + * True if the panel is shown or showing, false otherwise. + */ + contentsShown: function() { + return this._tooltip.isShown(); + }, + + /** + * Functions for getting customized variables view evaluation macros. + * + * @param string aPrefix + * See the corresponding VariablesView.* functions. + */ + _getSimpleValueEvalMacro: function(aPrefix) { + return (item, string) => + VariablesView.simpleValueEvalMacro(item, string, aPrefix); + }, + _getGetterOrSetterEvalMacro: function(aPrefix) { + return (item, string) => + VariablesView.getterOrSetterEvalMacro(item, string, aPrefix); + }, + _getOverrideValueEvalMacro: function(aPrefix) { + return (item, string) => + VariablesView.overrideValueEvalMacro(item, string, aPrefix); + }, + + /** + * The mousemove listener for the source editor. + */ + _onMouseMove: function(e) { + // Prevent the variable inspection popup from showing when the thread client + // is not paused, or while a popup is already visible, or when the user tries + // to select text in the editor. + let isResumed = gThreadClient && gThreadClient.state != "paused"; + let isSelecting = DebuggerView.editor.somethingSelected() && e.buttons > 0; + let isPopupVisible = !this._tooltip.isHidden(); + if (isResumed || isSelecting || isPopupVisible) { + clearNamedTimeout("editor-mouse-move"); + return; + } + // Allow events to settle down first. If the mouse hovers over + // a certain point in the editor long enough, try showing a variable bubble. + setNamedTimeout("editor-mouse-move", + EDITOR_VARIABLE_HOVER_DELAY, () => this._findIdentifier(e.clientX, e.clientY)); + }, + + /** + * The mouseout listener for the source editor container node. + */ + _onMouseOut: function() { + clearNamedTimeout("editor-mouse-move"); + }, + + /** + * Listener handling the popup hiding event. + */ + _onPopupHiding: function({ target }) { + if (this._tooltip.panel != target) { + return; + } + if (this._markedText) { + this._markedText.clear(); + this._markedText = null; + } + if (!this._tooltip.isEmpty()) { + this._tooltip.empty(); + } + }, + + _editorContainer: null, + _markedText: null, + _tooltip: null +}; + +/** + * Functions handling the watch expressions UI. + */ +function WatchExpressionsView() { + dumpn("WatchExpressionsView was instantiated"); + + this.switchExpression = this.switchExpression.bind(this); + this.deleteExpression = this.deleteExpression.bind(this); + this._createItemView = this._createItemView.bind(this); + this._onClick = this._onClick.bind(this); + this._onClose = this._onClose.bind(this); + this._onBlur = this._onBlur.bind(this); + this._onKeyPress = this._onKeyPress.bind(this); +} + +WatchExpressionsView.prototype = Heritage.extend(WidgetMethods, { + /** + * Initialization function, called when the debugger is started. + */ + initialize: function() { + dumpn("Initializing the WatchExpressionsView"); + + this.widget = new SimpleListWidget(document.getElementById("expressions")); + this.widget.setAttribute("context", "debuggerWatchExpressionsContextMenu"); + this.widget.addEventListener("click", this._onClick, false); + + this.headerText = L10N.getStr("addWatchExpressionText"); + }, + + /** + * Destruction function, called when the debugger is closed. + */ + destroy: function() { + dumpn("Destroying the WatchExpressionsView"); + + this.widget.removeEventListener("click", this._onClick, false); + }, + + /** + * Adds a watch expression in this container. + * + * @param string aExpression [optional] + * An optional initial watch expression text. + * @param boolean aSkipUserInput [optional] + * Pass true to avoid waiting for additional user input + * on the watch expression. + */ + addExpression: function(aExpression = "", aSkipUserInput = false) { + // Watch expressions are UI elements which benefit from visible panes. + DebuggerView.showInstrumentsPane(); + + // Create the element node for the watch expression item. + let itemView = this._createItemView(aExpression); + + // Append a watch expression item to this container. + let expressionItem = this.push([itemView.container], { + index: 0, /* specifies on which position should the item be appended */ + attachment: { + view: itemView, + initialExpression: aExpression, + currentExpression: "", + } + }); + + // Automatically focus the new watch expression input + // if additional user input is desired. + if (!aSkipUserInput) { + expressionItem.attachment.view.inputNode.select(); + expressionItem.attachment.view.inputNode.focus(); + DebuggerView.Variables.parentNode.scrollTop = 0; + } + // Otherwise, add and evaluate the new watch expression immediately. + else { + this.toggleContents(false); + this._onBlur({ target: expressionItem.attachment.view.inputNode }); + } + }, + + /** + * Changes the watch expression corresponding to the specified variable item. + * This function is called whenever a watch expression's code is edited in + * the variables view container. + * + * @param Variable aVar + * The variable representing the watch expression evaluation. + * @param string aExpression + * The new watch expression text. + */ + switchExpression: function(aVar, aExpression) { + let expressionItem = + [i for (i of this) if (i.attachment.currentExpression == aVar.name)][0]; + + // Remove the watch expression if it's going to be empty or a duplicate. + if (!aExpression || this.getAllStrings().indexOf(aExpression) != -1) { + this.deleteExpression(aVar); + return; + } + + // Save the watch expression code string. + expressionItem.attachment.currentExpression = aExpression; + expressionItem.attachment.view.inputNode.value = aExpression; + + // Synchronize with the controller's watch expressions store. + DebuggerController.StackFrames.syncWatchExpressions(); + }, + + /** + * Removes the watch expression corresponding to the specified variable item. + * This function is called whenever a watch expression's value is edited in + * the variables view container. + * + * @param Variable aVar + * The variable representing the watch expression evaluation. + */ + deleteExpression: function(aVar) { + let expressionItem = + [i for (i of this) if (i.attachment.currentExpression == aVar.name)][0]; + + // Remove the watch expression. + this.remove(expressionItem); + + // Synchronize with the controller's watch expressions store. + DebuggerController.StackFrames.syncWatchExpressions(); + }, + + /** + * Gets the watch expression code string for an item in this container. + * + * @param number aIndex + * The index used to identify the watch expression. + * @return string + * The watch expression code string. + */ + getString: function(aIndex) { + return this.getItemAtIndex(aIndex).attachment.currentExpression; + }, + + /** + * Gets the watch expressions code strings for all items in this container. + * + * @return array + * The watch expressions code strings. + */ + getAllStrings: function() { + return this.items.map(e => e.attachment.currentExpression); + }, + + /** + * Customization function for creating an item's UI. + * + * @param string aExpression + * The watch expression string. + */ + _createItemView: function(aExpression) { + let container = document.createElement("hbox"); + container.className = "list-widget-item dbg-expression"; + container.setAttribute("align", "center"); + + let arrowNode = document.createElement("hbox"); + arrowNode.className = "dbg-expression-arrow"; + + let inputNode = document.createElement("textbox"); + inputNode.className = "plain dbg-expression-input devtools-monospace"; + inputNode.setAttribute("value", aExpression); + inputNode.setAttribute("flex", "1"); + + let closeNode = document.createElement("toolbarbutton"); + closeNode.className = "plain variables-view-delete"; + + closeNode.addEventListener("click", this._onClose, false); + inputNode.addEventListener("blur", this._onBlur, false); + inputNode.addEventListener("keypress", this._onKeyPress, false); + + container.appendChild(arrowNode); + container.appendChild(inputNode); + container.appendChild(closeNode); + + return { + container: container, + arrowNode: arrowNode, + inputNode: inputNode, + closeNode: closeNode + }; + }, + + /** + * Called when the add watch expression key sequence was pressed. + */ + _onCmdAddExpression: function(aText) { + // Only add a new expression if there's no pending input. + if (this.getAllStrings().indexOf("") == -1) { + this.addExpression(aText || DebuggerView.editor.getSelection()); + } + }, + + /** + * Called when the remove all watch expressions key sequence was pressed. + */ + _onCmdRemoveAllExpressions: function() { + // Empty the view of all the watch expressions and clear the cache. + this.empty(); + + // Synchronize with the controller's watch expressions store. + DebuggerController.StackFrames.syncWatchExpressions(); + }, + + /** + * The click listener for this container. + */ + _onClick: function(e) { + if (e.button != 0) { + // Only allow left-click to trigger this event. + return; + } + let expressionItem = this.getItemForElement(e.target); + if (!expressionItem) { + // The container is empty or we didn't click on an actual item. + this.addExpression(); + } + }, + + /** + * The click listener for a watch expression's close button. + */ + _onClose: function(e) { + // Remove the watch expression. + this.remove(this.getItemForElement(e.target)); + + // Synchronize with the controller's watch expressions store. + DebuggerController.StackFrames.syncWatchExpressions(); + + // Prevent clicking the expression element itself. + e.preventDefault(); + e.stopPropagation(); + }, + + /** + * The blur listener for a watch expression's textbox. + */ + _onBlur: function({ target: textbox }) { + let expressionItem = this.getItemForElement(textbox); + let oldExpression = expressionItem.attachment.currentExpression; + let newExpression = textbox.value.trim(); + + // Remove the watch expression if it's empty. + if (!newExpression) { + this.remove(expressionItem); + } + // Remove the watch expression if it's a duplicate. + else if (!oldExpression && this.getAllStrings().indexOf(newExpression) != -1) { + this.remove(expressionItem); + } + // Expression is eligible. + else { + expressionItem.attachment.currentExpression = newExpression; + } + + // Synchronize with the controller's watch expressions store. + DebuggerController.StackFrames.syncWatchExpressions(); + }, + + /** + * The keypress listener for a watch expression's textbox. + */ + _onKeyPress: function(e) { + switch (e.keyCode) { + case e.DOM_VK_RETURN: + case e.DOM_VK_ESCAPE: + e.stopPropagation(); + DebuggerView.editor.focus(); + } + } +}); + +/** + * Functions handling the event listeners UI. + */ +function EventListenersView() { + dumpn("EventListenersView was instantiated"); + + this._onCheck = this._onCheck.bind(this); + this._onClick = this._onClick.bind(this); +} + +EventListenersView.prototype = Heritage.extend(WidgetMethods, { + /** + * Initialization function, called when the debugger is started. + */ + initialize: function() { + dumpn("Initializing the EventListenersView"); + + this.widget = new SideMenuWidget(document.getElementById("event-listeners"), { + showItemCheckboxes: true, + showGroupCheckboxes: true + }); + + this.emptyText = L10N.getStr("noEventListenersText"); + this._eventCheckboxTooltip = L10N.getStr("eventCheckboxTooltip"); + this._onSelectorString = " " + L10N.getStr("eventOnSelector") + " "; + this._inSourceString = " " + L10N.getStr("eventInSource") + " "; + this._inNativeCodeString = L10N.getStr("eventNative"); + + this.widget.addEventListener("check", this._onCheck, false); + this.widget.addEventListener("click", this._onClick, false); + }, + + /** + * Destruction function, called when the debugger is closed. + */ + destroy: function() { + dumpn("Destroying the EventListenersView"); + + this.widget.removeEventListener("check", this._onCheck, false); + this.widget.removeEventListener("click", this._onClick, false); + }, + + /** + * Adds an event to this event listeners container. + * + * @param object aListener + * The listener object coming from the active thread. + * @param object aOptions [optional] + * Additional options for adding the source. Supported options: + * - staged: true to stage the item to be appended later + */ + addListener: function(aListener, aOptions = {}) { + let { node: { selector }, function: { url }, type } = aListener; + if (!type) return; + + // Some listener objects may be added from plugins, thus getting + // translated to native code. + if (!url) { + url = this._inNativeCodeString; + } + + // If an event item for this listener's url and type was already added, + // avoid polluting the view and simply increase the "targets" count. + let eventItem = this.getItemForPredicate(aItem => + aItem.attachment.url == url && + aItem.attachment.type == type); + + if (eventItem) { + let { selectors, view: { targets } } = eventItem.attachment; + if (selectors.indexOf(selector) == -1) { + selectors.push(selector); + targets.setAttribute("value", L10N.getFormatStr("eventNodes", selectors.length)); + } + return; + } + + // There's no easy way of grouping event types into higher-level groups, + // so we need to do this by hand. + let is = (...args) => args.indexOf(type) != -1; + let has = str => type.contains(str); + let starts = str => type.startsWith(str); + let group; + + if (starts("animation")) { + group = L10N.getStr("animationEvents"); + } else if (starts("audio")) { + group = L10N.getStr("audioEvents"); + } else if (is("levelchange")) { + group = L10N.getStr("batteryEvents"); + } else if (is("cut", "copy", "paste")) { + group = L10N.getStr("clipboardEvents"); + } else if (starts("composition")) { + group = L10N.getStr("compositionEvents"); + } else if (starts("device")) { + group = L10N.getStr("deviceEvents"); + } else if (is("fullscreenchange", "fullscreenerror", "orientationchange", + "overflow", "resize", "scroll", "underflow", "zoom")) { + group = L10N.getStr("displayEvents"); + } else if (starts("drag") || starts("drop")) { + group = L10N.getStr("dragAndDropEvents"); + } else if (starts("gamepad")) { + group = L10N.getStr("gamepadEvents"); + } else if (is("canplay", "canplaythrough", "durationchange", "emptied", + "ended", "loadeddata", "loadedmetadata", "pause", "play", "playing", + "ratechange", "seeked", "seeking", "stalled", "suspend", "timeupdate", + "volumechange", "waiting")) { + group = L10N.getStr("mediaEvents"); + } else if (is("blocked", "complete", "success", "upgradeneeded", "versionchange")) { + group = L10N.getStr("indexedDBEvents"); + } else if (is("blur", "change", "focus", "focusin", "focusout", "invalid", + "reset", "select", "submit")) { + group = L10N.getStr("interactionEvents"); + } else if (starts("key") || is("input")) { + group = L10N.getStr("keyboardEvents"); + } else if (starts("mouse") || has("click") || is("contextmenu", "show", "wheel")) { + group = L10N.getStr("mouseEvents"); + } else if (starts("DOM")) { + group = L10N.getStr("mutationEvents"); + } else if (is("abort", "error", "hashchange", "load", "loadend", "loadstart", + "pagehide", "pageshow", "progress", "timeout", "unload", "uploadprogress", + "visibilitychange")) { + group = L10N.getStr("navigationEvents"); + } else if (is("pointerlockchange", "pointerlockerror")) { + group = L10N.getStr("pointerLockEvents"); + } else if (is("compassneedscalibration", "userproximity")) { + group = L10N.getStr("sensorEvents"); + } else if (starts("storage")) { + group = L10N.getStr("storageEvents"); + } else if (is("beginEvent", "endEvent", "repeatEvent")) { + group = L10N.getStr("timeEvents"); + } else if (starts("touch")) { + group = L10N.getStr("touchEvents"); + } else { + group = L10N.getStr("otherEvents"); + } + + // Create the element node for the event listener item. + let itemView = this._createItemView(type, selector, url); + + // Event breakpoints survive target navigations. Make sure the newly + // inserted event item is correctly checked. + let checkboxState = + DebuggerController.Breakpoints.DOM.activeEventNames.indexOf(type) != -1; + + // Append an event listener item to this container. + this.push([itemView.container], { + staged: aOptions.staged, /* stage the item to be appended later? */ + attachment: { + url: url, + type: type, + view: itemView, + selectors: [selector], + group: group, + checkboxState: checkboxState, + checkboxTooltip: this._eventCheckboxTooltip + } + }); + }, + + /** + * Gets all the event types known to this container. + * + * @return array + * List of event types, for example ["load", "click"...] + */ + getAllEvents: function() { + return this.attachments.map(e => e.type); + }, + + /** + * Gets the checked event types in this container. + * + * @return array + * List of event types, for example ["load", "click"...] + */ + getCheckedEvents: function() { + return this.attachments.filter(e => e.checkboxState).map(e => e.type); + }, + + /** + * Customization function for creating an item's UI. + * + * @param string aType + * The event type, for example "click". + * @param string aSelector + * The target element's selector. + * @param string url + * The source url in which the event listener is located. + * @return object + * An object containing the event listener view nodes. + */ + _createItemView: function(aType, aSelector, aUrl) { + let container = document.createElement("hbox"); + container.className = "dbg-event-listener"; + + let eventType = document.createElement("label"); + eventType.className = "plain dbg-event-listener-type"; + eventType.setAttribute("value", aType); + container.appendChild(eventType); + + let typeSeparator = document.createElement("label"); + typeSeparator.className = "plain dbg-event-listener-separator"; + typeSeparator.setAttribute("value", this._onSelectorString); + container.appendChild(typeSeparator); + + let eventTargets = document.createElement("label"); + eventTargets.className = "plain dbg-event-listener-targets"; + eventTargets.setAttribute("value", aSelector); + container.appendChild(eventTargets); + + let selectorSeparator = document.createElement("label"); + selectorSeparator.className = "plain dbg-event-listener-separator"; + selectorSeparator.setAttribute("value", this._inSourceString); + container.appendChild(selectorSeparator); + + let eventLocation = document.createElement("label"); + eventLocation.className = "plain dbg-event-listener-location"; + eventLocation.setAttribute("value", SourceUtils.getSourceLabel(aUrl)); + eventLocation.setAttribute("flex", "1"); + eventLocation.setAttribute("crop", "center"); + container.appendChild(eventLocation); + + return { + container: container, + type: eventType, + targets: eventTargets, + location: eventLocation + }; + }, + + /** + * The check listener for the event listeners container. + */ + _onCheck: function({ detail: { description, checked }, target }) { + if (description == "item") { + this.getItemForElement(target).attachment.checkboxState = checked; + DebuggerController.Breakpoints.DOM.scheduleEventBreakpointsUpdate(); + return; + } + + // Check all the event items in this group. + this.items + .filter(e => e.attachment.group == description) + .forEach(e => this.callMethod("checkItem", e.target, checked)); + }, + + /** + * The select listener for the event listeners container. + */ + _onClick: function({ target }) { + // Changing the checkbox state is handled by the _onCheck event. Avoid + // handling that again in this click event, so pass in "noSiblings" + // when retrieving the target's item, to ignore the checkbox. + let eventItem = this.getItemForElement(target, { noSiblings: true }); + if (eventItem) { + let newState = eventItem.attachment.checkboxState ^= 1; + this.callMethod("checkItem", eventItem.target, newState); + } + }, + + _eventCheckboxTooltip: "", + _onSelectorString: "", + _inSourceString: "", + _inNativeCodeString: "" +}); + +/** + * Functions handling the global search UI. + */ +function GlobalSearchView() { + dumpn("GlobalSearchView was instantiated"); + + this._onHeaderClick = this._onHeaderClick.bind(this); + this._onLineClick = this._onLineClick.bind(this); + this._onMatchClick = this._onMatchClick.bind(this); +} + +GlobalSearchView.prototype = Heritage.extend(WidgetMethods, { + /** + * Initialization function, called when the debugger is started. + */ + initialize: function() { + dumpn("Initializing the GlobalSearchView"); + + this.widget = new SimpleListWidget(document.getElementById("globalsearch")); + this._splitter = document.querySelector("#globalsearch + .devtools-horizontal-splitter"); + + this.emptyText = L10N.getStr("noMatchingStringsText"); + }, + + /** + * Destruction function, called when the debugger is closed. + */ + destroy: function() { + dumpn("Destroying the GlobalSearchView"); + }, + + /** + * Sets the results container hidden or visible. It's hidden by default. + * @param boolean aFlag + */ + set hidden(aFlag) { + this.widget.setAttribute("hidden", aFlag); + this._splitter.setAttribute("hidden", aFlag); + }, + + /** + * Gets the visibility state of the global search container. + * @return boolean + */ + get hidden() + this.widget.getAttribute("hidden") == "true" || + this._splitter.getAttribute("hidden") == "true", + + /** + * Hides and removes all items from this search container. + */ + clearView: function() { + this.hidden = true; + this.empty(); + }, + + /** + * Selects the next found item in this container. + * Does not change the currently focused node. + */ + selectNext: function() { + let totalLineResults = LineResults.size(); + if (!totalLineResults) { + return; + } + if (++this._currentlyFocusedMatch >= totalLineResults) { + this._currentlyFocusedMatch = 0; + } + this._onMatchClick({ + target: LineResults.getElementAtIndex(this._currentlyFocusedMatch) + }); + }, + + /** + * Selects the previously found item in this container. + * Does not change the currently focused node. + */ + selectPrev: function() { + let totalLineResults = LineResults.size(); + if (!totalLineResults) { + return; + } + if (--this._currentlyFocusedMatch < 0) { + this._currentlyFocusedMatch = totalLineResults - 1; + } + this._onMatchClick({ + target: LineResults.getElementAtIndex(this._currentlyFocusedMatch) + }); + }, + + /** + * Schedules searching for a string in all of the sources. + * + * @param string aToken + * The string to search for. + * @param number aWait + * The amount of milliseconds to wait until draining. + */ + scheduleSearch: function(aToken, aWait) { + // The amount of time to wait for the requests to settle. + let maxDelay = GLOBAL_SEARCH_ACTION_MAX_DELAY; + let delay = aWait === undefined ? maxDelay / aToken.length : aWait; + + // Allow requests to settle down first. + setNamedTimeout("global-search", delay, () => { + // Start fetching as many sources as possible, then perform the search. + let actors = DebuggerView.Sources.values; + let sourcesFetched = DebuggerController.SourceScripts.getTextForSources(actors); + sourcesFetched.then(aSources => this._doSearch(aToken, aSources)); + }); + }, + + /** + * Finds string matches in all the sources stored in the controller's cache, + * and groups them by url and line number. + * + * @param string aToken + * The string to search for. + * @param array aSources + * An array of [url, text] tuples for each source. + */ + _doSearch: function(aToken, aSources) { + // Don't continue filtering if the searched token is an empty string. + if (!aToken) { + this.clearView(); + return; + } + + // Search is not case sensitive, prepare the actual searched token. + let lowerCaseToken = aToken.toLowerCase(); + let tokenLength = aToken.length; + + // Create a Map containing search details for each source. + let globalResults = new GlobalResults(); + + // Search for the specified token in each source's text. + for (let [actor, text] of aSources) { + let item = DebuggerView.Sources.getItemByValue(actor); + let url = item.attachment.source.url; + if (!url) { + continue; + } + + // Verify that the search token is found anywhere in the source. + if (!text.toLowerCase().contains(lowerCaseToken)) { + continue; + } + // ...and if so, create a Map containing search details for each line. + let sourceResults = new SourceResults(actor, globalResults); + + // Search for the specified token in each line's text. + text.split("\n").forEach((aString, aLine) => { + // Search is not case sensitive, prepare the actual searched line. + let lowerCaseLine = aString.toLowerCase(); + + // Verify that the search token is found anywhere in this line. + if (!lowerCaseLine.contains(lowerCaseToken)) { + return; + } + // ...and if so, create a Map containing search details for each word. + let lineResults = new LineResults(aLine, sourceResults); + + // Search for the specified token this line's text. + lowerCaseLine.split(lowerCaseToken).reduce((aPrev, aCurr, aIndex, aArray) => { + let prevLength = aPrev.length; + let currLength = aCurr.length; + + // Everything before the token is unmatched. + let unmatched = aString.substr(prevLength, currLength); + lineResults.add(unmatched); + + // The lowered-case line was split by the lowered-case token. So, + // get the actual matched text from the original line's text. + if (aIndex != aArray.length - 1) { + let matched = aString.substr(prevLength + currLength, tokenLength); + let range = { start: prevLength + currLength, length: matched.length }; + lineResults.add(matched, range, true); + } + + // Continue with the next sub-region in this line's text. + return aPrev + aToken + aCurr; + }, ""); + + if (lineResults.matchCount) { + sourceResults.add(lineResults); + } + }); + + if (sourceResults.matchCount) { + globalResults.add(sourceResults); + } + } + + // Rebuild the results, then signal if there are any matches. + if (globalResults.matchCount) { + this.hidden = false; + this._currentlyFocusedMatch = -1; + this._createGlobalResultsUI(globalResults); + window.emit(EVENTS.GLOBAL_SEARCH_MATCH_FOUND); + } else { + window.emit(EVENTS.GLOBAL_SEARCH_MATCH_NOT_FOUND); + } + }, + + /** + * Creates global search results entries and adds them to this container. + * + * @param GlobalResults aGlobalResults + * An object containing all source results, grouped by source location. + */ + _createGlobalResultsUI: function(aGlobalResults) { + let i = 0; + + for (let sourceResults of aGlobalResults) { + if (i++ == 0) { + this._createSourceResultsUI(sourceResults); + } else { + // Dispatch subsequent document manipulation operations, to avoid + // blocking the main thread when a large number of search results + // is found, thus giving the impression of faster searching. + Services.tm.currentThread.dispatch({ run: + this._createSourceResultsUI.bind(this, sourceResults) + }, 0); + } + } + }, + + /** + * Creates source search results entries and adds them to this container. + * + * @param SourceResults aSourceResults + * An object containing all the matched lines for a specific source. + */ + _createSourceResultsUI: function(aSourceResults) { + // Create the element node for the source results item. + let container = document.createElement("hbox"); + aSourceResults.createView(container, { + onHeaderClick: this._onHeaderClick, + onLineClick: this._onLineClick, + onMatchClick: this._onMatchClick + }); + + // Append a source results item to this container. + let item = this.push([container], { + index: -1, /* specifies on which position should the item be appended */ + attachment: { + sourceResults: aSourceResults + } + }); + }, + + /** + * The click listener for a results header. + */ + _onHeaderClick: function(e) { + let sourceResultsItem = SourceResults.getItemForElement(e.target); + sourceResultsItem.instance.toggle(e); + }, + + /** + * The click listener for a results line. + */ + _onLineClick: function(e) { + let lineResultsItem = LineResults.getItemForElement(e.target); + this._onMatchClick({ target: lineResultsItem.firstMatch }); + }, + + /** + * The click listener for a result match. + */ + _onMatchClick: function(e) { + if (e instanceof Event) { + e.preventDefault(); + e.stopPropagation(); + } + + let target = e.target; + let sourceResultsItem = SourceResults.getItemForElement(target); + let lineResultsItem = LineResults.getItemForElement(target); + + sourceResultsItem.instance.expand(); + this._currentlyFocusedMatch = LineResults.indexOfElement(target); + this._scrollMatchIntoViewIfNeeded(target); + this._bounceMatch(target); + + let actor = sourceResultsItem.instance.actor; + let line = lineResultsItem.instance.line; + + DebuggerView.setEditorLocation(actor, line + 1, { noDebug: true }); + + let range = lineResultsItem.lineData.range; + let cursor = DebuggerView.editor.getOffset({ line: line, ch: 0 }); + let [ anchor, head ] = DebuggerView.editor.getPosition( + cursor + range.start, + cursor + range.start + range.length + ); + + DebuggerView.editor.setSelection(anchor, head); + }, + + /** + * Scrolls a match into view if not already visible. + * + * @param nsIDOMNode aMatch + * The match to scroll into view. + */ + _scrollMatchIntoViewIfNeeded: function(aMatch) { + this.widget.ensureElementIsVisible(aMatch); + }, + + /** + * Starts a bounce animation for a match. + * + * @param nsIDOMNode aMatch + * The match to start a bounce animation for. + */ + _bounceMatch: function(aMatch) { + Services.tm.currentThread.dispatch({ run: () => { + aMatch.addEventListener("transitionend", function onEvent() { + aMatch.removeEventListener("transitionend", onEvent); + aMatch.removeAttribute("focused"); + }); + aMatch.setAttribute("focused", ""); + }}, 0); + aMatch.setAttribute("focusing", ""); + }, + + _splitter: null, + _currentlyFocusedMatch: -1, + _forceExpandResults: false +}); + +/** + * An object containing all source results, grouped by source location. + * Iterable via "for (let [location, sourceResults] of globalResults) { }". + */ +function GlobalResults() { + this._store = []; + SourceResults._itemsByElement = new Map(); + LineResults._itemsByElement = new Map(); +} + +GlobalResults.prototype = { + /** + * Adds source results to this store. + * + * @param SourceResults aSourceResults + * An object containing search results for a specific source. + */ + add: function(aSourceResults) { + this._store.push(aSourceResults); + }, + + /** + * Gets the number of source results in this store. + */ + get matchCount() this._store.length +}; + +/** + * An object containing all the matched lines for a specific source. + * Iterable via "for (let [lineNumber, lineResults] of sourceResults) { }". + * + * @param string aActor + * The target source actor id. + * @param GlobalResults aGlobalResults + * An object containing all source results, grouped by source location. + */ +function SourceResults(aActor, aGlobalResults) { + let item = DebuggerView.Sources.getItemByValue(aActor); + this.actor = aActor; + this.label = item.attachment.source.url; + this._globalResults = aGlobalResults; + this._store = []; +} + +SourceResults.prototype = { + /** + * Adds line results to this store. + * + * @param LineResults aLineResults + * An object containing search results for a specific line. + */ + add: function(aLineResults) { + this._store.push(aLineResults); + }, + + /** + * Gets the number of line results in this store. + */ + get matchCount() this._store.length, + + /** + * Expands the element, showing all the added details. + */ + expand: function() { + this._resultsContainer.removeAttribute("hidden"); + this._arrow.setAttribute("open", ""); + }, + + /** + * Collapses the element, hiding all the added details. + */ + collapse: function() { + this._resultsContainer.setAttribute("hidden", "true"); + this._arrow.removeAttribute("open"); + }, + + /** + * Toggles between the element collapse/expand state. + */ + toggle: function(e) { + this.expanded ^= 1; + }, + + /** + * Gets this element's expanded state. + * @return boolean + */ + get expanded() + this._resultsContainer.getAttribute("hidden") != "true" && + this._arrow.hasAttribute("open"), + + /** + * Sets this element's expanded state. + * @param boolean aFlag + */ + set expanded(aFlag) this[aFlag ? "expand" : "collapse"](), + + /** + * Gets the element associated with this item. + * @return nsIDOMNode + */ + get target() this._target, + + /** + * Customization function for creating this item's UI. + * + * @param nsIDOMNode aElementNode + * The element associated with the displayed item. + * @param object aCallbacks + * An object containing all the necessary callback functions: + * - onHeaderClick + * - onMatchClick + */ + createView: function(aElementNode, aCallbacks) { + this._target = aElementNode; + + let arrow = this._arrow = document.createElement("box"); + arrow.className = "arrow"; + + let locationNode = document.createElement("label"); + locationNode.className = "plain dbg-results-header-location"; + locationNode.setAttribute("value", this.label); + + let matchCountNode = document.createElement("label"); + matchCountNode.className = "plain dbg-results-header-match-count"; + matchCountNode.setAttribute("value", "(" + this.matchCount + ")"); + + let resultsHeader = this._resultsHeader = document.createElement("hbox"); + resultsHeader.className = "dbg-results-header"; + resultsHeader.setAttribute("align", "center") + resultsHeader.appendChild(arrow); + resultsHeader.appendChild(locationNode); + resultsHeader.appendChild(matchCountNode); + resultsHeader.addEventListener("click", aCallbacks.onHeaderClick, false); + + let resultsContainer = this._resultsContainer = document.createElement("vbox"); + resultsContainer.className = "dbg-results-container"; + resultsContainer.setAttribute("hidden", "true"); + + // Create lines search results entries and add them to this container. + // Afterwards, if the number of matches is reasonable, expand this + // container automatically. + for (let lineResults of this._store) { + lineResults.createView(resultsContainer, aCallbacks); + } + if (this.matchCount < GLOBAL_SEARCH_EXPAND_MAX_RESULTS) { + this.expand(); + } + + let resultsBox = document.createElement("vbox"); + resultsBox.setAttribute("flex", "1"); + resultsBox.appendChild(resultsHeader); + resultsBox.appendChild(resultsContainer); + + aElementNode.id = "source-results-" + this.actor; + aElementNode.className = "dbg-source-results"; + aElementNode.appendChild(resultsBox); + + SourceResults._itemsByElement.set(aElementNode, { instance: this }); + }, + + actor: "", + _globalResults: null, + _store: null, + _target: null, + _arrow: null, + _resultsHeader: null, + _resultsContainer: null +}; + +/** + * An object containing all the matches for a specific line. + * Iterable via "for (let chunk of lineResults) { }". + * + * @param number aLine + * The target line in the source. + * @param SourceResults aSourceResults + * An object containing all the matched lines for a specific source. + */ +function LineResults(aLine, aSourceResults) { + this.line = aLine; + this._sourceResults = aSourceResults; + this._store = []; + this._matchCount = 0; +} + +LineResults.prototype = { + /** + * Adds string details to this store. + * + * @param string aString + * The text contents chunk in the line. + * @param object aRange + * An object containing the { start, length } of the chunk. + * @param boolean aMatchFlag + * True if the chunk is a matched string, false if just text content. + */ + add: function(aString, aRange, aMatchFlag) { + this._store.push({ string: aString, range: aRange, match: !!aMatchFlag }); + this._matchCount += aMatchFlag ? 1 : 0; + }, + + /** + * Gets the number of word results in this store. + */ + get matchCount() this._matchCount, + + /** + * Gets the element associated with this item. + * @return nsIDOMNode + */ + get target() this._target, + + /** + * Customization function for creating this item's UI. + * + * @param nsIDOMNode aElementNode + * The element associated with the displayed item. + * @param object aCallbacks + * An object containing all the necessary callback functions: + * - onMatchClick + * - onLineClick + */ + createView: function(aElementNode, aCallbacks) { + this._target = aElementNode; + + let lineNumberNode = document.createElement("label"); + lineNumberNode.className = "plain dbg-results-line-number"; + lineNumberNode.classList.add("devtools-monospace"); + lineNumberNode.setAttribute("value", this.line + 1); + + let lineContentsNode = document.createElement("hbox"); + lineContentsNode.className = "dbg-results-line-contents"; + lineContentsNode.classList.add("devtools-monospace"); + lineContentsNode.setAttribute("flex", "1"); + + let lineString = ""; + let lineLength = 0; + let firstMatch = null; + + for (let lineChunk of this._store) { + let { string, range, match } = lineChunk; + lineString = string.substr(0, GLOBAL_SEARCH_LINE_MAX_LENGTH - lineLength); + lineLength += string.length; + + let lineChunkNode = document.createElement("label"); + lineChunkNode.className = "plain dbg-results-line-contents-string"; + lineChunkNode.setAttribute("value", lineString); + lineChunkNode.setAttribute("match", match); + lineContentsNode.appendChild(lineChunkNode); + + if (match) { + this._entangleMatch(lineChunkNode, lineChunk); + lineChunkNode.addEventListener("click", aCallbacks.onMatchClick, false); + firstMatch = firstMatch || lineChunkNode; + } + if (lineLength >= GLOBAL_SEARCH_LINE_MAX_LENGTH) { + lineContentsNode.appendChild(this._ellipsis.cloneNode(true)); + break; + } + } + + this._entangleLine(lineContentsNode, firstMatch); + lineContentsNode.addEventListener("click", aCallbacks.onLineClick, false); + + let searchResult = document.createElement("hbox"); + searchResult.className = "dbg-search-result"; + searchResult.appendChild(lineNumberNode); + searchResult.appendChild(lineContentsNode); + + aElementNode.appendChild(searchResult); + }, + + /** + * Handles a match while creating the view. + * @param nsIDOMNode aNode + * @param object aMatchChunk + */ + _entangleMatch: function(aNode, aMatchChunk) { + LineResults._itemsByElement.set(aNode, { + instance: this, + lineData: aMatchChunk + }); + }, + + /** + * Handles a line while creating the view. + * @param nsIDOMNode aNode + * @param nsIDOMNode aFirstMatch + */ + _entangleLine: function(aNode, aFirstMatch) { + LineResults._itemsByElement.set(aNode, { + instance: this, + firstMatch: aFirstMatch, + ignored: true + }); + }, + + /** + * An nsIDOMNode label with an ellipsis value. + */ + _ellipsis: (function() { + let label = document.createElement("label"); + label.className = "plain dbg-results-line-contents-string"; + label.setAttribute("value", L10N.ellipsis); + return label; + })(), + + line: 0, + _sourceResults: null, + _store: null, + _target: null +}; + +/** + * A generator-iterator over the global, source or line results. + */ +GlobalResults.prototype[Symbol.iterator] = +SourceResults.prototype[Symbol.iterator] = +LineResults.prototype[Symbol.iterator] = function*() { + yield* this._store; +}; + +/** + * Gets the item associated with the specified element. + * + * @param nsIDOMNode aElement + * The element used to identify the item. + * @return object + * The matched item, or null if nothing is found. + */ +SourceResults.getItemForElement = +LineResults.getItemForElement = function(aElement) { + return WidgetMethods.getItemForElement.call(this, aElement, { noSiblings: true }); +}; + +/** + * Gets the element associated with a particular item at a specified index. + * + * @param number aIndex + * The index used to identify the item. + * @return nsIDOMNode + * The matched element, or null if nothing is found. + */ +SourceResults.getElementAtIndex = +LineResults.getElementAtIndex = function(aIndex) { + for (let [element, item] of this._itemsByElement) { + if (!item.ignored && !aIndex--) { + return element; + } + } + return null; +}; + +/** + * Gets the index of an item associated with the specified element. + * + * @param nsIDOMNode aElement + * The element to get the index for. + * @return number + * The index of the matched element, or -1 if nothing is found. + */ +SourceResults.indexOfElement = +LineResults.indexOfElement = function(aElement) { + let count = 0; + for (let [element, item] of this._itemsByElement) { + if (element == aElement) { + return count; + } + if (!item.ignored) { + count++; + } + } + return -1; +}; + +/** + * Gets the number of cached items associated with a specified element. + * + * @return number + * The number of key/value pairs in the corresponding map. + */ +SourceResults.size = +LineResults.size = function() { + let count = 0; + for (let [, item] of this._itemsByElement) { + if (!item.ignored) { + count++; + } + } + return count; +}; + +/** + * Preliminary setup for the DebuggerView object. + */ +DebuggerView.Sources = new SourcesView(); +DebuggerView.VariableBubble = new VariableBubbleView(); +DebuggerView.Tracer = new TracerView(); +DebuggerView.WatchExpressions = new WatchExpressionsView(); +DebuggerView.EventListeners = new EventListenersView(); +DebuggerView.GlobalSearch = new GlobalSearchView(); diff --git a/toolkit/devtools/debugger/debugger-toolbar.js b/toolkit/devtools/debugger/debugger-toolbar.js new file mode 100644 index 000000000..695bbd68d --- /dev/null +++ b/toolkit/devtools/debugger/debugger-toolbar.js @@ -0,0 +1,1594 @@ +/* -*- 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"; + +// A time interval sufficient for the options popup panel to finish hiding +// itself. +const POPUP_HIDDEN_DELAY = 100; // ms + +/** + * Functions handling the toolbar view: close button, expand/collapse button, + * pause/resume and stepping buttons etc. + */ +function ToolbarView() { + dumpn("ToolbarView was instantiated"); + + this._onTogglePanesPressed = this._onTogglePanesPressed.bind(this); + this._onResumePressed = this._onResumePressed.bind(this); + this._onStepOverPressed = this._onStepOverPressed.bind(this); + this._onStepInPressed = this._onStepInPressed.bind(this); + this._onStepOutPressed = this._onStepOutPressed.bind(this); +} + +ToolbarView.prototype = { + /** + * Initialization function, called when the debugger is started. + */ + initialize: function() { + dumpn("Initializing the ToolbarView"); + + this._instrumentsPaneToggleButton = document.getElementById("instruments-pane-toggle"); + this._resumeButton = document.getElementById("resume"); + this._stepOverButton = document.getElementById("step-over"); + this._stepInButton = document.getElementById("step-in"); + this._stepOutButton = document.getElementById("step-out"); + this._resumeOrderTooltip = new Tooltip(document); + this._resumeOrderTooltip.defaultPosition = TOOLBAR_ORDER_POPUP_POSITION; + + let resumeKey = ShortcutUtils.prettifyShortcut(document.getElementById("resumeKey")); + let stepOverKey = ShortcutUtils.prettifyShortcut(document.getElementById("stepOverKey")); + let stepInKey = ShortcutUtils.prettifyShortcut(document.getElementById("stepInKey")); + let stepOutKey = ShortcutUtils.prettifyShortcut(document.getElementById("stepOutKey")); + this._resumeTooltip = L10N.getFormatStr("resumeButtonTooltip", resumeKey); + this._pauseTooltip = L10N.getFormatStr("pauseButtonTooltip", resumeKey); + this._stepOverTooltip = L10N.getFormatStr("stepOverTooltip", stepOverKey); + this._stepInTooltip = L10N.getFormatStr("stepInTooltip", stepInKey); + this._stepOutTooltip = L10N.getFormatStr("stepOutTooltip", stepOutKey); + + this._instrumentsPaneToggleButton.addEventListener("mousedown", this._onTogglePanesPressed, false); + this._resumeButton.addEventListener("mousedown", this._onResumePressed, false); + this._stepOverButton.addEventListener("mousedown", this._onStepOverPressed, false); + this._stepInButton.addEventListener("mousedown", this._onStepInPressed, false); + this._stepOutButton.addEventListener("mousedown", this._onStepOutPressed, false); + + this._stepOverButton.setAttribute("tooltiptext", this._stepOverTooltip); + this._stepInButton.setAttribute("tooltiptext", this._stepInTooltip); + this._stepOutButton.setAttribute("tooltiptext", this._stepOutTooltip); + }, + + /** + * Destruction function, called when the debugger is closed. + */ + destroy: function() { + dumpn("Destroying the ToolbarView"); + + this._instrumentsPaneToggleButton.removeEventListener("mousedown", this._onTogglePanesPressed, false); + this._resumeButton.removeEventListener("mousedown", this._onResumePressed, false); + this._stepOverButton.removeEventListener("mousedown", this._onStepOverPressed, false); + this._stepInButton.removeEventListener("mousedown", this._onStepInPressed, false); + this._stepOutButton.removeEventListener("mousedown", this._onStepOutPressed, false); + }, + + /** + * Display a warning when trying to resume a debuggee while another is paused. + * Debuggees must be unpaused in a Last-In-First-Out order. + * + * @param string aPausedUrl + * The URL of the last paused debuggee. + */ + showResumeWarning: function(aPausedUrl) { + let label = L10N.getFormatStr("resumptionOrderPanelTitle", aPausedUrl); + let defaultStyle = "default-tooltip-simple-text-colors"; + this._resumeOrderTooltip.setTextContent({ messages: [label], isAlertTooltip: true }); + this._resumeOrderTooltip.show(this._resumeButton); + }, + + /** + * Sets the resume button state based on the debugger active thread. + * + * @param string aState + * Either "paused" or "attached". + */ + toggleResumeButtonState: function(aState) { + // If we're paused, check and show a resume label on the button. + if (aState == "paused") { + this._resumeButton.setAttribute("checked", "true"); + this._resumeButton.setAttribute("tooltiptext", this._resumeTooltip); + } + // If we're attached, do the opposite. + else if (aState == "attached") { + this._resumeButton.removeAttribute("checked"); + this._resumeButton.setAttribute("tooltiptext", this._pauseTooltip); + } + }, + + /** + * Listener handling the toggle button click event. + */ + _onTogglePanesPressed: function() { + DebuggerView.toggleInstrumentsPane({ + visible: DebuggerView.instrumentsPaneHidden, + animated: true, + delayed: true + }); + }, + + /** + * Listener handling the pause/resume button click event. + */ + _onResumePressed: function() { + if (DebuggerController.StackFrames._currentFrameDescription != FRAME_TYPE.NORMAL) { + return; + } + + if (DebuggerController.activeThread.paused) { + let warn = DebuggerController._ensureResumptionOrder; + DebuggerController.StackFrames.currentFrameDepth = -1; + DebuggerController.activeThread.resume(warn); + } else { + DebuggerController.ThreadState.interruptedByResumeButton = true; + DebuggerController.activeThread.interrupt(); + } + }, + + /** + * Listener handling the step over button click event. + */ + _onStepOverPressed: function() { + if (DebuggerController.activeThread.paused) { + DebuggerController.StackFrames.currentFrameDepth = -1; + let warn = DebuggerController._ensureResumptionOrder; + DebuggerController.activeThread.stepOver(warn); + } + }, + + /** + * Listener handling the step in button click event. + */ + _onStepInPressed: function() { + if (DebuggerController.StackFrames._currentFrameDescription != FRAME_TYPE.NORMAL) { + return; + } + + if (DebuggerController.activeThread.paused) { + DebuggerController.StackFrames.currentFrameDepth = -1; + let warn = DebuggerController._ensureResumptionOrder; + DebuggerController.activeThread.stepIn(warn); + } + }, + + /** + * Listener handling the step out button click event. + */ + _onStepOutPressed: function() { + if (DebuggerController.activeThread.paused) { + DebuggerController.StackFrames.currentFrameDepth = -1; + let warn = DebuggerController._ensureResumptionOrder; + DebuggerController.activeThread.stepOut(warn); + } + }, + + _instrumentsPaneToggleButton: null, + _resumeButton: null, + _stepOverButton: null, + _stepInButton: null, + _stepOutButton: null, + _resumeOrderTooltip: null, + _resumeTooltip: "", + _pauseTooltip: "", + _stepOverTooltip: "", + _stepInTooltip: "", + _stepOutTooltip: "" +}; + +/** + * Functions handling the options UI. + */ +function OptionsView() { + dumpn("OptionsView was instantiated"); + + this._toggleAutoPrettyPrint = this._toggleAutoPrettyPrint.bind(this); + this._togglePauseOnExceptions = this._togglePauseOnExceptions.bind(this); + this._toggleIgnoreCaughtExceptions = this._toggleIgnoreCaughtExceptions.bind(this); + this._toggleShowPanesOnStartup = this._toggleShowPanesOnStartup.bind(this); + this._toggleShowVariablesOnlyEnum = this._toggleShowVariablesOnlyEnum.bind(this); + this._toggleShowVariablesFilterBox = this._toggleShowVariablesFilterBox.bind(this); + this._toggleShowOriginalSource = this._toggleShowOriginalSource.bind(this); + this._toggleAutoBlackBox = this._toggleAutoBlackBox.bind(this); +} + +OptionsView.prototype = { + /** + * Initialization function, called when the debugger is started. + */ + initialize: function() { + dumpn("Initializing the OptionsView"); + + this._button = document.getElementById("debugger-options"); + this._autoPrettyPrint = document.getElementById("auto-pretty-print"); + this._pauseOnExceptionsItem = document.getElementById("pause-on-exceptions"); + this._ignoreCaughtExceptionsItem = document.getElementById("ignore-caught-exceptions"); + this._showPanesOnStartupItem = document.getElementById("show-panes-on-startup"); + this._showVariablesOnlyEnumItem = document.getElementById("show-vars-only-enum"); + this._showVariablesFilterBoxItem = document.getElementById("show-vars-filter-box"); + this._showOriginalSourceItem = document.getElementById("show-original-source"); + this._autoBlackBoxItem = document.getElementById("auto-black-box"); + + this._autoPrettyPrint.setAttribute("checked", Prefs.autoPrettyPrint); + this._pauseOnExceptionsItem.setAttribute("checked", Prefs.pauseOnExceptions); + this._ignoreCaughtExceptionsItem.setAttribute("checked", Prefs.ignoreCaughtExceptions); + this._showPanesOnStartupItem.setAttribute("checked", Prefs.panesVisibleOnStartup); + this._showVariablesOnlyEnumItem.setAttribute("checked", Prefs.variablesOnlyEnumVisible); + this._showVariablesFilterBoxItem.setAttribute("checked", Prefs.variablesSearchboxVisible); + this._showOriginalSourceItem.setAttribute("checked", Prefs.sourceMapsEnabled); + this._autoBlackBoxItem.setAttribute("checked", Prefs.autoBlackBox); + }, + + + /** + * Destruction function, called when the debugger is closed. + */ + destroy: function() { + dumpn("Destroying the OptionsView"); + // Nothing to do here yet. + }, + + /** + * Listener handling the 'gear menu' popup showing event. + */ + _onPopupShowing: function() { + this._button.setAttribute("open", "true"); + window.emit(EVENTS.OPTIONS_POPUP_SHOWING); + }, + + /** + * Listener handling the 'gear menu' popup hiding event. + */ + _onPopupHiding: function() { + this._button.removeAttribute("open"); + }, + + /** + * Listener handling the 'gear menu' popup hidden event. + */ + _onPopupHidden: function() { + window.emit(EVENTS.OPTIONS_POPUP_HIDDEN); + }, + + /** + * Listener handling the 'auto pretty print' menuitem command. + */ + _toggleAutoPrettyPrint: function(){ + Prefs.autoPrettyPrint = + this._autoPrettyPrint.getAttribute("checked") == "true"; + }, + + /** + * Listener handling the 'pause on exceptions' menuitem command. + */ + _togglePauseOnExceptions: function() { + Prefs.pauseOnExceptions = + this._pauseOnExceptionsItem.getAttribute("checked") == "true"; + + DebuggerController.activeThread.pauseOnExceptions( + Prefs.pauseOnExceptions, + Prefs.ignoreCaughtExceptions); + }, + + _toggleIgnoreCaughtExceptions: function() { + Prefs.ignoreCaughtExceptions = + this._ignoreCaughtExceptionsItem.getAttribute("checked") == "true"; + + DebuggerController.activeThread.pauseOnExceptions( + Prefs.pauseOnExceptions, + Prefs.ignoreCaughtExceptions); + }, + + /** + * Listener handling the 'show panes on startup' menuitem command. + */ + _toggleShowPanesOnStartup: function() { + Prefs.panesVisibleOnStartup = + this._showPanesOnStartupItem.getAttribute("checked") == "true"; + }, + + /** + * Listener handling the 'show non-enumerables' menuitem command. + */ + _toggleShowVariablesOnlyEnum: function() { + let pref = Prefs.variablesOnlyEnumVisible = + this._showVariablesOnlyEnumItem.getAttribute("checked") == "true"; + + DebuggerView.Variables.onlyEnumVisible = pref; + }, + + /** + * Listener handling the 'show variables searchbox' menuitem command. + */ + _toggleShowVariablesFilterBox: function() { + let pref = Prefs.variablesSearchboxVisible = + this._showVariablesFilterBoxItem.getAttribute("checked") == "true"; + + DebuggerView.Variables.searchEnabled = pref; + }, + + /** + * Listener handling the 'show original source' menuitem command. + */ + _toggleShowOriginalSource: function() { + let pref = Prefs.sourceMapsEnabled = + this._showOriginalSourceItem.getAttribute("checked") == "true"; + + // Don't block the UI while reconfiguring the server. + window.once(EVENTS.OPTIONS_POPUP_HIDDEN, () => { + // The popup panel needs more time to hide after triggering onpopuphidden. + window.setTimeout(() => { + DebuggerController.reconfigureThread({ + useSourceMaps: pref, + autoBlackBox: Prefs.autoBlackBox + }); + }, POPUP_HIDDEN_DELAY); + }); + }, + + /** + * Listener handling the 'automatically black box minified sources' menuitem + * command. + */ + _toggleAutoBlackBox: function() { + let pref = Prefs.autoBlackBox = + this._autoBlackBoxItem.getAttribute("checked") == "true"; + + // Don't block the UI while reconfiguring the server. + window.once(EVENTS.OPTIONS_POPUP_HIDDEN, () => { + // The popup panel needs more time to hide after triggering onpopuphidden. + window.setTimeout(() => { + DebuggerController.reconfigureThread({ + useSourceMaps: Prefs.sourceMapsEnabled, + autoBlackBox: pref + }); + }, POPUP_HIDDEN_DELAY); + }); + }, + + _button: null, + _pauseOnExceptionsItem: null, + _showPanesOnStartupItem: null, + _showVariablesOnlyEnumItem: null, + _showVariablesFilterBoxItem: null, + _showOriginalSourceItem: null, + _autoBlackBoxItem: null +}; + +/** + * Functions handling the stackframes UI. + */ +function StackFramesView() { + dumpn("StackFramesView was instantiated"); + + this._onStackframeRemoved = this._onStackframeRemoved.bind(this); + this._onSelect = this._onSelect.bind(this); + this._onScroll = this._onScroll.bind(this); + this._afterScroll = this._afterScroll.bind(this); +} + +StackFramesView.prototype = Heritage.extend(WidgetMethods, { + /** + * Initialization function, called when the debugger is started. + */ + initialize: function() { + dumpn("Initializing the StackFramesView"); + + this.widget = new BreadcrumbsWidget(document.getElementById("stackframes")); + this.widget.addEventListener("select", this._onSelect, false); + this.widget.addEventListener("scroll", this._onScroll, true); + window.addEventListener("resize", this._onScroll, true); + + this.autoFocusOnFirstItem = false; + this.autoFocusOnSelection = false; + + // This view's contents are also mirrored in a different container. + this._mirror = DebuggerView.StackFramesClassicList; + }, + + /** + * Destruction function, called when the debugger is closed. + */ + destroy: function() { + dumpn("Destroying the StackFramesView"); + + this.widget.removeEventListener("select", this._onSelect, false); + this.widget.removeEventListener("scroll", this._onScroll, true); + window.removeEventListener("resize", this._onScroll, true); + }, + + /** + * Adds a frame in this stackframes container. + * + * @param string aTitle + * The frame title (function name). + * @param string aUrl + * The frame source url. + * @param string aLine + * The frame line number. + * @param number aDepth + * The frame depth in the stack. + * @param boolean aIsBlackBoxed + * Whether or not the frame is black boxed. + */ + addFrame: function(aTitle, aUrl, aLine, aDepth, aIsBlackBoxed) { + // Blackboxed stack frames are collapsed into a single entry in + // the view. By convention, only the first frame is displayed. + if (aIsBlackBoxed) { + if (this._prevBlackBoxedUrl == aUrl) { + return; + } + this._prevBlackBoxedUrl = aUrl; + } else { + this._prevBlackBoxedUrl = null; + } + + // Create the element node for the stack frame item. + let frameView = this._createFrameView.apply(this, arguments); + + // Append a stack frame item to this container. + this.push([frameView], { + index: 0, /* specifies on which position should the item be appended */ + attachment: { + title: aTitle, + url: aUrl, + line: aLine, + depth: aDepth + }, + // Make sure that when the stack frame item is removed, the corresponding + // mirrored item in the classic list is also removed. + finalize: this._onStackframeRemoved + }); + + // Mirror this newly inserted item inside the "Call Stack" tab. + this._mirror.addFrame(aTitle, aUrl, aLine, aDepth); + }, + + /** + * Selects the frame at the specified depth in this container. + * @param number aDepth + */ + set selectedDepth(aDepth) { + this.selectedItem = aItem => aItem.attachment.depth == aDepth; + }, + + /** + * Gets the currently selected stack frame's depth in this container. + * This will essentially be the opposite of |selectedIndex|, which deals + * with the position in the view, where the last item added is actually + * the bottommost, not topmost. + * @return number + */ + get selectedDepth() { + return this.selectedItem.attachment.depth; + }, + + /** + * Specifies if the active thread has more frames that need to be loaded. + */ + dirty: false, + + /** + * Customization function for creating an item's UI. + * + * @param string aTitle + * The frame title to be displayed in the list. + * @param string aUrl + * The frame source url. + * @param string aLine + * The frame line number. + * @param number aDepth + * The frame depth in the stack. + * @param boolean aIsBlackBoxed + * Whether or not the frame is black boxed. + * @return nsIDOMNode + * The stack frame view. + */ + _createFrameView: function(aTitle, aUrl, aLine, aDepth, aIsBlackBoxed) { + let container = document.createElement("hbox"); + container.id = "stackframe-" + aDepth; + container.className = "dbg-stackframe"; + + let frameDetails = SourceUtils.trimUrlLength( + SourceUtils.getSourceLabel(aUrl), + STACK_FRAMES_SOURCE_URL_MAX_LENGTH, + STACK_FRAMES_SOURCE_URL_TRIM_SECTION); + + if (aIsBlackBoxed) { + container.classList.add("dbg-stackframe-black-boxed"); + } else { + let frameTitleNode = document.createElement("label"); + frameTitleNode.className = "plain dbg-stackframe-title breadcrumbs-widget-item-tag"; + frameTitleNode.setAttribute("value", aTitle); + container.appendChild(frameTitleNode); + + frameDetails += SEARCH_LINE_FLAG + aLine; + } + + let frameDetailsNode = document.createElement("label"); + frameDetailsNode.className = "plain dbg-stackframe-details breadcrumbs-widget-item-id"; + frameDetailsNode.setAttribute("value", frameDetails); + container.appendChild(frameDetailsNode); + + return container; + }, + + /** + * Function called each time a stack frame item is removed. + * + * @param object aItem + * The corresponding item. + */ + _onStackframeRemoved: function(aItem) { + dumpn("Finalizing stackframe item: " + aItem.stringify()); + + // Remove the mirrored item in the classic list. + let depth = aItem.attachment.depth; + this._mirror.remove(this._mirror.getItemForAttachment(e => e.depth == depth)); + + // Forget the previously blackboxed stack frame url. + this._prevBlackBoxedUrl = null; + }, + + /** + * The select listener for the stackframes container. + */ + _onSelect: function(e) { + let stackframeItem = this.selectedItem; + if (stackframeItem) { + // The container is not empty and an actual item was selected. + let depth = stackframeItem.attachment.depth; + + // Mirror the selected item in the classic list. + this.suppressSelectionEvents = true; + this._mirror.selectedItem = e => e.attachment.depth == depth; + this.suppressSelectionEvents = false; + + DebuggerController.StackFrames.selectFrame(depth); + } + }, + + /** + * The scroll listener for the stackframes container. + */ + _onScroll: function() { + // Update the stackframes container only if we have to. + if (!this.dirty) { + return; + } + // Allow requests to settle down first. + setNamedTimeout("stack-scroll", STACK_FRAMES_SCROLL_DELAY, this._afterScroll); + }, + + /** + * Requests the addition of more frames from the controller. + */ + _afterScroll: function() { + let scrollPosition = this.widget.getAttribute("scrollPosition"); + let scrollWidth = this.widget.getAttribute("scrollWidth"); + + // If the stackframes container scrolled almost to the end, with only + // 1/10 of a breadcrumb remaining, load more content. + if (scrollPosition - scrollWidth / 10 < 1) { + this.ensureIndexIsVisible(CALL_STACK_PAGE_SIZE - 1); + this.dirty = false; + + // Loads more stack frames from the debugger server cache. + DebuggerController.StackFrames.addMoreFrames(); + } + }, + + _mirror: null, + _prevBlackBoxedUrl: null +}); + +/* + * Functions handling the stackframes classic list UI. + * Controlled by the DebuggerView.StackFrames isntance. + */ +function StackFramesClassicListView() { + dumpn("StackFramesClassicListView was instantiated"); + + this._onSelect = this._onSelect.bind(this); +} + +StackFramesClassicListView.prototype = Heritage.extend(WidgetMethods, { + /** + * Initialization function, called when the debugger is started. + */ + initialize: function() { + dumpn("Initializing the StackFramesClassicListView"); + + this.widget = new SideMenuWidget(document.getElementById("callstack-list")); + this.widget.addEventListener("select", this._onSelect, false); + + this.emptyText = L10N.getStr("noStackFramesText"); + this.autoFocusOnFirstItem = false; + this.autoFocusOnSelection = false; + + // This view's contents are also mirrored in a different container. + this._mirror = DebuggerView.StackFrames; + }, + + /** + * Destruction function, called when the debugger is closed. + */ + destroy: function() { + dumpn("Destroying the StackFramesClassicListView"); + + this.widget.removeEventListener("select", this._onSelect, false); + }, + + /** + * Adds a frame in this stackframes container. + * + * @param string aTitle + * The frame title (function name). + * @param string aUrl + * The frame source url. + * @param string aLine + * The frame line number. + * @param number aDepth + * The frame depth in the stack. + */ + addFrame: function(aTitle, aUrl, aLine, aDepth) { + // Create the element node for the stack frame item. + let frameView = this._createFrameView.apply(this, arguments); + + // Append a stack frame item to this container. + this.push([frameView], { + attachment: { + depth: aDepth + } + }); + }, + + /** + * Customization function for creating an item's UI. + * + * @param string aTitle + * The frame title to be displayed in the list. + * @param string aUrl + * The frame source url. + * @param string aLine + * The frame line number. + * @param number aDepth + * The frame depth in the stack. + * @return nsIDOMNode + * The stack frame view. + */ + _createFrameView: function(aTitle, aUrl, aLine, aDepth) { + let container = document.createElement("hbox"); + container.id = "classic-stackframe-" + aDepth; + container.className = "dbg-classic-stackframe"; + container.setAttribute("flex", "1"); + + let frameTitleNode = document.createElement("label"); + frameTitleNode.className = "plain dbg-classic-stackframe-title"; + frameTitleNode.setAttribute("value", aTitle); + frameTitleNode.setAttribute("crop", "center"); + + let frameDetailsNode = document.createElement("hbox"); + frameDetailsNode.className = "plain dbg-classic-stackframe-details"; + + let frameUrlNode = document.createElement("label"); + frameUrlNode.className = "plain dbg-classic-stackframe-details-url"; + frameUrlNode.setAttribute("value", SourceUtils.getSourceLabel(aUrl)); + frameUrlNode.setAttribute("crop", "center"); + frameDetailsNode.appendChild(frameUrlNode); + + let frameDetailsSeparator = document.createElement("label"); + frameDetailsSeparator.className = "plain dbg-classic-stackframe-details-sep"; + frameDetailsSeparator.setAttribute("value", SEARCH_LINE_FLAG); + frameDetailsNode.appendChild(frameDetailsSeparator); + + let frameLineNode = document.createElement("label"); + frameLineNode.className = "plain dbg-classic-stackframe-details-line"; + frameLineNode.setAttribute("value", aLine); + frameDetailsNode.appendChild(frameLineNode); + + container.appendChild(frameTitleNode); + container.appendChild(frameDetailsNode); + + return container; + }, + + /** + * The select listener for the stackframes container. + */ + _onSelect: function(e) { + let stackframeItem = this.selectedItem; + if (stackframeItem) { + // The container is not empty and an actual item was selected. + // Mirror the selected item in the breadcrumbs list. + let depth = stackframeItem.attachment.depth; + this._mirror.selectedItem = e => e.attachment.depth == depth; + } + }, + + _mirror: null +}); + +/** + * Functions handling the filtering UI. + */ +function FilterView() { + dumpn("FilterView was instantiated"); + + this._onClick = this._onClick.bind(this); + this._onInput = this._onInput.bind(this); + this._onKeyPress = this._onKeyPress.bind(this); + this._onBlur = this._onBlur.bind(this); +} + +FilterView.prototype = { + /** + * Initialization function, called when the debugger is started. + */ + initialize: function() { + dumpn("Initializing the FilterView"); + + this._searchbox = document.getElementById("searchbox"); + this._searchboxHelpPanel = document.getElementById("searchbox-help-panel"); + this._filterLabel = document.getElementById("filter-label"); + this._globalOperatorButton = document.getElementById("global-operator-button"); + this._globalOperatorLabel = document.getElementById("global-operator-label"); + this._functionOperatorButton = document.getElementById("function-operator-button"); + this._functionOperatorLabel = document.getElementById("function-operator-label"); + this._tokenOperatorButton = document.getElementById("token-operator-button"); + this._tokenOperatorLabel = document.getElementById("token-operator-label"); + this._lineOperatorButton = document.getElementById("line-operator-button"); + this._lineOperatorLabel = document.getElementById("line-operator-label"); + this._variableOperatorButton = document.getElementById("variable-operator-button"); + this._variableOperatorLabel = document.getElementById("variable-operator-label"); + + this._fileSearchKey = ShortcutUtils.prettifyShortcut(document.getElementById("fileSearchKey")); + this._globalSearchKey = ShortcutUtils.prettifyShortcut(document.getElementById("globalSearchKey")); + this._filteredFunctionsKey = ShortcutUtils.prettifyShortcut(document.getElementById("functionSearchKey")); + this._tokenSearchKey = ShortcutUtils.prettifyShortcut(document.getElementById("tokenSearchKey")); + this._lineSearchKey = ShortcutUtils.prettifyShortcut(document.getElementById("lineSearchKey")); + this._variableSearchKey = ShortcutUtils.prettifyShortcut(document.getElementById("variableSearchKey")); + + this._searchbox.addEventListener("click", this._onClick, false); + this._searchbox.addEventListener("select", this._onInput, false); + this._searchbox.addEventListener("input", this._onInput, false); + this._searchbox.addEventListener("keypress", this._onKeyPress, false); + this._searchbox.addEventListener("blur", this._onBlur, false); + + let placeholder = L10N.getFormatStr("emptySearchText", this._fileSearchKey); + this._searchbox.setAttribute("placeholder", placeholder); + + this._globalOperatorButton.setAttribute("label", SEARCH_GLOBAL_FLAG); + this._functionOperatorButton.setAttribute("label", SEARCH_FUNCTION_FLAG); + this._tokenOperatorButton.setAttribute("label", SEARCH_TOKEN_FLAG); + this._lineOperatorButton.setAttribute("label", SEARCH_LINE_FLAG); + this._variableOperatorButton.setAttribute("label", SEARCH_VARIABLE_FLAG); + + this._filterLabel.setAttribute("value", + L10N.getFormatStr("searchPanelFilter", this._fileSearchKey)); + this._globalOperatorLabel.setAttribute("value", + L10N.getFormatStr("searchPanelGlobal", this._globalSearchKey)); + this._functionOperatorLabel.setAttribute("value", + L10N.getFormatStr("searchPanelFunction", this._filteredFunctionsKey)); + this._tokenOperatorLabel.setAttribute("value", + L10N.getFormatStr("searchPanelToken", this._tokenSearchKey)); + this._lineOperatorLabel.setAttribute("value", + L10N.getFormatStr("searchPanelGoToLine", this._lineSearchKey)); + this._variableOperatorLabel.setAttribute("value", + L10N.getFormatStr("searchPanelVariable", this._variableSearchKey)); + }, + + /** + * Destruction function, called when the debugger is closed. + */ + destroy: function() { + dumpn("Destroying the FilterView"); + + this._searchbox.removeEventListener("click", this._onClick, false); + this._searchbox.removeEventListener("select", this._onInput, false); + this._searchbox.removeEventListener("input", this._onInput, false); + this._searchbox.removeEventListener("keypress", this._onKeyPress, false); + this._searchbox.removeEventListener("blur", this._onBlur, false); + }, + + /** + * Gets the entered operator and arguments in the searchbox. + * @return array + */ + get searchData() { + let operator = "", args = []; + + let rawValue = this._searchbox.value; + let rawLength = rawValue.length; + let globalFlagIndex = rawValue.indexOf(SEARCH_GLOBAL_FLAG); + let functionFlagIndex = rawValue.indexOf(SEARCH_FUNCTION_FLAG); + let variableFlagIndex = rawValue.indexOf(SEARCH_VARIABLE_FLAG); + let tokenFlagIndex = rawValue.lastIndexOf(SEARCH_TOKEN_FLAG); + let lineFlagIndex = rawValue.lastIndexOf(SEARCH_LINE_FLAG); + + // This is not a global, function or variable search, allow file/line flags. + if (globalFlagIndex != 0 && functionFlagIndex != 0 && variableFlagIndex != 0) { + // Token search has precedence over line search. + if (tokenFlagIndex != -1) { + operator = SEARCH_TOKEN_FLAG; + args.push(rawValue.slice(0, tokenFlagIndex)); // file + args.push(rawValue.substr(tokenFlagIndex + 1, rawLength)); // token + } else if (lineFlagIndex != -1) { + operator = SEARCH_LINE_FLAG; + args.push(rawValue.slice(0, lineFlagIndex)); // file + args.push(+rawValue.substr(lineFlagIndex + 1, rawLength) || 0); // line + } else { + args.push(rawValue); + } + } + // Global searches dissalow the use of file or line flags. + else if (globalFlagIndex == 0) { + operator = SEARCH_GLOBAL_FLAG; + args.push(rawValue.slice(1)); + } + // Function searches dissalow the use of file or line flags. + else if (functionFlagIndex == 0) { + operator = SEARCH_FUNCTION_FLAG; + args.push(rawValue.slice(1)); + } + // Variable searches dissalow the use of file or line flags. + else if (variableFlagIndex == 0) { + operator = SEARCH_VARIABLE_FLAG; + args.push(rawValue.slice(1)); + } + + return [operator, args]; + }, + + /** + * Returns the current search operator. + * @return string + */ + get searchOperator() this.searchData[0], + + /** + * Returns the current search arguments. + * @return array + */ + get searchArguments() this.searchData[1], + + /** + * Clears the text from the searchbox and any changed views. + */ + clearSearch: function() { + this._searchbox.value = ""; + this.clearViews(); + }, + + /** + * Clears all the views that may pop up when searching. + */ + clearViews: function() { + DebuggerView.GlobalSearch.clearView(); + DebuggerView.FilteredSources.clearView(); + DebuggerView.FilteredFunctions.clearView(); + this._searchboxHelpPanel.hidePopup(); + }, + + /** + * Performs a line search if necessary. + * (Jump to lines in the currently visible source). + * + * @param number aLine + * The source line number to jump to. + */ + _performLineSearch: function(aLine) { + // Make sure we're actually searching for a valid line. + if (aLine) { + DebuggerView.editor.setCursor({ line: aLine - 1, ch: 0 }, "center"); + } + }, + + /** + * Performs a token search if necessary. + * (Search for tokens in the currently visible source). + * + * @param string aToken + * The source token to find. + */ + _performTokenSearch: function(aToken) { + // Make sure we're actually searching for a valid token. + if (!aToken) { + return; + } + DebuggerView.editor.find(aToken); + }, + + /** + * The click listener for the search container. + */ + _onClick: function() { + // If there's some text in the searchbox, displaying a panel would + // interfere with double/triple click default behaviors. + if (!this._searchbox.value) { + this._searchboxHelpPanel.openPopup(this._searchbox); + } + }, + + /** + * The input listener for the search container. + */ + _onInput: function() { + this.clearViews(); + + // Make sure we're actually searching for something. + if (!this._searchbox.value) { + return; + } + + // Perform the required search based on the specified operator. + switch (this.searchOperator) { + case SEARCH_GLOBAL_FLAG: + // Schedule a global search for when the user stops typing. + DebuggerView.GlobalSearch.scheduleSearch(this.searchArguments[0]); + break; + case SEARCH_FUNCTION_FLAG: + // Schedule a function search for when the user stops typing. + DebuggerView.FilteredFunctions.scheduleSearch(this.searchArguments[0]); + break; + case SEARCH_VARIABLE_FLAG: + // Schedule a variable search for when the user stops typing. + DebuggerView.Variables.scheduleSearch(this.searchArguments[0]); + break; + case SEARCH_TOKEN_FLAG: + // Schedule a file+token search for when the user stops typing. + DebuggerView.FilteredSources.scheduleSearch(this.searchArguments[0]); + this._performTokenSearch(this.searchArguments[1]); + break; + case SEARCH_LINE_FLAG: + // Schedule a file+line search for when the user stops typing. + DebuggerView.FilteredSources.scheduleSearch(this.searchArguments[0]); + this._performLineSearch(this.searchArguments[1]); + break; + default: + // Schedule a file only search for when the user stops typing. + DebuggerView.FilteredSources.scheduleSearch(this.searchArguments[0]); + break; + } + }, + + /** + * The key press listener for the search container. + */ + _onKeyPress: function(e) { + // This attribute is not implemented in Gecko at this time, see bug 680830. + e.char = String.fromCharCode(e.charCode); + + // Perform the required action based on the specified operator. + let [operator, args] = this.searchData; + let isGlobalSearch = operator == SEARCH_GLOBAL_FLAG; + let isFunctionSearch = operator == SEARCH_FUNCTION_FLAG; + let isVariableSearch = operator == SEARCH_VARIABLE_FLAG; + let isTokenSearch = operator == SEARCH_TOKEN_FLAG; + let isLineSearch = operator == SEARCH_LINE_FLAG; + let isFileOnlySearch = !operator && args.length == 1; + + // Depending on the pressed keys, determine to correct action to perform. + let actionToPerform; + + // Meta+G and Ctrl+N focus next matches. + if ((e.char == "g" && e.metaKey) || e.char == "n" && e.ctrlKey) { + actionToPerform = "selectNext"; + } + // Meta+Shift+G and Ctrl+P focus previous matches. + else if ((e.char == "G" && e.metaKey) || e.char == "p" && e.ctrlKey) { + actionToPerform = "selectPrev"; + } + // Return, enter, down and up keys focus next or previous matches, while + // the escape key switches focus from the search container. + else switch (e.keyCode) { + case e.DOM_VK_RETURN: + var isReturnKey = true; + // If the shift key is pressed, focus on the previous result + actionToPerform = e.shiftKey ? "selectPrev" : "selectNext"; + break; + case e.DOM_VK_DOWN: + actionToPerform = "selectNext"; + break; + case e.DOM_VK_UP: + actionToPerform = "selectPrev"; + break; + } + + // If there's no action to perform, or no operator, file line or token + // were specified, then this is either a broken or empty search. + if (!actionToPerform || (!operator && !args.length)) { + DebuggerView.editor.dropSelection(); + return; + } + + e.preventDefault(); + e.stopPropagation(); + + // Jump to the next/previous entry in the global search, or perform + // a new global search immediately + if (isGlobalSearch) { + let targetView = DebuggerView.GlobalSearch; + if (!isReturnKey) { + targetView[actionToPerform](); + } else if (targetView.hidden) { + targetView.scheduleSearch(args[0], 0); + } + return; + } + + // Jump to the next/previous entry in the function search, perform + // a new function search immediately, or clear it. + if (isFunctionSearch) { + let targetView = DebuggerView.FilteredFunctions; + if (!isReturnKey) { + targetView[actionToPerform](); + } else if (targetView.hidden) { + targetView.scheduleSearch(args[0], 0); + } else { + if (!targetView.selectedItem) { + targetView.selectedIndex = 0; + } + this.clearSearch(); + } + return; + } + + // Perform a new variable search immediately. + if (isVariableSearch) { + let targetView = DebuggerView.Variables; + if (isReturnKey) { + targetView.scheduleSearch(args[0], 0); + } + return; + } + + // Jump to the next/previous entry in the file search, perform + // a new file search immediately, or clear it. + if (isFileOnlySearch) { + let targetView = DebuggerView.FilteredSources; + if (!isReturnKey) { + targetView[actionToPerform](); + } else if (targetView.hidden) { + targetView.scheduleSearch(args[0], 0); + } else { + if (!targetView.selectedItem) { + targetView.selectedIndex = 0; + } + this.clearSearch(); + } + return; + } + + // Jump to the next/previous instance of the currently searched token. + if (isTokenSearch) { + let methods = { selectNext: "findNext", selectPrev: "findPrev" }; + DebuggerView.editor[methods[actionToPerform]](); + return; + } + + // Increment/decrement the currently searched caret line. + if (isLineSearch) { + let [, line] = args; + let amounts = { selectNext: 1, selectPrev: -1 }; + + // Modify the line number and jump to it. + line += !isReturnKey ? amounts[actionToPerform] : 0; + let lineCount = DebuggerView.editor.lineCount(); + let lineTarget = line < 1 ? 1 : line > lineCount ? lineCount : line; + this._doSearch(SEARCH_LINE_FLAG, lineTarget); + return; + } + }, + + /** + * The blur listener for the search container. + */ + _onBlur: function() { + this.clearViews(); + }, + + /** + * Called when a filtering key sequence was pressed. + * + * @param string aOperator + * The operator to use for filtering. + */ + _doSearch: function(aOperator = "", aText = "") { + this._searchbox.focus(); + this._searchbox.value = ""; // Need to clear value beforehand. Bug 779738. + + if (aText) { + this._searchbox.value = aOperator + aText; + return; + } + if (DebuggerView.editor.somethingSelected()) { + this._searchbox.value = aOperator + DebuggerView.editor.getSelection(); + return; + } + if (SEARCH_AUTOFILL.indexOf(aOperator) != -1) { + let cursor = DebuggerView.editor.getCursor(); + let content = DebuggerView.editor.getText(); + let location = DebuggerView.Sources.selectedItem.attachment.source.url; + let source = DebuggerController.Parser.get(content, location); + let identifier = source.getIdentifierAt({ line: cursor.line+1, column: cursor.ch }); + + if (identifier && identifier.name) { + this._searchbox.value = aOperator + identifier.name; + this._searchbox.select(); + this._searchbox.selectionStart += aOperator.length; + return; + } + } + this._searchbox.value = aOperator; + }, + + /** + * Called when the source location filter key sequence was pressed. + */ + _doFileSearch: function() { + this._doSearch(); + this._searchboxHelpPanel.openPopup(this._searchbox); + }, + + /** + * Called when the global search filter key sequence was pressed. + */ + _doGlobalSearch: function() { + this._doSearch(SEARCH_GLOBAL_FLAG); + this._searchboxHelpPanel.hidePopup(); + }, + + /** + * Called when the source function filter key sequence was pressed. + */ + _doFunctionSearch: function() { + this._doSearch(SEARCH_FUNCTION_FLAG); + this._searchboxHelpPanel.hidePopup(); + }, + + /** + * Called when the source token filter key sequence was pressed. + */ + _doTokenSearch: function() { + this._doSearch(SEARCH_TOKEN_FLAG); + this._searchboxHelpPanel.hidePopup(); + }, + + /** + * Called when the source line filter key sequence was pressed. + */ + _doLineSearch: function() { + this._doSearch(SEARCH_LINE_FLAG); + this._searchboxHelpPanel.hidePopup(); + }, + + /** + * Called when the variable search filter key sequence was pressed. + */ + _doVariableSearch: function() { + this._doSearch(SEARCH_VARIABLE_FLAG); + this._searchboxHelpPanel.hidePopup(); + }, + + /** + * Called when the variables focus key sequence was pressed. + */ + _doVariablesFocus: function() { + DebuggerView.showInstrumentsPane(); + DebuggerView.Variables.focusFirstVisibleItem(); + }, + + _searchbox: null, + _searchboxHelpPanel: null, + _globalOperatorButton: null, + _globalOperatorLabel: null, + _functionOperatorButton: null, + _functionOperatorLabel: null, + _tokenOperatorButton: null, + _tokenOperatorLabel: null, + _lineOperatorButton: null, + _lineOperatorLabel: null, + _variableOperatorButton: null, + _variableOperatorLabel: null, + _fileSearchKey: "", + _globalSearchKey: "", + _filteredFunctionsKey: "", + _tokenSearchKey: "", + _lineSearchKey: "", + _variableSearchKey: "", +}; + +/** + * Functions handling the filtered sources UI. + */ +function FilteredSourcesView() { + dumpn("FilteredSourcesView was instantiated"); + + this._onClick = this._onClick.bind(this); + this._onSelect = this._onSelect.bind(this); +} + +FilteredSourcesView.prototype = Heritage.extend(ResultsPanelContainer.prototype, { + /** + * Initialization function, called when the debugger is started. + */ + initialize: function() { + dumpn("Initializing the FilteredSourcesView"); + + this.anchor = document.getElementById("searchbox"); + this.widget.addEventListener("select", this._onSelect, false); + this.widget.addEventListener("click", this._onClick, false); + }, + + /** + * Destruction function, called when the debugger is closed. + */ + destroy: function() { + dumpn("Destroying the FilteredSourcesView"); + + this.widget.removeEventListener("select", this._onSelect, false); + this.widget.removeEventListener("click", this._onClick, false); + this.anchor = null; + }, + + /** + * Schedules searching for a source. + * + * @param string aToken + * The function to search for. + * @param number aWait + * The amount of milliseconds to wait until draining. + */ + scheduleSearch: function(aToken, aWait) { + // The amount of time to wait for the requests to settle. + let maxDelay = FILE_SEARCH_ACTION_MAX_DELAY; + let delay = aWait === undefined ? maxDelay / aToken.length : aWait; + + // Allow requests to settle down first. + setNamedTimeout("sources-search", delay, () => this._doSearch(aToken)); + }, + + /** + * Finds file matches in all the displayed sources. + * + * @param string aToken + * The string to search for. + */ + _doSearch: function(aToken, aStore = []) { + // Don't continue filtering if the searched token is an empty string. + // In contrast with function searching, in this case we don't want to + // show a list of all the files when no search token was supplied. + if (!aToken) { + return; + } + + for (let item of DebuggerView.Sources.items) { + let lowerCaseLabel = item.attachment.label.toLowerCase(); + let lowerCaseToken = aToken.toLowerCase(); + if (lowerCaseLabel.match(lowerCaseToken)) { + aStore.push(item); + } + + // Once the maximum allowed number of results is reached, proceed + // with building the UI immediately. + if (aStore.length >= RESULTS_PANEL_MAX_RESULTS) { + this._syncView(aStore); + return; + } + } + + // Couldn't reach the maximum allowed number of results, but that's ok, + // continue building the UI. + this._syncView(aStore); + }, + + /** + * Updates the list of sources displayed in this container. + * + * @param array aSearchResults + * The results array, containing search details for each source. + */ + _syncView: function(aSearchResults) { + // If there are no matches found, keep the popup hidden and avoid + // creating the view. + if (!aSearchResults.length) { + window.emit(EVENTS.FILE_SEARCH_MATCH_NOT_FOUND); + return; + } + + for (let item of aSearchResults) { + let url = item.attachment.source.url; + + if (url) { + // Create the element node for the location item. + let itemView = this._createItemView( + SourceUtils.trimUrlLength(item.attachment.label), + SourceUtils.trimUrlLength(url, 0, "start") + ); + + // Append a location item to this container for each match. + this.push([itemView], { + index: -1, /* specifies on which position should the item be appended */ + attachment: { + url: url + } + }); + } + } + + // There's at least one item displayed in this container. Don't select it + // automatically if not forced (by tests) or in tandem with an operator. + if (this._autoSelectFirstItem || DebuggerView.Filtering.searchOperator) { + this.selectedIndex = 0; + } + this.hidden = false; + + // Signal that file search matches were found and displayed. + window.emit(EVENTS.FILE_SEARCH_MATCH_FOUND); + }, + + /** + * The click listener for this container. + */ + _onClick: function(e) { + let locationItem = this.getItemForElement(e.target); + if (locationItem) { + this.selectedItem = locationItem; + DebuggerView.Filtering.clearSearch(); + } + }, + + /** + * The select listener for this container. + * + * @param object aItem + * The item associated with the element to select. + */ + _onSelect: function({ detail: locationItem }) { + if (locationItem) { + let actor = DebuggerView.Sources.getActorForLocation({ url: locationItem.attachment.url }); + DebuggerView.setEditorLocation(actor, undefined, { + noCaret: true, + noDebug: true + }); + } + } +}); + +/** + * Functions handling the function search UI. + */ +function FilteredFunctionsView() { + dumpn("FilteredFunctionsView was instantiated"); + + this._onClick = this._onClick.bind(this); + this._onSelect = this._onSelect.bind(this); +} + +FilteredFunctionsView.prototype = Heritage.extend(ResultsPanelContainer.prototype, { + /** + * Initialization function, called when the debugger is started. + */ + initialize: function() { + dumpn("Initializing the FilteredFunctionsView"); + + this.anchor = document.getElementById("searchbox"); + this.widget.addEventListener("select", this._onSelect, false); + this.widget.addEventListener("click", this._onClick, false); + }, + + /** + * Destruction function, called when the debugger is closed. + */ + destroy: function() { + dumpn("Destroying the FilteredFunctionsView"); + + this.widget.removeEventListener("select", this._onSelect, false); + this.widget.removeEventListener("click", this._onClick, false); + this.anchor = null; + }, + + /** + * Schedules searching for a function in all of the sources. + * + * @param string aToken + * The function to search for. + * @param number aWait + * The amount of milliseconds to wait until draining. + */ + scheduleSearch: function(aToken, aWait) { + // The amount of time to wait for the requests to settle. + let maxDelay = FUNCTION_SEARCH_ACTION_MAX_DELAY; + let delay = aWait === undefined ? maxDelay / aToken.length : aWait; + + // Allow requests to settle down first. + setNamedTimeout("function-search", delay, () => { + // Start fetching as many sources as possible, then perform the search. + let actors = DebuggerView.Sources.values; + let sourcesFetched = DebuggerController.SourceScripts.getTextForSources(actors); + sourcesFetched.then(aSources => this._doSearch(aToken, aSources)); + }); + }, + + /** + * Finds function matches in all the sources stored in the cache, and groups + * them by location and line number. + * + * @param string aToken + * The string to search for. + * @param array aSources + * An array of [url, text] tuples for each source. + */ + _doSearch: function(aToken, aSources, aStore = []) { + // Continue parsing even if the searched token is an empty string, to + // cache the syntax tree nodes generated by the reflection API. + + // Make sure the currently displayed source is parsed first. Once the + // maximum allowed number of results are found, parsing will be halted. + let currentActor = DebuggerView.Sources.selectedValue; + let currentSource = aSources.filter(([actor]) => actor == currentActor)[0]; + aSources.splice(aSources.indexOf(currentSource), 1); + aSources.unshift(currentSource); + + // If not searching for a specific function, only parse the displayed source, + // which is now the first item in the sources array. + if (!aToken) { + aSources.splice(1); + } + + for (let [actor, contents] of aSources) { + let item = DebuggerView.Sources.getItemByValue(actor); + let url = item.attachment.source.url; + if (!url) { + continue; + } + + let parsedSource = DebuggerController.Parser.get(contents, url); + let sourceResults = parsedSource.getNamedFunctionDefinitions(aToken); + + for (let scriptResult of sourceResults) { + for (let parseResult of scriptResult) { + aStore.push({ + sourceUrl: scriptResult.sourceUrl, + scriptOffset: scriptResult.scriptOffset, + functionName: parseResult.functionName, + functionLocation: parseResult.functionLocation, + inferredName: parseResult.inferredName, + inferredChain: parseResult.inferredChain, + inferredLocation: parseResult.inferredLocation + }); + + // Once the maximum allowed number of results is reached, proceed + // with building the UI immediately. + if (aStore.length >= RESULTS_PANEL_MAX_RESULTS) { + this._syncView(aStore); + return; + } + } + } + } + + // Couldn't reach the maximum allowed number of results, but that's ok, + // continue building the UI. + this._syncView(aStore); + }, + + /** + * Updates the list of functions displayed in this container. + * + * @param array aSearchResults + * The results array, containing search details for each source. + */ + _syncView: function(aSearchResults) { + // If there are no matches found, keep the popup hidden and avoid + // creating the view. + if (!aSearchResults.length) { + window.emit(EVENTS.FUNCTION_SEARCH_MATCH_NOT_FOUND); + return; + } + + for (let item of aSearchResults) { + // Some function expressions don't necessarily have a name, but the + // parser provides us with an inferred name from an enclosing + // VariableDeclarator, AssignmentExpression, ObjectExpression node. + if (item.functionName && item.inferredName && + item.functionName != item.inferredName) { + let s = " " + L10N.getStr("functionSearchSeparatorLabel") + " "; + item.displayedName = item.inferredName + s + item.functionName; + } + // The function doesn't have an explicit name, but it could be inferred. + else if (item.inferredName) { + item.displayedName = item.inferredName; + } + // The function only has an explicit name. + else { + item.displayedName = item.functionName; + } + + // Some function expressions have unexpected bounds, since they may not + // necessarily have an associated name defining them. + if (item.inferredLocation) { + item.actualLocation = item.inferredLocation; + } else { + item.actualLocation = item.functionLocation; + } + + // Create the element node for the function item. + let itemView = this._createItemView( + SourceUtils.trimUrlLength(item.displayedName + "()"), + SourceUtils.trimUrlLength(item.sourceUrl, 0, "start"), + (item.inferredChain || []).join(".") + ); + + // Append a function item to this container for each match. + this.push([itemView], { + index: -1, /* specifies on which position should the item be appended */ + attachment: item + }); + } + + // There's at least one item displayed in this container. Don't select it + // automatically if not forced (by tests). + if (this._autoSelectFirstItem) { + this.selectedIndex = 0; + } + this.hidden = false; + + // Signal that function search matches were found and displayed. + window.emit(EVENTS.FUNCTION_SEARCH_MATCH_FOUND); + }, + + /** + * The click listener for this container. + */ + _onClick: function(e) { + let functionItem = this.getItemForElement(e.target); + if (functionItem) { + this.selectedItem = functionItem; + DebuggerView.Filtering.clearSearch(); + } + }, + + /** + * The select listener for this container. + */ + _onSelect: function({ detail: functionItem }) { + if (functionItem) { + let sourceUrl = functionItem.attachment.sourceUrl; + let actor = DebuggerView.Sources.getActorForLocation({ url: sourceUrl }); + let scriptOffset = functionItem.attachment.scriptOffset; + let actualLocation = functionItem.attachment.actualLocation; + + DebuggerView.setEditorLocation(actor, actualLocation.start.line, { + charOffset: scriptOffset, + columnOffset: actualLocation.start.column, + align: "center", + noDebug: true + }); + } + }, + + _searchTimeout: null, + _searchFunction: null, + _searchedToken: "" +}); + +/** + * Preliminary setup for the DebuggerView object. + */ +DebuggerView.Toolbar = new ToolbarView(); +DebuggerView.Options = new OptionsView(); +DebuggerView.Filtering = new FilterView(); +DebuggerView.FilteredSources = new FilteredSourcesView(); +DebuggerView.FilteredFunctions = new FilteredFunctionsView(); +DebuggerView.StackFrames = new StackFramesView(); +DebuggerView.StackFramesClassicList = new StackFramesClassicListView(); diff --git a/toolkit/devtools/debugger/debugger-view.js b/toolkit/devtools/debugger/debugger-view.js new file mode 100644 index 000000000..6333fe264 --- /dev/null +++ b/toolkit/devtools/debugger/debugger-view.js @@ -0,0 +1,847 @@ +/* -*- 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 SOURCE_SYNTAX_HIGHLIGHT_MAX_FILE_SIZE = 1048576; // 1 MB in bytes +const SOURCE_URL_DEFAULT_MAX_LENGTH = 64; // chars +const STACK_FRAMES_SOURCE_URL_MAX_LENGTH = 15; // chars +const STACK_FRAMES_SOURCE_URL_TRIM_SECTION = "center"; +const STACK_FRAMES_SCROLL_DELAY = 100; // ms +const BREAKPOINT_LINE_TOOLTIP_MAX_LENGTH = 1000; // chars +const BREAKPOINT_CONDITIONAL_POPUP_POSITION = "before_start"; +const BREAKPOINT_CONDITIONAL_POPUP_OFFSET_X = 7; // px +const BREAKPOINT_CONDITIONAL_POPUP_OFFSET_Y = -3; // px +const RESULTS_PANEL_POPUP_POSITION = "before_end"; +const RESULTS_PANEL_MAX_RESULTS = 10; +const FILE_SEARCH_ACTION_MAX_DELAY = 300; // ms +const GLOBAL_SEARCH_EXPAND_MAX_RESULTS = 50; +const GLOBAL_SEARCH_LINE_MAX_LENGTH = 300; // chars +const GLOBAL_SEARCH_ACTION_MAX_DELAY = 1500; // ms +const FUNCTION_SEARCH_ACTION_MAX_DELAY = 400; // ms +const SEARCH_GLOBAL_FLAG = "!"; +const SEARCH_FUNCTION_FLAG = "@"; +const SEARCH_TOKEN_FLAG = "#"; +const SEARCH_LINE_FLAG = ":"; +const SEARCH_VARIABLE_FLAG = "*"; +const SEARCH_AUTOFILL = [SEARCH_GLOBAL_FLAG, SEARCH_FUNCTION_FLAG, SEARCH_TOKEN_FLAG]; +const EDITOR_VARIABLE_HOVER_DELAY = 750; // ms +const EDITOR_VARIABLE_POPUP_POSITION = "topcenter bottomleft"; +const TOOLBAR_ORDER_POPUP_POSITION = "topcenter bottomleft"; + +/** + * Object defining the debugger view components. + */ +let DebuggerView = { + /** + * Initializes the debugger view. + * + * @return object + * A promise that is resolved when the view finishes initializing. + */ + initialize: function() { + if (this._startup) { + return this._startup; + } + + let deferred = promise.defer(); + this._startup = deferred.promise; + + this._initializePanes(); + this.Toolbar.initialize(); + this.Options.initialize(); + this.Filtering.initialize(); + this.FilteredSources.initialize(); + this.FilteredFunctions.initialize(); + this.StackFrames.initialize(); + this.StackFramesClassicList.initialize(); + this.Sources.initialize(); + this.VariableBubble.initialize(); + this.Tracer.initialize(); + this.WatchExpressions.initialize(); + this.EventListeners.initialize(); + this.GlobalSearch.initialize(); + this._initializeVariablesView(); + this._initializeEditor(deferred.resolve); + + document.title = L10N.getStr("DebuggerWindowTitle"); + + return deferred.promise; + }, + + /** + * Destroys the debugger view. + * + * @return object + * A promise that is resolved when the view finishes destroying. + */ + destroy: function() { + if (this._shutdown) { + return this._shutdown; + } + + let deferred = promise.defer(); + this._shutdown = deferred.promise; + + this.Toolbar.destroy(); + this.Options.destroy(); + this.Filtering.destroy(); + this.FilteredSources.destroy(); + this.FilteredFunctions.destroy(); + this.StackFrames.destroy(); + this.StackFramesClassicList.destroy(); + this.Sources.destroy(); + this.VariableBubble.destroy(); + this.Tracer.destroy(); + this.WatchExpressions.destroy(); + this.EventListeners.destroy(); + this.GlobalSearch.destroy(); + this._destroyPanes(); + this._destroyEditor(deferred.resolve); + + return deferred.promise; + }, + + /** + * Initializes the UI for all the displayed panes. + */ + _initializePanes: function() { + dumpn("Initializing the DebuggerView panes"); + + this._body = document.getElementById("body"); + this._editorDeck = document.getElementById("editor-deck"); + this._sourcesPane = document.getElementById("sources-pane"); + this._instrumentsPane = document.getElementById("instruments-pane"); + this._instrumentsPaneToggleButton = document.getElementById("instruments-pane-toggle"); + + this.showEditor = this.showEditor.bind(this); + this.showBlackBoxMessage = this.showBlackBoxMessage.bind(this); + this.showProgressBar = this.showProgressBar.bind(this); + this.maybeShowBlackBoxMessage = this.maybeShowBlackBoxMessage.bind(this); + + this._onTabSelect = this._onInstrumentsPaneTabSelect.bind(this); + this._instrumentsPane.tabpanels.addEventListener("select", this._onTabSelect); + + this._collapsePaneString = L10N.getStr("collapsePanes"); + this._expandPaneString = L10N.getStr("expandPanes"); + + this._sourcesPane.setAttribute("width", Prefs.sourcesWidth); + this._instrumentsPane.setAttribute("width", Prefs.instrumentsWidth); + this.toggleInstrumentsPane({ visible: Prefs.panesVisibleOnStartup }); + + // Side hosts requires a different arrangement of the debugger widgets. + if (gHostType == "side") { + this.handleHostChanged(gHostType); + } + }, + + /** + * Destroys the UI for all the displayed panes. + */ + _destroyPanes: function() { + dumpn("Destroying the DebuggerView panes"); + + if (gHostType != "side") { + Prefs.sourcesWidth = this._sourcesPane.getAttribute("width"); + Prefs.instrumentsWidth = this._instrumentsPane.getAttribute("width"); + } + + this._sourcesPane = null; + this._instrumentsPane = null; + this._instrumentsPaneToggleButton = null; + }, + + /** + * Initializes the VariablesView instance and attaches a controller. + */ + _initializeVariablesView: function() { + this.Variables = new VariablesView(document.getElementById("variables"), { + searchPlaceholder: L10N.getStr("emptyVariablesFilterText"), + emptyText: L10N.getStr("emptyVariablesText"), + onlyEnumVisible: Prefs.variablesOnlyEnumVisible, + searchEnabled: Prefs.variablesSearchboxVisible, + eval: (variable, value) => { + let string = variable.evaluationMacro(variable, value); + DebuggerController.StackFrames.evaluate(string); + }, + lazyEmpty: true + }); + + // Attach the current toolbox to the VView so it can link DOMNodes to + // the inspector/highlighter + this.Variables.toolbox = DebuggerController._toolbox; + + // Attach a controller that handles interfacing with the debugger protocol. + VariablesViewController.attach(this.Variables, { + getEnvironmentClient: aObject => gThreadClient.environment(aObject), + getObjectClient: aObject => { + return aObject instanceof DebuggerController.Tracer.WrappedObject + ? DebuggerController.Tracer.syncGripClient(aObject.object) + : gThreadClient.pauseGrip(aObject) + } + }); + + // Relay events from the VariablesView. + this.Variables.on("fetched", (aEvent, aType) => { + switch (aType) { + case "scopes": + window.emit(EVENTS.FETCHED_SCOPES); + break; + case "variables": + window.emit(EVENTS.FETCHED_VARIABLES); + break; + case "properties": + window.emit(EVENTS.FETCHED_PROPERTIES); + break; + } + }); + }, + + /** + * Initializes the Editor instance. + * + * @param function aCallback + * Called after the editor finishes initializing. + */ + _initializeEditor: function(aCallback) { + dumpn("Initializing the DebuggerView editor"); + + let extraKeys = {}; + bindKey("_doTokenSearch", "tokenSearchKey"); + bindKey("_doGlobalSearch", "globalSearchKey", { alt: true }); + bindKey("_doFunctionSearch", "functionSearchKey"); + extraKeys[Editor.keyFor("jumpToLine")] = false; + extraKeys["Esc"] = false; + + function bindKey(func, key, modifiers = {}) { + key = document.getElementById(key).getAttribute("key"); + let shortcut = Editor.accel(key, modifiers); + extraKeys[shortcut] = () => DebuggerView.Filtering[func](); + } + + let gutters = ["breakpoints"]; + if (Services.prefs.getBoolPref("devtools.debugger.tracer")) { + gutters.unshift("hit-counts"); + } + + this.editor = new Editor({ + mode: Editor.modes.text, + readOnly: true, + lineNumbers: true, + showAnnotationRuler: true, + gutters: gutters, + extraKeys: extraKeys, + contextMenu: "sourceEditorContextMenu", + enableCodeFolding: false + }); + + this.editor.appendTo(document.getElementById("editor")).then(() => { + this.editor.extend(DebuggerEditor); + this._loadingText = L10N.getStr("loadingText"); + this._onEditorLoad(aCallback); + }); + + this.editor.on("gutterClick", (ev, line, button) => { + // A right-click shouldn't do anything but keep track of where + // it was clicked. + if (button == 2) { + this.clickedLine = line; + } + else { + if (this.editor.hasBreakpoint(line)) { + this.editor.removeBreakpoint(line); + } else { + this.editor.addBreakpoint(line); + } + } + }); + }, + + /** + * The load event handler for the source editor, also executing any necessary + * post-load operations. + * + * @param function aCallback + * Called after the editor finishes loading. + */ + _onEditorLoad: function(aCallback) { + dumpn("Finished loading the DebuggerView editor"); + + DebuggerController.Breakpoints.initialize().then(() => { + window.emit(EVENTS.EDITOR_LOADED, this.editor); + aCallback(); + }); + }, + + /** + * Destroys the Editor instance and also executes any necessary + * post-unload operations. + * + * @param function aCallback + * Called after the editor finishes destroying. + */ + _destroyEditor: function(aCallback) { + dumpn("Destroying the DebuggerView editor"); + + DebuggerController.Breakpoints.destroy().then(() => { + window.emit(EVENTS.EDITOR_UNLOADED, this.editor); + this.editor.destroy(); + this.editor = null; + aCallback(); + }); + }, + + /** + * Display the source editor. + */ + showEditor: function() { + this._editorDeck.selectedIndex = 0; + }, + + /** + * Display the black box message. + */ + showBlackBoxMessage: function() { + this._editorDeck.selectedIndex = 1; + }, + + /** + * Display the progress bar. + */ + showProgressBar: function() { + this._editorDeck.selectedIndex = 2; + }, + + /** + * Show or hide the black box message vs. source editor depending on if the + * selected source is black boxed or not. + */ + maybeShowBlackBoxMessage: function() { + let { source } = DebuggerView.Sources.selectedItem.attachment; + if (gThreadClient.source(source).isBlackBoxed) { + this.showBlackBoxMessage(); + } else { + this.showEditor(); + } + }, + + /** + * Sets the currently displayed text contents in the source editor. + * This resets the mode and undo stack. + * + * @param string aTextContent + * The source text content. + */ + _setEditorText: function(aTextContent = "") { + this.editor.setMode(Editor.modes.text); + this.editor.setText(aTextContent); + this.editor.clearDebugLocation(); + this.editor.clearHistory(); + }, + + /** + * Sets the proper editor mode (JS or HTML) according to the specified + * content type, or by determining the type from the url or text content. + * + * @param string aUrl + * The source url. + * @param string aContentType [optional] + * The source content type. + * @param string aTextContent [optional] + * The source text content. + */ + _setEditorMode: function(aUrl, aContentType = "", aTextContent = "") { + // Avoid setting the editor mode for very large files. + // Is this still necessary? See bug 929225. + if (aTextContent.length >= SOURCE_SYNTAX_HIGHLIGHT_MAX_FILE_SIZE) { + return void this.editor.setMode(Editor.modes.text); + } + + // Use JS mode for files with .js and .jsm extensions. + if (SourceUtils.isJavaScript(aUrl, aContentType)) { + return void this.editor.setMode(Editor.modes.js); + } + + // Use HTML mode for files in which the first non whitespace character is + // <, regardless of extension. + if (aTextContent.match(/^\s*</)) { + return void this.editor.setMode(Editor.modes.html); + } + + // Unknown language, use text. + this.editor.setMode(Editor.modes.text); + }, + + /** + * Sets the currently displayed source text in the editor. + * + * You should use DebuggerView.updateEditor instead. It updates the current + * caret and debug location based on a requested url and line. + * + * @param object aSource + * The source object coming from the active thread. + * @param object aFlags + * Additional options for setting the source. Supported options: + * - force: boolean forcing all text to be reshown in the editor + * @return object + * A promise that is resolved after the source text has been set. + */ + _setEditorSource: function(aSource, aFlags={}) { + // Avoid setting the same source text in the editor again. + if (this._editorSource.actor == aSource.actor && !aFlags.force) { + return this._editorSource.promise; + } + let transportType = gClient.localTransport ? "_LOCAL" : "_REMOTE"; + let histogramId = "DEVTOOLS_DEBUGGER_DISPLAY_SOURCE" + transportType + "_MS"; + let histogram = Services.telemetry.getHistogramById(histogramId); + let startTime = Date.now(); + + let deferred = promise.defer(); + + this._setEditorText(L10N.getStr("loadingText")); + this._editorSource = { actor: aSource.actor, promise: deferred.promise }; + + DebuggerController.SourceScripts.getText(aSource).then(([, aText, aContentType]) => { + // Avoid setting an unexpected source. This may happen when switching + // very fast between sources that haven't been fetched yet. + if (this._editorSource.actor != aSource.actor) { + return; + } + + this._setEditorText(aText); + this._setEditorMode(aSource.url, aContentType, aText); + + // Synchronize any other components with the currently displayed + // source. + DebuggerView.Sources.selectedValue = aSource.actor; + DebuggerController.Breakpoints.updateEditorBreakpoints(); + DebuggerController.HitCounts.updateEditorHitCounts(); + + histogram.add(Date.now() - startTime); + + // Resolve and notify that a source file was shown. + window.emit(EVENTS.SOURCE_SHOWN, aSource); + deferred.resolve([aSource, aText, aContentType]); + }, + ([, aError]) => { + let msg = L10N.getStr("errorLoadingText") + DevToolsUtils.safeErrorString(aError); + this._setEditorText(msg); + Cu.reportError(msg); + dumpn(msg); + + // Reject and notify that there was an error showing the source file. + window.emit(EVENTS.SOURCE_ERROR_SHOWN, aSource); + deferred.reject([aSource, aError]); + }); + + return deferred.promise; + }, + + /** + * Update the source editor's current caret and debug location based on + * a requested url and line. + * + * @param string aActor + * The target actor id. + * @param number aLine [optional] + * The target line in the source. + * @param object aFlags [optional] + * Additional options for showing the source. Supported options: + * - charOffset: character offset for the caret or debug location + * - lineOffset: line offset for the caret or debug location + * - columnOffset: column offset for the caret or debug location + * - noCaret: don't set the caret location at the specified line + * - noDebug: don't set the debug location at the specified line + * - align: string specifying whether to align the specified line + * at the "top", "center" or "bottom" of the editor + * - force: boolean forcing all text to be reshown in the editor + * @return object + * A promise that is resolved after the source text has been set. + */ + setEditorLocation: function(aActor, aLine = 0, aFlags = {}) { + // Avoid trying to set a source for a url that isn't known yet. + if (!this.Sources.containsValue(aActor)) { + return promise.reject(new Error("Unknown source for the specified URL.")); + } + + // If the line is not specified, default to the current frame's position, + // if available and the frame's url corresponds to the requested url. + if (!aLine) { + let cachedFrames = DebuggerController.activeThread.cachedFrames; + let currentDepth = DebuggerController.StackFrames.currentFrameDepth; + let frame = cachedFrames[currentDepth]; + if (frame && frame.source.actor == aActor) { + aLine = frame.where.line; + } + } + + let sourceItem = this.Sources.getItemByValue(aActor); + let sourceForm = sourceItem.attachment.source; + + this._editorLoc = { actor: sourceForm.actor }; + + // Make sure the requested source client is shown in the editor, then + // update the source editor's caret position and debug location. + return this._setEditorSource(sourceForm, aFlags).then(([,, aContentType]) => { + if (this._editorLoc.actor !== sourceForm.actor) { + return; + } + + // Record the contentType learned from fetching + sourceForm.contentType = aContentType; + // Line numbers in the source editor should start from 1. If invalid + // or not specified, then don't do anything. + if (aLine < 1) { + window.emit(EVENTS.EDITOR_LOCATION_SET); + return; + } + if (aFlags.charOffset) { + aLine += this.editor.getPosition(aFlags.charOffset).line; + } + if (aFlags.lineOffset) { + aLine += aFlags.lineOffset; + } + if (!aFlags.noCaret) { + let location = { line: aLine -1, ch: aFlags.columnOffset || 0 }; + this.editor.setCursor(location, aFlags.align); + } + if (!aFlags.noDebug) { + this.editor.setDebugLocation(aLine - 1); + } + window.emit(EVENTS.EDITOR_LOCATION_SET); + }).then(null, console.error); + }, + + /** + * Gets the visibility state of the instruments pane. + * @return boolean + */ + get instrumentsPaneHidden() + this._instrumentsPane.hasAttribute("pane-collapsed"), + + /** + * Gets the currently selected tab in the instruments pane. + * @return string + */ + get instrumentsPaneTab() + this._instrumentsPane.selectedTab.id, + + /** + * Sets the instruments pane hidden or visible. + * + * @param object aFlags + * An object containing some of the following properties: + * - visible: true if the pane should be shown, false to hide + * - animated: true to display an animation on toggle + * - delayed: true to wait a few cycles before toggle + * - callback: a function to invoke when the toggle finishes + * @param number aTabIndex [optional] + * The index of the intended selected tab in the details pane. + */ + toggleInstrumentsPane: function(aFlags, aTabIndex) { + let pane = this._instrumentsPane; + let button = this._instrumentsPaneToggleButton; + + ViewHelpers.togglePane(aFlags, pane); + + if (aFlags.visible) { + button.removeAttribute("pane-collapsed"); + button.setAttribute("tooltiptext", this._collapsePaneString); + } else { + button.setAttribute("pane-collapsed", ""); + button.setAttribute("tooltiptext", this._expandPaneString); + } + + if (aTabIndex !== undefined) { + pane.selectedIndex = aTabIndex; + } + }, + + /** + * Sets the instruments pane visible after a short period of time. + * + * @param function aCallback + * A function to invoke when the toggle finishes. + */ + showInstrumentsPane: function(aCallback) { + DebuggerView.toggleInstrumentsPane({ + visible: true, + animated: true, + delayed: true, + callback: aCallback + }, 0); + }, + + /** + * Handles a tab selection event on the instruments pane. + */ + _onInstrumentsPaneTabSelect: function() { + if (this._instrumentsPane.selectedTab.id == "events-tab") { + DebuggerController.Breakpoints.DOM.scheduleEventListenersFetch(); + } + }, + + /** + * Handles a host change event issued by the parent toolbox. + * + * @param string aType + * The host type, either "bottom", "side" or "window". + */ + handleHostChanged: function(aType) { + let newLayout = ""; + + if (aType == "side") { + newLayout = "vertical"; + this._enterVerticalLayout(); + } else { + newLayout = "horizontal"; + this._enterHorizontalLayout(); + } + + this._hostType = aType; + this._body.setAttribute("layout", newLayout); + window.emit(EVENTS.LAYOUT_CHANGED, newLayout); + }, + + /** + * Switches the debugger widgets to a horizontal layout. + */ + _enterVerticalLayout: function() { + let normContainer = document.getElementById("debugger-widgets"); + let vertContainer = document.getElementById("vertical-layout-panes-container"); + + // Move the soruces and instruments panes in a different container. + let splitter = document.getElementById("sources-and-instruments-splitter"); + vertContainer.insertBefore(this._sourcesPane, splitter); + vertContainer.appendChild(this._instrumentsPane); + + // Make sure the vertical layout container's height doesn't repeatedly + // grow or shrink based on the displayed sources, variables etc. + vertContainer.setAttribute("height", + vertContainer.getBoundingClientRect().height); + }, + + /** + * Switches the debugger widgets to a vertical layout. + */ + _enterHorizontalLayout: function() { + let normContainer = document.getElementById("debugger-widgets"); + let vertContainer = document.getElementById("vertical-layout-panes-container"); + + // The sources and instruments pane need to be inserted at their + // previous locations in their normal container. + let splitter = document.getElementById("sources-and-editor-splitter"); + normContainer.insertBefore(this._sourcesPane, splitter); + normContainer.appendChild(this._instrumentsPane); + + // Revert to the preferred sources and instruments widths, because + // they flexed in the vertical layout. + this._sourcesPane.setAttribute("width", Prefs.sourcesWidth); + this._instrumentsPane.setAttribute("width", Prefs.instrumentsWidth); + }, + + /** + * Handles any initialization on a tab navigation event issued by the client. + */ + handleTabNavigation: function() { + dumpn("Handling tab navigation in the DebuggerView"); + + this.Filtering.clearSearch(); + this.FilteredSources.clearView(); + this.FilteredFunctions.clearView(); + this.GlobalSearch.clearView(); + this.StackFrames.empty(); + this.Sources.empty(); + this.Variables.empty(); + this.EventListeners.empty(); + + if (this.editor) { + this.editor.setMode(Editor.modes.text); + this.editor.setText(""); + this.editor.clearHistory(); + this._editorSource = {}; + } + + this.Sources.emptyText = L10N.getStr("loadingSourcesText"); + }, + + _startup: null, + _shutdown: null, + Toolbar: null, + Options: null, + Filtering: null, + FilteredSources: null, + FilteredFunctions: null, + GlobalSearch: null, + StackFrames: null, + Sources: null, + Tracer: null, + Variables: null, + VariableBubble: null, + WatchExpressions: null, + EventListeners: null, + editor: null, + _editorSource: {}, + _loadingText: "", + _body: null, + _editorDeck: null, + _sourcesPane: null, + _instrumentsPane: null, + _instrumentsPaneToggleButton: null, + _collapsePaneString: "", + _expandPaneString: "" +}; + +/** + * A custom items container, used for displaying views like the + * FilteredSources, FilteredFunctions etc., inheriting the generic WidgetMethods. + */ +function ResultsPanelContainer() { +} + +ResultsPanelContainer.prototype = Heritage.extend(WidgetMethods, { + /** + * Sets the anchor node for this container panel. + * @param nsIDOMNode aNode + */ + set anchor(aNode) { + this._anchor = aNode; + + // If the anchor node is not null, create a panel to attach to the anchor + // when showing the popup. + if (aNode) { + if (!this._panel) { + this._panel = document.createElement("panel"); + this._panel.id = "results-panel"; + this._panel.setAttribute("level", "top"); + this._panel.setAttribute("noautofocus", "true"); + this._panel.setAttribute("consumeoutsideclicks", "false"); + document.documentElement.appendChild(this._panel); + } + if (!this.widget) { + this.widget = new SimpleListWidget(this._panel); + this.autoFocusOnFirstItem = false; + this.autoFocusOnSelection = false; + this.maintainSelectionVisible = false; + } + } + // Cleanup the anchor and remove the previously created panel. + else { + this._panel.remove(); + this._panel = null; + this.widget = null; + } + }, + + /** + * Gets the anchor node for this container panel. + * @return nsIDOMNode + */ + get anchor() { + return this._anchor; + }, + + /** + * Sets the container panel hidden or visible. It's hidden by default. + * @param boolean aFlag + */ + set hidden(aFlag) { + if (aFlag) { + this._panel.hidden = true; + this._panel.hidePopup(); + } else { + this._panel.hidden = false; + this._panel.openPopup(this._anchor, this.position, this.left, this.top); + } + }, + + /** + * Gets this container's visibility state. + * @return boolean + */ + get hidden() + this._panel.state == "closed" || + this._panel.state == "hiding", + + /** + * Removes all items from this container and hides it. + */ + clearView: function() { + this.hidden = true; + this.empty(); + }, + + /** + * Selects the next found item in this container. + * Does not change the currently focused node. + */ + selectNext: function() { + let nextIndex = this.selectedIndex + 1; + if (nextIndex >= this.itemCount) { + nextIndex = 0; + } + this.selectedItem = this.getItemAtIndex(nextIndex); + }, + + /** + * Selects the previously found item in this container. + * Does not change the currently focused node. + */ + selectPrev: function() { + let prevIndex = this.selectedIndex - 1; + if (prevIndex < 0) { + prevIndex = this.itemCount - 1; + } + this.selectedItem = this.getItemAtIndex(prevIndex); + }, + + /** + * Customization function for creating an item's UI. + * + * @param string aLabel + * The item's label string. + * @param string aBeforeLabel + * An optional string shown before the label. + * @param string aBelowLabel + * An optional string shown underneath the label. + */ + _createItemView: function(aLabel, aBelowLabel, aBeforeLabel) { + let container = document.createElement("vbox"); + container.className = "results-panel-item"; + + let firstRowLabels = document.createElement("hbox"); + let secondRowLabels = document.createElement("hbox"); + + if (aBeforeLabel) { + let beforeLabelNode = document.createElement("label"); + beforeLabelNode.className = "plain results-panel-item-label-before"; + beforeLabelNode.setAttribute("value", aBeforeLabel); + firstRowLabels.appendChild(beforeLabelNode); + } + + let labelNode = document.createElement("label"); + labelNode.className = "plain results-panel-item-label"; + labelNode.setAttribute("value", aLabel); + firstRowLabels.appendChild(labelNode); + + if (aBelowLabel) { + let belowLabelNode = document.createElement("label"); + belowLabelNode.className = "plain results-panel-item-label-below"; + belowLabelNode.setAttribute("value", aBelowLabel); + secondRowLabels.appendChild(belowLabelNode); + } + + container.appendChild(firstRowLabels); + container.appendChild(secondRowLabels); + + return container; + }, + + _anchor: null, + _panel: null, + position: RESULTS_PANEL_POPUP_POSITION, + left: 0, + top: 0 +}); diff --git a/toolkit/devtools/debugger/debugger.css b/toolkit/devtools/debugger/debugger.css new file mode 100644 index 000000000..13eab6096 --- /dev/null +++ b/toolkit/devtools/debugger/debugger.css @@ -0,0 +1,50 @@ +/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +/* Side pane views */ + +#sources-pane > tabpanels > tabpanel, +#instruments-pane > tabpanels > tabpanel { + -moz-box-orient: vertical; +} + +/* Toolbar controls */ + +.devtools-toolbarbutton:not([label]) > .toolbarbutton-text { + display: none; +} + +/* Horizontal vs. vertical layout */ + +#body[layout=vertical] #debugger-widgets { + -moz-box-orient: vertical; +} + +#body[layout=vertical] #sources-pane { + -moz-box-flex: 1; +} + +#body[layout=vertical] #instruments-pane { + -moz-box-flex: 2; +} + +#body[layout=vertical] #instruments-pane-toggle { + display: none; +} + +#body[layout=vertical] #sources-and-editor-splitter, +#body[layout=vertical] #editor-and-instruments-splitter { + display: none; +} + +#body[layout=horizontal] #vertical-layout-splitter, +#body[layout=horizontal] #vertical-layout-panes-container { + display: none; +} + +#body[layout=vertical] #stackframes { + visibility: hidden; +} diff --git a/toolkit/devtools/debugger/debugger.xul b/toolkit/devtools/debugger/debugger.xul new file mode 100644 index 000000000..eb42add05 --- /dev/null +++ b/toolkit/devtools/debugger/debugger.xul @@ -0,0 +1,524 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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/. --> +<?xml-stylesheet href="chrome://browser/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://browser/content/devtools/widgets.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/content/devtools/debugger.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/devtools/common.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/devtools/widgets.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/devtools/debugger.css" type="text/css"?> +<!DOCTYPE window [ + <!ENTITY % debuggerDTD SYSTEM "chrome://browser/locale/devtools/debugger.dtd"> + %debuggerDTD; +]> +<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + macanimationtype="document" + fullscreenbutton="true" + screenX="4" screenY="4" + width="960" height="480" + persist="screenX screenY width height sizemode"> + + <script type="application/javascript;version=1.8" + src="chrome://browser/content/devtools/theme-switching.js"/> + <script type="text/javascript" src="chrome://global/content/globalOverlay.js"/> + <script type="text/javascript" src="debugger-controller.js"/> + <script type="text/javascript" src="debugger-view.js"/> + <script type="text/javascript" src="debugger-toolbar.js"/> + <script type="text/javascript" src="debugger-panes.js"/> + + <commandset id="editMenuCommands"/> + + <commandset id="debuggerCommands"> + <command id="blackBoxCommand" + oncommand="DebuggerView.Sources.toggleBlackBoxing()"/> + <command id="unBlackBoxButton" + oncommand="DebuggerView.Sources._onStopBlackBoxing()"/> + <command id="prettyPrintCommand" + oncommand="DebuggerView.Sources.togglePrettyPrint()"/> + <command id="toggleBreakpointsCommand" + oncommand="DebuggerView.Sources.toggleBreakpoints()"/> + <command id="nextSourceCommand" + oncommand="DebuggerView.Sources.selectNextItem()"/> + <command id="prevSourceCommand" + oncommand="DebuggerView.Sources.selectPrevItem()"/> + <command id="resumeCommand" + oncommand="DebuggerView.Toolbar._onResumePressed()"/> + <command id="stepOverCommand" + oncommand="DebuggerView.Toolbar._onStepOverPressed()"/> + <command id="stepInCommand" + oncommand="DebuggerView.Toolbar._onStepInPressed()"/> + <command id="stepOutCommand" + oncommand="DebuggerView.Toolbar._onStepOutPressed()"/> + <command id="fileSearchCommand" + oncommand="DebuggerView.Filtering._doFileSearch()"/> + <command id="globalSearchCommand" + oncommand="DebuggerView.Filtering._doGlobalSearch()"/> + <command id="functionSearchCommand" + oncommand="DebuggerView.Filtering._doFunctionSearch()"/> + <command id="tokenSearchCommand" + oncommand="DebuggerView.Filtering._doTokenSearch()"/> + <command id="lineSearchCommand" + oncommand="DebuggerView.Filtering._doLineSearch()"/> + <command id="variableSearchCommand" + oncommand="DebuggerView.Filtering._doVariableSearch()"/> + <command id="variablesFocusCommand" + oncommand="DebuggerView.Filtering._doVariablesFocus()"/> + <command id="addBreakpointCommand" + oncommand="DebuggerView.Sources._onCmdAddBreakpoint(event)"/> + <command id="addConditionalBreakpointCommand" + oncommand="DebuggerView.Sources._onCmdAddConditionalBreakpoint(event)"/> + <command id="addWatchExpressionCommand" + oncommand="DebuggerView.WatchExpressions._onCmdAddExpression()"/> + <command id="removeAllWatchExpressionsCommand" + oncommand="DebuggerView.WatchExpressions._onCmdRemoveAllExpressions()"/> + <command id="toggleAutoPrettyPrint" + oncommand="DebuggerView.Options._toggleAutoPrettyPrint()"/> + <command id="togglePauseOnExceptions" + oncommand="DebuggerView.Options._togglePauseOnExceptions()"/> + <command id="toggleIgnoreCaughtExceptions" + oncommand="DebuggerView.Options._toggleIgnoreCaughtExceptions()"/> + <command id="toggleShowPanesOnStartup" + oncommand="DebuggerView.Options._toggleShowPanesOnStartup()"/> + <command id="toggleShowOnlyEnum" + oncommand="DebuggerView.Options._toggleShowVariablesOnlyEnum()"/> + <command id="toggleShowVariablesFilterBox" + oncommand="DebuggerView.Options._toggleShowVariablesFilterBox()"/> + <command id="toggleShowOriginalSource" + oncommand="DebuggerView.Options._toggleShowOriginalSource()"/> + <command id="toggleAutoBlackBox" + oncommand="DebuggerView.Options._toggleAutoBlackBox()"/> + <command id="toggleTracing" + oncommand="DebuggerView.Tracer._onToggleTracing()"/> + <command id="startTracing" + oncommand="DebuggerView.Tracer._onStartTracing()"/> + <command id="clearTraces" + oncommand="DebuggerView.Tracer._onClear()"/> + </commandset> + + <popupset id="debuggerPopupset"> + <menupopup id="sourceEditorContextMenu" + onpopupshowing="goUpdateGlobalEditMenuItems()"> + <menuitem id="se-dbg-cMenu-addBreakpoint" + label="&debuggerUI.seMenuBreak;" + key="addBreakpointKey" + command="addBreakpointCommand"/> + <menuitem id="se-dbg-cMenu-addConditionalBreakpoint" + label="&debuggerUI.seMenuCondBreak;" + key="addConditionalBreakpointKey" + command="addConditionalBreakpointCommand"/> + <menuitem id="se-dbg-cMenu-addAsWatch" + label="&debuggerUI.seMenuAddWatch;" + key="addWatchExpressionKey" + command="addWatchExpressionCommand"/> + <menuseparator/> + <menuitem id="cMenu_copy"/> + <menuseparator/> + <menuitem id="cMenu_selectAll"/> + <menuseparator/> + <menuitem id="se-dbg-cMenu-findFile" + label="&debuggerUI.searchFile;" + accesskey="&debuggerUI.searchFile.accesskey;" + key="fileSearchKey" + command="fileSearchCommand"/> + <menuitem id="se-dbg-cMenu-findGlobal" + label="&debuggerUI.searchGlobal;" + accesskey="&debuggerUI.searchGlobal.accesskey;" + key="globalSearchKey" + command="globalSearchCommand"/> + <menuitem id="se-dbg-cMenu-findFunction" + label="&debuggerUI.searchFunction;" + accesskey="&debuggerUI.searchFunction.accesskey;" + key="functionSearchKey" + command="functionSearchCommand"/> + <menuseparator/> + <menuitem id="se-dbg-cMenu-findToken" + label="&debuggerUI.searchToken;" + accesskey="&debuggerUI.searchToken.accesskey;" + key="tokenSearchKey" + command="tokenSearchCommand"/> + <menuitem id="se-dbg-cMenu-findLine" + label="&debuggerUI.searchGoToLine;" + accesskey="&debuggerUI.searchGoToLine.accesskey;" + key="lineSearchKey" + command="lineSearchCommand"/> + <menuseparator/> + <menuitem id="se-dbg-cMenu-findVariable" + label="&debuggerUI.searchVariable;" + accesskey="&debuggerUI.searchVariable.accesskey;" + key="variableSearchKey" + command="variableSearchCommand"/> + <menuitem id="se-dbg-cMenu-focusVariables" + label="&debuggerUI.focusVariables;" + accesskey="&debuggerUI.focusVariables.accesskey;" + key="variablesFocusKey" + command="variablesFocusCommand"/> + <menuitem id="se-dbg-cMenu-prettyPrint" + label="&debuggerUI.sources.prettyPrint;" + command="prettyPrintCommand"/> + </menupopup> + <menupopup id="debuggerWatchExpressionsContextMenu"> + <menuitem id="add-watch-expression" + label="&debuggerUI.addWatch;" + accesskey="&debuggerUI.addWatch.accesskey;" + key="addWatchExpressionKey" + command="addWatchExpressionCommand"/> + <menuitem id="removeAll-watch-expression" + label="&debuggerUI.removeAllWatch;" + accesskey="&debuggerUI.removeAllWatch.accesskey;" + key="removeAllWatchExpressionsKey" + command="removeAllWatchExpressionsCommand"/> + </menupopup> + <menupopup id="debuggerPrefsContextMenu" + position="before_end" + onpopupshowing="DebuggerView.Options._onPopupShowing()" + onpopuphiding="DebuggerView.Options._onPopupHiding()" + onpopuphidden="DebuggerView.Options._onPopupHidden()"> + <menuitem id="auto-pretty-print" + type="checkbox" + label="&debuggerUI.autoPrettyPrint;" + accesskey="&debuggerUI.autoPrettyPrint.accesskey;" + command="toggleAutoPrettyPrint"/> + <menuitem id="pause-on-exceptions" + type="checkbox" + label="&debuggerUI.pauseExceptions;" + accesskey="&debuggerUI.pauseExceptions.accesskey;" + command="togglePauseOnExceptions"/> + <menuitem id="ignore-caught-exceptions" + type="checkbox" + label="&debuggerUI.ignoreCaughtExceptions;" + accesskey="&debuggerUI.ignoreCaughtExceptions.accesskey;" + command="toggleIgnoreCaughtExceptions"/> + <menuitem id="show-panes-on-startup" + type="checkbox" + label="&debuggerUI.showPanesOnInit;" + accesskey="&debuggerUI.showPanesOnInit.accesskey;" + command="toggleShowPanesOnStartup"/> + <menuitem id="show-vars-only-enum" + type="checkbox" + label="&debuggerUI.showOnlyEnum;" + accesskey="&debuggerUI.showOnlyEnum.accesskey;" + command="toggleShowOnlyEnum"/> + <menuitem id="show-vars-filter-box" + type="checkbox" + label="&debuggerUI.showVarsFilter;" + accesskey="&debuggerUI.showVarsFilter.accesskey;" + command="toggleShowVariablesFilterBox"/> + <menuitem id="show-original-source" + type="checkbox" + label="&debuggerUI.showOriginalSource;" + accesskey="&debuggerUI.showOriginalSource.accesskey;" + command="toggleShowOriginalSource"/> + <menuitem id="auto-black-box" + type="checkbox" + label="&debuggerUI.autoBlackBox;" + accesskey="&debuggerUI.autoBlackBox.accesskey;" + command="toggleAutoBlackBox"/> + </menupopup> + </popupset> + + <keyset id="debuggerKeys"> + <key id="nextSourceKey" + keycode="VK_DOWN" + modifiers="accel alt" + command="nextSourceCommand"/> + <key id="prevSourceKey" + keycode="VK_UP" + modifiers="accel alt" + command="prevSourceCommand"/> + <key id="resumeKey" + keycode="&debuggerUI.stepping.resume1;" + command="resumeCommand"/> + <key id="resumeKey2" + keycode="&debuggerUI.stepping.resume2;" + modifiers="accel" + command="resumeCommand"/> + <key id="stepOverKey" + keycode="&debuggerUI.stepping.stepOver1;" + command="stepOverCommand"/> + <key id="stepOverKey2" + keycode="&debuggerUI.stepping.stepOver2;" + modifiers="accel" + command="stepOverCommand"/> + <key id="stepInKey" + keycode="&debuggerUI.stepping.stepIn1;" + command="stepInCommand"/> + <key id="stepInKey2" + keycode="&debuggerUI.stepping.stepIn2;" + modifiers="accel" + command="stepInCommand"/> + <key id="stepOutKey" + keycode="&debuggerUI.stepping.stepOut1;" + modifiers="shift" + command="stepOutCommand"/> + <key id="stepOutKey2" + keycode="&debuggerUI.stepping.stepOut2;" + modifiers="accel shift" + command="stepOutCommand"/> + <key id="fileSearchKey" + key="&debuggerUI.searchFile.key;" + modifiers="accel" + command="fileSearchCommand"/> + <key id="fileSearchKey" + key="&debuggerUI.searchFile.altkey;" + modifiers="accel" + command="fileSearchCommand"/> + <key id="globalSearchKey" + key="&debuggerUI.searchGlobal.key;" + modifiers="accel alt" + command="globalSearchCommand"/> + <key id="functionSearchKey" + key="&debuggerUI.searchFunction.key;" + modifiers="accel" + command="functionSearchCommand"/> + <key id="tokenSearchKey" + key="&debuggerUI.searchToken.key;" + modifiers="accel" + command="tokenSearchCommand"/> + <key id="lineSearchKey" + key="&debuggerUI.searchGoToLine.key;" + modifiers="accel" + command="lineSearchCommand"/> + <key id="variableSearchKey" + key="&debuggerUI.searchVariable.key;" + modifiers="accel alt" + command="variableSearchCommand"/> + <key id="variablesFocusKey" + key="&debuggerUI.focusVariables.key;" + modifiers="accel shift" + command="variablesFocusCommand"/> + <key id="addBreakpointKey" + key="&debuggerUI.seMenuBreak.key;" + modifiers="accel" + command="addBreakpointCommand"/> + <key id="addConditionalBreakpointKey" + key="&debuggerUI.seMenuCondBreak.key;" + modifiers="accel shift" + command="addConditionalBreakpointCommand"/> + <key id="addWatchExpressionKey" + key="&debuggerUI.seMenuAddWatch.key;" + modifiers="accel shift" + command="addWatchExpressionCommand"/> + <key id="removeAllWatchExpressionsKey" + key="&debuggerUI.removeAllWatch.key;" + modifiers="accel alt" + command="removeAllWatchExpressionsCommand"/> + </keyset> + + <vbox id="body" + class="theme-body" + layout="horizontal" + flex="1"> + <toolbar id="debugger-toolbar" + class="devtools-toolbar"> + <hbox id="debugger-controls" + class="devtools-toolbarbutton-group"> + <toolbarbutton id="resume" + class="devtools-toolbarbutton" + tabindex="0"/> + <toolbarbutton id="step-over" + class="devtools-toolbarbutton" + tabindex="0"/> + <toolbarbutton id="step-in" + class="devtools-toolbarbutton" + tabindex="0"/> + <toolbarbutton id="step-out" + class="devtools-toolbarbutton" + tabindex="0"/> + </hbox> + <hbox> + <toolbarbutton id="trace" + class="devtools-toolbarbutton" + command="toggleTracing" + tabindex="0" + hidden="true"/> + </hbox> + <vbox id="stackframes" flex="1"/> + <textbox id="searchbox" + class="devtools-searchinput" type="search"/> + <toolbarbutton id="instruments-pane-toggle" + class="devtools-toolbarbutton" + tooltiptext="&debuggerUI.panesButton.tooltip;" + tabindex="0"/> + <toolbarbutton id="debugger-options" + class="devtools-toolbarbutton devtools-option-toolbarbutton" + tooltiptext="&debuggerUI.optsButton.tooltip;" + popup="debuggerPrefsContextMenu" + tabindex="0"/> + </toolbar> + <vbox id="globalsearch" orient="vertical" hidden="true"/> + <splitter class="devtools-horizontal-splitter" hidden="true"/> + <hbox id="debugger-widgets" flex="1"> + <tabbox id="sources-pane" + class="devtools-sidebar-tabs"> + <tabs> + <tab id="sources-tab" label="&debuggerUI.tabs.sources;"/> + <tab id="callstack-tab" label="&debuggerUI.tabs.callstack;"/> + <tab id="tracer-tab" label="&debuggerUI.tabs.traces;" hidden="true"/> + </tabs> + <tabpanels flex="1"> + <tabpanel id="sources-tabpanel"> + <vbox id="sources" flex="1"/> + <toolbar id="sources-toolbar" class="devtools-toolbar"> + <hbox id="sources-controls" + class="devtools-toolbarbutton-group"> + <toolbarbutton id="black-box" + class="devtools-toolbarbutton" + tooltiptext="&debuggerUI.sources.blackBoxTooltip;" + command="blackBoxCommand"/> + <toolbarbutton id="pretty-print" + class="devtools-toolbarbutton" + tooltiptext="&debuggerUI.sources.prettyPrint;" + command="prettyPrintCommand" + hidden="true"/> + </hbox> + <vbox class="devtools-separator"/> + <toolbarbutton id="toggle-breakpoints" + class="devtools-toolbarbutton" + tooltiptext="&debuggerUI.sources.toggleBreakpoints;" + command="toggleBreakpointsCommand"/> + </toolbar> + </tabpanel> + <tabpanel id="callstack-tabpanel"> + <vbox id="callstack-list" flex="1"/> + </tabpanel> + <tabpanel id="tracer-tabpanel"> + <vbox id="tracer-traces" flex="1"/> + <hbox class="trace-item-template" hidden="true"> + <hbox class="trace-item" align="center" flex="1" crop="end"> + <label class="trace-type plain"/> + <label class="trace-name plain" crop="end"/> + </hbox> + </hbox> + <toolbar id="tracer-toolbar" class="devtools-toolbar"> + <toolbarbutton id="clear-tracer" + label="&debuggerUI.clearButton;" + tooltiptext="&debuggerUI.clearButton.tooltip;" + command="clearTraces" + class="devtools-toolbarbutton"/> + <textbox id="tracer-search" + class="devtools-searchinput" + flex="1" + type="search"/> + </toolbar> + </tabpanel> + </tabpanels> + </tabbox> + <splitter id="sources-and-editor-splitter" + class="devtools-side-splitter"/> + <deck id="editor-deck" flex="1" class="devtools-main-content"> + <vbox id="editor"/> + <vbox id="black-boxed-message" + align="center" + pack="center"> + <description id="black-boxed-message-label"> + &debuggerUI.blackBoxMessage.label; + </description> + <button id="black-boxed-message-button" + class="devtools-toolbarbutton" + label="&debuggerUI.blackBoxMessage.unBlackBoxButton;" + command="unBlackBoxCommand"/> + </vbox> + <vbox id="source-progress-container" + align="center" + pack="center"> + <progressmeter id="source-progress" + mode="undetermined"/> + </vbox> + </deck> + <splitter id="editor-and-instruments-splitter" + class="devtools-side-splitter"/> + <tabbox id="instruments-pane" + class="devtools-sidebar-tabs" + hidden="true"> + <tabs> + <tab id="variables-tab" label="&debuggerUI.tabs.variables;"/> + <tab id="events-tab" label="&debuggerUI.tabs.events;"/> + </tabs> + <tabpanels flex="1"> + <tabpanel id="variables-tabpanel"> + <vbox id="expressions"/> + <splitter class="devtools-horizontal-splitter"/> + <vbox id="variables" flex="1"/> + </tabpanel> + <tabpanel id="events-tabpanel"> + <vbox id="event-listeners" flex="1"/> + </tabpanel> + </tabpanels> + </tabbox> + <splitter id="vertical-layout-splitter" + class="devtools-horizontal-splitter"/> + <hbox id="vertical-layout-panes-container"> + <splitter id="sources-and-instruments-splitter" + class="devtools-side-splitter"/> + <!-- The sources-pane and instruments-pane will be moved in this + container if the toolbox's host requires it. --> + </hbox> + </hbox> + </vbox> + + <panel id="searchbox-help-panel" + level="top" + type="arrow" + position="before_start" + noautofocus="true" + consumeoutsideclicks="false"> + <vbox> + <hbox> + <label id="filter-label"/> + </hbox> + <label id="searchbox-panel-operators" + value="&debuggerUI.searchPanelOperators;"/> + <hbox align="center"> + <button id="global-operator-button" + class="searchbox-panel-operator-button devtools-monospace" + command="globalSearchCommand"/> + <label id="global-operator-label" + class="plain searchbox-panel-operator-label"/> + </hbox> + <hbox align="center"> + <button id="function-operator-button" + class="searchbox-panel-operator-button devtools-monospace" + command="functionSearchCommand"/> + <label id="function-operator-label" + class="plain searchbox-panel-operator-label"/> + </hbox> + <hbox align="center"> + <button id="token-operator-button" + class="searchbox-panel-operator-button devtools-monospace" + command="tokenSearchCommand"/> + <label id="token-operator-label" + class="plain searchbox-panel-operator-label"/> + </hbox> + <hbox align="center"> + <button id="line-operator-button" + class="searchbox-panel-operator-button devtools-monospace" + command="lineSearchCommand"/> + <label id="line-operator-label" + class="plain searchbox-panel-operator-label"/> + </hbox> + <hbox align="center"> + <button id="variable-operator-button" + class="searchbox-panel-operator-button devtools-monospace" + command="variableSearchCommand"/> + <label id="variable-operator-label" + class="plain searchbox-panel-operator-label"/> + </hbox> + </vbox> + </panel> + + <panel id="conditional-breakpoint-panel" + level="top" + type="arrow" + noautofocus="true" + consumeoutsideclicks="false"> + <vbox> + <label id="conditional-breakpoint-panel-description" + value="&debuggerUI.condBreakPanelTitle;"/> + <textbox id="conditional-breakpoint-panel-textbox"/> + </vbox> + </panel> + +</window> diff --git a/toolkit/devtools/debugger/moz.build b/toolkit/devtools/debugger/moz.build new file mode 100644 index 000000000..8276e2982 --- /dev/null +++ b/toolkit/devtools/debugger/moz.build @@ -0,0 +1,11 @@ +# 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/. + +EXTRA_JS_MODULES.devtools.debugger += [ + 'debugger-commands.js', + 'panel.js' +] + +BROWSER_CHROME_MANIFESTS += ['test/browser.ini'] diff --git a/toolkit/devtools/debugger/panel.js b/toolkit/devtools/debugger/panel.js new file mode 100644 index 000000000..1411c3e41 --- /dev/null +++ b/toolkit/devtools/debugger/panel.js @@ -0,0 +1,126 @@ +/* -*- 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 { Cc, Ci, Cu, Cr } = require("chrome"); +const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {}); +const EventEmitter = require("devtools/toolkit/event-emitter"); +const { DevToolsUtils } = Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm", {}); + +function DebuggerPanel(iframeWindow, toolbox) { + this.panelWin = iframeWindow; + this._toolbox = toolbox; + this._destroyer = null; + + this._view = this.panelWin.DebuggerView; + this._controller = this.panelWin.DebuggerController; + this._view._hostType = this._toolbox.hostType; + this._controller._target = this.target; + this._controller._toolbox = this._toolbox; + + this.handleHostChanged = this.handleHostChanged.bind(this); + this.highlightWhenPaused = this.highlightWhenPaused.bind(this); + this.unhighlightWhenResumed = this.unhighlightWhenResumed.bind(this); + + EventEmitter.decorate(this); +}; + +exports.DebuggerPanel = DebuggerPanel; + +DebuggerPanel.prototype = { + /** + * Open is effectively an asynchronous constructor. + * + * @return object + * A promise that is resolved when the Debugger completes opening. + */ + open: function() { + let targetPromise; + + // Local debugging needs to make the target remote. + if (!this.target.isRemote) { + targetPromise = this.target.makeRemote(); + // Listen for tab switching events to manage focus when the content window + // is paused and events suppressed. + this.target.tab.addEventListener('TabSelect', this); + } else { + targetPromise = promise.resolve(this.target); + } + + return targetPromise + .then(() => this._controller.startupDebugger()) + .then(() => this._controller.connect()) + .then(() => { + this._toolbox.on("host-changed", this.handleHostChanged); + this.target.on("thread-paused", this.highlightWhenPaused); + this.target.on("thread-resumed", this.unhighlightWhenResumed); + this.isReady = true; + this.emit("ready"); + return this; + }) + .then(null, function onError(aReason) { + DevToolsUtils.reportException("DebuggerPanel.prototype.open", aReason); + }); + }, + + // DevToolPanel API + + get target() this._toolbox.target, + + destroy: function() { + // Make sure this panel is not already destroyed. + if (this._destroyer) { + return this._destroyer; + } + + this.target.off("thread-paused", this.highlightWhenPaused); + this.target.off("thread-resumed", this.unhighlightWhenResumed); + + if (!this.target.isRemote) { + this.target.tab.removeEventListener('TabSelect', this); + } + + return this._destroyer = this._controller.shutdownDebugger().then(() => { + this.emit("destroyed"); + }); + }, + + // DebuggerPanel API + + addBreakpoint: function(aLocation, aOptions) { + return this._controller.Breakpoints.addBreakpoint(aLocation, aOptions); + }, + + removeBreakpoint: function(aLocation) { + return this._controller.Breakpoints.removeBreakpoint(aLocation); + }, + + handleHostChanged: function() { + this._view.handleHostChanged(this._toolbox.hostType); + }, + + highlightWhenPaused: function() { + this._toolbox.highlightTool("jsdebugger"); + + // Also raise the toolbox window if it is undocked or select the + // corresponding tab when toolbox is docked. + this._toolbox.raise(); + }, + + unhighlightWhenResumed: function() { + this._toolbox.unhighlightTool("jsdebugger"); + }, + + // nsIDOMEventListener API + + handleEvent: function(aEvent) { + if (aEvent.target == this.target.tab && + this._controller.activeThread.state == "paused") { + // Wait a tick for the content focus event to be delivered. + DevToolsUtils.executeSoon(() => this._toolbox.focusTool("jsdebugger")); + } + } +}; diff --git a/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon3/lib/main.js b/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon3/lib/main.js new file mode 100644 index 000000000..fc00b60a1 --- /dev/null +++ b/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon3/lib/main.js @@ -0,0 +1,13 @@ +var { Cc, Ci } = require("chrome"); +var { once } = require("sdk/system/events"); + +var observerService = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService); +var observer = { + observe: function () { + debugger; + } +}; + +once("sdk:loader:destroy", () => observerService.removeObserver(observer, "debuggerAttached")); + +observerService.addObserver(observer, "debuggerAttached", false); diff --git a/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon3/package.json b/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon3/package.json new file mode 100644 index 000000000..4bf1bed50 --- /dev/null +++ b/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon3/package.json @@ -0,0 +1,9 @@ +{ + "name": "browser_dbg_addon3", + "title": "browser_dbg_addon3", + "id": "jid1-ami3akps3baaeg", + "description": "a basic add-on", + "author": "", + "license": "MPL 2.0", + "version": "0.1" +} diff --git a/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon4/bootstrap.js b/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon4/bootstrap.js new file mode 100644 index 000000000..360468ab2 --- /dev/null +++ b/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon4/bootstrap.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { interfaces: Ci, classes: Cc, utils: Cu } = Components; + +function notify() { + // Log objects so makeDebuggeeValue can get the global to use + console.log({ msg: "Hello again" }); +} + +function startup(aParams, aReason) { + Cu.import("resource://gre/modules/Services.jsm"); + let res = Services.io.getProtocolHandler("resource") + .QueryInterface(Ci.nsIResProtocolHandler); + res.setSubstitution("browser_dbg_addon4", aParams.resourceURI); + + // Load a JS module + Cu.import("resource://browser_dbg_addon4/test.jsm"); + // Log objects so makeDebuggeeValue can get the global to use + console.log({ msg: "Hello from the test add-on" }); + + Services.obs.addObserver(notify, "addon-test-ping", false); +} + +function shutdown(aParams, aReason) { + Services.obs.removeObserver(notify, "addon-test-ping"); + + // Unload the JS module + Cu.unload("resource://browser_dbg_addon4/test.jsm"); + + let res = Services.io.getProtocolHandler("resource") + .QueryInterface(Ci.nsIResProtocolHandler); + res.setSubstitution("browser_dbg_addon4", null); +} diff --git a/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon4/chrome.manifest b/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon4/chrome.manifest new file mode 100644 index 000000000..ccb88ddf1 --- /dev/null +++ b/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon4/chrome.manifest @@ -0,0 +1 @@ +content browser_dbg_addon4 . diff --git a/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon4/install.rdf b/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon4/install.rdf new file mode 100644 index 000000000..45679ffc9 --- /dev/null +++ b/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon4/install.rdf @@ -0,0 +1,19 @@ +<?xml version="1.0"?> + +<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:em="http://www.mozilla.org/2004/em-rdf#"> + + <Description about="urn:mozilla:install-manifest"> + <em:id>browser_dbg_addon4@tests.mozilla.org</em:id> + <em:version>1.0</em:version> + <em:name>Test add-on with JS Modules</em:name> + <em:bootstrap>true</em:bootstrap> + <em:targetApplication> + <Description> + <em:id>toolkit@mozilla.org</em:id> + <em:minVersion>0</em:minVersion> + <em:maxVersion>*</em:maxVersion> + </Description> + </em:targetApplication> + </Description> +</RDF> diff --git a/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon4/test.jsm b/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon4/test.jsm new file mode 100644 index 000000000..17bebfd8e --- /dev/null +++ b/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon4/test.jsm @@ -0,0 +1,6 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const EXPORTED_SYMBOLS = ["Foo"]; + +const Foo = {}; diff --git a/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon4/test.xul b/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon4/test.xul new file mode 100644 index 000000000..733817ad8 --- /dev/null +++ b/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon4/test.xul @@ -0,0 +1,8 @@ +<?xml version="1.0"?> + +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="text/javascript" src="testxul.js"/> + <label value="test.xul"/> +</window> diff --git a/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon4/test2.jsm b/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon4/test2.jsm new file mode 100644 index 000000000..703869f43 --- /dev/null +++ b/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon4/test2.jsm @@ -0,0 +1,6 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const EXPORTED_SYMBOLS = ["Bar"]; + +const Bar = {}; diff --git a/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon4/test2.xul b/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon4/test2.xul new file mode 100644 index 000000000..372d05587 --- /dev/null +++ b/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon4/test2.xul @@ -0,0 +1,8 @@ +<?xml version="1.0"?> + +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="text/javascript" src="testxul2.js"/> + <label value="test2.xul"/> +</window> diff --git a/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon4/testxul.js b/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon4/testxul.js new file mode 100644 index 000000000..7ac4eabc7 --- /dev/null +++ b/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon4/testxul.js @@ -0,0 +1,4 @@ +// Define something in here or the script may get collected +window.addEventListener("unload", function() { + window.foo = "bar"; +}); diff --git a/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon4/testxul2.js b/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon4/testxul2.js new file mode 100644 index 000000000..7ac4eabc7 --- /dev/null +++ b/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon4/testxul2.js @@ -0,0 +1,4 @@ +// Define something in here or the script may get collected +window.addEventListener("unload", function() { + window.foo = "bar"; +}); diff --git a/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon5/bootstrap.js b/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon5/bootstrap.js new file mode 100644 index 000000000..c8f89bd34 --- /dev/null +++ b/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon5/bootstrap.js @@ -0,0 +1,23 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { interfaces: Ci, classes: Cc } = Components; + +function startup(aParams, aReason) { + Components.utils.import("resource://gre/modules/Services.jsm"); + let res = Services.io.getProtocolHandler("resource") + .QueryInterface(Ci.nsIResProtocolHandler); + res.setSubstitution("browser_dbg_addon5", aParams.resourceURI); + + // Load a JS module + Components.utils.import("resource://browser_dbg_addon5/test.jsm"); +} + +function shutdown(aParams, aReason) { + // Unload the JS module + Components.utils.unload("resource://browser_dbg_addon5/test.jsm"); + + let res = Services.io.getProtocolHandler("resource") + .QueryInterface(Ci.nsIResProtocolHandler); + res.setSubstitution("browser_dbg_addon5", null); +} diff --git a/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon5/chrome.manifest b/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon5/chrome.manifest new file mode 100644 index 000000000..ceef8d06d --- /dev/null +++ b/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon5/chrome.manifest @@ -0,0 +1 @@ +content browser_dbg_addon5 . diff --git a/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon5/install.rdf b/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon5/install.rdf new file mode 100644 index 000000000..af2cbbb5d --- /dev/null +++ b/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon5/install.rdf @@ -0,0 +1,20 @@ +<?xml version="1.0"?> + +<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:em="http://www.mozilla.org/2004/em-rdf#"> + + <Description about="urn:mozilla:install-manifest"> + <em:id>browser_dbg_addon5@tests.mozilla.org</em:id> + <em:version>1.0</em:version> + <em:name>Test unpacked add-on with JS Modules</em:name> + <em:bootstrap>true</em:bootstrap> + <em:unpack>true</em:unpack> + <em:targetApplication> + <Description> + <em:id>toolkit@mozilla.org</em:id> + <em:minVersion>0</em:minVersion> + <em:maxVersion>*</em:maxVersion> + </Description> + </em:targetApplication> + </Description> +</RDF> diff --git a/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon5/test.jsm b/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon5/test.jsm new file mode 100644 index 000000000..17bebfd8e --- /dev/null +++ b/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon5/test.jsm @@ -0,0 +1,6 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const EXPORTED_SYMBOLS = ["Foo"]; + +const Foo = {}; diff --git a/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon5/test.xul b/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon5/test.xul new file mode 100644 index 000000000..733817ad8 --- /dev/null +++ b/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon5/test.xul @@ -0,0 +1,8 @@ +<?xml version="1.0"?> + +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="text/javascript" src="testxul.js"/> + <label value="test.xul"/> +</window> diff --git a/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon5/test2.jsm b/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon5/test2.jsm new file mode 100644 index 000000000..703869f43 --- /dev/null +++ b/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon5/test2.jsm @@ -0,0 +1,6 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const EXPORTED_SYMBOLS = ["Bar"]; + +const Bar = {}; diff --git a/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon5/test2.xul b/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon5/test2.xul new file mode 100644 index 000000000..372d05587 --- /dev/null +++ b/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon5/test2.xul @@ -0,0 +1,8 @@ +<?xml version="1.0"?> + +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="text/javascript" src="testxul2.js"/> + <label value="test2.xul"/> +</window> diff --git a/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon5/testxul.js b/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon5/testxul.js new file mode 100644 index 000000000..7ac4eabc7 --- /dev/null +++ b/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon5/testxul.js @@ -0,0 +1,4 @@ +// Define something in here or the script may get collected +window.addEventListener("unload", function() { + window.foo = "bar"; +}); diff --git a/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon5/testxul2.js b/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon5/testxul2.js new file mode 100644 index 000000000..7ac4eabc7 --- /dev/null +++ b/toolkit/devtools/debugger/test/addon-source/browser_dbg_addon5/testxul2.js @@ -0,0 +1,4 @@ +// Define something in here or the script may get collected +window.addEventListener("unload", function() { + window.foo = "bar"; +}); diff --git a/toolkit/devtools/debugger/test/addon1.xpi b/toolkit/devtools/debugger/test/addon1.xpi Binary files differnew file mode 100644 index 000000000..b77ec9531 --- /dev/null +++ b/toolkit/devtools/debugger/test/addon1.xpi diff --git a/toolkit/devtools/debugger/test/addon2.xpi b/toolkit/devtools/debugger/test/addon2.xpi Binary files differnew file mode 100644 index 000000000..460eaca8a --- /dev/null +++ b/toolkit/devtools/debugger/test/addon2.xpi diff --git a/toolkit/devtools/debugger/test/addon3.xpi b/toolkit/devtools/debugger/test/addon3.xpi Binary files differnew file mode 100644 index 000000000..673b31b9d --- /dev/null +++ b/toolkit/devtools/debugger/test/addon3.xpi diff --git a/toolkit/devtools/debugger/test/addon4.xpi b/toolkit/devtools/debugger/test/addon4.xpi Binary files differnew file mode 100644 index 000000000..56dc98f6e --- /dev/null +++ b/toolkit/devtools/debugger/test/addon4.xpi diff --git a/toolkit/devtools/debugger/test/addon5.xpi b/toolkit/devtools/debugger/test/addon5.xpi Binary files differnew file mode 100644 index 000000000..16991f7a0 --- /dev/null +++ b/toolkit/devtools/debugger/test/addon5.xpi diff --git a/toolkit/devtools/debugger/test/browser.ini b/toolkit/devtools/debugger/test/browser.ini new file mode 100644 index 000000000..eeba980ae --- /dev/null +++ b/toolkit/devtools/debugger/test/browser.ini @@ -0,0 +1,562 @@ +[DEFAULT] +subsuite = devtools +support-files = + addon1.xpi + addon2.xpi + addon3.xpi + addon4.xpi + addon5.xpi + code_binary_search.coffee + code_binary_search.js + code_binary_search.map + code_blackboxing_blackboxme.js + code_blackboxing_one.js + code_blackboxing_three.js + code_blackboxing_two.js + code_breakpoints-break-on-last-line-of-script-on-reload.js + code_breakpoints-other-tabs.js + code_frame-script.js + code_function-search-01.js + code_function-search-02.js + code_function-search-03.js + code_location-changes.js + code_math.js + code_math.map + code_math.min.js + code_math_bogus_map.js + code_same-line-functions.js + code_script-eval.js + code_script-switching-01.js + code_script-switching-02.js + code_test-editor-mode + code_tracing-01.js + code_ugly.js + code_ugly-2.js + code_ugly-3.js + code_ugly-4.js + code_ugly-5.js + code_ugly-6.js + code_ugly-7.js + code_ugly-8 + code_ugly-8^headers^ + doc_auto-pretty-print-01.html + doc_auto-pretty-print-02.html + doc_binary_search.html + doc_blackboxing.html + doc_breakpoints-break-on-last-line-of-script-on-reload.html + doc_breakpoints-other-tabs.html + doc_breakpoints-reload.html + doc_closures.html + doc_closure-optimized-out.html + doc_cmd-break.html + doc_cmd-dbg.html + doc_breakpoint-move.html + doc_conditional-breakpoints.html + doc_domnode-variables.html + doc_editor-mode.html + doc_empty-tab-01.html + doc_empty-tab-02.html + doc_event-listeners-01.html + doc_event-listeners-02.html + doc_event-listeners-03.html + doc_frame-parameters.html + doc_function-display-name.html + doc_function-search.html + doc_global-method-override.html + doc_iframes.html + doc_included-script.html + doc_inline-debugger-statement.html + doc_inline-script.html + doc_large-array-buffer.html + doc_minified.html + doc_minified_bogus_map.html + doc_native-event-handler.html + doc_no-page-sources.html + doc_pause-exceptions.html + doc_pretty-print.html + doc_pretty-print-2.html + doc_pretty-print-3.html + doc_pretty-print-on-paused.html + doc_promise.html + doc_random-javascript.html + doc_recursion-stack.html + doc_same-line-functions.html + doc_scope-variable.html + doc_scope-variable-2.html + doc_scope-variable-3.html + doc_scope-variable-4.html + doc_script-eval.html + doc_script-bookmarklet.html + doc_script-switching-01.html + doc_script-switching-02.html + doc_split-console-paused-reload.html + doc_step-out.html + doc_terminate-on-tab-close.html + doc_tracing-01.html + doc_watch-expressions.html + doc_watch-expression-button.html + doc_with-frame.html + head.js + sjs_random-javascript.sjs + testactors.js + +[browser_dbg_aaa_run_first_leaktest.js] +skip-if = e10s && debug +[browser_dbg_addonactor.js] +skip-if = e10s && debug +[browser_dbg_addon-sources.js] +skip-if = e10s && debug +[browser_dbg_addon-modules.js] +skip-if = e10s # TODO +[browser_dbg_addon-modules-unpacked.js] +skip-if = e10s # TODO +[browser_dbg_addon-panels.js] +skip-if = e10s && debug +[browser_dbg_addon-console.js] +skip-if = e10s && debug || os == 'win' # bug 1005274 +[browser_dbg_auto-pretty-print-01.js] +skip-if = e10s && debug +[browser_dbg_auto-pretty-print-02.js] +skip-if = e10s && debug +[browser_dbg_bfcache.js] +skip-if = e10s || true # bug 1113935 +[browser_dbg_blackboxing-01.js] +skip-if = e10s && debug +[browser_dbg_blackboxing-02.js] +skip-if = e10s && debug +[browser_dbg_blackboxing-03.js] +skip-if = e10s && debug +[browser_dbg_blackboxing-04.js] +skip-if = e10s && debug +[browser_dbg_blackboxing-05.js] +skip-if = e10s && debug +[browser_dbg_blackboxing-06.js] +skip-if = e10s && debug +[browser_dbg_breadcrumbs-access.js] +skip-if = e10s && debug +[browser_dbg_break-on-dom-01.js] +skip-if = e10s && debug +[browser_dbg_break-on-dom-02.js] +skip-if = e10s && debug +[browser_dbg_break-on-dom-03.js] +skip-if = e10s && debug +[browser_dbg_break-on-dom-04.js] +skip-if = e10s && debug +[browser_dbg_break-on-dom-05.js] +skip-if = e10s && debug +[browser_dbg_break-on-dom-06.js] +skip-if = e10s && debug +[browser_dbg_break-on-dom-07.js] +skip-if = e10s && debug +[browser_dbg_break-on-dom-08.js] +skip-if = e10s && debug +[browser_dbg_break-on-dom-event-01.js] +skip-if = e10s || os == "mac" || e10s # Bug 895426 +[browser_dbg_break-on-dom-event-02.js] +skip-if = e10s # TODO +[browser_dbg_breakpoints-actual-location.js] +skip-if = e10s && debug +[browser_dbg_breakpoints-actual-location2.js] +skip-if = e10s && debug +[browser_dbg_breakpoints-break-on-last-line-of-script-on-reload.js] +skip-if = e10s # Bug 1093535 +[browser_dbg_breakpoints-button-01.js] +skip-if = e10s && debug +[browser_dbg_breakpoints-button-02.js] +skip-if = e10s && debug +[browser_dbg_breakpoints-contextmenu-add.js] +skip-if = e10s && debug +[browser_dbg_breakpoints-contextmenu.js] +skip-if = e10s && debug +[browser_dbg_breakpoints-disabled-reload.js] +skip-if = e10s # Bug 1093535 +[browser_dbg_breakpoints-editor.js] +skip-if = e10s && debug +[browser_dbg_breakpoints-eval.js] +skip-if = e10s && debug +[browser_dbg_breakpoints-highlight.js] +skip-if = e10s && debug +[browser_dbg_breakpoints-new-script.js] +skip-if = e10s && debug +[browser_dbg_breakpoints-other-tabs.js] +skip-if = e10s && debug +[browser_dbg_breakpoints-pane.js] +skip-if = e10s && debug +[browser_dbg_breakpoints-reload.js] +skip-if = e10s && debug +[browser_dbg_chrome-create.js] +skip-if = e10s && debug +[browser_dbg_chrome-debugging.js] +skip-if = e10s && debug +[browser_dbg_clean-exit-window.js] +skip-if = true # Bug 933950 (leaky test) +[browser_dbg_clean-exit.js] +skip-if = true # Bug 1044985 (racy test) +[browser_dbg_closure-inspection.js] +skip-if = e10s && debug +[browser_dbg_cmd-blackbox.js] +skip-if = e10s && debug +[browser_dbg_cmd-break.js] +skip-if = e10s # TODO +[browser_dbg_cmd-dbg.js] +skip-if = e10s # TODO +[browser_dbg_conditional-breakpoints-01.js] +skip-if = e10s && debug +[browser_dbg_conditional-breakpoints-02.js] +skip-if = e10s && debug +[browser_dbg_conditional-breakpoints-03.js] +skip-if = e10s && debug +[browser_dbg_conditional-breakpoints-04.js] +skip-if = e10s && debug +[browser_dbg_server-conditional-bp-01.js] +skip-if = e10s && debug +[browser_dbg_server-conditional-bp-02.js] +skip-if = e10s && debug +[browser_dbg_server-conditional-bp-03.js] +skip-if = e10s && debug +[browser_dbg_server-conditional-bp-04.js] +skip-if = e10s && debug +[browser_dbg_controller-evaluate-01.js] +skip-if = e10s && debug +[browser_dbg_controller-evaluate-02.js] +skip-if = e10s && debug +[browser_dbg_debugger-statement.js] +skip-if = e10s && debug +[browser_dbg_editor-contextmenu.js] +skip-if = e10s && debug +[browser_dbg_editor-mode.js] +skip-if = e10s && debug +[browser_dbg_event-listeners-01.js] +skip-if = e10s && debug +[browser_dbg_event-listeners-02.js] +skip-if = e10s && debug +[browser_dbg_event-listeners-03.js] +skip-if = e10s && debug +[browser_dbg_file-reload.js] +skip-if = e10s && debug +[browser_dbg_function-display-name.js] +skip-if = e10s && debug +[browser_dbg_global-method-override.js] +skip-if = e10s && debug +[browser_dbg_globalactor.js] +skip-if = e10s # TODO +[browser_dbg_hide-toolbar-buttons.js] +skip-if = e10s +[browser_dbg_hit-counts-01.js] +skip-if = e10s && debug +[browser_dbg_hit-counts-02.js] +skip-if = e10s && debug +[browser_dbg_host-layout.js] +skip-if = e10s && debug +[browser_dbg_iframes.js] +skip-if = e10s # TODO +[browser_dbg_instruments-pane-collapse.js] +skip-if = e10s && debug +[browser_dbg_interrupts.js] +skip-if = e10s && debug +[browser_dbg_listaddons.js] +skip-if = e10s && debug +[browser_dbg_listtabs-01.js] +skip-if = e10s # TODO +[browser_dbg_listtabs-02.js] +skip-if = e10s # TODO +[browser_dbg_listtabs-03.js] +skip-if = e10s && debug +[browser_dbg_location-changes-01-simple.js] +skip-if = e10s && debug +[browser_dbg_location-changes-02-blank.js] +skip-if = e10s && debug +[browser_dbg_location-changes-03-new.js] +skip-if = e10s # TODO +[browser_dbg_location-changes-04-breakpoint.js] +skip-if = e10s # TODO +[browser_dbg_multiple-windows.js] +skip-if = e10s # TODO +[browser_dbg_navigation.js] +skip-if = e10s && debug +[browser_dbg_no-page-sources.js] +skip-if = e10s && debug +[browser_dbg_on-pause-highlight.js] +skip-if = e10s && debug +[browser_dbg_on-pause-raise.js] +skip-if = e10s && debug || os == "linux" # Bug 888811 & bug 891176 +[browser_dbg_optimized-out-vars.js] +skip-if = e10s && debug +[browser_dbg_panel-size.js] +skip-if = e10s && debug +[browser_dbg_parser-01.js] +skip-if = e10s && debug +[browser_dbg_parser-02.js] +skip-if = e10s && debug +[browser_dbg_parser-03.js] +skip-if = e10s && debug +[browser_dbg_parser-04.js] +skip-if = e10s && debug +[browser_dbg_parser-05.js] +skip-if = e10s && debug +[browser_dbg_parser-06.js] +skip-if = e10s && debug +[browser_dbg_parser-07.js] +skip-if = e10s && debug +[browser_dbg_parser-08.js] +skip-if = e10s && debug +[browser_dbg_parser-09.js] +skip-if = e10s && debug +[browser_dbg_parser-10.js] +skip-if = e10s && debug +[browser_dbg_pause-exceptions-01.js] +skip-if = e10s && debug +[browser_dbg_pause-exceptions-02.js] +skip-if = e10s && debug +[browser_dbg_pause-resume.js] +skip-if = e10s && debug +[browser_dbg_pause-warning.js] +skip-if = e10s && debug +[browser_dbg_paused-keybindings.js] +skip-if = e10s +[browser_dbg_pretty-print-01.js] +skip-if = e10s && debug +[browser_dbg_pretty-print-02.js] +skip-if = e10s && debug +[browser_dbg_pretty-print-03.js] +skip-if = e10s && debug +[browser_dbg_pretty-print-04.js] +skip-if = e10s && debug +[browser_dbg_pretty-print-05.js] +skip-if = e10s && debug +[browser_dbg_pretty-print-06.js] +skip-if = e10s && debug +[browser_dbg_pretty-print-07.js] +skip-if = e10s && debug +[browser_dbg_pretty-print-08.js] +skip-if = e10s && debug +[browser_dbg_pretty-print-09.js] +skip-if = e10s && debug +[browser_dbg_pretty-print-10.js] +skip-if = e10s && debug +[browser_dbg_pretty-print-11.js] +skip-if = e10s && debug +[browser_dbg_pretty-print-12.js] +skip-if = e10s && debug +[browser_dbg_pretty-print-13.js] +skip-if = e10s && debug +[browser_dbg_pretty-print-on-paused.js] +skip-if = e10s && debug +[browser_dbg_progress-listener-bug.js] +skip-if = e10a && debug +[browser_dbg_reload-preferred-script-01.js] +skip-if = e10s && debug +[browser_dbg_reload-preferred-script-02.js] +skip-if = e10s && debug +[browser_dbg_reload-preferred-script-03.js] +skip-if = e10s && debug +[browser_dbg_reload-same-script.js] +skip-if = e10s && debug +[browser_dbg_scripts-switching-01.js] +skip-if = e10s && debug +[browser_dbg_scripts-switching-02.js] +skip-if = e10s && debug +[browser_dbg_scripts-switching-03.js] +skip-if = e10s && debug +[browser_dbg_search-autofill-identifier.js] +skip-if = e10s && debug +[browser_dbg_search-basic-01.js] +skip-if = e10s && debug +[browser_dbg_search-basic-02.js] +skip-if = e10s && debug +[browser_dbg_search-basic-03.js] +skip-if = e10s && debug +[browser_dbg_search-basic-04.js] +skip-if = e10s && debug +[browser_dbg_search-global-01.js] +skip-if = e10s && debug +[browser_dbg_search-global-02.js] +skip-if = e10s && debug +[browser_dbg_search-global-03.js] +skip-if = e10s # Bug 1093535 +[browser_dbg_search-global-04.js] +skip-if = e10s && debug +[browser_dbg_search-global-05.js] +skip-if = e10s && debug +[browser_dbg_search-global-06.js] +skip-if = e10s && debug +[browser_dbg_search-popup-jank.js] +skip-if = e10s && debug +[browser_dbg_search-sources-01.js] +skip-if = e10s && debug +[browser_dbg_search-sources-02.js] +skip-if = e10s && debug +[browser_dbg_search-sources-03.js] +skip-if = e10s && debug +[browser_dbg_search-symbols.js] +skip-if = e10s && debug +[browser_dbg_searchbox-help-popup-01.js] +skip-if = e10s && debug +[browser_dbg_searchbox-help-popup-02.js] +skip-if = e10s && debug +[browser_dbg_searchbox-parse.js] +skip-if = e10s && debug +[browser_dbg_source-maps-01.js] +skip-if = e10s && debug +[browser_dbg_source-maps-02.js] +skip-if = e10s && debug +[browser_dbg_source-maps-03.js] +skip-if = e10s && debug +[browser_dbg_source-maps-04.js] +skip-if = e10s # Bug 1093535 +[browser_dbg_sources-cache.js] +skip-if = e10s && debug +[browser_dbg_sources-eval-01.js] +skip-if = true # non-named eval sources turned off for now, bug 1124106 +[browser_dbg_sources-eval-02.js] +skip-if = e10s && debug +[browser_dbg_sources-labels.js] +skip-if = e10s && debug +[browser_dbg_sources-sorting.js] +skip-if = e10s && debug +[browser_dbg_sources-bookmarklet.js] +skip-if = e10s && debug +[browser_dbg_split-console-paused-reload.js] +skip-if = e10s && debug +[browser_dbg_stack-01.js] +skip-if = e10s && debug +[browser_dbg_stack-02.js] +skip-if = e10s && debug +[browser_dbg_stack-03.js] +skip-if = e10s # TODO +[browser_dbg_stack-04.js] +skip-if = e10s && debug +[browser_dbg_stack-05.js] +skip-if = e10s && debug +[browser_dbg_stack-06.js] +skip-if = e10s && debug +[browser_dbg_stack-07.js] +skip-if = e10s && debug +[browser_dbg_step-out.js] +skip-if = e10s && debug +[browser_dbg_tabactor-01.js] +skip-if = e10s # TODO +[browser_dbg_tabactor-02.js] +skip-if = e10s # TODO +[browser_dbg_terminate-on-tab-close.js] +skip-if = e10s && debug +[browser_dbg_tracing-01.js] +skip-if = e10s && debug +[browser_dbg_tracing-02.js] +skip-if = e10s && debug +[browser_dbg_tracing-03.js] +skip-if = e10s && debug +[browser_dbg_tracing-04.js] +skip-if = e10s && debug +[browser_dbg_tracing-05.js] +skip-if = e10s && debug +[browser_dbg_tracing-06.js] +skip-if = e10s && debug +[browser_dbg_tracing-07.js] +skip-if = e10s && debug +[browser_dbg_tracing-08.js] +skip-if = e10s && debug +[browser_dbg_variables-view-01.js] +skip-if = e10s && debug +[browser_dbg_variables-view-02.js] +skip-if = e10s && debug +[browser_dbg_variables-view-03.js] +skip-if = e10s && debug +[browser_dbg_variables-view-04.js] +skip-if = e10s && debug +[browser_dbg_variables-view-05.js] +skip-if = e10s && debug +[browser_dbg_variables-view-06.js] +skip-if = e10s && debug +[browser_dbg_variables-view-accessibility.js] +skip-if = e10s && debug +[browser_dbg_variables-view-data.js] +skip-if = e10s && debug +[browser_dbg_variables-view-edit-cancel.js] +skip-if = e10s && debug +[browser_dbg_variables-view-edit-click.js] +skip-if = e10s || (os == 'mac' || os == 'win') && (debug == false) # Bug 986166 +[browser_dbg_variables-view-edit-getset-01.js] +skip-if = e10s && debug +[browser_dbg_variables-view-edit-getset-02.js] +skip-if = e10s && debug +[browser_dbg_variables-view-edit-value.js] +skip-if = e10s && debug +[browser_dbg_variables-view-edit-watch.js] +skip-if = e10s && debug +[browser_dbg_variables-view-filter-01.js] +skip-if = e10s && debug +[browser_dbg_variables-view-filter-02.js] +skip-if = e10s && debug +[browser_dbg_variables-view-filter-03.js] +skip-if = e10s && debug +[browser_dbg_variables-view-filter-04.js] +skip-if = e10s && debug +[browser_dbg_variables-view-filter-05.js] +skip-if = e10s && debug +[browser_dbg_variables-view-filter-pref.js] +skip-if = e10s && debug +[browser_dbg_variables-view-filter-searchbox.js] +skip-if = e10s && debug +[browser_dbg_variables-view-frame-parameters-01.js] +skip-if = e10s && debug +[browser_dbg_variables-view-frame-parameters-02.js] +skip-if = e10s && debug +[browser_dbg_variables-view-frame-parameters-03.js] +skip-if = e10s && debug +[browser_dbg_variables-view-frame-with.js] +skip-if = e10s && debug +[browser_dbg_variables-view-frozen-sealed-nonext.js] +skip-if = e10s && debug || buildapp == 'mulet' +[browser_dbg_variables-view-hide-non-enums.js] +skip-if = e10s && debug +[browser_dbg_variables-view-large-array-buffer.js] +skip-if = e10s && debug +[browser_dbg_variables-view-override-01.js] +skip-if = e10s && debug +[browser_dbg_variables-view-override-02.js] +skip-if = e10s && debug +[browser_dbg_variables-view-popup-01.js] +skip-if = e10s && debug +[browser_dbg_variables-view-popup-02.js] +skip-if = e10s && debug +[browser_dbg_variables-view-popup-03.js] +skip-if = e10s && debug +[browser_dbg_variables-view-popup-04.js] +skip-if = e10s && debug +[browser_dbg_variables-view-popup-05.js] +skip-if = e10s && debug +[browser_dbg_variables-view-popup-06.js] +skip-if = e10s && debug +[browser_dbg_variables-view-popup-07.js] +skip-if = e10s && debug +[browser_dbg_variables-view-popup-08.js] +skip-if = e10s && debug +[browser_dbg_variables-view-popup-09.js] +skip-if = e10s && debug +[browser_dbg_variables-view-popup-10.js] +skip-if = e10s && debug +[browser_dbg_variables-view-popup-11.js] +skip-if = e10s && debug +[browser_dbg_variables-view-popup-12.js] +skip-if = e10s && debug +[browser_dbg_variables-view-popup-13.js] +skip-if = e10s && debug +[browser_dbg_variables-view-popup-14.js] +skip-if = true # Bug 1029545 +[browser_dbg_variables-view-popup-15.js] +skip-if = e10s && debug +[browser_dbg_variables-view-popup-16.js] +skip-if = e10s && debug +[browser_dbg_variables-view-reexpand-01.js] +skip-if = e10s && debug +[browser_dbg_variables-view-reexpand-02.js] +skip-if = e10s && debug +[browser_dbg_variables-view-reexpand-03.js] +skip-if = e10s && debug +[browser_dbg_variables-view-webidl.js] +skip-if = e10s && debug +[browser_dbg_watch-expressions-01.js] +skip-if = e10s && debug +[browser_dbg_watch-expressions-02.js] +skip-if = e10s && debug diff --git a/toolkit/devtools/debugger/test/browser_dbg_aaa_run_first_leaktest.js b/toolkit/devtools/debugger/test/browser_dbg_aaa_run_first_leaktest.js new file mode 100644 index 000000000..720dbeba5 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_aaa_run_first_leaktest.js @@ -0,0 +1,27 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This tests if the debugger leaks on initialization and sudden destruction. + * You can also use this initialization format as a template for other tests. + * If leaks happen here, there's something very, very fishy going on. + */ + +const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html"; + +function test() { + // Wait longer for this very simple test that comes first, to make sure that + // GC from previous tests does not interfere with the debugger suite. + requestLongerTimeout(2); + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + ok(aTab, "Should have a tab available."); + ok(aPanel, "Should have a debugger pane available."); + + waitForSourceAndCaretAndScopes(aPanel, "-02.js", 1).then(() => { + resumeDebuggerThenCloseAndFinish(aPanel); + }); + + callInTab(aTab, "firstCall"); + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_addon-console.js b/toolkit/devtools/debugger/test/browser_dbg_addon-console.js new file mode 100644 index 000000000..3539e5e62 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_addon-console.js @@ -0,0 +1,44 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that the we can see console messages from the add-on + +const ADDON_URL = EXAMPLE_URL + "addon4.xpi"; + +function getCachedMessages(webConsole) { + let deferred = promise.defer(); + webConsole.getCachedMessages(["ConsoleAPI"], (aResponse) => { + if (aResponse.error) { + deferred.reject(aResponse.error); + return; + } + deferred.resolve(aResponse.messages); + }); + return deferred.promise; +} + +function test() { + Task.spawn(function () { + let addon = yield addAddon(ADDON_URL); + let addonDebugger = yield initAddonDebugger(ADDON_URL); + + let webConsole = addonDebugger.webConsole; + let messages = yield getCachedMessages(webConsole); + is(messages.length, 1, "Should be one cached message"); + is(messages[0].arguments[0].type, "object", "Should have logged an object"); + is(messages[0].arguments[0].preview.ownProperties.msg.value, "Hello from the test add-on", "Should have got the right message"); + + let consolePromise = addonDebugger.once("console"); + + console.log("Bad message"); + Services.obs.notifyObservers(null, "addon-test-ping", ""); + + let messageGrip = yield consolePromise; + is(messageGrip.arguments[0].type, "object", "Should have logged an object"); + is(messageGrip.arguments[0].preview.ownProperties.msg.value, "Hello again", "Should have got the right message"); + + yield addonDebugger.destroy(); + yield removeAddon(addon); + finish(); + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_addon-modules-unpacked.js b/toolkit/devtools/debugger/test/browser_dbg_addon-modules-unpacked.js new file mode 100644 index 000000000..382f56c4a --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_addon-modules-unpacked.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Make sure the add-on actor can see loaded JS Modules from an add-on + +const ADDON_URL = EXAMPLE_URL + "addon5.xpi"; + +function test() { + Task.spawn(function () { + let addon = yield addAddon(ADDON_URL); + let tab1 = yield addTab("chrome://browser_dbg_addon5/content/test.xul"); + + let addonDebugger = yield initAddonDebugger(ADDON_URL); + + is(addonDebugger.title, "Debugger - Test unpacked add-on with JS Modules", "Saw the right toolbox title."); + + // Check the inital list of sources is correct + let groups = yield addonDebugger.getSourceGroups(); + is(groups[0].name, "browser_dbg_addon5@tests.mozilla.org", "Add-on code should be the first group"); + is(groups[1].name, "chrome://global", "XUL code should be the second group"); + is(groups.length, 2, "Should be only two groups."); + + let sources = groups[0].sources; + is(sources.length, 3, "Should be three sources"); + ok(sources[0].url.endsWith("/browser_dbg_addon5@tests.mozilla.org/bootstrap.js"), "correct url for bootstrap code") + is(sources[0].label, "bootstrap.js", "correct label for bootstrap code") + is(sources[1].url, "resource://browser_dbg_addon5/test.jsm", "correct url for addon code") + is(sources[1].label, "test.jsm", "correct label for addon code") + is(sources[2].url, "chrome://browser_dbg_addon5/content/testxul.js", "correct url for addon tab code") + is(sources[2].label, "testxul.js", "correct label for addon tab code") + + // Load a new module and tab and check they appear in the list of sources + Cu.import("resource://browser_dbg_addon5/test2.jsm", {}); + let tab2 = yield addTab("chrome://browser_dbg_addon5/content/test2.xul"); + + groups = yield addonDebugger.getSourceGroups(); + is(groups[0].name, "browser_dbg_addon5@tests.mozilla.org", "Add-on code should be the first group"); + is(groups[1].name, "chrome://global", "XUL code should be the second group"); + is(groups.length, 2, "Should be only two groups."); + + sources = groups[0].sources; + is(sources.length, 5, "Should be five sources"); + ok(sources[0].url.endsWith("/browser_dbg_addon5@tests.mozilla.org/bootstrap.js"), "correct url for bootstrap code") + is(sources[0].label, "bootstrap.js", "correct label for bootstrap code") + is(sources[1].url, "resource://browser_dbg_addon5/test.jsm", "correct url for addon code") + is(sources[1].label, "test.jsm", "correct label for addon code") + is(sources[2].url, "chrome://browser_dbg_addon5/content/testxul.js", "correct url for addon tab code") + is(sources[2].label, "testxul.js", "correct label for addon tab code") + is(sources[3].url, "resource://browser_dbg_addon5/test2.jsm", "correct url for addon code") + is(sources[3].label, "test2.jsm", "correct label for addon code") + is(sources[4].url, "chrome://browser_dbg_addon5/content/testxul2.js", "correct url for addon tab code") + is(sources[4].label, "testxul2.js", "correct label for addon tab code") + + Cu.unload("resource://browser_dbg_addon5/test2.jsm"); + yield addonDebugger.destroy(); + yield removeTab(tab1); + yield removeTab(tab2); + yield removeAddon(addon); + finish(); + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_addon-modules.js b/toolkit/devtools/debugger/test/browser_dbg_addon-modules.js new file mode 100644 index 000000000..1f4ae393d --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_addon-modules.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Make sure the add-on actor can see loaded JS Modules from an add-on + +const ADDON_URL = EXAMPLE_URL + "addon4.xpi"; + +function test() { + Task.spawn(function () { + let addon = yield addAddon(ADDON_URL); + let tab1 = yield addTab("chrome://browser_dbg_addon4/content/test.xul"); + + let addonDebugger = yield initAddonDebugger(ADDON_URL); + + is(addonDebugger.title, "Debugger - Test add-on with JS Modules", "Saw the right toolbox title."); + + // Check the inital list of sources is correct + let groups = yield addonDebugger.getSourceGroups(); + is(groups[0].name, "browser_dbg_addon4@tests.mozilla.org", "Add-on code should be the first group"); + is(groups[1].name, "chrome://global", "XUL code should be the second group"); + is(groups.length, 2, "Should be only two groups."); + + let sources = groups[0].sources; + is(sources.length, 3, "Should be three sources"); + ok(sources[0].url.endsWith("/browser_dbg_addon4@tests.mozilla.org.xpi!/bootstrap.js"), "correct url for bootstrap code") + is(sources[0].label, "bootstrap.js", "correct label for bootstrap code") + is(sources[1].url, "resource://browser_dbg_addon4/test.jsm", "correct url for addon code") + is(sources[1].label, "test.jsm", "correct label for addon code") + is(sources[2].url, "chrome://browser_dbg_addon4/content/testxul.js", "correct url for addon tab code") + is(sources[2].label, "testxul.js", "correct label for addon tab code") + + // Load a new module and tab and check they appear in the list of sources + Cu.import("resource://browser_dbg_addon4/test2.jsm", {}); + let tab2 = yield addTab("chrome://browser_dbg_addon4/content/test2.xul"); + + groups = yield addonDebugger.getSourceGroups(); + is(groups[0].name, "browser_dbg_addon4@tests.mozilla.org", "Add-on code should be the first group"); + is(groups[1].name, "chrome://global", "XUL code should be the second group"); + is(groups.length, 2, "Should be only two groups."); + + sources = groups[0].sources; + is(sources.length, 5, "Should be five sources"); + ok(sources[0].url.endsWith("/browser_dbg_addon4@tests.mozilla.org.xpi!/bootstrap.js"), "correct url for bootstrap code") + is(sources[0].label, "bootstrap.js", "correct label for bootstrap code") + is(sources[1].url, "resource://browser_dbg_addon4/test.jsm", "correct url for addon code") + is(sources[1].label, "test.jsm", "correct label for addon code") + is(sources[2].url, "chrome://browser_dbg_addon4/content/testxul.js", "correct url for addon tab code") + is(sources[2].label, "testxul.js", "correct label for addon tab code") + is(sources[3].url, "resource://browser_dbg_addon4/test2.jsm", "correct url for addon code") + is(sources[3].label, "test2.jsm", "correct label for addon code") + is(sources[4].url, "chrome://browser_dbg_addon4/content/testxul2.js", "correct url for addon tab code") + is(sources[4].label, "testxul2.js", "correct label for addon tab code") + + Cu.unload("resource://browser_dbg_addon4/test2.jsm"); + yield addonDebugger.destroy(); + yield removeTab(tab1); + yield removeTab(tab2); + yield removeAddon(addon); + finish(); + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_addon-panels.js b/toolkit/devtools/debugger/test/browser_dbg_addon-panels.js new file mode 100644 index 000000000..98d9fc60e --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_addon-panels.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Ensure that only panels that are relevant to the addon debugger +// display in the toolbox + +const ADDON_URL = EXAMPLE_URL + "addon3.xpi"; + +let gAddon, gClient, gThreadClient, gDebugger, gSources; +let PREFS = [ + "devtools.canvasdebugger.enabled", + "devtools.shadereditor.enabled", + "devtools.profiler.enabled", + "devtools.netmonitor.enabled" +]; +function test() { + Task.spawn(function () { + let addon = yield addAddon(ADDON_URL); + let addonDebugger = yield initAddonDebugger(ADDON_URL); + + // Store and enable all optional dev tools panels + let originalPrefs = PREFS.map(pref => { + let original = Services.prefs.getBoolPref(pref); + Services.prefs.setBoolPref(pref, true) + return original; + }); + + // Check only valid tabs are shown + let tabs = addonDebugger.frame.contentDocument.getElementById("toolbox-tabs").children; + let expectedTabs = ["webconsole", "jsdebugger", "scratchpad"]; + + is(tabs.length, expectedTabs.length, "displaying only " + expectedTabs.length + " tabs in addon debugger"); + Array.forEach(tabs, (tab, i) => { + let toolName = expectedTabs[i]; + is(tab.getAttribute("toolid"), toolName, "displaying " + toolName); + }); + + // Check no toolbox buttons are shown + let buttons = addonDebugger.frame.contentDocument.getElementById("toolbox-buttons").children; + Array.forEach(buttons, (btn, i) => { + is(btn.hidden, true, "no toolbox buttons for the addon debugger -- " + btn.className); + }); + + yield addonDebugger.destroy(); + yield removeAddon(addon); + + PREFS.forEach((pref, i) => Services.prefs.setBoolPref(pref, originalPrefs[i])); + + finish(); + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_addon-sources.js b/toolkit/devtools/debugger/test/browser_dbg_addon-sources.js new file mode 100644 index 000000000..89b4ebcbd --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_addon-sources.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Ensure that the sources listed when debugging an addon are either from the +// addon itself, or the SDK, with proper groups and labels. + +const ADDON_URL = EXAMPLE_URL + "addon3.xpi"; +let gClient; + +function test() { + Task.spawn(function () { + let addon = yield addAddon(ADDON_URL); + let addonDebugger = yield initAddonDebugger(ADDON_URL); + + is(addonDebugger.title, "Debugger - browser_dbg_addon3", "Saw the right toolbox title."); + + // Check the inital list of sources is correct + let groups = yield addonDebugger.getSourceGroups(); + is(groups[0].name, "jid1-ami3akps3baaeg@jetpack", "Add-on code should be the first group"); + is(groups[1].name, "Add-on SDK", "Add-on SDK should be the second group"); + is(groups.length, 2, "Should be only two groups."); + + let sources = groups[0].sources; + is(sources.length, 2, "Should be two sources"); + ok(sources[0].url.endsWith("/jid1-ami3akps3baaeg@jetpack.xpi!/bootstrap.js"), "correct url for bootstrap code") + is(sources[0].label, "bootstrap.js", "correct label for bootstrap code") + is(sources[1].url, "resource://jid1-ami3akps3baaeg-at-jetpack/browser_dbg_addon3/lib/main.js", "correct url for add-on code") + is(sources[1].label, "resources/browser_dbg_addon3/lib/main.js", "correct label for add-on code") + + ok(groups[1].sources.length > 10, "SDK modules are listed"); + + yield addonDebugger.destroy(); + yield removeAddon(addon); + finish(); + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_addonactor.js b/toolkit/devtools/debugger/test/browser_dbg_addonactor.js new file mode 100644 index 000000000..9c511ceb8 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_addonactor.js @@ -0,0 +1,98 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Make sure we can attach to addon actors. + +const ADDON3_URL = EXAMPLE_URL + "addon3.xpi"; +const ADDON_MODULE_URL = "resource://jid1-ami3akps3baaeg-at-jetpack/browser_dbg_addon3/lib/main.js"; + +var gAddon, gClient, gThreadClient; + +function test() { + if (!DebuggerServer.initialized) { + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + } + + let transport = DebuggerServer.connectPipe(); + gClient = new DebuggerClient(transport); + gClient.connect((aType, aTraits) => { + is(aType, "browser", + "Root actor should identify itself as a browser."); + + installAddon() + .then(attachAddonActorForUrl.bind(null, gClient, ADDON3_URL)) + .then(attachAddonThread) + .then(testDebugger) + .then(testSources) + .then(closeConnection) + .then(uninstallAddon) + .then(finish) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); +} + +function installAddon () { + return addAddon(ADDON3_URL).then(aAddon => { + gAddon = aAddon; + }); +} + +function attachAddonThread ([aGrip, aResponse]) { + info("attached addon actor for URL"); + let deferred = promise.defer(); + + gClient.attachThread(aResponse.threadActor, (aResponse, aThreadClient) => { + info("attached thread"); + gThreadClient = aThreadClient; + gThreadClient.resume(deferred.resolve); + }); + return deferred.promise; +} + +function testDebugger() { + info('Entering testDebugger'); + let deferred = promise.defer(); + + once(gClient, "paused").then(() => { + ok(true, "Should be able to attach to addon actor"); + gThreadClient.resume(deferred.resolve) + }); + + Services.obs.notifyObservers(null, "debuggerAttached", null); + + return deferred.promise; +} + +function testSources() { + let deferred = promise.defer(); + + gThreadClient.getSources(aResponse => { + // source URLs contain launch-specific temporary directory path, + // hence the ".contains" call. + const matches = aResponse.sources.filter(s => s.url.contains(ADDON_MODULE_URL)); + ok(matches.length > 0, + "the main script of the addon is present in the source list"); + deferred.resolve(); + }); + + return deferred.promise; +} + +function uninstallAddon() { + return removeAddon(gAddon); +} + +function closeConnection () { + let deferred = promise.defer(); + gClient.close(deferred.resolve); + return deferred.promise; +} + +registerCleanupFunction(function() { + gClient = null; + gAddon = null; + gThreadClient = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_auto-pretty-print-01.js b/toolkit/devtools/debugger/test/browser_dbg_auto-pretty-print-01.js new file mode 100644 index 000000000..2ed04bd11 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_auto-pretty-print-01.js @@ -0,0 +1,110 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test auto pretty printing. + +const TAB_URL = EXAMPLE_URL + "doc_auto-pretty-print-01.html"; + +let gTab, gPanel, gDebugger; +let gEditor, gSources, gPrefs, gOptions, gView; + +let gFirstSourceLabel = "code_ugly-5.js"; +let gSecondSourceLabel = "code_ugly-6.js"; + +let gOriginalPref = Services.prefs.getBoolPref("devtools.debugger.auto-pretty-print"); +Services.prefs.setBoolPref("devtools.debugger.auto-pretty-print", true); + +function test(){ + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gSources = gDebugger.DebuggerView.Sources; + gPrefs = gDebugger.Prefs; + gOptions = gDebugger.DebuggerView.Options; + gView = gDebugger.DebuggerView; + + // Should be on by default. + testAutoPrettyPrintOn(); + + waitForSourceShown(gPanel, gFirstSourceLabel) + .then(testSourceIsUgly) + .then(() => waitForSourceShown(gPanel, gFirstSourceLabel)) + .then(testSourceIsPretty) + .then(disableAutoPrettyPrint) + .then(testAutoPrettyPrintOff) + .then(() => { + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN); + gSources.selectedIndex = 1; + return finished; + }) + .then(testSecondSourceLabel) + .then(testSourceIsUgly) + // Re-enable auto pretty printing for browser_dbg_auto-pretty-print-02.js + .then(enableAutoPrettyPrint) + .then(() => closeDebuggerAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + DevToolsUtils.safeErrorString(aError)); + }) + }); +} + +function testSourceIsUgly() { + ok(!gEditor.getText().contains("\n "), + "The source shouldn't be pretty printed yet."); +} + +function testSecondSourceLabel(){ + let source = gSources.selectedItem.attachment.source; + ok(source.url === EXAMPLE_URL + gSecondSourceLabel, + "Second source url is correct."); +} + +function testProgressBarShown() { + const deck = gDebugger.document.getElementById("editor-deck"); + is(deck.selectedIndex, 2, "The progress bar should be shown"); +} + +function testAutoPrettyPrintOn(){ + is(gPrefs.autoPrettyPrint, true, + "The auto-pretty-print pref should be on."); + is(gOptions._autoPrettyPrint.getAttribute("checked"), "true", + "The Auto pretty print menu item should be checked."); +} + +function disableAutoPrettyPrint(){ + gOptions._autoPrettyPrint.setAttribute("checked", "false"); + gOptions._toggleAutoPrettyPrint(); + gOptions._onPopupHidden(); +} + +function enableAutoPrettyPrint(){ + gOptions._autoPrettyPrint.setAttribute("checked", "true"); + gOptions._toggleAutoPrettyPrint(); + gOptions._onPopupHidden(); +} + +function testAutoPrettyPrintOff(){ + is(gPrefs.autoPrettyPrint, false, + "The auto-pretty-print pref should be off."); + isnot(gOptions._autoPrettyPrint.getAttribute("checked"), "true", + "The Auto pretty print menu item should not be checked."); +} + +function testSourceIsPretty() { + ok(gEditor.getText().contains("\n "), + "The source should be pretty printed.") +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gEditor = null; + gSources = null; + gOptions = null; + gPrefs = null; + gView = null; + Services.prefs.setBoolPref("devtools.debugger.auto-pretty-print", gOriginalPref); +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_auto-pretty-print-02.js b/toolkit/devtools/debugger/test/browser_dbg_auto-pretty-print-02.js new file mode 100644 index 000000000..65261a040 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_auto-pretty-print-02.js @@ -0,0 +1,119 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that auto pretty printing doesn't accidentally toggle + * pretty printing off when we switch to a minified source + * that is already pretty printed. + */ + +const TAB_URL = EXAMPLE_URL + "doc_auto-pretty-print-02.html"; + +let gTab, gDebuggee, gPanel, gDebugger; +let gEditor, gSources, gPrefs, gOptions, gView; + +let gFirstSourceLabel = "code_ugly-6.js"; +let gSecondSourceLabel = "code_ugly-7.js"; + +let gOriginalPref = Services.prefs.getBoolPref("devtools.debugger.auto-pretty-print"); +Services.prefs.setBoolPref("devtools.debugger.auto-pretty-print", true); + +function test(){ + initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => { + gTab = aTab; + gDebuggee = aDebuggee; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gSources = gDebugger.DebuggerView.Sources; + gPrefs = gDebugger.Prefs; + gOptions = gDebugger.DebuggerView.Options; + gView = gDebugger.DebuggerView; + + // Should be on by default. + testAutoPrettyPrintOn(); + + waitForSourceShown(gPanel, gFirstSourceLabel) + .then(testSourceIsUgly) + .then(() => waitForSourceShown(gPanel, gFirstSourceLabel)) + .then(testSourceIsPretty) + .then(testPrettyPrintButtonOn) + .then(() => { + // Switch to the second source. + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN); + gSources.selectedIndex = 1; + return finished; + }) + .then(testSecondSourceLabel) + .then(() => { + // Switch back to first source. + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN); + gSources.selectedIndex = 0; + return finished; + }) + .then(testFirstSourceLabel) + .then(testPrettyPrintButtonOn) + // Disable auto pretty printing so it does not affect the following tests. + .then(disableAutoPrettyPrint) + .then(() => closeDebuggerAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + DevToolsUtils.safeErrorString(aError)); + }) + }); +} + +function testSourceIsUgly() { + ok(!gEditor.getText().contains("\n "), + "The source shouldn't be pretty printed yet."); +} + +function testFirstSourceLabel(){ + let source = gSources.selectedItem.attachment.source; + ok(source.url === EXAMPLE_URL + gFirstSourceLabel, + "First source url is correct."); +} + +function testSecondSourceLabel(){ + let source = gSources.selectedItem.attachment.source; + ok(source.url === EXAMPLE_URL + gSecondSourceLabel, + "Second source url is correct."); +} + +function testAutoPrettyPrintOn(){ + is(gPrefs.autoPrettyPrint, true, + "The auto-pretty-print pref should be on."); + is(gOptions._autoPrettyPrint.getAttribute("checked"), "true", + "The Auto pretty print menu item should be checked."); +} + +function testPrettyPrintButtonOn(){ + is(gDebugger.document.getElementById("pretty-print").checked, true, + "The button should be checked when the source is selected."); +} + +function disableAutoPrettyPrint(){ + gOptions._autoPrettyPrint.setAttribute("checked", "false"); + gOptions._toggleAutoPrettyPrint(); + gOptions._onPopupHidden(); + info("Disabled auto pretty printing."); + // Wait for the pref update to be communicated to the server. + return waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN); +} + +function testSourceIsPretty() { + ok(gEditor.getText().contains("\n "), + "The source should be pretty printed.") +} + +registerCleanupFunction(function() { + gTab = null; + gDebuggee = null; + gPanel = null; + gDebugger = null; + gEditor = null; + gSources = null; + gOptions = null; + gPrefs = null; + gView = null; + Services.prefs.setBoolPref("devtools.debugger.auto-pretty-print", gOriginalPref); +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_bfcache.js b/toolkit/devtools/debugger/test/browser_dbg_bfcache.js new file mode 100644 index 000000000..9af0d4989 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_bfcache.js @@ -0,0 +1,89 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that the debugger is updated with the correct sources when moving + * back and forward in the tab. + */ + +const TAB_URL_1 = EXAMPLE_URL + "doc_script-switching-01.html"; +const TAB_URL_2 = EXAMPLE_URL + "doc_recursion-stack.html"; + +let gTab, gDebuggee, gPanel, gDebugger; +let gSources; + +const test = Task.async(function* () { + info("Starting browser_dbg_bfcache.js's `test`."); + + ([gTab, gDebuggee, gPanel]) = yield initDebugger(TAB_URL_1); + gDebugger = gPanel.panelWin; + gSources = gDebugger.DebuggerView.Sources; + + yield testFirstPage(); + yield testLocationChange(); + yield testBack(); + yield testForward(); + return closeDebuggerAndFinish(gPanel); +}); + +function testFirstPage() { + info("Testing first page."); + + // Spin the event loop before causing the debuggee to pause, to allow + // this function to return first. + executeSoon(() => gDebuggee.firstCall()); + + return waitForSourceAndCaretAndScopes(gPanel, "-02.js", 1) + .then(validateFirstPage); +} + +function testLocationChange() { + info("Navigating to a different page."); + + return navigateActiveTabTo(gPanel, + TAB_URL_2, + gDebugger.EVENTS.SOURCES_ADDED) + .then(validateSecondPage); +} + +function testBack() { + info("Going back."); + + return navigateActiveTabInHistory(gPanel, + "back", + gDebugger.EVENTS.SOURCES_ADDED) + .then(validateFirstPage); +} + +function testForward() { + info("Going forward."); + + return navigateActiveTabInHistory(gPanel, + "forward", + gDebugger.EVENTS.SOURCES_ADDED) + .then(validateSecondPage); +} + +function validateFirstPage() { + is(gSources.itemCount, 2, + "Found the expected number of sources."); + ok(gSources.getItemForAttachment(e => e.label == "code_script-switching-01.js"), + "Found the first source label."); + ok(gSources.getItemForAttachment(e => e.label == "code_script-switching-02.js"), + "Found the second source label."); +} + +function validateSecondPage() { + is(gSources.itemCount, 1, + "Found the expected number of sources."); + ok(gSources.getItemForAttachment(e => e.label == "doc_recursion-stack.html"), + "Found the single source label."); +} + +registerCleanupFunction(function() { + gTab = null; + gDebuggee = null; + gPanel = null; + gDebugger = null; + gSources = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_blackboxing-01.js b/toolkit/devtools/debugger/test/browser_dbg_blackboxing-01.js new file mode 100644 index 000000000..4bcb052a9 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_blackboxing-01.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that if we black box a source and then refresh, it is still black boxed. + */ + +const TAB_URL = EXAMPLE_URL + "doc_binary_search.html"; + +let gTab, gPanel, gDebugger; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + + waitForSourceShown(gPanel, ".coffee") + .then(testBlackBoxSource) + .then(testBlackBoxReload) + .then(() => closeDebuggerAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); +} + +function testBlackBoxSource() { + const bbButton = getBlackBoxButton(gPanel); + ok(!bbButton.checked, "Should not be black boxed by default"); + + return toggleBlackBoxing(gPanel).then(aSource => { + ok(aSource.isBlackBoxed, "The source should be black boxed now."); + ok(bbButton.checked, "The checkbox should no longer be checked."); + }); +} + +function testBlackBoxReload() { + return reloadActiveTab(gPanel, gDebugger.EVENTS.SOURCE_SHOWN).then(() => { + const bbButton = getBlackBoxButton(gPanel); + const selectedSource = getSelectedSourceElement(gPanel); + ok(bbButton.checked, "Should still be black boxed."); + ok(selectedSource.classList.contains("black-boxed"), + "'black-boxed' class should still be applied"); + }); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_blackboxing-02.js b/toolkit/devtools/debugger/test/browser_dbg_blackboxing-02.js new file mode 100644 index 000000000..4a66c7203 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_blackboxing-02.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that black boxed frames are compressed into a single frame on the stack + * view. + */ + +const TAB_URL = EXAMPLE_URL + "doc_blackboxing.html"; +const BLACKBOXME_URL = EXAMPLE_URL + "code_blackboxing_blackboxme.js" + +let gTab, gPanel, gDebugger; +let gFrames; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gFrames = gDebugger.DebuggerView.StackFrames; + + waitForSourceShown(gPanel, BLACKBOXME_URL) + .then(testBlackBoxSource) + .then(testBlackBoxStack) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); +} + +function testBlackBoxSource() { + return toggleBlackBoxing(gPanel).then(aSource => { + ok(aSource.isBlackBoxed, "The source should be black boxed now."); + }); +} + +function testBlackBoxStack() { + let finished = waitForSourceAndCaretAndScopes(gPanel, ".html", 21).then(() => { + is(gFrames.itemCount, 3, + "Should only get 3 frames."); + is(gDebugger.document.querySelectorAll(".dbg-stackframe-black-boxed").length, 1, + "And one of them should be the combined black boxed frames."); + }); + + callInTab(gTab, "runTest"); + return finished; +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gFrames = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_blackboxing-03.js b/toolkit/devtools/debugger/test/browser_dbg_blackboxing-03.js new file mode 100644 index 000000000..374fb8f1f --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_blackboxing-03.js @@ -0,0 +1,59 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that black boxed frames are compressed into a single frame on the stack + * view when we are already paused. + */ + +const TAB_URL = EXAMPLE_URL + "doc_blackboxing.html"; +const BLACKBOXME_URL = EXAMPLE_URL + "code_blackboxing_blackboxme.js" + +let gTab, gPanel, gDebugger; +let gFrames, gSources; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gFrames = gDebugger.DebuggerView.StackFrames; + gSources = gDebugger.DebuggerView.Sources; + + waitForSourceAndCaretAndScopes(gPanel, ".html", 21) + .then(testBlackBoxStack) + .then(testBlackBoxSource) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + + callInTab(gTab, "runTest"); + }); +} + +function testBlackBoxStack() { + is(gFrames.itemCount, 6, + "Should get 6 frames."); + is(gDebugger.document.querySelectorAll(".dbg-stackframe-black-boxed").length, 0, + "And none of them are black boxed."); +} + +function testBlackBoxSource() { + return toggleBlackBoxing(gPanel, getSourceActor(gSources, BLACKBOXME_URL)).then(aSource => { + ok(aSource.isBlackBoxed, "The source should be black boxed now."); + + is(gFrames.itemCount, 3, + "Should only get 3 frames."); + is(gDebugger.document.querySelectorAll(".dbg-stackframe-black-boxed").length, 1, + "And one of them should be the combined black boxed frames."); + }); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gFrames = null; + gSources = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_blackboxing-04.js b/toolkit/devtools/debugger/test/browser_dbg_blackboxing-04.js new file mode 100644 index 000000000..4d3df406d --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_blackboxing-04.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we get a stack frame for each black boxed source, not a single one + * for all of them. + */ + +const TAB_URL = EXAMPLE_URL + "doc_blackboxing.html"; +const BLACKBOXME_URL = EXAMPLE_URL + "code_blackboxing_blackboxme.js" + +let gTab, gPanel, gDebugger; +let gFrames, gSources; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gFrames = gDebugger.DebuggerView.StackFrames; + gSources = gDebugger.DebuggerView.Sources; + + waitForSourceShown(gPanel, BLACKBOXME_URL) + .then(blackBoxSources) + .then(testBlackBoxStack) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); +} + +function blackBoxSources() { + let finished = waitForThreadEvents(gPanel, "blackboxchange", 3); + + toggleBlackBoxing(gPanel, getSourceActor(gSources, EXAMPLE_URL + "code_blackboxing_one.js")); + toggleBlackBoxing(gPanel, getSourceActor(gSources, EXAMPLE_URL + "code_blackboxing_two.js")); + toggleBlackBoxing(gPanel, getSourceActor(gSources, EXAMPLE_URL + "code_blackboxing_three.js")); + return finished; +} + +function testBlackBoxStack() { + let finished = waitForSourceAndCaretAndScopes(gPanel, ".html", 21).then(() => { + is(gFrames.itemCount, 4, + "Should get 4 frames (one -> two -> three -> doDebuggerStatement)."); + is(gDebugger.document.querySelectorAll(".dbg-stackframe-black-boxed").length, 3, + "And 'one', 'two', and 'three' should each have their own black boxed frame."); + }); + + callInTab(gTab, "one"); + return finished; +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gFrames = null; + gSources = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_blackboxing-05.js b/toolkit/devtools/debugger/test/browser_dbg_blackboxing-05.js new file mode 100644 index 000000000..86d13f76a --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_blackboxing-05.js @@ -0,0 +1,69 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that a "this source is blackboxed" message is shown when necessary + * and can be properly dismissed. + */ + +const TAB_URL = EXAMPLE_URL + "doc_binary_search.html"; + +let gTab, gPanel, gDebugger; +let gDeck; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gDeck = gDebugger.document.getElementById("editor-deck"); + + waitForSourceShown(gPanel, ".coffee") + .then(testSourceEditorShown) + .then(toggleBlackBoxing.bind(null, gPanel)) + .then(testBlackBoxMessageShown) + .then(clickStopBlackBoxingButton) + .then(testSourceEditorShownAgain) + .then(() => closeDebuggerAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); +} + +function testSourceEditorShown() { + is(gDeck.selectedIndex, "0", + "The first item in the deck should be selected (the source editor)."); +} + +function testBlackBoxMessageShown() { + is(gDeck.selectedIndex, "1", + "The second item in the deck should be selected (the black box message)."); +} + +function clickStopBlackBoxingButton() { + // Give the test a chance to finish before triggering the click event. + executeSoon(() => getEditorBlackboxMessageButton().click()); + return waitForThreadEvents(gPanel, "blackboxchange"); +} + +function testSourceEditorShownAgain() { + // Wait a tick for the final check to make sure the frontend's click handlers + // have finished. + return new Promise(resolve => { + is(gDeck.selectedIndex, "0", + "The first item in the deck should be selected again (the source editor)."); + resolve(); + }); +} + +function getEditorBlackboxMessageButton() { + return gDebugger.document.getElementById("black-boxed-message-button"); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gDeck = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_blackboxing-06.js b/toolkit/devtools/debugger/test/browser_dbg_blackboxing-06.js new file mode 100644 index 000000000..25fbd3ae3 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_blackboxing-06.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that clicking the black box checkbox when paused doesn't re-select the + * currently paused frame's source. + */ + +const TAB_URL = EXAMPLE_URL + "doc_blackboxing.html"; + +let gTab, gPanel, gDebugger; +let gSources; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gSources = gDebugger.DebuggerView.Sources; + + waitForSourceAndCaretAndScopes(gPanel, ".html", 21) + .then(testBlackBox) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + + callInTab(gTab, "runTest"); + }); +} + +function testBlackBox() { + const selectedActor = gSources.selectedValue; + + let finished = waitForSourceShown(gPanel, "blackboxme.js").then(() => { + const newSelectedActor = gSources.selectedValue; + isnot(selectedActor, newSelectedActor, + "Should not have the same url selected."); + + return toggleBlackBoxing(gPanel).then(() => { + is(gSources.selectedValue, newSelectedActor, + "The selected source did not change."); + }); + }); + + gSources.selectedIndex = 0; + return finished; +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gSources = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_breadcrumbs-access.js b/toolkit/devtools/debugger/test/browser_dbg_breadcrumbs-access.js new file mode 100644 index 000000000..40b8a5170 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_breadcrumbs-access.js @@ -0,0 +1,86 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the stackframe breadcrumbs are keyboard accessible. + */ + +const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html"; + +function test() { + let gTab, gPanel, gDebugger; + let gSources, gFrames; + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gSources = gDebugger.DebuggerView.Sources; + gFrames = gDebugger.DebuggerView.StackFrames; + + waitForSourceAndCaretAndScopes(gPanel, "-02.js", 6) + .then(checkNavigationWhileNotFocused) + .then(focusCurrentStackFrame) + .then(checkNavigationWhileFocused) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + + callInTab(gTab, "firstCall"); + }); + + function checkNavigationWhileNotFocused() { + checkState({ frame: 1, source: 1, line: 6 }); + + EventUtils.sendKey("DOWN", gDebugger); + checkState({ frame: 1, source: 1, line: 7 }); + + EventUtils.sendKey("UP", gDebugger); + checkState({ frame: 1, source: 1, line: 6 }); + } + + function focusCurrentStackFrame() { + EventUtils.sendMouseEvent({ type: "mousedown" }, + gFrames.selectedItem.target, + gDebugger); + } + + function checkNavigationWhileFocused() { + return Task.spawn(function() { + yield promise.all([ + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES), + waitForSourceAndCaret(gPanel, "-01.js", 5), + waitForEditorLocationSet(gPanel), + EventUtils.sendKey("UP", gDebugger) + ]); + checkState({ frame: 0, source: 0, line: 5 }); + + yield promise.all([ + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES), + waitForSourceAndCaret(gPanel, "-02.js", 6), + waitForEditorLocationSet(gPanel), + EventUtils.sendKey("END", gDebugger) + ]); + checkState({ frame: 1, source: 1, line: 6 }); + + yield promise.all([ + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES), + waitForSourceAndCaret(gPanel, "-01.js", 5), + waitForEditorLocationSet(gPanel), + EventUtils.sendKey("HOME", gDebugger) + ]); + + checkState({ frame: 0, source: 0, line: 5 }); + }); + } + + function checkState({ frame, source, line, column }) { + is(gFrames.selectedIndex, frame, + "The currently selected stackframe is incorrect."); + is(gSources.selectedIndex, source, + "The currently selected source is incorrect."); + ok(isCaretPos(gPanel, line, column), + "The source editor caret position was incorrect."); + } +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_break-on-dom-01.js b/toolkit/devtools/debugger/test/browser_dbg_break-on-dom-01.js new file mode 100644 index 000000000..86be67b00 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_break-on-dom-01.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that event listeners aren't fetched when the events tab isn't selected. + */ + +const TAB_URL = EXAMPLE_URL + "doc_event-listeners-02.html"; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + let gDebugger = aPanel.panelWin; + let gView = gDebugger.DebuggerView; + let gEvents = gView.EventListeners; + + gDebugger.on(gDebugger.EVENTS.EVENT_LISTENERS_FETCHED, () => { + ok(false, "Shouldn't have fetched any event listeners."); + }); + gDebugger.on(gDebugger.EVENTS.EVENT_BREAKPOINTS_UPDATED, () => { + ok(false, "Shouldn't have updated any event breakpoints."); + }); + + gView.toggleInstrumentsPane({ visible: true, animated: false }); + + is(gView.instrumentsPaneHidden, false, + "The instruments pane should be visible now."); + is(gView.instrumentsPaneTab, "variables-tab", + "The variables tab should be selected by default."); + + Task.spawn(function() { + yield waitForSourceShown(aPanel, ".html"); + is(gEvents.itemCount, 0, "There should be no events before reloading."); + + let reloaded = waitForSourcesAfterReload(); + gDebugger.DebuggerController._target.activeTab.reload(); + + is(gEvents.itemCount, 0, "There should be no events while reloading."); + yield reloaded; + is(gEvents.itemCount, 0, "There should be no events after reloading."); + + yield closeDebuggerAndFinish(aPanel); + }); + + function waitForSourcesAfterReload() { + return promise.all([ + waitForDebuggerEvents(aPanel, gDebugger.EVENTS.NEW_SOURCE), + waitForDebuggerEvents(aPanel, gDebugger.EVENTS.SOURCES_ADDED), + waitForDebuggerEvents(aPanel, gDebugger.EVENTS.SOURCE_SHOWN) + ]); + } + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_break-on-dom-02.js b/toolkit/devtools/debugger/test/browser_dbg_break-on-dom-02.js new file mode 100644 index 000000000..3a5d127c9 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_break-on-dom-02.js @@ -0,0 +1,126 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that event listeners are fetched when the events tab is selected + * or while sources are fetched and the events tab is focused. + */ + +const TAB_URL = EXAMPLE_URL + "doc_event-listeners-02.html"; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + let gDebugger = aPanel.panelWin; + let gView = gDebugger.DebuggerView; + let gEvents = gView.EventListeners; + + Task.spawn(function() { + yield waitForSourceShown(aPanel, ".html"); + yield testFetchOnFocus(); + yield testFetchOnReloadWhenFocused(); + yield testFetchOnReloadWhenNotFocused(); + yield closeDebuggerAndFinish(aPanel); + }); + + function testFetchOnFocus() { + return Task.spawn(function() { + let fetched = waitForDebuggerEvents(aPanel, gDebugger.EVENTS.EVENT_LISTENERS_FETCHED); + + gView.toggleInstrumentsPane({ visible: true, animated: false }, 1); + is(gView.instrumentsPaneHidden, false, + "The instruments pane should be visible now."); + is(gView.instrumentsPaneTab, "events-tab", + "The events tab should be selected."); + + yield fetched; + + ok(true, + "Event listeners were fetched when the events tab was selected"); + is(gEvents.itemCount, 4, + "There should be 4 events displayed in the view."); + }); + } + + function testFetchOnReloadWhenFocused() { + return Task.spawn(function() { + let fetched = waitForDebuggerEvents(aPanel, gDebugger.EVENTS.EVENT_LISTENERS_FETCHED); + + let reloading = once(gDebugger.gTarget, "will-navigate"); + let reloaded = waitForSourcesAfterReload(); + gDebugger.DebuggerController._target.activeTab.reload(); + + yield reloading; + + is(gEvents.itemCount, 0, + "There should be no events displayed in the view while reloading."); + ok(true, + "Event listeners were removed when the target started navigating."); + + yield reloaded; + + is(gView.instrumentsPaneHidden, false, + "The instruments pane should still be visible."); + is(gView.instrumentsPaneTab, "events-tab", + "The events tab should still be selected."); + + yield fetched; + + is(gEvents.itemCount, 4, + "There should be 4 events displayed in the view after reloading."); + ok(true, + "Event listeners were added back after the target finished navigating."); + }); + } + + function testFetchOnReloadWhenNotFocused() { + return Task.spawn(function() { + gDebugger.on(gDebugger.EVENTS.EVENT_LISTENERS_FETCHED, () => { + ok(false, "Shouldn't have fetched any event listeners."); + }); + gDebugger.on(gDebugger.EVENTS.EVENT_BREAKPOINTS_UPDATED, () => { + ok(false, "Shouldn't have updated any event breakpoints."); + }); + + gView.toggleInstrumentsPane({ visible: true, animated: false }, 0); + is(gView.instrumentsPaneHidden, false, + "The instruments pane should still be visible."); + is(gView.instrumentsPaneTab, "variables-tab", + "The variables tab should be selected."); + + let reloading = once(gDebugger.gTarget, "will-navigate"); + let reloaded = waitForSourcesAfterReload(); + gDebugger.DebuggerController._target.activeTab.reload(); + + yield reloading; + + is(gEvents.itemCount, 0, + "There should be no events displayed in the view while reloading."); + ok(true, + "Event listeners were removed when the target started navigating."); + + yield reloaded; + + is(gView.instrumentsPaneHidden, false, + "The instruments pane should still be visible."); + is(gView.instrumentsPaneTab, "variables-tab", + "The variables tab should still be selected."); + + // Just to be really sure that the events will never ever fire. + yield waitForTime(1000); + + is(gEvents.itemCount, 0, + "There should be no events displayed in the view after reloading."); + ok(true, + "Event listeners were not added after the target finished navigating."); + }); + } + + function waitForSourcesAfterReload() { + return promise.all([ + waitForDebuggerEvents(aPanel, gDebugger.EVENTS.NEW_SOURCE), + waitForDebuggerEvents(aPanel, gDebugger.EVENTS.SOURCES_ADDED), + waitForDebuggerEvents(aPanel, gDebugger.EVENTS.SOURCE_SHOWN) + ]); + } + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_break-on-dom-03.js b/toolkit/devtools/debugger/test/browser_dbg_break-on-dom-03.js new file mode 100644 index 000000000..5ce03e561 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_break-on-dom-03.js @@ -0,0 +1,89 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that event listeners are properly displayed in the view. + */ + +const TAB_URL = EXAMPLE_URL + "doc_event-listeners-02.html"; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + let gDebugger = aPanel.panelWin; + let gView = gDebugger.DebuggerView; + let gEvents = gView.EventListeners; + + Task.spawn(function() { + yield waitForSourceShown(aPanel, ".html"); + + let fetched = waitForDebuggerEvents(aPanel, gDebugger.EVENTS.EVENT_LISTENERS_FETCHED); + gView.toggleInstrumentsPane({ visible: true, animated: false }, 1); + yield fetched; + + is(gEvents.widget._parent.querySelectorAll(".side-menu-widget-group").length, 3, + "There should be 3 groups shown in the view."); + is(gEvents.widget._parent.querySelectorAll(".side-menu-widget-group-checkbox").length, 3, + "There should be a checkbox for each group shown in the view."); + + is(gEvents.widget._parent.querySelectorAll(".side-menu-widget-item").length, 4, + "There should be 4 items shown in the view."); + is(gEvents.widget._parent.querySelectorAll(".side-menu-widget-item-checkbox").length, 4, + "There should be a checkbox for each item shown in the view."); + + testEventItem(0, "doc_event-listeners-02.html", "change", ["body > input:nth-child(2)"], false); + testEventItem(1, "doc_event-listeners-02.html", "click", ["body > button:nth-child(1)"], false); + testEventItem(2, "doc_event-listeners-02.html", "keydown", ["window", "body"], false); + testEventItem(3, "doc_event-listeners-02.html", "keyup", ["body > input:nth-child(2)"], false); + + testEventGroup("interactionEvents", false); + testEventGroup("keyboardEvents", false); + testEventGroup("mouseEvents", false); + + is(gEvents.getAllEvents().toString(), "change,click,keydown,keyup", + "The getAllEvents() method returns the correct stuff."); + is(gEvents.getCheckedEvents().toString(), "", + "The getCheckedEvents() method returns the correct stuff."); + + yield ensureThreadClientState(aPanel, "resumed"); + yield closeDebuggerAndFinish(aPanel); + }); + + function testEventItem(index, label, type, selectors, checked) { + let item = gEvents.items[index]; + let node = item.target; + + ok(item.attachment.url.contains(label), + "The event at index " + index + " has the correct url."); + is(item.attachment.type, type, + "The event at index " + index + " has the correct type."); + is(item.attachment.selectors.toString(), selectors, + "The event at index " + index + " has the correct selectors."); + is(item.attachment.checkboxState, checked, + "The event at index " + index + " has the correct checkbox state."); + + let targets = selectors.length > 1 + ? gDebugger.L10N.getFormatStr("eventNodes", selectors.length) + : selectors.toString(); + + is(node.querySelector(".dbg-event-listener-type").getAttribute("value"), type, + "The correct type is shown for this event."); + is(node.querySelector(".dbg-event-listener-targets").getAttribute("value"), targets, + "The correct target is shown for this event."); + is(node.querySelector(".dbg-event-listener-location").getAttribute("value"), label, + "The correct location is shown for this event."); + is(node.parentNode.querySelector(".side-menu-widget-item-checkbox").checked, checked, + "The correct checkbox state is shown for this event."); + } + + function testEventGroup(string, checked) { + let name = gDebugger.L10N.getStr(string); + let group = gEvents.widget._parent + .querySelector(".side-menu-widget-group[name=" + name + "]"); + + is(group.querySelector(".side-menu-widget-group-title > .name").value, name, + "The correct label is shown for the group named " + name + "."); + is(group.querySelector(".side-menu-widget-group-checkbox").checked, checked, + "The correct checkbox state is shown for the group named " + name + "."); + } + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_break-on-dom-04.js b/toolkit/devtools/debugger/test/browser_dbg_break-on-dom-04.js new file mode 100644 index 000000000..e68c9b0c2 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_break-on-dom-04.js @@ -0,0 +1,97 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that checking/unchecking an event listener in the view correctly + * causes the active thread to get updated with the new event breakpoints. + */ + +const TAB_URL = EXAMPLE_URL + "doc_event-listeners-02.html"; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + let gDebugger = aPanel.panelWin; + let gView = gDebugger.DebuggerView; + let gController = gDebugger.DebuggerController + let gEvents = gView.EventListeners; + let gBreakpoints = gController.Breakpoints; + + Task.spawn(function() { + yield waitForSourceShown(aPanel, ".html"); + + let fetched = waitForDebuggerEvents(aPanel, gDebugger.EVENTS.EVENT_LISTENERS_FETCHED); + gView.toggleInstrumentsPane({ visible: true, animated: false }, 1); + yield fetched; + + testEventItem(0, false); + testEventItem(1, false); + testEventItem(2, false); + testEventItem(3, false); + testEventGroup("interactionEvents", false); + testEventGroup("keyboardEvents", false); + testEventGroup("mouseEvents", false); + testEventArrays("change,click,keydown,keyup", ""); + + let updated = waitForDebuggerEvents(aPanel, gDebugger.EVENTS.EVENT_BREAKPOINTS_UPDATED); + EventUtils.sendMouseEvent({ type: "click" }, getItemCheckboxNode(0), gDebugger); + yield updated; + + testEventItem(0, true); + testEventItem(1, false); + testEventItem(2, false); + testEventItem(3, false); + testEventGroup("interactionEvents", false); + testEventGroup("keyboardEvents", false); + testEventGroup("mouseEvents", false); + testEventArrays("change,click,keydown,keyup", "change"); + + updated = waitForDebuggerEvents(aPanel, gDebugger.EVENTS.EVENT_BREAKPOINTS_UPDATED); + EventUtils.sendMouseEvent({ type: "click" }, getItemCheckboxNode(0), gDebugger); + yield updated; + + testEventItem(0, false); + testEventItem(1, false); + testEventItem(2, false); + testEventItem(3, false); + testEventGroup("interactionEvents", false); + testEventGroup("keyboardEvents", false); + testEventGroup("mouseEvents", false); + testEventArrays("change,click,keydown,keyup", ""); + + yield ensureThreadClientState(aPanel, "resumed"); + yield closeDebuggerAndFinish(aPanel); + }); + + function getItemCheckboxNode(index) { + return gEvents.items[index].target.parentNode + .querySelector(".side-menu-widget-item-checkbox"); + } + + function getGroupCheckboxNode(string) { + return gEvents.widget._parent + .querySelector(".side-menu-widget-group[name=" + gDebugger.L10N.getStr(string) + "]") + .querySelector(".side-menu-widget-group-checkbox"); + } + + function testEventItem(index, checked) { + is(gEvents.attachments[index].checkboxState, checked, + "The event at index " + index + " has the correct checkbox state."); + is(getItemCheckboxNode(index).checked, checked, + "The correct checkbox state is shown for this event."); + } + + function testEventGroup(string, checked) { + is(getGroupCheckboxNode(string).checked, checked, + "The correct checkbox state is shown for the group " + string + "."); + } + + function testEventArrays(all, checked) { + is(gEvents.getAllEvents().toString(), all, + "The getAllEvents() method returns the correct stuff."); + is(gEvents.getCheckedEvents().toString(), checked, + "The getCheckedEvents() method returns the correct stuff."); + is(gBreakpoints.DOM.activeEventNames.toString(), checked, + "The correct event names are listed as being active breakpoints."); + } + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_break-on-dom-05.js b/toolkit/devtools/debugger/test/browser_dbg_break-on-dom-05.js new file mode 100644 index 000000000..5356e5b22 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_break-on-dom-05.js @@ -0,0 +1,124 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that checking/unchecking an event listener's group in the view will + * cause the active thread to get updated with the new event breakpoints for + * all children inside that group. + */ + +const TAB_URL = EXAMPLE_URL + "doc_event-listeners-02.html"; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + let gDebugger = aPanel.panelWin; + let gView = gDebugger.DebuggerView; + let gController = gDebugger.DebuggerController + let gEvents = gView.EventListeners; + let gBreakpoints = gController.Breakpoints; + + Task.spawn(function() { + yield waitForSourceShown(aPanel, ".html"); + + let fetched = waitForDebuggerEvents(aPanel, gDebugger.EVENTS.EVENT_LISTENERS_FETCHED); + gView.toggleInstrumentsPane({ visible: true, animated: false }, 1); + yield fetched; + + testEventItem(0, false); + testEventItem(1, false); + testEventItem(2, false); + testEventItem(3, false); + testEventGroup("interactionEvents", false); + testEventGroup("keyboardEvents", false); + testEventGroup("mouseEvents", false); + testEventArrays("change,click,keydown,keyup", ""); + + let updated = waitForDebuggerEvents(aPanel, gDebugger.EVENTS.EVENT_BREAKPOINTS_UPDATED); + EventUtils.sendMouseEvent({ type: "click" }, getGroupCheckboxNode("interactionEvents"), gDebugger); + yield updated; + + testEventItem(0, true); + testEventItem(1, false); + testEventItem(2, false); + testEventItem(3, false); + testEventGroup("interactionEvents", true); + testEventGroup("keyboardEvents", false); + testEventGroup("mouseEvents", false); + testEventArrays("change,click,keydown,keyup", "change"); + + updated = waitForDebuggerEvents(aPanel, gDebugger.EVENTS.EVENT_BREAKPOINTS_UPDATED); + EventUtils.sendMouseEvent({ type: "click" }, getGroupCheckboxNode("interactionEvents"), gDebugger); + yield updated; + + testEventItem(0, false); + testEventItem(1, false); + testEventItem(2, false); + testEventItem(3, false); + testEventGroup("interactionEvents", false); + testEventGroup("keyboardEvents", false); + testEventGroup("mouseEvents", false); + testEventArrays("change,click,keydown,keyup", ""); + + updated = waitForDebuggerEvents(aPanel, gDebugger.EVENTS.EVENT_BREAKPOINTS_UPDATED); + EventUtils.sendMouseEvent({ type: "click" }, getGroupCheckboxNode("keyboardEvents"), gDebugger); + yield updated; + + testEventItem(0, false); + testEventItem(1, false); + testEventItem(2, true); + testEventItem(3, true); + testEventGroup("interactionEvents", false); + testEventGroup("keyboardEvents", true); + testEventGroup("mouseEvents", false); + testEventArrays("change,click,keydown,keyup", "keydown,keyup"); + + updated = waitForDebuggerEvents(aPanel, gDebugger.EVENTS.EVENT_BREAKPOINTS_UPDATED); + EventUtils.sendMouseEvent({ type: "click" }, getGroupCheckboxNode("keyboardEvents"), gDebugger); + yield updated; + + testEventItem(0, false); + testEventItem(1, false); + testEventItem(2, false); + testEventItem(3, false); + testEventGroup("interactionEvents", false); + testEventGroup("keyboardEvents", false); + testEventGroup("mouseEvents", false); + testEventArrays("change,click,keydown,keyup", ""); + + yield ensureThreadClientState(aPanel, "resumed"); + yield closeDebuggerAndFinish(aPanel); + }); + + function getItemCheckboxNode(index) { + return gEvents.items[index].target.parentNode + .querySelector(".side-menu-widget-item-checkbox"); + } + + function getGroupCheckboxNode(string) { + return gEvents.widget._parent + .querySelector(".side-menu-widget-group[name=" + gDebugger.L10N.getStr(string) + "]") + .querySelector(".side-menu-widget-group-checkbox"); + } + + function testEventItem(index, checked) { + is(gEvents.attachments[index].checkboxState, checked, + "The event at index " + index + " has the correct checkbox state."); + is(getItemCheckboxNode(index).checked, checked, + "The correct checkbox state is shown for this event."); + } + + function testEventGroup(string, checked) { + is(getGroupCheckboxNode(string).checked, checked, + "The correct checkbox state is shown for the group " + string + "."); + } + + function testEventArrays(all, checked) { + is(gEvents.getAllEvents().toString(), all, + "The getAllEvents() method returns the correct stuff."); + is(gEvents.getCheckedEvents().toString(), checked, + "The getCheckedEvents() method returns the correct stuff."); + is(gBreakpoints.DOM.activeEventNames.toString(), checked, + "The correct event names are listed as being active breakpoints."); + } + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_break-on-dom-06.js b/toolkit/devtools/debugger/test/browser_dbg_break-on-dom-06.js new file mode 100644 index 000000000..38e5e3ee3 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_break-on-dom-06.js @@ -0,0 +1,123 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that the event listener states are preserved in the view after the + * target navigates. + */ + +const TAB_URL = EXAMPLE_URL + "doc_event-listeners-02.html"; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + let gDebugger = aPanel.panelWin; + let gView = gDebugger.DebuggerView; + let gController = gDebugger.DebuggerController + let gEvents = gView.EventListeners; + let gBreakpoints = gController.Breakpoints; + + Task.spawn(function() { + yield waitForSourceShown(aPanel, ".html"); + + let fetched = waitForDebuggerEvents(aPanel, gDebugger.EVENTS.EVENT_LISTENERS_FETCHED); + gView.toggleInstrumentsPane({ visible: true, animated: false }, 1); + yield fetched; + + testEventItem(0, false); + testEventItem(1, false); + testEventItem(2, false); + testEventItem(3, false); + testEventGroup("interactionEvents", false); + testEventGroup("keyboardEvents", false); + testEventGroup("mouseEvents", false); + testEventArrays("change,click,keydown,keyup", ""); + + let updated = waitForDebuggerEvents(aPanel, gDebugger.EVENTS.EVENT_BREAKPOINTS_UPDATED); + EventUtils.sendMouseEvent({ type: "click" }, getItemCheckboxNode(0), gDebugger); + EventUtils.sendMouseEvent({ type: "click" }, getItemCheckboxNode(1), gDebugger); + EventUtils.sendMouseEvent({ type: "click" }, getItemCheckboxNode(2), gDebugger); + yield updated; + + testEventItem(0, true); + testEventItem(1, true); + testEventItem(2, true); + testEventItem(3, false); + testEventGroup("interactionEvents", false); + testEventGroup("keyboardEvents", false); + testEventGroup("mouseEvents", false); + testEventArrays("change,click,keydown,keyup", "change,click,keydown"); + + yield reloadActiveTab(aPanel, gDebugger.EVENTS.EVENT_LISTENERS_FETCHED); + + testEventItem(0, true); + testEventItem(1, true); + testEventItem(2, true); + testEventItem(3, false); + testEventGroup("interactionEvents", false); + testEventGroup("keyboardEvents", false); + testEventGroup("mouseEvents", false); + testEventArrays("change,click,keydown,keyup", "change,click,keydown"); + + updated = waitForDebuggerEvents(aPanel, gDebugger.EVENTS.EVENT_BREAKPOINTS_UPDATED); + EventUtils.sendMouseEvent({ type: "click" }, getItemCheckboxNode(0), gDebugger); + EventUtils.sendMouseEvent({ type: "click" }, getItemCheckboxNode(1), gDebugger); + EventUtils.sendMouseEvent({ type: "click" }, getItemCheckboxNode(2), gDebugger); + yield updated; + + testEventItem(0, false); + testEventItem(1, false); + testEventItem(2, false); + testEventItem(3, false); + testEventGroup("interactionEvents", false); + testEventGroup("keyboardEvents", false); + testEventGroup("mouseEvents", false); + testEventArrays("change,click,keydown,keyup", ""); + + yield reloadActiveTab(aPanel, gDebugger.EVENTS.EVENT_LISTENERS_FETCHED); + + testEventItem(0, false); + testEventItem(1, false); + testEventItem(2, false); + testEventItem(3, false); + testEventGroup("interactionEvents", false); + testEventGroup("keyboardEvents", false); + testEventGroup("mouseEvents", false); + testEventArrays("change,click,keydown,keyup", ""); + + yield ensureThreadClientState(aPanel, "resumed"); + yield closeDebuggerAndFinish(aPanel); + }); + + function getItemCheckboxNode(index) { + return gEvents.items[index].target.parentNode + .querySelector(".side-menu-widget-item-checkbox"); + } + + function getGroupCheckboxNode(string) { + return gEvents.widget._parent + .querySelector(".side-menu-widget-group[name=" + gDebugger.L10N.getStr(string) + "]") + .querySelector(".side-menu-widget-group-checkbox"); + } + + function testEventItem(index, checked) { + is(gEvents.attachments[index].checkboxState, checked, + "The event at index " + index + " has the correct checkbox state."); + is(getItemCheckboxNode(index).checked, checked, + "The correct checkbox state is shown for this event."); + } + + function testEventGroup(string, checked) { + is(getGroupCheckboxNode(string).checked, checked, + "The correct checkbox state is shown for the group " + string + "."); + } + + function testEventArrays(all, checked) { + is(gEvents.getAllEvents().toString(), all, + "The getAllEvents() method returns the correct stuff."); + is(gEvents.getCheckedEvents().toString(), checked, + "The getCheckedEvents() method returns the correct stuff."); + is(gBreakpoints.DOM.activeEventNames.toString(), checked, + "The correct event names are listed as being active breakpoints."); + } + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_break-on-dom-07.js b/toolkit/devtools/debugger/test/browser_dbg_break-on-dom-07.js new file mode 100644 index 000000000..1c5ce9034 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_break-on-dom-07.js @@ -0,0 +1,104 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that system event listeners don't get duplicated in the view. + */ + +function test() { + initDebugger("about:blank").then(([aTab,, aPanel]) => { + let gDebugger = aPanel.panelWin; + let gView = gDebugger.DebuggerView; + let gEvents = gView.EventListeners; + let gL10N = gDebugger.L10N; + + is(gEvents.itemCount, 0, + "There are no events displayed in the corresponding pane yet."); + + gEvents.addListener({ + type: "foo", + node: { selector: "#first" }, + function: { url: null } + }); + + is(gEvents.itemCount, 1, + "There was a system event listener added in the view."); + is(gEvents.attachments[0].url, gL10N.getStr("eventNative"), + "The correct string is used as the event's url."); + is(gEvents.attachments[0].type, "foo", + "The correct string is used as the event's type."); + is(gEvents.attachments[0].selectors.toString(), "#first", + "The correct array of selectors is used as the event's target."); + + gEvents.addListener({ + type: "bar", + node: { selector: "#second" }, + function: { url: null } + }); + + is(gEvents.itemCount, 2, + "There was another system event listener added in the view."); + is(gEvents.attachments[1].url, gL10N.getStr("eventNative"), + "The correct string is used as the event's url."); + is(gEvents.attachments[1].type, "bar", + "The correct string is used as the event's type."); + is(gEvents.attachments[1].selectors.toString(), "#second", + "The correct array of selectors is used as the event's target."); + + gEvents.addListener({ + type: "foo", + node: { selector: "#first" }, + function: { url: null } + }); + + is(gEvents.itemCount, 2, + "There wasn't another system event listener added in the view."); + is(gEvents.attachments[0].url, gL10N.getStr("eventNative"), + "The correct string is used as the event's url."); + is(gEvents.attachments[0].type, "foo", + "The correct string is used as the event's type."); + is(gEvents.attachments[0].selectors.toString(), "#first", + "The correct array of selectors is used as the event's target."); + + gEvents.addListener({ + type: "foo", + node: { selector: "#second" }, + function: { url: null } + }); + + is(gEvents.itemCount, 2, + "There still wasn't another system event listener added in the view."); + is(gEvents.attachments[0].url, gL10N.getStr("eventNative"), + "The correct string is used as the event's url."); + is(gEvents.attachments[0].type, "foo", + "The correct string is used as the event's type."); + is(gEvents.attachments[0].selectors.toString(), "#first,#second", + "The correct array of selectors is used as the event's target."); + + + gEvents.addListener({ + type: null, + node: { selector: "#bogus" }, + function: { url: null } + }); + + is(gEvents.itemCount, 2, + "No bogus system event listener was added in the view."); + + is(gEvents.attachments[0].url, gL10N.getStr("eventNative"), + "The correct string is used as the first event's url."); + is(gEvents.attachments[0].type, "foo", + "The correct string is used as the first event's type."); + is(gEvents.attachments[0].selectors.toString(), "#first,#second", + "The correct array of selectors is used as the first event's target."); + + is(gEvents.attachments[1].url, gL10N.getStr("eventNative"), + "The correct string is used as the second event's url."); + is(gEvents.attachments[1].type, "bar", + "The correct string is used as the second event's type."); + is(gEvents.attachments[1].selectors.toString(), "#second", + "The correct array of selectors is used as the second event's target."); + + closeDebuggerAndFinish(aPanel); + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_break-on-dom-08.js b/toolkit/devtools/debugger/test/browser_dbg_break-on-dom-08.js new file mode 100644 index 000000000..ce92a9ac1 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_break-on-dom-08.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that breaking on an event selects the variables view tab. + */ + +const TAB_URL = EXAMPLE_URL + "doc_event-listeners-02.html"; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + let gTab = aTab; + let gDebugger = aPanel.panelWin; + let gView = gDebugger.DebuggerView; + let gEvents = gView.EventListeners; + + Task.spawn(function() { + yield waitForSourceShown(aPanel, ".html"); + yield callInTab(gTab, "addBodyClickEventListener"); + + let fetched = waitForDebuggerEvents(aPanel, gDebugger.EVENTS.EVENT_LISTENERS_FETCHED); + gView.toggleInstrumentsPane({ visible: true, animated: false }, 1); + yield fetched; + yield ensureThreadClientState(aPanel, "resumed"); + + is(gView.instrumentsPaneHidden, false, + "The instruments pane should be visible."); + is(gView.instrumentsPaneTab, "events-tab", + "The events tab should be selected."); + + let updated = waitForDebuggerEvents(aPanel, gDebugger.EVENTS.EVENT_BREAKPOINTS_UPDATED); + EventUtils.sendMouseEvent({ type: "click" }, getItemCheckboxNode(1), gDebugger); + yield updated; + yield ensureThreadClientState(aPanel, "resumed"); + + let paused = waitForCaretAndScopes(aPanel, 48); + sendMouseClickToTab(gTab, content.document.body); + yield paused; + yield ensureThreadClientState(aPanel, "paused"); + + is(gView.instrumentsPaneHidden, false, + "The instruments pane should be visible."); + is(gView.instrumentsPaneTab, "variables-tab", + "The variables tab should be selected."); + + yield resumeDebuggerThenCloseAndFinish(aPanel); + }); + + function getItemCheckboxNode(index) { + return gEvents.items[index].target.parentNode + .querySelector(".side-menu-widget-item-checkbox"); + } + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_break-on-dom-event-01.js b/toolkit/devtools/debugger/test/browser_dbg_break-on-dom-event-01.js new file mode 100644 index 000000000..d8dfb8a08 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_break-on-dom-event-01.js @@ -0,0 +1,229 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that the break-on-dom-events request works. + */ + +const TAB_URL = EXAMPLE_URL + "doc_event-listeners-01.html"; + +let gClient, gThreadClient, gInput, gButton; + +function test() { + if (!DebuggerServer.initialized) { + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + } + + let transport = DebuggerServer.connectPipe(); + gClient = new DebuggerClient(transport); + gClient.connect((aType, aTraits) => { + is(aType, "browser", + "Root actor should identify itself as a browser."); + + addTab(TAB_URL) + .then(() => attachThreadActorForUrl(gClient, TAB_URL)) + .then(setupGlobals) + .then(pauseDebuggee) + .then(testBreakOnAll) + .then(testBreakOnDisabled) + .then(testBreakOnNone) + .then(testBreakOnClick) + .then(closeConnection) + .then(finish) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); +} + +function setupGlobals(aThreadClient) { + gThreadClient = aThreadClient; + gInput = content.document.querySelector("input"); + gButton = content.document.querySelector("button"); +} + +function pauseDebuggee() { + let deferred = promise.defer(); + + gClient.addOneTimeListener("paused", (aEvent, aPacket) => { + is(aPacket.type, "paused", + "We should now be paused."); + is(aPacket.why.type, "debuggerStatement", + "The debugger statement was hit."); + + deferred.resolve(); + }); + + // Spin the event loop before causing the debuggee to pause, to allow + // this function to return first. + executeSoon(triggerButtonClick); + + return deferred.promise; +} + +// Test pause on all events. +function testBreakOnAll() { + let deferred = promise.defer(); + + // Test calling pauseOnDOMEvents from a paused state. + gThreadClient.pauseOnDOMEvents("*", (aPacket) => { + is(aPacket.error, undefined, + "The pause-on-any-event request completed successfully."); + + gClient.addOneTimeListener("paused", (aEvent, aPacket) => { + is(aPacket.why.type, "pauseOnDOMEvents", + "A hidden breakpoint was hit."); + is(aPacket.frame.callee.name, "keyupHandler", + "The keyupHandler is entered."); + + gClient.addOneTimeListener("paused", (aEvent, aPacket) => { + is(aPacket.why.type, "pauseOnDOMEvents", + "A hidden breakpoint was hit."); + is(aPacket.frame.callee.name, "clickHandler", + "The clickHandler is entered."); + + gClient.addOneTimeListener("paused", (aEvent, aPacket) => { + is(aPacket.why.type, "pauseOnDOMEvents", + "A hidden breakpoint was hit."); + is(aPacket.frame.callee.name, "onchange", + "The onchange handler is entered."); + + gThreadClient.resume(deferred.resolve); + }); + + gThreadClient.resume(triggerInputChange); + }); + + gThreadClient.resume(triggerButtonClick); + }); + + gThreadClient.resume(triggerInputKeyup); + }); + + return deferred.promise; +} + +// Test that removing events from the array disables them. +function testBreakOnDisabled() { + let deferred = promise.defer(); + + // Test calling pauseOnDOMEvents from a running state. + gThreadClient.pauseOnDOMEvents(["click"], (aPacket) => { + is(aPacket.error, undefined, + "The pause-on-click-only request completed successfully."); + + gClient.addListener("paused", unexpectedListener); + + // This non-capturing event listener is guaranteed to run after the page's + // capturing one had a chance to execute and modify window.foobar. + once(gInput, "keyup").then(() => { + is(content.wrappedJSObject.foobar, "keyupHandler", + "No hidden breakpoint was hit."); + + gClient.removeListener("paused", unexpectedListener); + deferred.resolve(); + }); + + triggerInputKeyup(); + }); + + return deferred.promise; +} + +// Test that specifying an empty event array clears all hidden breakpoints. +function testBreakOnNone() { + let deferred = promise.defer(); + + // Test calling pauseOnDOMEvents from a running state. + gThreadClient.pauseOnDOMEvents([], (aPacket) => { + is(aPacket.error, undefined, + "The pause-on-none request completed successfully."); + + gClient.addListener("paused", unexpectedListener); + + // This non-capturing event listener is guaranteed to run after the page's + // capturing one had a chance to execute and modify window.foobar. + once(gInput, "keyup").then(() => { + is(content.wrappedJSObject.foobar, "keyupHandler", + "No hidden breakpoint was hit."); + + gClient.removeListener("paused", unexpectedListener); + deferred.resolve(); + }); + + triggerInputKeyup(); + }); + + return deferred.promise; +} + +// Test pause on a single event. +function testBreakOnClick() { + let deferred = promise.defer(); + + // Test calling pauseOnDOMEvents from a running state. + gThreadClient.pauseOnDOMEvents(["click"], (aPacket) => { + is(aPacket.error, undefined, + "The pause-on-click request completed successfully."); + + gClient.addOneTimeListener("paused", (aEvent, aPacket) => { + is(aPacket.why.type, "pauseOnDOMEvents", + "A hidden breakpoint was hit."); + is(aPacket.frame.callee.name, "clickHandler", + "The clickHandler is entered."); + + gThreadClient.resume(deferred.resolve); + }); + + triggerButtonClick(); + }); + + return deferred.promise; +} + +function closeConnection() { + let deferred = promise.defer(); + gClient.close(deferred.resolve); + return deferred.promise; +} + +function unexpectedListener() { + gClient.removeListener("paused", unexpectedListener); + ok(false, "An unexpected hidden breakpoint was hit."); + gThreadClient.resume(testBreakOnClick); +} + +function triggerInputKeyup() { + // Make sure that the focus is not on the input box so that a focus event + // will be triggered. + window.focus(); + gBrowser.selectedBrowser.focus(); + gButton.focus(); + + // Focus the element and wait for focus event. + once(gInput, "focus").then(() => { + executeSoon(() => { + EventUtils.synthesizeKey("e", { shiftKey: 1 }, content); + }); + }); + + gInput.focus(); +} + +function triggerButtonClick() { + EventUtils.sendMouseEvent({ type: "click" }, gButton); +} + +function triggerInputChange() { + gInput.focus(); + gInput.value = "foo"; + gInput.blur(); +} + +registerCleanupFunction(function() { + gClient = null; + gThreadClient = null; + gInput = null; + gButton = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_break-on-dom-event-02.js b/toolkit/devtools/debugger/test/browser_dbg_break-on-dom-event-02.js new file mode 100644 index 000000000..dd7a33a30 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_break-on-dom-event-02.js @@ -0,0 +1,109 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that the break-on-dom-events request works even for bound event + * listeners and handler objects with 'handleEvent' methods. + */ + +const TAB_URL = EXAMPLE_URL + "doc_event-listeners-03.html"; + +let gClient, gThreadClient; + +function test() { + if (!DebuggerServer.initialized) { + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + } + + let transport = DebuggerServer.connectPipe(); + gClient = new DebuggerClient(transport); + gClient.connect((aType, aTraits) => { + is(aType, "browser", + "Root actor should identify itself as a browser."); + + addTab(TAB_URL) + .then(() => attachThreadActorForUrl(gClient, TAB_URL)) + .then(aThreadClient => gThreadClient = aThreadClient) + .then(pauseDebuggee) + .then(testBreakOnClick) + .then(closeConnection) + .then(finish) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); +} + +function pauseDebuggee() { + let deferred = promise.defer(); + + gClient.addOneTimeListener("paused", (aEvent, aPacket) => { + is(aPacket.type, "paused", + "We should now be paused."); + is(aPacket.why.type, "debuggerStatement", + "The debugger statement was hit."); + + gThreadClient.resume(deferred.resolve); + }); + + // Spin the event loop before causing the debuggee to pause, to allow + // this function to return first. + executeSoon(() => triggerButtonClick("initialSetup")); + + return deferred.promise; +} + +// Test pause on a single event. +function testBreakOnClick() { + let deferred = promise.defer(); + + // Test calling pauseOnDOMEvents from a running state. + gThreadClient.pauseOnDOMEvents(["click"], (aPacket) => { + is(aPacket.error, undefined, + "The pause-on-click request completed successfully."); + let handlers = ["clicker"]; + + gClient.addListener("paused", function tester(aEvent, aPacket) { + is(aPacket.why.type, "pauseOnDOMEvents", + "A hidden breakpoint was hit."); + + switch(handlers.length) { + case 1: + is(aPacket.frame.where.line, 26, "Found the clicker handler."); + handlers.push("handleEventClick"); + break; + case 2: + is(aPacket.frame.where.line, 36, "Found the handleEventClick handler."); + handlers.push("boundHandleEventClick"); + break; + case 3: + is(aPacket.frame.where.line, 46, "Found the boundHandleEventClick handler."); + gClient.removeListener("paused", tester); + deferred.resolve(); + } + + gThreadClient.resume(() => triggerButtonClick(handlers.slice(-1))); + }); + + triggerButtonClick(handlers.slice(-1)); + }); + + return deferred.promise; +} + +function triggerButtonClick(aNodeId) { + let button = content.document.getElementById(aNodeId); + EventUtils.sendMouseEvent({ type: "click" }, button); +} + +function closeConnection() { + let deferred = promise.defer(); + gClient.close(deferred.resolve); + return deferred.promise; +} + +registerCleanupFunction(function() { + gClient = null; + gThreadClient = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_breakpoints-actual-location.js b/toolkit/devtools/debugger/test/browser_dbg_breakpoints-actual-location.js new file mode 100644 index 000000000..cc9fcb72e --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_breakpoints-actual-location.js @@ -0,0 +1,96 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Bug 737803: Setting a breakpoint in a line without code should move + * the icon to the actual location. + */ + +const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html"; + +function test() { + let gTab, gPanel, gDebugger; + let gEditor, gSources, gBreakpoints, gBreakpointsAdded, gBreakpointsRemoving; + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gSources = gDebugger.DebuggerView.Sources; + gBreakpoints = gDebugger.DebuggerController.Breakpoints; + gBreakpointsAdded = gBreakpoints._added; + gBreakpointsRemoving = gBreakpoints._removing; + + waitForSourceAndCaretAndScopes(gPanel, "-02.js", 1).then(performTest); + callInTab(gTab, "firstCall"); + }); + + function performTest() { + is(gBreakpointsAdded.size, 0, + "No breakpoints currently added."); + is(gBreakpointsRemoving.size, 0, + "No breakpoints currently being removed."); + is(gEditor.getBreakpoints().length, 0, + "No breakpoints currently shown in the editor."); + + gEditor.on("breakpointAdded", onEditorBreakpointAdd); + gPanel.addBreakpoint({ actor: gSources.selectedValue, line: 4 }).then(onBreakpointAdd); + } + + let onBpDebuggerAdd = false; + let onBpEditorAdd = false; + + function onBreakpointAdd(aBreakpointClient) { + ok(aBreakpointClient, + "Breakpoint added, client received."); + is(aBreakpointClient.location.actor, gSources.selectedValue, + "Breakpoint client url is the same."); + is(aBreakpointClient.location.line, 6, + "Breakpoint client line is new."); + + is(aBreakpointClient.requestedLocation.actor, gSources.selectedValue, + "Requested location url is correct"); + is(aBreakpointClient.requestedLocation.line, 4, + "Requested location line is correct"); + + onBpDebuggerAdd = true; + maybeFinish(); + } + + function onEditorBreakpointAdd() { + gEditor.off("breakpointAdded", onEditorBreakpointAdd); + + is(gEditor.getBreakpoints().length, 1, + "There is only one breakpoint in the editor"); + + ok(!gBreakpoints._getAdded({ actor: gSources.selectedValue, line: 4 }), + "There isn't any breakpoint added on an invalid line."); + ok(!gBreakpoints._getRemoving({ actor: gSources.selectedValue, line: 4 }), + "There isn't any breakpoint removed from an invalid line."); + + ok(gBreakpoints._getAdded({ actor: gSources.selectedValue, line: 6 }), + "There is a breakpoint added on the actual line."); + ok(!gBreakpoints._getRemoving({ actor: gSources.selectedValue, line: 6 }), + "There isn't any breakpoint removed from the actual line."); + + gBreakpoints._getAdded({ actor: gSources.selectedValue, line: 6 }).then(aBreakpointClient => { + is(aBreakpointClient.location.actor, gSources.selectedValue, + "Breakpoint client location actor is correct."); + is(aBreakpointClient.location.line, 6, + "Breakpoint client location line is correct."); + + onBpEditorAdd = true; + maybeFinish(); + }); + } + + function maybeFinish() { + info("onBpDebuggerAdd: " + onBpDebuggerAdd); + info("onBpEditorAdd: " + onBpEditorAdd); + + if (onBpDebuggerAdd && onBpEditorAdd) { + resumeDebuggerThenCloseAndFinish(gPanel); + } + } +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_breakpoints-actual-location2.js b/toolkit/devtools/debugger/test/browser_dbg_breakpoints-actual-location2.js new file mode 100644 index 000000000..e44335792 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_breakpoints-actual-location2.js @@ -0,0 +1,102 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Bug 1008372: Setting a breakpoint in a line without code should move + * the icon to the actual location, and if a breakpoint already exists + * on the new location don't duplicate + */ + +const TAB_URL = EXAMPLE_URL + "doc_breakpoint-move.html"; + +function test() { + let gTab, gPanel, gDebugger; + let gEditor, gSources, gBreakpoints, gBreakpointsAdded, gBreakpointsRemoving; + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gSources = gDebugger.DebuggerView.Sources; + gBreakpoints = gDebugger.DebuggerController.Breakpoints; + gBreakpointsAdded = gBreakpoints._added; + gBreakpointsRemoving = gBreakpoints._removing; + + waitForSourceAndCaretAndScopes(gPanel, ".html", 1).then(performTest); + callInTab(gTab, "ermahgerd"); + }); + + function performTest() { + is(gBreakpointsAdded.size, 0, + "No breakpoints currently added."); + is(gBreakpointsRemoving.size, 0, + "No breakpoints currently being removed."); + is(gEditor.getBreakpoints().length, 0, + "No breakpoints currently shown in the editor."); + + Task.spawn(function*() { + let bpClient = yield gPanel.addBreakpoint({ + actor: gSources.selectedValue, + line: 19 + }); + yield gPanel.addBreakpoint({ + actor: gSources.selectedValue, + line: 20 + }); + + let movedBpClient = yield gPanel.addBreakpoint({ + actor: gSources.selectedValue, + line: 17 + }); + testMovedLocation(movedBpClient); + + yield resumeAndTestBreakpoint(19); + + yield gPanel.removeBreakpoint({ + actor: gSources.selectedValue, + line: 19 + }); + + yield resumeAndTestBreakpoint(20); + yield doResume(gPanel); + + callInTab(gTab, "ermahgerd"); + yield waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES); + + yield resumeAndTestBreakpoint(20); + resumeDebuggerThenCloseAndFinish(gPanel); + }); + } + + function resumeAndTestBreakpoint(line) { + return Task.spawn(function*() { + let event = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES); + doResume(gPanel); + yield event; + testBreakpoint(line); + }); + }; + + function testBreakpoint(line) { + let selectedBreakpoint = gSources._selectedBreakpointItem; + ok(selectedBreakpoint, + "There should be a selected breakpoint on line " + line); + is(selectedBreakpoint.attachment.line, line, + "The breakpoint on line " + line + " was not hit"); + } + + function testMovedLocation(breakpointClient) { + ok(breakpointClient, + "Breakpoint added, client received."); + is(breakpointClient.location.actor, gSources.selectedValue, + "Breakpoint client url is the same."); + is(breakpointClient.location.line, 19, + "Breakpoint client line is new."); + + is(breakpointClient.requestedLocation.actor, gSources.selectedValue, + "Requested location url is correct"); + is(breakpointClient.requestedLocation.line, 17, + "Requested location line is correct"); + } +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_breakpoints-break-on-last-line-of-script-on-reload.js b/toolkit/devtools/debugger/test/browser_dbg_breakpoints-break-on-last-line-of-script-on-reload.js new file mode 100644 index 000000000..b069e944c --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_breakpoints-break-on-last-line-of-script-on-reload.js @@ -0,0 +1,108 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Bug 978019: Setting a breakpoint on the last line of a Debugger.Script and + * reloading should still hit the breakpoint. + */ + +const TAB_URL = EXAMPLE_URL + "doc_breakpoints-break-on-last-line-of-script-on-reload.html"; +const CODE_URL = EXAMPLE_URL + "code_breakpoints-break-on-last-line-of-script-on-reload.js"; + +function test() { + // Debug test slaves are a bit slow at this test. + requestLongerTimeout(2); + + let gPanel, gDebugger, gThreadClient, gEvents, gSources; + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gThreadClient = gDebugger.gThreadClient; + gEvents = gDebugger.EVENTS; + gSources = gDebugger.DebuggerView.Sources; + + Task.spawn(function* () { + try { + + // Refresh and hit the debugger statement before the location we want to + // set our breakpoints. We have to pause before the breakpoint locations + // so that GC doesn't get a chance to kick in and collect the IIFE's + // script, which would causes us to receive a 'noScript' error from the + // server when we try to set the breakpoints. + const [paused, ] = yield promise.all([ + waitForThreadEvents(gPanel, "paused"), + reloadActiveTab(gPanel, gEvents.SOURCE_SHOWN), + ]); + + is(paused.why.type, "debuggerStatement"); + + // Set our breakpoints. + const [bp1, bp2, bp3] = yield promise.all([ + setBreakpoint({ + url: CODE_URL, + line: 3 + }), + setBreakpoint({ + url: CODE_URL, + line: 4 + }), + setBreakpoint({ + url: CODE_URL, + line: 5 + }) + ]); + + // Refresh and hit the debugger statement again. + yield promise.all([ + reloadActiveTab(gPanel, gEvents.SOURCE_SHOWN), + waitForCaretAndScopes(gPanel, 1) + ]); + + // And we should hit the breakpoints as we resume. + yield promise.all([ + doResume(gPanel), + waitForCaretAndScopes(gPanel, 3) + ]); + yield promise.all([ + doResume(gPanel), + waitForCaretAndScopes(gPanel, 4) + ]); + yield promise.all([ + doResume(gPanel), + waitForCaretAndScopes(gPanel, 5) + ]); + + // Clean up the breakpoints. + yield promise.all([ + rdpInvoke(bp1, bp1.remove), + rdpInvoke(bp2, bp1.remove), + rdpInvoke(bp3, bp1.remove), + ]); + + yield resumeDebuggerThenCloseAndFinish(gPanel); + + } catch (e) { + DevToolsUtils.reportException( + "browser_dbg_breakpoints-break-on-last-line-of-script-on-reload.js", + e + ); + ok(false); + } + }); + }); + + function setBreakpoint(location) { + let item = gSources.getItemByValue(getSourceActor(gSources, location.url)); + let source = gThreadClient.source(item.attachment.source); + + let deferred = promise.defer(); + source.setBreakpoint(location, ({ error, message }, bpClient) => { + if (error) { + deferred.reject(error + ": " + message); + } + deferred.resolve(bpClient); + }); + return deferred.promise; + } +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_breakpoints-button-01.js b/toolkit/devtools/debugger/test/browser_dbg_breakpoints-button-01.js new file mode 100644 index 000000000..40d787a8a --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_breakpoints-button-01.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test if the breakpoints toggle button works as advertised. + */ + +const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html"; + +function test() { + let gTab, gPanel, gDebugger; + let gSources, gBreakpoints; + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gSources = gDebugger.DebuggerView.Sources; + gBreakpoints = gDebugger.DebuggerController.Breakpoints; + + waitForSourceShown(gPanel, "-01.js") + .then(addBreakpoints) + .then(testDisableBreakpoints) + .then(testEnableBreakpoints) + .then(() => ensureThreadClientState(gPanel, "resumed")) + .then(() => closeDebuggerAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); + + function addBreakpoints() { + return promise.resolve(null) + .then(() => gPanel.addBreakpoint({ actor: gSources.values[0], line: 5 })) + .then(() => gPanel.addBreakpoint({ actor: gSources.values[1], line: 6 })) + .then(() => gPanel.addBreakpoint({ actor: gSources.values[1], line: 7 })) + .then(() => ensureThreadClientState(gPanel, "resumed")); + } + + function testDisableBreakpoints() { + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_REMOVED, 3); + gSources.toggleBreakpoints(); + return finished.then(() => checkBreakpointsDisabled(true)); + } + + function testEnableBreakpoints() { + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_ADDED, 3); + gSources.toggleBreakpoints(); + return finished.then(() => checkBreakpointsDisabled(false)); + } + + function checkBreakpointsDisabled(aState, aTotal = 3) { + let breakpoints = gSources.getAllBreakpoints(); + + is(breakpoints.length, aTotal, + "Breakpoints should still be set."); + is(breakpoints.filter(e => e.attachment.disabled == aState).length, aTotal, + "Breakpoints should be " + (aState ? "disabled" : "enabled") + "."); + } +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_breakpoints-button-02.js b/toolkit/devtools/debugger/test/browser_dbg_breakpoints-button-02.js new file mode 100644 index 000000000..9ae835396 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_breakpoints-button-02.js @@ -0,0 +1,69 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test if the breakpoints toggle button works as advertised when there are + * some breakpoints already disabled. + */ + +const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html"; + +function test() { + let gTab, gPanel, gDebugger; + let gSources, gBreakpoints; + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gSources = gDebugger.DebuggerView.Sources; + gBreakpoints = gDebugger.DebuggerController.Breakpoints; + + waitForSourceShown(gPanel, "-01.js") + .then(addBreakpoints) + .then(disableSomeBreakpoints) + .then(testToggleBreakpoints) + .then(testEnableBreakpoints) + .then(() => ensureThreadClientState(gPanel, "resumed")) + .then(() => closeDebuggerAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); + + function addBreakpoints() { + return promise.resolve(null) + .then(() => gPanel.addBreakpoint({ actor: gSources.values[0], line: 5 })) + .then(() => gPanel.addBreakpoint({ actor: gSources.values[1], line: 6 })) + .then(() => gPanel.addBreakpoint({ actor: gSources.values[1], line: 7 })) + .then(() => ensureThreadClientState(gPanel, "resumed")); + } + + function disableSomeBreakpoints() { + return promise.all([ + gSources.disableBreakpoint({ actor: gSources.values[0], line: 5 }), + gSources.disableBreakpoint({ actor: gSources.values[1], line: 6 }) + ]); + } + + function testToggleBreakpoints() { + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_REMOVED, 1); + gSources.toggleBreakpoints(); + return finished.then(() => checkBreakpointsDisabled(true)); + } + + function testEnableBreakpoints() { + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_ADDED, 3); + gSources.toggleBreakpoints(); + return finished.then(() => checkBreakpointsDisabled(false)); + } + + function checkBreakpointsDisabled(aState, aTotal = 3) { + let breakpoints = gSources.getAllBreakpoints(); + + is(breakpoints.length, aTotal, + "Breakpoints should still be set."); + is(breakpoints.filter(e => e.attachment.disabled == aState).length, aTotal, + "Breakpoints should be " + (aState ? "disabled" : "enabled") + "."); + } +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_breakpoints-contextmenu-add.js b/toolkit/devtools/debugger/test/browser_dbg_breakpoints-contextmenu-add.js new file mode 100644 index 000000000..36cdac034 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_breakpoints-contextmenu-add.js @@ -0,0 +1,95 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test adding breakpoints from the source editor context menu + */ + +const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html"; + +function test() { + let gTab, gPanel, gDebugger; + let gEditor, gSources, gContextMenu, gBreakpoints, gBreakpointsAdded; + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gSources = gDebugger.DebuggerView.Sources; + gBreakpoints = gDebugger.DebuggerController.Breakpoints; + gBreakpointsAdded = gBreakpoints._added; + gContextMenu = gDebugger.document.getElementById("sourceEditorContextMenu"); + + waitForSourceAndCaretAndScopes(gPanel, "-02.js", 1) + .then(performTest) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + + callInTab(gTab, "firstCall"); + }); + + function performTest() { + is(gDebugger.gThreadClient.state, "paused", + "Should only be getting stack frames while paused."); + is(gSources.itemCount, 2, + "Found the expected number of sources."); + isnot(gEditor.getText().indexOf("debugger"), -1, + "The correct source was loaded initially."); + is(gSources.selectedValue, gSources.values[1], + "The correct source is selected."); + + ok(gContextMenu, + "The source editor's context menupopup is available."); + + gEditor.focus(); + gEditor.setSelection({ line: 1, ch: 0 }, { line: 1, ch: 10 }); + + return testAddBreakpoint().then(testAddConditionalBreakpoint); + } + + function testAddBreakpoint() { + gContextMenu.openPopup(gEditor.container, "overlap", 0, 0, true, false); + gEditor.emit("gutterClick", 6, 2); + + return once(gContextMenu, "popupshown").then(() => { + is(gBreakpointsAdded.size, 0, "no breakpoints added"); + + let cmd = gContextMenu.querySelector('menuitem[command=addBreakpointCommand]'); + let bpShown = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_SHOWN_IN_EDITOR); + EventUtils.synthesizeMouseAtCenter(cmd, {}, gDebugger); + return bpShown; + }).then(() => { + is(gBreakpointsAdded.size, 1, + "1 breakpoint correctly added"); + is(gEditor.getBreakpoints().length, 1, + "1 breakpoint currently shown in the editor."); + ok(gBreakpoints._getAdded({ actor: gSources.values[1], line: 7 }), + "Breakpoint on line 7 exists"); + }); + } + + function testAddConditionalBreakpoint() { + gContextMenu.openPopup(gEditor.container, "overlap", 0, 0, true, false); + gEditor.emit("gutterClick", 7, 2); + + return once(gContextMenu, "popupshown").then(() => { + is(gBreakpointsAdded.size, 1, + "1 breakpoint correctly added"); + + let cmd = gContextMenu.querySelector('menuitem[command=addConditionalBreakpointCommand]'); + let bpShown = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.CONDITIONAL_BREAKPOINT_POPUP_SHOWING); + EventUtils.synthesizeMouseAtCenter(cmd, {}, gDebugger); + return bpShown; + }).then(() => { + is(gBreakpointsAdded.size, 2, + "2 breakpoints correctly added"); + is(gEditor.getBreakpoints().length, 2, + "2 breakpoints currently shown in the editor."); + ok(gBreakpoints._getAdded({ actor: gSources.values[1], line: 8 }), + "Breakpoint on line 8 exists"); + }); + } +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_breakpoints-contextmenu.js b/toolkit/devtools/debugger/test/browser_dbg_breakpoints-contextmenu.js new file mode 100644 index 000000000..2b8139dd3 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_breakpoints-contextmenu.js @@ -0,0 +1,322 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test if the context menu associated with each breakpoint does what it should. + */ + +const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html"; + +function test() { + // Debug test slaves are a bit slow at this test. + requestLongerTimeout(2); + + let gTab, gPanel, gDebugger; + let gSources, gBreakpoints; + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gSources = gDebugger.DebuggerView.Sources; + gBreakpoints = gDebugger.DebuggerController.Breakpoints; + + waitForSourceShown(gPanel, "-01.js") + .then(performTestWhileNotPaused) + .then(performTestWhilePaused) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); + + function addBreakpoints() { + return promise.resolve(null) + .then(() => gPanel.addBreakpoint({ actor: gSources.values[0], line: 5 })) + .then(() => gPanel.addBreakpoint({ actor: gSources.values[1], line: 6 })) + .then(() => gPanel.addBreakpoint({ actor: gSources.values[1], line: 7 })) + .then(() => gPanel.addBreakpoint({ actor: gSources.values[1], line: 8 })) + .then(() => gPanel.addBreakpoint({ actor: gSources.values[1], line: 9 })) + .then(() => ensureThreadClientState(gPanel, "resumed")); + } + + function performTestWhileNotPaused() { + info("Performing test while not paused..."); + + return addBreakpoints() + .then(initialChecks) + .then(() => checkBreakpointToggleSelf(0)) + .then(() => checkBreakpointToggleOthers(0)) + .then(() => checkBreakpointToggleSelf(1)) + .then(() => checkBreakpointToggleOthers(1)) + .then(() => checkBreakpointToggleSelf(2)) + .then(() => checkBreakpointToggleOthers(2)) + .then(() => checkBreakpointToggleSelf(3)) + .then(() => checkBreakpointToggleOthers(3)) + .then(() => checkBreakpointToggleSelf(4)) + .then(() => checkBreakpointToggleOthers(4)) + .then(testDeleteAll); + } + + function performTestWhilePaused() { + info("Performing test while paused..."); + + return addBreakpoints() + .then(initialChecks) + .then(pauseAndCheck) + .then(() => checkBreakpointToggleSelf(0)) + .then(() => checkBreakpointToggleOthers(0)) + .then(() => checkBreakpointToggleSelf(1)) + .then(() => checkBreakpointToggleOthers(1)) + .then(() => checkBreakpointToggleSelf(2)) + .then(() => checkBreakpointToggleOthers(2)) + .then(() => checkBreakpointToggleSelf(3)) + .then(() => checkBreakpointToggleOthers(3)) + .then(() => checkBreakpointToggleSelf(4)) + .then(() => checkBreakpointToggleOthers(4)) + .then(testDeleteAll); + } + + function pauseAndCheck() { + let finished = waitForSourceAndCaretAndScopes(gPanel, "-01.js", 5).then(() => { + let source = gSources.selectedItem.attachment.source; + is(source.url, EXAMPLE_URL + "code_script-switching-01.js", + "The currently selected source is incorrect (3)."); + is(gSources.selectedIndex, 0, + "The currently selected source is incorrect (4)."); + ok(isCaretPos(gPanel, 5), + "The editor location is correct after pausing."); + }); + + let source = gSources.selectedItem.attachment.source; + is(source.url, EXAMPLE_URL + "code_script-switching-02.js", + "The currently selected source is incorrect (1)."); + is(gSources.selectedIndex, 1, + "The currently selected source is incorrect (2)."); + ok(isCaretPos(gPanel, 9), + "The editor location is correct before pausing."); + + sendMouseClickToTab(gTab, content.document.querySelector("button")); + + return finished; + } + + function initialChecks() { + for (let source of gSources) { + for (let breakpoint of source) { + ok(gBreakpoints._getAdded(breakpoint.attachment), + "All breakpoint items should have corresponding promises (1)."); + ok(!gBreakpoints._getRemoving(breakpoint.attachment), + "All breakpoint items should have corresponding promises (2)."); + ok(breakpoint.attachment.actor, + "All breakpoint items should have corresponding promises (3)."); + is(!!breakpoint.attachment.disabled, false, + "All breakpoints should initially be enabled."); + + let prefix = "bp-cMenu-"; // "breakpoints context menu" + let identifier = gBreakpoints.getIdentifier(breakpoint.attachment); + let enableSelfId = prefix + "enableSelf-" + identifier + "-menuitem"; + let disableSelfId = prefix + "disableSelf-" + identifier + "-menuitem"; + + is(gDebugger.document.getElementById(enableSelfId).getAttribute("hidden"), "true", + "The 'Enable breakpoint' context menu item should initially be hidden'."); + ok(!gDebugger.document.getElementById(disableSelfId).hasAttribute("hidden"), + "The 'Disable breakpoint' context menu item should initially not be hidden'."); + is(breakpoint.attachment.view.checkbox.getAttribute("checked"), "true", + "All breakpoints should initially have a checked checkbox."); + } + } + } + + function checkBreakpointToggleSelf(aIndex) { + let deferred = promise.defer(); + + EventUtils.sendMouseEvent({ type: "click" }, + gDebugger.document.querySelectorAll(".dbg-breakpoint")[aIndex], + gDebugger); + + let selectedBreakpoint = gSources._selectedBreakpointItem; + + ok(gBreakpoints._getAdded(selectedBreakpoint.attachment), + "There should be a breakpoint client available (1)."); + ok(!gBreakpoints._getRemoving(selectedBreakpoint.attachment), + "There should be a breakpoint client available (2)."); + ok(selectedBreakpoint.attachment.actor, + "There should be a breakpoint client available (3)."); + is(!!selectedBreakpoint.attachment.disabled, false, + "The breakpoint should not be disabled yet (" + aIndex + ")."); + + gBreakpoints._getAdded(selectedBreakpoint.attachment).then(aBreakpointClient => { + ok(aBreakpointClient, + "There should be a breakpoint client available as a promise."); + }); + + let prefix = "bp-cMenu-"; // "breakpoints context menu" + let identifier = gBreakpoints.getIdentifier(selectedBreakpoint.attachment); + let enableSelfId = prefix + "enableSelf-" + identifier + "-menuitem"; + let disableSelfId = prefix + "disableSelf-" + identifier + "-menuitem"; + + is(gDebugger.document.getElementById(enableSelfId).getAttribute("hidden"), "true", + "The 'Enable breakpoint' context menu item should be hidden'."); + ok(!gDebugger.document.getElementById(disableSelfId).hasAttribute("hidden"), + "The 'Disable breakpoint' context menu item should not be hidden'."); + + ok(isCaretPos(gPanel, selectedBreakpoint.attachment.line), + "The source editor caret position was incorrect (" + aIndex + ")."); + + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_REMOVED).then(() => { + ok(!gBreakpoints._getAdded(selectedBreakpoint.attachment), + "There should be no breakpoint client available (4)."); + + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_ADDED).then(() => { + ok(gBreakpoints._getAdded(selectedBreakpoint.attachment), + "There should be a breakpoint client available (5)."); + + deferred.resolve(); + }); + + // Test re-disabling this breakpoint. + gSources._onEnableSelf(selectedBreakpoint.attachment); + is(selectedBreakpoint.attachment.disabled, false, + "The current breakpoint should now be enabled.") + + is(gDebugger.document.getElementById(enableSelfId).getAttribute("hidden"), "true", + "The 'Enable breakpoint' context menu item should be hidden'."); + ok(!gDebugger.document.getElementById(disableSelfId).hasAttribute("hidden"), + "The 'Disable breakpoint' context menu item should not be hidden'."); + ok(selectedBreakpoint.attachment.view.checkbox.hasAttribute("checked"), + "The breakpoint should now be checked."); + }); + + // Test disabling this breakpoint. + gSources._onDisableSelf(selectedBreakpoint.attachment); + is(selectedBreakpoint.attachment.disabled, true, + "The current breakpoint should now be disabled.") + + ok(!gDebugger.document.getElementById(enableSelfId).hasAttribute("hidden"), + "The 'Enable breakpoint' context menu item should not be hidden'."); + is(gDebugger.document.getElementById(disableSelfId).getAttribute("hidden"), "true", + "The 'Disable breakpoint' context menu item should be hidden'."); + ok(!selectedBreakpoint.attachment.view.checkbox.hasAttribute("checked"), + "The breakpoint should now be unchecked."); + + return deferred.promise; + } + + function checkBreakpointToggleOthers(aIndex) { + let deferred = promise.defer(); + + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_REMOVED, 4).then(() => { + let selectedBreakpoint = gSources._selectedBreakpointItem; + + ok(gBreakpoints._getAdded(selectedBreakpoint.attachment), + "There should be a breakpoint client available (6)."); + ok(!gBreakpoints._getRemoving(selectedBreakpoint.attachment), + "There should be a breakpoint client available (7)."); + ok(selectedBreakpoint.attachment.actor, + "There should be a breakpoint client available (8)."); + is(!!selectedBreakpoint.attachment.disabled, false, + "The targetted breakpoint should not have been disabled (" + aIndex + ")."); + + for (let source of gSources) { + for (let otherBreakpoint of source) { + if (otherBreakpoint != selectedBreakpoint) { + ok(!gBreakpoints._getAdded(otherBreakpoint.attachment), + "There should be no breakpoint client for a disabled breakpoint (9)."); + is(otherBreakpoint.attachment.disabled, true, + "Non-targetted breakpoints should have been disabled (10)."); + } + } + } + + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_ADDED, 4).then(() => { + for (let source of gSources) { + for (let someBreakpoint of source) { + ok(gBreakpoints._getAdded(someBreakpoint.attachment), + "There should be a breakpoint client for all enabled breakpoints (11)."); + is(someBreakpoint.attachment.disabled, false, + "All breakpoints should now have been enabled (12)."); + } + } + + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_REMOVED, 5).then(() => { + for (let source of gSources) { + for (let someBreakpoint of source) { + ok(!gBreakpoints._getAdded(someBreakpoint.attachment), + "There should be no breakpoint client for a disabled breakpoint (13)."); + is(someBreakpoint.attachment.disabled, true, + "All breakpoints should now have been disabled (14)."); + } + } + + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_ADDED, 5).then(() => { + for (let source of gSources) { + for (let someBreakpoint of source) { + ok(gBreakpoints._getAdded(someBreakpoint.attachment), + "There should be a breakpoint client for all enabled breakpoints (15)."); + is(someBreakpoint.attachment.disabled, false, + "All breakpoints should now have been enabled (16)."); + } + } + + // Done. + deferred.resolve(); + }); + + // Test re-enabling all breakpoints. + enableAll(); + }); + + // Test disabling all breakpoints. + disableAll(); + }); + + // Test re-enabling other breakpoints. + enableOthers(); + }); + + // Test disabling other breakpoints. + disableOthers(); + + return deferred.promise; + } + + function testDeleteAll() { + let deferred = promise.defer(); + + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_REMOVED, 5).then(() => { + ok(!gSources._selectedBreakpointItem, + "There should be no breakpoint available after removing all breakpoints."); + + for (let source of gSources) { + for (let otherBreakpoint of source) { + ok(false, "It's a trap!"); + } + } + + // Done. + deferred.resolve() + }); + + // Test deleting all breakpoints. + deleteAll(); + + return deferred.promise; + } + + function disableOthers() { + gSources._onDisableOthers(gSources._selectedBreakpointItem.attachment); + } + function enableOthers() { + gSources._onEnableOthers(gSources._selectedBreakpointItem.attachment); + } + function disableAll() { + gSources._onDisableAll(); + } + function enableAll() { + gSources._onEnableAll(); + } + function deleteAll() { + gSources._onDeleteAll(); + } +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_breakpoints-disabled-reload.js b/toolkit/devtools/debugger/test/browser_dbg_breakpoints-disabled-reload.js new file mode 100644 index 000000000..f9e491475 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_breakpoints-disabled-reload.js @@ -0,0 +1,118 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that disabled breakpoints survive target navigation. + */ + +const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html"; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + let gTab = aTab; + let gDebugger = aPanel.panelWin; + let gEvents = gDebugger.EVENTS; + let gEditor = gDebugger.DebuggerView.editor; + let gSources = gDebugger.DebuggerView.Sources; + let gBreakpoints = gDebugger.DebuggerController.Breakpoints; + let gBreakpointLocation; + Task.spawn(function() { + yield waitForSourceShown(aPanel, "-01.js"); + gBreakpointLocation = { actor: getSourceActor(gSources, EXAMPLE_URL + "code_script-switching-01.js"), + line: 5 }; + + yield aPanel.addBreakpoint(gBreakpointLocation); + + yield ensureThreadClientState(aPanel, "resumed"); + yield testWhenBreakpointEnabledAndFirstSourceShown(); + + yield reloadActiveTab(aPanel, gEvents.SOURCE_SHOWN); + yield testWhenBreakpointEnabledAndSecondSourceShown(); + + yield gSources.disableBreakpoint(gBreakpointLocation); + yield reloadActiveTab(aPanel, gEvents.SOURCE_SHOWN); + yield testWhenBreakpointDisabledAndSecondSourceShown(); + + yield gSources.enableBreakpoint(gBreakpointLocation); + yield reloadActiveTab(aPanel, gEvents.SOURCE_SHOWN); + yield testWhenBreakpointEnabledAndSecondSourceShown(); + + yield resumeDebuggerThenCloseAndFinish(aPanel); + }); + + function verifyView({ disabled, visible }) { + return Task.spawn(function() { + // It takes a tick for the checkbox in the SideMenuWidget and the + // gutter in the editor to get updated. + yield waitForTick(); + + let breakpointItem = gSources.getBreakpoint(gBreakpointLocation); + let visibleBreakpoints = gEditor.getBreakpoints(); + is(!!breakpointItem.attachment.disabled, disabled, + "The selected brekapoint state was correct."); + is(breakpointItem.attachment.view.checkbox.hasAttribute("checked"), !disabled, + "The selected brekapoint's checkbox state was correct."); + + // Source editor starts counting line and column numbers from 0. + let breakpointLine = breakpointItem.attachment.line - 1; + let matchedBreakpoints = visibleBreakpoints.filter(e => e.line == breakpointLine); + is(!!matchedBreakpoints.length, visible, + "The selected breakpoint's visibility in the editor was correct."); + }); + } + + // All the following executeSoon()'s are required to spin the event loop + // before causing the debuggee to pause, to allow functions to yield first. + + function testWhenBreakpointEnabledAndFirstSourceShown() { + return Task.spawn(function() { + yield ensureSourceIs(aPanel, "-01.js"); + yield verifyView({ disabled: false, visible: true }); + + callInTab(gTab, "firstCall"); + yield waitForDebuggerEvents(aPanel, gEvents.FETCHED_SCOPES); + yield ensureSourceIs(aPanel, "-01.js"); + yield ensureCaretAt(aPanel, 5); + yield verifyView({ disabled: false, visible: true }); + + executeSoon(() => gDebugger.gThreadClient.resume()); + yield waitForSourceAndCaretAndScopes(aPanel, "-02.js", 1); + yield verifyView({ disabled: false, visible: false }); + }); + } + + function testWhenBreakpointEnabledAndSecondSourceShown() { + return Task.spawn(function() { + yield ensureSourceIs(aPanel, "-02.js", true); + yield verifyView({ disabled: false, visible: false }); + + callInTab(gTab, "firstCall"); + yield waitForSourceAndCaretAndScopes(aPanel, "-01.js", 1); + yield verifyView({ disabled: false, visible: true }); + + executeSoon(() => gDebugger.gThreadClient.resume()); + yield waitForSourceAndCaretAndScopes(aPanel, "-02.js", 1); + yield verifyView({ disabled: false, visible: false }); + }); + } + + function testWhenBreakpointDisabledAndSecondSourceShown() { + return Task.spawn(function() { + yield ensureSourceIs(aPanel, "-02.js", true); + yield verifyView({ disabled: true, visible: false }); + + callInTab(gTab, "firstCall"); + yield waitForDebuggerEvents(aPanel, gEvents.FETCHED_SCOPES); + yield ensureSourceIs(aPanel, "-02.js"); + yield ensureCaretAt(aPanel, 6); + yield verifyView({ disabled: true, visible: false }); + + executeSoon(() => gDebugger.gThreadClient.resume()); + yield waitForDebuggerEvents(aPanel, gEvents.AFTER_FRAMES_CLEARED); + yield ensureSourceIs(aPanel, "-02.js"); + yield ensureCaretAt(aPanel, 6); + yield verifyView({ disabled: true, visible: false }); + }); + } + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_breakpoints-editor.js b/toolkit/devtools/debugger/test/browser_dbg_breakpoints-editor.js new file mode 100644 index 000000000..c7174c8f8 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_breakpoints-editor.js @@ -0,0 +1,301 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Bug 723069: Test the debugger breakpoint API and connection to the + * source editor. + */ + +const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html"; + +function test() { + let gTab, gPanel, gDebugger; + let gEditor, gSources, gBreakpoints, gBreakpointsAdded, gBreakpointsRemoving; + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gSources = gDebugger.DebuggerView.Sources; + gBreakpoints = gDebugger.DebuggerController.Breakpoints; + gBreakpointsAdded = gBreakpoints._added; + gBreakpointsRemoving = gBreakpoints._removing; + + waitForSourceAndCaretAndScopes(gPanel, "-02.js", 1).then(performTest); + callInTab(gTab, "firstCall"); + }); + + function performTest() { + is(gDebugger.gThreadClient.state, "paused", + "Should only be getting stack frames while paused."); + is(gSources.itemCount, 2, + "Found the expected number of sources."); + is(gEditor.getText().indexOf("debugger"), 166, + "The correct source was loaded initially."); + is(gSources.selectedValue, gSources.values[1], + "The correct source is selected."); + + is(gBreakpointsAdded.size, 0, + "No breakpoints currently added."); + is(gBreakpointsRemoving.size, 0, + "No breakpoints currently being removed."); + is(gEditor.getBreakpoints().length, 0, + "No breakpoints currently shown in the editor."); + + ok(!gBreakpoints._getAdded({ url: "foo", line: 3 }), + "_getAdded('foo', 3) returns falsey."); + ok(!gBreakpoints._getRemoving({ url: "bar", line: 3 }), + "_getRemoving('bar', 3) returns falsey."); + + is(gSources.values[1], gSources.selectedValue, + "The second source should be currently selected."); + + info("Add the first breakpoint."); + let location = { actor: gSources.selectedValue, line: 6 }; + gEditor.once("breakpointAdded", onEditorBreakpointAddFirst); + gPanel.addBreakpoint(location).then(onBreakpointAddFirst); + } + + let breakpointsAdded = 0; + let breakpointsRemoved = 0; + let editorBreakpointChanges = 0; + + function onEditorBreakpointAddFirst(aEvent, aLine) { + editorBreakpointChanges++; + + ok(aEvent, + "breakpoint1 added to the editor."); + is(aLine, 5, + "Editor breakpoint line is correct."); + + is(gEditor.getBreakpoints().length, 1, + "editor.getBreakpoints().length is correct."); + } + + function onBreakpointAddFirst(aBreakpointClient) { + breakpointsAdded++; + + ok(aBreakpointClient, + "breakpoint1 added, client received."); + is(aBreakpointClient.location.actor, gSources.selectedValue, + "breakpoint1 client url is correct."); + is(aBreakpointClient.location.line, 6, + "breakpoint1 client line is correct."); + + ok(gBreakpoints._getAdded(aBreakpointClient.location), + "breakpoint1 client found in the list of added breakpoints."); + ok(!gBreakpoints._getRemoving(aBreakpointClient.location), + "breakpoint1 client found in the list of removing breakpoints."); + + is(gBreakpointsAdded.size, 1, + "The list of added breakpoints holds only one breakpoint."); + is(gBreakpointsRemoving.size, 0, + "The list of removing breakpoints holds no breakpoint."); + + gBreakpoints._getAdded(aBreakpointClient.location).then(aClient => { + is(aClient, aBreakpointClient, + "_getAdded() returns the correct breakpoint."); + }); + + is(gSources.values[1], gSources.selectedValue, + "The second source should be currently selected."); + + info("Remove the first breakpoint."); + gEditor.once("breakpointRemoved", onEditorBreakpointRemoveFirst); + gPanel.removeBreakpoint(aBreakpointClient.location).then(onBreakpointRemoveFirst); + } + + function onEditorBreakpointRemoveFirst(aEvent, aLine) { + editorBreakpointChanges++; + + ok(aEvent, + "breakpoint1 removed from the editor."); + is(aLine, 5, + "Editor breakpoint line is correct."); + + is(gEditor.getBreakpoints().length, 0, + "editor.getBreakpoints().length is correct."); + } + + function onBreakpointRemoveFirst(aLocation) { + breakpointsRemoved++; + + ok(aLocation, + "breakpoint1 removed"); + is(aLocation.actor, gSources.selectedValue, + "breakpoint1 removal url is correct."); + is(aLocation.line, 6, + "breakpoint1 removal line is correct."); + + testBreakpointAddBackground(); + } + + function testBreakpointAddBackground() { + is(gBreakpointsAdded.size, 0, + "No breakpoints currently added."); + is(gBreakpointsRemoving.size, 0, + "No breakpoints currently being removed."); + is(gEditor.getBreakpoints().length, 0, + "No breakpoints currently shown in the editor."); + + ok(!gBreakpoints._getAdded({ actor: gSources.selectedValue, line: 6 }), + "_getAdded('gSources.selectedValue', 6) returns falsey."); + ok(!gBreakpoints._getRemoving({ actor: gSources.selectedValue, line: 6 }), + "_getRemoving('gSources.selectedValue', 6) returns falsey."); + + is(gSources.values[1], gSources.selectedValue, + "The second source should be currently selected."); + + info("Add a breakpoint to the first source, which is not selected."); + let location = { actor: gSources.values[0], line: 5 }; + let options = { noEditorUpdate: true }; + gEditor.on("breakpointAdded", onEditorBreakpointAddBackgroundTrap); + gPanel.addBreakpoint(location, options).then(onBreakpointAddBackground); + } + + function onEditorBreakpointAddBackgroundTrap() { + // Trap listener: no breakpoint must be added to the editor when a + // breakpoint is added to a source that is not currently selected. + editorBreakpointChanges++; + ok(false, "breakpoint2 must not be added to the editor."); + } + + function onBreakpointAddBackground(aBreakpointClient, aResponseError) { + breakpointsAdded++; + + ok(aBreakpointClient, + "breakpoint2 added, client received"); + is(aBreakpointClient.location.actor, gSources.values[0], + "breakpoint2 client url is correct."); + is(aBreakpointClient.location.line, 5, + "breakpoint2 client line is correct."); + + ok(gBreakpoints._getAdded(aBreakpointClient.location), + "breakpoint2 client found in the list of added breakpoints."); + ok(!gBreakpoints._getRemoving(aBreakpointClient.location), + "breakpoint2 client found in the list of removing breakpoints."); + + is(gBreakpointsAdded.size, 1, + "The list of added breakpoints holds only one breakpoint."); + is(gBreakpointsRemoving.size, 0, + "The list of removing breakpoints holds no breakpoint."); + + gBreakpoints._getAdded(aBreakpointClient.location).then(aClient => { + is(aClient, aBreakpointClient, + "_getAdded() returns the correct breakpoint."); + }); + + is(gSources.values[1], gSources.selectedValue, + "The second source should be currently selected."); + + // Remove the trap listener. + gEditor.off("breakpointAdded", onEditorBreakpointAddBackgroundTrap); + + info("Switch to the first source, which is not yet selected"); + gEditor.once("breakpointAdded", onEditorBreakpointAddSwitch); + gEditor.once("change", onEditorTextChanged); + gSources.selectedIndex = 0; + } + + function onEditorBreakpointAddSwitch(aEvent, aLine) { + editorBreakpointChanges++; + + ok(aEvent, + "breakpoint2 added to the editor."); + is(aLine, 4, + "Editor breakpoint line is correct."); + + is(gEditor.getBreakpoints().length, 1, + "editor.getBreakpoints().length is correct"); + } + + function onEditorTextChanged() { + // Wait for the actual text to be shown. + if (gEditor.getText() == gDebugger.L10N.getStr("loadingText")) + return void gEditor.once("change", onEditorTextChanged); + + is(gEditor.getText().indexOf("debugger"), -1, + "The second source is no longer displayed."); + is(gEditor.getText().indexOf("firstCall"), 118, + "The first source is displayed."); + + is(gSources.values[0], gSources.selectedValue, + "The first source should be currently selected."); + + let window = gEditor.container.contentWindow; + executeSoon(() => window.mozRequestAnimationFrame(onReadyForClick)); + } + + function onReadyForClick() { + info("Remove the second breakpoint using the mouse."); + gEditor.once("breakpointRemoved", onEditorBreakpointRemoveSecond); + + let iframe = gEditor.container; + let testWin = iframe.ownerDocument.defaultView; + + // Flush the layout for the iframe. + info("rect " + iframe.contentDocument.documentElement.getBoundingClientRect()); + + let utils = testWin + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + + let coords = gEditor.getCoordsFromPosition({ line: 4, ch: 0 }); + let rect = iframe.getBoundingClientRect(); + let left = rect.left + 10; + let top = rect.top + coords.top + 4; + utils.sendMouseEventToWindow("mousedown", left, top, 0, 1, 0, false, 0, 0); + utils.sendMouseEventToWindow("mouseup", left, top, 0, 1, 0, false, 0, 0); + } + + function onEditorBreakpointRemoveSecond(aEvent, aLine) { + editorBreakpointChanges++; + + ok(aEvent, + "breakpoint2 removed from the editor."); + is(aLine, 4, + "Editor breakpoint line is correct."); + + is(gEditor.getBreakpoints().length, 0, + "editor.getBreakpoints().length is correct."); + + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.AFTER_FRAMES_CLEARED).then(() => { + finalCheck(); + closeDebuggerAndFinish(gPanel); + }); + + gDebugger.gThreadClient.resume(); + } + + function finalCheck() { + is(gBreakpointsAdded.size, 0, + "No breakpoints currently added."); + is(gBreakpointsRemoving.size, 0, + "No breakpoints currently being removed."); + is(gEditor.getBreakpoints().length, 0, + "No breakpoints currently shown in the editor."); + + ok(!gBreakpoints._getAdded({ actor: gSources.values[0], line: 5 }), + "_getAdded('gSources.values[0]', 5) returns falsey."); + ok(!gBreakpoints._getRemoving({ actor: gSources.values[0], line: 5 }), + "_getRemoving('gSources.values[0]', 5) returns falsey."); + + ok(!gBreakpoints._getAdded({ actor: gSources.values[1], line: 6 }), + "_getAdded('gSources.values[1]', 6) returns falsey."); + ok(!gBreakpoints._getRemoving({ actor: gSources.values[1], line: 6 }), + "_getRemoving('gSources.values[1]', 6) returns falsey."); + + ok(!gBreakpoints._getAdded({ actor: "foo", line: 3 }), + "_getAdded('foo', 3) returns falsey."); + ok(!gBreakpoints._getRemoving({ actor: "bar", line: 3 }), + "_getRemoving('bar', 3) returns falsey."); + + is(breakpointsAdded, 2, + "Correct number of breakpoints have been added."); + is(breakpointsRemoved, 1, + "Correct number of breakpoints have been removed."); + is(editorBreakpointChanges, 4, + "Correct number of editor breakpoint changes."); + } +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_breakpoints-eval.js b/toolkit/devtools/debugger/test/browser_dbg_breakpoints-eval.js new file mode 100644 index 000000000..2fb63abb9 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_breakpoints-eval.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test setting breakpoints on an eval script + */ + +const TAB_URL = EXAMPLE_URL + "doc_script-eval.html"; + +function test() { + let gTab, gPanel, gDebugger; + let gSources, gBreakpoints; + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gSources = gDebugger.DebuggerView.Sources; + gBreakpoints = gDebugger.DebuggerController.Breakpoints; + + waitForSourceShown(gPanel, "-eval.js") + .then(run) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); + + function run() { + return Task.spawn(function*() { + let newSource = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.NEW_SOURCE); + callInTab(gTab, "evalSourceWithSourceURL"); + yield newSource; + + yield gPanel.addBreakpoint({ actor: gSources.values[0], line: 2 }); + yield ensureThreadClientState(gPanel, "resumed"); + + const paused = waitForThreadEvents(gPanel, "paused"); + callInTab(gTab, "bar"); + let frame = (yield paused).frame; + is(frame.where.source.actor, gSources.values[0], "Should have broken on the eval'ed source"); + is(frame.where.line, 2, "Should break on line 2"); + + yield resumeDebuggerThenCloseAndFinish(gPanel); + }); + } +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_breakpoints-highlight.js b/toolkit/devtools/debugger/test/browser_dbg_breakpoints-highlight.js new file mode 100644 index 000000000..468cb0d90 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_breakpoints-highlight.js @@ -0,0 +1,103 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test if breakpoints are highlighted when they should. + */ + +const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html"; + +function test() { + let gTab, gPanel, gDebugger; + let gEditor, gSources; + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gSources = gDebugger.DebuggerView.Sources; + + waitForSourceShown(gPanel, "-01.js") + .then(addBreakpoints) + .then(() => clickBreakpointAndCheck(0, 0, 5)) + .then(() => clickBreakpointAndCheck(1, 1, 6)) + .then(() => clickBreakpointAndCheck(2, 1, 7)) + .then(() => clickBreakpointAndCheck(3, 1, 8)) + .then(() => clickBreakpointAndCheck(4, 1, 9)) + .then(() => ensureThreadClientState(gPanel, "resumed")) + .then(() => closeDebuggerAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); + + function addBreakpoints() { + return promise.resolve(null) + .then(() => initialChecks(0, 1)) + .then(() => gPanel.addBreakpoint({ actor: gSources.values[0], line: 5 })) + .then(() => initialChecks(0, 5)) + .then(() => gPanel.addBreakpoint({ actor: gSources.values[1], line: 6 })) + .then(() => waitForSourceShown(gPanel, "-02.js")) + .then(() => waitForCaretUpdated(gPanel, 6)) + .then(() => initialChecks(1, 6)) + .then(() => gPanel.addBreakpoint({ actor: gSources.values[1], line: 7 })) + .then(() => initialChecks(1, 7)) + .then(() => gPanel.addBreakpoint({ actor: gSources.values[1], line: 8 })) + .then(() => initialChecks(1, 8)) + .then(() => gPanel.addBreakpoint({ actor: gSources.values[1], line: 9 })) + .then(() => initialChecks(1, 9)); + } + + function initialChecks(aSourceIndex, aCaretLine) { + checkEditorContents(aSourceIndex); + + is(gSources.selectedLabel, gSources.items[aSourceIndex].label, + "The currently selected source label is incorrect (0)."); + is(gSources.selectedValue, gSources.items[aSourceIndex].value, + "The currently selected source value is incorrect (0)."); + ok(isCaretPos(gPanel, aCaretLine), + "The editor caret line and column were incorrect (0)."); + } + + function clickBreakpointAndCheck(aBreakpointIndex, aSourceIndex, aCaretLine) { + let finished = waitForCaretUpdated(gPanel, aCaretLine).then(() => { + checkHighlight(gSources.values[aSourceIndex], aCaretLine); + checkEditorContents(aSourceIndex); + + is(gSources.selectedLabel, gSources.items[aSourceIndex].label, + "The currently selected source label is incorrect (1)."); + is(gSources.selectedValue, gSources.items[aSourceIndex].value, + "The currently selected source value is incorrect (1)."); + ok(isCaretPos(gPanel, aCaretLine), + "The editor caret line and column were incorrect (1)."); + }); + + EventUtils.sendMouseEvent({ type: "click" }, + gDebugger.document.querySelectorAll(".dbg-breakpoint")[aBreakpointIndex], + gDebugger); + + return finished; + } + + function checkHighlight(aActor, aLine) { + is(gSources._selectedBreakpointItem, gSources.getBreakpoint({ actor: aActor, line: aLine }), + "The currently selected breakpoint item is incorrect."); + is(gSources._selectedBreakpointItem.attachment.actor, aActor, + "The selected breakpoint item's source location attachment is incorrect."); + is(gSources._selectedBreakpointItem.attachment.line, aLine, + "The selected breakpoint item's source line number is incorrect."); + ok(gSources._selectedBreakpointItem.target.classList.contains("selected"), + "The selected breakpoint item's target should have a selected class."); + } + + function checkEditorContents(aSourceIndex) { + if (aSourceIndex == 0) { + is(gEditor.getText().indexOf("firstCall"), 118, + "The first source is correctly displayed."); + } else { + is(gEditor.getText().indexOf("debugger"), 166, + "The second source is correctly displayed."); + } + } +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_breakpoints-new-script.js b/toolkit/devtools/debugger/test/browser_dbg_breakpoints-new-script.js new file mode 100644 index 000000000..b92472c65 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_breakpoints-new-script.js @@ -0,0 +1,88 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Bug 771452: Make sure that setting a breakpoint in an inline source doesn't + * add it twice. + */ + +const TAB_URL = EXAMPLE_URL + "doc_inline-script.html"; + +let gTab, gPanel, gDebugger, gSources; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gSources = gDebugger.DebuggerView.Sources; + + addBreakpoint(); + }); +} + +function addBreakpoint() { + waitForSourceAndCaretAndScopes(gPanel, ".html", 16).then(() => { + is(gDebugger.gThreadClient.state, "paused", + "The debugger statement was reached."); + ok(isCaretPos(gPanel, 16), + "The source editor caret position is incorrect (1)."); + + gPanel.addBreakpoint({ actor: getSourceActor(gSources, TAB_URL), line: 20 }).then(() => { + testResume(); + }); + }); + + callInTab(gTab, "runDebuggerStatement"); +} + +function testResume() { + is(gDebugger.gThreadClient.state, "paused", + "The breakpoint wasn't hit yet."); + + gDebugger.gThreadClient.resume(() => { + gDebugger.gThreadClient.addOneTimeListener("paused", (aEvent, aPacket) => { + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES).then(() => { + is(aPacket.why.type, "breakpoint", + "Execution has advanced to the next breakpoint."); + isnot(aPacket.why.type, "debuggerStatement", + "The breakpoint was hit before the debugger statement."); + ok(isCaretPos(gPanel, 20), + "The source editor caret position is incorrect (2)."); + + testBreakpointHit(); + }); + }); + + sendMouseClickToTab(gTab, content.document.querySelector("button")); + }); +} + +function testBreakpointHit() { + is(gDebugger.gThreadClient.state, "paused", + "The breakpoint was hit."); + + gDebugger.gThreadClient.addOneTimeListener("paused", (aEvent, aPacket) => { + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES).then(() => { + is(aPacket.why.type, "debuggerStatement", + "Execution has advanced to the next line."); + isnot(aPacket.why.type, "breakpoint", + "No ghost breakpoint was hit."); + ok(isCaretPos(gPanel, 20), + "The source editor caret position is incorrect (3)."); + + resumeDebuggerThenCloseAndFinish(gPanel); + }); + }); + + EventUtils.sendMouseEvent({ type: "mousedown" }, + gDebugger.document.getElementById("resume"), + gDebugger); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gSources = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_breakpoints-other-tabs.js b/toolkit/devtools/debugger/test/browser_dbg_breakpoints-other-tabs.js new file mode 100644 index 000000000..7aed9c502 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_breakpoints-other-tabs.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that setting a breakpoint in one tab, doesn't cause another tab at + * the same source to pause at that location. + */ + +const TAB_URL = EXAMPLE_URL + "doc_breakpoints-other-tabs.html"; + +let test = Task.async(function* () { + const [tab1,, panel1] = yield initDebugger(TAB_URL); + const [tab2,, panel2] = yield initDebugger(TAB_URL); + + yield ensureSourceIs(panel1, "code_breakpoints-other-tabs.js", true); + + const sources = panel1.panelWin.DebuggerView.Sources; + + yield panel1.addBreakpoint({ + actor: sources.selectedValue, + line: 2 + }); + + const paused = waitForThreadEvents(panel2, "paused"); + callInTab(tab2, "testCase"); + const packet = yield paused; + + is(packet.why.type, "debuggerStatement", + "Should have stopped at the debugger statement, not the other tab's breakpoint"); + is(packet.frame.where.line, 3, + "Should have stopped at line 3 (debugger statement), not line 2 (other tab's breakpoint)"); + + yield teardown(panel1); + yield resumeDebuggerThenCloseAndFinish(panel2); +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_breakpoints-pane.js b/toolkit/devtools/debugger/test/browser_dbg_breakpoints-pane.js new file mode 100644 index 000000000..452f3cd2a --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_breakpoints-pane.js @@ -0,0 +1,254 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Bug 723071: Test adding a pane to display the list of breakpoints across + * all sources in the debuggee. + */ + +const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html"; + +function test() { + let gTab, gPanel, gDebugger; + let gEditor, gSources, gBreakpoints, gBreakpointsAdded, gBreakpointsRemoving; + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gSources = gDebugger.DebuggerView.Sources; + gBreakpoints = gDebugger.DebuggerController.Breakpoints; + gBreakpointsAdded = gBreakpoints._added; + gBreakpointsRemoving = gBreakpoints._removing; + + waitForSourceAndCaretAndScopes(gPanel, "-02.js", 1).then(performTest); + callInTab(gTab, "firstCall"); + }); + + let breakpointsAdded = 0; + let breakpointsDisabled = 0; + let breakpointsRemoved = 0; + + function performTest() { + is(gDebugger.gThreadClient.state, "paused", + "Should only be getting stack frames while paused."); + is(gSources.itemCount, 2, + "Found the expected number of sources."); + is(gEditor.getText().indexOf("debugger"), 166, + "The correct source was loaded initially."); + is(gSources.selectedValue, gSources.values[1], + "The correct source is selected."); + + is(gBreakpointsAdded.size, 0, + "No breakpoints currently added."); + is(gBreakpointsRemoving.size, 0, + "No breakpoints currently being removed."); + is(gEditor.getBreakpoints().length, 0, + "No breakpoints currently shown in the editor."); + + ok(!gBreakpoints._getAdded({ url: "foo", line: 3 }), + "_getAdded('foo', 3) returns falsey."); + ok(!gBreakpoints._getRemoving({ url: "bar", line: 3 }), + "_getRemoving('bar', 3) returns falsey."); + + let breakpointsParent = gSources.widget._parent; + let breakpointsList = gSources.widget._list; + + is(breakpointsParent.childNodes.length, 1, // one sources list + "Found junk in the breakpoints container."); + is(breakpointsList.childNodes.length, 1, // one sources group + "Found junk in the breakpoints container."); + is(breakpointsList.querySelectorAll(".dbg-breakpoint").length, 0, + "No breakpoints should be visible at this point."); + + addBreakpoints(true).then(() => { + is(breakpointsAdded, 3, + "Should have added 3 breakpoints so far."); + is(breakpointsDisabled, 0, + "Shouldn't have disabled anything so far."); + is(breakpointsRemoved, 0, + "Shouldn't have removed anything so far."); + + is(breakpointsParent.childNodes.length, 1, // one sources list + "Found junk in the breakpoints container."); + is(breakpointsList.childNodes.length, 1, // one sources group + "Found junk in the breakpoints container."); + is(breakpointsList.querySelectorAll(".dbg-breakpoint").length, 3, + "3 breakpoints should be visible at this point."); + + disableBreakpoints().then(() => { + is(breakpointsAdded, 3, + "Should still have 3 breakpoints added so far."); + is(breakpointsDisabled, 3, + "Should have 3 disabled breakpoints."); + is(breakpointsRemoved, 0, + "Shouldn't have removed anything so far."); + + is(breakpointsParent.childNodes.length, 1, // one sources list + "Found junk in the breakpoints container."); + is(breakpointsList.childNodes.length, 1, // one sources group + "Found junk in the breakpoints container."); + is(breakpointsList.querySelectorAll(".dbg-breakpoint").length, breakpointsAdded, + "Should have the same number of breakpoints in the pane."); + is(breakpointsList.querySelectorAll(".dbg-breakpoint").length, breakpointsDisabled, + "Should have the same number of disabled breakpoints."); + + addBreakpoints().then(() => { + is(breakpointsAdded, 3, + "Should still have only 3 breakpoints added so far."); + is(breakpointsDisabled, 3, + "Should still have 3 disabled breakpoints."); + is(breakpointsRemoved, 0, + "Shouldn't have removed anything so far."); + + is(breakpointsParent.childNodes.length, 1, // one sources list + "Found junk in the breakpoints container."); + is(breakpointsList.childNodes.length, 1, // one sources group + "Found junk in the breakpoints container."); + is(breakpointsList.querySelectorAll(".dbg-breakpoint").length, breakpointsAdded, + "Since half of the breakpoints already existed, but disabled, " + + "only half of the added breakpoints are actually in the pane."); + + removeBreakpoints().then(() => { + is(breakpointsRemoved, 3, + "Should have 3 removed breakpoints."); + + is(breakpointsParent.childNodes.length, 1, // one sources list + "Found junk in the breakpoints container."); + is(breakpointsList.childNodes.length, 1, // one sources group + "Found junk in the breakpoints container."); + is(breakpointsList.querySelectorAll(".dbg-breakpoint").length, 0, + "No breakpoints should be visible at this point."); + + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.AFTER_FRAMES_CLEARED).then(() => { + finalCheck(); + closeDebuggerAndFinish(gPanel); + }); + + gDebugger.gThreadClient.resume(); + }); + }); + }); + }); + + function addBreakpoints(aIncrementFlag) { + let deferred = promise.defer(); + + gPanel.addBreakpoint({ actor: gSources.selectedValue, line: 6 }).then(aClient => { + onBreakpointAdd(aClient, { + increment: aIncrementFlag, + line: 6, + text: "debugger;" + }); + + gPanel.addBreakpoint({ actor: gSources.selectedValue, line: 7 }).then(aClient => { + onBreakpointAdd(aClient, { + increment: aIncrementFlag, + line: 7, + text: "function foo() {}" + }); + + gPanel.addBreakpoint({ actor: gSources.selectedValue, line: 9 }).then(aClient => { + onBreakpointAdd(aClient, { + increment: aIncrementFlag, + line: 9, + text: "foo();" + }); + + deferred.resolve(); + }); + }); + }); + + return deferred.promise; + } + + function disableBreakpoints() { + let deferred = promise.defer(); + + let nodes = breakpointsList.querySelectorAll(".dbg-breakpoint"); + info("Nodes to disable: " + breakpointsAdded.length); + + is(nodes.length, breakpointsAdded, + "The number of nodes to disable is incorrect."); + + for (let node of nodes) { + info("Disabling breakpoint: " + node.id); + + let sourceItem = gSources.getItemForElement(node); + let breakpointItem = gSources.getItemForElement.call(sourceItem, node); + info("Found data: " + breakpointItem.attachment.toSource()); + + gSources.disableBreakpoint(breakpointItem.attachment).then(() => { + if (++breakpointsDisabled == breakpointsAdded) { + deferred.resolve(); + } + }); + } + + return deferred.promise; + } + + function removeBreakpoints() { + let deferred = promise.defer(); + + let nodes = breakpointsList.querySelectorAll(".dbg-breakpoint"); + info("Nodes to remove: " + breakpointsAdded.length); + + is(nodes.length, breakpointsAdded, + "The number of nodes to remove is incorrect."); + + for (let node of nodes) { + info("Removing breakpoint: " + node.id); + + let sourceItem = gSources.getItemForElement(node); + let breakpointItem = gSources.getItemForElement.call(sourceItem, node); + info("Found data: " + breakpointItem.attachment.toSource()); + + gPanel.removeBreakpoint(breakpointItem.attachment).then(() => { + if (++breakpointsRemoved == breakpointsAdded) { + deferred.resolve(); + } + }); + } + + return deferred.promise; + } + + function onBreakpointAdd(aBreakpointClient, aTestData) { + if (aTestData.increment) { + breakpointsAdded++; + } + + is(breakpointsList.querySelectorAll(".dbg-breakpoint").length, breakpointsAdded, + aTestData.increment + ? "Should have added a breakpoint in the pane." + : "Should have the same number of breakpoints in the pane."); + + let identifier = gBreakpoints.getIdentifier(aBreakpointClient.location); + let node = gDebugger.document.getElementById("breakpoint-" + identifier); + let line = node.getElementsByClassName("dbg-breakpoint-line")[0]; + let text = node.getElementsByClassName("dbg-breakpoint-text")[0]; + let check = node.querySelector("checkbox"); + + ok(node, + "Breakpoint element found successfully."); + is(line.getAttribute("value"), aTestData.line, + "The expected information wasn't found in the breakpoint element."); + is(text.getAttribute("value"), aTestData.text, + "The expected line text wasn't found in the breakpoint element."); + is(check.getAttribute("checked"), "true", + "The breakpoint enable checkbox is checked as expected."); + } + } + + function finalCheck() { + is(gBreakpointsAdded.size, 0, + "No breakpoints currently added."); + is(gBreakpointsRemoving.size, 0, + "No breakpoints currently being removed."); + is(gEditor.getBreakpoints().length, 0, + "No breakpoints currently shown in the editor."); + } +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_breakpoints-reload.js b/toolkit/devtools/debugger/test/browser_dbg_breakpoints-reload.js new file mode 100644 index 000000000..312ea389e --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_breakpoints-reload.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that setting a breakpoint on code that gets run on load, will get + * hit when we reload. + */ + +const TAB_URL = EXAMPLE_URL + "doc_breakpoints-reload.html"; + +let test = Task.async(function* () { + requestLongerTimeout(4); + + const [tab,, panel] = yield initDebugger(TAB_URL); + + yield ensureSourceIs(panel, "doc_breakpoints-reload.html", true); + + const sources = panel.panelWin.DebuggerView.Sources; + yield panel.addBreakpoint({ + actor: sources.selectedValue, + line: 10 // "break on me" string + }); + + const paused = waitForThreadEvents(panel, "paused"); + reloadActiveTab(panel); + const packet = yield paused; + + is(packet.why.type, "breakpoint", + "Should have hit the breakpoint after the reload"); + is(packet.frame.where.line, 10, + "Should have stopped at line 10, where we set the breakpoint"); + + yield waitForDebuggerEvents(panel, panel.panelWin.EVENTS.SOURCE_SHOWN) + yield resumeDebuggerThenCloseAndFinish(panel); +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_chrome-create.js b/toolkit/devtools/debugger/test/browser_dbg_chrome-create.js new file mode 100644 index 000000000..185bd6f1b --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_chrome-create.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that a chrome debugger can be created in a new process. + */ + +let gProcess; + +function test() { + // Windows XP and 8.1 test slaves are terribly slow at this test. + requestLongerTimeout(5); + Services.prefs.setBoolPref("devtools.debugger.remote-enabled", true); + + initChromeDebugger(aOnClose).then(aProcess => { + gProcess = aProcess; + + info("Starting test..."); + performTest(); + }); +} + +function performTest() { + ok(gProcess._dbgProcess, + "The remote debugger process wasn't created properly!"); + ok(gProcess._dbgProcess.isRunning, + "The remote debugger process isn't running!"); + is(typeof gProcess._dbgProcess.pid, "number", + "The remote debugger process doesn't have a pid (?!)"); + + info("process location: " + gProcess._dbgProcess.location); + info("process pid: " + gProcess._dbgProcess.pid); + info("process name: " + gProcess._dbgProcess.processName); + info("process sig: " + gProcess._dbgProcess.processSignature); + + ok(gProcess._dbgProfilePath, + "The remote debugger profile wasn't created properly!"); + is(gProcess._dbgProfilePath, OS.Path.join(OS.Constants.Path.profileDir, "chrome_debugger_profile"), + "The remote debugger profile isn't where we expect it!"); + + info("profile path: " + gProcess._dbgProfilePath); + + gProcess.close(); +} + +function aOnClose() { + ok(!gProcess._dbgProcess.isRunning, + "The remote debugger process isn't closed as it should be!"); + is(gProcess._dbgProcess.exitValue, (Services.appinfo.OS == "WINNT" ? 0 : 256), + "The remote debugger process didn't die cleanly."); + + info("process exit value: " + gProcess._dbgProcess.exitValue); + + info("profile path: " + gProcess._dbgProfilePath); + + finish(); +} + +registerCleanupFunction(function() { + Services.prefs.clearUserPref("devtools.debugger.remote-enabled"); + gProcess = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_chrome-debugging.js b/toolkit/devtools/debugger/test/browser_dbg_chrome-debugging.js new file mode 100644 index 000000000..7043d3eeb --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_chrome-debugging.js @@ -0,0 +1,100 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that chrome debugging works. + */ + +const TAB_URL = EXAMPLE_URL + "doc_inline-debugger-statement.html"; + +let gClient, gThreadClient; +let gAttached = promise.defer(); +let gNewGlobal = promise.defer() +let gNewChromeSource = promise.defer() + +let { DevToolsLoader } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); +let loader = new DevToolsLoader(); +loader.invisibleToDebugger = true; +loader.main("devtools/server/main"); +let DebuggerServer = loader.DebuggerServer; + +function test() { + if (!DebuggerServer.initialized) { + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + } + + let transport = DebuggerServer.connectPipe(); + gClient = new DebuggerClient(transport); + gClient.connect((aType, aTraits) => { + is(aType, "browser", + "Root actor should identify itself as a browser."); + + promise.all([gAttached.promise, gNewGlobal.promise, gNewChromeSource.promise]) + .then(resumeAndCloseConnection) + .then(finish) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + + testChromeActor(); + }); +} + +function testChromeActor() { + gClient.listTabs(aResponse => { + ok(aResponse.chromeDebugger.contains("chromeDebugger"), + "Chrome debugger actor should identify itself accordingly."); + + gClient.addListener("newGlobal", onNewGlobal); + gClient.addListener("newSource", onNewSource); + + gClient.attachThread(aResponse.chromeDebugger, (aResponse, aThreadClient) => { + gThreadClient = aThreadClient; + + if (aResponse.error) { + ok(false, "Couldn't attach to the chrome debugger."); + gAttached.reject(); + } else { + ok(true, "Attached to the chrome debugger."); + gAttached.resolve(); + + // Ensure that a new chrome global will be created. + gBrowser.selectedTab = gBrowser.addTab("about:mozilla"); + } + }); + }); +} + +function onNewGlobal() { + ok(true, "Received a new chrome global."); + + gClient.removeListener("newGlobal", onNewGlobal); + gNewGlobal.resolve(); +} + +function onNewSource(aEvent, aPacket) { + if (aPacket.source.url.startsWith("chrome:")) { + ok(true, "Received a new chrome source: " + aPacket.source.url); + + gClient.removeListener("newSource", onNewSource); + gNewChromeSource.resolve(); + } +} + +function resumeAndCloseConnection() { + let deferred = promise.defer(); + gThreadClient.resume(() => gClient.close(deferred.resolve)); + return deferred.promise; +} + +registerCleanupFunction(function() { + gClient = null; + gThreadClient = null; + gAttached = null; + gNewGlobal = null; + gNewChromeSource = null; + + loader = null; + DebuggerServer = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_clean-exit-window.js b/toolkit/devtools/debugger/test/browser_dbg_clean-exit-window.js new file mode 100644 index 000000000..61b92cffa --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_clean-exit-window.js @@ -0,0 +1,84 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that closing a window with the debugger in a paused state exits cleanly. + */ + +let gDebuggee, gPanel, gDebugger, gWindow; + +const TAB_URL = EXAMPLE_URL + "doc_inline-debugger-statement.html"; + +function test() { + addWindow(TAB_URL) + .then(win => initDebugger(TAB_URL, win)) + .then(([aTab, aDebuggee, aPanel, aWindow]) => { + gDebuggee = aDebuggee; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gWindow = aWindow; + + return testCleanExit(); + }) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); +} + +function testCleanExit() { + let deferred = promise.defer(); + + ok(!!gWindow, "Second window created."); + + gWindow.focus(); + + is(Services.wm.getMostRecentWindow("navigator:browser"), gWindow, + "The second window is on top."); + + let isActive = promise.defer(); + let isLoaded = promise.defer(); + + promise.all([isActive.promise, isLoaded.promise]).then(() => { + waitForSourceAndCaretAndScopes(gPanel, ".html", 16).then(() => { + is(gDebugger.gThreadClient.paused, true, + "Should be paused after the debugger statement."); + gWindow.close(); + deferred.resolve(); + finish(); + }); + + gDebuggee.runDebuggerStatement(); + }); + + if (Services.focus.activeWindow != gWindow) { + gWindow.addEventListener("activate", function onActivate(aEvent) { + if (aEvent.target != gWindow) { + return; + } + gWindow.removeEventListener("activate", onActivate, true); + isActive.resolve(); + }, true); + } else { + isActive.resolve(); + } + + if (gWindow.content.location.href != TAB_URL) { + gWindow.document.addEventListener("load", function onLoad(aEvent) { + if (aEvent.target.documentURI != TAB_URL) { + return; + } + gWindow.document.removeEventListener("load", onLoad, true); + isLoaded.resolve(); + }, true); + } else { + isLoaded.resolve(); + } + return deferred.promise; +} + +registerCleanupFunction(function() { + gWindow = null; + gDebuggee = null; + gPanel = null; + gDebugger = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_clean-exit.js b/toolkit/devtools/debugger/test/browser_dbg_clean-exit.js new file mode 100644 index 000000000..4d41504ee --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_clean-exit.js @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that closing a tab with the debugger in a paused state exits cleanly. + */ + +let gTab, gPanel, gDebugger; + +const TAB_URL = EXAMPLE_URL + "doc_inline-debugger-statement.html"; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + + testCleanExit(); + }); +} + +function testCleanExit() { + promise.all([ + waitForSourceAndCaretAndScopes(gPanel, ".html", 16), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.AFTER_FRAMES_REFILLED) + ]).then(() => { + is(gDebugger.gThreadClient.paused, true, + "Should be paused after the debugger statement."); + }).then(() => closeDebuggerAndFinish(gPanel, { whilePaused: true })); + + callInTab(gTab, "runDebuggerStatement"); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_closure-inspection.js b/toolkit/devtools/debugger/test/browser_dbg_closure-inspection.js new file mode 100644 index 000000000..47c99e57d --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_closure-inspection.js @@ -0,0 +1,148 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const TAB_URL = EXAMPLE_URL + "doc_closures.html"; + +// Test that inspecting a closure works as expected. + +function test() { + let gPanel, gTab, gDebugger; + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + + waitForSourceShown(gPanel, ".html") + .then(testClosure) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); + + function testClosure() { + sendMouseClickToTab(gTab, content.document.querySelector("button")); + + return waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES).then(() => { + let gVars = gDebugger.DebuggerView.Variables; + let localScope = gVars.getScopeAtIndex(0); + let localNodes = localScope.target.querySelector(".variables-view-element-details").childNodes; + + is(localNodes[4].querySelector(".name").getAttribute("value"), "person", + "Should have the right property name for |person|."); + is(localNodes[4].querySelector(".value").getAttribute("value"), "Object", + "Should have the right property value for |person|."); + + // Expand the 'person' tree node. This causes its properties to be + // retrieved and displayed. + let personNode = gVars.getItemForNode(localNodes[4]); + let personFetched = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES); + personNode.expand(); + + return personFetched.then(() => { + is(personNode.expanded, true, + "|person| should be expanded at this point."); + + is(personNode.get("getName").target.querySelector(".name") + .getAttribute("value"), "getName", + "Should have the right property name for 'getName' in person."); + is(personNode.get("getName").target.querySelector(".value") + .getAttribute("value"), "_pfactory/<.getName()", + "'getName' in person should have the right value."); + is(personNode.get("getFoo").target.querySelector(".name") + .getAttribute("value"), "getFoo", + "Should have the right property name for 'getFoo' in person."); + is(personNode.get("getFoo").target.querySelector(".value") + .getAttribute("value"), "_pfactory/<.getFoo()", + "'getFoo' in person should have the right value."); + + // Expand the function nodes. This causes their properties to be + // retrieved and displayed. + let getFooNode = personNode.get("getFoo"); + let getNameNode = personNode.get("getName"); + let funcsFetched = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 2); + let funcClosuresFetched = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES, 2); + getFooNode.expand(); + getNameNode.expand(); + + return funcsFetched.then(() => { + is(getFooNode.expanded, true, + "|person.getFoo| should be expanded at this point."); + is(getNameNode.expanded, true, + "|person.getName| should be expanded at this point."); + + is(getFooNode.get("<Closure>").target.querySelector(".name") + .getAttribute("value"), "<Closure>", + "Found the closure node for getFoo."); + is(getFooNode.get("<Closure>").target.querySelector(".value") + .getAttribute("value"), "", + "The closure node has no value for getFoo."); + is(getNameNode.get("<Closure>").target.querySelector(".name") + .getAttribute("value"), "<Closure>", + "Found the closure node for getName."); + is(getNameNode.get("<Closure>").target.querySelector(".value") + .getAttribute("value"), "", + "The closure node has no value for getName."); + + // Expand the closure nodes. This causes their environments to be + // retrieved and displayed. + let getFooClosure = getFooNode.get("<Closure>"); + let getNameClosure = getNameNode.get("<Closure>"); + getFooClosure.expand(); + getNameClosure.expand(); + + return funcClosuresFetched.then(() => { + is(getFooClosure.expanded, true, + "|person.getFoo| closure should be expanded at this point."); + is(getNameClosure.expanded, true, + "|person.getName| closure should be expanded at this point."); + + is(getFooClosure.get("Function scope [_pfactory]").target.querySelector(".name") + .getAttribute("value"), "Function scope [_pfactory]", + "Found the function scope node for the getFoo closure."); + is(getFooClosure.get("Function scope [_pfactory]").target.querySelector(".value") + .getAttribute("value"), "", + "The function scope node has no value for the getFoo closure."); + is(getNameClosure.get("Function scope [_pfactory]").target.querySelector(".name") + .getAttribute("value"), "Function scope [_pfactory]", + "Found the function scope node for the getName closure."); + is(getNameClosure.get("Function scope [_pfactory]").target.querySelector(".value") + .getAttribute("value"), "", + "The function scope node has no value for the getName closure."); + + // Expand the scope nodes. + let getFooInnerScope = getFooClosure.get("Function scope [_pfactory]"); + let getNameInnerScope = getNameClosure.get("Function scope [_pfactory]"); + let innerFuncsFetched = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 2); + getFooInnerScope.expand(); + getNameInnerScope.expand(); + + return funcsFetched.then(() => { + is(getFooInnerScope.expanded, true, + "|person.getFoo| inner scope should be expanded at this point."); + is(getNameInnerScope.expanded, true, + "|person.getName| inner scope should be expanded at this point."); + + // Only test that each function closes over the necessary variable. + // We wouldn't want future SpiderMonkey closure space + // optimizations to break this test. + is(getFooInnerScope.get("foo").target.querySelector(".name") + .getAttribute("value"), "foo", + "Found the foo node for the getFoo inner scope."); + is(getFooInnerScope.get("foo").target.querySelector(".value") + .getAttribute("value"), "10", + "The foo node has the expected value."); + is(getNameInnerScope.get("name").target.querySelector(".name") + .getAttribute("value"), "name", + "Found the name node for the getName inner scope."); + is(getNameInnerScope.get("name").target.querySelector(".value") + .getAttribute("value"), '"Bob"', + "The name node has the expected value."); + }); + }); + }); + }); + }); + } +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_cmd-blackbox.js b/toolkit/devtools/debugger/test/browser_dbg_cmd-blackbox.js new file mode 100644 index 000000000..797efc1a4 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_cmd-blackbox.js @@ -0,0 +1,114 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that the 'dbg blackbox' and 'dbg unblackbox' commands work as + * they should. + */ + +const TEST_URL = EXAMPLE_URL + "doc_blackboxing.html"; +const BLACKBOXME_URL = EXAMPLE_URL + "code_blackboxing_blackboxme.js"; +const BLACKBOXONE_URL = EXAMPLE_URL + "code_blackboxing_one.js"; +const BLACKBOXTWO_URL = EXAMPLE_URL + "code_blackboxing_two.js"; +const BLACKBOXTHREE_URL = EXAMPLE_URL + "code_blackboxing_three.js"; + +function test() { + return Task.spawn(spawnTest).then(finish, helpers.handleError); +} + +function spawnTest() { + let options = yield helpers.openTab(TEST_URL); + yield helpers.openToolbar(options); + + let toolbox = yield gDevTools.showToolbox(options.target, "jsdebugger"); + let panel = toolbox.getCurrentPanel(); + + yield waitForDebuggerEvents(panel, panel.panelWin.EVENTS.SOURCE_SHOWN); + + function cmd(aTyped, aEventRepeat = 1, aOutput = "") { + return promise.all([ + waitForThreadEvents(panel, "blackboxchange", aEventRepeat), + helpers.audit(options, [{ setup: aTyped, output: aOutput, exec: {} }]) + ]); + } + + // test Black-Box Source + yield cmd("dbg blackbox " + BLACKBOXME_URL); + + let bbButton = yield selectSourceAndGetBlackBoxButton(panel, BLACKBOXME_URL); + ok(bbButton.checked, + "Should be able to black box a specific source."); + + // test Un-Black-Box Source + yield cmd("dbg unblackbox " + BLACKBOXME_URL); + + bbButton = yield selectSourceAndGetBlackBoxButton(panel, BLACKBOXME_URL); + ok(!bbButton.checked, + "Should be able to stop black boxing a specific source."); + + // test Black-Box Glob + yield cmd("dbg blackbox --glob *blackboxing_t*.js", 2, + [/blackboxing_three\.js/g, /blackboxing_two\.js/g]); + + bbButton = yield selectSourceAndGetBlackBoxButton(panel, BLACKBOXME_URL); + ok(!bbButton.checked, + "blackboxme should not be black boxed because it doesn't match the glob."); + bbButton = yield selectSourceAndGetBlackBoxButton(panel, BLACKBOXONE_URL); + ok(!bbButton.checked, + "blackbox_one should not be black boxed because it doesn't match the glob."); + + bbButton = yield selectSourceAndGetBlackBoxButton(panel, BLACKBOXTWO_URL); + ok(bbButton.checked, + "blackbox_two should be black boxed because it matches the glob."); + bbButton = yield selectSourceAndGetBlackBoxButton(panel, BLACKBOXTHREE_URL); + ok(bbButton.checked, + "blackbox_three should be black boxed because it matches the glob."); + + // test Un-Black-Box Glob + yield cmd("dbg unblackbox --glob *blackboxing_t*.js", 2); + + bbButton = yield selectSourceAndGetBlackBoxButton(panel, BLACKBOXTWO_URL); + ok(!bbButton.checked, + "blackbox_two should be un-black boxed because it matches the glob."); + bbButton = yield selectSourceAndGetBlackBoxButton(panel, BLACKBOXTHREE_URL); + ok(!bbButton.checked, + "blackbox_three should be un-black boxed because it matches the glob."); + + // test Black-Box Invert + yield cmd("dbg blackbox --invert --glob *blackboxing_t*.js", 3, + [/blackboxing_three\.js/g, /blackboxing_two\.js/g]); + + bbButton = yield selectSourceAndGetBlackBoxButton(panel, BLACKBOXME_URL); + ok(bbButton.checked, + "blackboxme should be black boxed because it doesn't match the glob."); + bbButton = yield selectSourceAndGetBlackBoxButton(panel, BLACKBOXONE_URL); + ok(bbButton.checked, + "blackbox_one should be black boxed because it doesn't match the glob."); + bbButton = yield selectSourceAndGetBlackBoxButton(panel, TEST_URL); + ok(bbButton.checked, + "TEST_URL should be black boxed because it doesn't match the glob."); + + bbButton = yield selectSourceAndGetBlackBoxButton(panel, BLACKBOXTWO_URL); + ok(!bbButton.checked, + "blackbox_two should not be black boxed because it matches the glob."); + bbButton = yield selectSourceAndGetBlackBoxButton(panel, BLACKBOXTHREE_URL); + ok(!bbButton.checked, + "blackbox_three should not be black boxed because it matches the glob."); + + // test Un-Black-Box Invert + yield cmd("dbg unblackbox --invert --glob *blackboxing_t*.js", 3); + + bbButton = yield selectSourceAndGetBlackBoxButton(panel, BLACKBOXME_URL); + ok(!bbButton.checked, + "blackboxme should be un-black boxed because it does not match the glob."); + bbButton = yield selectSourceAndGetBlackBoxButton(panel, BLACKBOXONE_URL); + ok(!bbButton.checked, + "blackbox_one should be un-black boxed because it does not match the glob."); + bbButton = yield selectSourceAndGetBlackBoxButton(panel, TEST_URL); + ok(!bbButton.checked, + "TEST_URL should be un-black boxed because it doesn't match the glob."); + + yield teardown(panel, { noTabRemoval: true }); + yield helpers.closeToolbar(options); + yield helpers.closeTab(options); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_cmd-break.js b/toolkit/devtools/debugger/test/browser_dbg_cmd-break.js new file mode 100644 index 000000000..702c01059 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_cmd-break.js @@ -0,0 +1,221 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that the break commands works as they should. + */ + +const TAB_URL = EXAMPLE_URL + "doc_cmd-break.html"; +let TAB_URL_ACTOR; + +function test() { + let gPanel, gDebugger, gThreadClient, gSources; + let gLineNumber; + + let expectedActorObj = { + value: null, + message: '' + }; + + helpers.addTabWithToolbar(TAB_URL, aOptions => { + return Task.spawn(function() { + yield helpers.audit(aOptions, [{ + setup: 'break', + check: { + input: 'break', + hints: ' add line', + markup: 'IIIII', + status: 'ERROR', + } + }]); + + yield helpers.audit(aOptions, [{ + setup: 'break add', + check: { + input: 'break add', + hints: ' line', + markup: 'IIIIIVIII', + status: 'ERROR' + } + }]); + + yield helpers.audit(aOptions, [{ + setup: 'break add line', + check: { + input: 'break add line', + hints: ' <file> <line>', + markup: 'VVVVVVVVVVVVVV', + status: 'ERROR' + } + }]); + + yield helpers.audit(aOptions, [{ + name: 'open toolbox', + setup: function() { + return initDebugger(gBrowser.selectedTab).then(([aTab, aDebuggee, aPanel]) => { + // Spin the event loop before causing the debuggee to pause, to allow + // this function to return first. + executeSoon(() => aDebuggee.firstCall()); + + return waitForSourceAndCaretAndScopes(aPanel, ".html", 1).then(() => { + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gThreadClient = gPanel.panelWin.gThreadClient; + gLineNumber = '' + aOptions.window.wrappedJSObject.gLineNumber; + gSources = gDebugger.DebuggerView.Sources; + + expectedActorObj.value = getSourceActor(gSources, TAB_URL); + }); + }); + }, + post: function() { + ok(gThreadClient, "Debugger client exists."); + is(gLineNumber, 14, "gLineNumber is correct."); + }, + }]); + + yield helpers.audit(aOptions, [{ + name: 'break add line .../doc_cmd-break.html 14', + setup: function() { + // We have to setup in a function to allow gLineNumber to be initialized. + let line = 'break add line ' + TAB_URL + ' ' + gLineNumber; + return helpers.setInput(aOptions, line); + }, + check: { + hints: '', + status: 'VALID', + message: '', + args: { + file: expectedActorObj, + line: { value: 14 } + } + }, + exec: { + output: 'Added breakpoint' + } + }]); + + yield helpers.audit(aOptions, [{ + setup: 'break add line ' + TAB_URL + ' 17', + check: { + hints: '', + status: 'VALID', + message: '', + args: { + file: expectedActorObj, + line: { value: 17 } + } + }, + exec: { + output: 'Added breakpoint' + } + }]); + + yield helpers.audit(aOptions, [{ + setup: 'break list', + check: { + input: 'break list', + hints: '', + markup: 'VVVVVVVVVV', + status: 'VALID' + }, + exec: { + output: [ + /Source/, /Remove/, + /doc_cmd-break\.html:14/, + /doc_cmd-break\.html:17/ + ] + } + }]); + + yield helpers.audit(aOptions, [{ + name: 'cleanup', + setup: function() { + let deferred = promise.defer(); + gThreadClient.resume(deferred.resolve); + return deferred.promise; + } + }]); + + yield helpers.audit(aOptions, [{ + setup: 'break del 14', + check: { + input: 'break del 14', + hints: ' -> doc_cmd-break.html:14', + markup: 'VVVVVVVVVVII', + status: 'ERROR', + args: { + breakpoint: { + status: 'INCOMPLETE', + message: 'Value required for \'breakpoint\'.' + } + } + } + }]); + + yield helpers.audit(aOptions, [{ + setup: 'break del doc_cmd-break.html:14', + check: { + input: 'break del doc_cmd-break.html:14', + hints: '', + markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV', + status: 'VALID', + args: { + breakpoint: { arg: ' doc_cmd-break.html:14' }, + } + }, + exec: { + output: 'Breakpoint removed' + } + }]); + + yield helpers.audit(aOptions, [{ + setup: 'break list', + check: { + input: 'break list', + hints: '', + markup: 'VVVVVVVVVV', + status: 'VALID' + }, + exec: { + output: [ + /Source/, /Remove/, + /doc_cmd-break\.html:17/ + ] + } + }]); + + yield helpers.audit(aOptions, [{ + setup: 'break del doc_cmd-break.html:17', + check: { + input: 'break del doc_cmd-break.html:17', + hints: '', + markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV', + status: 'VALID', + args: { + breakpoint: { arg: ' doc_cmd-break.html:17' }, + } + }, + exec: { + output: 'Breakpoint removed' + } + }]); + + yield helpers.audit(aOptions, [{ + setup: 'break list', + check: { + input: 'break list', + hints: '', + markup: 'VVVVVVVVVV', + status: 'VALID' + }, + exec: { + output: 'No breakpoints set' + }, + post: function() { + return teardown(gPanel, { noTabRemoval: true }); + } + }]); + }); + }).then(finish); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_cmd-dbg.js b/toolkit/devtools/debugger/test/browser_dbg_cmd-dbg.js new file mode 100644 index 000000000..7627acc4e --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_cmd-dbg.js @@ -0,0 +1,100 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that the debugger commands work as they should. + */ + +const TEST_URI = EXAMPLE_URL + "doc_cmd-dbg.html"; + +function test() { + return Task.spawn(function() { + let options = yield helpers.openTab(TEST_URI); + yield helpers.openToolbar(options); + + yield helpers.audit(options, [{ + setup: "dbg open", + exec: { output: "" } + }]); + + let [gTab, gDebuggee, gPanel] = yield initDebugger(gBrowser.selectedTab); + let gDebugger = gPanel.panelWin; + let gThreadClient = gDebugger.gThreadClient; + + yield helpers.audit(options, [{ + setup: "dbg list", + exec: { output: /doc_cmd-dbg.html/ } + }]); + + let button = gDebuggee.document.querySelector("input[type=button]"); + let output = gDebuggee.document.querySelector("input[type=text]"); + + let cmd = function(aTyped, aState) { + return promise.all([ + waitForThreadEvents(gPanel, aState), + helpers.audit(options, [{ setup: aTyped, exec: { output: "" } }]) + ]); + }; + + let click = function(aElement, aState) { + return promise.all([ + waitForThreadEvents(gPanel, aState), + executeSoon(() => EventUtils.sendMouseEvent({ type: "click" }, aElement, gDebuggee)) + ]); + } + + yield cmd("dbg interrupt", "paused"); + is(gThreadClient.state, "paused", "Debugger is paused."); + + yield cmd("dbg continue", "resumed"); + isnot(gThreadClient.state, "paused", "Debugger has continued."); + + yield click(button, "paused"); + is(gThreadClient.state, "paused", "Debugger is paused again."); + + yield cmd("dbg step in", "paused"); + yield cmd("dbg step in", "paused"); + yield cmd("dbg step in", "paused"); + is(output.value, "step in", "Debugger stepped in."); + + yield cmd("dbg step over", "paused"); + is(output.value, "step over", "Debugger stepped over."); + + yield cmd("dbg step out", "paused"); + is(output.value, "step out", "Debugger stepped out."); + + yield cmd("dbg continue", "paused"); + is(output.value, "dbg continue", "Debugger continued."); + + let closeDebugger = function() { + let deferred = promise.defer(); + + helpers.audit(options, [{ + setup: "dbg close", + exec: { output: "" } + }]) + .then(() => { + let toolbox = gDevTools.getToolbox(options.target); + if (!toolbox) { + ok(true, "Debugger is closed."); + deferred.resolve(); + } else { + toolbox.on("destroyed", () => { + ok(true, "Debugger just closed."); + deferred.resolve(); + }); + } + }); + + return deferred.promise; + }; + + // We close the debugger twice to ensure 'dbg close' doesn't error when + // toolbox is already closed. See bug 884638 for more info. + yield closeDebugger(); + yield closeDebugger(); + yield helpers.closeToolbar(options); + yield helpers.closeTab(options); + + }).then(finish, helpers.handleError); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_conditional-breakpoints-01.js b/toolkit/devtools/debugger/test/browser_dbg_conditional-breakpoints-01.js new file mode 100644 index 000000000..952035efb --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_conditional-breakpoints-01.js @@ -0,0 +1,243 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Bug 740825: Test the debugger conditional breakpoints. + */ + +const TAB_URL = EXAMPLE_URL + "doc_conditional-breakpoints.html"; + +function test() { + // Linux debug test slaves are a bit slow at this test sometimes. + requestLongerTimeout(2); + + let gTab, gPanel, gDebugger; + let gEditor, gSources, gBreakpoints, gBreakpointsAdded, gBreakpointsRemoving; + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gSources = gDebugger.DebuggerView.Sources; + gBreakpoints = gDebugger.DebuggerController.Breakpoints; + gBreakpointsAdded = gBreakpoints._added; + gBreakpointsRemoving = gBreakpoints._removing; + + // This test forces conditional breakpoints to be evaluated on the + // client-side + var client = gPanel.target.client; + client.mainRoot.traits.conditionalBreakpoints = false; + + waitForSourceAndCaretAndScopes(gPanel, ".html", 17) + .then(() => addBreakpoints()) + .then(() => initialChecks()) + .then(() => resumeAndTestBreakpoint(20)) + .then(() => resumeAndTestBreakpoint(21)) + .then(() => resumeAndTestBreakpoint(22)) + .then(() => resumeAndTestBreakpoint(23)) + .then(() => resumeAndTestBreakpoint(24)) + .then(() => resumeAndTestBreakpoint(25)) + .then(() => resumeAndTestBreakpoint(27)) + .then(() => resumeAndTestBreakpoint(28)) + .then(() => { + // Note: the breakpoint on line 29 should not be hit since the + // conditional expression evaluates to undefined. It used to + // be on line 30, but it can't be the last breakpoint because + // there is a race condition (the "frames cleared" event might + // fire from the conditional expression evaluation if it's too + // slow, which is what we wait for to reload the page) + return resumeAndTestBreakpoint(30); + }) + .then(() => resumeAndTestNoBreakpoint()) + .then(() => { + return promise.all([ + reloadActiveTab(gPanel, gDebugger.EVENTS.BREAKPOINT_SHOWN_IN_EDITOR, 13), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_SHOWN_IN_PANE, 13) + ]); + }) + .then(() => testAfterReload()) + .then(() => { + // Reset traits back to default value + client.mainRoot.traits.conditionalBreakpoints = true; + }) + .then(() => closeDebuggerAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + + callInTab(gTab, "ermahgerd"); + }); + + function addBreakpoints() { + return promise.resolve(null) + .then(() => gPanel.addBreakpoint({ actor: gSources.selectedValue, line: 18 })) + .then(aClient => aClient.conditionalExpression = "undefined") + .then(() => gPanel.addBreakpoint({ actor: gSources.selectedValue, line: 19 })) + .then(aClient => aClient.conditionalExpression = "null") + .then(() => gPanel.addBreakpoint({ actor: gSources.selectedValue, line: 20 })) + .then(aClient => aClient.conditionalExpression = "42") + .then(() => gPanel.addBreakpoint({ actor: gSources.selectedValue, line: 21 })) + .then(aClient => aClient.conditionalExpression = "true") + .then(() => gPanel.addBreakpoint({ actor: gSources.selectedValue, line: 22 })) + .then(aClient => aClient.conditionalExpression = "'nasu'") + .then(() => gPanel.addBreakpoint({ actor: gSources.selectedValue, line: 23 })) + .then(aClient => aClient.conditionalExpression = "/regexp/") + .then(() => gPanel.addBreakpoint({ actor: gSources.selectedValue, line: 24 })) + .then(aClient => aClient.conditionalExpression = "({})") + .then(() => gPanel.addBreakpoint({ actor: gSources.selectedValue, line: 25 })) + .then(aClient => aClient.conditionalExpression = "(function() {})") + .then(() => gPanel.addBreakpoint({ actor: gSources.selectedValue, line: 26 })) + .then(aClient => aClient.conditionalExpression = "(function() { return false; })()") + .then(() => gPanel.addBreakpoint({ actor: gSources.selectedValue, line: 27 })) + .then(aClient => aClient.conditionalExpression = "a") + .then(() => gPanel.addBreakpoint({ actor: gSources.selectedValue, line: 28 })) + .then(aClient => aClient.conditionalExpression = "a !== undefined") + .then(() => gPanel.addBreakpoint({ actor: gSources.selectedValue, line: 29 })) + .then(aClient => aClient.conditionalExpression = "b") + .then(() => gPanel.addBreakpoint({ actor: gSources.selectedValue, line: 30 })) + .then(aClient => aClient.conditionalExpression = "a !== null"); + } + + function initialChecks() { + is(gDebugger.gThreadClient.state, "paused", + "Should only be getting stack frames while paused."); + is(gSources.itemCount, 1, + "Found the expected number of sources."); + is(gEditor.getText().indexOf("ermahgerd"), 253, + "The correct source was loaded initially."); + is(gSources.selectedValue, gSources.values[0], + "The correct source is selected."); + + is(gBreakpointsAdded.size, 13, + "13 breakpoints currently added."); + is(gBreakpointsRemoving.size, 0, + "No breakpoints currently being removed."); + is(gEditor.getBreakpoints().length, 13, + "13 breakpoints currently shown in the editor."); + + ok(!gBreakpoints._getAdded({ url: "foo", line: 3 }), + "_getAdded('foo', 3) returns falsey."); + ok(!gBreakpoints._getRemoving({ url: "bar", line: 3 }), + "_getRemoving('bar', 3) returns falsey."); + } + + function resumeAndTestBreakpoint(aLine) { + let finished = waitForCaretUpdated(gPanel, aLine).then(() => testBreakpoint(aLine)); + + EventUtils.sendMouseEvent({ type: "mousedown" }, + gDebugger.document.getElementById("resume"), + gDebugger); + + return finished; + } + + function resumeAndTestNoBreakpoint() { + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.AFTER_FRAMES_CLEARED).then(() => { + is(gSources.itemCount, 1, + "Found the expected number of sources."); + is(gEditor.getText().indexOf("ermahgerd"), 253, + "The correct source was loaded initially."); + is(gSources.selectedValue, gSources.values[0], + "The correct source is selected."); + + ok(gSources.selectedItem, + "There should be a selected source in the sources pane.") + ok(!gSources._selectedBreakpointItem, + "There should be no selected breakpoint in the sources pane.") + is(gSources._conditionalPopupVisible, false, + "The breakpoint conditional expression popup should not be shown."); + + is(gDebugger.document.querySelectorAll(".dbg-stackframe").length, 0, + "There should be no visible stackframes."); + is(gDebugger.document.querySelectorAll(".dbg-breakpoint").length, 13, + "There should be thirteen visible breakpoints."); + }); + + gDebugger.gThreadClient.resume(); + + return finished; + } + + function testBreakpoint(aLine, aHighlightBreakpoint) { + // Highlight the breakpoint only if required. + if (aHighlightBreakpoint) { + let finished = waitForCaretUpdated(gPanel, aLine).then(() => testBreakpoint(aLine)); + gSources.highlightBreakpoint({ actor: gSources.selectedValue, line: aLine }); + return finished; + } + + let selectedActor = gSources.selectedValue; + let selectedBreakpoint = gSources._selectedBreakpointItem; + + ok(selectedActor, + "There should be a selected item in the sources pane."); + ok(selectedBreakpoint, + "There should be a selected breakpoint in the sources pane."); + + let source = gSources.selectedItem.attachment.source; + + is(selectedBreakpoint.attachment.actor, source.actor, + "The breakpoint on line " + aLine + " wasn't added on the correct source."); + is(selectedBreakpoint.attachment.line, aLine, + "The breakpoint on line " + aLine + " wasn't found."); + is(!!selectedBreakpoint.attachment.disabled, false, + "The breakpoint on line " + aLine + " should be enabled."); + is(!!selectedBreakpoint.attachment.openPopup, false, + "The breakpoint on line " + aLine + " should not have opened a popup."); + is(gSources._conditionalPopupVisible, false, + "The breakpoint conditional expression popup should not have been shown."); + + return gBreakpoints._getAdded(selectedBreakpoint.attachment).then(aBreakpointClient => { + is(aBreakpointClient.location.url, source.url, + "The breakpoint's client url is correct"); + is(aBreakpointClient.location.line, aLine, + "The breakpoint's client line is correct"); + isnot(aBreakpointClient.conditionalExpression, undefined, + "The breakpoint on line " + aLine + " should have a conditional expression."); + + ok(isCaretPos(gPanel, aLine), + "The editor caret position is not properly set."); + }); + } + + function testAfterReload() { + let selectedActor = gSources.selectedValue; + let selectedBreakpoint = gSources._selectedBreakpointItem; + + ok(selectedActor, + "There should be a selected item in the sources pane after reload."); + ok(!selectedBreakpoint, + "There should be no selected breakpoint in the sources pane after reload."); + + return promise.resolve(null) + .then(() => testBreakpoint(18, true)) + .then(() => testBreakpoint(19, true)) + .then(() => testBreakpoint(20, true)) + .then(() => testBreakpoint(21, true)) + .then(() => testBreakpoint(22, true)) + .then(() => testBreakpoint(23, true)) + .then(() => testBreakpoint(24, true)) + .then(() => testBreakpoint(25, true)) + .then(() => testBreakpoint(26, true)) + .then(() => testBreakpoint(27, true)) + .then(() => testBreakpoint(28, true)) + .then(() => testBreakpoint(29, true)) + .then(() => testBreakpoint(30, true)) + .then(() => { + is(gSources.itemCount, 1, + "Found the expected number of sources."); + is(gEditor.getText().indexOf("ermahgerd"), 253, + "The correct source was loaded again."); + is(gSources.selectedValue, gSources.values[0], + "The correct source is selected."); + + ok(gSources.selectedItem, + "There should be a selected source in the sources pane.") + ok(gSources._selectedBreakpointItem, + "There should be a selected breakpoint in the sources pane.") + is(gSources._conditionalPopupVisible, false, + "The breakpoint conditional expression popup should not be shown."); + }); + } +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_conditional-breakpoints-02.js b/toolkit/devtools/debugger/test/browser_dbg_conditional-breakpoints-02.js new file mode 100644 index 000000000..2fff3b6dd --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_conditional-breakpoints-02.js @@ -0,0 +1,202 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Bug 740825: Test the debugger conditional breakpoints. + */ + +const TAB_URL = EXAMPLE_URL + "doc_conditional-breakpoints.html"; + +function test() { + let gTab, gPanel, gDebugger; + let gEditor, gSources, gBreakpoints, gBreakpointsAdded, gBreakpointsRemoving; + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gSources = gDebugger.DebuggerView.Sources; + gBreakpoints = gDebugger.DebuggerController.Breakpoints; + gBreakpointsAdded = gBreakpoints._added; + gBreakpointsRemoving = gBreakpoints._removing; + + // This test forces conditional breakpoints to be evaluated on the + // client-side + var client = gPanel.target.client; + client.mainRoot.traits.conditionalBreakpoints = false; + + waitForSourceAndCaretAndScopes(gPanel, ".html", 17) + .then(() => initialChecks()) + .then(() => addBreakpoint1()) + .then(() => testBreakpoint(18, false, false, undefined)) + .then(() => addBreakpoint2()) + .then(() => testBreakpoint(19, false, false, undefined)) + .then(() => modBreakpoint2()) + .then(() => testBreakpoint(19, false, true, undefined)) + .then(() => addBreakpoint3()) + .then(() => testBreakpoint(20, true, false, undefined)) + .then(() => modBreakpoint3()) + .then(() => testBreakpoint(20, true, false, "bamboocha")) + .then(() => addBreakpoint4()) + .then(() => testBreakpoint(21, false, false, undefined)) + .then(() => delBreakpoint4()) + .then(() => setCaretPosition(18)) + .then(() => testBreakpoint(18, false, false, undefined)) + .then(() => setCaretPosition(19)) + .then(() => testBreakpoint(19, false, false, undefined)) + .then(() => setCaretPosition(20)) + .then(() => testBreakpoint(20, true, false, "bamboocha")) + .then(() => setCaretPosition(17)) + .then(() => testNoBreakpoint(17)) + .then(() => setCaretPosition(21)) + .then(() => testNoBreakpoint(21)) + .then(() => clickOnBreakpoint(0)) + .then(() => testBreakpoint(18, false, false, undefined)) + .then(() => clickOnBreakpoint(1)) + .then(() => testBreakpoint(19, false, false, undefined)) + .then(() => clickOnBreakpoint(2)) + .then(() => testBreakpoint(20, true, true, "bamboocha")) + .then(() => { + // Reset traits back to default value + client.mainRoot.traits.conditionalBreakpoints = true; + }) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + + callInTab(gTab, "ermahgerd"); + }); + + function initialChecks() { + is(gDebugger.gThreadClient.state, "paused", + "Should only be getting stack frames while paused."); + is(gSources.itemCount, 1, + "Found the expected number of sources."); + is(gEditor.getText().indexOf("ermahgerd"), 253, + "The correct source was loaded initially."); + is(gSources.selectedValue, gSources.values[0], + "The correct source is selected."); + + is(gBreakpointsAdded.size, 0, + "No breakpoints currently added."); + is(gBreakpointsRemoving.size, 0, + "No breakpoints currently being removed."); + is(gEditor.getBreakpoints().length, 0, + "No breakpoints currently shown in the editor."); + + ok(!gBreakpoints._getAdded({ actor: "foo", line: 3 }), + "_getAdded('foo', 3) returns falsey."); + ok(!gBreakpoints._getRemoving({ actor: "bar", line: 3 }), + "_getRemoving('bar', 3) returns falsey."); + } + + function addBreakpoint1() { + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_ADDED); + gPanel.addBreakpoint({ actor: gSources.selectedValue, line: 18 }); + return finished; + } + + function addBreakpoint2() { + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_ADDED); + setCaretPosition(19); + gSources._onCmdAddBreakpoint(); + return finished; + } + + function modBreakpoint2() { + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.CONDITIONAL_BREAKPOINT_POPUP_SHOWING); + setCaretPosition(19); + gSources._onCmdAddConditionalBreakpoint(); + return finished; + } + + function addBreakpoint3() { + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_ADDED); + setCaretPosition(20); + gSources._onCmdAddConditionalBreakpoint(); + return finished; + } + + function modBreakpoint3() { + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.CONDITIONAL_BREAKPOINT_POPUP_HIDING); + typeText(gSources._cbTextbox, "bamboocha"); + EventUtils.sendKey("RETURN", gDebugger); + return finished; + } + + function addBreakpoint4() { + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_ADDED); + setCaretPosition(21); + gSources._onCmdAddBreakpoint(); + return finished; + } + + function delBreakpoint4() { + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_REMOVED); + setCaretPosition(21); + gSources._onCmdAddBreakpoint(); + return finished; + } + + function testBreakpoint(aLine, aOpenPopupFlag, aPopupVisible, aConditionalExpression) { + let selectedActor = gSources.selectedValue; + let selectedBreakpoint = gSources._selectedBreakpointItem; + + ok(selectedActor, + "There should be a selected item in the sources pane."); + ok(selectedBreakpoint, + "There should be a selected brekapoint in the sources pane."); + + let source = gSources.selectedItem.attachment.source; + + is(selectedBreakpoint.attachment.actor, source.actor, + "The breakpoint on line " + aLine + " wasn't added on the correct source."); + is(selectedBreakpoint.attachment.line, aLine, + "The breakpoint on line " + aLine + " wasn't found."); + is(!!selectedBreakpoint.attachment.disabled, false, + "The breakpoint on line " + aLine + " should be enabled."); + is(!!selectedBreakpoint.attachment.openPopup, aOpenPopupFlag, + "The breakpoint on line " + aLine + " should have a correct popup state (1)."); + is(gSources._conditionalPopupVisible, aPopupVisible, + "The breakpoint on line " + aLine + " should have a correct popup state (2)."); + + return gBreakpoints._getAdded(selectedBreakpoint.attachment).then(aBreakpointClient => { + is(aBreakpointClient.location.actor, selectedActor, + "The breakpoint's client actor is correct"); + is(aBreakpointClient.location.line, aLine, + "The breakpoint's client line is correct"); + is(aBreakpointClient.conditionalExpression, aConditionalExpression, + "The breakpoint on line " + aLine + " should have a correct conditional expression."); + is("conditionalExpression" in aBreakpointClient, !!aConditionalExpression, + "The breakpoint on line " + aLine + " should have a correct conditional state."); + + ok(isCaretPos(gPanel, aLine), + "The editor caret position is not properly set."); + }); + } + + function testNoBreakpoint(aLine) { + let selectedActor = gSources.selectedValue; + let selectedBreakpoint = gSources._selectedBreakpointItem; + + ok(selectedActor, + "There should be a selected item in the sources pane for line " + aLine + "."); + ok(!selectedBreakpoint, + "There should be no selected brekapoint in the sources pane for line " + aLine + "."); + + ok(isCaretPos(gPanel, aLine), + "The editor caret position is not properly set."); + } + + function setCaretPosition(aLine) { + gEditor.setCursor({ line: aLine - 1, ch: 0 }); + } + + function clickOnBreakpoint(aIndex) { + EventUtils.sendMouseEvent({ type: "click" }, + gDebugger.document.querySelectorAll(".dbg-breakpoint")[aIndex], + gDebugger); + } +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_conditional-breakpoints-03.js b/toolkit/devtools/debugger/test/browser_dbg_conditional-breakpoints-03.js new file mode 100644 index 000000000..18edd85ae --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_conditional-breakpoints-03.js @@ -0,0 +1,90 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that conditional breakpoint expressions survive disabled breakpoints. + */ + +const TAB_URL = EXAMPLE_URL + "doc_conditional-breakpoints.html"; + +function test() { + let gTab, gPanel, gDebugger; + let gSources, gBreakpoints, gLocation; + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gSources = gDebugger.DebuggerView.Sources; + gBreakpoints = gDebugger.DebuggerController.Breakpoints; + + // This test forces conditional breakpoints to be evaluated on the + // client-side + var client = gPanel.target.client; + client.mainRoot.traits.conditionalBreakpoints = false; + + gLocation = { actor: gSources.selectedValue, line: 18 }; + + waitForSourceAndCaretAndScopes(gPanel, ".html", 17) + .then(addBreakpoint) + .then(setConditional) + .then(() => { + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_REMOVED); + toggleBreakpoint(); + return finished; + }) + .then(() => { + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_ADDED); + toggleBreakpoint(); + return finished; + }) + .then(testConditionalExpressionOnClient) + .then(() => { + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.CONDITIONAL_BREAKPOINT_POPUP_SHOWING); + openConditionalPopup(); + return finished; + }) + .then(testConditionalExpressionInPopup) + .then(() => { + // Reset traits back to default value + client.mainRoot.traits.conditionalBreakpoints = true; + }) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + + callInTab(gTab, "ermahgerd"); + }); + + function addBreakpoint() { + return gPanel.addBreakpoint(gLocation); + } + + function setConditional(aClient) { + aClient.conditionalExpression = "hello"; + } + + function toggleBreakpoint() { + EventUtils.sendMouseEvent({ type: "click" }, + gDebugger.document.querySelector(".dbg-breakpoint-checkbox"), + gDebugger); + } + + function openConditionalPopup() { + EventUtils.sendMouseEvent({ type: "click" }, + gDebugger.document.querySelector(".dbg-breakpoint"), + gDebugger); + } + + function testConditionalExpressionOnClient() { + return gBreakpoints._getAdded(gLocation).then(aClient => { + is(aClient.conditionalExpression, "hello", "The expression is correct (1)."); + }); + } + + function testConditionalExpressionInPopup() { + let textbox = gDebugger.document.getElementById("conditional-breakpoint-panel-textbox"); + is(textbox.value, "hello", "The expression is correct (2).") + } +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_conditional-breakpoints-04.js b/toolkit/devtools/debugger/test/browser_dbg_conditional-breakpoints-04.js new file mode 100644 index 000000000..3197139c1 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_conditional-breakpoints-04.js @@ -0,0 +1,92 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that conditional breakpoints with undefined expressions + * are stored as plain breakpoints when re-enabling them. + */ + +const TAB_URL = EXAMPLE_URL + "doc_conditional-breakpoints.html"; + +function test() { + let gTab, gPanel, gDebugger; + let gSources, gBreakpoints, gLocation; + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gSources = gDebugger.DebuggerView.Sources; + gBreakpoints = gDebugger.DebuggerController.Breakpoints; + + // This test forces conditional breakpoints to be evaluated on the + // client-side + var client = gPanel.target.client; + client.mainRoot.traits.conditionalBreakpoints = false; + + gLocation = { actor: gSources.selectedValue, line: 18 }; + + waitForSourceAndCaretAndScopes(gPanel, ".html", 17) + .then(addBreakpoint) + .then(setDummyConditional) + .then(() => { + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_REMOVED); + toggleBreakpoint(); + return finished; + }) + .then(() => { + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_ADDED); + toggleBreakpoint(); + return finished; + }) + .then(testConditionalExpressionOnClient) + .then(() => { + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.CONDITIONAL_BREAKPOINT_POPUP_SHOWING); + openConditionalPopup(); + finished.then(() => ok(false, "The popup shouldn't have opened.")); + return waitForTime(1000); + }) + .then(() => { + // Reset traits back to default value + client.mainRoot.traits.conditionalBreakpoints = true; + }) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + + callInTab(gTab, "ermahgerd"); + }); + + function addBreakpoint() { + return gPanel.addBreakpoint(gLocation); + } + + function setDummyConditional(aClient) { + // This happens when a conditional expression input popup is shown + // but the user doesn't type anything into it. + aClient.conditionalExpression = ""; + } + + function toggleBreakpoint() { + EventUtils.sendMouseEvent({ type: "click" }, + gDebugger.document.querySelector(".dbg-breakpoint-checkbox"), + gDebugger); + } + + function openConditionalPopup() { + EventUtils.sendMouseEvent({ type: "click" }, + gDebugger.document.querySelector(".dbg-breakpoint"), + gDebugger); + } + + function testConditionalExpressionOnClient() { + return gBreakpoints._getAdded(gLocation).then(aClient => { + if ("conditionalExpression" in aClient) { + ok(false, "A conditional expression shouldn't have been set."); + } else { + ok(true, "The conditional expression wasn't set, as expected."); + } + }); + } +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_controller-evaluate-01.js b/toolkit/devtools/debugger/test/browser_dbg_controller-evaluate-01.js new file mode 100644 index 000000000..41b98756c --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_controller-evaluate-01.js @@ -0,0 +1,91 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests the public evaluation API from the debugger controller. + */ + +const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html"; + +function test() { + Task.spawn(function() { + let [tab,, panel] = yield initDebugger(TAB_URL); + let win = panel.panelWin; + let frames = win.DebuggerController.StackFrames; + let framesView = win.DebuggerView.StackFrames; + let sources = win.DebuggerController.SourceScripts; + let sourcesView = win.DebuggerView.Sources; + let editorView = win.DebuggerView.editor; + let events = win.EVENTS; + + function checkView(frameDepth, selectedSource, caretLine, editorText) { + is(win.gThreadClient.state, "paused", + "Should only be getting stack frames while paused."); + is(framesView.itemCount, 2, + "Should have four frames."); + is(framesView.selectedDepth, frameDepth, + "The correct frame is selected in the widget."); + is(sourcesView.selectedIndex, selectedSource, + "The correct source is selected in the widget."); + ok(isCaretPos(panel, caretLine), + "Editor caret location is correct."); + is(editorView.getText().search(editorText[0]), editorText[1], + "The correct source is not displayed."); + } + + // Cache the sources text to avoid having to wait for their retrieval. + yield promise.all(sourcesView.attachments.map(e => sources.getText(e.source))); + is(sources._cache.size, 2, "There should be two cached sources in the cache."); + + // Eval while not paused. + try { + yield frames.evaluate("foo"); + } catch (error) { + is(error.message, "No stack frame available.", + "Evaluating shouldn't work while the debuggee isn't paused."); + } + + callInTab(tab, "firstCall"); + yield waitForSourceAndCaretAndScopes(panel, "-02.js", 6); + checkView(0, 1, 6, [/secondCall/, 118]); + + // Eval in the topmost frame, while paused. + let updatedView = waitForDebuggerEvents(panel, events.FETCHED_SCOPES); + let result = yield frames.evaluate("foo"); + ok(!result.throw, "The evaluation hasn't thrown."); + is(result.return.type, "object", "The evaluation return type is correct."); + is(result.return.class, "Function", "The evaluation return class is correct."); + + yield updatedView; + checkView(0, 1, 6, [/secondCall/, 118]); + ok(true, "Evaluating in the topmost frame works properly."); + + // Eval in a different frame, while paused. + updatedView = waitForDebuggerEvents(panel, events.FETCHED_SCOPES); + try { + yield frames.evaluate("foo", { depth: 1 }); // oldest frame + } catch (result) { + is(result.return.type, "object", "The evaluation thrown type is correct."); + is(result.return.class, "Error", "The evaluation thrown class is correct."); + ok(!result.return, "The evaluation hasn't returned."); + } + + yield updatedView; + checkView(0, 1, 6, [/secondCall/, 118]); + ok(true, "Evaluating in a custom frame works properly."); + + // Eval in a non-existent frame, while paused. + waitForDebuggerEvents(panel, events.FETCHED_SCOPES).then(() => { + ok(false, "Shouldn't have updated the view when trying to evaluate " + + "an expression in a non-existent stack frame."); + }); + try { + yield frames.evaluate("foo", { depth: 4 }); // non-existent frame + } catch (error) { + is(error.message, "No stack frame available.", + "Evaluating shouldn't work if the specified frame doesn't exist."); + } + + yield resumeDebuggerThenCloseAndFinish(panel); + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_controller-evaluate-02.js b/toolkit/devtools/debugger/test/browser_dbg_controller-evaluate-02.js new file mode 100644 index 000000000..441bac541 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_controller-evaluate-02.js @@ -0,0 +1,66 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests the public evaluation API from the debugger controller. + */ + +const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html"; + +function test() { + Task.spawn(function() { + let [tab,, panel] = yield initDebugger(TAB_URL); + let win = panel.panelWin; + let frames = win.DebuggerController.StackFrames; + let framesView = win.DebuggerView.StackFrames; + let sources = win.DebuggerController.SourceScripts; + let sourcesView = win.DebuggerView.Sources; + let editorView = win.DebuggerView.editor; + let events = win.EVENTS; + + function checkView(selectedFrame, selectedSource, caretLine, editorText) { + is(win.gThreadClient.state, "paused", + "Should only be getting stack frames while paused."); + is(framesView.itemCount, 2, + "Should have four frames."); + is(framesView.selectedDepth, selectedFrame, + "The correct frame is selected in the widget."); + is(sourcesView.selectedIndex, selectedSource, + "The correct source is selected in the widget."); + ok(isCaretPos(panel, caretLine), + "Editor caret location is correct."); + is(editorView.getText().search(editorText[0]), editorText[1], + "The correct source is not displayed."); + } + + // Cache the sources text to avoid having to wait for their retrieval. + yield promise.all(sourcesView.attachments.map(e => sources.getText(e.source))); + is(sources._cache.size, 2, "There should be two cached sources in the cache."); + + // Allow this generator function to yield first. + callInTab(tab, "firstCall"); + yield waitForSourceAndCaretAndScopes(panel, "-02.js", 6); + checkView(0, 1, 6, [/secondCall/, 118]); + + // Change the selected frame and eval inside it. + let updatedFrame = waitForDebuggerEvents(panel, events.FETCHED_SCOPES); + framesView.selectedDepth = 1; // oldest frame + yield updatedFrame; + checkView(1, 0, 5, [/firstCall/, 118]); + + let updatedView = waitForDebuggerEvents(panel, events.FETCHED_SCOPES); + try { + yield frames.evaluate("foo"); + } catch (result) { + is(result.return.type, "object", "The evaluation thrown type is correct."); + is(result.return.class, "Error", "The evaluation thrown class is correct."); + ok(!result.return, "The evaluation hasn't returned."); + } + + yield updatedView; + checkView(1, 0, 5, [/firstCall/, 118]); + ok(true, "Evaluating while in a user-selected frame works properly."); + + yield resumeDebuggerThenCloseAndFinish(panel); + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_debugger-statement.js b/toolkit/devtools/debugger/test/browser_dbg_debugger-statement.js new file mode 100644 index 000000000..df63a2f4f --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_debugger-statement.js @@ -0,0 +1,89 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests the behavior of the debugger statement. + */ + +const TAB_URL = EXAMPLE_URL + "doc_inline-debugger-statement.html"; + +let gClient; +let gTab; + +function test() { + if (!DebuggerServer.initialized) { + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + } + + let transport = DebuggerServer.connectPipe(); + gClient = new DebuggerClient(transport); + gClient.connect((aType, aTraits) => { + is(aType, "browser", + "Root actor should identify itself as a browser."); + + addTab(TAB_URL) + .then((aTab) => { + gTab = aTab; + return attachTabActorForUrl(gClient, TAB_URL) + }) + .then(testEarlyDebuggerStatement) + .then(testDebuggerStatement) + .then(closeConnection) + .then(finish) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); +} + +function testEarlyDebuggerStatement([aGrip, aResponse]) { + let deferred = promise.defer(); + + let onPaused = function(aEvent, aPacket) { + ok(false, "Pause shouldn't be called before we've attached!"); + deferred.reject(); + }; + + gClient.addListener("paused", onPaused); + + // This should continue without nesting an event loop and calling + // the onPaused hook, because we haven't attached yet. + callInTab(gTab, "runDebuggerStatement"); + + gClient.removeListener("paused", onPaused); + + // Now attach and resume... + gClient.request({ to: aResponse.threadActor, type: "attach" }, () => { + gClient.request({ to: aResponse.threadActor, type: "resume" }, () => { + ok(true, "Pause wasn't called before we've attached."); + deferred.resolve([aGrip, aResponse]); + }); + }); + + return deferred.promise; +} + +function testDebuggerStatement([aGrip, aResponse]) { + let deferred = promise.defer(); + + gClient.addListener("paused", (aEvent, aPacket) => { + gClient.request({ to: aResponse.threadActor, type: "resume" }, () => { + ok(true, "The pause handler was triggered on a debugger statement."); + deferred.resolve(); + }); + }); + + // Reach around the debugging protocol and execute the debugger statement. + callInTab(gTab, "runDebuggerStatement"); +} + +function closeConnection() { + let deferred = promise.defer(); + gClient.close(deferred.resolve); + return deferred.promise; +} + +registerCleanupFunction(function() { + gClient = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_editor-contextmenu.js b/toolkit/devtools/debugger/test/browser_dbg_editor-contextmenu.js new file mode 100644 index 000000000..00d45e77f --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_editor-contextmenu.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Bug 731394: Test the debugger source editor default context menu. + */ + +const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html"; + +function test() { + let gTab, gPanel, gDebugger; + let gEditor, gSources, gContextMenu; + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gSources = gDebugger.DebuggerView.Sources; + gContextMenu = gDebugger.document.getElementById("sourceEditorContextMenu"); + + waitForSourceAndCaretAndScopes(gPanel, "-02.js", 1).then(performTest).then(null, info); + callInTab(gTab, "firstCall"); + }); + + function performTest() { + is(gDebugger.gThreadClient.state, "paused", + "Should only be getting stack frames while paused."); + is(gSources.itemCount, 2, + "Found the expected number of sources."); + is(gEditor.getText().indexOf("debugger"), 166, + "The correct source was loaded initially."); + is(gSources.selectedValue, gSources.values[1], + "The correct source is selected."); + + is(gEditor.getText().indexOf("\u263a"), 162, + "Unicode characters are converted correctly."); + + ok(gContextMenu, + "The source editor's context menupopup is available."); + ok(gEditor.getOption("readOnly"), + "The source editor is read only."); + + gEditor.focus(); + gEditor.setSelection({ line: 1, ch: 0 }, { line: 1, ch: 10 }); + + once(gContextMenu, "popupshown").then(testContextMenu).then(null, info); + gContextMenu.openPopup(gEditor.container, "overlap", 0, 0, true, false); + } + + function testContextMenu() { + let document = gDebugger.document; + + ok(document.getElementById("editMenuCommands"), + "#editMenuCommands found."); + ok(!document.getElementById("editMenuKeys"), + "#editMenuKeys not found."); + + gContextMenu.hidePopup(); + resumeDebuggerThenCloseAndFinish(gPanel); + } +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_editor-mode.js b/toolkit/devtools/debugger/test/browser_dbg_editor-mode.js new file mode 100644 index 000000000..dc379e517 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_editor-mode.js @@ -0,0 +1,91 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that updating the editor mode sets the right highlighting engine, + * and source URIs with extra query parameters also get the right engine. + */ + +const TAB_URL = EXAMPLE_URL + "doc_editor-mode.html"; + +let gTab, gPanel, gDebugger; +let gEditor, gSources; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gSources = gDebugger.DebuggerView.Sources; + + waitForSourceAndCaretAndScopes(gPanel, "code_test-editor-mode", 1) + .then(testInitialSource) + .then(testSwitch1) + .then(testSwitch2) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + + callInTab(gTab, "firstCall"); + }); +} + +function testInitialSource() { + is(gSources.itemCount, 3, + "Found the expected number of sources."); + + is(gEditor.getMode().name, "text", + "Found the expected editor mode."); + is(gEditor.getText().search(/firstCall/), -1, + "The first source is not displayed."); + is(gEditor.getText().search(/debugger/), 135, + "The second source is displayed."); + is(gEditor.getText().search(/banana/), -1, + "The third source is not displayed."); + + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN); + gSources.selectedItem = e => e.attachment.label == "code_script-switching-01.js"; + return finished; +} + +function testSwitch1() { + is(gSources.itemCount, 3, + "Found the expected number of sources."); + + is(gEditor.getMode().name, "javascript", + "Found the expected editor mode."); + is(gEditor.getText().search(/firstCall/), 118, + "The first source is displayed."); + is(gEditor.getText().search(/debugger/), -1, + "The second source is not displayed."); + is(gEditor.getText().search(/banana/), -1, + "The third source is not displayed."); + + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN); + gSources.selectedItem = e => e.attachment.label == "doc_editor-mode.html"; + return finished; +} + +function testSwitch2() { + is(gSources.itemCount, 3, + "Found the expected number of sources."); + + is(gEditor.getMode().name, "htmlmixed", + "Found the expected editor mode."); + is(gEditor.getText().search(/firstCall/), -1, + "The first source is not displayed."); + is(gEditor.getText().search(/debugger/), -1, + "The second source is not displayed."); + is(gEditor.getText().search(/banana/), 443, + "The third source is displayed."); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gEditor = null; + gSources = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_event-listeners-01.js b/toolkit/devtools/debugger/test/browser_dbg_event-listeners-01.js new file mode 100644 index 000000000..675bd64cf --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_event-listeners-01.js @@ -0,0 +1,151 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that the eventListeners request works. + */ + +const TAB_URL = EXAMPLE_URL + "doc_event-listeners-01.html"; + +let gClient; +let gTab; + +function test() { + if (!DebuggerServer.initialized) { + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + } + + let transport = DebuggerServer.connectPipe(); + gClient = new DebuggerClient(transport); + gClient.connect((aType, aTraits) => { + is(aType, "browser", + "Root actor should identify itself as a browser."); + + addTab(TAB_URL) + .then((aTab) => { + gTab = aTab; + return attachThreadActorForUrl(gClient, TAB_URL) + }) + .then(pauseDebuggee) + .then(testEventListeners) + .then(closeConnection) + .then(finish) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); +} + +function pauseDebuggee(aThreadClient) { + let deferred = promise.defer(); + + gClient.addOneTimeListener("paused", (aEvent, aPacket) => { + is(aPacket.type, "paused", + "We should now be paused."); + is(aPacket.why.type, "debuggerStatement", + "The debugger statement was hit."); + + deferred.resolve(aThreadClient); + }); + + sendMouseClickToTab(gTab, content.document.querySelector("button")); + + return deferred.promise; +} + +function testEventListeners(aThreadClient) { + let deferred = promise.defer(); + + aThreadClient.eventListeners(aPacket => { + if (aPacket.error) { + let msg = "Error getting event listeners: " + aPacket.message; + ok(false, msg); + deferred.reject(msg); + return; + } + + is(aPacket.listeners.length, 3, + "Found all event listeners."); + + promise.all(aPacket.listeners.map(listener => { + const lDeferred = promise.defer(); + aThreadClient.pauseGrip(listener.function).getDefinitionSite(aResponse => { + if (aResponse.error) { + const msg = "Error getting function definition site: " + aResponse.message; + ok(false, msg); + lDeferred.reject(msg); + return; + } + listener.function.url = aResponse.source.url; + lDeferred.resolve(listener); + }); + return lDeferred.promise; + })).then(listeners => { + let types = []; + + for (let l of listeners) { + info("Listener for the "+l.type+" event."); + let node = l.node; + ok(node, "There is a node property."); + ok(node.object, "There is a node object property."); + ok(node.selector == "window" || + content.document.querySelectorAll(node.selector).length == 1, + "The node property is a unique CSS selector."); + + let func = l.function; + ok(func, "There is a function property."); + is(func.type, "object", "The function form is of type 'object'."); + is(func.class, "Function", "The function form is of class 'Function'."); + + // The onchange handler is an inline string that doesn't have + // a URL because it's basically eval'ed + if (l.type !== 'change') { + is(func.url, TAB_URL, "The function url is correct."); + } + + is(l.allowsUntrusted, true, + "'allowsUntrusted' property has the right value."); + is(l.inSystemEventGroup, false, + "'inSystemEventGroup' property has the right value."); + + types.push(l.type); + + if (l.type == "keyup") { + is(l.capturing, true, + "Capturing property has the right value."); + is(l.isEventHandler, false, + "'isEventHandler' property has the right value."); + } else if (l.type == "load") { + is(l.capturing, false, + "Capturing property has the right value."); + is(l.isEventHandler, false, + "'isEventHandler' property has the right value."); + } else { + is(l.capturing, false, + "Capturing property has the right value."); + is(l.isEventHandler, true, + "'isEventHandler' property has the right value."); + } + } + + ok(types.indexOf("click") != -1, "Found the click handler."); + ok(types.indexOf("change") != -1, "Found the change handler."); + ok(types.indexOf("keyup") != -1, "Found the keyup handler."); + + aThreadClient.resume(deferred.resolve); + }); + }); + + return deferred.promise; +} + +function closeConnection() { + let deferred = promise.defer(); + gClient.close(deferred.resolve); + return deferred.promise; +} + +registerCleanupFunction(function() { + gClient = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_event-listeners-02.js b/toolkit/devtools/debugger/test/browser_dbg_event-listeners-02.js new file mode 100644 index 000000000..8f5c5e5bb --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_event-listeners-02.js @@ -0,0 +1,127 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that the eventListeners request works when bound functions are used as + * event listeners. + */ + +const TAB_URL = EXAMPLE_URL + "doc_event-listeners-03.html"; + +let gClient; +let gTab; + +function test() { + if (!DebuggerServer.initialized) { + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + } + + let transport = DebuggerServer.connectPipe(); + gClient = new DebuggerClient(transport); + gClient.connect((aType, aTraits) => { + is(aType, "browser", + "Root actor should identify itself as a browser."); + + addTab(TAB_URL) + .then((aTab) => { + gTab = aTab; + return attachThreadActorForUrl(gClient, TAB_URL); + }) + .then(pauseDebuggee) + .then(testEventListeners) + .then(closeConnection) + .then(finish) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); +} + +function pauseDebuggee(aThreadClient) { + let deferred = promise.defer(); + + gClient.addOneTimeListener("paused", (aEvent, aPacket) => { + is(aPacket.type, "paused", + "We should now be paused."); + is(aPacket.why.type, "debuggerStatement", + "The debugger statement was hit."); + + deferred.resolve(aThreadClient); + }); + + sendMouseClickToTab(gTab, content.document.querySelector("button")); + + return deferred.promise; +} + +function testEventListeners(aThreadClient) { + let deferred = promise.defer(); + + aThreadClient.eventListeners(aPacket => { + if (aPacket.error) { + let msg = "Error getting event listeners: " + aPacket.message; + ok(false, msg); + deferred.reject(msg); + return; + } + + is(aPacket.listeners.length, 3, + "Found all event listeners."); + + promise.all(aPacket.listeners.map(listener => { + const lDeferred = promise.defer(); + aThreadClient.pauseGrip(listener.function).getDefinitionSite(aResponse => { + if (aResponse.error) { + const msg = "Error getting function definition site: " + aResponse.message; + ok(false, msg); + lDeferred.reject(msg); + return; + } + listener.function.url = aResponse.source.url; + lDeferred.resolve(listener); + }); + return lDeferred.promise; + })).then(listeners => { + is (listeners.length, 3, "Found three event listeners."); + for (let l of listeners) { + let node = l.node; + ok(node, "There is a node property."); + ok(node.object, "There is a node object property."); + ok(node.selector == "window" || + content.document.querySelectorAll(node.selector).length == 1, + "The node property is a unique CSS selector."); + + let func = l.function; + ok(func, "There is a function property."); + is(func.type, "object", "The function form is of type 'object'."); + is(func.class, "Function", "The function form is of class 'Function'."); + is(func.url, TAB_URL, "The function url is correct."); + + is(l.type, "click", "This is a click event listener."); + is(l.allowsUntrusted, true, + "'allowsUntrusted' property has the right value."); + is(l.inSystemEventGroup, false, + "'inSystemEventGroup' property has the right value."); + is(l.isEventHandler, false, + "'isEventHandler' property has the right value."); + is(l.capturing, false, + "Capturing property has the right value."); + } + + aThreadClient.resume(deferred.resolve); + }); + }); + + return deferred.promise; +} + +function closeConnection() { + let deferred = promise.defer(); + gClient.close(deferred.resolve); + return deferred.promise; +} + +registerCleanupFunction(function() { + gClient = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_event-listeners-03.js b/toolkit/devtools/debugger/test/browser_dbg_event-listeners-03.js new file mode 100644 index 000000000..0173919df --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_event-listeners-03.js @@ -0,0 +1,86 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that the eventListeners request works when there are event handlers + * that the debugger cannot unwrap. + */ + +const TAB_URL = EXAMPLE_URL + "doc_native-event-handler.html"; + +let gClient; +let gTab; + +function test() { + if (!DebuggerServer.initialized) { + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + } + + let transport = DebuggerServer.connectPipe(); + gClient = new DebuggerClient(transport); + gClient.connect((aType, aTraits) => { + is(aType, "browser", + "Root actor should identify itself as a browser."); + + addTab(TAB_URL) + .then((aTab) => { + gTab = aTab; + return attachThreadActorForUrl(gClient, TAB_URL) + }) + .then(pauseDebuggee) + .then(testEventListeners) + .then(closeConnection) + .then(finish) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); +} + +function pauseDebuggee(aThreadClient) { + let deferred = promise.defer(); + + gClient.addOneTimeListener("paused", (aEvent, aPacket) => { + is(aPacket.type, "paused", + "We should now be paused."); + is(aPacket.why.type, "debuggerStatement", + "The debugger statement was hit."); + + deferred.resolve(aThreadClient); + }); + + sendMouseClickToTab(gTab, content.document.querySelector("button")); + + return deferred.promise; +} + +function testEventListeners(aThreadClient) { + let deferred = promise.defer(); + + aThreadClient.eventListeners(aPacket => { + if (aPacket.error) { + let msg = "Error getting event listeners: " + aPacket.message; + ok(false, msg); + deferred.reject(msg); + return; + } + + // There are 4 event listeners in the page: button.onclick, window.onload + // and two more from the video element controls. + is(aPacket.listeners.length, 4, "Found all event listeners."); + aThreadClient.resume(deferred.resolve); + }); + + return deferred.promise; +} + +function closeConnection() { + let deferred = promise.defer(); + gClient.close(deferred.resolve); + return deferred.promise; +} + +registerCleanupFunction(function() { + gClient = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_file-reload.js b/toolkit/devtools/debugger/test/browser_dbg_file-reload.js new file mode 100644 index 000000000..db1501569 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_file-reload.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that source contents are invalidated when the target navigates. + */ + +const TAB_URL = EXAMPLE_URL + "doc_random-javascript.html"; +const JS_URL = EXAMPLE_URL + "sjs_random-javascript.sjs"; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + let gDebugger = aPanel.panelWin; + let gEditor = gDebugger.DebuggerView.editor; + let gSources = gDebugger.DebuggerView.Sources; + let gControllerSources = gDebugger.DebuggerController.SourceScripts; + + Task.spawn(function() { + yield waitForSourceShown(aPanel, JS_URL); + + is(gSources.itemCount, 1, + "There should be one source displayed in the view.") + is(getSelectedSourceURL(gSources), JS_URL, + "The correct source is currently selected in the view."); + ok(gEditor.getText().contains("bacon"), + "The currently shown source contains bacon. Mmm, delicious!"); + + let { source } = gSources.selectedItem.attachment; + let [, firstText] = yield gControllerSources.getText(source); + let firstNumber = parseFloat(firstText.match(/\d\.\d+/)[0]); + + is(firstText, gEditor.getText(), + "gControllerSources.getText() returned the expected contents."); + ok(firstNumber <= 1 && firstNumber >= 0, + "The generated number seems to be created correctly."); + + yield reloadActiveTab(aPanel, gDebugger.EVENTS.SOURCE_SHOWN); + + is(gSources.itemCount, 1, + "There should be one source displayed in the view after reloading.") + is(getSelectedSourceURL(gSources), JS_URL, + "The correct source is currently selected in the view after reloading."); + ok(gEditor.getText().contains("bacon"), + "The newly shown source contains bacon. Mmm, delicious!"); + + ({ source } = gSources.selectedItem.attachment); + let [, secondText] = yield gControllerSources.getText(source); + let secondNumber = parseFloat(secondText.match(/\d\.\d+/)[0]); + + is(secondText, gEditor.getText(), + "gControllerSources.getText() returned the expected contents."); + ok(secondNumber <= 1 && secondNumber >= 0, + "The generated number seems to be created correctly."); + + isnot(firstText, secondText, + "The displayed sources were different across reloads."); + isnot(firstNumber, secondNumber, + "The displayed sources differences were correct across reloads."); + + yield closeDebuggerAndFinish(aPanel); + }); + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_function-display-name.js b/toolkit/devtools/debugger/test/browser_dbg_function-display-name.js new file mode 100644 index 000000000..0b0ef9433 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_function-display-name.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that anonymous functions appear in the stack frame list with either + * their displayName property or a SpiderMonkey-inferred name. + */ + +const TAB_URL = EXAMPLE_URL + "doc_function-display-name.html"; + +let gTab, gPanel, gDebugger; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + + testAnonCall(); + }); +} + +function testAnonCall() { + waitForSourceAndCaretAndScopes(gPanel, ".html", 15).then(() => { + ok(isCaretPos(gPanel, 15), + "The source editor caret position was incorrect."); + is(gDebugger.gThreadClient.state, "paused", + "Should only be getting stack frames while paused."); + is(gDebugger.document.querySelectorAll(".dbg-stackframe").length, 3, + "Should have three frames."); + is(gDebugger.document.querySelector("#stackframe-0 .dbg-stackframe-title").getAttribute("value"), + "anonFunc", "Frame name should be 'anonFunc'."); + + testInferredName(); + }); + + callInTab(gTab, "evalCall"); +} + +function testInferredName() { + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES).then(() => { + ok(isCaretPos(gPanel, 15), + "The source editor caret position was incorrect."); + is(gDebugger.gThreadClient.state, "paused", + "Should only be getting stack frames while paused."); + is(gDebugger.document.querySelectorAll(".dbg-stackframe").length, 3, + "Should have three frames."); + is(gDebugger.document.querySelector("#stackframe-0 .dbg-stackframe-title").getAttribute("value"), + "a/<", "Frame name should be 'a/<'."); + + resumeDebuggerThenCloseAndFinish(gPanel); + }); + + gDebugger.gThreadClient.resume(); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_global-method-override.js b/toolkit/devtools/debugger/test/browser_dbg_global-method-override.js new file mode 100644 index 000000000..e3ca15e9b --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_global-method-override.js @@ -0,0 +1,20 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that scripts that override properties of the global object, like + * toString don't break the debugger. The test page used to cause the debugger + * to throw when trying to attach to the thread actor. + */ + +const TAB_URL = EXAMPLE_URL + "doc_global-method-override.html"; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + let gDebugger = aPanel.panelWin; + ok(gDebugger, "Should have a debugger available."); + is(gDebugger.gThreadClient.state, "attached", "Debugger should be attached."); + + closeDebuggerAndFinish(aPanel); + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_globalactor.js b/toolkit/devtools/debugger/test/browser_dbg_globalactor.js new file mode 100644 index 000000000..fa21a6f7f --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_globalactor.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check extension-added global actor API. + */ + +const CHROME_URL = "chrome://mochitests/content/browser/browser/devtools/debugger/test/" +const ACTORS_URL = CHROME_URL + "testactors.js"; + +function test() { + let gClient; + + if (!DebuggerServer.initialized) { + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + } + + DebuggerServer.addActors(ACTORS_URL); + + let transport = DebuggerServer.connectPipe(); + gClient = new DebuggerClient(transport); + gClient.connect((aType, aTraits) => { + is(aType, "browser", + "Root actor should identify itself as a browser."); + + gClient.listTabs(aResponse => { + let globalActor = aResponse.testGlobalActor1; + ok(globalActor, "Found the test tab actor.") + ok(globalActor.contains("test_one"), + "testGlobalActor1's actorPrefix should be used."); + + gClient.request({ to: globalActor, type: "ping" }, aResponse => { + is(aResponse.pong, "pong", "Actor should respond to requests."); + + // Send another ping to see if the same actor is used. + gClient.request({ to: globalActor, type: "ping" }, aResponse => { + is(aResponse.pong, "pong", "Actor should respond to requests."); + + // Make sure that lazily-created actors are created only once. + let conn = transport._serverConnection; + + // First we look for the pool of global actors. + let extraPools = conn._extraPools; + let globalPool; + + let actorPrefix = conn._prefix + "test_one"; + let count = 0; + for (let pool of extraPools) { + count += Object.keys(pool._actors).filter(e => { + return e.startsWith(actorPrefix); + }).length; + } + is(count, 2, + "Only two actor exists in all pools. One tab actor and one global."); + + gClient.close(finish); + }); + }); + }); + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_hide-toolbar-buttons.js b/toolkit/devtools/debugger/test/browser_dbg_hide-toolbar-buttons.js new file mode 100644 index 000000000..41f83addb --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_hide-toolbar-buttons.js @@ -0,0 +1,32 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Bug 1093349: Test that the pretty-printing and blackboxing buttons + * are hidden if the server doesn't support them + */ + +const TAB_URL = EXAMPLE_URL + "doc_auto-pretty-print-01.html"; + +let devtools = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools; +let { RootActor } = devtools.require("devtools/server/actors/root"); + +function test() { + let gTab, gDebuggee, gPanel, gDebugger; + let gEditor, gSources, gBreakpoints, gBreakpointsAdded, gBreakpointsRemoving; + + RootActor.prototype.traits.noBlackBoxing = true; + RootActor.prototype.traits.noPrettyPrinting = true; + + initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => { + let document = aPanel.panelWin.document; + let ppButton = document.querySelector('#pretty-print'); + let bbButton = document.querySelector('#black-box'); + let sep = document.querySelector('#sources-toolbar .devtools-separator'); + + is(ppButton.style.display, 'none', 'The pretty-print button is hidden'); + is(bbButton.style.display, 'none', 'The blackboxing button is hidden'); + is(sep.style.display, 'none', 'The separator is hidden'); + closeDebuggerAndFinish(aPanel) + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_hit-counts-01.js b/toolkit/devtools/debugger/test/browser_dbg_hit-counts-01.js new file mode 100644 index 000000000..841362b7a --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_hit-counts-01.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Evaluating two functions on the same line and checking for correct hit count + * for both of them in CodeMirror's gutter. + */ + +const TAB_URL = EXAMPLE_URL + "doc_same-line-functions.html"; +const CODE_URL = "code_same-line-functions.js"; + +let gTab, gPanel, gDebugger; +let gEditor; + +function test() { + Task.async(function* () { + yield pushPrefs(["devtools.debugger.tracer", true]); + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + + Task.async(function* () { + yield waitForSourceShown(gPanel, CODE_URL); + yield startTracing(gPanel); + + clickButton(); + + yield waitForClientEvents(aPanel, "traces"); + + testHitCounts(); + + yield stopTracing(gPanel); + yield popPrefs(); + yield closeDebuggerAndFinish(gPanel); + })(); + }); + })().catch(e => { + ok(false, "Got an error: " + e.message + "\n" + e.stack); + }); +} + +function clickButton() { + sendMouseClickToTab(gTab, content.document.querySelector("button")); +} + +function testHitCounts() { + let marker = gEditor.getMarker(0, 'hit-counts'); + + is(marker.innerHTML, "1\u00D7|1\u00D7", + "Both functions should be hit only once."); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gEditor = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_hit-counts-02.js b/toolkit/devtools/debugger/test/browser_dbg_hit-counts-02.js new file mode 100644 index 000000000..fb9788e9d --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_hit-counts-02.js @@ -0,0 +1,66 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * When tracing is stopped all hit counters should be cleared. + */ + +const TAB_URL = EXAMPLE_URL + "doc_same-line-functions.html"; +const CODE_URL = "code_same-line-functions.js"; + +let gTab, gPanel, gDebugger; +let gEditor; + +function test() { + Task.async(function* () { + yield pushPrefs(["devtools.debugger.tracer", true]); + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + + Task.async(function* () { + yield waitForSourceShown(gPanel, CODE_URL); + yield startTracing(gPanel); + + clickButton(); + + yield waitForClientEvents(aPanel, "traces"); + + testHitCountsBeforeStopping(); + + yield stopTracing(gPanel); + + testHitCountsAfterStopping(); + + yield popPrefs(); + yield closeDebuggerAndFinish(gPanel); + })(); + }); + })().catch(e => { + ok(false, "Got an error: " + e.message + "\n" + e.stack); + }); +} + +function clickButton() { + sendMouseClickToTab(gTab, content.document.querySelector("button")); +} + +function testHitCountsBeforeStopping() { + let marker = gEditor.getMarker(0, 'hit-counts'); + ok(marker, "A counter should exists."); +} + +function testHitCountsAfterStopping() { + let marker = gEditor.getMarker(0, 'hit-counts'); + is(marker, undefined, "A counter should be cleared."); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gEditor = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_host-layout.js b/toolkit/devtools/debugger/test/browser_dbg_host-layout.js new file mode 100644 index 000000000..66b9c70bd --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_host-layout.js @@ -0,0 +1,104 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This if the debugger's layout is correctly modified when the toolbox's + * host changes. + */ + +let gDefaultHostType = Services.prefs.getCharPref("devtools.toolbox.host"); + +function test() { + Task.spawn(function() { + yield testHosts(["bottom", "side", "window"], ["horizontal", "vertical", "horizontal"]); + yield testHosts(["side", "bottom", "side"], ["vertical", "horizontal", "vertical"]); + yield testHosts(["bottom", "side", "bottom"], ["horizontal", "vertical", "horizontal"]); + yield testHosts(["side", "window", "side"], ["vertical", "horizontal", "vertical"]); + yield testHosts(["window", "side", "window"], ["horizontal", "vertical", "horizontal"]); + finish(); + }); +} + +function testHosts(aHostTypes, aLayoutTypes) { + let [firstHost, secondHost, thirdHost] = aHostTypes; + let [firstLayout, secondLayout, thirdLayout] = aLayoutTypes; + + Services.prefs.setCharPref("devtools.toolbox.host", firstHost); + + return Task.spawn(function() { + let [tab, debuggee, panel] = yield initDebugger("about:blank"); + yield testHost(tab, panel, firstHost, firstLayout); + yield switchAndTestHost(tab, panel, secondHost, secondLayout); + yield switchAndTestHost(tab, panel, thirdHost, thirdLayout); + yield teardown(panel); + }); +} + +function switchAndTestHost(aTab, aPanel, aHostType, aLayoutType) { + let gToolbox = aPanel._toolbox; + let gDebugger = aPanel.panelWin; + + return Task.spawn(function() { + let layoutChanged = once(gDebugger, gDebugger.EVENTS.LAYOUT_CHANGED); + let hostChanged = gToolbox.switchHost(aHostType); + + yield hostChanged; + ok(true, "The toolbox's host has changed."); + + yield layoutChanged; + ok(true, "The debugger's layout has changed."); + + yield testHost(aTab, aPanel, aHostType, aLayoutType); + }); + + function once(aTarget, aEvent) { + let deferred = promise.defer(); + aTarget.once(aEvent, deferred.resolve); + return deferred.promise; + } +} + +function testHost(aTab, aPanel, aHostType, aLayoutType) { + let gDebugger = aPanel.panelWin; + let gView = gDebugger.DebuggerView; + + is(gView._hostType, aHostType, + "The default host type should've been set on the panel window (1)."); + is(gDebugger.gHostType, aHostType, + "The default host type should've been set on the panel window (2)."); + + is(gView._body.getAttribute("layout"), aLayoutType, + "The default host type is present as an attribute on the panel's body."); + + if (aLayoutType == "horizontal") { + is(gView._sourcesPane.parentNode.id, "debugger-widgets", + "The sources pane's parent is correct for the horizontal layout."); + is(gView._instrumentsPane.parentNode.id, "debugger-widgets", + "The instruments pane's parent is correct for the horizontal layout."); + } else { + is(gView._sourcesPane.parentNode.id, "vertical-layout-panes-container", + "The sources pane's parent is correct for the vertical layout."); + is(gView._instrumentsPane.parentNode.id, "vertical-layout-panes-container", + "The instruments pane's parent is correct for the vertical layout."); + } + + let widgets = gDebugger.document.getElementById("debugger-widgets").childNodes; + let panes = gDebugger.document.getElementById("vertical-layout-panes-container").childNodes; + + if (aLayoutType == "horizontal") { + is(widgets.length, 7, // 2 panes, 1 editor, 3 splitters and a phantom box. + "Found the correct number of debugger widgets."); + is(panes.length, 1, // 1 lonely splitter in the phantom box. + "Found the correct number of debugger panes."); + } else { + is(widgets.length, 5, // 1 editor, 3 splitters and a phantom box. + "Found the correct number of debugger widgets."); + is(panes.length, 3, // 2 panes and 1 splitter in the phantom box. + "Found the correct number of debugger panes."); + } +} + +registerCleanupFunction(function() { + Services.prefs.setCharPref("devtools.toolbox.host", gDefaultHostType); + gDefaultHostType = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_iframes.js b/toolkit/devtools/debugger/test/browser_dbg_iframes.js new file mode 100644 index 000000000..b920a85a0 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_iframes.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that iframes can be added as debuggees. + */ + +const TAB_URL = EXAMPLE_URL + "doc_iframes.html"; + +function test() { + let gTab, gDebuggee, gPanel, gDebugger; + let gIframe, gEditor, gSources, gFrames; + + initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => { + gTab = aTab; + gDebuggee = aDebuggee; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gIframe = gDebuggee.frames[0]; + gEditor = gDebugger.DebuggerView.editor; + gSources = gDebugger.DebuggerView.Sources; + gFrames = gDebugger.DebuggerView.StackFrames; + + waitForSourceShown(gPanel, "inline-debugger-statement.html") + .then(checkIframeSource) + .then(checkIframePause) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); + + function checkIframeSource() { + is(gDebugger.gThreadClient.paused, false, + "Should be running after starting the test."); + + ok(isCaretPos(gPanel, 1), + "The source editor caret position was incorrect."); + is(gFrames.itemCount, 0, + "Should have only no frames."); + + is(gSources.itemCount, 1, + "Found the expected number of entries in the sources widget."); + is(gEditor.getText().indexOf("debugger"), 348, + "The correct source was loaded initially."); + is(getSelectedSourceURL(gSources), EXAMPLE_URL + "doc_inline-debugger-statement.html", + "The currently selected source value is incorrect (0)."); + is(gSources.selectedValue, gSources.values[0], + "The currently selected source value is incorrect (1)."); + } + + function checkIframePause() { + // Spin the event loop before causing the debuggee to pause, to allow + // this function to return first. + executeSoon(() => gIframe.runDebuggerStatement()); + + return waitForCaretAndScopes(gPanel, 16).then(() => { + is(gDebugger.gThreadClient.paused, true, + "Should be paused after an interrupt request."); + + ok(isCaretPos(gPanel, 16), + "The source editor caret position was incorrect."); + is(gFrames.itemCount, 1, + "Should have only one frame."); + }); + } +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_instruments-pane-collapse.js b/toolkit/devtools/debugger/test/browser_dbg_instruments-pane-collapse.js new file mode 100644 index 000000000..d3f8efd3f --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_instruments-pane-collapse.js @@ -0,0 +1,152 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that the debugger panes collapse properly. + */ + +const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html"; + +let gTab, gPanel, gDebugger; +let gPrefs, gOptions; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gPrefs = gDebugger.Prefs; + gOptions = gDebugger.DebuggerView.Options; + + testPanesState(); + + gDebugger.DebuggerView.toggleInstrumentsPane({ visible: true, animated: false }); + + testInstrumentsPaneCollapse(); + testPanesStartupPref(); + + closeDebuggerAndFinish(gPanel); + }); +} + +function testPanesState() { + let instrumentsPane = + gDebugger.document.getElementById("instruments-pane"); + let instrumentsPaneToggleButton = + gDebugger.document.getElementById("instruments-pane-toggle"); + + ok(instrumentsPane.hasAttribute("pane-collapsed") && + instrumentsPaneToggleButton.hasAttribute("pane-collapsed"), + "The debugger view instruments pane should initially be hidden."); + is(gPrefs.panesVisibleOnStartup, false, + "The debugger view instruments pane should initially be preffed as hidden."); + isnot(gOptions._showPanesOnStartupItem.getAttribute("checked"), "true", + "The options menu item should not be checked."); +} + +function testInstrumentsPaneCollapse() { + let instrumentsPane = + gDebugger.document.getElementById("instruments-pane"); + let instrumentsPaneToggleButton = + gDebugger.document.getElementById("instruments-pane-toggle"); + + let width = parseInt(instrumentsPane.getAttribute("width")); + is(width, gPrefs.instrumentsWidth, + "The instruments pane has an incorrect width."); + is(instrumentsPane.style.marginLeft, "0px", + "The instruments pane has an incorrect left margin."); + is(instrumentsPane.style.marginRight, "0px", + "The instruments pane has an incorrect right margin."); + ok(!instrumentsPane.hasAttribute("animated"), + "The instruments pane has an incorrect animated attribute."); + ok(!instrumentsPane.hasAttribute("pane-collapsed") && + !instrumentsPaneToggleButton.hasAttribute("pane-collapsed"), + "The instruments pane should at this point be visible."); + + gDebugger.DebuggerView.toggleInstrumentsPane({ visible: false, animated: true }); + + is(gPrefs.panesVisibleOnStartup, false, + "The debugger view panes should still initially be preffed as hidden."); + isnot(gOptions._showPanesOnStartupItem.getAttribute("checked"), "true", + "The options menu item should still not be checked."); + + let margin = -(width + 1) + "px"; + is(width, gPrefs.instrumentsWidth, + "The instruments pane has an incorrect width after collapsing."); + is(instrumentsPane.style.marginLeft, margin, + "The instruments pane has an incorrect left margin after collapsing."); + is(instrumentsPane.style.marginRight, margin, + "The instruments pane has an incorrect right margin after collapsing."); + ok(instrumentsPane.hasAttribute("animated"), + "The instruments pane has an incorrect attribute after an animated collapsing."); + ok(instrumentsPane.hasAttribute("pane-collapsed") && + instrumentsPaneToggleButton.hasAttribute("pane-collapsed"), + "The instruments pane should not be visible after collapsing."); + + gDebugger.DebuggerView.toggleInstrumentsPane({ visible: true, animated: false }); + + is(gPrefs.panesVisibleOnStartup, false, + "The debugger view panes should still initially be preffed as hidden."); + isnot(gOptions._showPanesOnStartupItem.getAttribute("checked"), "true", + "The options menu item should still not be checked."); + + is(width, gPrefs.instrumentsWidth, + "The instruments pane has an incorrect width after uncollapsing."); + is(instrumentsPane.style.marginLeft, "0px", + "The instruments pane has an incorrect left margin after uncollapsing."); + is(instrumentsPane.style.marginRight, "0px", + "The instruments pane has an incorrect right margin after uncollapsing."); + ok(!instrumentsPane.hasAttribute("animated"), + "The instruments pane has an incorrect attribute after an unanimated uncollapsing."); + ok(!instrumentsPane.hasAttribute("pane-collapsed") && + !instrumentsPaneToggleButton.hasAttribute("pane-collapsed"), + "The instruments pane should be visible again after uncollapsing."); +} + +function testPanesStartupPref() { + let instrumentsPane = + gDebugger.document.getElementById("instruments-pane"); + let instrumentsPaneToggleButton = + gDebugger.document.getElementById("instruments-pane-toggle"); + + is(gPrefs.panesVisibleOnStartup, false, + "The debugger view panes should still initially be preffed as hidden."); + + ok(!instrumentsPane.hasAttribute("pane-collapsed") && + !instrumentsPaneToggleButton.hasAttribute("pane-collapsed"), + "The debugger instruments pane should at this point be visible."); + is(gPrefs.panesVisibleOnStartup, false, + "The debugger view panes should initially be preffed as hidden."); + isnot(gOptions._showPanesOnStartupItem.getAttribute("checked"), "true", + "The options menu item should still not be checked."); + + gOptions._showPanesOnStartupItem.setAttribute("checked", "true"); + gOptions._toggleShowPanesOnStartup(); + + ok(!instrumentsPane.hasAttribute("pane-collapsed") && + !instrumentsPaneToggleButton.hasAttribute("pane-collapsed"), + "The debugger instruments pane should at this point be visible."); + is(gPrefs.panesVisibleOnStartup, true, + "The debugger view panes should now be preffed as visible."); + is(gOptions._showPanesOnStartupItem.getAttribute("checked"), "true", + "The options menu item should now be checked."); + + gOptions._showPanesOnStartupItem.setAttribute("checked", "false"); + gOptions._toggleShowPanesOnStartup(); + + ok(!instrumentsPane.hasAttribute("pane-collapsed") && + !instrumentsPaneToggleButton.hasAttribute("pane-collapsed"), + "The debugger instruments pane should at this point be visible."); + is(gPrefs.panesVisibleOnStartup, false, + "The debugger view panes should now be preffed as hidden."); + isnot(gOptions._showPanesOnStartupItem.getAttribute("checked"), "true", + "The options menu item should now be unchecked."); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gPrefs = null; + gOptions = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_interrupts.js b/toolkit/devtools/debugger/test/browser_dbg_interrupts.js new file mode 100644 index 000000000..aedf87697 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_interrupts.js @@ -0,0 +1,85 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test if the breakpoints toggle button works as advertised. + */ + +const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html"; + +function test() { + let gTab, gPanel, gDebugger; + let gSources, gBreakpoints, gTarget, gResumeButton, gResumeKey, gThreadClient; + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gSources = gDebugger.DebuggerView.Sources; + gBreakpoints = gDebugger.DebuggerController.Breakpoints; + gTarget = gDebugger.gTarget; + gThreadClient = gDebugger.gThreadClient; + gResumeButton = gDebugger.document.getElementById("resume"); + gResumeKey = gDebugger.document.getElementById("resumeKey"); + + waitForSourceShown(gPanel, "-01.js") + .then(() => { gTarget.on("thread-paused", failOnPause); }) + .then(addBreakpoints) + .then(() => { gTarget.off("thread-paused", failOnPause); }) + .then(testResumeButton) + .then(testResumeKeyboard) + .then(() => closeDebuggerAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); + + function failOnPause() { + ok (false, "A pause was sent, but it shouldn't have been"); + } + + function addBreakpoints() { + return promise.resolve(null) + .then(() => gPanel.addBreakpoint({ actor: gSources.values[0], line: 5 })) + .then(() => gPanel.addBreakpoint({ actor: gSources.values[1], line: 6 })) + .then(() => gPanel.addBreakpoint({ actor: gSources.values[1], line: 7 })) + .then(() => ensureThreadClientState(gPanel, "resumed")); + } + + function resume() { + let onceResumed = gTarget.once("thread-resumed"); + gThreadClient.resume(); + return onceResumed; + } + + function testResumeButton() { + info ("Pressing the resume button, expecting a thread-paused"); + + ok (!gResumeButton.hasAttribute("checked"), "Resume button is not checked"); + let oncePaused = gTarget.once("thread-paused"); + EventUtils.sendMouseEvent({ type: "mousedown" }, gResumeButton, gDebugger); + + return oncePaused + .then(() => { + is (gResumeButton.getAttribute("checked"), "true", "Resume button is checked"); + }) + .then(() => gThreadClient.resume()) + .then(() => ensureThreadClientState(gPanel, "resumed")) + } + + function testResumeKeyboard() { + let key = gResumeKey.getAttribute("keycode"); + info ("Triggering a pause with keyboard (" + key + "), expecting a thread-paused"); + + ok (!gResumeButton.hasAttribute("checked"), "Resume button is not checked"); + let oncePaused = gTarget.once("thread-paused"); + EventUtils.synthesizeKey(key, { }, gDebugger); + + return oncePaused + .then(() => { + is (gResumeButton.getAttribute("checked"), "true", "Resume button is checked"); + }) + .then(() => gThreadClient.resume()) + .then(() => ensureThreadClientState(gPanel, "resumed")) + } +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_listaddons.js b/toolkit/devtools/debugger/test/browser_dbg_listaddons.js new file mode 100644 index 000000000..bf3014ef3 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_listaddons.js @@ -0,0 +1,114 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure the listAddons request works as specified. + */ +const ADDON1_URL = EXAMPLE_URL + "addon1.xpi"; +const ADDON2_URL = EXAMPLE_URL + "addon2.xpi"; + +let gAddon1, gAddon1Actor, gAddon2, gAddon2Actor, gClient; + +function test() { + if (!DebuggerServer.initialized) { + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + } + + let transport = DebuggerServer.connectPipe(); + gClient = new DebuggerClient(transport); + gClient.connect((aType, aTraits) => { + is(aType, "browser", + "Root actor should identify itself as a browser."); + + promise.resolve(null) + .then(testFirstAddon) + .then(testSecondAddon) + .then(testRemoveFirstAddon) + .then(testRemoveSecondAddon) + .then(closeConnection) + .then(finish) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); +} + +function testFirstAddon() { + let addonListChanged = false; + gClient.addOneTimeListener("addonListChanged", () => { + addonListChanged = true; + }); + + return addAddon(ADDON1_URL).then(aAddon => { + gAddon1 = aAddon; + + return getAddonActorForUrl(gClient, ADDON1_URL).then(aGrip => { + ok(!addonListChanged, "Should not yet be notified that list of addons changed."); + ok(aGrip, "Should find an addon actor for addon1."); + gAddon1Actor = aGrip.actor; + }); + }); +} + +function testSecondAddon() { + let addonListChanged = false; + gClient.addOneTimeListener("addonListChanged", function () { + addonListChanged = true; + }); + + return addAddon(ADDON2_URL).then(aAddon => { + gAddon2 = aAddon; + + return getAddonActorForUrl(gClient, ADDON1_URL).then(aFirstGrip => { + return getAddonActorForUrl(gClient, ADDON2_URL).then(aSecondGrip => { + ok(addonListChanged, "Should be notified that list of addons changed."); + is(aFirstGrip.actor, gAddon1Actor, "First addon's actor shouldn't have changed."); + ok(aSecondGrip, "Should find a addon actor for the second addon."); + gAddon2Actor = aSecondGrip.actor; + }); + }); + }); +} + +function testRemoveFirstAddon() { + let addonListChanged = false; + gClient.addOneTimeListener("addonListChanged", function () { + addonListChanged = true; + }); + + removeAddon(gAddon1).then(() => { + return getAddonActorForUrl(gClient, ADDON1_URL).then(aGrip => { + ok(addonListChanged, "Should be notified that list of addons changed."); + ok(!aGrip, "Shouldn't find a addon actor for the first addon anymore."); + }); + }); +} + +function testRemoveSecondAddon() { + let addonListChanged = false; + gClient.addOneTimeListener("addonListChanged", function () { + addonListChanged = true; + }); + + removeAddon(gAddon2).then(() => { + return getAddonActorForUrl(gClient, ADDON2_URL).then(aGrip => { + ok(addonListChanged, "Should be notified that list of addons changed."); + ok(!aGrip, "Shouldn't find a addon actor for the second addon anymore."); + }); + }); +} + +function closeConnection() { + let deferred = promise.defer(); + gClient.close(deferred.resolve); + return deferred.promise; +} + +registerCleanupFunction(function() { + gAddon1 = null; + gAddon1Actor = null; + gAddon2 = null; + gAddon2Actor = null; + gClient = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_listtabs-01.js b/toolkit/devtools/debugger/test/browser_dbg_listtabs-01.js new file mode 100644 index 000000000..0f6bc608d --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_listtabs-01.js @@ -0,0 +1,101 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure the listTabs request works as specified. + */ + +const TAB1_URL = EXAMPLE_URL + "doc_empty-tab-01.html"; +const TAB2_URL = EXAMPLE_URL + "doc_empty-tab-02.html"; + +let gTab1, gTab1Actor, gTab2, gTab2Actor, gClient; + +function test() { + if (!DebuggerServer.initialized) { + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + } + + let transport = DebuggerServer.connectPipe(); + gClient = new DebuggerClient(transport); + gClient.connect((aType, aTraits) => { + is(aType, "browser", + "Root actor should identify itself as a browser."); + + promise.resolve(null) + .then(testFirstTab) + .then(testSecondTab) + .then(testRemoveTab) + .then(testAttachRemovedTab) + .then(closeConnection) + .then(finish) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); +} + +function testFirstTab() { + return addTab(TAB1_URL).then(aTab => { + gTab1 = aTab; + + return getTabActorForUrl(gClient, TAB1_URL).then(aGrip => { + ok(aGrip, "Should find a tab actor for the first tab."); + gTab1Actor = aGrip.actor; + }); + }); +} + +function testSecondTab() { + return addTab(TAB2_URL).then(aTab => { + gTab2 = aTab; + + return getTabActorForUrl(gClient, TAB1_URL).then(aFirstGrip => { + return getTabActorForUrl(gClient, TAB2_URL).then(aSecondGrip => { + is(aFirstGrip.actor, gTab1Actor, "First tab's actor shouldn't have changed."); + ok(aSecondGrip, "Should find a tab actor for the second tab."); + gTab2Actor = aSecondGrip.actor; + }); + }); + }); +} + +function testRemoveTab() { + return removeTab(gTab1).then(() => { + return getTabActorForUrl(gClient, TAB1_URL).then(aGrip => { + ok(!aGrip, "Shouldn't find a tab actor for the first tab anymore."); + }); + }); +} + +function testAttachRemovedTab() { + return removeTab(gTab2).then(() => { + let deferred = promise.defer(); + + gClient.addListener("paused", (aEvent, aPacket) => { + ok(false, "Attaching to an exited tab actor shouldn't generate a pause."); + deferred.reject(); + }); + + gClient.request({ to: gTab2Actor, type: "attach" }, aResponse => { + is(aResponse.type, "exited", "Tab should consider itself exited."); + deferred.resolve(); + }); + + return deferred.promise; + }); +} + +function closeConnection() { + let deferred = promise.defer(); + gClient.close(deferred.resolve); + return deferred.promise; +} + +registerCleanupFunction(function() { + gTab1 = null; + gTab1Actor = null; + gTab2 = null; + gTab2Actor = null; + gClient = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_listtabs-02.js b/toolkit/devtools/debugger/test/browser_dbg_listtabs-02.js new file mode 100644 index 000000000..d9878a70a --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_listtabs-02.js @@ -0,0 +1,218 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure the root actor's live tab list implementation works as specified. + */ + +let devtools = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools; +let { BrowserTabList } = devtools.require("devtools/server/actors/webbrowser"); + +let gTestPage = "data:text/html;charset=utf-8," + encodeURIComponent( + "<title>JS Debugger BrowserTabList test page</title><body>Yo.</body>"); + +// The tablist object whose behavior we observe. +let gTabList; +let gFirstActor, gActorA; +let gTabA, gTabB, gTabC; +let gNewWindow; + +// Stock onListChanged handler. +let onListChangedCount = 0; +function onListChangedHandler() { + onListChangedCount++; +} + +function test() { + if (!DebuggerServer.initialized) { + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + } + + gTabList = new BrowserTabList("fake DebuggerServerConnection"); + gTabList._testing = true; + gTabList.onListChanged = onListChangedHandler; + + checkSingleTab() + .then(addTabA) + .then(testTabA) + .then(addTabB) + .then(testTabB) + .then(removeTabA) + .then(testTabClosed) + .then(addTabC) + .then(testTabC) + .then(removeTabC) + .then(testNewWindow) + .then(removeNewWindow) + .then(testWindowClosed) + .then(removeTabB) + .then(checkSingleTab) + .then(finishUp); +} + +function checkSingleTab() { + return gTabList.getList().then(aTabActors => { + is(aTabActors.length, 1, "initial tab list: contains initial tab"); + gFirstActor = aTabActors[0]; + is(gFirstActor.url, "about:blank", "initial tab list: initial tab URL is 'about:blank'"); + is(gFirstActor.title, "New Tab", "initial tab list: initial tab title is 'New Tab'"); + }); +} + +function addTabA() { + return addTab(gTestPage).then(aTab => { + gTabA = aTab; + }); +} + +function testTabA() { + is(onListChangedCount, 1, "onListChanged handler call count"); + + return gTabList.getList().then(aTabActors => { + let tabActors = new Set(aTabActors); + is(tabActors.size, 2, "gTabA opened: two tabs in list"); + ok(tabActors.has(gFirstActor), "gTabA opened: initial tab present"); + + info("actors: " + [a.url for (a of tabActors)]); + gActorA = [a for (a of tabActors) if (a !== gFirstActor)][0]; + ok(gActorA.url.match(/^data:text\/html;/), "gTabA opened: new tab URL"); + is(gActorA.title, "JS Debugger BrowserTabList test page", "gTabA opened: new tab title"); + }); +} + +function addTabB() { + return addTab(gTestPage).then(aTab => { + gTabB = aTab; + }); +} + +function testTabB() { + is(onListChangedCount, 2, "onListChanged handler call count"); + + return gTabList.getList().then(aTabActors => { + let tabActors = new Set(aTabActors); + is(tabActors.size, 3, "gTabB opened: three tabs in list"); + }); +} + +function removeTabA() { + let deferred = promise.defer(); + + once(gBrowser.tabContainer, "TabClose").then(aEvent => { + ok(!aEvent.detail, "This was a normal tab close"); + + // Let the actor's TabClose handler finish first. + executeSoon(deferred.resolve); + }, false); + + removeTab(gTabA); + return deferred.promise; +} + +function testTabClosed() { + is(onListChangedCount, 3, "onListChanged handler call count"); + + gTabList.getList().then(aTabActors => { + let tabActors = new Set(aTabActors); + is(tabActors.size, 2, "gTabA closed: two tabs in list"); + ok(tabActors.has(gFirstActor), "gTabA closed: initial tab present"); + + info("actors: " + [a.url for (a of tabActors)]); + gActorA = [a for (a of tabActors) if (a !== gFirstActor)][0]; + ok(gActorA.url.match(/^data:text\/html;/), "gTabA closed: new tab URL"); + is(gActorA.title, "JS Debugger BrowserTabList test page", "gTabA closed: new tab title"); + }); +} + +function addTabC() { + return addTab(gTestPage).then(aTab => { + gTabC = aTab; + }); +} + +function testTabC() { + is(onListChangedCount, 4, "onListChanged handler call count"); + + gTabList.getList().then(aTabActors => { + let tabActors = new Set(aTabActors); + is(tabActors.size, 3, "gTabC opened: three tabs in list"); + }); +} + +function removeTabC() { + let deferred = promise.defer(); + + once(gBrowser.tabContainer, "TabClose").then(aEvent => { + ok(aEvent.detail, "This was a tab closed by moving"); + + // Let the actor's TabClose handler finish first. + executeSoon(deferred.resolve); + }, false); + + gNewWindow = gBrowser.replaceTabWithWindow(gTabC); + return deferred.promise; +} + +function testNewWindow() { + is(onListChangedCount, 5, "onListChanged handler call count"); + + return gTabList.getList().then(aTabActors => { + let tabActors = new Set(aTabActors); + is(tabActors.size, 3, "gTabC closed: three tabs in list"); + ok(tabActors.has(gFirstActor), "gTabC closed: initial tab present"); + + info("actors: " + [a.url for (a of tabActors)]); + gActorA = [a for (a of tabActors) if (a !== gFirstActor)][0]; + ok(gActorA.url.match(/^data:text\/html;/), "gTabC closed: new tab URL"); + is(gActorA.title, "JS Debugger BrowserTabList test page", "gTabC closed: new tab title"); + }); +} + +function removeNewWindow() { + let deferred = promise.defer(); + + once(gNewWindow, "unload").then(aEvent => { + ok(!aEvent.detail, "This was a normal window close"); + + // Let the actor's TabClose handler finish first. + executeSoon(deferred.resolve); + }, false); + + gNewWindow.close(); + return deferred.promise; +} + +function testWindowClosed() { + is(onListChangedCount, 6, "onListChanged handler call count"); + + return gTabList.getList().then(aTabActors => { + let tabActors = new Set(aTabActors); + is(tabActors.size, 2, "gNewWindow closed: two tabs in list"); + ok(tabActors.has(gFirstActor), "gNewWindow closed: initial tab present"); + + info("actors: " + [a.url for (a of tabActors)]); + gActorA = [a for (a of tabActors) if (a !== gFirstActor)][0]; + ok(gActorA.url.match(/^data:text\/html;/), "gNewWindow closed: new tab URL"); + is(gActorA.title, "JS Debugger BrowserTabList test page", "gNewWindow closed: new tab title"); + }); +} + +function removeTabB() { + let deferred = promise.defer(); + + once(gBrowser.tabContainer, "TabClose").then(aEvent => { + ok(!aEvent.detail, "This was a normal tab close"); + + // Let the actor's TabClose handler finish first. + executeSoon(deferred.resolve); + }, false); + + removeTab(gTabB); + return deferred.promise; +} + +function finishUp() { + gTabList = gFirstActor = gActorA = gTabA = gTabB = gTabC = gNewWindow = null; + finish(); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_listtabs-03.js b/toolkit/devtools/debugger/test/browser_dbg_listtabs-03.js new file mode 100644 index 000000000..26ccc6837 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_listtabs-03.js @@ -0,0 +1,80 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure the listTabs request works as specified. + */ + +const TAB1_URL = EXAMPLE_URL + "doc_empty-tab-01.html"; + +let gTab1, gTab1Actor, gTab2, gTab2Actor, gClient; + +function listTabs() { + let deferred = promise.defer(); + + gClient.listTabs(aResponse => { + deferred.resolve(aResponse.tabs); + }); + + return deferred.promise; +} + +function request(params) { + let deferred = promise.defer(); + gClient.request(params, deferred.resolve); + return deferred.promise; +} + +function test() { + if (!DebuggerServer.initialized) { + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + } + + let transport = DebuggerServer.connectPipe(); + gClient = new DebuggerClient(transport); + gClient.connect(Task.async(function*(aType, aTraits) { + is(aType, "browser", + "Root actor should identify itself as a browser."); + let tab = yield addTab(TAB1_URL); + + let tabs = yield listTabs(); + is(tabs.length, 2, "Should be two tabs"); + let tabGrip = tabs.filter(a => a.url ==TAB1_URL).pop(); + ok(tabGrip, "Should have an actor for the tab"); + + let response = yield request({ to: tabGrip.actor, type: "attach" }); + is(response.type, "tabAttached", "Should have attached"); + + tabs = yield listTabs(); + + response = yield request({ to: tabGrip.actor, type: "detach" }); + is(response.type, "detached", "Should have detached"); + + let newGrip = tabs.filter(a => a.url ==TAB1_URL).pop(); + is(newGrip.actor, tabGrip.actor, "Should have the same actor for the same tab"); + + response = yield request({ to: tabGrip.actor, type: "attach" }); + is(response.type, "tabAttached", "Should have attached"); + response = yield request({ to: tabGrip.actor, type: "detach" }); + is(response.type, "detached", "Should have detached"); + + yield removeTab(tab); + yield closeConnection(); + finish(); + })); +} + +function closeConnection() { + let deferred = promise.defer(); + gClient.close(deferred.resolve); + return deferred.promise; +} + +registerCleanupFunction(function() { + gTab1 = null; + gTab1Actor = null; + gTab2 = null; + gTab2Actor = null; + gClient = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_location-changes-01-simple.js b/toolkit/devtools/debugger/test/browser_dbg_location-changes-01-simple.js new file mode 100644 index 000000000..17feedb91 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_location-changes-01-simple.js @@ -0,0 +1,69 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that changing the tab location URL works. + */ + +const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html"; + +let gTab, gPanel, gDebugger; +let gEditor, gSources, gFrames; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gSources = gDebugger.DebuggerView.Sources; + gFrames = gDebugger.DebuggerView.StackFrames; + + waitForSourceAndCaretAndScopes(gPanel, ".html", 14).then(performTest); + callInTab(gTab, "simpleCall"); + }); +} + +function performTest() { + is(gDebugger.gThreadClient.state, "paused", + "Should only be getting stack frames while paused."); + + is(gFrames.itemCount, 1, + "Should have only one frame."); + + is(gSources.itemCount, 1, + "Found the expected number of entries in the sources widget."); + + isnot(gSources.selectedValue, null, + "There should be a selected source value."); + isnot(gEditor.getText().length, 0, + "The source editor should have some text displayed."); + isnot(gEditor.getText(), gDebugger.L10N.getStr("loadingText"), + "The source editor text should not be 'Loading...'"); + + is(gDebugger.document.querySelectorAll("#sources .side-menu-widget-empty-notice-container").length, 0, + "The sources widget should not display any notice at this point (1)."); + is(gDebugger.document.querySelectorAll("#sources .side-menu-widget-empty-notice").length, 0, + "The sources widget should not display any notice at this point (2)."); + is(gDebugger.document.querySelector("#sources .side-menu-widget-empty-notice > label"), null, + "The sources widget should not display a notice at this point (3)."); + + gDebugger.gThreadClient.resume(() => { + testLocationChange(); + }); +} + +function testLocationChange() { + navigateActiveTabTo(gPanel, "about:blank", gDebugger.EVENTS.SOURCES_ADDED).then(() => { + closeDebuggerAndFinish(gPanel); + }); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gEditor = null; + gSources = null; + gFrames = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_location-changes-02-blank.js b/toolkit/devtools/debugger/test/browser_dbg_location-changes-02-blank.js new file mode 100644 index 000000000..20a23ca7c --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_location-changes-02-blank.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that changing the tab location URL to a page with no sources works. + */ + +const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html"; + +let gTab, gPanel, gDebugger; +let gEditor, gSources, gFrames; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gSources = gDebugger.DebuggerView.Sources; + gFrames = gDebugger.DebuggerView.StackFrames; + + waitForSourceAndCaretAndScopes(gPanel, ".html", 14).then(testLocationChange); + callInTab(gTab, "simpleCall"); + }); +} + +function testLocationChange() { + navigateActiveTabTo(gPanel, "about:blank", gDebugger.EVENTS.SOURCES_ADDED).then(() => { + isnot(gDebugger.gThreadClient.state, "paused", + "Should not be paused after a tab navigation."); + + is(gFrames.itemCount, 0, + "Should have no frames."); + + is(gSources.itemCount, 0, + "Found no entries in the sources widget."); + + is(gSources.selectedValue, "", + "There should be no selected source value."); + is(gEditor.getText().length, 0, + "The source editor should not have any text displayed."); + + is(gDebugger.document.querySelectorAll("#sources .side-menu-widget-empty-text").length, 1, + "The sources widget should now display a notice (1)."); + is(gDebugger.document.querySelectorAll("#sources .side-menu-widget-empty-text")[0].getAttribute("value"), + gDebugger.L10N.getStr("noSourcesText"), + "The sources widget should now display a notice (2)."); + + closeDebuggerAndFinish(gPanel); + }); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gEditor = null; + gSources = null; + gFrames = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_location-changes-03-new.js b/toolkit/devtools/debugger/test/browser_dbg_location-changes-03-new.js new file mode 100644 index 000000000..b680b2ff3 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_location-changes-03-new.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that changing the tab location URL to a page with other sources works. + */ + +const TAB_URL_1 = EXAMPLE_URL + "doc_recursion-stack.html"; +const TAB_URL_2 = EXAMPLE_URL + "doc_iframes.html"; + +let gTab, gDebuggee, gPanel, gDebugger; +let gEditor, gSources, gFrames; + +function test() { + initDebugger(TAB_URL_1).then(([aTab, aDebuggee, aPanel]) => { + gTab = aTab; + gDebuggee = aDebuggee; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gSources = gDebugger.DebuggerView.Sources; + gFrames = gDebugger.DebuggerView.StackFrames; + + waitForSourceAndCaretAndScopes(gPanel, ".html", 14).then(testLocationChange); + gDebuggee.simpleCall(); + }); +} + +function testLocationChange() { + navigateActiveTabTo(gPanel, TAB_URL_2, gDebugger.EVENTS.SOURCES_ADDED).then(() => { + isnot(gDebugger.gThreadClient.state, "paused", + "Should not be paused after a tab navigation."); + + is(gFrames.itemCount, 0, + "Should have no frames."); + + is(gSources.itemCount, 1, + "Found the expected number of entries in the sources widget."); + + is(getSelectedSourceURL(gSources), EXAMPLE_URL + "doc_inline-debugger-statement.html", + "There should be a selected source value."); + isnot(gEditor.getText().length, 0, + "The source editor should have some text displayed."); + is(gEditor.getText(), gDebugger.L10N.getStr("loadingText"), + "The source editor text should not be 'Loading...'"); + + is(gDebugger.document.querySelectorAll("#sources .side-menu-widget-empty-text").length, 0, + "The sources widget should not display any notice at this point."); + + closeDebuggerAndFinish(gPanel); + }); +} + +registerCleanupFunction(function() { + gTab = null; + gDebuggee = null; + gPanel = null; + gDebugger = null; + gEditor = null; + gSources = null; + gFrames = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_location-changes-04-breakpoint.js b/toolkit/devtools/debugger/test/browser_dbg_location-changes-04-breakpoint.js new file mode 100644 index 000000000..f3f37abd0 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_location-changes-04-breakpoint.js @@ -0,0 +1,201 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that reloading a page with a breakpoint set does not cause it to + * fire more than once. + */ + +const TAB_URL = EXAMPLE_URL + "doc_included-script.html"; +const SOURCE_URL = EXAMPLE_URL + "code_location-changes.js"; + +let gTab, gDebuggee, gPanel, gDebugger; +let gEditor, gSources; + +function test() { + initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => { + gTab = aTab; + gDebuggee = aDebuggee; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gSources = gDebugger.DebuggerView.Sources; + + waitForSourceAndCaretAndScopes(gPanel, ".html", 17).then(addBreakpoint); + gDebuggee.runDebuggerStatement(); + }); +} + +function addBreakpoint() { + waitForSourceAndCaret(gPanel, ".js", 5).then(() => { + ok(true, + "Switched to the desired function when adding a breakpoint " + + "but not passing { noEditorUpdate: true } as an option."); + + testResume(); + }); + + gPanel.addBreakpoint({ actor: getSourceActor(gSources, SOURCE_URL), line: 5 }); +} + +function testResume() { + is(gDebugger.gThreadClient.state, "paused", + "The breakpoint wasn't hit yet (1)."); + is(getSelectedSourceURL(gSources), SOURCE_URL, + "The currently shown source is incorrect (1)."); + ok(isCaretPos(gPanel, 5), + "The source editor caret position is incorrect (1)."); + + gDebugger.gThreadClient.resume(testClick); +} + +function testClick() { + isnot(gDebugger.gThreadClient.state, "paused", + "The breakpoint wasn't hit yet (2)."); + is(getSelectedSourceURL(gSources), SOURCE_URL, + "The currently shown source is incorrect (2)."); + ok(isCaretPos(gPanel, 5), + "The source editor caret position is incorrect (2)."); + + gDebugger.gThreadClient.addOneTimeListener("paused", (aEvent, aPacket) => { + is(aPacket.why.type, "breakpoint", + "Execution has advanced to the breakpoint."); + isnot(aPacket.why.type, "debuggerStatement", + "The breakpoint was hit before the debugger statement."); + + ensureCaretAt(gPanel, 5, 1, true).then(afterBreakpointHit); + }); + + EventUtils.sendMouseEvent({ type: "click" }, + gDebuggee.document.querySelector("button"), + gDebuggee); +} + +function afterBreakpointHit() { + is(gDebugger.gThreadClient.state, "paused", + "The breakpoint was hit (3)."); + is(getSelectedSourceURL(gSources), SOURCE_URL, + "The currently shown source is incorrect (3)."); + ok(isCaretPos(gPanel, 5), + "The source editor caret position is incorrect (3)."); + + gDebugger.gThreadClient.addOneTimeListener("paused", (aEvent, aPacket) => { + is(aPacket.why.type, "debuggerStatement", + "Execution has advanced to the next line."); + isnot(aPacket.why.type, "breakpoint", + "No ghost breakpoint was hit."); + + ensureCaretAt(gPanel, 6, 1, true).then(afterDebuggerStatementHit); + }); + + gDebugger.gThreadClient.resume(); +} + +function afterDebuggerStatementHit() { + is(gDebugger.gThreadClient.state, "paused", + "The debugger statement was hit (4)."); + is(getSelectedSourceURL(gSources), SOURCE_URL, + "The currently shown source is incorrect (4)."); + ok(isCaretPos(gPanel, 6), + "The source editor caret position is incorrect (4)."); + + promise.all([ + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.NEW_SOURCE), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCES_ADDED), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN), + reloadActiveTab(gPanel, gDebugger.EVENTS.BREAKPOINT_SHOWN_IN_EDITOR), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_SHOWN_IN_PANE) + ]).then(testClickAgain); +} + +function testClickAgain() { + isnot(gDebugger.gThreadClient.state, "paused", + "The breakpoint wasn't hit yet (5)."); + is(getSelectedSourceURL(gSources), SOURCE_URL, + "The currently shown source is incorrect (5)."); + ok(isCaretPos(gPanel, 1), + "The source editor caret position is incorrect (5)."); + + gDebugger.gThreadClient.addOneTimeListener("paused", (aEvent, aPacket) => { + is(aPacket.why.type, "breakpoint", + "Execution has advanced to the breakpoint."); + isnot(aPacket.why.type, "debuggerStatement", + "The breakpoint was hit before the debugger statement."); + + ensureCaretAt(gPanel, 5, 1, true).then(afterBreakpointHitAgain); + }); + + EventUtils.sendMouseEvent({ type: "click" }, + gDebuggee.document.querySelector("button"), + gDebuggee); +} + +function afterBreakpointHitAgain() { + is(gDebugger.gThreadClient.state, "paused", + "The breakpoint was hit (6)."); + is(getSelectedSourceURL(gSources), SOURCE_URL, + "The currently shown source is incorrect (6)."); + ok(isCaretPos(gPanel, 5), + "The source editor caret position is incorrect (6)."); + + gDebugger.gThreadClient.addOneTimeListener("paused", (aEvent, aPacket) => { + is(aPacket.why.type, "debuggerStatement", + "Execution has advanced to the next line."); + isnot(aPacket.why.type, "breakpoint", + "No ghost breakpoint was hit."); + + ensureCaretAt(gPanel, 6, 1, true).then(afterDebuggerStatementHitAgain); + }); + + gDebugger.gThreadClient.resume(); +} + +function afterDebuggerStatementHitAgain() { + is(gDebugger.gThreadClient.state, "paused", + "The debugger statement was hit (7)."); + is(getSelectedSourceURL(gSources), SOURCE_URL, + "The currently shown source is incorrect (7)."); + ok(isCaretPos(gPanel, 6), + "The source editor caret position is incorrect (7)."); + + showSecondSource(); +} + +function showSecondSource() { + gDebugger.once(gDebugger.EVENTS.SOURCE_SHOWN, () => { + is(gEditor.getText().indexOf("debugger"), 447, + "The correct source is shown in the source editor.") + is(gEditor.getBreakpoints().length, 0, + "No breakpoints should be shown for the second source."); + + ensureCaretAt(gPanel, 1, 1, true).then(showFirstSourceAgain); + }); + + EventUtils.sendMouseEvent({ type: "mousedown" }, + gDebugger.document.querySelectorAll(".side-menu-widget-item-contents")[1], + gDebugger); +} + +function showFirstSourceAgain() { + gDebugger.once(gDebugger.EVENTS.SOURCE_SHOWN, () => { + is(gEditor.getText().indexOf("debugger"), 148, + "The correct source is shown in the source editor.") + is(gEditor.getBreakpoints().length, 1, + "One breakpoint should be shown for the first source."); + + ensureCaretAt(gPanel, 6, 1, true).then(() => resumeDebuggerThenCloseAndFinish(gPanel)); + }); + + EventUtils.sendMouseEvent({ type: "mousedown" }, + gDebugger.document.querySelectorAll(".side-menu-widget-item-contents")[0], + gDebugger); +} + +registerCleanupFunction(function() { + gTab = null; + gDebuggee = null; + gPanel = null; + gDebugger = null; + gEditor = null; + gSources = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_multiple-windows.js b/toolkit/devtools/debugger/test/browser_dbg_multiple-windows.js new file mode 100644 index 000000000..f842a3fc1 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_multiple-windows.js @@ -0,0 +1,169 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that the debugger attaches to the right tab when multiple windows + * are open. + */ + +const TAB1_URL = EXAMPLE_URL + "doc_script-switching-01.html"; +const TAB2_URL = EXAMPLE_URL + "doc_script-switching-02.html"; + +let gNewTab, gNewWindow; +let gClient; + +function test() { + if (!DebuggerServer.initialized) { + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + } + + let transport = DebuggerServer.connectPipe(); + gClient = new DebuggerClient(transport); + gClient.connect((aType, aTraits) => { + is(aType, "browser", + "Root actor should identify itself as a browser."); + + promise.resolve(null) + .then(() => addTab(TAB1_URL)) + .then(testFirstTab) + .then(() => addWindow(TAB2_URL)) + .then(testNewWindow) + .then(testFocusFirst) + .then(testRemoveTab) + .then(closeConnection) + .then(finish) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); +} + +function testFirstTab(aTab) { + let deferred = promise.defer(); + + gNewTab = aTab; + ok(!!gNewTab, "Second tab created."); + + gClient.listTabs(aResponse => { + let tabActor = aResponse.tabs.filter(aGrip => aGrip.url == TAB1_URL).pop(); + ok(tabActor, + "Should find a tab actor for the first tab."); + + is(aResponse.selected, 1, + "The first tab is selected."); + + deferred.resolve(); + }); + + return deferred.promise; +} + +function testNewWindow(aWindow) { + let deferred = promise.defer(); + + gNewWindow = aWindow; + ok(!!gNewWindow, "Second window created."); + + gNewWindow.focus(); + + let topWindow = Services.wm.getMostRecentWindow("navigator:browser"); + is(topWindow, gNewWindow, + "The second window is on top."); + + let isActive = promise.defer(); + let isLoaded = promise.defer(); + + promise.all([isActive.promise, isLoaded.promise]).then(() => { + gClient.listTabs(aResponse => { + is(aResponse.selected, 2, + "The second tab is selected."); + + deferred.resolve(); + }); + }); + + if (Services.focus.activeWindow != gNewWindow) { + gNewWindow.addEventListener("activate", function onActivate(aEvent) { + if (aEvent.target != gNewWindow) { + return; + } + gNewWindow.removeEventListener("activate", onActivate, true); + isActive.resolve(); + }, true); + } else { + isActive.resolve(); + } + + let contentLocation = gNewWindow.content.location.href; + if (contentLocation != TAB2_URL) { + gNewWindow.document.addEventListener("load", function onLoad(aEvent) { + if (aEvent.target.documentURI != TAB2_URL) { + return; + } + gNewWindow.document.removeEventListener("load", onLoad, true); + isLoaded.resolve(); + }, true); + } else { + isLoaded.resolve(); + } + + return deferred.promise; +} + +function testFocusFirst() { + let deferred = promise.defer(); + + once(window.content, "focus").then(() => { + gClient.listTabs(aResponse => { + is(aResponse.selected, 1, + "The first tab is selected after focusing on it."); + + deferred.resolve(); + }); + }); + + window.content.focus(); + + return deferred.promise; +} + +function testRemoveTab() { + let deferred = promise.defer(); + + gNewWindow.close(); + + // give it time to close + executeSoon(function() { continue_remove_tab(deferred) }); + return deferred.promise; +} + +function continue_remove_tab(deferred) +{ + removeTab(gNewTab); + + gClient.listTabs(aResponse => { + // Verify that tabs are no longer included in listTabs. + let foundTab1 = aResponse.tabs.some(aGrip => aGrip.url == TAB1_URL); + let foundTab2 = aResponse.tabs.some(aGrip => aGrip.url == TAB2_URL); + ok(!foundTab1, "Tab1 should be gone."); + ok(!foundTab2, "Tab2 should be gone."); + + is(aResponse.selected, 0, + "The original tab is selected."); + + deferred.resolve(); + }); +} + +function closeConnection() { + let deferred = promise.defer(); + gClient.close(deferred.resolve); + return deferred.promise; +} + +registerCleanupFunction(function() { + gNewTab = null; + gNewWindow = null; + gClient = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_navigation.js b/toolkit/devtools/debugger/test/browser_dbg_navigation.js new file mode 100644 index 000000000..39f4612c8 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_navigation.js @@ -0,0 +1,73 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check tab attach/navigation. + */ + +const TAB1_URL = EXAMPLE_URL + "doc_empty-tab-01.html"; +const TAB2_URL = EXAMPLE_URL + "doc_empty-tab-02.html"; + +let gClient; + +function test() { + if (!DebuggerServer.initialized) { + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + } + + let transport = DebuggerServer.connectPipe(); + gClient = new DebuggerClient(transport); + gClient.connect((aType, aTraits) => { + is(aType, "browser", + "Root actor should identify itself as a browser."); + + addTab(TAB1_URL) + .then(() => attachTabActorForUrl(gClient, TAB1_URL)) + .then(testNavigate) + .then(testDetach) + .then(finish) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); +} + +function testNavigate([aGrip, aResponse]) { + let outstanding = [promise.defer(), promise.defer()]; + + gClient.addListener("tabNavigated", function onTabNavigated(aEvent, aPacket) { + is(aPacket.url, TAB2_URL, + "Got a tab navigation notification."); + + if (aPacket.state == "start") { + ok(true, "Tab started to navigate."); + outstanding[0].resolve(); + } else { + ok(true, "Tab finished navigating."); + gClient.removeListener("tabNavigated", onTabNavigated); + outstanding[1].resolve(); + } + }); + + gBrowser.selectedBrowser.loadURI(TAB2_URL); + return promise.all(outstanding.map(e => e.promise)) + .then(() => aGrip.actor); +} + +function testDetach(aActor) { + let deferred = promise.defer(); + + gClient.addOneTimeListener("tabDetached", (aType, aPacket) => { + ok(true, "Got a tab detach notification."); + is(aPacket.from, aActor, "tab detach message comes from the expected actor"); + gClient.close(deferred.resolve); + }); + + removeTab(gBrowser.selectedTab); + return deferred.promise; +} + +registerCleanupFunction(function() { + gClient = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_no-page-sources.js b/toolkit/devtools/debugger/test/browser_dbg_no-page-sources.js new file mode 100644 index 000000000..997280f37 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_no-page-sources.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure the right text shows when the page has no sources. + */ + +const TAB_URL = EXAMPLE_URL + "doc_no-page-sources.html"; + +let gTab, gDebuggee, gPanel, gDebugger; +let gEditor, gSources; + +function test() { + initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => { + gTab = aTab; + gDebuggee = aDebuggee; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gSources = gDebugger.DebuggerView.Sources; + + reloadActiveTab(gPanel, gDebugger.EVENTS.SOURCES_ADDED) + .then(testSourcesEmptyText) + .then(() => closeDebuggerAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); +} + +function testSourcesEmptyText() { + is(gSources.itemCount, 0, + "Found no entries in the sources widget."); + + is(gEditor.getText().length, 0, + "The source editor should not have any text displayed."); + + is(gDebugger.document.querySelector("#sources .side-menu-widget-empty-text").getAttribute("value"), + gDebugger.L10N.getStr("noSourcesText"), + "The sources widget should now display 'This page has no sources'."); +} + +registerCleanupFunction(function() { + gTab = null; + gDebuggee = null; + gPanel = null; + gDebugger = null; + gEditor = null; + gSources = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_on-pause-highlight.js b/toolkit/devtools/debugger/test/browser_dbg_on-pause-highlight.js new file mode 100644 index 000000000..d173cacad --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_on-pause-highlight.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that debugger's tab is highlighted when it is paused and not the + * currently selected tool. + */ + +const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html"; + +let gTab, gPanel, gDebugger; +let gToolbox, gToolboxTab; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gToolbox = gPanel._toolbox; + gToolboxTab = gToolbox.doc.getElementById("toolbox-tab-jsdebugger"); + + waitForSourceShown(gPanel, ".html").then(testPause); + }); +} + +function testPause() { + is(gDebugger.gThreadClient.paused, false, + "Should be running after starting test."); + + gDebugger.gThreadClient.addOneTimeListener("paused", () => { + gToolbox.selectTool("webconsole").then(() => { + ok(gToolboxTab.hasAttribute("highlighted") && + gToolboxTab.getAttribute("highlighted") == "true", + "The highlighted class is present"); + ok(!gToolboxTab.hasAttribute("selected") || + gToolboxTab.getAttribute("selected") != "true", + "The tab is not selected"); + }).then(() => gToolbox.selectTool("jsdebugger")).then(() => { + ok(gToolboxTab.hasAttribute("highlighted") && + gToolboxTab.getAttribute("highlighted") == "true", + "The highlighted class is present"); + ok(gToolboxTab.hasAttribute("selected") && + gToolboxTab.getAttribute("selected") == "true", + "...and the tab is selected, so the glow will not be present."); + }).then(testResume); + }); + + EventUtils.sendMouseEvent({ type: "mousedown" }, + gDebugger.document.getElementById("resume"), + gDebugger); +} + +function testResume() { + gDebugger.gThreadClient.addOneTimeListener("resumed", () => { + gToolbox.selectTool("webconsole").then(() => { + ok(!gToolboxTab.classList.contains("highlighted"), + "The highlighted class is not present now after the resume"); + ok(!gToolboxTab.hasAttribute("selected") || + gToolboxTab.getAttribute("selected") != "true", + "The tab is not selected"); + }).then(() => closeDebuggerAndFinish(gPanel)); + }); + + EventUtils.sendMouseEvent({ type: "mousedown" }, + gDebugger.document.getElementById("resume"), + gDebugger); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gToolbox = null; + gToolboxTab = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_on-pause-raise.js b/toolkit/devtools/debugger/test/browser_dbg_on-pause-raise.js new file mode 100644 index 000000000..ccf74c828 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_on-pause-raise.js @@ -0,0 +1,139 @@ +/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the toolbox is raised when the debugger gets paused.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html";
+
+let gTab, gPanel, gDebugger;
+let gFocusedWindow, gToolbox, gToolboxTab;
+
+function test() {
+ initDebugger(TAB_URL).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gToolbox = gPanel._toolbox;
+ gToolboxTab = gToolbox.doc.getElementById("toolbox-tab-jsdebugger");
+
+ waitForSourceShown(gPanel, ".html").then(performTest);
+ });
+}
+
+function performTest() {
+ addTab(TAB_URL).then(aTab => {
+ isnot(aTab, gTab,
+ "The newly added tab is different from the debugger's tab.");
+ is(gBrowser.selectedTab, aTab,
+ "Debugger's tab is not the selected tab.");
+
+ gFocusedWindow = window;
+ testPause();
+ });
+}
+
+function focusMainWindow() {
+ // Make sure toolbox is not focused.
+ window.addEventListener("focus", onFocus, true);
+ info("Focusing main window.")
+
+ // Execute soon to avoid any race conditions between toolbox and main window
+ // getting focused.
+ executeSoon(() => {
+ window.focus();
+ });
+}
+
+function onFocus() {
+ window.removeEventListener("focus", onFocus, true);
+ info("Main window focused.")
+
+ gFocusedWindow = window;
+ testPause();
+}
+
+function testPause() {
+ is(gDebugger.gThreadClient.paused, false,
+ "Should be running after starting the test.");
+
+ is(gFocusedWindow, window,
+ "Main window is the top level window before pause.");
+
+ if (gToolbox.hostType == devtools.Toolbox.HostType.WINDOW) {
+ gToolbox._host._window.onfocus = () => {
+ gFocusedWindow = gToolbox._host._window;
+ };
+ }
+
+ gDebugger.gThreadClient.addOneTimeListener("paused", () => {
+ if (gToolbox.hostType == devtools.Toolbox.HostType.WINDOW) {
+ is(gFocusedWindow, gToolbox._host._window,
+ "Toolbox window is the top level window on pause.");
+ } else {
+ is(gBrowser.selectedTab, gTab,
+ "Debugger's tab got selected.");
+ }
+ gToolbox.selectTool("webconsole").then(() => {
+ ok(gToolboxTab.hasAttribute("highlighted") &&
+ gToolboxTab.getAttribute("highlighted") == "true",
+ "The highlighted class is present");
+ ok(!gToolboxTab.hasAttribute("selected") ||
+ gToolboxTab.getAttribute("selected") != "true",
+ "The tab is not selected");
+ }).then(() => gToolbox.selectTool("jsdebugger")).then(() => {
+ ok(gToolboxTab.hasAttribute("highlighted") &&
+ gToolboxTab.getAttribute("highlighted") == "true",
+ "The highlighted class is present");
+ ok(gToolboxTab.hasAttribute("selected") &&
+ gToolboxTab.getAttribute("selected") == "true",
+ "...and the tab is selected, so the glow will not be present.");
+ }).then(testResume);
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gDebugger.document.getElementById("resume"),
+ gDebugger);
+}
+
+function testResume() {
+ gDebugger.gThreadClient.addOneTimeListener("resumed", () => {
+ gToolbox.selectTool("webconsole").then(() => {
+ ok(!gToolboxTab.hasAttribute("highlighted") ||
+ gToolboxTab.getAttribute("highlighted") != "true",
+ "The highlighted class is not present now after the resume");
+ ok(!gToolboxTab.hasAttribute("selected") ||
+ gToolboxTab.getAttribute("selected") != "true",
+ "The tab is not selected");
+ }).then(maybeEndTest);
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gDebugger.document.getElementById("resume"),
+ gDebugger);
+}
+
+function maybeEndTest() {
+ if (gToolbox.hostType == devtools.Toolbox.HostType.BOTTOM) {
+ info("Switching to a toolbox window host.");
+ gToolbox.switchHost(devtools.Toolbox.HostType.WINDOW).then(focusMainWindow);
+ } else {
+ info("Switching to main window host.");
+ gToolbox.switchHost(devtools.Toolbox.HostType.BOTTOM).then(() => closeDebuggerAndFinish(gPanel));
+ }
+}
+
+registerCleanupFunction(function() {
+ // Revert to the default toolbox host, so that the following tests proceed
+ // normally and not inside a non-default host.
+ Services.prefs.setCharPref("devtools.toolbox.host", devtools.Toolbox.HostType.BOTTOM);
+
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+
+ gFocusedWindow = null;
+ gToolbox = null;
+ gToolboxTab = null;
+});
diff --git a/toolkit/devtools/debugger/test/browser_dbg_optimized-out-vars.js b/toolkit/devtools/debugger/test/browser_dbg_optimized-out-vars.js new file mode 100644 index 000000000..a88f7d04a --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_optimized-out-vars.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that optimized out variables aren't present in the variables view. + +function test() { + Task.spawn(function* () { + const TAB_URL = EXAMPLE_URL + "doc_closure-optimized-out.html"; + let gDebugger, sources; + + let [tab,, panel] = yield initDebugger(TAB_URL); + gDebugger = panel.panelWin; + sources = gDebugger.DebuggerView.Sources; + + yield waitForSourceShown(panel, ".html"); + yield panel.addBreakpoint({ actor: sources.values[0], + line: 18 }); + yield ensureThreadClientState(panel, "resumed"); + + // Spin the event loop before causing the debuggee to pause, to allow + // this function to return first. + sendMouseClickToTab(tab, content.document.querySelector("button")); + + yield waitForDebuggerEvents(panel, gDebugger.EVENTS.FETCHED_SCOPES); + let gVars = gDebugger.DebuggerView.Variables; + let outerScope = gVars.getScopeAtIndex(1); + outerScope.expand(); + + let upvarVar = outerScope.get("upvar"); + ok(upvarVar, "The variable `upvar` is shown."); + is(upvarVar.target.querySelector(".value").getAttribute("value"), + gDebugger.L10N.getStr('variablesViewOptimizedOut'), + "Should show the optimized out message for upvar."); + + let argVar = outerScope.get("arg"); + is(argVar.target.querySelector(".name").getAttribute("value"), "arg", + "Should have the right property name for |arg|."); + is(argVar.target.querySelector(".value").getAttribute("value"), 42, + "Should have the right property value for |arg|."); + + yield resumeDebuggerThenCloseAndFinish(panel); + }).then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_panel-size.js b/toolkit/devtools/debugger/test/browser_dbg_panel-size.js new file mode 100644 index 000000000..8ae8aad6d --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_panel-size.js @@ -0,0 +1,82 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that the sources and instruments panels widths are properly + * remembered when the debugger closes. + */ + +const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html"; + +function test() { + let gTab, gPanel, gDebugger; + let gPrefs, gSources, gInstruments; + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gPrefs = gDebugger.Prefs; + gSources = gDebugger.document.getElementById("sources-pane"); + gInstruments = gDebugger.document.getElementById("instruments-pane"); + + waitForSourceShown(gPanel, ".html").then(performTest); + }); + + function performTest() { + let preferredSw = Services.prefs.getIntPref("devtools.debugger.ui.panes-sources-width"); + let preferredIw = Services.prefs.getIntPref("devtools.debugger.ui.panes-instruments-width"); + let someWidth1, someWidth2; + + do { + someWidth1 = parseInt(Math.random() * 200) + 100; + someWidth2 = parseInt(Math.random() * 300) + 100; + } while ((someWidth1 == preferredSw) || (someWidth2 == preferredIw)); + + info("Preferred sources width: " + preferredSw); + info("Preferred instruments width: " + preferredIw); + info("Generated sources width: " + someWidth1); + info("Generated instruments width: " + someWidth2); + + ok(gPrefs.sourcesWidth, + "The debugger preferences should have a saved sourcesWidth value."); + ok(gPrefs.instrumentsWidth, + "The debugger preferences should have a saved instrumentsWidth value."); + + is(gPrefs.sourcesWidth, preferredSw, + "The debugger preferences should have a correct sourcesWidth value."); + is(gPrefs.instrumentsWidth, preferredIw, + "The debugger preferences should have a correct instrumentsWidth value."); + + is(gSources.getAttribute("width"), gPrefs.sourcesWidth, + "The sources pane width should be the same as the preferred value."); + is(gInstruments.getAttribute("width"), gPrefs.instrumentsWidth, + "The instruments pane width should be the same as the preferred value."); + + gSources.setAttribute("width", someWidth1); + gInstruments.setAttribute("width", someWidth2); + + is(gPrefs.sourcesWidth, preferredSw, + "The sources pane width pref should still be the same as the preferred value."); + is(gPrefs.instrumentsWidth, preferredIw, + "The instruments pane width pref should still be the same as the preferred value."); + + isnot(gSources.getAttribute("width"), gPrefs.sourcesWidth, + "The sources pane width should not be the preferred value anymore."); + isnot(gInstruments.getAttribute("width"), gPrefs.instrumentsWidth, + "The instruments pane width should not be the preferred value anymore."); + + teardown(gPanel).then(() => { + is(gPrefs.sourcesWidth, someWidth1, + "The sources pane width should have been saved by now."); + is(gPrefs.instrumentsWidth, someWidth2, + "The instruments pane width should have been saved by now."); + + // Cleanup after ourselves! + Services.prefs.setIntPref("devtools.debugger.ui.panes-sources-width", preferredSw); + Services.prefs.setIntPref("devtools.debugger.ui.panes-instruments-width", preferredIw); + + finish(); + }); + } +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_parser-01.js b/toolkit/devtools/debugger/test/browser_dbg_parser-01.js new file mode 100644 index 000000000..f2bb87c0a --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_parser-01.js @@ -0,0 +1,31 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check that simple JS can be parsed and cached with the reflection API. + */ + +function test() { + let { Parser } = Cu.import("resource:///modules/devtools/Parser.jsm", {}); + + let source = "let x = 42;"; + let parser = new Parser(); + let first = parser.get(source); + let second = parser.get(source); + + isnot(first, second, + "The two syntax trees should be different."); + + let third = parser.get(source, "url"); + let fourth = parser.get(source, "url"); + + isnot(first, third, + "The new syntax trees should be different than the old ones."); + is(third, fourth, + "The new syntax trees were cached once an identifier was specified."); + + is(parser.errors.length, 0, + "There should be no errors logged when parsing."); + + finish(); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_parser-02.js b/toolkit/devtools/debugger/test/browser_dbg_parser-02.js new file mode 100644 index 000000000..45508d864 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_parser-02.js @@ -0,0 +1,26 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check that syntax errors are reported correctly. + */ + +function test() { + let { Parser } = Cu.import("resource:///modules/devtools/Parser.jsm", {}); + + let source = "let x + 42;"; + let parser = new Parser(); + let parsed = parser.get(source); + + ok(parsed, + "An object should be returned even though the source had a syntax error."); + + is(parser.errors.length, 1, + "There should be one error logged when parsing."); + is(parser.errors[0].name, "SyntaxError", + "The correct exception was caught."); + is(parser.errors[0].message, "missing ; before statement", + "The correct exception was caught."); + + finish(); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_parser-03.js b/toolkit/devtools/debugger/test/browser_dbg_parser-03.js new file mode 100644 index 000000000..a4c0fce91 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_parser-03.js @@ -0,0 +1,77 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check that JS inside HTML can be separated and parsed correctly. + */ + +function test() { + let { Parser } = Cu.import("resource:///modules/devtools/Parser.jsm", {}); + + let source = [ + "<!doctype html>", + "<head>", + "<script>", + "let a = 42;", + "</script>", + "<script type='text/javascript'>", + "let b = 42;", + "</script>", + "<script type='text/javascript;version=1.8'>", + "let c = 42;", + "</script>", + "</head>" + ].join("\n"); + let parser = new Parser(); + let parsed = parser.get(source); + + ok(parsed, + "HTML code should be parsed correctly."); + is(parser.errors.length, 0, + "There should be no errors logged when parsing."); + + is(parsed.scriptCount, 3, + "There should be 3 scripts parsed in the parent HTML source."); + + is(parsed.getScriptInfo(0).toSource(), "({start:-1, length:-1, index:-1})", + "There is no script at the beginning of the parent source."); + is(parsed.getScriptInfo(source.length - 1).toSource(), "({start:-1, length:-1, index:-1})", + "There is no script at the end of the parent source."); + + is(parsed.getScriptInfo(source.indexOf("let a")).toSource(), "({start:31, length:13, index:0})", + "The first script was located correctly."); + is(parsed.getScriptInfo(source.indexOf("let b")).toSource(), "({start:85, length:13, index:1})", + "The second script was located correctly."); + is(parsed.getScriptInfo(source.indexOf("let c")).toSource(), "({start:151, length:13, index:2})", + "The third script was located correctly."); + + is(parsed.getScriptInfo(source.indexOf("let a") - 1).toSource(), "({start:31, length:13, index:0})", + "The left edge of the first script was interpreted correctly."); + is(parsed.getScriptInfo(source.indexOf("let b") - 1).toSource(), "({start:85, length:13, index:1})", + "The left edge of the second script was interpreted correctly."); + is(parsed.getScriptInfo(source.indexOf("let c") - 1).toSource(), "({start:151, length:13, index:2})", + "The left edge of the third script was interpreted correctly."); + + is(parsed.getScriptInfo(source.indexOf("let a") - 2).toSource(), "({start:-1, length:-1, index:-1})", + "The left outside of the first script was interpreted correctly."); + is(parsed.getScriptInfo(source.indexOf("let b") - 2).toSource(), "({start:-1, length:-1, index:-1})", + "The left outside of the second script was interpreted correctly."); + is(parsed.getScriptInfo(source.indexOf("let c") - 2).toSource(), "({start:-1, length:-1, index:-1})", + "The left outside of the third script was interpreted correctly."); + + is(parsed.getScriptInfo(source.indexOf("let a") + 12).toSource(), "({start:31, length:13, index:0})", + "The right edge of the first script was interpreted correctly."); + is(parsed.getScriptInfo(source.indexOf("let b") + 12).toSource(), "({start:85, length:13, index:1})", + "The right edge of the second script was interpreted correctly."); + is(parsed.getScriptInfo(source.indexOf("let c") + 12).toSource(), "({start:151, length:13, index:2})", + "The right edge of the third script was interpreted correctly."); + + is(parsed.getScriptInfo(source.indexOf("let a") + 13).toSource(), "({start:-1, length:-1, index:-1})", + "The right outside of the first script was interpreted correctly."); + is(parsed.getScriptInfo(source.indexOf("let b") + 13).toSource(), "({start:-1, length:-1, index:-1})", + "The right outside of the second script was interpreted correctly."); + is(parsed.getScriptInfo(source.indexOf("let c") + 13).toSource(), "({start:-1, length:-1, index:-1})", + "The right outside of the third script was interpreted correctly."); + + finish(); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_parser-04.js b/toolkit/devtools/debugger/test/browser_dbg_parser-04.js new file mode 100644 index 000000000..2ef653bf3 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_parser-04.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check that faulty JS inside HTML can be separated and identified correctly. + */ + +function test() { + let { Parser } = Cu.import("resource:///modules/devtools/Parser.jsm", {}); + + let source = [ + "<!doctype html>", + "<head>", + "<SCRIPT>", + "let a + 42;", + "</SCRIPT>", + "<script type='text/javascript'>", + "let b = 42;", + "</SCRIPT>", + "<script type='text/javascript;version=1.8'>", + "let c + 42;", + "</SCRIPT>", + "</head>" + ].join("\n"); + let parser = new Parser(); + let parsed = parser.get(source); + + ok(parsed, + "HTML code should be parsed correctly."); + is(parser.errors.length, 2, + "There should be two errors logged when parsing."); + + is(parser.errors[0].name, "SyntaxError", + "The correct first exception was caught."); + is(parser.errors[0].message, "missing ; before statement", + "The correct first exception was caught."); + + is(parser.errors[1].name, "SyntaxError", + "The correct second exception was caught."); + is(parser.errors[1].message, "missing ; before statement", + "The correct second exception was caught."); + + is(parsed.scriptCount, 1, + "There should be 1 script parsed in the parent HTML source."); + + is(parsed.getScriptInfo(source.indexOf("let a")).toSource(), "({start:-1, length:-1, index:-1})", + "The first script shouldn't be considered valid."); + is(parsed.getScriptInfo(source.indexOf("let b")).toSource(), "({start:85, length:13, index:0})", + "The second script was located correctly."); + is(parsed.getScriptInfo(source.indexOf("let c")).toSource(), "({start:-1, length:-1, index:-1})", + "The third script shouldn't be considered valid."); + + finish(); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_parser-05.js b/toolkit/devtools/debugger/test/browser_dbg_parser-05.js new file mode 100644 index 000000000..4c1d935a2 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_parser-05.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check that JS code containing strings that might look like <script> tags + * inside an HTML source is parsed correctly. + */ + +function test() { + let { Parser } = Cu.import("resource:///modules/devtools/Parser.jsm", {}); + + let source = [ + "let a = [];", + "a.push('<script>');", + "a.push('var a = 42;');", + "a.push('</script>');", + "a.push('<script type=\"text/javascript\">');", + "a.push('var b = 42;');", + "a.push('</script>');", + "a.push('<script type=\"text/javascript;version=1.8\">');", + "a.push('var c = 42;');", + "a.push('</script>');" + ].join("\n"); + let parser = new Parser(); + let parsed = parser.get(source); + + ok(parsed, + "The javascript code should be parsed correctly."); + is(parser.errors.length, 0, + "There should be no errors logged when parsing."); + + is(parsed.scriptCount, 1, + "There should be 1 script parsed in the parent source."); + + is(parsed.getScriptInfo(source.indexOf("let a")).toSource(), "({start:0, length:261, index:0})", + "The script location is correct (1)."); + is(parsed.getScriptInfo(source.indexOf("<script>")).toSource(), "({start:0, length:261, index:0})", + "The script location is correct (2)."); + is(parsed.getScriptInfo(source.indexOf("</script>")).toSource(), "({start:0, length:261, index:0})", + "The script location is correct (3)."); + + finish(); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_parser-06.js b/toolkit/devtools/debugger/test/browser_dbg_parser-06.js new file mode 100644 index 000000000..cab13235e --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_parser-06.js @@ -0,0 +1,78 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check that some potentially problematic identifier nodes have the + * right location information attached. + */ + +function test() { + let { Parser, ParserHelpers, SyntaxTreeVisitor } = + Cu.import("resource:///modules/devtools/Parser.jsm", {}); + + function verify(source, predicate, [sline, scol], [eline, ecol]) { + let ast = Parser.reflectionAPI.parse(source); + let node = SyntaxTreeVisitor.filter(ast, predicate).pop(); + let loc = ParserHelpers.getNodeLocation(node); + + is(loc.start.toSource(), { line: sline, column: scol }.toSource(), + "The start location was correct for the identifier in: '" + source + "'."); + is(loc.end.toSource(), { line: eline, column: ecol }.toSource(), + "The end location was correct for the identifier in: '" + source + "'."); + } + + // FunctionDeclarations and FunctionExpressions. + + // The location is unavailable for the identifier node "foo". + verify("function foo(){}", e => e.name == "foo", [1, 9], [1, 12]); + verify("\nfunction\nfoo\n(\n)\n{\n}\n", e => e.name == "foo", [3, 0], [3, 3]); + + verify("({bar:function foo(){}})", e => e.name == "foo", [1, 15], [1, 18]); + verify("(\n{\nbar\n:\nfunction\nfoo\n(\n)\n{\n}\n}\n)", e => e.name == "foo", [6, 0], [6, 3]); + + // Just to be sure, check the identifier node "bar" as well. + verify("({bar:function foo(){}})", e => e.name == "bar", [1, 2], [1, 5]); + verify("(\n{\nbar\n:\nfunction\nfoo\n(\n)\n{\n}\n}\n)", e => e.name == "bar", [3, 0], [3, 3]); + + // MemberExpressions. + + // The location is unavailable for the identifier node "bar". + verify("foo.bar", e => e.name == "bar", [1, 4], [1, 7]); + verify("\nfoo\n.\nbar\n", e => e.name == "bar", [4, 0], [4, 3]); + + // Just to be sure, check the identifier node "foo" as well. + verify("foo.bar", e => e.name == "foo", [1, 0], [1, 3]); + verify("\nfoo\n.\nbar\n", e => e.name == "foo", [2, 0], [2, 3]); + + // VariableDeclarator + + // The location is incorrect for the identifier node "foo". + verify("let foo = bar", e => e.name == "foo", [1, 4], [1, 7]); + verify("\nlet\nfoo\n=\nbar\n", e => e.name == "foo", [3, 0], [3, 3]); + + // Just to be sure, check the identifier node "bar" as well. + verify("let foo = bar", e => e.name == "bar", [1, 10], [1, 13]); + verify("\nlet\nfoo\n=\nbar\n", e => e.name == "bar", [5, 0], [5, 3]); + + // Just to be sure, check AssignmentExpreesions as well. + verify("foo = bar", e => e.name == "foo", [1, 0], [1, 3]); + verify("\nfoo\n=\nbar\n", e => e.name == "foo", [2, 0], [2, 3]); + verify("foo = bar", e => e.name == "bar", [1, 6], [1, 9]); + verify("\nfoo\n=\nbar\n", e => e.name == "bar", [4, 0], [4, 3]); + + // LabeledStatement, BreakStatement and ContinueStatement, because it's 1968 again + + verify("foo: bar", e => e.name == "foo", [1, 0], [1, 3]); + verify("\nfoo\n:\nbar\n", e => e.name == "foo", [2, 0], [2, 3]); + + verify("foo: for(;;) break foo", e => e.name == "foo", [1, 19], [1, 22]); + verify("\nfoo\n:\nfor(\n;\n;\n)\nbreak\nfoo\n", e => e.name == "foo", [9, 0], [9, 3]); + + verify("foo: bar", e => e.name == "foo", [1, 0], [1, 3]); + verify("\nfoo\n:\nbar\n", e => e.name == "foo", [2, 0], [2, 3]); + + verify("foo: for(;;) continue foo", e => e.name == "foo", [1, 22], [1, 25]); + verify("\nfoo\n:\nfor(\n;\n;\n)\ncontinue\nfoo\n", e => e.name == "foo", [9, 0], [9, 3]); + + finish(); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_parser-07.js b/toolkit/devtools/debugger/test/browser_dbg_parser-07.js new file mode 100644 index 000000000..099c16301 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_parser-07.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check that nodes with locaiton information attached can be properly + * verified for containing lines and columns. + */ + +function test() { + let { ParserHelpers } = Cu.import("resource:///modules/devtools/Parser.jsm", {}); + + let node1 = { loc: { + start: { line: 1, column: 10 }, + end: { line: 10, column: 1 } + }}; + let node2 = { loc: { + start: { line: 1, column: 10 }, + end: { line: 1, column: 20 } + }}; + + ok(ParserHelpers.nodeContainsLine(node1, 1), "1st check."); + ok(ParserHelpers.nodeContainsLine(node1, 5), "2nd check."); + ok(ParserHelpers.nodeContainsLine(node1, 10), "3rd check."); + + ok(!ParserHelpers.nodeContainsLine(node1, 0), "4th check."); + ok(!ParserHelpers.nodeContainsLine(node1, 11), "5th check."); + + ok(ParserHelpers.nodeContainsLine(node2, 1), "6th check."); + ok(!ParserHelpers.nodeContainsLine(node2, 0), "7th check."); + ok(!ParserHelpers.nodeContainsLine(node2, 2), "8th check."); + + ok(!ParserHelpers.nodeContainsPoint(node1, 1, 10), "9th check."); + ok(!ParserHelpers.nodeContainsPoint(node1, 10, 1), "10th check."); + + ok(!ParserHelpers.nodeContainsPoint(node1, 0, 10), "11th check."); + ok(!ParserHelpers.nodeContainsPoint(node1, 11, 1), "12th check."); + + ok(!ParserHelpers.nodeContainsPoint(node1, 1, 9), "13th check."); + ok(!ParserHelpers.nodeContainsPoint(node1, 10, 2), "14th check."); + + ok(ParserHelpers.nodeContainsPoint(node2, 1, 10), "15th check."); + ok(ParserHelpers.nodeContainsPoint(node2, 1, 15), "16th check."); + ok(ParserHelpers.nodeContainsPoint(node2, 1, 20), "17th check."); + + ok(!ParserHelpers.nodeContainsPoint(node2, 0, 10), "18th check."); + ok(!ParserHelpers.nodeContainsPoint(node2, 2, 20), "19th check."); + + ok(!ParserHelpers.nodeContainsPoint(node2, 0, 9), "20th check."); + ok(!ParserHelpers.nodeContainsPoint(node2, 2, 21), "21th check."); + + ok(!ParserHelpers.nodeContainsPoint(node2, 1, 9), "22th check."); + ok(!ParserHelpers.nodeContainsPoint(node2, 1, 21), "23th check."); + + finish(); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_parser-08.js b/toolkit/devtools/debugger/test/browser_dbg_parser-08.js new file mode 100644 index 000000000..0286d9be2 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_parser-08.js @@ -0,0 +1,289 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that inferring anonymous function information is done correctly. + */ + +function test() { + let { Parser, ParserHelpers, SyntaxTreeVisitor } = + Cu.import("resource:///modules/devtools/Parser.jsm", {}); + + function verify(source, predicate, details) { + let { name, chain } = details; + let [[sline, scol], [eline, ecol]] = details.loc; + let ast = Parser.reflectionAPI.parse(source); + let node = SyntaxTreeVisitor.filter(ast, predicate).pop(); + let info = ParserHelpers.inferFunctionExpressionInfo(node); + + is(info.name, name, + "The function expression assignment property name is correct."); + is(chain ? info.chain.toSource() : info.chain, chain ? chain.toSource() : chain, + "The function expression assignment property chain is correct."); + is(info.loc.start.toSource(), { line: sline, column: scol }.toSource(), + "The start location was correct for the identifier in: '" + source + "'."); + is(info.loc.end.toSource(), { line: eline, column: ecol }.toSource(), + "The end location was correct for the identifier in: '" + source + "'."); + } + + // VariableDeclarator + + verify("var foo=function(){}", e => e.type == "FunctionExpression", { + name: "foo", + chain: null, + loc: [[1, 4], [1, 7]] + }); + verify("\nvar\nfoo\n=\nfunction\n(\n)\n{\n}\n", e => e.type == "FunctionExpression", { + name: "foo", + chain: null, + loc: [[3, 0], [3, 3]] + }); + + // AssignmentExpression + + verify("foo=function(){}", e => e.type == "FunctionExpression", + { name: "foo", chain: [], loc: [[1, 0], [1, 3]] }); + + verify("\nfoo\n=\nfunction\n(\n)\n{\n}\n", e => e.type == "FunctionExpression", + { name: "foo", chain: [], loc: [[2, 0], [2, 3]] }); + + verify("foo.bar=function(){}", e => e.type == "FunctionExpression", + { name: "bar", chain: ["foo"], loc: [[1, 0], [1, 7]] }); + + verify("\nfoo.bar\n=\nfunction\n(\n)\n{\n}\n", e => e.type == "FunctionExpression", + { name: "bar", chain: ["foo"], loc: [[2, 0], [2, 7]] }); + + verify("this.foo=function(){}", e => e.type == "FunctionExpression", + { name: "foo", chain: ["this"], loc: [[1, 0], [1, 8]] }); + + verify("\nthis.foo\n=\nfunction\n(\n)\n{\n}\n", e => e.type == "FunctionExpression", + { name: "foo", chain: ["this"], loc: [[2, 0], [2, 8]] }); + + verify("this.foo.bar=function(){}", e => e.type == "FunctionExpression", + { name: "bar", chain: ["this", "foo"], loc: [[1, 0], [1, 12]] }); + + verify("\nthis.foo.bar\n=\nfunction\n(\n)\n{\n}\n", e => e.type == "FunctionExpression", + { name: "bar", chain: ["this", "foo"], loc: [[2, 0], [2, 12]] }); + + verify("foo.this.bar=function(){}", e => e.type == "FunctionExpression", + { name: "bar", chain: ["foo", "this"], loc: [[1, 0], [1, 12]] }); + + verify("\nfoo.this.bar\n=\nfunction\n(\n)\n{\n}\n", e => e.type == "FunctionExpression", + { name: "bar", chain: ["foo", "this"], loc: [[2, 0], [2, 12]] }); + + // ObjectExpression + + verify("({foo:function(){}})", e => e.type == "FunctionExpression", + { name: "foo", chain: [], loc: [[1, 2], [1, 5]] }); + + verify("(\n{\nfoo\n:\nfunction\n(\n)\n{\n}\n}\n)", e => e.type == "FunctionExpression", + { name: "foo", chain: [], loc: [[3, 0], [3, 3]] }); + + verify("({foo:{bar:function(){}}})", e => e.type == "FunctionExpression", + { name: "bar", chain: ["foo"], loc: [[1, 7], [1, 10]] }); + + verify("(\n{\nfoo\n:\n{\nbar\n:\nfunction\n(\n)\n{\n}\n}\n}\n)", e => e.type == "FunctionExpression", + { name: "bar", chain: ["foo"], loc: [[6, 0], [6, 3]] }); + + // AssignmentExpression + ObjectExpression + + verify("foo={bar:function(){}}", e => e.type == "FunctionExpression", + { name: "bar", chain: ["foo"], loc: [[1, 5], [1, 8]] }); + + verify("\nfoo\n=\n{\nbar\n:\nfunction\n(\n)\n{\n}\n}\n", e => e.type == "FunctionExpression", + { name: "bar", chain: ["foo"], loc: [[5, 0], [5, 3]] }); + + verify("foo={bar:{baz:function(){}}}", e => e.type == "FunctionExpression", + { name: "baz", chain: ["foo", "bar"], loc: [[1, 10], [1, 13]] }); + + verify("\nfoo\n=\n{\nbar\n:\n{\nbaz\n:\nfunction\n(\n)\n{\n}\n}\n}\n", e => e.type == "FunctionExpression", + { name: "baz", chain: ["foo", "bar"], loc: [[8, 0], [8, 3]] }); + + verify("nested.foo={bar:function(){}}", e => e.type == "FunctionExpression", + { name: "bar", chain: ["nested", "foo"], loc: [[1, 12], [1, 15]] }); + + verify("\nnested.foo\n=\n{\nbar\n:\nfunction\n(\n)\n{\n}\n}\n", e => e.type == "FunctionExpression", + { name: "bar", chain: ["nested", "foo"], loc: [[5, 0], [5, 3]] }); + + verify("nested.foo={bar:{baz:function(){}}}", e => e.type == "FunctionExpression", + { name: "baz", chain: ["nested", "foo", "bar"], loc: [[1, 17], [1, 20]] }); + + verify("\nnested.foo\n=\n{\nbar\n:\n{\nbaz\n:\nfunction\n(\n)\n{\n}\n}\n}\n", e => e.type == "FunctionExpression", + { name: "baz", chain: ["nested", "foo", "bar"], loc: [[8, 0], [8, 3]] }); + + verify("this.foo={bar:function(){}}", e => e.type == "FunctionExpression", + { name: "bar", chain: ["this", "foo"], loc: [[1, 10], [1, 13]] }); + + verify("\nthis.foo\n=\n{\nbar\n:\nfunction\n(\n)\n{\n}\n}\n", e => e.type == "FunctionExpression", + { name: "bar", chain: ["this", "foo"], loc: [[5, 0], [5, 3]] }); + + verify("this.foo={bar:{baz:function(){}}}", e => e.type == "FunctionExpression", + { name: "baz", chain: ["this", "foo", "bar"], loc: [[1, 15], [1, 18]] }); + + verify("\nthis.foo\n=\n{\nbar\n:\n{\nbaz\n:\nfunction\n(\n)\n{\n}\n}\n}\n", e => e.type == "FunctionExpression", + { name: "baz", chain: ["this", "foo", "bar"], loc: [[8, 0], [8, 3]] }); + + verify("this.nested.foo={bar:function(){}}", e => e.type == "FunctionExpression", + { name: "bar", chain: ["this", "nested", "foo"], loc: [[1, 17], [1, 20]] }); + + verify("\nthis.nested.foo\n=\n{\nbar\n:\nfunction\n(\n)\n{\n}\n}\n", e => e.type == "FunctionExpression", + { name: "bar", chain: ["this", "nested", "foo"], loc: [[5, 0], [5, 3]] }); + + verify("this.nested.foo={bar:{baz:function(){}}}", e => e.type == "FunctionExpression", + { name: "baz", chain: ["this", "nested", "foo", "bar"], loc: [[1, 22], [1, 25]] }); + + verify("\nthis.nested.foo\n=\n{\nbar\n:\n{\nbaz\n:\nfunction\n(\n)\n{\n}\n}\n}\n", e => e.type == "FunctionExpression", + { name: "baz", chain: ["this", "nested", "foo", "bar"], loc: [[8, 0], [8, 3]] }); + + verify("nested.this.foo={bar:function(){}}", e => e.type == "FunctionExpression", + { name: "bar", chain: ["nested", "this", "foo"], loc: [[1, 17], [1, 20]] }); + + verify("\nnested.this.foo\n=\n{\nbar\n:\nfunction\n(\n)\n{\n}\n}\n", e => e.type == "FunctionExpression", + { name: "bar", chain: ["nested", "this", "foo"], loc: [[5, 0], [5, 3]] }); + + verify("nested.this.foo={bar:{baz:function(){}}}", e => e.type == "FunctionExpression", + { name: "baz", chain: ["nested", "this", "foo", "bar"], loc: [[1, 22], [1, 25]] }); + + verify("\nnested.this.foo\n=\n{\nbar\n:\n{\nbaz\n:\nfunction\n(\n)\n{\n}\n}\n}\n", e => e.type == "FunctionExpression", + { name: "baz", chain: ["nested", "this", "foo", "bar"], loc: [[8, 0], [8, 3]] }); + + // VariableDeclarator + AssignmentExpression + ObjectExpression + + verify("let foo={bar:function(){}}", e => e.type == "FunctionExpression", + { name: "bar", chain: ["foo"], loc: [[1, 9], [1, 12]] }); + + verify("\nlet\nfoo\n=\n{\nbar\n:\nfunction\n(\n)\n{\n}\n}\n", e => e.type == "FunctionExpression", + { name: "bar", chain: ["foo"], loc: [[6, 0], [6, 3]] }); + + verify("let foo={bar:{baz:function(){}}}", e => e.type == "FunctionExpression", + { name: "baz", chain: ["foo", "bar"], loc: [[1, 14], [1, 17]] }); + + verify("\nlet\nfoo\n=\n{\nbar\n:\n{\nbaz\n:\nfunction\n(\n)\n{\n}\n}\n}\n", e => e.type == "FunctionExpression", + { name: "baz", chain: ["foo", "bar"], loc: [[9, 0], [9, 3]] }); + + // New/CallExpression + AssignmentExpression + ObjectExpression + + verify("foo({bar:function(){}})", e => e.type == "FunctionExpression", + { name: "bar", chain: [], loc: [[1, 5], [1, 8]] }); + + verify("\nfoo\n(\n{\nbar\n:\nfunction\n(\n)\n{\n}\n}\n)\n", e => e.type == "FunctionExpression", + { name: "bar", chain: [], loc: [[5, 0], [5, 3]] }); + + verify("foo({bar:{baz:function(){}}})", e => e.type == "FunctionExpression", + { name: "baz", chain: ["bar"], loc: [[1, 10], [1, 13]] }); + + verify("\nfoo\n(\n{\nbar\n:\n{\nbaz\n:\nfunction\n(\n)\n{\n}\n}\n}\n)\n", e => e.type == "FunctionExpression", + { name: "baz", chain: ["bar"], loc: [[8, 0], [8, 3]] }); + + verify("nested.foo({bar:function(){}})", e => e.type == "FunctionExpression", + { name: "bar", chain: [], loc: [[1, 12], [1, 15]] }); + + verify("\nnested.foo\n(\n{\nbar\n:\nfunction\n(\n)\n{\n}\n}\n)\n", e => e.type == "FunctionExpression", + { name: "bar", chain: [], loc: [[5, 0], [5, 3]] }); + + verify("nested.foo({bar:{baz:function(){}}})", e => e.type == "FunctionExpression", + { name: "baz", chain: ["bar"], loc: [[1, 17], [1, 20]] }); + + verify("\nnested.foo\n(\n{\nbar\n:\n{\nbaz\n:\nfunction\n(\n)\n{\n}\n}\n}\n)\n", e => e.type == "FunctionExpression", + { name: "baz", chain: ["bar"], loc: [[8, 0], [8, 3]] }); + + verify("this.foo({bar:function(){}})", e => e.type == "FunctionExpression", + { name: "bar", chain: [], loc: [[1, 10], [1, 13]] }); + + verify("\nthis.foo\n(\n{\nbar\n:\nfunction\n(\n)\n{\n}\n}\n)\n", e => e.type == "FunctionExpression", + { name: "bar", chain: [], loc: [[5, 0], [5, 3]] }); + + verify("this.foo({bar:{baz:function(){}}})", e => e.type == "FunctionExpression", + { name: "baz", chain: ["bar"], loc: [[1, 15], [1, 18]] }); + + verify("\nthis.foo\n(\n{\nbar\n:\n{\nbaz\n:\nfunction\n(\n)\n{\n}\n}\n}\n)\n", e => e.type == "FunctionExpression", + { name: "baz", chain: ["bar"], loc: [[8, 0], [8, 3]] }); + + verify("this.nested.foo({bar:function(){}})", e => e.type == "FunctionExpression", + { name: "bar", chain: [], loc: [[1, 17], [1, 20]] }); + + verify("\nthis.nested.foo\n(\n{\nbar\n:\nfunction\n(\n)\n{\n}\n}\n)\n", e => e.type == "FunctionExpression", + { name: "bar", chain: [], loc: [[5, 0], [5, 3]] }); + + verify("this.nested.foo({bar:{baz:function(){}}})", e => e.type == "FunctionExpression", + { name: "baz", chain: ["bar"], loc: [[1, 22], [1, 25]] }); + + verify("\nthis.nested.foo\n(\n{\nbar\n:\n{\nbaz\n:\nfunction\n(\n)\n{\n}\n}\n}\n)\n", e => e.type == "FunctionExpression", + { name: "baz", chain: ["bar"], loc: [[8, 0], [8, 3]] }); + + verify("nested.this.foo({bar:function(){}})", e => e.type == "FunctionExpression", + { name: "bar", chain: [], loc: [[1, 17], [1, 20]] }); + + verify("\nnested.this.foo\n(\n{\nbar\n:\nfunction\n(\n)\n{\n}\n}\n)\n", e => e.type == "FunctionExpression", + { name: "bar", chain: [], loc: [[5, 0], [5, 3]] }); + + verify("nested.this.foo({bar:{baz:function(){}}})", e => e.type == "FunctionExpression", + { name: "baz", chain: ["bar"], loc: [[1, 22], [1, 25]] }); + + verify("\nnested.this.foo\n(\n{\nbar\n:\n{\nbaz\n:\nfunction\n(\n)\n{\n}\n}\n}\n)\n", e => e.type == "FunctionExpression", + { name: "baz", chain: ["bar"], loc: [[8, 0], [8, 3]] }); + + // New/CallExpression + VariableDeclarator + AssignmentExpression + ObjectExpression + + verify("let target=foo({bar:function(){}})", e => e.type == "FunctionExpression", + { name: "bar", chain: ["target"], loc: [[1, 16], [1, 19]] }); + + verify("\nlet\ntarget=\nfoo\n(\n{\nbar\n:\nfunction\n(\n)\n{\n}\n}\n)\n", e => e.type == "FunctionExpression", + { name: "bar", chain: ["target"], loc: [[7, 0], [7, 3]] }); + + verify("let target=foo({bar:{baz:function(){}}})", e => e.type == "FunctionExpression", + { name: "baz", chain: ["target", "bar"], loc: [[1, 21], [1, 24]] }); + + verify("\nlet\ntarget=\nfoo\n(\n{\nbar\n:\n{\nbaz\n:\nfunction\n(\n)\n{\n}\n}\n}\n)\n", e => e.type == "FunctionExpression", + { name: "baz", chain: ["target", "bar"], loc: [[10, 0], [10, 3]] }); + + verify("let target=nested.foo({bar:function(){}})", e => e.type == "FunctionExpression", + { name: "bar", chain: ["target"], loc: [[1, 23], [1, 26]] }); + + verify("\nlet\ntarget=\nnested.foo\n(\n{\nbar\n:\nfunction\n(\n)\n{\n}\n}\n)\n", e => e.type == "FunctionExpression", + { name: "bar", chain: ["target"], loc: [[7, 0], [7, 3]] }); + + verify("let target=nested.foo({bar:{baz:function(){}}})", e => e.type == "FunctionExpression", + { name: "baz", chain: ["target", "bar"], loc: [[1, 28], [1, 31]] }); + + verify("\nlet\ntarget=\nnested.foo\n(\n{\nbar\n:\n{\nbaz\n:\nfunction\n(\n)\n{\n}\n}\n}\n)\n", e => e.type == "FunctionExpression", + { name: "baz", chain: ["target", "bar"], loc: [[10, 0], [10, 3]] }); + + verify("let target=this.foo({bar:function(){}})", e => e.type == "FunctionExpression", + { name: "bar", chain: ["target"], loc: [[1, 21], [1, 24]] }); + + verify("\nlet\ntarget=\nthis.foo\n(\n{\nbar\n:\nfunction\n(\n)\n{\n}\n}\n)\n", e => e.type == "FunctionExpression", + { name: "bar", chain: ["target"], loc: [[7, 0], [7, 3]] }); + + verify("let target=this.foo({bar:{baz:function(){}}})", e => e.type == "FunctionExpression", + { name: "baz", chain: ["target", "bar"], loc: [[1, 26], [1, 29]] }); + + verify("\nlet\ntarget=\nthis.foo\n(\n{\nbar\n:\n{\nbaz\n:\nfunction\n(\n)\n{\n}\n}\n}\n)\n", e => e.type == "FunctionExpression", + { name: "baz", chain: ["target", "bar"], loc: [[10, 0], [10, 3]] }); + + verify("let target=this.nested.foo({bar:function(){}})", e => e.type == "FunctionExpression", + { name: "bar", chain: ["target"], loc: [[1, 28], [1, 31]] }); + + verify("\nlet\ntarget=\nthis.nested.foo\n(\n{\nbar\n:\nfunction\n(\n)\n{\n}\n}\n)\n", e => e.type == "FunctionExpression", + { name: "bar", chain: ["target"], loc: [[7, 0], [7, 3]] }); + + verify("let target=this.nested.foo({bar:{baz:function(){}}})", e => e.type == "FunctionExpression", + { name: "baz", chain: ["target", "bar"], loc: [[1, 33], [1, 36]] }); + + verify("\nlet\ntarget=\nthis.nested.foo\n(\n{\nbar\n:\n{\nbaz\n:\nfunction\n(\n)\n{\n}\n}\n}\n)\n", e => e.type == "FunctionExpression", + { name: "baz", chain: ["target", "bar"], loc: [[10, 0], [10, 3]] }); + + verify("let target=nested.this.foo({bar:function(){}})", e => e.type == "FunctionExpression", + { name: "bar", chain: ["target"], loc: [[1, 28], [1, 31]] }); + + verify("\nlet\ntarget=\nnested.this.foo\n(\n{\nbar\n:\nfunction\n(\n)\n{\n}\n}\n)\n", e => e.type == "FunctionExpression", + { name: "bar", chain: ["target"], loc: [[7, 0], [7, 3]] }); + + verify("let target=nested.this.foo({bar:{baz:function(){}}})", e => e.type == "FunctionExpression", + { name: "baz", chain: ["target", "bar"], loc: [[1, 33], [1, 36]] }); + + verify("\nlet\ntarget=\nnested.this.foo\n(\n{\nbar\n:\n{\nbaz\n:\nfunction\n(\n)\n{\n}\n}\n}\n)\n", e => e.type == "FunctionExpression", + { name: "baz", chain: ["target", "bar"], loc: [[10, 0], [10, 3]] }); + + finish(); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_parser-09.js b/toolkit/devtools/debugger/test/browser_dbg_parser-09.js new file mode 100644 index 000000000..a8a9ad2c3 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_parser-09.js @@ -0,0 +1,290 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that inferring anonymous function information is done correctly + * from arrow expressions. + */ + +function test() { + let { Parser, ParserHelpers, SyntaxTreeVisitor } = + Cu.import("resource:///modules/devtools/Parser.jsm", {}); + + function verify(source, predicate, details) { + let { name, chain } = details; + let [[sline, scol], [eline, ecol]] = details.loc; + let ast = Parser.reflectionAPI.parse(source); + let node = SyntaxTreeVisitor.filter(ast, predicate).pop(); + let info = ParserHelpers.inferFunctionExpressionInfo(node); + + is(info.name, name, + "The function expression assignment property name is correct."); + is(chain ? info.chain.toSource() : info.chain, chain ? chain.toSource() : chain, + "The function expression assignment property chain is correct."); + is(info.loc.start.toSource(), { line: sline, column: scol }.toSource(), + "The start location was correct for the identifier in: '" + source + "'."); + is(info.loc.end.toSource(), { line: eline, column: ecol }.toSource(), + "The end location was correct for the identifier in: '" + source + "'."); + } + + // VariableDeclarator + + verify("var foo=()=>{}", e => e.type == "ArrowExpression", { + name: "foo", + chain: null, + loc: [[1, 4], [1, 7]] + }); + verify("\nvar\nfoo\n=\n(\n)\n=>\n{\n}\n", e => e.type == "ArrowExpression", { + name: "foo", + chain: null, + loc: [[3, 0], [3, 3]] + }); + + // AssignmentExpression + + verify("foo=()=>{}", e => e.type == "ArrowExpression", + { name: "foo", chain: [], loc: [[1, 0], [1, 3]] }); + + verify("\nfoo\n=\n(\n)\n=>\n{\n}\n", e => e.type == "ArrowExpression", + { name: "foo", chain: [], loc: [[2, 0], [2, 3]] }); + + verify("foo.bar=()=>{}", e => e.type == "ArrowExpression", + { name: "bar", chain: ["foo"], loc: [[1, 0], [1, 7]] }); + + verify("\nfoo.bar\n=\n(\n)\n=>\n{\n}\n", e => e.type == "ArrowExpression", + { name: "bar", chain: ["foo"], loc: [[2, 0], [2, 7]] }); + + verify("this.foo=()=>{}", e => e.type == "ArrowExpression", + { name: "foo", chain: ["this"], loc: [[1, 0], [1, 8]] }); + + verify("\nthis.foo\n=\n(\n)\n=>\n{\n}\n", e => e.type == "ArrowExpression", + { name: "foo", chain: ["this"], loc: [[2, 0], [2, 8]] }); + + verify("this.foo.bar=()=>{}", e => e.type == "ArrowExpression", + { name: "bar", chain: ["this", "foo"], loc: [[1, 0], [1, 12]] }); + + verify("\nthis.foo.bar\n=\n(\n)\n=>\n{\n}\n", e => e.type == "ArrowExpression", + { name: "bar", chain: ["this", "foo"], loc: [[2, 0], [2, 12]] }); + + verify("foo.this.bar=()=>{}", e => e.type == "ArrowExpression", + { name: "bar", chain: ["foo", "this"], loc: [[1, 0], [1, 12]] }); + + verify("\nfoo.this.bar\n=\n(\n)\n=>\n{\n}\n", e => e.type == "ArrowExpression", + { name: "bar", chain: ["foo", "this"], loc: [[2, 0], [2, 12]] }); + + // ObjectExpression + + verify("({foo:()=>{}})", e => e.type == "ArrowExpression", + { name: "foo", chain: [], loc: [[1, 2], [1, 5]] }); + + verify("(\n{\nfoo\n:\n(\n)\n=>\n{\n}\n}\n)", e => e.type == "ArrowExpression", + { name: "foo", chain: [], loc: [[3, 0], [3, 3]] }); + + verify("({foo:{bar:()=>{}}})", e => e.type == "ArrowExpression", + { name: "bar", chain: ["foo"], loc: [[1, 7], [1, 10]] }); + + verify("(\n{\nfoo\n:\n{\nbar\n:\n(\n)\n=>\n{\n}\n}\n}\n)", e => e.type == "ArrowExpression", + { name: "bar", chain: ["foo"], loc: [[6, 0], [6, 3]] }); + + // AssignmentExpression + ObjectExpression + + verify("foo={bar:()=>{}}", e => e.type == "ArrowExpression", + { name: "bar", chain: ["foo"], loc: [[1, 5], [1, 8]] }); + + verify("\nfoo\n=\n{\nbar\n:\n(\n)\n=>\n{\n}\n}\n", e => e.type == "ArrowExpression", + { name: "bar", chain: ["foo"], loc: [[5, 0], [5, 3]] }); + + verify("foo={bar:{baz:()=>{}}}", e => e.type == "ArrowExpression", + { name: "baz", chain: ["foo", "bar"], loc: [[1, 10], [1, 13]] }); + + verify("\nfoo\n=\n{\nbar\n:\n{\nbaz\n:\n(\n)\n=>\n{\n}\n}\n}\n", e => e.type == "ArrowExpression", + { name: "baz", chain: ["foo", "bar"], loc: [[8, 0], [8, 3]] }); + + verify("nested.foo={bar:()=>{}}", e => e.type == "ArrowExpression", + { name: "bar", chain: ["nested", "foo"], loc: [[1, 12], [1, 15]] }); + + verify("\nnested.foo\n=\n{\nbar\n:\n(\n)\n=>\n{\n}\n}\n", e => e.type == "ArrowExpression", + { name: "bar", chain: ["nested", "foo"], loc: [[5, 0], [5, 3]] }); + + verify("nested.foo={bar:{baz:()=>{}}}", e => e.type == "ArrowExpression", + { name: "baz", chain: ["nested", "foo", "bar"], loc: [[1, 17], [1, 20]] }); + + verify("\nnested.foo\n=\n{\nbar\n:\n{\nbaz\n:\n(\n)\n=>\n{\n}\n}\n}\n", e => e.type == "ArrowExpression", + { name: "baz", chain: ["nested", "foo", "bar"], loc: [[8, 0], [8, 3]] }); + + verify("this.foo={bar:()=>{}}", e => e.type == "ArrowExpression", + { name: "bar", chain: ["this", "foo"], loc: [[1, 10], [1, 13]] }); + + verify("\nthis.foo\n=\n{\nbar\n:\n(\n)\n=>\n{\n}\n}\n", e => e.type == "ArrowExpression", + { name: "bar", chain: ["this", "foo"], loc: [[5, 0], [5, 3]] }); + + verify("this.foo={bar:{baz:()=>{}}}", e => e.type == "ArrowExpression", + { name: "baz", chain: ["this", "foo", "bar"], loc: [[1, 15], [1, 18]] }); + + verify("\nthis.foo\n=\n{\nbar\n:\n{\nbaz\n:\n(\n)\n=>\n{\n}\n}\n}\n", e => e.type == "ArrowExpression", + { name: "baz", chain: ["this", "foo", "bar"], loc: [[8, 0], [8, 3]] }); + + verify("this.nested.foo={bar:()=>{}}", e => e.type == "ArrowExpression", + { name: "bar", chain: ["this", "nested", "foo"], loc: [[1, 17], [1, 20]] }); + + verify("\nthis.nested.foo\n=\n{\nbar\n:\n(\n)\n=>\n{\n}\n}\n", e => e.type == "ArrowExpression", + { name: "bar", chain: ["this", "nested", "foo"], loc: [[5, 0], [5, 3]] }); + + verify("this.nested.foo={bar:{baz:()=>{}}}", e => e.type == "ArrowExpression", + { name: "baz", chain: ["this", "nested", "foo", "bar"], loc: [[1, 22], [1, 25]] }); + + verify("\nthis.nested.foo\n=\n{\nbar\n:\n{\nbaz\n:\n(\n)\n=>\n{\n}\n}\n}\n", e => e.type == "ArrowExpression", + { name: "baz", chain: ["this", "nested", "foo", "bar"], loc: [[8, 0], [8, 3]] }); + + verify("nested.this.foo={bar:()=>{}}", e => e.type == "ArrowExpression", + { name: "bar", chain: ["nested", "this", "foo"], loc: [[1, 17], [1, 20]] }); + + verify("\nnested.this.foo\n=\n{\nbar\n:\n(\n)\n=>\n{\n}\n}\n", e => e.type == "ArrowExpression", + { name: "bar", chain: ["nested", "this", "foo"], loc: [[5, 0], [5, 3]] }); + + verify("nested.this.foo={bar:{baz:()=>{}}}", e => e.type == "ArrowExpression", + { name: "baz", chain: ["nested", "this", "foo", "bar"], loc: [[1, 22], [1, 25]] }); + + verify("\nnested.this.foo\n=\n{\nbar\n:\n{\nbaz\n:\n(\n)\n=>\n{\n}\n}\n}\n", e => e.type == "ArrowExpression", + { name: "baz", chain: ["nested", "this", "foo", "bar"], loc: [[8, 0], [8, 3]] }); + + // VariableDeclarator + AssignmentExpression + ObjectExpression + + verify("let foo={bar:()=>{}}", e => e.type == "ArrowExpression", + { name: "bar", chain: ["foo"], loc: [[1, 9], [1, 12]] }); + + verify("\nlet\nfoo\n=\n{\nbar\n:\n(\n)\n=>\n{\n}\n}\n", e => e.type == "ArrowExpression", + { name: "bar", chain: ["foo"], loc: [[6, 0], [6, 3]] }); + + verify("let foo={bar:{baz:()=>{}}}", e => e.type == "ArrowExpression", + { name: "baz", chain: ["foo", "bar"], loc: [[1, 14], [1, 17]] }); + + verify("\nlet\nfoo\n=\n{\nbar\n:\n{\nbaz\n:\n(\n)\n=>\n{\n}\n}\n}\n", e => e.type == "ArrowExpression", + { name: "baz", chain: ["foo", "bar"], loc: [[9, 0], [9, 3]] }); + + // New/CallExpression + AssignmentExpression + ObjectExpression + + verify("foo({bar:()=>{}})", e => e.type == "ArrowExpression", + { name: "bar", chain: [], loc: [[1, 5], [1, 8]] }); + + verify("\nfoo\n(\n{\nbar\n:\n(\n)\n=>\n{\n}\n}\n)\n", e => e.type == "ArrowExpression", + { name: "bar", chain: [], loc: [[5, 0], [5, 3]] }); + + verify("foo({bar:{baz:()=>{}}})", e => e.type == "ArrowExpression", + { name: "baz", chain: ["bar"], loc: [[1, 10], [1, 13]] }); + + verify("\nfoo\n(\n{\nbar\n:\n{\nbaz\n:\n(\n)\n=>\n{\n}\n}\n}\n)\n", e => e.type == "ArrowExpression", + { name: "baz", chain: ["bar"], loc: [[8, 0], [8, 3]] }); + + verify("nested.foo({bar:()=>{}})", e => e.type == "ArrowExpression", + { name: "bar", chain: [], loc: [[1, 12], [1, 15]] }); + + verify("\nnested.foo\n(\n{\nbar\n:\n(\n)\n=>\n{\n}\n}\n)\n", e => e.type == "ArrowExpression", + { name: "bar", chain: [], loc: [[5, 0], [5, 3]] }); + + verify("nested.foo({bar:{baz:()=>{}}})", e => e.type == "ArrowExpression", + { name: "baz", chain: ["bar"], loc: [[1, 17], [1, 20]] }); + + verify("\nnested.foo\n(\n{\nbar\n:\n{\nbaz\n:\n(\n)\n=>\n{\n}\n}\n}\n)\n", e => e.type == "ArrowExpression", + { name: "baz", chain: ["bar"], loc: [[8, 0], [8, 3]] }); + + verify("this.foo({bar:()=>{}})", e => e.type == "ArrowExpression", + { name: "bar", chain: [], loc: [[1, 10], [1, 13]] }); + + verify("\nthis.foo\n(\n{\nbar\n:\n(\n)\n=>\n{\n}\n}\n)\n", e => e.type == "ArrowExpression", + { name: "bar", chain: [], loc: [[5, 0], [5, 3]] }); + + verify("this.foo({bar:{baz:()=>{}}})", e => e.type == "ArrowExpression", + { name: "baz", chain: ["bar"], loc: [[1, 15], [1, 18]] }); + + verify("\nthis.foo\n(\n{\nbar\n:\n{\nbaz\n:\n(\n)\n=>\n{\n}\n}\n}\n)\n", e => e.type == "ArrowExpression", + { name: "baz", chain: ["bar"], loc: [[8, 0], [8, 3]] }); + + verify("this.nested.foo({bar:()=>{}})", e => e.type == "ArrowExpression", + { name: "bar", chain: [], loc: [[1, 17], [1, 20]] }); + + verify("\nthis.nested.foo\n(\n{\nbar\n:\n(\n)\n=>\n{\n}\n}\n)\n", e => e.type == "ArrowExpression", + { name: "bar", chain: [], loc: [[5, 0], [5, 3]] }); + + verify("this.nested.foo({bar:{baz:()=>{}}})", e => e.type == "ArrowExpression", + { name: "baz", chain: ["bar"], loc: [[1, 22], [1, 25]] }); + + verify("\nthis.nested.foo\n(\n{\nbar\n:\n{\nbaz\n:\n(\n)\n=>\n{\n}\n}\n}\n)\n", e => e.type == "ArrowExpression", + { name: "baz", chain: ["bar"], loc: [[8, 0], [8, 3]] }); + + verify("nested.this.foo({bar:()=>{}})", e => e.type == "ArrowExpression", + { name: "bar", chain: [], loc: [[1, 17], [1, 20]] }); + + verify("\nnested.this.foo\n(\n{\nbar\n:\n(\n)\n=>\n{\n}\n}\n)\n", e => e.type == "ArrowExpression", + { name: "bar", chain: [], loc: [[5, 0], [5, 3]] }); + + verify("nested.this.foo({bar:{baz:()=>{}}})", e => e.type == "ArrowExpression", + { name: "baz", chain: ["bar"], loc: [[1, 22], [1, 25]] }); + + verify("\nnested.this.foo\n(\n{\nbar\n:\n{\nbaz\n:\n(\n)\n=>\n{\n}\n}\n}\n)\n", e => e.type == "ArrowExpression", + { name: "baz", chain: ["bar"], loc: [[8, 0], [8, 3]] }); + + // New/CallExpression + VariableDeclarator + AssignmentExpression + ObjectExpression + + verify("let target=foo({bar:()=>{}})", e => e.type == "ArrowExpression", + { name: "bar", chain: ["target"], loc: [[1, 16], [1, 19]] }); + + verify("\nlet\ntarget=\nfoo\n(\n{\nbar\n:\n(\n)\n=>\n{\n}\n}\n)\n", e => e.type == "ArrowExpression", + { name: "bar", chain: ["target"], loc: [[7, 0], [7, 3]] }); + + verify("let target=foo({bar:{baz:()=>{}}})", e => e.type == "ArrowExpression", + { name: "baz", chain: ["target", "bar"], loc: [[1, 21], [1, 24]] }); + + verify("\nlet\ntarget=\nfoo\n(\n{\nbar\n:\n{\nbaz\n:\n(\n)\n=>\n{\n}\n}\n}\n)\n", e => e.type == "ArrowExpression", + { name: "baz", chain: ["target", "bar"], loc: [[10, 0], [10, 3]] }); + + verify("let target=nested.foo({bar:()=>{}})", e => e.type == "ArrowExpression", + { name: "bar", chain: ["target"], loc: [[1, 23], [1, 26]] }); + + verify("\nlet\ntarget=\nnested.foo\n(\n{\nbar\n:\n(\n)\n=>\n{\n}\n}\n)\n", e => e.type == "ArrowExpression", + { name: "bar", chain: ["target"], loc: [[7, 0], [7, 3]] }); + + verify("let target=nested.foo({bar:{baz:()=>{}}})", e => e.type == "ArrowExpression", + { name: "baz", chain: ["target", "bar"], loc: [[1, 28], [1, 31]] }); + + verify("\nlet\ntarget=\nnested.foo\n(\n{\nbar\n:\n{\nbaz\n:\n(\n)\n=>\n{\n}\n}\n}\n)\n", e => e.type == "ArrowExpression", + { name: "baz", chain: ["target", "bar"], loc: [[10, 0], [10, 3]] }); + + verify("let target=this.foo({bar:()=>{}})", e => e.type == "ArrowExpression", + { name: "bar", chain: ["target"], loc: [[1, 21], [1, 24]] }); + + verify("\nlet\ntarget=\nthis.foo\n(\n{\nbar\n:\n(\n)\n=>\n{\n}\n}\n)\n", e => e.type == "ArrowExpression", + { name: "bar", chain: ["target"], loc: [[7, 0], [7, 3]] }); + + verify("let target=this.foo({bar:{baz:()=>{}}})", e => e.type == "ArrowExpression", + { name: "baz", chain: ["target", "bar"], loc: [[1, 26], [1, 29]] }); + + verify("\nlet\ntarget=\nthis.foo\n(\n{\nbar\n:\n{\nbaz\n:\n(\n)\n=>\n{\n}\n}\n}\n)\n", e => e.type == "ArrowExpression", + { name: "baz", chain: ["target", "bar"], loc: [[10, 0], [10, 3]] }); + + verify("let target=this.nested.foo({bar:()=>{}})", e => e.type == "ArrowExpression", + { name: "bar", chain: ["target"], loc: [[1, 28], [1, 31]] }); + + verify("\nlet\ntarget=\nthis.nested.foo\n(\n{\nbar\n:\n(\n)\n=>\n{\n}\n}\n)\n", e => e.type == "ArrowExpression", + { name: "bar", chain: ["target"], loc: [[7, 0], [7, 3]] }); + + verify("let target=this.nested.foo({bar:{baz:()=>{}}})", e => e.type == "ArrowExpression", + { name: "baz", chain: ["target", "bar"], loc: [[1, 33], [1, 36]] }); + + verify("\nlet\ntarget=\nthis.nested.foo\n(\n{\nbar\n:\n{\nbaz\n:\n(\n)\n=>\n{\n}\n}\n}\n)\n", e => e.type == "ArrowExpression", + { name: "baz", chain: ["target", "bar"], loc: [[10, 0], [10, 3]] }); + + verify("let target=nested.this.foo({bar:()=>{}})", e => e.type == "ArrowExpression", + { name: "bar", chain: ["target"], loc: [[1, 28], [1, 31]] }); + + verify("\nlet\ntarget=\nnested.this.foo\n(\n{\nbar\n:\n(\n)\n=>\n{\n}\n}\n)\n", e => e.type == "ArrowExpression", + { name: "bar", chain: ["target"], loc: [[7, 0], [7, 3]] }); + + verify("let target=nested.this.foo({bar:{baz:()=>{}}})", e => e.type == "ArrowExpression", + { name: "baz", chain: ["target", "bar"], loc: [[1, 33], [1, 36]] }); + + verify("\nlet\ntarget=\nnested.this.foo\n(\n{\nbar\n:\n{\nbaz\n:\n(\n)\n=>\n{\n}\n}\n}\n)\n", e => e.type == "ArrowExpression", + { name: "baz", chain: ["target", "bar"], loc: [[10, 0], [10, 3]] }); + + finish(); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_parser-10.js b/toolkit/devtools/debugger/test/browser_dbg_parser-10.js new file mode 100644 index 000000000..af44ebdf6 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_parser-10.js @@ -0,0 +1,127 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that creating an evaluation string for certain nodes works properly. + */ + +function test() { + let { Parser, ParserHelpers, SyntaxTreeVisitor } = + Cu.import("resource:///modules/devtools/Parser.jsm", {}); + + function verify(source, predicate, string) { + let ast = Parser.reflectionAPI.parse(source); + let node = SyntaxTreeVisitor.filter(ast, predicate).pop(); + let info = ParserHelpers.getIdentifierEvalString(node); + is(info, string, "The identifier evaluation string is correct."); + } + + // Indentifier or Literal + + verify("foo", e => e.type == "Identifier", "foo"); + verify("undefined", e => e.type == "Identifier", "undefined"); + verify("null", e => e.type == "Literal", "null"); + verify("42", e => e.type == "Literal", "42"); + verify("true", e => e.type == "Literal", "true"); + verify("\"nasu\"", e => e.type == "Literal", "\"nasu\""); + + // MemberExpression or ThisExpression + + verify("this", e => e.type == "ThisExpression", "this"); + verify("foo.bar", e => e.name == "foo", "foo"); + verify("foo.bar", e => e.name == "bar", "foo.bar"); + + // MemberExpression + ThisExpression + + verify("this.foo.bar", e => e.type == "ThisExpression", "this"); + verify("this.foo.bar", e => e.name == "foo", "this.foo"); + verify("this.foo.bar", e => e.name == "bar", "this.foo.bar"); + + verify("foo.this.bar", e => e.name == "foo", "foo"); + verify("foo.this.bar", e => e.name == "this", "foo.this"); + verify("foo.this.bar", e => e.name == "bar", "foo.this.bar"); + + // ObjectExpression + VariableDeclarator + + verify("let foo={bar:baz}", e => e.name == "baz", "baz"); + verify("let foo={bar:undefined}", e => e.name == "undefined", "undefined"); + verify("let foo={bar:null}", e => e.type == "Literal", "null"); + verify("let foo={bar:42}", e => e.type == "Literal", "42"); + verify("let foo={bar:true}", e => e.type == "Literal", "true"); + verify("let foo={bar:\"nasu\"}", e => e.type == "Literal", "\"nasu\""); + verify("let foo={bar:this}", e => e.type == "ThisExpression", "this"); + + verify("let foo={bar:{nested:baz}}", e => e.name == "baz", "baz"); + verify("let foo={bar:{nested:undefined}}", e => e.name == "undefined", "undefined"); + verify("let foo={bar:{nested:null}}", e => e.type == "Literal", "null"); + verify("let foo={bar:{nested:42}}", e => e.type == "Literal", "42"); + verify("let foo={bar:{nested:true}}", e => e.type == "Literal", "true"); + verify("let foo={bar:{nested:\"nasu\"}}", e => e.type == "Literal", "\"nasu\""); + verify("let foo={bar:{nested:this}}", e => e.type == "ThisExpression", "this"); + + verify("let foo={bar:baz}", e => e.name == "bar", "foo.bar"); + verify("let foo={bar:baz}", e => e.name == "foo", "foo"); + + verify("let foo={bar:{nested:baz}}", e => e.name == "nested", "foo.bar.nested"); + verify("let foo={bar:{nested:baz}}", e => e.name == "bar", "foo.bar"); + verify("let foo={bar:{nested:baz}}", e => e.name == "foo", "foo"); + + // ObjectExpression + MemberExpression + + verify("parent.foo={bar:baz}", e => e.name == "bar", "parent.foo.bar"); + verify("parent.foo={bar:baz}", e => e.name == "foo", "parent.foo"); + verify("parent.foo={bar:baz}", e => e.name == "parent", "parent"); + + verify("parent.foo={bar:{nested:baz}}", e => e.name == "nested", "parent.foo.bar.nested"); + verify("parent.foo={bar:{nested:baz}}", e => e.name == "bar", "parent.foo.bar"); + verify("parent.foo={bar:{nested:baz}}", e => e.name == "foo", "parent.foo"); + verify("parent.foo={bar:{nested:baz}}", e => e.name == "parent", "parent"); + + verify("this.foo={bar:{nested:baz}}", e => e.name == "nested", "this.foo.bar.nested"); + verify("this.foo={bar:{nested:baz}}", e => e.name == "bar", "this.foo.bar"); + verify("this.foo={bar:{nested:baz}}", e => e.name == "foo", "this.foo"); + verify("this.foo={bar:{nested:baz}}", e => e.type == "ThisExpression", "this"); + + verify("this.parent.foo={bar:{nested:baz}}", e => e.name == "nested", "this.parent.foo.bar.nested"); + verify("this.parent.foo={bar:{nested:baz}}", e => e.name == "bar", "this.parent.foo.bar"); + verify("this.parent.foo={bar:{nested:baz}}", e => e.name == "foo", "this.parent.foo"); + verify("this.parent.foo={bar:{nested:baz}}", e => e.name == "parent", "this.parent"); + verify("this.parent.foo={bar:{nested:baz}}", e => e.type == "ThisExpression", "this"); + + verify("parent.this.foo={bar:{nested:baz}}", e => e.name == "nested", "parent.this.foo.bar.nested"); + verify("parent.this.foo={bar:{nested:baz}}", e => e.name == "bar", "parent.this.foo.bar"); + verify("parent.this.foo={bar:{nested:baz}}", e => e.name == "foo", "parent.this.foo"); + verify("parent.this.foo={bar:{nested:baz}}", e => e.name == "this", "parent.this"); + verify("parent.this.foo={bar:{nested:baz}}", e => e.name == "parent", "parent"); + + // FunctionExpression + + verify("function foo(){}", e => e.name == "foo", "foo"); + verify("var foo=function(){}", e => e.name == "foo", "foo"); + verify("var foo=function bar(){}", e => e.name == "bar", "bar"); + + // New/CallExpression + + verify("foo()", e => e.name == "foo", "foo"); + verify("new foo()", e => e.name == "foo", "foo"); + + verify("foo(bar)", e => e.name == "bar", "bar"); + verify("foo(bar, baz)", e => e.name == "baz", "baz"); + verify("foo(undefined)", e => e.name == "undefined", "undefined"); + verify("foo(null)", e => e.type == "Literal", "null"); + verify("foo(42)", e => e.type == "Literal", "42"); + verify("foo(true)", e => e.type == "Literal", "true"); + verify("foo(\"nasu\")", e => e.type == "Literal", "\"nasu\""); + verify("foo(this)", e => e.type == "ThisExpression", "this"); + + // New/CallExpression + ObjectExpression + MemberExpression + + verify("fun(this.parent.foo={bar:{nested:baz}})", e => e.name == "nested", "this.parent.foo.bar.nested"); + verify("fun(this.parent.foo={bar:{nested:baz}})", e => e.name == "bar", "this.parent.foo.bar"); + verify("fun(this.parent.foo={bar:{nested:baz}})", e => e.name == "foo", "this.parent.foo"); + verify("fun(this.parent.foo={bar:{nested:baz}})", e => e.name == "parent", "this.parent"); + verify("fun(this.parent.foo={bar:{nested:baz}})", e => e.type == "ThisExpression", "this"); + verify("fun(this.parent.foo={bar:{nested:baz}})", e => e.name == "fun", "fun"); + + finish(); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_pause-exceptions-01.js b/toolkit/devtools/debugger/test/browser_dbg_pause-exceptions-01.js new file mode 100644 index 000000000..e894050e3 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_pause-exceptions-01.js @@ -0,0 +1,240 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that pausing on exceptions works. + */ + +const TAB_URL = EXAMPLE_URL + "doc_pause-exceptions.html"; + +let gTab, gPanel, gDebugger; +let gFrames, gVariables, gPrefs, gOptions; + +function test() { + requestLongerTimeout(2); + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gFrames = gDebugger.DebuggerView.StackFrames; + gVariables = gDebugger.DebuggerView.Variables; + gPrefs = gDebugger.Prefs; + gOptions = gDebugger.DebuggerView.Options; + + is(gPrefs.pauseOnExceptions, false, + "The pause-on-exceptions pref should be disabled by default."); + isnot(gOptions._pauseOnExceptionsItem.getAttribute("checked"), "true", + "The pause-on-exceptions menu item should not be checked."); + + testPauseOnExceptionsDisabled() + .then(enablePauseOnExceptions) + .then(disableIgnoreCaughtExceptions) + .then(testPauseOnExceptionsEnabled) + .then(disablePauseOnExceptions) + .then(enableIgnoreCaughtExceptions) + .then(() => closeDebuggerAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); +} + +function testPauseOnExceptionsDisabled() { + let finished = waitForCaretAndScopes(gPanel, 26).then(() => { + info("Testing disabled pause-on-exceptions."); + + is(gDebugger.gThreadClient.state, "paused", + "Should only be getting stack frames while paused (1)."); + ok(isCaretPos(gPanel, 26), + "Should be paused on the debugger statement (1)."); + + let innerScope = gVariables.getScopeAtIndex(0); + let innerNodes = innerScope.target.querySelector(".variables-view-element-details").childNodes; + + is(gFrames.itemCount, 1, + "Should have one frame."); + is(gVariables._store.length, 3, + "Should have three scopes."); + + is(innerNodes[0].querySelector(".name").getAttribute("value"), "this", + "Should have the right property name for 'this'."); + is(innerNodes[0].querySelector(".value").getAttribute("value"), "<button>", + "Should have the right property value for 'this'."); + + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.AFTER_FRAMES_CLEARED).then(() => { + isnot(gDebugger.gThreadClient.state, "paused", + "Should not be paused after resuming."); + ok(isCaretPos(gPanel, 26), + "Should be idle on the debugger statement."); + + ok(true, "Frames were cleared, debugger didn't pause again."); + }); + + EventUtils.sendMouseEvent({ type: "mousedown" }, + gDebugger.document.getElementById("resume"), + gDebugger); + + return finished; + }); + + sendMouseClickToTab(gTab, content.document.querySelector("button")); + + return finished; +} + +function testPauseOnExceptionsEnabled() { + let finished = waitForCaretAndScopes(gPanel, 19).then(() => { + info("Testing enabled pause-on-exceptions."); + + is(gDebugger.gThreadClient.state, "paused", + "Should only be getting stack frames while paused."); + ok(isCaretPos(gPanel, 19), + "Should be paused on the debugger statement."); + + let innerScope = gVariables.getScopeAtIndex(0); + let innerNodes = innerScope.target.querySelector(".variables-view-element-details").childNodes; + + is(gFrames.itemCount, 1, + "Should have one frame."); + is(gVariables._store.length, 3, + "Should have three scopes."); + + is(innerNodes[0].querySelector(".name").getAttribute("value"), "<exception>", + "Should have the right property name for <exception>."); + is(innerNodes[0].querySelector(".value").getAttribute("value"), "Error", + "Should have the right property value for <exception>."); + + let finished = waitForCaretAndScopes(gPanel, 26).then(() => { + info("Testing enabled pause-on-exceptions and resumed after pause."); + + is(gDebugger.gThreadClient.state, "paused", + "Should only be getting stack frames while paused."); + ok(isCaretPos(gPanel, 26), + "Should be paused on the debugger statement."); + + let innerScope = gVariables.getScopeAtIndex(0); + let innerNodes = innerScope.target.querySelector(".variables-view-element-details").childNodes; + + is(gFrames.itemCount, 1, + "Should have one frame."); + is(gVariables._store.length, 3, + "Should have three scopes."); + + is(innerNodes[0].querySelector(".name").getAttribute("value"), "this", + "Should have the right property name for 'this'."); + is(innerNodes[0].querySelector(".value").getAttribute("value"), "<button>", + "Should have the right property value for 'this'."); + + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.AFTER_FRAMES_CLEARED).then(() => { + isnot(gDebugger.gThreadClient.state, "paused", + "Should not be paused after resuming."); + ok(isCaretPos(gPanel, 26), + "Should be idle on the debugger statement."); + + ok(true, "Frames were cleared, debugger didn't pause again."); + }); + + EventUtils.sendMouseEvent({ type: "mousedown" }, + gDebugger.document.getElementById("resume"), + gDebugger); + + return finished; + }); + + EventUtils.sendMouseEvent({ type: "mousedown" }, + gDebugger.document.getElementById("resume"), + gDebugger); + + return finished; + }); + + sendMouseClickToTab(gTab, content.document.querySelector("button")); + + return finished; +} + +function enablePauseOnExceptions() { + let deferred = promise.defer(); + + gDebugger.gThreadClient.addOneTimeListener("resumed", () => { + is(gPrefs.pauseOnExceptions, true, + "The pause-on-exceptions pref should now be enabled."); + is(gOptions._pauseOnExceptionsItem.getAttribute("checked"), "true", + "The pause-on-exceptions menu item should now be checked."); + + ok(true, "Pausing on exceptions was enabled."); + deferred.resolve(); + }); + + gOptions._pauseOnExceptionsItem.setAttribute("checked", "true"); + gOptions._togglePauseOnExceptions(); + + return deferred.promise; +} + +function disablePauseOnExceptions() { + let deferred = promise.defer(); + + gDebugger.gThreadClient.addOneTimeListener("resumed", () => { + is(gPrefs.pauseOnExceptions, false, + "The pause-on-exceptions pref should now be disabled."); + isnot(gOptions._pauseOnExceptionsItem.getAttribute("checked"), "true", + "The pause-on-exceptions menu item should now be unchecked."); + + ok(true, "Pausing on exceptions was disabled."); + deferred.resolve(); + }); + + gOptions._pauseOnExceptionsItem.setAttribute("checked", "false"); + gOptions._togglePauseOnExceptions(); + + return deferred.promise; +} + +function enableIgnoreCaughtExceptions() { + let deferred = promise.defer(); + + gDebugger.gThreadClient.addOneTimeListener("resumed", () => { + is(gPrefs.ignoreCaughtExceptions, true, + "The ignore-caught-exceptions pref should now be enabled."); + is(gOptions._ignoreCaughtExceptionsItem.getAttribute("checked"), "true", + "The ignore-caught-exceptions menu item should now be checked."); + + ok(true, "Ignore caught exceptions was enabled."); + deferred.resolve(); + }); + + gOptions._ignoreCaughtExceptionsItem.setAttribute("checked", "true"); + gOptions._toggleIgnoreCaughtExceptions(); + + return deferred.promise; +} + +function disableIgnoreCaughtExceptions() { + let deferred = promise.defer(); + + gDebugger.gThreadClient.addOneTimeListener("resumed", () => { + is(gPrefs.ignoreCaughtExceptions, false, + "The ignore-caught-exceptions pref should now be disabled."); + isnot(gOptions._ignoreCaughtExceptionsItem.getAttribute("checked"), "true", + "The ignore-caught-exceptions menu item should now be unchecked."); + + ok(true, "Ignore caught exceptions was disabled."); + deferred.resolve(); + }); + + gOptions._ignoreCaughtExceptionsItem.setAttribute("checked", "false"); + gOptions._toggleIgnoreCaughtExceptions(); + + return deferred.promise; +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gFrames = null; + gVariables = null; + gPrefs = null; + gOptions = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_pause-exceptions-02.js b/toolkit/devtools/debugger/test/browser_dbg_pause-exceptions-02.js new file mode 100644 index 000000000..aa7c03ada --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_pause-exceptions-02.js @@ -0,0 +1,196 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that pausing on exceptions works after reload. + */ + +const TAB_URL = EXAMPLE_URL + "doc_pause-exceptions.html"; + +let gTab, gPanel, gDebugger; +let gFrames, gVariables, gPrefs, gOptions; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gFrames = gDebugger.DebuggerView.StackFrames; + gVariables = gDebugger.DebuggerView.Variables; + gPrefs = gDebugger.Prefs; + gOptions = gDebugger.DebuggerView.Options; + + is(gPrefs.pauseOnExceptions, false, + "The pause-on-exceptions pref should be disabled by default."); + isnot(gOptions._pauseOnExceptionsItem.getAttribute("checked"), "true", + "The pause-on-exceptions menu item should not be checked."); + + enablePauseOnExceptions() + .then(disableIgnoreCaughtExceptions) + .then(() => reloadActiveTab(gPanel, gDebugger.EVENTS.SOURCE_SHOWN)) + .then(testPauseOnExceptionsAfterReload) + .then(disablePauseOnExceptions) + .then(enableIgnoreCaughtExceptions) + .then(() => closeDebuggerAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); +} + +function testPauseOnExceptionsAfterReload() { + let finished = waitForCaretAndScopes(gPanel, 19).then(() => { + info("Testing enabled pause-on-exceptions."); + + is(gDebugger.gThreadClient.state, "paused", + "Should only be getting stack frames while paused."); + ok(isCaretPos(gPanel, 19), + "Should be paused on the debugger statement."); + + let innerScope = gVariables.getScopeAtIndex(0); + let innerNodes = innerScope.target.querySelector(".variables-view-element-details").childNodes; + + is(gFrames.itemCount, 1, + "Should have one frame."); + is(gVariables._store.length, 3, + "Should have three scopes."); + + is(innerNodes[0].querySelector(".name").getAttribute("value"), "<exception>", + "Should have the right property name for <exception>."); + is(innerNodes[0].querySelector(".value").getAttribute("value"), "Error", + "Should have the right property value for <exception>."); + + let finished = waitForCaretAndScopes(gPanel, 26).then(() => { + info("Testing enabled pause-on-exceptions and resumed after pause."); + + is(gDebugger.gThreadClient.state, "paused", + "Should only be getting stack frames while paused."); + ok(isCaretPos(gPanel, 26), + "Should be paused on the debugger statement."); + + let innerScope = gVariables.getScopeAtIndex(0); + let innerNodes = innerScope.target.querySelector(".variables-view-element-details").childNodes; + + is(gFrames.itemCount, 1, + "Should have one frame."); + is(gVariables._store.length, 3, + "Should have three scopes."); + + is(innerNodes[0].querySelector(".name").getAttribute("value"), "this", + "Should have the right property name for 'this'."); + is(innerNodes[0].querySelector(".value").getAttribute("value"), "<button>", + "Should have the right property value for 'this'."); + + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.AFTER_FRAMES_CLEARED).then(() => { + isnot(gDebugger.gThreadClient.state, "paused", + "Should not be paused after resuming."); + ok(isCaretPos(gPanel, 26), + "Should be idle on the debugger statement."); + + ok(true, "Frames were cleared, debugger didn't pause again."); + }); + + EventUtils.sendMouseEvent({ type: "mousedown" }, + gDebugger.document.getElementById("resume"), + gDebugger); + + return finished; + }); + + EventUtils.sendMouseEvent({ type: "mousedown" }, + gDebugger.document.getElementById("resume"), + gDebugger); + + return finished; + }); + + sendMouseClickToTab(gTab, content.document.querySelector("button")); + + return finished; +} + +function enablePauseOnExceptions() { + let deferred = promise.defer(); + + gDebugger.gThreadClient.addOneTimeListener("resumed", () => { + is(gPrefs.pauseOnExceptions, true, + "The pause-on-exceptions pref should now be enabled."); + is(gOptions._pauseOnExceptionsItem.getAttribute("checked"), "true", + "The pause-on-exceptions menu item should now be checked."); + + ok(true, "Pausing on exceptions was enabled."); + deferred.resolve(); + }); + + gOptions._pauseOnExceptionsItem.setAttribute("checked", "true"); + gOptions._togglePauseOnExceptions(); + + return deferred.promise; +} + +function disablePauseOnExceptions() { + let deferred = promise.defer(); + + gDebugger.gThreadClient.addOneTimeListener("resumed", () => { + is(gPrefs.pauseOnExceptions, false, + "The pause-on-exceptions pref should now be disabled."); + isnot(gOptions._pauseOnExceptionsItem.getAttribute("checked"), "true", + "The pause-on-exceptions menu item should now be unchecked."); + + ok(true, "Pausing on exceptions was disabled."); + deferred.resolve(); + }); + + gOptions._pauseOnExceptionsItem.setAttribute("checked", "false"); + gOptions._togglePauseOnExceptions(); + + return deferred.promise; +} + +function enableIgnoreCaughtExceptions() { + let deferred = promise.defer(); + + gDebugger.gThreadClient.addOneTimeListener("resumed", () => { + is(gPrefs.ignoreCaughtExceptions, true, + "The ignore-caught-exceptions pref should now be enabled."); + is(gOptions._ignoreCaughtExceptionsItem.getAttribute("checked"), "true", + "The ignore-caught-exceptions menu item should now be checked."); + + ok(true, "Ignore caught exceptions was enabled."); + deferred.resolve(); + }); + + gOptions._ignoreCaughtExceptionsItem.setAttribute("checked", "true"); + gOptions._toggleIgnoreCaughtExceptions(); + + return deferred.promise; +} + +function disableIgnoreCaughtExceptions() { + let deferred = promise.defer(); + + gDebugger.gThreadClient.addOneTimeListener("resumed", () => { + is(gPrefs.ignoreCaughtExceptions, false, + "The ignore-caught-exceptions pref should now be disabled."); + isnot(gOptions._ignoreCaughtExceptionsItem.getAttribute("checked"), "true", + "The ignore-caught-exceptions menu item should now be unchecked."); + + ok(true, "Ignore caught exceptions was disabled."); + deferred.resolve(); + }); + + gOptions._ignoreCaughtExceptionsItem.setAttribute("checked", "false"); + gOptions._toggleIgnoreCaughtExceptions(); + + return deferred.promise; +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gFrames = null; + gVariables = null; + gPrefs = null; + gOptions = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_pause-resume.js b/toolkit/devtools/debugger/test/browser_dbg_pause-resume.js new file mode 100644 index 000000000..0fd898671 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_pause-resume.js @@ -0,0 +1,76 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if pausing and resuming in the main loop works properly. + */ + +const TAB_URL = EXAMPLE_URL + "doc_pause-exceptions.html"; + +let gTab, gPanel, gDebugger; +let gResumeButton, gResumeKey, gFrames; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gResumeButton = gDebugger.document.getElementById("resume"); + gResumeKey = gDebugger.document.getElementById("resumeKey"); + gFrames = gDebugger.DebuggerView.StackFrames; + + testPause(); + }); +} + +function testPause() { + is(gDebugger.gThreadClient.paused, false, + "Should be running after starting the test."); + + is(gResumeButton.getAttribute("tooltiptext"), + gDebugger.L10N.getFormatStr("pauseButtonTooltip", + gDebugger.ShortcutUtils.prettifyShortcut(gResumeKey)), + "Button tooltip should be 'pause' when running."); + + gDebugger.gThreadClient.addOneTimeListener("paused", () => { + is(gDebugger.gThreadClient.paused, true, + "Should be paused after an interrupt request."); + + is(gResumeButton.getAttribute("tooltiptext"), + gDebugger.L10N.getFormatStr("resumeButtonTooltip", + gDebugger.ShortcutUtils.prettifyShortcut(gResumeKey)), + "Button tooltip should be 'resume' when paused."); + + is(gFrames.itemCount, 0, + "Should have no frames when paused in the main loop."); + + testResume(); + }); + + EventUtils.sendMouseEvent({ type: "mousedown" }, gResumeButton, gDebugger); +} + +function testResume() { + gDebugger.gThreadClient.addOneTimeListener("resumed", () => { + is(gDebugger.gThreadClient.paused, false, + "Should be paused after an interrupt request."); + + is(gResumeButton.getAttribute("tooltiptext"), + gDebugger.L10N.getFormatStr("pauseButtonTooltip", + gDebugger.ShortcutUtils.prettifyShortcut(gResumeKey)), + "Button tooltip should be pause when running."); + + closeDebuggerAndFinish(gPanel); + }); + + EventUtils.sendMouseEvent({ type: "mousedown" }, gResumeButton, gDebugger); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gResumeButton = null; + gResumeKey = null; + gFrames = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_pause-warning.js b/toolkit/devtools/debugger/test/browser_dbg_pause-warning.js new file mode 100644 index 000000000..4da111900 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_pause-warning.js @@ -0,0 +1,98 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if a warning is shown in the inspector when debugger is paused. + */ + +const TAB_URL = EXAMPLE_URL + "doc_inline-script.html"; + +let gTab, gPanel, gDebugger; +let gTarget, gToolbox; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gTarget = gPanel.target; + gToolbox = gPanel._toolbox; + + testPause(); + }); +} + +function testPause() { + gDebugger.gThreadClient.addOneTimeListener("paused", () => { + ok(gTarget.isThreadPaused, + "target.isThreadPaused has been updated to true."); + + gToolbox.once("inspector-selected").then(inspector => { + inspector.once("inspector-updated").then(testNotificationIsUp1); + }); + gToolbox.selectTool("inspector"); + }); + + EventUtils.sendMouseEvent({ type: "mousedown" }, + gDebugger.document.getElementById("resume"), + gDebugger); +} + +function testNotificationIsUp1() { + let notificationBox = gToolbox.getNotificationBox(); + let notification = notificationBox.getNotificationWithValue("inspector-script-paused"); + + ok(notification, + "Inspector notification is present (1)."); + + gToolbox.once("jsdebugger-selected", testNotificationIsHidden); + gToolbox.selectTool("jsdebugger"); +} + +function testNotificationIsHidden() { + let notificationBox = gToolbox.getNotificationBox(); + let notification = notificationBox.getNotificationWithValue("inspector-script-paused"); + + ok(!notification, + "Inspector notification is hidden (2)."); + + gToolbox.once("inspector-selected", testNotificationIsUp2); + gToolbox.selectTool("inspector"); +} + +function testNotificationIsUp2() { + let notificationBox = gToolbox.getNotificationBox(); + let notification = notificationBox.getNotificationWithValue("inspector-script-paused"); + + ok(notification, + "Inspector notification is present again (3)."); + + testResume(); +} + +function testResume() { + gDebugger.gThreadClient.addOneTimeListener("resumed", () => { + ok(!gTarget.isThreadPaused, + "target.isThreadPaused has been updated to false."); + + let notificationBox = gToolbox.getNotificationBox(); + let notification = notificationBox.getNotificationWithValue("inspector-script-paused"); + + ok(!notification, + "Inspector notification was removed once debugger resumed."); + + closeDebuggerAndFinish(gPanel); + }); + + EventUtils.sendMouseEvent({ type: "mousedown" }, + gDebugger.document.getElementById("resume"), + gDebugger); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gTarget = null; + gToolbox = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_paused-keybindings.js b/toolkit/devtools/debugger/test/browser_dbg_paused-keybindings.js new file mode 100644 index 000000000..8b707bddd --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_paused-keybindings.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that keybindings still work when the content window is paused and +// the tab is selected again. + +function test() { + Task.spawn(function* () { + const TAB_URL = EXAMPLE_URL + "doc_inline-script.html"; + let gDebugger, searchBox; + + let [, debuggee, panel] = yield initDebugger(TAB_URL); + gDebugger = panel.panelWin; + searchBox = gDebugger.DebuggerView.Filtering._searchbox; + + // Spin the event loop before causing the debuggee to pause, to allow + // this function to return first. + executeSoon(() => { + EventUtils.sendMouseEvent({ type: "click" }, + debuggee.document.querySelector("button"), + debuggee); + }); + yield waitForSourceAndCaretAndScopes(panel, ".html", 20); + yield ensureThreadClientState(panel, "paused"); + + // Now open a tab and close it. + let tab2 = yield addTab(TAB_URL); + yield removeTab(tab2); + yield ensureCaretAt(panel, 20); + + // Try to use the Cmd-L keybinding to see if it still works. + let caretMove = ensureCaretAt(panel, 15, 1, true); + // Wait a tick for the editor focus event to occur first. + executeSoon(function () { + EventUtils.synthesizeKey("l", { accelKey: true }); + EventUtils.synthesizeKey("1", {}); + EventUtils.synthesizeKey("5", {}); + }); + yield caretMove; + + yield resumeDebuggerThenCloseAndFinish(panel); + }).then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_pretty-print-01.js b/toolkit/devtools/debugger/test/browser_dbg_pretty-print-01.js new file mode 100644 index 000000000..ccec068b0 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_pretty-print-01.js @@ -0,0 +1,82 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that clicking the pretty print button prettifies the source. + */ + +const TAB_URL = EXAMPLE_URL + "doc_pretty-print.html"; + +let gTab, gPanel, gDebugger; +let gEditor, gSources; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gSources = gDebugger.DebuggerView.Sources; + + waitForSourceShown(gPanel, "code_ugly.js") + .then(testSourceIsUgly) + .then(() => { + const finished = waitForSourceShown(gPanel, "code_ugly.js"); + clickPrettyPrintButton(); + testProgressBarShown(); + return finished; + }) + .then(testSourceIsPretty) + .then(testEditorShown) + .then(testSourceIsStillPretty) + .then(() => closeDebuggerAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + DevToolsUtils.safeErrorString(aError)); + }); + }); +} + +function testSourceIsUgly() { + ok(!gEditor.getText().contains("\n "), + "The source shouldn't be pretty printed yet."); +} + +function clickPrettyPrintButton() { + gDebugger.document.getElementById("pretty-print").click(); +} + +function testProgressBarShown() { + const deck = gDebugger.document.getElementById("editor-deck"); + is(deck.selectedIndex, 2, "The progress bar should be shown"); +} + +function testSourceIsPretty() { + ok(gEditor.getText().contains("\n "), + "The source should be pretty printed.") +} + +function testEditorShown() { + const deck = gDebugger.document.getElementById("editor-deck"); + is(deck.selectedIndex, 0, "The editor should be shown"); +} + +function testSourceIsStillPretty() { + const deferred = promise.defer(); + + const { source } = gSources.selectedItem.attachment; + gDebugger.DebuggerController.SourceScripts.getText(source).then(([, text]) => { + ok(text.contains("\n "), + "Subsequent calls to getText return the pretty printed source."); + deferred.resolve(); + }); + + return deferred.promise; +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gEditor = null; + gSources = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_pretty-print-02.js b/toolkit/devtools/debugger/test/browser_dbg_pretty-print-02.js new file mode 100644 index 000000000..26c938347 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_pretty-print-02.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that right clicking and selecting the pretty print context menu + * item prettifies the source. + */ + +const TAB_URL = EXAMPLE_URL + "doc_pretty-print.html"; + +let gTab, gPanel, gDebugger; +let gEditor, gContextMenu; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gContextMenu = gDebugger.document.getElementById("sourceEditorContextMenu"); + + waitForSourceShown(gPanel, "code_ugly.js") + .then(() => { + const finished = waitForSourceShown(gPanel, "code_ugly.js"); + selectContextMenuItem(); + return finished; + }) + .then(testSourceIsPretty) + .then(closeDebuggerAndFinish.bind(null, gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + DevToolsUtils.safeErrorString(aError)); + }); + }); +} + +function selectContextMenuItem() { + once(gContextMenu, "popupshown").then(() => { + const menuItem = gDebugger.document.getElementById("se-dbg-cMenu-prettyPrint"); + menuItem.click(); + }); + gContextMenu.openPopup(gEditor.container, "overlap", 0, 0, true, false); +} + +function testSourceIsPretty() { + ok(gEditor.getText().contains("\n "), + "The source should be pretty printed.") +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gEditor = null; + gContextMenu = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_pretty-print-03.js b/toolkit/devtools/debugger/test/browser_dbg_pretty-print-03.js new file mode 100644 index 000000000..888fbcc02 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_pretty-print-03.js @@ -0,0 +1,49 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that we have the correct line selected after pretty printing. + */ + +const TAB_URL = EXAMPLE_URL + "doc_pretty-print.html"; + +let gTab, gPanel, gDebugger; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + + waitForSourceShown(gPanel, "code_ugly.js") + .then(runCodeAndPause) + .then(() => { + const sourceShown = waitForSourceShown(gPanel, "code_ugly.js"); + const caretUpdated = waitForCaretUpdated(gPanel, 7); + const finished = promise.all([sourceShown, caretUpdated]); + clickPrettyPrintButton(); + return finished; + }) + .then(resumeDebuggerThenCloseAndFinish.bind(null, gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + DevToolsUtils.safeErrorString(aError)); + }); + }); +} + +function runCodeAndPause() { + const deferred = promise.defer(); + once(gDebugger.gThreadClient, "paused").then(deferred.resolve); + callInTab(gTab, "foo"); + return deferred.promise; +} + +function clickPrettyPrintButton() { + gDebugger.document.getElementById("pretty-print").click(); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_pretty-print-04.js b/toolkit/devtools/debugger/test/browser_dbg_pretty-print-04.js new file mode 100644 index 000000000..f0a19d671 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_pretty-print-04.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that the function searching works with pretty printed sources. + */ + +const TAB_URL = EXAMPLE_URL + "doc_pretty-print.html"; + +let gTab, gPanel, gDebugger; +let gSearchBox; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gSearchBox = gDebugger.DebuggerView.Filtering._searchbox; + + waitForSourceShown(gPanel, "code_ugly.js") + .then(testUglySearch) + .then(() => { + const finished = waitForSourceShown(gPanel, "code_ugly.js"); + clickPrettyPrintButton(); + return finished; + }) + .then(testPrettyPrintedSearch) + .then(() => closeDebuggerAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); +} + +function testUglySearch() { + const deferred = promise.defer(); + + once(gDebugger, "popupshown").then(() => { + ok(isCaretPos(gPanel, 2, 10), + "The bar function's non-pretty-printed location should be shown."); + deferred.resolve(); + }); + + setText(gSearchBox, "@bar"); + return deferred.promise; +} + +function clickPrettyPrintButton() { + gDebugger.document.getElementById("pretty-print").click(); +} + +function testPrettyPrintedSearch() { + const deferred = promise.defer(); + + once(gDebugger, "popupshown").then(() => { + ok(isCaretPos(gPanel, 6, 10), + "The bar function's pretty printed location should be shown."); + deferred.resolve(); + }); + + setText(gSearchBox, "@bar"); + return deferred.promise; +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gSearchBox = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_pretty-print-05.js b/toolkit/devtools/debugger/test/browser_dbg_pretty-print-05.js new file mode 100644 index 000000000..8bbaafdff --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_pretty-print-05.js @@ -0,0 +1,78 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that prettifying HTML sources doesn't do anything. + */ + +const TAB_URL = EXAMPLE_URL + "doc_included-script.html"; + +let gTab, gPanel, gDebugger; +let gEditor, gSources, gControllerSources; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gSources = gDebugger.DebuggerView.Sources; + gControllerSources = gDebugger.DebuggerController.SourceScripts; + + Task.spawn(function() { + yield waitForSourceShown(gPanel, TAB_URL); + + // From this point onward, the source editor's text should never change. + gEditor.once("change", () => { + ok(false, "The source editor text shouldn't have changed."); + }); + + is(getSelectedSourceURL(gSources), TAB_URL, + "The correct source is currently selected."); + ok(gEditor.getText().contains("myFunction"), + "The source shouldn't be pretty printed yet."); + + clickPrettyPrintButton(); + + let { source } = gSources.selectedItem.attachment; + try { + yield gControllerSources.togglePrettyPrint(source); + ok(false, "The promise for a prettified source should be rejected!"); + } catch ([source, error]) { + is(error, "Can't prettify non-javascript files.", + "The promise was correctly rejected with a meaningful message."); + } + + let text; + [source, text] = yield gControllerSources.getText(source); + is(getSelectedSourceURL(gSources), TAB_URL, + "The correct source is still selected."); + ok(gEditor.getText().contains("myFunction"), + "The displayed source hasn't changed."); + ok(text.contains("myFunction"), + "The cached source text wasn't altered in any way."); + + yield closeDebuggerAndFinish(gPanel); + }); + }); +} + +function clickPrettyPrintButton() { + gDebugger.document.getElementById("pretty-print").click(); +} + +function prepareDebugger(aPanel) { + aPanel._view.Sources.preferredSource = getSourceActor( + aPanel.panelWin.DebuggerView.Sources, + TAB_URL + ); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gEditor = null; + gSources = null; + gControllerSources = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_pretty-print-06.js b/toolkit/devtools/debugger/test/browser_dbg_pretty-print-06.js new file mode 100644 index 000000000..48f866935 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_pretty-print-06.js @@ -0,0 +1,92 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that prettifying JS sources with type errors works as expected. + */ + +const TAB_URL = EXAMPLE_URL + "doc_included-script.html"; +const JS_URL = EXAMPLE_URL + "code_location-changes.js"; + +let gTab, gPanel, gDebugger, gClient; +let gEditor, gSources, gControllerSources, gPrettyPrinted; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gClient = gDebugger.gClient; + gEditor = gDebugger.DebuggerView.editor; + gSources = gDebugger.DebuggerView.Sources; + gControllerSources = gDebugger.DebuggerController.SourceScripts; + + // We can't feed javascript files with syntax errors to the debugger, + // because they will never run, thus sometimes getting gc'd before the + // debugger is opened, or even before the target finishes navigating. + // Make the client lie about being able to parse perfectly fine code. + gClient.request = (function (aOriginalRequestMethod) { + return function (aPacket, aCallback) { + if (aPacket.type == "prettyPrint") { + gPrettyPrinted = true; + return executeSoon(() => aCallback({ error: "prettyPrintError" })); + } + return aOriginalRequestMethod(aPacket, aCallback); + }; + }(gClient.request)); + + Task.spawn(function() { + yield waitForSourceShown(gPanel, JS_URL); + + // From this point onward, the source editor's text should never change. + gEditor.once("change", () => { + ok(false, "The source editor text shouldn't have changed."); + }); + + is(getSelectedSourceURL(gSources), JS_URL, + "The correct source is currently selected."); + ok(gEditor.getText().contains("myFunction"), + "The source shouldn't be pretty printed yet."); + + clickPrettyPrintButton(); + + let { source } = gSources.selectedItem.attachment; + try { + yield gControllerSources.togglePrettyPrint(source); + ok(false, "The promise for a prettified source should be rejected!"); + } catch ([source, error]) { + ok(error.contains("prettyPrintError"), + "The promise was correctly rejected with a meaningful message."); + } + + let text; + [source, text] = yield gControllerSources.getText(source); + is(getSelectedSourceURL(gSources), JS_URL, + "The correct source is still selected."); + ok(gEditor.getText().contains("myFunction"), + "The displayed source hasn't changed."); + ok(text.contains("myFunction"), + "The cached source text wasn't altered in any way."); + + is(gPrettyPrinted, true, + "The hijacked pretty print method was executed."); + + yield closeDebuggerAndFinish(gPanel); + }); + }); +} + +function clickPrettyPrintButton() { + gDebugger.document.getElementById("pretty-print").click(); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gClient = null; + gEditor = null; + gSources = null; + gControllerSources = null; + gPrettyPrinted = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_pretty-print-07.js b/toolkit/devtools/debugger/test/browser_dbg_pretty-print-07.js new file mode 100644 index 000000000..718d23a81 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_pretty-print-07.js @@ -0,0 +1,57 @@ +/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test basic pretty printing functionality. Would be an xpcshell test, except +// for bug 921252. + +let gTab, gPanel, gClient, gThreadClient, gSource; + +const TAB_URL = EXAMPLE_URL + "doc_pretty-print-2.html"; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gClient = gPanel.panelWin.gClient; + gThreadClient = gPanel.panelWin.DebuggerController.activeThread; + + findSource(); + }); +} + +function findSource() { + gThreadClient.getSources(({ error, sources }) => { + ok(!error); + sources = sources.filter(s => s.url.contains('code_ugly-2.js')); + is(sources.length, 1); + gSource = sources[0]; + prettyPrintSource(); + }); +} + +function prettyPrintSource() { + gThreadClient.source(gSource).prettyPrint(4, testPrettyPrinted); +} + +function testPrettyPrinted({ error, source }) { + ok(!error, "Should not get an error while pretty-printing"); + ok(source.contains("\n "), + "Source should be pretty-printed"); + disablePrettyPrint(); +} + +function disablePrettyPrint() { + gThreadClient.source(gSource).disablePrettyPrint(testUgly); +} + +function testUgly({ error, source }) { + ok(!error, "Should not get an error while disabling pretty-printing"); + ok(!source.contains("\n "), + "Source should not be pretty after disabling pretty-printing"); + closeDebuggerAndFinish(gPanel); +} + +registerCleanupFunction(function() { + gTab = gPanel = gClient = gThreadClient = gSource = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_pretty-print-08.js b/toolkit/devtools/debugger/test/browser_dbg_pretty-print-08.js new file mode 100644 index 000000000..50c1f1c9b --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_pretty-print-08.js @@ -0,0 +1,94 @@ +/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test stepping through pretty printed sources. + +let gTab, gPanel, gClient, gThreadClient, gSource; + +const TAB_URL = EXAMPLE_URL + "doc_pretty-print-2.html"; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gClient = gPanel.panelWin.gClient; + gThreadClient = gPanel.panelWin.DebuggerController.activeThread; + + findSource(); + }); +} + +const BP_LOCATION = { + line: 5, + column: 11 +}; + +function findSource() { + gThreadClient.getSources(({ error, sources }) => { + ok(!error, "error should exist"); + sources = sources.filter(s => s.url.contains("code_ugly-3.js")); + is(sources.length, 1, "sources.length should be 1"); + [gSource] = sources; + BP_LOCATION.actor = gSource.actor; + + prettyPrintSource(sources[0]); + }); +} + +function prettyPrintSource(source) { + gThreadClient.source(gSource).prettyPrint(2, runCode); +} + +function runCode({ error }) { + ok(!error); + gClient.addOneTimeListener("paused", testDbgStatement); + callInTab(gTab, "main3"); +} + +function testDbgStatement(event, { why, frame }) { + is(why.type, "debuggerStatement"); + const { source, line, column } = frame.where; + is(source.actor, BP_LOCATION.actor, "source.actor should be the right actor"); + is(line, 3, "the line should be 3"); + setBreakpoint(); +} + +function setBreakpoint() { + gThreadClient.source(gSource).setBreakpoint( + { line: BP_LOCATION.line, + column: BP_LOCATION.column }, + ({ error, actualLocation }) => { + ok(!error, "error should not exist"); + ok(!actualLocation, "actualLocation should not exist"); + testStepping(); + } + ); +} + +function testStepping() { + gClient.addOneTimeListener("paused", (event, { why, frame }) => { + is(why.type, "resumeLimit"); + const { source, line } = frame.where; + is(source.actor, BP_LOCATION.actor, "source.actor should be the right actor"); + is(line, 4, "the line should be 4"); + testHitBreakpoint(); + }); + gThreadClient.stepIn(); +} + +function testHitBreakpoint() { + gClient.addOneTimeListener("paused", (event, { why, frame }) => { + is(why.type, "breakpoint"); + const { source, line } = frame.where; + is(source.actor, BP_LOCATION.actor, "source.actor should be the right actor"); + is(line, BP_LOCATION.line, "the line should the right line"); + + resumeDebuggerThenCloseAndFinish(gPanel); + }); + gThreadClient.resume(); +} + +registerCleanupFunction(function() { + gTab = gPanel = gClient = gThreadClient = gSource = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_pretty-print-09.js b/toolkit/devtools/debugger/test/browser_dbg_pretty-print-09.js new file mode 100644 index 000000000..fdc5a779c --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_pretty-print-09.js @@ -0,0 +1,87 @@ +/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test pretty printing source mapped sources. + +var gClient; +var gThreadClient; +var gSource; + +let gTab, gPanel, gClient, gThreadClient, gSource; + +const TAB_URL = EXAMPLE_URL + "doc_pretty-print-2.html"; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gClient = gPanel.panelWin.gClient; + gThreadClient = gPanel.panelWin.DebuggerController.activeThread; + + findSource(); + }); +} + +const dataUrl = s => "data:text/javascript," + s; + +// These should match the instructions in code_ugly-4.js. +const A = "function a(){b()}"; +const A_URL = dataUrl(A); +const B = "function b(){debugger}"; +const B_URL = dataUrl(B); + +function findSource() { + gThreadClient.getSources(({ error, sources }) => { + ok(!error); + sources = sources.filter(s => s.url === B_URL); + is(sources.length, 1); + gSource = sources[0]; + prettyPrint(); + }); +} + +function prettyPrint() { + gThreadClient.source(gSource).prettyPrint(2, runCode); +} + +function runCode({ error }) { + ok(!error); + gClient.addOneTimeListener("paused", testDbgStatement); + callInTab(gTab, "a"); +} + +function testDbgStatement(event, { frame, why }) { + is(why.type, "debuggerStatement"); + const { source, line } = frame.where; + is(source.url, B_URL); + is(line, 2); + + disablePrettyPrint(); +} + +function disablePrettyPrint() { + gThreadClient.source(gSource).disablePrettyPrint(testUgly); +} + +function testUgly({ error, source }) { + ok(!error); + ok(!source.contains("\n ")); + getFrame(); +} + +function getFrame() { + gThreadClient.getFrames(0, 1, testFrame); +} + +function testFrame({ frames: [frame] }) { + const { source, line } = frame.where; + is(source.url, B_URL); + is(line, 1); + + resumeDebuggerThenCloseAndFinish(gPanel); +} + +registerCleanupFunction(function() { + gTab = gPanel = gClient = gThreadClient = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_pretty-print-10.js b/toolkit/devtools/debugger/test/browser_dbg_pretty-print-10.js new file mode 100644 index 000000000..63d32b671 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_pretty-print-10.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that we disable the pretty print button for black boxed sources, + * and that clicking it doesn't do anything. + */ + +const TAB_URL = EXAMPLE_URL + "doc_pretty-print.html"; + +let gTab, gPanel, gDebugger; +let gEditor, gSources; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gSources = gDebugger.DebuggerView.Sources; + + waitForSourceShown(gPanel, "code_ugly.js") + .then(testSourceIsUgly) + .then(toggleBlackBoxing.bind(null, gPanel)) + .then(clickPrettyPrintButton) + .then(testSourceIsStillUgly) + .then(() => closeDebuggerAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + DevToolsUtils.safeErrorString(aError)); + }); + }); +} + +function testSourceIsUgly() { + ok(!gEditor.getText().contains("\n "), + "The source shouldn't be pretty printed yet."); +} + +function clickPrettyPrintButton() { + // Wait a tick before clicking to make sure the frontend's blackboxchange + // handlers have finished. + return new Promise(resolve => { + gDebugger.document.getElementById("pretty-print").click(); + resolve(); + }); +} + +function testSourceIsStillUgly() { + const { source } = gSources.selectedItem.attachment; + return gDebugger.DebuggerController.SourceScripts.getText(source).then(([, text]) => { + ok(!text.contains("\n ")); + }); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gEditor = null; + gSources = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_pretty-print-11.js b/toolkit/devtools/debugger/test/browser_dbg_pretty-print-11.js new file mode 100644 index 000000000..bced65ea8 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_pretty-print-11.js @@ -0,0 +1,58 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that pretty printing is maintained across refreshes. + */ + +const TAB_URL = EXAMPLE_URL + "doc_pretty-print.html"; + +let gTab, gPanel, gDebugger; +let gEditor, gSources; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gSources = gDebugger.DebuggerView.Sources; + + waitForSourceShown(gPanel, "code_ugly.js") + .then(testSourceIsUgly) + .then(() => { + const finished = waitForSourceShown(gPanel, "code_ugly.js"); + clickPrettyPrintButton(); + return finished; + }) + .then(testSourceIsPretty) + .then(reloadActiveTab.bind(null, gPanel, gDebugger.EVENTS.SOURCE_SHOWN)) + .then(testSourceIsPretty) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + DevToolsUtils.safeErrorString(aError)); + }); + }); +} + +function testSourceIsUgly() { + ok(!gEditor.getText().contains("\n "), + "The source shouldn't be pretty printed yet."); +} + +function clickPrettyPrintButton() { + gDebugger.document.getElementById("pretty-print").click(); +} + +function testSourceIsPretty() { + ok(gEditor.getText().contains("\n "), + "The source should be pretty printed.") +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gEditor = null; + gSources = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_pretty-print-12.js b/toolkit/devtools/debugger/test/browser_dbg_pretty-print-12.js new file mode 100644 index 000000000..6e56640d6 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_pretty-print-12.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that we don't leave the pretty print button checked when we fail to + * pretty print a source (because it isn't a JS file, for example). + */ + +const TAB_URL = EXAMPLE_URL + "doc_blackboxing.html"; + +let gTab, gPanel, gDebugger; +let gEditor, gSources; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gSources = gDebugger.DebuggerView.Sources; + + waitForSourceShown(gPanel, "") + .then(() => { + let shown = ensureSourceIs(gPanel, TAB_URL, true); + gSources.selectedValue = getSourceActor(gSources, TAB_URL); + return shown; + }) + .then(clickPrettyPrintButton) + .then(testButtonIsntChecked) + .then(() => closeDebuggerAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + DevToolsUtils.safeErrorString(aError)); + }); + }); +} + +function clickPrettyPrintButton() { + gDebugger.document.getElementById("pretty-print").click(); +} + +function testButtonIsntChecked() { + is(gDebugger.document.getElementById("pretty-print").checked, false, + "The button shouldn't be checked after trying to pretty print a non-js file."); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gEditor = null; + gSources = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_pretty-print-13.js b/toolkit/devtools/debugger/test/browser_dbg_pretty-print-13.js new file mode 100644 index 000000000..c7901befe --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_pretty-print-13.js @@ -0,0 +1,85 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that clicking the pretty print button prettifies the source, even + * when the source URL does not end in ".js", but the content type is + * JavaScript. + */ + +const TAB_URL = EXAMPLE_URL + "doc_pretty-print-3.html"; + +let gTab, gPanel, gDebugger; +let gEditor, gSources; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gSources = gDebugger.DebuggerView.Sources; + + promise.all([waitForSourceShown(gPanel, "code_ugly-8"), + waitForEditorLocationSet(gPanel)]) + .then(testSourceIsUgly) + .then(() => { + const finished = waitForSourceShown(gPanel, "code_ugly-8"); + clickPrettyPrintButton(); + testProgressBarShown(); + return finished; + }) + .then(testSourceIsPretty) + .then(testEditorShown) + .then(testSourceIsStillPretty) + .then(() => closeDebuggerAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + DevToolsUtils.safeErrorString(aError)); + }); + }); +} + +function testSourceIsUgly() { + ok(!gEditor.getText().contains("\n "), + "The source shouldn't be pretty printed yet."); +} + +function clickPrettyPrintButton() { + gDebugger.document.getElementById("pretty-print").click(); +} + +function testProgressBarShown() { + const deck = gDebugger.document.getElementById("editor-deck"); + is(deck.selectedIndex, 2, "The progress bar should be shown"); +} + +function testSourceIsPretty() { + ok(gEditor.getText().contains("\n "), + "The source should be pretty printed.") +} + +function testEditorShown() { + const deck = gDebugger.document.getElementById("editor-deck"); + is(deck.selectedIndex, 0, "The editor should be shown"); +} + +function testSourceIsStillPretty() { + const deferred = promise.defer(); + + const { source } = gSources.selectedItem.attachment; + gDebugger.DebuggerController.SourceScripts.getText(source).then(([, text]) => { + ok(text.contains("\n "), + "Subsequent calls to getText return the pretty printed source."); + deferred.resolve(); + }); + + return deferred.promise; +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gEditor = null; + gSources = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_pretty-print-on-paused.js b/toolkit/devtools/debugger/test/browser_dbg_pretty-print-on-paused.js new file mode 100644 index 000000000..faba77e5c --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_pretty-print-on-paused.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that pretty printing when the debugger is paused does not switch away + * from the selected source. + */ + +const TAB_URL = EXAMPLE_URL + "doc_pretty-print-on-paused.html"; + +let gTab, gPanel, gDebugger, gThreadClient, gSources; + +const SECOND_SOURCE_VALUE = EXAMPLE_URL + "code_ugly-2.js"; + +function test(){ + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gThreadClient = gDebugger.gThreadClient; + gSources = gDebugger.DebuggerView.Sources; + + Task.spawn(function* () { + try { + yield ensureSourceIs(gPanel, "code_script-switching-02.js", true); + + yield doInterrupt(gPanel); + + let source = gThreadClient.source(getSourceForm(gSources, SECOND_SOURCE_VALUE)); + yield rdpInvoke(source, source.setBreakpoint, { + line: 6 + }); + yield doResume(gPanel); + + const bpHit = waitForCaretAndScopes(gPanel, 6); + callInTab(gTab, "secondCall"); + yield bpHit; + + info("Switch to the second source."); + const sourceShown = waitForSourceShown(gPanel, SECOND_SOURCE_VALUE); + gSources.selectedValue = getSourceActor(gSources, SECOND_SOURCE_VALUE); + yield sourceShown; + + info("Pretty print the source."); + const prettyPrinted = waitForSourceShown(gPanel, SECOND_SOURCE_VALUE); + gDebugger.document.getElementById("pretty-print").click(); + yield prettyPrinted; + + yield resumeDebuggerThenCloseAndFinish(gPanel); + } catch (e) { + DevToolsUtils.reportException("browser_dbg_pretty-print-on-paused.js", e); + ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e)); + } + }); + }); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gThreadClient = null; + gSources = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_progress-listener-bug.js b/toolkit/devtools/debugger/test/browser_dbg_progress-listener-bug.js new file mode 100644 index 000000000..04aace77f --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_progress-listener-bug.js @@ -0,0 +1,85 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that the debugger does show up even if a progress listener reads the + * WebProgress argument's DOMWindow property in onStateChange() (bug 771655). + */ + +let gTab, gPanel, gDebugger; +let gOldListener; + +const TAB_URL = EXAMPLE_URL + "doc_inline-script.html"; + +function test() { + installListener(); + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + + is(!!gDebugger.DebuggerController._startup, true, + "Controller should be initialized after starting the test."); + is(!!gDebugger.DebuggerView._startup, true, + "View should be initialized after starting the test."); + + testPause(); + }); +} + +function testPause() { + waitForSourceAndCaretAndScopes(gPanel, ".html", 16).then(() => { + is(gDebugger.gThreadClient.state, "paused", + "The debugger statement was reached."); + + resumeDebuggerThenCloseAndFinish(gPanel); + }); + + callInTab(gTab, "runDebuggerStatement"); +} + +// This is taken almost verbatim from bug 771655. +function installListener() { + if ("_testPL" in window) { + gOldListener = _testPL; + + Cc['@mozilla.org/docloaderservice;1'] + .getService(Ci.nsIWebProgress) + .removeProgressListener(_testPL); + } + + window._testPL = { + START_DOC: Ci.nsIWebProgressListener.STATE_START | + Ci.nsIWebProgressListener.STATE_IS_DOCUMENT, + onStateChange: function(wp, req, stateFlags, status) { + if ((stateFlags & this.START_DOC) === this.START_DOC) { + // This DOMWindow access triggers the unload event. + wp.DOMWindow; + } + }, + QueryInterface: function(iid) { + if (iid.equals(Ci.nsISupportsWeakReference) || + iid.equals(Ci.nsIWebProgressListener)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + } + } + + Cc['@mozilla.org/docloaderservice;1'] + .getService(Ci.nsIWebProgress) + .addProgressListener(_testPL, Ci.nsIWebProgress.NOTIFY_STATE_REQUEST); +} + +registerCleanupFunction(function() { + if (gOldListener) { + window._testPL = gOldListener; + } else { + delete window._testPL; + } + + gTab = null; + gPanel = null; + gDebugger = null; + gOldListener = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_reload-preferred-script-01.js b/toolkit/devtools/debugger/test/browser_dbg_reload-preferred-script-01.js new file mode 100644 index 000000000..c1a889bfb --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_reload-preferred-script-01.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the preferred source is shown when a page is loaded and + * the preferred source is specified before any other source was shown. + */ + +const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html"; +const PREFERRED_URL = EXAMPLE_URL + "code_script-switching-02.js"; + +let gTab, gDebuggee, gPanel, gDebugger; +let gSources; + +function test() { + initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => { + gTab = aTab; + gDebuggee = aDebuggee; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gSources = gDebugger.DebuggerView.Sources; + + waitForSourceShown(gPanel, PREFERRED_URL).then(finishTest); + }); +} + +function finishTest() { + info("Currently preferred source: " + gSources.preferredValue); + info("Currently selected source: " + gSources.selectedValue); + + is(getSourceURL(gSources, gSources.preferredValue), PREFERRED_URL, + "The preferred source url wasn't set correctly."); + is(getSourceURL(gSources, gSources.selectedValue), PREFERRED_URL, + "The selected source isn't the correct one."); + + closeDebuggerAndFinish(gPanel); +} + +function prepareDebugger(aPanel) { + let sources = aPanel._view.Sources; + sources.preferredSource = getSourceActor(sources, PREFERRED_URL); +} + +registerCleanupFunction(function() { + gTab = null; + gDebuggee = null; + gPanel = null; + gDebugger = null; + gSources = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_reload-preferred-script-02.js b/toolkit/devtools/debugger/test/browser_dbg_reload-preferred-script-02.js new file mode 100644 index 000000000..60fb571b5 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_reload-preferred-script-02.js @@ -0,0 +1,44 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the preferred source is shown when a page is loaded and + * the preferred source is specified after another source might have been shown. + */ + +const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html"; +const PREFERRED_URL = EXAMPLE_URL + "code_script-switching-02.js"; + +let gTab, gPanel, gDebugger; +let gSources; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gSources = gDebugger.DebuggerView.Sources; + + waitForSourceShown(gPanel, PREFERRED_URL).then(finishTest); + gSources.preferredSource = getSourceActor(gSources, PREFERRED_URL); + }); +} + +function finishTest() { + info("Currently preferred source: " + gSources.preferredValue); + info("Currently selected source: " + gSources.selectedValue); + + is(getSourceURL(gSources, gSources.preferredValue), PREFERRED_URL, + "The preferred source url wasn't set correctly."); + is(getSourceURL(gSources, gSources.selectedValue), PREFERRED_URL, + "The selected source isn't the correct one."); + + closeDebuggerAndFinish(gPanel); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gSources = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_reload-preferred-script-03.js b/toolkit/devtools/debugger/test/browser_dbg_reload-preferred-script-03.js new file mode 100644 index 000000000..01cd51869 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_reload-preferred-script-03.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the preferred source is shown when a page is loaded and + * the preferred source is specified after another source was definitely shown. + */ + +const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html"; +const FIRST_URL = EXAMPLE_URL + "code_script-switching-01.js"; +const SECOND_URL = EXAMPLE_URL + "code_script-switching-02.js"; + +let gTab, gPanel, gDebugger; +let gSources; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gSources = gDebugger.DebuggerView.Sources; + + waitForSourceShown(gPanel, FIRST_URL) + .then(() => testSource(undefined, FIRST_URL)) + .then(() => switchToSource(SECOND_URL)) + .then(() => testSource(SECOND_URL)) + .then(() => switchToSource(FIRST_URL)) + .then(() => testSource(FIRST_URL)) + .then(() => closeDebuggerAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); +} + +function testSource(aPreferredUrl, aSelectedUrl = aPreferredUrl) { + info("Currently preferred source: " + gSources.preferredValue); + info("Currently selected source: " + gSources.selectedValue); + + is(getSourceURL(gSources, gSources.preferredValue), aPreferredUrl, + "The preferred source url wasn't set correctly."); + is(getSourceURL(gSources, gSources.selectedValue), aSelectedUrl, + "The selected source isn't the correct one."); +} + +function switchToSource(aUrl) { + let finished = waitForSourceShown(gPanel, aUrl); + gSources.preferredSource = getSourceActor(gSources, aUrl); + return finished; +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gSources = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_reload-same-script.js b/toolkit/devtools/debugger/test/browser_dbg_reload-same-script.js new file mode 100644 index 000000000..e03505458 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_reload-same-script.js @@ -0,0 +1,77 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the same source is shown after a page is reloaded. + */ + +const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html"; +const FIRST_URL = EXAMPLE_URL + "code_script-switching-01.js"; +const SECOND_URL = EXAMPLE_URL + "code_script-switching-02.js"; + +function test() { + // Debug test slaves are a bit slow at this test. + requestLongerTimeout(2); + + let gTab, gPanel, gDebugger; + let gSources, gStep; + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = aPanel.panelWin; + gSources = gDebugger.DebuggerView.Sources; + gStep = 0; + + waitForSourceShown(gPanel, FIRST_URL).then(performTest); + }); + + function performTest() { + switch (gStep++) { + case 0: + testCurrentSource(FIRST_URL, null); + reload().then(performTest); + break; + case 1: + testCurrentSource(FIRST_URL); + reload().then(performTest); + break; + case 2: + testCurrentSource(FIRST_URL); + switchAndReload(SECOND_URL).then(performTest); + break; + case 3: + testCurrentSource(SECOND_URL); + reload().then(performTest); + break; + case 4: + testCurrentSource(SECOND_URL); + reload().then(performTest); + break; + case 5: + testCurrentSource(SECOND_URL); + closeDebuggerAndFinish(gPanel); + break; + } + } + + function reload() { + return reloadActiveTab(gPanel, gDebugger.EVENTS.SOURCES_ADDED); + } + + function switchAndReload(aUrl) { + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN).then(reload); + gSources.selectedValue = getSourceActor(gSources, aUrl); + return finished; + } + + function testCurrentSource(aUrl, aExpectedUrl = aUrl) { + info("Currently preferred source: '" + gSources.preferredValue + "'."); + info("Currently selected source: '" + gSources.selectedValue + "'."); + + is(getSourceURL(gSources, gSources.preferredValue), aExpectedUrl, + "The preferred source url wasn't set correctly (" + gStep + ")."); + is(getSourceURL(gSources, gSources.selectedValue), aUrl, + "The selected source isn't the correct one (" + gStep + ")."); + } +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_scripts-switching-01.js b/toolkit/devtools/debugger/test/browser_dbg_scripts-switching-01.js new file mode 100644 index 000000000..f2202bd2d --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_scripts-switching-01.js @@ -0,0 +1,192 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that switching the displayed source in the UI works as advertised. + */ + +const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html"; + +let gTab, gPanel, gDebugger; +let gEditor, gSources; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gSources = gDebugger.DebuggerView.Sources; + + ok(gDebugger.document.title.endsWith(EXAMPLE_URL + gLabel1), + "Title with first source is correct."); + + waitForSourceAndCaretAndScopes(gPanel, "-02.js", 1) + .then(testSourcesDisplay) + .then(testSwitchPaused1) + .then(testSwitchPaused2) + .then(testSwitchRunning) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + + callInTab(gTab, "firstCall"); + }); +} + +let gLabel1 = "code_script-switching-01.js"; +let gLabel2 = "code_script-switching-02.js"; + +function testSourcesDisplay() { + let deferred = promise.defer(); + + is(gSources.itemCount, 2, + "Found the expected number of sources."); + + is(gSources.items[0].target.querySelector(".dbg-source-item").getAttribute("tooltiptext"), + EXAMPLE_URL + "code_script-switching-01.js", + "The correct tooltip text is displayed for the first source."); + is(gSources.items[1].target.querySelector(".dbg-source-item").getAttribute("tooltiptext"), + EXAMPLE_URL + "code_script-switching-02.js", + "The correct tooltip text is displayed for the second source."); + + ok(getSourceActor(gSources, EXAMPLE_URL + gLabel1), + "First source url is incorrect."); + ok(getSourceActor(gSources, EXAMPLE_URL + gLabel2), + "Second source url is incorrect."); + + ok(gSources.getItemForAttachment(e => e.label == gLabel1), + "First source label is incorrect."); + ok(gSources.getItemForAttachment(e => e.label == gLabel2), + "Second source label is incorrect."); + + ok(gSources.selectedItem, + "There should be a selected item in the sources pane."); + is(getSelectedSourceURL(gSources), EXAMPLE_URL + gLabel2, + "The selected value is the sources pane is incorrect."); + + is(gEditor.getText().search(/firstCall/), -1, + "The first source is not displayed."); + is(gEditor.getText().search(/debugger/), 166, + "The second source is displayed."); + + ok(gDebugger.document.title.endsWith(EXAMPLE_URL + gLabel2), + "Title with second source is correct."); + + ok(isCaretPos(gPanel, 1), + "Editor caret location is correct."); + + // The editor's debug location takes a tick to update. + executeSoon(() => { + is(gEditor.getDebugLocation(), 5, + "Editor debugger location is correct."); + ok(gEditor.hasLineClass(5, "debug-line"), + "The debugged line is highlighted appropriately (1)."); + + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN).then(deferred.resolve); + gSources.selectedIndex = 0; + }); + + return deferred.promise; +} + +function testSwitchPaused1() { + let deferred = promise.defer(); + + ok(gSources.selectedItem, + "There should be a selected item in the sources pane."); + is(getSelectedSourceURL(gSources), EXAMPLE_URL + gLabel1, + "The selected value is the sources pane is incorrect."); + + is(gEditor.getText().search(/firstCall/), 118, + "The first source is displayed."); + is(gEditor.getText().search(/debugger/), -1, + "The second source is not displayed."); + + // The editor's debug location takes a tick to update. + executeSoon(() => { + ok(isCaretPos(gPanel, 1), + "Editor caret location is correct."); + is(gEditor.getDebugLocation(), null, + "Editor debugger location is correct."); + ok(!gEditor.hasLineClass(5, "debug-line"), + "The debugged line highlight was removed."); + + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN).then(deferred.resolve); + gSources.selectedIndex = 1; + }); + + return deferred.promise; +} + +function testSwitchPaused2() { + let deferred = promise.defer(); + + ok(gSources.selectedItem, + "There should be a selected item in the sources pane."); + is(getSelectedSourceURL(gSources), EXAMPLE_URL + gLabel2, + "The selected value is the sources pane is incorrect."); + + is(gEditor.getText().search(/firstCall/), -1, + "The first source is not displayed."); + is(gEditor.getText().search(/debugger/), 166, + "The second source is displayed."); + + // The editor's debug location takes a tick to update. + executeSoon(() => { + ok(isCaretPos(gPanel, 6), + "Editor caret location is correct."); + is(gEditor.getDebugLocation(), 5, + "Editor debugger location is correct."); + ok(gEditor.hasLineClass(5, "debug-line"), + "The debugged line is highlighted appropriately (2)."); + + // Step out twice. + waitForThreadEvents(gPanel, "paused").then(() => { + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN).then(deferred.resolve); + gDebugger.gThreadClient.stepOut(); + }) + gDebugger.gThreadClient.stepOut(); + }); + + return deferred.promise; +} + +function testSwitchRunning() { + let deferred = promise.defer(); + + ok(gSources.selectedItem, + "There should be a selected item in the sources pane."); + is(getSelectedSourceURL(gSources), EXAMPLE_URL + gLabel1, + "The selected value is the sources pane is incorrect."); + + is(gEditor.getText().search(/firstCall/), 118, + "The first source is displayed."); + is(gEditor.getText().search(/debugger/), -1, + "The second source is not displayed."); + + // The editor's debug location takes a tick to update. + executeSoon(() => { + ok(isCaretPos(gPanel, 5), + "Editor caret location is correct."); + is(gEditor.getDebugLocation(), 4, + "Editor debugger location is correct."); + ok(gEditor.hasLineClass(4, "debug-line"), + "The debugged line is highlighted appropriately (3)."); + + deferred.resolve(); + }); + + return deferred.promise; +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gEditor = null; + gSources = null; + gLabel1 = null; + gLabel2 = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_scripts-switching-02.js b/toolkit/devtools/debugger/test/browser_dbg_scripts-switching-02.js new file mode 100644 index 000000000..a1cdade16 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_scripts-switching-02.js @@ -0,0 +1,182 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that switching the displayed source in the UI works as advertised. + */ + +const TAB_URL = EXAMPLE_URL + "doc_script-switching-02.html"; + +let gTab, gPanel, gDebugger; +let gEditor, gSources; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gSources = gDebugger.DebuggerView.Sources; + + waitForSourceAndCaretAndScopes(gPanel, "-02.js", 1) + .then(testSourcesDisplay) + .then(testSwitchPaused1) + .then(testSwitchPaused2) + .then(testSwitchRunning) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + + callInTab(gTab, "firstCall"); + }); +} + +let gLabel1 = "code_script-switching-01.js"; +let gLabel2 = "code_script-switching-02.js"; +let gParams = "?foo=bar,baz|lol"; + +function testSourcesDisplay() { + let deferred = promise.defer(); + + is(gSources.itemCount, 2, + "Found the expected number of sources."); + + ok(getSourceActor(gSources, EXAMPLE_URL + gLabel1), + "First source url is incorrect."); + ok(getSourceActor(gSources, EXAMPLE_URL + gLabel2 + gParams), + "Second source url is incorrect."); + + ok(gSources.getItemForAttachment(e => e.label == gLabel1), + "First source label is incorrect."); + ok(gSources.getItemForAttachment(e => e.label == gLabel2), + "Second source label is incorrect."); + + ok(gSources.selectedItem, + "There should be a selected item in the sources pane."); + is(getSelectedSourceURL(gSources), EXAMPLE_URL + gLabel2 + gParams, + "The selected value is the sources pane is incorrect."); + + is(gEditor.getText().search(/firstCall/), -1, + "The first source is not displayed."); + is(gEditor.getText().search(/debugger/), 166, + "The second source is displayed."); + + ok(isCaretPos(gPanel, 1), + "Editor caret location is correct."); + + // The editor's debug location takes a tick to update. + executeSoon(() => { + is(gEditor.getDebugLocation(), 5, + "Editor debugger location is correct."); + ok(gEditor.hasLineClass(5, "debug-line"), + "The debugged line is highlighted appropriately."); + + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN).then(deferred.resolve); + gSources.selectedItem = e => e.attachment.label == gLabel1; + }); + + return deferred.promise; +} + +function testSwitchPaused1() { + let deferred = promise.defer(); + + ok(gSources.selectedItem, + "There should be a selected item in the sources pane."); + is(getSelectedSourceURL(gSources), EXAMPLE_URL + gLabel1, + "The selected value is the sources pane is incorrect."); + + is(gEditor.getText().search(/firstCall/), 118, + "The first source is displayed."); + is(gEditor.getText().search(/debugger/), -1, + "The second source is not displayed."); + + // The editor's debug location takes a tick to update. + executeSoon(() => { + ok(isCaretPos(gPanel, 1), + "Editor caret location is correct."); + + is(gEditor.getDebugLocation(), null, + "Editor debugger location is correct."); + ok(!gEditor.hasLineClass(5, "debug-line"), + "The debugged line highlight was removed."); + + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN).then(deferred.resolve); + gSources.selectedItem = e => e.attachment.label == gLabel2; + }); + + return deferred.promise; +} + +function testSwitchPaused2() { + let deferred = promise.defer(); + + ok(gSources.selectedItem, + "There should be a selected item in the sources pane."); + is(getSelectedSourceURL(gSources), EXAMPLE_URL + gLabel2 + gParams, + "The selected value is the sources pane is incorrect."); + + is(gEditor.getText().search(/firstCall/), -1, + "The first source is not displayed."); + is(gEditor.getText().search(/debugger/), 166, + "The second source is displayed."); + + // The editor's debug location takes a tick to update. + executeSoon(() => { + ok(isCaretPos(gPanel, 6), + "Editor caret location is correct."); + is(gEditor.getDebugLocation(), 5, + "Editor debugger location is correct."); + ok(gEditor.hasLineClass(5, "debug-line"), + "The debugged line is highlighted appropriately."); + + // Step out three times. + waitForThreadEvents(gPanel, "paused").then(() => { + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN).then(deferred.resolve); + gDebugger.gThreadClient.stepOut(); + }); + gDebugger.gThreadClient.stepOut(); + }); + + return deferred.promise; +} + +function testSwitchRunning() { + let deferred = promise.defer(); + + ok(gSources.selectedItem, + "There should be a selected item in the sources pane."); + is(getSelectedSourceURL(gSources), EXAMPLE_URL + gLabel1, + "The selected value is the sources pane is incorrect."); + + is(gEditor.getText().search(/firstCall/), 118, + "The first source is displayed."); + is(gEditor.getText().search(/debugger/), -1, + "The second source is not displayed."); + + // The editor's debug location takes a tick to update. + executeSoon(() => { + ok(isCaretPos(gPanel, 5), + "Editor caret location is correct."); + is(gEditor.getDebugLocation(), 4, + "Editor debugger location is correct."); + ok(gEditor.hasLineClass(4, "debug-line"), + "The debugged line is highlighted appropriately."); + + deferred.resolve(); + }); + + return deferred.promise; +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gEditor = null; + gSources = null; + gLabel1 = null; + gLabel2 = null; + gParams = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_scripts-switching-03.js b/toolkit/devtools/debugger/test/browser_dbg_scripts-switching-03.js new file mode 100644 index 000000000..ddc34428a --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_scripts-switching-03.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that the DebuggerView error loading source text is correct. + */ + +const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html"; + +let gTab, gPanel, gDebugger; +let gView, gEditor, gL10N; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gView = gDebugger.DebuggerView; + gEditor = gDebugger.DebuggerView.editor; + gL10N = gDebugger.L10N; + + waitForSourceShown(gPanel, "-01.js") + .then(showBogusSource) + .then(testDebuggerLoadingError) + .then(() => closeDebuggerAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); +} + +function showBogusSource() { + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_ERROR_SHOWN); + gView._setEditorSource({ url: "http://example.com/fake.js", actor: "fake.actor" }); + return finished; +} + +function testDebuggerLoadingError() { + ok(gEditor.getText().contains(gL10N.getStr("errorLoadingText")), + "The valid error loading message is displayed."); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gView = null; + gEditor = null; + gL10N = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_search-autofill-identifier.js b/toolkit/devtools/debugger/test/browser_dbg_search-autofill-identifier.js new file mode 100644 index 000000000..60c184994 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_search-autofill-identifier.js @@ -0,0 +1,135 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that Debugger Search uses the identifier under cursor if nothing is + * selected or manually passed and searching using certain operators. + */ +"use strict"; + +function test() { + const TAB_URL = EXAMPLE_URL + "doc_function-search.html"; + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + let Source = 'code_function-search-01.js'; + let Debugger = aPanel.panelWin; + let Editor = Debugger.DebuggerView.editor; + let Filtering = Debugger.DebuggerView.Filtering; + + function doSearch(aOperator) { + Editor.dropSelection(); + Filtering._doSearch(aOperator); + } + + waitForSourceShown(aPanel, Source).then(() => { + info("Testing with cursor at the beginning of the file..."); + + doSearch(); + is(Filtering._searchbox.value, "", + "The searchbox value should not be auto-filled when searching for files."); + is(Filtering._searchbox.selectionStart, Filtering._searchbox.selectionEnd, + "The searchbox contents should not be selected"); + is(Editor.getSelection(), "", + "The selection in the editor should be empty."); + + doSearch("!"); + is(Filtering._searchbox.value, "!", + "The searchbox value should not be auto-filled when searching across all files."); + is(Filtering._searchbox.selectionStart, Filtering._searchbox.selectionEnd, + "The searchbox contents should not be selected"); + is(Editor.getSelection(), "", + "The selection in the editor should be empty."); + + doSearch("@"); + is(Filtering._searchbox.value, "@", + "The searchbox value should not be auto-filled when searching for functions."); + is(Filtering._searchbox.selectionStart, Filtering._searchbox.selectionEnd, + "The searchbox contents should not be selected"); + is(Editor.getSelection(), "", + "The selection in the editor should be empty."); + + doSearch("#"); + is(Filtering._searchbox.value, "#", + "The searchbox value should not be auto-filled when searching inside a file."); + is(Filtering._searchbox.selectionStart, Filtering._searchbox.selectionEnd, + "The searchbox contents should not be selected"); + is(Editor.getSelection(), "", + "The selection in the editor should be empty."); + + doSearch(":"); + is(Filtering._searchbox.value, ":", + "The searchbox value should not be auto-filled when searching for a line."); + is(Filtering._searchbox.selectionStart, Filtering._searchbox.selectionEnd, + "The searchbox contents should not be selected"); + is(Editor.getSelection(), "", + "The selection in the editor should be empty."); + + doSearch("*"); + is(Filtering._searchbox.value, "*", + "The searchbox value should not be auto-filled when searching for variables."); + is(Filtering._searchbox.selectionStart, Filtering._searchbox.selectionEnd, + "The searchbox contents should not be selected"); + is(Editor.getSelection(), "", + "The selection in the editor should be empty."); + + Editor.setCursor({ line: 7, ch: 0}); + info("Testing with cursor at line 8 and char 1..."); + + doSearch(); + is(Filtering._searchbox.value, "", + "The searchbox value should not be auto-filled when searching for files."); + is(Filtering._searchbox.selectionStart, Filtering._searchbox.selectionEnd, + "The searchbox contents should not be selected"); + is(Editor.getSelection(), "", + "The selection in the editor should be empty."); + + doSearch("!"); + is(Filtering._searchbox.value, "!test", + "The searchbox value was incorrect when searching across all files."); + is(Filtering._searchbox.selectionStart, 1, + "The searchbox operator should not be selected"); + is(Filtering._searchbox.selectionEnd, 5, + "The searchbox contents should be selected"); + is(Editor.getSelection(), "", + "The selection in the editor should be empty."); + + doSearch("@"); + is(Filtering._searchbox.value, "@test", + "The searchbox value was incorrect when searching for functions."); + is(Filtering._searchbox.selectionStart, 1, + "The searchbox operator should not be selected"); + is(Filtering._searchbox.selectionEnd, 5, + "The searchbox contents should be selected"); + is(Editor.getSelection(), "", + "The selection in the editor should be empty."); + + doSearch("#"); + is(Filtering._searchbox.value, "#test", + "The searchbox value should be auto-filled when searching inside a file."); + is(Filtering._searchbox.selectionStart, 1, + "The searchbox operator should not be selected"); + is(Filtering._searchbox.selectionEnd, 5, + "The searchbox contents should be selected"); + is(Editor.getSelection(), "test", + "The selection in the editor should be 'test'."); + + doSearch(":"); + is(Filtering._searchbox.value, ":", + "The searchbox value should not be auto-filled when searching for a line."); + is(Filtering._searchbox.selectionStart, Filtering._searchbox.selectionEnd, + "The searchbox contents should not be selected"); + is(Editor.getSelection(), "", + "The selection in the editor should be empty."); + + doSearch("*"); + is(Filtering._searchbox.value, "*", + "The searchbox value should not be auto-filled when searching for variables."); + is(Filtering._searchbox.selectionStart, Filtering._searchbox.selectionEnd, + "The searchbox contents should not be selected"); + is(Editor.getSelection(), "", + "The selection in the editor should be empty."); + + closeDebuggerAndFinish(aPanel); + }); + }); +}; diff --git a/toolkit/devtools/debugger/test/browser_dbg_search-basic-01.js b/toolkit/devtools/debugger/test/browser_dbg_search-basic-01.js new file mode 100644 index 000000000..5a746a5b3 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_search-basic-01.js @@ -0,0 +1,317 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests basic search functionality (find token and jump to line). + */ + +const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html"; + +let gTab, gPanel, gDebugger; +let gEditor, gSources, gFiltering, gSearchBox; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gSources = gDebugger.DebuggerView.Sources; + gFiltering = gDebugger.DebuggerView.Filtering; + gSearchBox = gDebugger.DebuggerView.Filtering._searchbox; + + waitForSourceShown(gPanel, ".html").then(performTest); + }); +} + +function performTest() { + setText(gSearchBox, "#html"); + + EventUtils.synthesizeKey("VK_RETURN", { shiftKey: true }, gDebugger); + is(gFiltering.searchData.toSource(), '["#", ["", "html"]]', + "The searchbox data wasn't parsed correctly."); + ok(isCaretPos(gPanel, 35, 7), + "The editor didn't jump to the correct line."); + + EventUtils.synthesizeKey("VK_RETURN", { shiftKey: true }, gDebugger); + is(gFiltering.searchData.toSource(), '["#", ["", "html"]]', + "The searchbox data wasn't parsed correctly."); + ok(isCaretPos(gPanel, 5, 6), + "The editor didn't jump to the correct line."); + + EventUtils.synthesizeKey("VK_RETURN", { shiftKey: true }, gDebugger); + is(gFiltering.searchData.toSource(), '["#", ["", "html"]]', + "The searchbox data wasn't parsed correctly."); + ok(isCaretPos(gPanel, 3, 15), + "The editor didn't jump to the correct line."); + + setText(gSearchBox, ":12"); + is(gFiltering.searchData.toSource(), '[":", ["", 12]]', + "The searchbox data wasn't parsed correctly."); + ok(isCaretPos(gPanel, 12), + "The editor didn't jump to the correct line."); + + EventUtils.synthesizeKey("g", { metaKey: true }, gDebugger); + is(gFiltering.searchData.toSource(), '[":", ["", 13]]', + "The searchbox data wasn't parsed correctly."); + ok(isCaretPos(gPanel, 13), + "The editor didn't jump to the correct line after Meta+G."); + + EventUtils.synthesizeKey("n", { ctrlKey: true }, gDebugger); + is(gFiltering.searchData.toSource(), '[":", ["", 14]]', + "The searchbox data wasn't parsed correctly."); + ok(isCaretPos(gPanel, 14), + "The editor didn't jump to the correct line after Ctrl+N."); + + EventUtils.synthesizeKey("G", { metaKey: true, shiftKey: true }, gDebugger); + is(gFiltering.searchData.toSource(), '[":", ["", 13]]', + "The searchbox data wasn't parsed correctly."); + ok(isCaretPos(gPanel, 13), + "The editor didn't jump to the correct line after Meta+Shift+G."); + + EventUtils.synthesizeKey("p", { ctrlKey: true }, gDebugger); + is(gFiltering.searchData.toSource(), '[":", ["", 12]]', + "The searchbox data wasn't parsed correctly."); + ok(isCaretPos(gPanel, 12), + "The editor didn't jump to the correct line after Ctrl+P."); + + for (let i = 0; i < 100; i++) { + EventUtils.sendKey("DOWN", gDebugger); + } + is(gFiltering.searchData.toSource(), '[":", ["", 36]]', + "The searchbox data wasn't parsed correctly."); + ok(isCaretPos(gPanel, 36), + "The editor didn't jump to the correct line after multiple DOWN keys."); + + for (let i = 0; i < 100; i++) { + EventUtils.sendKey("UP", gDebugger); + } + is(gFiltering.searchData.toSource(), '[":", ["", 1]]', + "The searchbox data wasn't parsed correctly."); + ok(isCaretPos(gPanel, 1), + "The editor didn't jump to the correct line after multiple UP keys."); + + + let token = "debugger"; + setText(gSearchBox, "#" + token); + is(gFiltering.searchData.toSource(), '["#", ["", "debugger"]]', + "The searchbox data wasn't parsed correctly."); + ok(isCaretPos(gPanel, 8, 12 + token.length), + "The editor didn't jump to the correct token (1)."); + + EventUtils.sendKey("DOWN", gDebugger); + is(gFiltering.searchData.toSource(), '["#", ["", "debugger"]]', + "The searchbox data wasn't parsed correctly."); + ok(isCaretPos(gPanel, 14, 9 + token.length), + "The editor didn't jump to the correct token (2)."); + + EventUtils.sendKey("DOWN", gDebugger); + is(gFiltering.searchData.toSource(), '["#", ["", "debugger"]]', + "The searchbox data wasn't parsed correctly."); + ok(isCaretPos(gPanel, 18, 15 + token.length), + "The editor didn't jump to the correct token (3)."); + + EventUtils.sendKey("RETURN", gDebugger); + is(gFiltering.searchData.toSource(), '["#", ["", "debugger"]]', + "The searchbox data wasn't parsed correctly."); + ok(isCaretPos(gPanel, 26, 11 + token.length), + "The editor didn't jump to the correct token (4)."); + + EventUtils.sendKey("RETURN", gDebugger); + is(gFiltering.searchData.toSource(), '["#", ["", "debugger"]]', + "The searchbox data wasn't parsed correctly."); + ok(isCaretPos(gPanel, 8, 12 + token.length), + "The editor didn't jump to the correct token (5)."); + + EventUtils.sendKey("UP", gDebugger); + is(gFiltering.searchData.toSource(), '["#", ["", "debugger"]]', + "The searchbox data wasn't parsed correctly."); + ok(isCaretPos(gPanel, 26, 11 + token.length), + "The editor didn't jump to the correct token (6)."); + + setText(gSearchBox, ":bogus#" + token + ";"); + is(gFiltering.searchData.toSource(), '["#", [":bogus", "debugger;"]]', + "The searchbox data wasn't parsed correctly."); + ok(isCaretPos(gPanel, 14, 9 + token.length + 1), + "The editor didn't jump to the correct token (7)."); + + setText(gSearchBox, ":13#" + token + ";"); + is(gFiltering.searchData.toSource(), '["#", [":13", "debugger;"]]', + "The searchbox data wasn't parsed correctly."); + ok(isCaretPos(gPanel, 14, 9 + token.length + 1), + "The editor didn't jump to the correct token (8)."); + + setText(gSearchBox, ":#" + token + ";"); + is(gFiltering.searchData.toSource(), '["#", [":", "debugger;"]]', + "The searchbox data wasn't parsed correctly."); + ok(isCaretPos(gPanel, 14, 9 + token.length + 1), + "The editor didn't jump to the correct token (9)."); + + setText(gSearchBox, "::#" + token + ";"); + is(gFiltering.searchData.toSource(), '["#", ["::", "debugger;"]]', + "The searchbox data wasn't parsed correctly."); + ok(isCaretPos(gPanel, 14, 9 + token.length + 1), + "The editor didn't jump to the correct token (10)."); + + setText(gSearchBox, ":::#" + token + ";"); + is(gFiltering.searchData.toSource(), '["#", [":::", "debugger;"]]', + "The searchbox data wasn't parsed correctly."); + ok(isCaretPos(gPanel, 14, 9 + token.length + 1), + "The editor didn't jump to the correct token (11)."); + + + setText(gSearchBox, "#" + token + ";" + ":bogus"); + is(gFiltering.searchData.toSource(), '["#", ["", "debugger;:bogus"]]', + "The searchbox data wasn't parsed correctly."); + ok(isCaretPos(gPanel, 14, 9 + token.length + 1), + "The editor didn't jump to the correct token (12)."); + + setText(gSearchBox, "#" + token + ";" + ":13"); + is(gFiltering.searchData.toSource(), '["#", ["", "debugger;:13"]]', + "The searchbox data wasn't parsed correctly."); + ok(isCaretPos(gPanel, 14, 9 + token.length + 1), + "The editor didn't jump to the correct token (13)."); + + setText(gSearchBox, "#" + token + ";" + ":"); + is(gFiltering.searchData.toSource(), '["#", ["", "debugger;:"]]', + "The searchbox data wasn't parsed correctly."); + ok(isCaretPos(gPanel, 14, 9 + token.length + 1), + "The editor didn't jump to the correct token (14)."); + + setText(gSearchBox, "#" + token + ";" + "::"); + is(gFiltering.searchData.toSource(), '["#", ["", "debugger;::"]]', + "The searchbox data wasn't parsed correctly."); + ok(isCaretPos(gPanel, 14, 9 + token.length + 1), + "The editor didn't jump to the correct token (15)."); + + setText(gSearchBox, "#" + token + ";" + ":::"); + is(gFiltering.searchData.toSource(), '["#", ["", "debugger;:::"]]', + "The searchbox data wasn't parsed correctly."); + ok(isCaretPos(gPanel, 14, 9 + token.length + 1), + "The editor didn't jump to the correct token (16)."); + + + setText(gSearchBox, ":i am not a number"); + is(gFiltering.searchData.toSource(), '[":", ["", 0]]', + "The searchbox data wasn't parsed correctly."); + ok(isCaretPos(gPanel, 14, 9 + token.length + 1), + "The editor didn't remain at the correct token (17)."); + + setText(gSearchBox, "#__i do not exist__"); + is(gFiltering.searchData.toSource(), '["#", ["", "__i do not exist__"]]', + "The searchbox data wasn't parsed correctly."); + ok(isCaretPos(gPanel, 14, 9 + token.length + 1), + "The editor didn't remain at the correct token (18)."); + + + setText(gSearchBox, "#" + token); + is(gFiltering.searchData.toSource(), '["#", ["", "debugger"]]', + "The searchbox data wasn't parsed correctly."); + ok(isCaretPos(gPanel, 8, 12 + token.length), + "The editor didn't jump to the correct token (19)."); + + + clearText(gSearchBox); + is(gFiltering.searchData.toSource(), '["", [""]]', + "The searchbox data wasn't parsed correctly."); + + EventUtils.sendKey("RETURN", gDebugger); + is(gFiltering.searchData.toSource(), '["", [""]]', + "The searchbox data wasn't parsed correctly."); + ok(isCaretPos(gPanel, 8, 12 + token.length), + "The editor shouldn't jump to another token (20)."); + + EventUtils.sendKey("RETURN", gDebugger); + is(gFiltering.searchData.toSource(), '["", [""]]', + "The searchbox data wasn't parsed correctly."); + ok(isCaretPos(gPanel, 8, 12 + token.length), + "The editor shouldn't jump to another token (21)."); + + + setText(gSearchBox, ":1:2:3:a:b:c:::12"); + is(gFiltering.searchData.toSource(), '[":", [":1:2:3:a:b:c::", 12]]', + "The searchbox data wasn't parsed correctly."); + ok(isCaretPos(gPanel, 12), + "The editor didn't jump to the correct line (22)."); + + setText(gSearchBox, "#don't#find#me#instead#find#" + token); + is(gFiltering.searchData.toSource(), '["#", ["#don\'t#find#me#instead#find", "debugger"]]', + "The searchbox data wasn't parsed correctly."); + ok(isCaretPos(gPanel, 8, 12 + token.length), + "The editor didn't jump to the correct token (23)."); + + EventUtils.sendKey("DOWN", gDebugger); + is(gFiltering.searchData.toSource(), '["#", ["#don\'t#find#me#instead#find", "debugger"]]', + "The searchbox data wasn't parsed correctly."); + ok(isCaretPos(gPanel, 14, 9 + token.length), + "The editor didn't jump to the correct token (24)."); + + EventUtils.sendKey("DOWN", gDebugger); + is(gFiltering.searchData.toSource(), '["#", ["#don\'t#find#me#instead#find", "debugger"]]', + "The searchbox data wasn't parsed correctly."); + ok(isCaretPos(gPanel, 18, 15 + token.length), + "The editor didn't jump to the correct token (25)."); + + EventUtils.sendKey("RETURN", gDebugger); + is(gFiltering.searchData.toSource(), '["#", ["#don\'t#find#me#instead#find", "debugger"]]', + "The searchbox data wasn't parsed correctly."); + ok(isCaretPos(gPanel, 26, 11 + token.length), + "The editor didn't jump to the correct token (26)."); + + EventUtils.sendKey("RETURN", gDebugger); + is(gFiltering.searchData.toSource(), '["#", ["#don\'t#find#me#instead#find", "debugger"]]', + "The searchbox data wasn't parsed correctly."); + ok(isCaretPos(gPanel, 8, 12 + token.length), + "The editor didn't jump to the correct token (27)."); + + EventUtils.sendKey("UP", gDebugger); + is(gFiltering.searchData.toSource(), '["#", ["#don\'t#find#me#instead#find", "debugger"]]', + "The searchbox data wasn't parsed correctly."); + ok(isCaretPos(gPanel, 26, 11 + token.length), + "The editor didn't jump to the correct token (28)."); + + + clearText(gSearchBox); + is(gFiltering.searchData.toSource(), '["", [""]]', + "The searchbox data wasn't parsed correctly."); + ok(isCaretPos(gPanel, 26, 11 + token.length), + "The editor didn't remain at the correct token (29)."); + is(gSources.visibleItems.length, 1, + "Not all the sources are shown after the search (30)."); + + + gEditor.focus(); + gEditor.setSelection.apply(gEditor, gEditor.getPosition(1, 5)); + ok(isCaretPos(gPanel, 1, 6), + "The editor caret position didn't update after selecting some text."); + + EventUtils.synthesizeKey("F", { accelKey: true }); + is(gFiltering.searchData.toSource(), '["#", ["", "!-- "]]', + "The searchbox data wasn't parsed correctly."); + is(gSearchBox.value, "#!-- ", + "The search field has the right initial value (1)."); + + gEditor.focus(); + gEditor.setSelection.apply(gEditor, gEditor.getPosition(415, 418)); + ok(isCaretPos(gPanel, 21, 30), + "The editor caret position didn't update after selecting some number."); + + EventUtils.synthesizeKey("L", { accelKey: true }); + is(gFiltering.searchData.toSource(), '[":", ["", 100]]', + "The searchbox data wasn't parsed correctly."); + is(gSearchBox.value, ":100", + "The search field has the right initial value (2)."); + + + closeDebuggerAndFinish(gPanel); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gEditor = null; + gSources = null; + gFiltering = null; + gSearchBox = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_search-basic-02.js b/toolkit/devtools/debugger/test/browser_dbg_search-basic-02.js new file mode 100644 index 000000000..c933b6f3a --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_search-basic-02.js @@ -0,0 +1,122 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests basic file search functionality. + */ + +const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html"; + +let gTab, gPanel, gDebugger; +let gSources, gSearchBox; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gSources = gDebugger.DebuggerView.Sources; + gSearchBox = gDebugger.DebuggerView.Filtering._searchbox; + + waitForSourceAndCaretAndScopes(gPanel, "-02.js", 1) + .then(performSimpleSearch) + .then(() => verifySourceAndCaret("-01.js", 1, 1, [1, 1])) + .then(combineWithLineSearch) + .then(() => verifySourceAndCaret("-01.js", 2, 1, [53, 53])) + .then(combineWithTokenSearch) + .then(() => verifySourceAndCaret("-01.js", 2, 48, [96, 100])) + .then(combineWithTokenColonSearch) + .then(() => verifySourceAndCaret("-01.js", 2, 11, [56, 63])) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + + callInTab(gTab, "firstCall"); + }); +} + +function performSimpleSearch() { + let finished = promise.all([ + ensureSourceIs(gPanel, "-02.js"), + ensureCaretAt(gPanel, 1), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND), + waitForSourceShown(gPanel, "-01.js") + ]); + + setText(gSearchBox, "1"); + + return finished.then(() => promise.all([ + ensureSourceIs(gPanel, "-01.js"), + ensureCaretAt(gPanel, 1) + ])); +} + +function combineWithLineSearch() { + let finished = promise.all([ + ensureSourceIs(gPanel, "-01.js"), + ensureCaretAt(gPanel, 1), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND), + waitForCaretUpdated(gPanel, 2) + ]); + + typeText(gSearchBox, ":2"); + + return finished.then(() => promise.all([ + ensureSourceIs(gPanel, "-01.js"), + ensureCaretAt(gPanel, 2) + ])); +} + +function combineWithTokenSearch() { + let finished = promise.all([ + ensureSourceIs(gPanel, "-01.js"), + ensureCaretAt(gPanel, 2), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND), + waitForCaretUpdated(gPanel, 2, 48) + ]); + + backspaceText(gSearchBox, 2); + typeText(gSearchBox, "#zero"); + + return finished.then(() => promise.all([ + ensureSourceIs(gPanel, "-01.js"), + ensureCaretAt(gPanel, 2, 48) + ])); +} + +function combineWithTokenColonSearch() { + let finished = promise.all([ + ensureSourceIs(gPanel, "-01.js"), + ensureCaretAt(gPanel, 2, 48), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND), + waitForCaretUpdated(gPanel, 2, 11) + ]); + + backspaceText(gSearchBox, 4); + typeText(gSearchBox, "http://"); + + return finished.then(() => promise.all([ + ensureSourceIs(gPanel, "-01.js"), + ensureCaretAt(gPanel, 2, 11) + ])); +} + +function verifySourceAndCaret(aUrl, aLine, aColumn, aSelection) { + ok(gSources.selectedItem.attachment.label.contains(aUrl), + "The selected item's label appears to be correct."); + ok(gSources.selectedItem.attachment.source.url.contains(aUrl), + "The selected item's value appears to be correct."); + ok(isCaretPos(gPanel, aLine, aColumn), + "The current caret position appears to be correct."); + ok(isEditorSel(gPanel, aSelection), + "The current editor selection appears to be correct."); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gSources = null; + gSearchBox = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_search-basic-03.js b/toolkit/devtools/debugger/test/browser_dbg_search-basic-03.js new file mode 100644 index 000000000..74020e8e4 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_search-basic-03.js @@ -0,0 +1,118 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that searches which cause a popup to be shown properly handle the + * ESCAPE key. + */ + +const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html"; + +let gTab, gPanel, gDebugger; +let gSources, gSearchBox; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gSources = gDebugger.DebuggerView.Sources; + gSearchBox = gDebugger.DebuggerView.Filtering._searchbox; + + waitForSourceAndCaretAndScopes(gPanel, "-02.js", 1) + .then(performFileSearch) + .then(escapeAndHide) + .then(escapeAndClear) + .then(() => verifySourceAndCaret("-01.js", 1, 1)) + .then(performFunctionSearch) + .then(escapeAndHide) + .then(escapeAndClear) + .then(() => verifySourceAndCaret("-01.js", 4, 10)) + .then(performGlobalSearch) + .then(escapeAndClear) + .then(() => verifySourceAndCaret("-01.js", 4, 10)) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + + callInTab(gTab, "firstCall"); + }); +} + +function performFileSearch() { + let finished = promise.all([ + ensureSourceIs(gPanel, "-02.js"), + ensureCaretAt(gPanel, 1), + once(gDebugger, "popupshown"), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND), + waitForSourceShown(gPanel, "-01.js") + ]); + + setText(gSearchBox, "."); + + return finished.then(() => promise.all([ + ensureSourceIs(gPanel, "-01.js"), + ensureCaretAt(gPanel, 1) + ])); +} + +function performFunctionSearch() { + let finished = promise.all([ + ensureSourceIs(gPanel, "-01.js"), + ensureCaretAt(gPanel, 1), + once(gDebugger, "popupshown"), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FUNCTION_SEARCH_MATCH_FOUND) + ]); + + setText(gSearchBox, "@"); + + return finished.then(() => promise.all([ + ensureSourceIs(gPanel, "-01.js"), + ensureCaretAt(gPanel, 4, 10) + ])); +} + +function performGlobalSearch() { + let finished = promise.all([ + ensureSourceIs(gPanel, "-01.js"), + ensureCaretAt(gPanel, 4, 10), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.GLOBAL_SEARCH_MATCH_FOUND) + ]); + + setText(gSearchBox, "!first"); + + return finished.then(() => promise.all([ + ensureSourceIs(gPanel, "-01.js"), + ensureCaretAt(gPanel, 4, 10) + ])); +} + +function escapeAndHide() { + let finished = once(gDebugger, "popuphidden", true); + EventUtils.sendKey("ESCAPE", gDebugger); + return finished; +} + +function escapeAndClear() { + EventUtils.sendKey("ESCAPE", gDebugger); + is(gSearchBox.getAttribute("value"), "", + "The searchbox has properly emptied after pressing escape."); +} + +function verifySourceAndCaret(aUrl, aLine, aColumn) { + ok(gSources.selectedItem.attachment.label.contains(aUrl), + "The selected item's label appears to be correct."); + ok(gSources.selectedItem.attachment.source.url.contains(aUrl), + "The selected item's value appears to be correct."); + ok(isCaretPos(gPanel, aLine, aColumn), + "The current caret position appears to be correct."); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gSources = null; + gSearchBox = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_search-basic-04.js b/toolkit/devtools/debugger/test/browser_dbg_search-basic-04.js new file mode 100644 index 000000000..be115654b --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_search-basic-04.js @@ -0,0 +1,130 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that the selection is dropped for line and token searches, after + * pressing backspace enough times. + */ + +const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html"; + +let gTab, gPanel, gDebugger; +let gEditor, gSources, gSearchBox; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gSources = gDebugger.DebuggerView.Sources; + gSearchBox = gDebugger.DebuggerView.Filtering._searchbox; + + waitForSourceShown(gPanel, "-01.js") + .then(testLineSearch) + .then(testTokenSearch) + .then(() => closeDebuggerAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); +} + +function testLineSearch() { + setText(gSearchBox, ":42"); + ok(isCaretPos(gPanel, 7), + "The editor caret position appears to be correct (1.1)."); + ok(isEditorSel(gPanel, [151, 151]), + "The editor selection appears to be correct (1.1)."); + is(gEditor.getSelection(), "", + "The editor selected text appears to be correct (1.1)."); + + backspaceText(gSearchBox, 1); + ok(isCaretPos(gPanel, 4), + "The editor caret position appears to be correct (1.2)."); + ok(isEditorSel(gPanel, [110, 110]), + "The editor selection appears to be correct (1.2)."); + is(gEditor.getSelection(), "", + "The editor selected text appears to be correct (1.2)."); + + backspaceText(gSearchBox, 1); + ok(isCaretPos(gPanel, 4), + "The editor caret position appears to be correct (1.3)."); + ok(isEditorSel(gPanel, [110, 110]), + "The editor selection appears to be correct (1.3)."); + is(gEditor.getSelection(), "", + "The editor selected text appears to be correct (1.3)."); + + setText(gSearchBox, ":4"); + ok(isCaretPos(gPanel, 4), + "The editor caret position appears to be correct (1.4)."); + ok(isEditorSel(gPanel, [110, 110]), + "The editor selection appears to be correct (1.4)."); + is(gEditor.getSelection(), "", + "The editor selected text appears to be correct (1.4)."); + + gSearchBox.select(); + backspaceText(gSearchBox, 1); + ok(isCaretPos(gPanel, 4), + "The editor caret position appears to be correct (1.5)."); + ok(isEditorSel(gPanel, [110, 110]), + "The editor selection appears to be correct (1.5)."); + is(gEditor.getSelection(), "", + "The editor selected text appears to be correct (1.5)."); + is(gSearchBox.value, "", + "The searchbox should have been cleared."); +} + +function testTokenSearch() { + setText(gSearchBox, "#();"); + ok(isCaretPos(gPanel, 5, 16), + "The editor caret position appears to be correct (2.1)."); + ok(isEditorSel(gPanel, [145, 148]), + "The editor selection appears to be correct (2.1)."); + is(gEditor.getSelection(), "();", + "The editor selected text appears to be correct (2.1)."); + + backspaceText(gSearchBox, 1); + ok(isCaretPos(gPanel, 4, 21), + "The editor caret position appears to be correct (2.2)."); + ok(isEditorSel(gPanel, [128, 130]), + "The editor selection appears to be correct (2.2)."); + is(gEditor.getSelection(), "()", + "The editor selected text appears to be correct (2.2)."); + + backspaceText(gSearchBox, 2); + ok(isCaretPos(gPanel, 4, 20), + "The editor caret position appears to be correct (2.3)."); + ok(isEditorSel(gPanel, [129, 129]), + "The editor selection appears to be correct (2.3)."); + is(gEditor.getSelection(), "", + "The editor selected text appears to be correct (2.3)."); + + setText(gSearchBox, "#;"); + ok(isCaretPos(gPanel, 5, 16), + "The editor caret position appears to be correct (2.4)."); + ok(isEditorSel(gPanel, [147, 148]), + "The editor selection appears to be correct (2.4)."); + is(gEditor.getSelection(), ";", + "The editor selected text appears to be correct (2.4)."); + + gSearchBox.select(); + backspaceText(gSearchBox, 1); + ok(isCaretPos(gPanel, 5, 16), + "The editor caret position appears to be correct (2.5)."); + ok(isEditorSel(gPanel, [148, 148]), + "The editor selection appears to be correct (2.5)."); + is(gEditor.getSelection(), "", + "The editor selected text appears to be correct (2.5)."); + is(gSearchBox.value, "", + "The searchbox should have been cleared."); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gEditor = null; + gSources = null; + gSearchBox = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_search-global-01.js b/toolkit/devtools/debugger/test/browser_dbg_search-global-01.js new file mode 100644 index 000000000..265ff92f6 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_search-global-01.js @@ -0,0 +1,272 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests basic functionality of global search (lowercase + upper case, expected + * UI behavior, number of results found etc.) + */ + +const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html"; + +let gTab, gPanel, gDebugger; +let gEditor, gSources, gSearchView, gSearchBox; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gSources = gDebugger.DebuggerView.Sources; + gSearchView = gDebugger.DebuggerView.GlobalSearch; + gSearchBox = gDebugger.DebuggerView.Filtering._searchbox; + + waitForSourceAndCaretAndScopes(gPanel, "-02.js", 1) + .then(firstSearch) + .then(secondSearch) + .then(clearSearch) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + + callInTab(gTab, "firstCall"); + }); +} + +function firstSearch() { + let deferred = promise.defer(); + + is(gSearchView.itemCount, 0, + "The global search pane shouldn't have any entries yet."); + is(gSearchView.widget._parent.hidden, true, + "The global search pane shouldn't be visible yet."); + is(gSearchView._splitter.hidden, true, + "The global search pane splitter shouldn't be visible yet."); + + gDebugger.once(gDebugger.EVENTS.GLOBAL_SEARCH_MATCH_FOUND, () => { + // Some operations are synchronously dispatched on the main thread, + // to avoid blocking UI, thus giving the impression of faster searching. + executeSoon(() => { + info("Current source url:\n" + getSelectedSourceURL(gSources)); + info("Debugger editor text:\n" + gEditor.getText()); + + ok(isCaretPos(gPanel, 6), + "The editor shouldn't have jumped to a matching line yet."); + ok(getSelectedSourceURL(gSources).contains("-02.js"), + "The current source shouldn't have changed after a global search."); + is(gSources.visibleItems.length, 2, + "Not all the sources are shown after the global search."); + + let sourceResults = gDebugger.document.querySelectorAll(".dbg-source-results"); + is(sourceResults.length, 2, + "There should be matches found in two sources."); + + let item0 = gDebugger.SourceResults.getItemForElement(sourceResults[0]); + let item1 = gDebugger.SourceResults.getItemForElement(sourceResults[1]); + is(item0.instance.expanded, true, + "The first source results should automatically be expanded.") + is(item1.instance.expanded, true, + "The second source results should automatically be expanded.") + + let searchResult0 = sourceResults[0].querySelectorAll(".dbg-search-result"); + let searchResult1 = sourceResults[1].querySelectorAll(".dbg-search-result"); + is(searchResult0.length, 1, + "There should be one line result for the first url."); + is(searchResult1.length, 2, + "There should be two line results for the second url."); + + let firstLine0 = searchResult0[0]; + is(firstLine0.querySelector(".dbg-results-line-number").getAttribute("value"), "1", + "The first result for the first source doesn't have the correct line attached."); + + is(firstLine0.querySelectorAll(".dbg-results-line-contents").length, 1, + "The first result for the first source doesn't have the correct number of nodes for a line."); + is(firstLine0.querySelectorAll(".dbg-results-line-contents-string").length, 3, + "The first result for the first source doesn't have the correct number of strings in a line."); + + is(firstLine0.querySelectorAll(".dbg-results-line-contents-string[match=true]").length, 1, + "The first result for the first source doesn't have the correct number of matches in a line."); + is(firstLine0.querySelectorAll(".dbg-results-line-contents-string[match=true]")[0].getAttribute("value"), "de", + "The first result for the first source doesn't have the correct match in a line."); + + is(firstLine0.querySelectorAll(".dbg-results-line-contents-string[match=false]").length, 2, + "The first result for the first source doesn't have the correct number of non-matches in a line."); + is(firstLine0.querySelectorAll(".dbg-results-line-contents-string[match=false]")[0].getAttribute("value"), "/* Any copyright is ", + "The first result for the first source doesn't have the correct non-matches in a line."); + is(firstLine0.querySelectorAll(".dbg-results-line-contents-string[match=false]")[1].getAttribute("value"), "dicated to the Public Domain.", + "The first result for the first source doesn't have the correct non-matches in a line."); + + let firstLine1 = searchResult1[0]; + is(firstLine1.querySelector(".dbg-results-line-number").getAttribute("value"), "1", + "The first result for the second source doesn't have the correct line attached."); + + is(firstLine1.querySelectorAll(".dbg-results-line-contents").length, 1, + "The first result for the second source doesn't have the correct number of nodes for a line."); + is(firstLine1.querySelectorAll(".dbg-results-line-contents-string").length, 3, + "The first result for the second source doesn't have the correct number of strings in a line."); + + is(firstLine1.querySelectorAll(".dbg-results-line-contents-string[match=true]").length, 1, + "The first result for the second source doesn't have the correct number of matches in a line."); + is(firstLine1.querySelectorAll(".dbg-results-line-contents-string[match=true]")[0].getAttribute("value"), "de", + "The first result for the second source doesn't have the correct match in a line."); + + is(firstLine1.querySelectorAll(".dbg-results-line-contents-string[match=false]").length, 2, + "The first result for the second source doesn't have the correct number of non-matches in a line."); + is(firstLine1.querySelectorAll(".dbg-results-line-contents-string[match=false]")[0].getAttribute("value"), "/* Any copyright is ", + "The first result for the second source doesn't have the correct non-matches in a line."); + is(firstLine1.querySelectorAll(".dbg-results-line-contents-string[match=false]")[1].getAttribute("value"), "dicated to the Public Domain.", + "The first result for the second source doesn't have the correct non-matches in a line."); + + let secondLine1 = searchResult1[1]; + is(secondLine1.querySelector(".dbg-results-line-number").getAttribute("value"), "6", + "The second result for the second source doesn't have the correct line attached."); + + is(secondLine1.querySelectorAll(".dbg-results-line-contents").length, 1, + "The second result for the second source doesn't have the correct number of nodes for a line."); + is(secondLine1.querySelectorAll(".dbg-results-line-contents-string").length, 3, + "The second result for the second source doesn't have the correct number of strings in a line."); + + is(secondLine1.querySelectorAll(".dbg-results-line-contents-string[match=true]").length, 1, + "The second result for the second source doesn't have the correct number of matches in a line."); + is(secondLine1.querySelectorAll(".dbg-results-line-contents-string[match=true]")[0].getAttribute("value"), "de", + "The second result for the second source doesn't have the correct match in a line."); + + is(secondLine1.querySelectorAll(".dbg-results-line-contents-string[match=false]").length, 2, + "The second result for the second source doesn't have the correct number of non-matches in a line."); + is(secondLine1.querySelectorAll(".dbg-results-line-contents-string[match=false]")[0].getAttribute("value"), ' ', + "The second result for the second source doesn't have the correct non-matches in a line."); + is(secondLine1.querySelectorAll(".dbg-results-line-contents-string[match=false]")[1].getAttribute("value"), 'bugger;', + "The second result for the second source doesn't have the correct non-matches in a line."); + + deferred.resolve(); + }); + }); + + setText(gSearchBox, "!de"); + + return deferred.promise; +} + +function secondSearch() { + let deferred = promise.defer(); + + is(gSearchView.itemCount, 2, + "The global search pane should have some child nodes from the previous search."); + is(gSearchView.widget._parent.hidden, false, + "The global search pane should be visible from the previous search."); + is(gSearchView._splitter.hidden, false, + "The global search pane splitter should be visible from the previous search."); + + gDebugger.once(gDebugger.EVENTS.GLOBAL_SEARCH_MATCH_FOUND, () => { + // Some operations are synchronously dispatched on the main thread, + // to avoid blocking UI, thus giving the impression of faster searching. + executeSoon(() => { + info("Current source url:\n" + getSelectedSourceURL(gSources)); + info("Debugger editor text:\n" + gEditor.getText()); + + ok(isCaretPos(gPanel, 6), + "The editor shouldn't have jumped to a matching line yet."); + ok(getSelectedSourceURL(gSources).contains("-02.js"), + "The current source shouldn't have changed after a global search."); + is(gSources.visibleItems.length, 2, + "Not all the sources are shown after the global search."); + + let sourceResults = gDebugger.document.querySelectorAll(".dbg-source-results"); + is(sourceResults.length, 2, + "There should be matches found in two sources."); + + let item0 = gDebugger.SourceResults.getItemForElement(sourceResults[0]); + let item1 = gDebugger.SourceResults.getItemForElement(sourceResults[1]); + is(item0.instance.expanded, true, + "The first source results should automatically be expanded.") + is(item1.instance.expanded, true, + "The second source results should automatically be expanded.") + + let searchResult0 = sourceResults[0].querySelectorAll(".dbg-search-result"); + let searchResult1 = sourceResults[1].querySelectorAll(".dbg-search-result"); + is(searchResult0.length, 1, + "There should be one line result for the first url."); + is(searchResult1.length, 1, + "There should be one line result for the second url."); + + let firstLine0 = searchResult0[0]; + is(firstLine0.querySelector(".dbg-results-line-number").getAttribute("value"), "1", + "The first result for the first source doesn't have the correct line attached."); + + is(firstLine0.querySelectorAll(".dbg-results-line-contents").length, 1, + "The first result for the first source doesn't have the correct number of nodes for a line."); + is(firstLine0.querySelectorAll(".dbg-results-line-contents-string").length, 5, + "The first result for the first source doesn't have the correct number of strings in a line."); + + is(firstLine0.querySelectorAll(".dbg-results-line-contents-string[match=true]").length, 2, + "The first result for the first source doesn't have the correct number of matches in a line."); + is(firstLine0.querySelectorAll(".dbg-results-line-contents-string[match=true]")[0].getAttribute("value"), "ed", + "The first result for the first source doesn't have the correct matches in a line."); + is(firstLine0.querySelectorAll(".dbg-results-line-contents-string[match=true]")[1].getAttribute("value"), "ed", + "The first result for the first source doesn't have the correct matches in a line."); + + is(firstLine0.querySelectorAll(".dbg-results-line-contents-string[match=false]").length, 3, + "The first result for the first source doesn't have the correct number of non-matches in a line."); + is(firstLine0.querySelectorAll(".dbg-results-line-contents-string[match=false]")[0].getAttribute("value"), "/* Any copyright is d", + "The first result for the first source doesn't have the correct non-matches in a line."); + is(firstLine0.querySelectorAll(".dbg-results-line-contents-string[match=false]")[1].getAttribute("value"), "icat", + "The first result for the first source doesn't have the correct non-matches in a line."); + is(firstLine0.querySelectorAll(".dbg-results-line-contents-string[match=false]")[2].getAttribute("value"), " to the Public Domain.", + "The first result for the first source doesn't have the correct non-matches in a line."); + + let firstLine1 = searchResult1[0]; + is(firstLine1.querySelector(".dbg-results-line-number").getAttribute("value"), "1", + "The first result for the second source doesn't have the correct line attached."); + + is(firstLine1.querySelectorAll(".dbg-results-line-contents").length, 1, + "The first result for the second source doesn't have the correct number of nodes for a line."); + is(firstLine1.querySelectorAll(".dbg-results-line-contents-string").length, 5, + "The first result for the second source doesn't have the correct number of strings in a line."); + + is(firstLine1.querySelectorAll(".dbg-results-line-contents-string[match=true]").length, 2, + "The first result for the second source doesn't have the correct number of matches in a line."); + is(firstLine1.querySelectorAll(".dbg-results-line-contents-string[match=true]")[0].getAttribute("value"), "ed", + "The first result for the second source doesn't have the correct matches in a line."); + is(firstLine1.querySelectorAll(".dbg-results-line-contents-string[match=true]")[1].getAttribute("value"), "ed", + "The first result for the second source doesn't have the correct matches in a line."); + + is(firstLine1.querySelectorAll(".dbg-results-line-contents-string[match=false]").length, 3, + "The first result for the second source doesn't have the correct number of non-matches in a line."); + is(firstLine1.querySelectorAll(".dbg-results-line-contents-string[match=false]")[0].getAttribute("value"), "/* Any copyright is d", + "The first result for the second source doesn't have the correct non-matches in a line."); + is(firstLine1.querySelectorAll(".dbg-results-line-contents-string[match=false]")[1].getAttribute("value"), "icat", + "The first result for the second source doesn't have the correct non-matches in a line."); + is(firstLine1.querySelectorAll(".dbg-results-line-contents-string[match=false]")[2].getAttribute("value"), " to the Public Domain.", + "The first result for the second source doesn't have the correct non-matches in a line."); + + deferred.resolve(); + }); + }); + + backspaceText(gSearchBox, 2); + typeText(gSearchBox, "ED"); + + return deferred.promise; +} + +function clearSearch() { + gSearchView.clearView(); + + is(gSearchView.itemCount, 0, + "The global search pane shouldn't have any child nodes after clearing."); + is(gSearchView.widget._parent.hidden, true, + "The global search pane shouldn't be visible after clearing."); + is(gSearchView._splitter.hidden, true, + "The global search pane splitter shouldn't be visible after clearing."); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gEditor = null; + gSources = null; + gSearchView = null; + gSearchBox = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_search-global-02.js b/toolkit/devtools/debugger/test/browser_dbg_search-global-02.js new file mode 100644 index 000000000..64d6f3c46 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_search-global-02.js @@ -0,0 +1,217 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the global search results switch back and forth, and wrap around + * when switching between them. + */ + +const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html"; + +let gTab, gPanel, gDebugger; +let gEditor, gSources, gSearchView, gSearchBox; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gSources = gDebugger.DebuggerView.Sources; + gSearchView = gDebugger.DebuggerView.GlobalSearch; + gSearchBox = gDebugger.DebuggerView.Filtering._searchbox; + + waitForSourceAndCaretAndScopes(gPanel, "-02.js", 1) + .then(firstSearch) + .then(doFirstJump) + .then(doSecondJump) + .then(doWrapAroundJump) + .then(doBackwardsWrapAroundJump) + .then(testSearchTokenEmpty) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + + callInTab(gTab, "firstCall"); + }); +} + +function firstSearch() { + let deferred = promise.defer(); + + is(gSearchView.itemCount, 0, + "The global search pane shouldn't have any entries yet."); + is(gSearchView.widget._parent.hidden, true, + "The global search pane shouldn't be visible yet."); + is(gSearchView._splitter.hidden, true, + "The global search pane splitter shouldn't be visible yet."); + + gDebugger.once(gDebugger.EVENTS.GLOBAL_SEARCH_MATCH_FOUND, () => { + // Some operations are synchronously dispatched on the main thread, + // to avoid blocking UI, thus giving the impression of faster searching. + executeSoon(() => { + info("Current source url:\n" + getSelectedSourceURL(gSources)); + info("Debugger editor text:\n" + gEditor.getText()); + + ok(isCaretPos(gPanel, 6), + "The editor shouldn't have jumped to a matching line yet."); + ok(getSelectedSourceURL(gSources).contains("-02.js"), + "The current source shouldn't have changed after a global search."); + is(gSources.visibleItems.length, 2, + "Not all the sources are shown after the global search."); + + deferred.resolve(); + }); + }); + + setText(gSearchBox, "!function"); + + return deferred.promise; +} + +function doFirstJump() { + let deferred = promise.defer(); + + waitForSourceAndCaret(gPanel, "-01.js", 4).then(() => { + info("Current source url:\n" + getSelectedSourceURL(gSources)); + info("Debugger editor text:\n" + gEditor.getText()); + + ok(getSelectedSourceURL(gSources).contains("-01.js"), + "The currently shown source is incorrect (1)."); + is(gSources.visibleItems.length, 2, + "Not all the sources are shown after the global search (1)."); + + // The editor's selected text takes a tick to update. + executeSoon(() => { + ok(isCaretPos(gPanel, 4, 9), + "The editor didn't jump to the correct line (1)."); + is(gEditor.getSelection(), "function", + "The editor didn't select the correct text (1)."); + + deferred.resolve(); + }); + }); + + EventUtils.sendKey("DOWN", gDebugger); + + return deferred.promise; +} + +function doSecondJump() { + let deferred = promise.defer(); + + waitForSourceAndCaret(gPanel, "-02.js", 4).then(() => { + info("Current source url:\n" + getSelectedSourceURL(gSources)); + info("Debugger editor text:\n" + gEditor.getText()); + + ok(getSelectedSourceURL(gSources).contains("-02.js"), + "The currently shown source is incorrect (2)."); + is(gSources.visibleItems.length, 2, + "Not all the sources are shown after the global search (2)."); + + // The editor's selected text takes a tick to update. + executeSoon(() => { + ok(isCaretPos(gPanel, 4, 9), + "The editor didn't jump to the correct line (2)."); + is(gEditor.getSelection(), "function", + "The editor didn't select the correct text (2)."); + + deferred.resolve(); + }); + }); + + EventUtils.sendKey("DOWN", gDebugger); + + return deferred.promise; +} + +function doWrapAroundJump() { + let deferred = promise.defer(); + + waitForSourceAndCaret(gPanel, "-01.js", 4).then(() => { + info("Current source url:\n" + getSelectedSourceURL(gSources)); + info("Debugger editor text:\n" + gEditor.getText()); + + ok(getSelectedSourceURL(gSources).contains("-01.js"), + "The currently shown source is incorrect (3)."); + is(gSources.visibleItems.length, 2, + "Not all the sources are shown after the global search (3)."); + + // The editor's selected text takes a tick to update. + executeSoon(() => { + ok(isCaretPos(gPanel, 4, 9), + "The editor didn't jump to the correct line (3)."); + is(gEditor.getSelection(), "function", + "The editor didn't select the correct text (3)."); + + deferred.resolve(); + }); + }); + + EventUtils.sendKey("DOWN", gDebugger); + EventUtils.sendKey("DOWN", gDebugger); + + return deferred.promise; +} + +function doBackwardsWrapAroundJump() { + let deferred = promise.defer(); + + waitForSourceAndCaret(gPanel, "-02.js", 7).then(() => { + info("Current source url:\n" + getSelectedSourceURL(gSources)); + info("Debugger editor text:\n" + gEditor.getText()); + + ok(getSelectedSourceURL(gSources).contains("-02.js"), + "The currently shown source is incorrect (4)."); + is(gSources.visibleItems.length, 2, + "Not all the sources are shown after the global search (4)."); + + // The editor's selected text takes a tick to update. + executeSoon(() => { + ok(isCaretPos(gPanel, 7, 11), + "The editor didn't jump to the correct line (4)."); + is(gEditor.getSelection(), "function", + "The editor didn't select the correct text (4)."); + + deferred.resolve(); + }); + }); + + EventUtils.sendKey("UP", gDebugger); + + return deferred.promise; +} + +function testSearchTokenEmpty() { + backspaceText(gSearchBox, 4); + + info("Current source url:\n" + getSelectedSourceURL(gSources)); + info("Debugger editor text:\n" + gEditor.getText()); + + ok(getSelectedSourceURL(gSources).contains("-02.js"), + "The currently shown source is incorrect (4)."); + is(gSources.visibleItems.length, 2, + "Not all the sources are shown after the global search (4)."); + ok(isCaretPos(gPanel, 7, 11), + "The editor didn't remain at the correct line (4)."); + is(gEditor.getSelection(), "", + "The editor shouldn't keep the previous text selected (4)."); + + is(gSearchView.itemCount, 0, + "The global search pane shouldn't have any child nodes after clearing."); + is(gSearchView.widget._parent.hidden, true, + "The global search pane shouldn't be visible after clearing."); + is(gSearchView._splitter.hidden, true, + "The global search pane splitter shouldn't be visible after clearing."); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gEditor = null; + gSources = null; + gSearchView = null; + gSearchBox = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_search-global-03.js b/toolkit/devtools/debugger/test/browser_dbg_search-global-03.js new file mode 100644 index 000000000..5613f661b --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_search-global-03.js @@ -0,0 +1,104 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the global search results are cleared on location changes, and + * the expected UI behaviors are triggered. + */ + +const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html"; + +let gTab, gPanel, gDebugger; +let gEditor, gSources, gSearchView, gSearchBox; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gSources = gDebugger.DebuggerView.Sources; + gSearchView = gDebugger.DebuggerView.GlobalSearch; + gSearchBox = gDebugger.DebuggerView.Filtering._searchbox; + + waitForSourceAndCaretAndScopes(gPanel, "-02.js", 1) + .then(firstSearch) + .then(performTest) + .then(() => closeDebuggerAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + + callInTab(gTab, "firstCall"); + }); +} + +function firstSearch() { + let deferred = promise.defer(); + + is(gSearchView.itemCount, 0, + "The global search pane shouldn't have any entries yet."); + is(gSearchView.widget._parent.hidden, true, + "The global search pane shouldn't be visible yet."); + is(gSearchView._splitter.hidden, true, + "The global search pane splitter shouldn't be visible yet."); + + gDebugger.once(gDebugger.EVENTS.GLOBAL_SEARCH_MATCH_FOUND, () => { + // Some operations are synchronously dispatched on the main thread, + // to avoid blocking UI, thus giving the impression of faster searching. + executeSoon(() => { + info("Current source url:\n" + getSelectedSourceURL(gSources)); + info("Debugger editor text:\n" + gEditor.getText()); + + ok(isCaretPos(gPanel, 6), + "The editor shouldn't have jumped to a matching line yet."); + ok(getSelectedSourceURL(gSources).contains("-02.js"), + "The current source shouldn't have changed after a global search."); + is(gSources.visibleItems.length, 2, + "Not all the sources are shown after the global search."); + + deferred.resolve(); + }); + }); + + setText(gSearchBox, "!function"); + + return deferred.promise; +} + +function performTest() { + let deferred = promise.defer(); + + is(gSearchView.itemCount, 2, + "The global search pane should have some entries from the previous search."); + is(gSearchView.widget._parent.hidden, false, + "The global search pane should be visible from the previous search."); + is(gSearchView._splitter.hidden, false, + "The global search pane splitter should be visible from the previous search."); + + reloadActiveTab(gPanel, gDebugger.EVENTS.SOURCE_SHOWN).then(() => { + info("Current source url:\n" + getSelectedSourceURL(gSources)); + info("Debugger editor text:\n" + gEditor.getText()); + + is(gSearchView.itemCount, 0, + "The global search pane shouldn't have any entries after a page navigation."); + is(gSearchView.widget._parent.hidden, true, + "The global search pane shouldn't be visible after a page navigation."); + is(gSearchView._splitter.hidden, true, + "The global search pane splitter shouldn't be visible after a page navigation."); + + deferred.resolve(); + }); + + return deferred.promise; +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gEditor = null; + gSources = null; + gSearchView = null; + gSearchBox = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_search-global-04.js b/toolkit/devtools/debugger/test/browser_dbg_search-global-04.js new file mode 100644 index 000000000..b22065bc8 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_search-global-04.js @@ -0,0 +1,92 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the global search results trigger MatchFound and NoMatchFound events + * properly, and triggers the expected UI behavior. + */ + +const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html"; + +let gTab, gPanel, gDebugger; +let gEditor, gSources, gSearchView, gSearchBox; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gSources = gDebugger.DebuggerView.Sources; + gSearchView = gDebugger.DebuggerView.GlobalSearch; + gSearchBox = gDebugger.DebuggerView.Filtering._searchbox; + + waitForSourceAndCaretAndScopes(gPanel, "-02.js", 1) + .then(firstSearch) + .then(secondSearch) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + + callInTab(gTab, "firstCall"); + }); +} + +function firstSearch() { + let deferred = promise.defer(); + + gDebugger.once(gDebugger.EVENTS.GLOBAL_SEARCH_MATCH_FOUND, () => { + // Some operations are synchronously dispatched on the main thread, + // to avoid blocking UI, thus giving the impression of faster searching. + executeSoon(() => { + info("Current source url:\n" + getSelectedSourceURL(gSources)); + info("Debugger editor text:\n" + gEditor.getText()); + + ok(isCaretPos(gPanel, 6), + "The editor shouldn't have jumped to a matching line yet."); + ok(getSelectedSourceURL(gSources).contains("-02.js"), + "The current source shouldn't have changed after a global search."); + is(gSources.visibleItems.length, 2, + "Not all the sources are shown after the global search."); + + deferred.resolve(); + }); + }); + + setText(gSearchBox, "!function"); + + return deferred.promise; +} + +function secondSearch() { + let deferred = promise.defer(); + + gDebugger.once(gDebugger.EVENTS.GLOBAL_SEARCH_MATCH_NOT_FOUND, () => { + info("Current source url:\n" + getSelectedSourceURL(gSources)); + info("Debugger editor text:\n" + gEditor.getText()); + + ok(isCaretPos(gPanel, 6), + "The editor shouldn't have jumped to a matching line yet."); + ok(getSelectedSourceURL(gSources).contains("-02.js"), + "The current source shouldn't have changed after a global search."); + is(gSources.visibleItems.length, 2, + "Not all the sources are shown after the global search."); + + deferred.resolve(); + }); + + typeText(gSearchBox, "/"); + + return deferred.promise; +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gEditor = null; + gSources = null; + gSearchView = null; + gSearchBox = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_search-global-05.js b/toolkit/devtools/debugger/test/browser_dbg_search-global-05.js new file mode 100644 index 000000000..c408e49c1 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_search-global-05.js @@ -0,0 +1,154 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the global search results are expanded/collapsed on click, and + * clicking matches makes the source editor shows the correct source and + * makes a selection based on the match. + */ + +const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html"; + +let gTab, gPanel, gDebugger; +let gEditor, gSources, gSearchView, gSearchBox; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gSources = gDebugger.DebuggerView.Sources; + gSearchView = gDebugger.DebuggerView.GlobalSearch; + gSearchBox = gDebugger.DebuggerView.Filtering._searchbox; + + waitForSourceAndCaretAndScopes(gPanel, "-02.js", 1) + .then(doSearch) + .then(testExpandCollapse) + .then(testClickLineToJump) + .then(testClickMatchToJump) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + + callInTab(gTab, "firstCall"); + }); +} + +function doSearch() { + let deferred = promise.defer(); + + gDebugger.once(gDebugger.EVENTS.GLOBAL_SEARCH_MATCH_FOUND, () => { + // Some operations are synchronously dispatched on the main thread, + // to avoid blocking UI, thus giving the impression of faster searching. + executeSoon(() => { + info("Current source url:\n" + getSelectedSourceURL(gSources)); + info("Debugger editor text:\n" + gEditor.getText()); + + ok(isCaretPos(gPanel, 6), + "The editor shouldn't have jumped to a matching line yet."); + ok(getSelectedSourceURL(gSources).contains("-02.js"), + "The current source shouldn't have changed after a global search."); + is(gSources.visibleItems.length, 2, + "Not all the sources are shown after the global search."); + + deferred.resolve(); + }); + }); + + setText(gSearchBox, "!a"); + + return deferred.promise; +} + +function testExpandCollapse() { + let sourceResults = gDebugger.document.querySelectorAll(".dbg-source-results"); + let item0 = gDebugger.SourceResults.getItemForElement(sourceResults[0]); + let item1 = gDebugger.SourceResults.getItemForElement(sourceResults[1]); + let firstHeader = sourceResults[0].querySelector(".dbg-results-header"); + let secondHeader = sourceResults[1].querySelector(".dbg-results-header"); + + EventUtils.sendMouseEvent({ type: "click" }, firstHeader); + EventUtils.sendMouseEvent({ type: "click" }, secondHeader); + + is(item0.instance.expanded, false, + "The first source results should be collapsed on click.") + is(item1.instance.expanded, false, + "The second source results should be collapsed on click.") + + EventUtils.sendMouseEvent({ type: "click" }, firstHeader); + EventUtils.sendMouseEvent({ type: "click" }, secondHeader); + + is(item0.instance.expanded, true, + "The first source results should be expanded on an additional click."); + is(item1.instance.expanded, true, + "The second source results should be expanded on an additional click."); +} + +function testClickLineToJump() { + let deferred = promise.defer(); + + let sourceResults = gDebugger.document.querySelectorAll(".dbg-source-results"); + let firstHeader = sourceResults[0].querySelector(".dbg-results-header"); + let firstLine = sourceResults[0].querySelector(".dbg-results-line-contents"); + + waitForSourceAndCaret(gPanel, "-01.js", 1, 1).then(() => { + info("Current source url:\n" + getSelectedSourceURL(gSources)); + info("Debugger editor text:\n" + gEditor.getText()); + + ok(isCaretPos(gPanel, 1, 1), + "The editor didn't jump to the correct line (1)."); + is(gEditor.getSelection(), "", + "The editor didn't select the correct text (1)."); + ok(getSelectedSourceURL(gSources).contains("-01.js"), + "The currently shown source is incorrect (1)."); + is(gSources.visibleItems.length, 2, + "Not all the sources are shown after the global search (1)."); + + deferred.resolve(); + }); + + EventUtils.sendMouseEvent({ type: "click" }, firstLine); + + return deferred.promise; +} + +function testClickMatchToJump() { + let deferred = promise.defer(); + + let sourceResults = gDebugger.document.querySelectorAll(".dbg-source-results"); + let secondHeader = sourceResults[1].querySelector(".dbg-results-header"); + let secondMatches = sourceResults[1].querySelectorAll(".dbg-results-line-contents-string[match=true]"); + let lastMatch = Array.slice(secondMatches).pop(); + + waitForSourceAndCaret(gPanel, "-02.js", 1, 1).then(() => { + info("Current source url:\n" + getSelectedSourceURL(gSources)); + info("Debugger editor text:\n" + gEditor.getText()); + + ok(isCaretPos(gPanel, 1, 1), + "The editor didn't jump to the correct line (2)."); + is(gEditor.getSelection(), "", + "The editor didn't select the correct text (2)."); + ok(getSelectedSourceURL(gSources).contains("-02.js"), + "The currently shown source is incorrect (2)."); + is(gSources.visibleItems.length, 2, + "Not all the sources are shown after the global search (2)."); + + deferred.resolve(); + }); + + EventUtils.sendMouseEvent({ type: "click" }, lastMatch); + + return deferred.promise; +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gEditor = null; + gSources = null; + gSearchView = null; + gSearchBox = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_search-global-06.js b/toolkit/devtools/debugger/test/browser_dbg_search-global-06.js new file mode 100644 index 000000000..578120a08 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_search-global-06.js @@ -0,0 +1,119 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the global search results are hidden when they're supposed to + * (after a focus lost, or when ESCAPE is pressed). + */ + +const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html"; + +let gTab, gPanel, gDebugger; +let gEditor, gSources, gSearchView, gSearchBox; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gSources = gDebugger.DebuggerView.Sources; + gSearchView = gDebugger.DebuggerView.GlobalSearch; + gSearchBox = gDebugger.DebuggerView.Filtering._searchbox; + + waitForSourceAndCaretAndScopes(gPanel, "-02.js", 1) + .then(doSearch) + .then(testFocusLost) + .then(doSearch) + .then(testEscape) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + + callInTab(gTab, "firstCall"); + }); +} + +function doSearch() { + let deferred = promise.defer(); + + is(gSearchView.itemCount, 0, + "The global search pane shouldn't have any entries yet."); + is(gSearchView.widget._parent.hidden, true, + "The global search pane shouldn't be visible yet."); + is(gSearchView._splitter.hidden, true, + "The global search pane splitter shouldn't be visible yet."); + + gDebugger.once(gDebugger.EVENTS.GLOBAL_SEARCH_MATCH_FOUND, () => { + // Some operations are synchronously dispatched on the main thread, + // to avoid blocking UI, thus giving the impression of faster searching. + executeSoon(() => { + info("Current source url:\n" + getSelectedSourceURL(gSources)); + info("Debugger editor text:\n" + gEditor.getText()); + + ok(isCaretPos(gPanel, 6), + "The editor shouldn't have jumped to a matching line yet."); + ok(getSelectedSourceURL(gSources).contains("-02.js"), + "The current source shouldn't have changed after a global search."); + is(gSources.visibleItems.length, 2, + "Not all the sources are shown after the global search."); + + deferred.resolve(); + }); + }); + + setText(gSearchBox, "!a"); + + return deferred.promise; +} + +function testFocusLost() { + is(gSearchView.itemCount, 2, + "The global search pane should have some entries from the previous search."); + is(gSearchView.widget._parent.hidden, false, + "The global search pane should be visible from the previous search."); + is(gSearchView._splitter.hidden, false, + "The global search pane splitter should be visible from the previous search."); + + gDebugger.DebuggerView.editor.focus(); + + info("Current source url:\n" + getSelectedSourceURL(gSources)); + info("Debugger editor text:\n" + gEditor.getText()); + + is(gSearchView.itemCount, 0, + "The global search pane shouldn't have any child nodes after clearing."); + is(gSearchView.widget._parent.hidden, true, + "The global search pane shouldn't be visible after clearing."); + is(gSearchView._splitter.hidden, true, + "The global search pane splitter shouldn't be visible after clearing."); +} + +function testEscape() { + is(gSearchView.itemCount, 2, + "The global search pane should have some entries from the previous search."); + is(gSearchView.widget._parent.hidden, false, + "The global search pane should be visible from the previous search."); + is(gSearchView._splitter.hidden, false, + "The global search pane splitter should be visible from the previous search."); + + gSearchBox.focus(); + EventUtils.sendKey("ESCAPE", gDebugger); + + is(gSearchView.itemCount, 0, + "The global search pane shouldn't have any child nodes after clearing."); + is(gSearchView.widget._parent.hidden, true, + "The global search pane shouldn't be visible after clearing."); + is(gSearchView._splitter.hidden, true, + "The global search pane splitter shouldn't be visible after clearing."); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gEditor = null; + gSources = null; + gSearchView = null; + gSearchBox = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_search-popup-jank.js b/toolkit/devtools/debugger/test/browser_dbg_search-popup-jank.js new file mode 100644 index 000000000..e00d28933 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_search-popup-jank.js @@ -0,0 +1,117 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that sources aren't selected by default when finding a match. + */ + +const TAB_URL = EXAMPLE_URL + "doc_editor-mode.html"; + +let gTab, gPanel, gDebugger; +let gSearchBox; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gSearchBox = gDebugger.DebuggerView.Filtering._searchbox; + + gDebugger.DebuggerView.FilteredSources._autoSelectFirstItem = false; + gDebugger.DebuggerView.FilteredFunctions._autoSelectFirstItem = false; + + waitForSourceShown(gPanel, "-01.js") + .then(superGenericFileSearch) + .then(() => ensureSourceIs(aPanel, "-01.js")) + .then(() => ensureCaretAt(aPanel, 1)) + + .then(superAccurateFileSearch) + .then(() => ensureSourceIs(aPanel, "-01.js")) + .then(() => ensureCaretAt(aPanel, 1)) + .then(() => pressKeyToHide("RETURN")) + .then(() => ensureSourceIs(aPanel, "code_test-editor-mode", true)) + .then(() => ensureCaretAt(aPanel, 1)) + + .then(superGenericFileSearch) + .then(() => ensureSourceIs(aPanel, "code_test-editor-mode")) + .then(() => ensureCaretAt(aPanel, 1)) + .then(() => pressKey("UP")) + .then(() => ensureSourceIs(aPanel, "doc_editor-mode", true)) + .then(() => ensureCaretAt(aPanel, 1)) + .then(() => pressKeyToHide("RETURN")) + .then(() => ensureSourceIs(aPanel, "doc_editor-mode")) + .then(() => ensureCaretAt(aPanel, 1)) + + .then(superAccurateFileSearch) + .then(() => ensureSourceIs(aPanel, "doc_editor-mode")) + .then(() => ensureCaretAt(aPanel, 1)) + .then(() => typeText(gSearchBox, ":")) + .then(() => waitForSourceShown(gPanel, "code_test-editor-mode")) + .then(() => ensureSourceIs(aPanel, "code_test-editor-mode", true)) + .then(() => ensureCaretAt(aPanel, 1)) + .then(() => typeText(gSearchBox, "5")) + .then(() => ensureSourceIs(aPanel, "code_test-editor-mode")) + .then(() => ensureCaretAt(aPanel, 5)) + .then(() => pressKey("DOWN")) + .then(() => ensureSourceIs(aPanel, "code_test-editor-mode")) + .then(() => ensureCaretAt(aPanel, 6)) + + .then(superGenericFunctionSearch) + .then(() => ensureSourceIs(aPanel, "code_test-editor-mode")) + .then(() => ensureCaretAt(aPanel, 6)) + .then(() => pressKey("RETURN")) + .then(() => ensureSourceIs(aPanel, "code_test-editor-mode")) + .then(() => ensureCaretAt(aPanel, 4, 10)) + + .then(() => closeDebuggerAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); +} + +function waitForMatchFoundAndResultsShown(aName) { + return promise.all([ + once(gDebugger, "popupshown"), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS[aName]) + ]); +} + +function waitForResultsHidden() { + return once(gDebugger, "popuphidden"); +} + +function superGenericFunctionSearch() { + let finished = waitForMatchFoundAndResultsShown("FUNCTION_SEARCH_MATCH_FOUND"); + setText(gSearchBox, "@"); + return finished; +} + +function superGenericFileSearch() { + let finished = waitForMatchFoundAndResultsShown("FILE_SEARCH_MATCH_FOUND"); + setText(gSearchBox, "."); + return finished; +} + +function superAccurateFileSearch() { + let finished = waitForMatchFoundAndResultsShown("FILE_SEARCH_MATCH_FOUND"); + setText(gSearchBox, "editor"); + return finished; +} + +function pressKey(aKey) { + EventUtils.sendKey(aKey, gDebugger); +} + +function pressKeyToHide(aKey) { + let finished = waitForResultsHidden(); + EventUtils.sendKey(aKey, gDebugger); + return finished; +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gSearchBox = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_search-sources-01.js b/toolkit/devtools/debugger/test/browser_dbg_search-sources-01.js new file mode 100644 index 000000000..cc2b6ff3a --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_search-sources-01.js @@ -0,0 +1,233 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests basic functionality of sources filtering (file search). + */ + +const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html"; + +let gTab, gPanel, gDebugger; +let gSources, gSearchView, gSearchBox; + +function test() { + // Debug test slaves are a bit slow at this test. + requestLongerTimeout(3); + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gSources = gDebugger.DebuggerView.Sources; + gSearchView = gDebugger.DebuggerView.FilteredSources; + gSearchBox = gDebugger.DebuggerView.Filtering._searchbox; + + waitForSourceShown(gPanel, "-01.js") + .then(bogusSearch) + .then(firstSearch) + .then(secondSearch) + .then(thirdSearch) + .then(fourthSearch) + .then(fifthSearch) + .then(sixthSearch) + .then(seventhSearch) + .then(() => closeDebuggerAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); +} + +function bogusSearch() { + let finished = promise.all([ + ensureSourceIs(gPanel, "-01.js"), + ensureCaretAt(gPanel, 1), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_NOT_FOUND) + ]); + + setText(gSearchBox, "BOGUS"); + + return finished.then(() => promise.all([ + ensureSourceIs(gPanel, "-01.js"), + ensureCaretAt(gPanel, 1), + verifyContents({ itemCount: 0, hidden: true }) + ])); +} + +function firstSearch() { + let finished = promise.all([ + ensureSourceIs(gPanel, "-01.js"), + ensureCaretAt(gPanel, 1), + once(gDebugger, "popupshown"), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND), + waitForSourceShown(gPanel, "-02.js") + ]); + + setText(gSearchBox, "-02.js"); + + return finished.then(() => promise.all([ + ensureSourceIs(gPanel, "-02.js"), + ensureCaretAt(gPanel, 1), + verifyContents({ itemCount: 1, hidden: false }) + ])); +} + +function secondSearch() { + let finished = promise.all([ + once(gDebugger, "popupshown"), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND), + waitForSourceShown(gPanel, "-01.js") + ]) + .then(() => { + let finished = promise.all([ + once(gDebugger, "popupshown"), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND), + waitForCaretUpdated(gPanel, 5) + ]) + .then(() => promise.all([ + ensureSourceIs(gPanel, "-01.js"), + ensureCaretAt(gPanel, 5), + verifyContents({ itemCount: 1, hidden: false }) + ])); + + typeText(gSearchBox, ":5"); + return finished; + }); + + setText(gSearchBox, ".*-01\.js"); + return finished; +} + +function thirdSearch() { + let finished = promise.all([ + once(gDebugger, "popupshown"), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND), + waitForSourceShown(gPanel, "-02.js") + ]) + .then(() => { + let finished = promise.all([ + once(gDebugger, "popupshown"), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND), + waitForCaretUpdated(gPanel, 6, 6) + ]) + .then(() => promise.all([ + ensureSourceIs(gPanel, "-02.js"), + ensureCaretAt(gPanel, 6, 6), + verifyContents({ itemCount: 1, hidden: false }) + ])); + + typeText(gSearchBox, "#deb"); + return finished; + }); + + setText(gSearchBox, ".*-02\.js"); + return finished; +} + +function fourthSearch() { + let finished = promise.all([ + once(gDebugger, "popupshown"), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND), + waitForSourceShown(gPanel, "-01.js") + ]) + .then(() => { + let finished = promise.all([ + once(gDebugger, "popupshown"), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND), + waitForCaretUpdated(gPanel, 2, 9), + ]) + .then(() => promise.all([ + ensureSourceIs(gPanel, "-01.js"), + ensureCaretAt(gPanel, 2, 9), + verifyContents({ itemCount: 1, hidden: false }) + // ...because we simply searched for ":" in the current file. + ])); + + typeText(gSearchBox, "#:"); // # has precedence. + return finished; + }); + + setText(gSearchBox, ".*-01\.js"); + return finished; +} + +function fifthSearch() { + let finished = promise.all([ + once(gDebugger, "popupshown"), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND), + waitForSourceShown(gPanel, "-02.js") + ]) + .then(() => { + let finished = promise.all([ + once(gDebugger, "popuphidden"), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_NOT_FOUND), + waitForCaretUpdated(gPanel, 1, 3) + ]) + .then(() => promise.all([ + ensureSourceIs(gPanel, "-02.js"), + ensureCaretAt(gPanel, 1, 3), + verifyContents({ itemCount: 0, hidden: true }) + // ...because the searched label includes ":5", so nothing is found. + ])); + + typeText(gSearchBox, ":5#*"); // # has precedence. + return finished; + }); + + setText(gSearchBox, ".*-02\.js"); + return finished; +} + +function sixthSearch() { + let finished = promise.all([ + ensureSourceIs(gPanel, "-02.js"), + ensureCaretAt(gPanel, 1, 3), + once(gDebugger, "popupshown"), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND), + waitForCaretUpdated(gPanel, 5) + ]); + + backspaceText(gSearchBox, 2); + + return finished.then(() => promise.all([ + ensureSourceIs(gPanel, "-02.js"), + ensureCaretAt(gPanel, 5), + verifyContents({ itemCount: 1, hidden: false }) + ])); +} + +function seventhSearch() { + let finished = promise.all([ + ensureSourceIs(gPanel, "-02.js"), + ensureCaretAt(gPanel, 5), + once(gDebugger, "popupshown"), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND), + waitForSourceShown(gPanel, "-01.js"), + ]); + + backspaceText(gSearchBox, 6); + + return finished.then(() => promise.all([ + ensureSourceIs(gPanel, "-01.js"), + ensureCaretAt(gPanel, 1), + verifyContents({ itemCount: 2, hidden: false }) + ])); +} + +function verifyContents(aArgs) { + is(gSources.visibleItems.length, 2, + "The unmatched sources in the widget should not be hidden."); + is(gSearchView.itemCount, aArgs.itemCount, + "No sources should be displayed in the sources container after a bogus search."); + is(gSearchView.hidden, aArgs.hidden, + "No sources should be displayed in the sources container after a bogus search."); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gSources = null; + gSearchView = null; + gSearchBox = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_search-sources-02.js b/toolkit/devtools/debugger/test/browser_dbg_search-sources-02.js new file mode 100644 index 000000000..e75f76726 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_search-sources-02.js @@ -0,0 +1,276 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests more complex functionality of sources filtering (file search). + */ + +const TAB_URL = EXAMPLE_URL + "doc_editor-mode.html"; + +let gTab, gPanel, gDebugger; +let gSources, gSourceUtils, gSearchView, gSearchBox; + +function test() { + // Debug test slaves are a bit slow at this test. + requestLongerTimeout(3); + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gSources = gDebugger.DebuggerView.Sources; + gSourceUtils = gDebugger.SourceUtils; + gSearchView = gDebugger.DebuggerView.FilteredSources; + gSearchBox = gDebugger.DebuggerView.Filtering._searchbox; + + waitForSourceShown(gPanel, "-01.js") + .then(firstSearch) + .then(secondSearch) + .then(thirdSearch) + .then(fourthSearch) + .then(fifthSearch) + .then(goDown) + .then(goDownAndWrap) + .then(goUpAndWrap) + .then(goUp) + .then(returnAndSwitch) + .then(firstSearch) + .then(clickAndSwitch) + .then(() => closeDebuggerAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); +} + +function firstSearch() { + let finished = promise.all([ + ensureSourceIs(gPanel, "-01.js"), + ensureCaretAt(gPanel, 1), + once(gDebugger, "popupshown"), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND) + ]); + + setText(gSearchBox, "."); + + return finished.then(() => promise.all([ + ensureSourceIs(gPanel, "-01.js"), + ensureCaretAt(gPanel, 1), + verifyContents([ + "code_script-switching-01.js?a=b", + "code_test-editor-mode?c=d", + "doc_editor-mode.html" + ]) + ])); +} + +function secondSearch() { + let finished = promise.all([ + ensureSourceIs(gPanel, "-01.js"), + ensureCaretAt(gPanel, 1), + once(gDebugger, "popupshown"), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND) + ]); + + typeText(gSearchBox, "-0"); + + return finished.then(() => promise.all([ + ensureSourceIs(gPanel, "-01.js"), + ensureCaretAt(gPanel, 1), + verifyContents(["code_script-switching-01.js?a=b"]) + ])); +} + +function thirdSearch() { + let finished = promise.all([ + ensureSourceIs(gPanel, "-01.js"), + ensureCaretAt(gPanel, 1), + once(gDebugger, "popupshown"), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND) + ]); + + backspaceText(gSearchBox, 1); + + return finished.then(() => promise.all([ + ensureSourceIs(gPanel, "-01.js"), + ensureCaretAt(gPanel, 1), + verifyContents([ + "code_script-switching-01.js?a=b", + "code_test-editor-mode?c=d", + "doc_editor-mode.html" + ]) + ])); +} + +function fourthSearch() { + let finished = promise.all([ + ensureSourceIs(gPanel, "-01.js"), + ensureCaretAt(gPanel, 1), + once(gDebugger, "popupshown"), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND), + waitForSourceShown(gPanel, "test-editor-mode") + ]); + + setText(gSearchBox, "code_test"); + + return finished.then(() => promise.all([ + ensureSourceIs(gPanel, "test-editor-mode"), + ensureCaretAt(gPanel, 1), + verifyContents(["code_test-editor-mode?c=d"]) + ])); +} + +function fifthSearch() { + let finished = promise.all([ + ensureSourceIs(gPanel, "test-editor-mode"), + ensureCaretAt(gPanel, 1), + once(gDebugger, "popupshown"), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND), + waitForSourceShown(gPanel, "-01.js") + ]); + + backspaceText(gSearchBox, 4); + + return finished.then(() => promise.all([ + ensureSourceIs(gPanel, "-01.js"), + ensureCaretAt(gPanel, 1), + verifyContents([ + "code_script-switching-01.js?a=b", + "code_test-editor-mode?c=d" + ]) + ])); +} + +function goDown() { + let finished = promise.all([ + ensureSourceIs(gPanel, "-01.js"), + ensureCaretAt(gPanel, 1), + waitForSourceShown(gPanel, "test-editor-mode"), + ]); + + EventUtils.sendKey("DOWN", gDebugger); + + return finished.then(() => promise.all([ + ensureSourceIs(gPanel,"test-editor-mode"), + ensureCaretAt(gPanel, 1), + verifyContents([ + "code_script-switching-01.js?a=b", + "code_test-editor-mode?c=d" + ]) + ])); +} + +function goDownAndWrap() { + let finished = promise.all([ + ensureSourceIs(gPanel, "test-editor-mode"), + ensureCaretAt(gPanel, 1), + waitForSourceShown(gPanel, "-01.js") + ]); + + EventUtils.synthesizeKey("g", { metaKey: true }, gDebugger); + + return finished.then(() => promise.all([ + ensureSourceIs(gPanel,"-01.js"), + ensureCaretAt(gPanel, 1), + verifyContents([ + "code_script-switching-01.js?a=b", + "code_test-editor-mode?c=d" + ]) + ])); +} + +function goUpAndWrap() { + let finished = promise.all([ + ensureSourceIs(gPanel,"-01.js"), + ensureCaretAt(gPanel, 1), + waitForSourceShown(gPanel, "test-editor-mode") + ]); + + EventUtils.synthesizeKey("G", { metaKey: true }, gDebugger); + + return finished.then(() => promise.all([ + ensureSourceIs(gPanel,"test-editor-mode"), + ensureCaretAt(gPanel, 1), + verifyContents([ + "code_script-switching-01.js?a=b", + "code_test-editor-mode?c=d" + ]) + ])); +} + +function goUp() { + let finished = promise.all([ + ensureSourceIs(gPanel,"test-editor-mode"), + ensureCaretAt(gPanel, 1), + waitForSourceShown(gPanel, "-01.js"), + ]); + + EventUtils.sendKey("UP", gDebugger); + + return finished.then(() => promise.all([ + ensureSourceIs(gPanel,"-01.js"), + ensureCaretAt(gPanel, 1), + verifyContents([ + "code_script-switching-01.js?a=b", + "code_test-editor-mode?c=d" + ]) + ])); +} + +function returnAndSwitch() { + let finished = promise.all([ + ensureSourceIs(gPanel,"-01.js"), + ensureCaretAt(gPanel, 1), + once(gDebugger, "popuphidden") + ]); + + EventUtils.sendKey("RETURN", gDebugger); + + return finished.then(() => promise.all([ + ensureSourceIs(gPanel,"-01.js"), + ensureCaretAt(gPanel, 1) + ])); +} + +function clickAndSwitch() { + let finished = promise.all([ + ensureSourceIs(gPanel,"-01.js"), + ensureCaretAt(gPanel, 1), + once(gDebugger, "popuphidden"), + waitForSourceShown(gPanel, "test-editor-mode") + ]); + + EventUtils.sendMouseEvent({ type: "click" }, gSearchView.items[1].target, gDebugger); + + return finished.then(() => promise.all([ + ensureSourceIs(gPanel,"test-editor-mode"), + ensureCaretAt(gPanel, 1) + ])); +} + +function verifyContents(aMatches) { + is(gSources.visibleItems.length, 3, + "The unmatched sources in the widget should not be hidden."); + is(gSearchView.itemCount, aMatches.length, + "The filtered sources view should have the right items available."); + + for (let i = 0; i < gSearchView.itemCount; i++) { + let trimmedLabel = gSourceUtils.trimUrlLength(gSourceUtils.trimUrlQuery(aMatches[i])); + let trimmedLocation = gSourceUtils.trimUrlLength(EXAMPLE_URL + aMatches[i], 0, "start"); + + ok(gSearchView.widget._parent.querySelector(".results-panel-item-label[value=\"" + trimmedLabel + "\"]"), + "The filtered sources view should have the correct source labels."); + ok(gSearchView.widget._parent.querySelector(".results-panel-item-label-below[value=\"" + trimmedLocation + "\"]"), + "The filtered sources view should have the correct source locations."); + } +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gSources = null; + gSourceUtils = null; + gSearchView = null; + gSearchBox = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_search-sources-03.js b/toolkit/devtools/debugger/test/browser_dbg_search-sources-03.js new file mode 100644 index 000000000..e783b9e8b --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_search-sources-03.js @@ -0,0 +1,98 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that while searching for files, the sources list remains unchanged. + */ + +const TAB_URL = EXAMPLE_URL + "doc_editor-mode.html"; + +let gTab, gPanel, gDebugger; +let gSources, gSearchBox; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gSources = gDebugger.DebuggerView.Sources; + gSearchBox = gDebugger.DebuggerView.Filtering._searchbox; + + waitForSourceShown(gPanel, "-01.js") + .then(superGenericSearch) + .then(verifySourcesPane) + .then(kindaInterpretableSearch) + .then(verifySourcesPane) + .then(incrediblySpecificSearch) + .then(verifySourcesPane) + .then(returnAndHide) + .then(verifySourcesPane) + .then(() => closeDebuggerAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); +} + +function waitForMatchFoundAndResultsShown() { + return promise.all([ + once(gDebugger, "popupshown"), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FILE_SEARCH_MATCH_FOUND) + ]).then(() => promise.all([ + ensureSourceIs(gPanel, "-01.js"), + ensureCaretAt(gPanel, 1) + ])); +} + +function waitForResultsHidden() { + return once(gDebugger, "popuphidden").then(() => promise.all([ + ensureSourceIs(gPanel, "-01.js"), + ensureCaretAt(gPanel, 1) + ])); +} + +function superGenericSearch() { + let finished = waitForMatchFoundAndResultsShown(); + setText(gSearchBox, "."); + return finished; +} + +function kindaInterpretableSearch() { + let finished = waitForMatchFoundAndResultsShown(); + typeText(gSearchBox, "-0"); + return finished; +} + +function incrediblySpecificSearch() { + let finished = waitForMatchFoundAndResultsShown(); + typeText(gSearchBox, "1.js"); + return finished; +} + +function returnAndHide() { + let finished = waitForResultsHidden(); + EventUtils.sendKey("RETURN", gDebugger); + return finished; +} + +function verifySourcesPane() { + is(gSources.itemCount, 3, + "There should be 3 items present in the sources container."); + is(gSources.visibleItems.length, 3, + "There should be no hidden items in the sources container."); + + ok(gSources.getItemForAttachment(e => e.label == "code_script-switching-01.js"), + "The first source's label should be correct."); + ok(gSources.getItemForAttachment(e => e.label == "code_test-editor-mode"), + "The second source's label should be correct."); + ok(gSources.getItemForAttachment(e => e.label == "doc_editor-mode.html"), + "The third source's label should be correct."); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gSources = null; + gSearchBox = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_search-symbols.js b/toolkit/devtools/debugger/test/browser_dbg_search-symbols.js new file mode 100644 index 000000000..803e51ef5 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_search-symbols.js @@ -0,0 +1,467 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the function searching works properly. + */ + +const TAB_URL = EXAMPLE_URL + "doc_function-search.html"; + +let gTab, gPanel, gDebugger; +let gEditor, gSources, gSearchBox, gFilteredFunctions; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gSources = gDebugger.DebuggerView.Sources; + gSearchBox = gDebugger.DebuggerView.Filtering._searchbox; + gFilteredFunctions = gDebugger.DebuggerView.FilteredFunctions; + + waitForSourceShown(gPanel, "-01.js") + .then(() => showSource("doc_function-search.html")) + .then(htmlSearch) + .then(() => showSource("code_function-search-01.js")) + .then(firstJsSearch) + .then(() => showSource("code_function-search-02.js")) + .then(secondJsSearch) + .then(() => showSource("code_function-search-03.js")) + .then(thirdJsSearch) + .then(saveSearch) + .then(filterSearch) + .then(bogusSearch) + .then(incrementalSearch) + .then(emptySearch) + .then(() => closeDebuggerAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); +} + +function htmlSearch() { + let deferred = promise.defer(); + + once(gDebugger, "popupshown").then(() => { + writeInfo(); + + is(gFilteredFunctions.selectedIndex, 0, + "An item should be selected in the filtered functions view (1)."); + ok(gFilteredFunctions.selectedItem, + "An item should be selected in the filtered functions view (2)."); + + if (gSources.selectedItem.attachment.source.url.indexOf(".html") != -1) { + let expectedResults = [ + ["inline", ".html", "", 19, 16], + ["arrow", ".html", "", 20, 11], + ["foo", ".html", "", 22, 11], + ["foo2", ".html", "", 23, 11], + ["bar2", ".html", "", 23, 18] + ]; + + for (let [label, value, description, line, column] of expectedResults) { + let target = gFilteredFunctions.selectedItem.target; + + if (label) { + is(target.querySelector(".results-panel-item-label").getAttribute("value"), + gDebugger.SourceUtils.trimUrlLength(label + "()"), + "The corect label (" + label + ") is currently selected."); + } else { + ok(!target.querySelector(".results-panel-item-label"), + "Shouldn't create empty label nodes."); + } + if (value) { + ok(target.querySelector(".results-panel-item-label-below").getAttribute("value").contains(value), + "The corect value (" + value + ") is attached."); + } else { + ok(!target.querySelector(".results-panel-item-label-below"), + "Shouldn't create empty label nodes."); + } + if (description) { + is(target.querySelector(".results-panel-item-label-before").getAttribute("value"), description, + "The corect description (" + description + ") is currently shown."); + } else { + ok(!target.querySelector(".results-panel-item-label-before"), + "Shouldn't create empty label nodes."); + } + + ok(isCaretPos(gPanel, line, column), + "The editor didn't jump to the correct line."); + + EventUtils.sendKey("DOWN", gDebugger); + } + + ok(isCaretPos(gPanel, expectedResults[0][3], expectedResults[0][4]), + "The editor didn't jump to the correct line again."); + + deferred.resolve(); + } else { + ok(false, "How did you get here? Go away, you."); + } + }); + + setText(gSearchBox, "@"); + return deferred.promise; +} + +function firstJsSearch() { + let deferred = promise.defer(); + + once(gDebugger, "popupshown").then(() => { + writeInfo(); + + is(gFilteredFunctions.selectedIndex, 0, + "An item should be selected in the filtered functions view (1)."); + ok(gFilteredFunctions.selectedItem, + "An item should be selected in the filtered functions view (2)."); + + if (gSources.selectedItem.attachment.source.url.indexOf("-01.js") != -1) { + let s = " " + gDebugger.L10N.getStr("functionSearchSeparatorLabel") + " "; + let expectedResults = [ + ["test", "-01.js", "", 4, 10], + ["anonymousExpression", "-01.js", "test.prototype", 9, 3], + ["namedExpression" + s + "NAME", "-01.js", "test.prototype", 11, 3], + ["a_test", "-01.js", "foo", 22, 3], + ["n_test" + s + "x", "-01.js", "foo", 24, 3], + ["a_test", "-01.js", "foo.sub", 27, 5], + ["n_test" + s + "y", "-01.js", "foo.sub", 29, 5], + ["a_test", "-01.js", "foo.sub.sub", 32, 7], + ["n_test" + s + "z", "-01.js", "foo.sub.sub", 34, 7], + ["test_SAME_NAME", "-01.js", "foo.sub.sub.sub", 37, 9] + ]; + + for (let [label, value, description, line, column] of expectedResults) { + let target = gFilteredFunctions.selectedItem.target; + + if (label) { + is(target.querySelector(".results-panel-item-label").getAttribute("value"), + gDebugger.SourceUtils.trimUrlLength(label + "()"), + "The corect label (" + label + ") is currently selected."); + } else { + ok(!target.querySelector(".results-panel-item-label"), + "Shouldn't create empty label nodes."); + } + if (value) { + ok(target.querySelector(".results-panel-item-label-below").getAttribute("value").contains(value), + "The corect value (" + value + ") is attached."); + } else { + ok(!target.querySelector(".results-panel-item-label-below"), + "Shouldn't create empty label nodes."); + } + if (description) { + is(target.querySelector(".results-panel-item-label-before").getAttribute("value"), description, + "The corect description (" + description + ") is currently shown."); + } else { + ok(!target.querySelector(".results-panel-item-label-before"), + "Shouldn't create empty label nodes."); + } + + ok(isCaretPos(gPanel, line, column), + "The editor didn't jump to the correct line."); + + EventUtils.sendKey("DOWN", gDebugger); + } + + ok(isCaretPos(gPanel, expectedResults[0][3], expectedResults[0][4]), + "The editor didn't jump to the correct line again."); + + deferred.resolve() + } else { + ok(false, "How did you get here? Go away, you."); + } + }); + + setText(gSearchBox, "@"); + return deferred.promise; +} + +function secondJsSearch() { + let deferred = promise.defer(); + + once(gDebugger, "popupshown").then(() => { + writeInfo(); + + is(gFilteredFunctions.selectedIndex, 0, + "An item should be selected in the filtered functions view (1)."); + ok(gFilteredFunctions.selectedItem, + "An item should be selected in the filtered functions view (2)."); + + if (gSources.selectedItem.attachment.source.url.indexOf("-02.js") != -1) { + let s = " " + gDebugger.L10N.getStr("functionSearchSeparatorLabel") + " "; + let expectedResults = [ + ["test2", "-02.js", "", 4, 5], + ["test3" + s + "test3_NAME", "-02.js", "", 8, 5], + ["test4_SAME_NAME", "-02.js", "", 11, 5], + ["x" + s + "X", "-02.js", "test.prototype", 14, 1], + ["y" + s + "Y", "-02.js", "test.prototype.sub", 16, 1], + ["z" + s + "Z", "-02.js", "test.prototype.sub.sub", 18, 1], + ["t", "-02.js", "test.prototype.sub.sub.sub", 20, 1], + ["x", "-02.js", "this", 20, 32], + ["y", "-02.js", "this", 20, 41], + ["z", "-02.js", "this", 20, 50] + ]; + + for (let [label, value, description, line, column] of expectedResults) { + let target = gFilteredFunctions.selectedItem.target; + + if (label) { + is(target.querySelector(".results-panel-item-label").getAttribute("value"), + gDebugger.SourceUtils.trimUrlLength(label + "()"), + "The corect label (" + label + ") is currently selected."); + } else { + ok(!target.querySelector(".results-panel-item-label"), + "Shouldn't create empty label nodes."); + } + if (value) { + ok(target.querySelector(".results-panel-item-label-below").getAttribute("value").contains(value), + "The corect value (" + value + ") is attached."); + } else { + ok(!target.querySelector(".results-panel-item-label-below"), + "Shouldn't create empty label nodes."); + } + if (description) { + is(target.querySelector(".results-panel-item-label-before").getAttribute("value"), description, + "The corect description (" + description + ") is currently shown."); + } else { + ok(!target.querySelector(".results-panel-item-label-before"), + "Shouldn't create empty label nodes."); + } + + ok(isCaretPos(gPanel, line, column), + "The editor didn't jump to the correct line."); + + EventUtils.sendKey("DOWN", gDebugger); + } + + ok(isCaretPos(gPanel, expectedResults[0][3], expectedResults[0][4]), + "The editor didn't jump to the correct line again."); + + deferred.resolve(); + } else { + ok(false, "How did you get here? Go away, you."); + } + }); + + setText(gSearchBox, "@"); + return deferred.promise; +} + +function thirdJsSearch() { + let deferred = promise.defer(); + + once(gDebugger, "popupshown").then(() => { + writeInfo(); + + is(gFilteredFunctions.selectedIndex, 0, + "An item should be selected in the filtered functions view (1)."); + ok(gFilteredFunctions.selectedItem, + "An item should be selected in the filtered functions view (2)."); + + if (gSources.selectedItem.attachment.source.url.indexOf("-03.js") != -1) { + let s = " " + gDebugger.L10N.getStr("functionSearchSeparatorLabel") + " "; + let expectedResults = [ + ["namedEventListener", "-03.js", "", 4, 43], + ["a" + s + "A", "-03.js", "bar", 10, 5], + ["b" + s + "B", "-03.js", "bar.alpha", 15, 5], + ["c" + s + "C", "-03.js", "bar.alpha.beta", 20, 5], + ["d" + s + "D", "-03.js", "this.theta", 25, 5], + ["fun", "-03.js", "", 29, 7], + ["foo", "-03.js", "", 29, 13], + ["bar", "-03.js", "", 29, 19], + ["t_foo", "-03.js", "this", 29, 25], + ["w_bar" + s + "baz", "-03.js", "window", 29, 38] + ]; + + for (let [label, value, description, line, column] of expectedResults) { + let target = gFilteredFunctions.selectedItem.target; + + if (label) { + is(target.querySelector(".results-panel-item-label").getAttribute("value"), + gDebugger.SourceUtils.trimUrlLength(label + "()"), + "The corect label (" + label + ") is currently selected."); + } else { + ok(!target.querySelector(".results-panel-item-label"), + "Shouldn't create empty label nodes."); + } + if (value) { + ok(target.querySelector(".results-panel-item-label-below").getAttribute("value").contains(value), + "The corect value (" + value + ") is attached."); + } else { + ok(!target.querySelector(".results-panel-item-label-below"), + "Shouldn't create empty label nodes."); + } + if (description) { + is(target.querySelector(".results-panel-item-label-before").getAttribute("value"), description, + "The corect description (" + description + ") is currently shown."); + } else { + ok(!target.querySelector(".results-panel-item-label-before"), + "Shouldn't create empty label nodes."); + } + + ok(isCaretPos(gPanel, line, column), + "The editor didn't jump to the correct line."); + + EventUtils.sendKey("DOWN", gDebugger); + } + + ok(isCaretPos(gPanel, expectedResults[0][3], expectedResults[0][4]), + "The editor didn't jump to the correct line again."); + + deferred.resolve(); + } else { + ok(false, "How did you get here? Go away, you."); + } + }); + + setText(gSearchBox, "@"); + return deferred.promise; +} + +function filterSearch() { + let deferred = promise.defer(); + + once(gDebugger, "popupshown").then(() => { + writeInfo(); + + is(gFilteredFunctions.selectedIndex, 0, + "An item should be selected in the filtered functions view (1)."); + ok(gFilteredFunctions.selectedItem, + "An item should be selected in the filtered functions view (2)."); + + if (gSources.selectedItem.attachment.source.url.indexOf("-03.js") != -1) { + let s = " " + gDebugger.L10N.getStr("functionSearchSeparatorLabel") + " "; + let expectedResults = [ + ["namedEventListener", "-03.js", "", 4, 43], + ["bar", "-03.js", "", 29, 19], + ["w_bar" + s + "baz", "-03.js", "window", 29, 38], + ["anonymousExpression", "-01.js", "test.prototype", 9, 3], + ["namedExpression" + s + "NAME", "-01.js", "test.prototype", 11, 3], + ["arrow", ".html", "", 20, 11], + ["bar2", ".html", "", 23, 18] + ]; + + for (let [label, value, description, line, column] of expectedResults) { + let target = gFilteredFunctions.selectedItem.target; + + if (label) { + is(target.querySelector(".results-panel-item-label").getAttribute("value"), + gDebugger.SourceUtils.trimUrlLength(label + "()"), + "The corect label (" + label + ") is currently selected."); + } else { + ok(!target.querySelector(".results-panel-item-label"), + "Shouldn't create empty label nodes."); + } + if (value) { + ok(target.querySelector(".results-panel-item-label-below").getAttribute("value").contains(value), + "The corect value (" + value + ") is attached."); + } else { + ok(!target.querySelector(".results-panel-item-label-below"), + "Shouldn't create empty label nodes."); + } + if (description) { + is(target.querySelector(".results-panel-item-label-before").getAttribute("value"), description, + "The corect description (" + description + ") is currently shown."); + } else { + ok(!target.querySelector(".results-panel-item-label-before"), + "Shouldn't create empty label nodes."); + } + + ok(isCaretPos(gPanel, line, column), + "The editor didn't jump to the correct line."); + + EventUtils.sendKey("DOWN", gDebugger); + } + + ok(isCaretPos(gPanel, expectedResults[0][3], expectedResults[0][4]), + "The editor didn't jump to the correct line again."); + + deferred.resolve(); + } else { + ok(false, "How did you get here? Go away, you."); + } + }); + + setText(gSearchBox, "@r"); + return deferred.promise; +} + +function bogusSearch() { + let deferred = promise.defer(); + + once(gDebugger, "popuphidden").then(() => { + ok(true, "Popup was successfully hidden after no matches were found."); + deferred.resolve(); + }); + + setText(gSearchBox, "@bogus"); + return deferred.promise; +} + +function incrementalSearch() { + let deferred = promise.defer(); + + once(gDebugger, "popupshown").then(() => { + ok(true, "Popup was successfully shown after some matches were found."); + deferred.resolve(); + }); + + setText(gSearchBox, "@NAME"); + return deferred.promise; +} + +function emptySearch() { + let deferred = promise.defer(); + + once(gDebugger, "popuphidden").then(() => { + ok(true, "Popup was successfully hidden when nothing was searched."); + deferred.resolve(); + }); + + clearText(gSearchBox); + return deferred.promise; +} + +function showSource(aLabel) { + let deferred = promise.defer(); + + waitForSourceShown(gPanel, aLabel).then(deferred.resolve); + gSources.selectedItem = e => e.attachment.label == aLabel; + + return deferred.promise; +} + +function saveSearch() { + let finished = once(gDebugger, "popuphidden"); + + // Either by pressing RETURN or clicking on an item in the popup, + // the popup should hide and the item should become selected. + let random = Math.random(); + if (random >= 0.33) { + EventUtils.sendKey("RETURN", gDebugger); + } else if (random >= 0.66) { + EventUtils.sendKey("RETURN", gDebugger); + } else { + EventUtils.sendMouseEvent({ type: "click" }, + gFilteredFunctions.selectedItem.target, + gDebugger); + } + + return finished; +} + +function writeInfo() { + info("Current source url:\n" + getSelectedSourceURL(gSources)); + info("Debugger editor text:\n" + gEditor.getText()); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gEditor = null; + gSources = null; + gSearchBox = null; + gFilteredFunctions = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_searchbox-help-popup-01.js b/toolkit/devtools/debugger/test/browser_dbg_searchbox-help-popup-01.js new file mode 100644 index 000000000..29e4848f0 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_searchbox-help-popup-01.js @@ -0,0 +1,58 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that the searchbox popup is displayed when focusing the searchbox, + * and hidden when the user starts typing. + */ + +const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html"; + +let gTab, gPanel, gDebugger; +let gSearchBox, gSearchBoxPanel; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gSearchBox = gDebugger.DebuggerView.Filtering._searchbox; + gSearchBoxPanel = gDebugger.DebuggerView.Filtering._searchboxHelpPanel; + + waitForSourceAndCaretAndScopes(gPanel, "-02.js", 1) + .then(showPopup) + .then(hidePopup) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + + callInTab(gTab, "firstCall"); + }); +} + +function showPopup() { + is(gSearchBoxPanel.state, "closed", + "The search box panel shouldn't be visible yet."); + + let finished = once(gSearchBoxPanel, "popupshown"); + EventUtils.sendMouseEvent({ type: "click" }, gSearchBox, gDebugger); + return finished; +} + +function hidePopup() { + is(gSearchBoxPanel.state, "open", + "The search box panel should be visible after searching started."); + + let finished = once(gSearchBoxPanel, "popuphidden"); + setText(gSearchBox, "#"); + return finished; +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gSearchBox = null; + gSearchBoxPanel = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_searchbox-help-popup-02.js b/toolkit/devtools/debugger/test/browser_dbg_searchbox-help-popup-02.js new file mode 100644 index 000000000..f71951bc7 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_searchbox-help-popup-02.js @@ -0,0 +1,84 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that the searchbox popup isn't displayed when there's some text + * already present. + */ + +const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html"; + +let gTab, gPanel, gDebugger; +let gEditor, gSearchBox, gSearchBoxPanel; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gSearchBox = gDebugger.DebuggerView.Filtering._searchbox; + gSearchBoxPanel = gDebugger.DebuggerView.Filtering._searchboxHelpPanel; + + once(gSearchBoxPanel, "popupshown").then(() => { + ok(false, "Damn it, this shouldn't have happened."); + }); + + waitForSourceAndCaretAndScopes(gPanel, "-02.js", 1) + .then(tryShowPopup) + .then(focusEditor) + .then(testFocusLost) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + + callInTab(gTab, "firstCall"); + }); +} + +function tryShowPopup() { + setText(gSearchBox, "#call()"); + ok(isCaretPos(gPanel, 4, 22), + "The editor caret position appears to be correct."); + ok(isEditorSel(gPanel, [125, 131]), + "The editor selection appears to be correct."); + is(gEditor.getSelection(), "Call()", + "The editor selected text appears to be correct."); + + is(gSearchBoxPanel.state, "closed", + "The search box panel shouldn't be visible yet."); + + EventUtils.sendMouseEvent({ type: "click" }, gSearchBox, gDebugger); +} + +function focusEditor() { + let deferred = promise.defer(); + + // Focusing the editor takes a tick to update the caret and selection. + gEditor.focus(); + executeSoon(deferred.resolve); + + return deferred.promise; +} + +function testFocusLost() { + ok(isCaretPos(gPanel, 6, 1), + "The editor caret position appears to be correct after gaining focus."); + ok(isEditorSel(gPanel, [165, 165]), + "The editor selection appears to be correct after gaining focus."); + is(gEditor.getSelection(), "", + "The editor selected text appears to be correct after gaining focus."); + + is(gSearchBoxPanel.state, "closed", + "The search box panel should still not be visible."); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gEditor = null; + gSearchBox = null; + gSearchBoxPanel = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_searchbox-parse.js b/toolkit/devtools/debugger/test/browser_dbg_searchbox-parse.js new file mode 100644 index 000000000..efc30144c --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_searchbox-parse.js @@ -0,0 +1,124 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that text entered in the debugger's searchbox is properly parsed. + */ + +function test() { + initDebugger("about:blank").then(([aTab,, aPanel]) => { + let filterView = aPanel.panelWin.DebuggerView.Filtering; + let searchbox = aPanel.panelWin.DebuggerView.Filtering._searchbox; + + setText(searchbox, ""); + is(filterView.searchData.toSource(), '["", [""]]', + "The searchbox data wasn't parsed correctly (1)."); + + setText(searchbox, "#token"); + is(filterView.searchData.toSource(), '["#", ["", "token"]]', + "The searchbox data wasn't parsed correctly (2)."); + + setText(searchbox, ":42"); + is(filterView.searchData.toSource(), '[":", ["", 42]]', + "The searchbox data wasn't parsed correctly (3)."); + + setText(searchbox, "#token:42"); + is(filterView.searchData.toSource(), '["#", ["", "token:42"]]', + "The searchbox data wasn't parsed correctly (4)."); + + setText(searchbox, ":42#token"); + is(filterView.searchData.toSource(), '["#", [":42", "token"]]', + "The searchbox data wasn't parsed correctly (5)."); + + setText(searchbox, "#token:42#token:42"); + is(filterView.searchData.toSource(), '["#", ["#token:42", "token:42"]]', + "The searchbox data wasn't parsed correctly (6)."); + + setText(searchbox, ":42#token:42#token"); + is(filterView.searchData.toSource(), '["#", [":42#token:42", "token"]]', + "The searchbox data wasn't parsed correctly (7)."); + + + setText(searchbox, "file"); + is(filterView.searchData.toSource(), '["", ["file"]]', + "The searchbox data wasn't parsed correctly (8)."); + + setText(searchbox, "file#token"); + is(filterView.searchData.toSource(), '["#", ["file", "token"]]', + "The searchbox data wasn't parsed correctly (9)."); + + setText(searchbox, "file:42"); + is(filterView.searchData.toSource(), '[":", ["file", 42]]', + "The searchbox data wasn't parsed correctly (10)."); + + setText(searchbox, "file#token:42"); + is(filterView.searchData.toSource(), '["#", ["file", "token:42"]]', + "The searchbox data wasn't parsed correctly (11)."); + + setText(searchbox, "file:42#token"); + is(filterView.searchData.toSource(), '["#", ["file:42", "token"]]', + "The searchbox data wasn't parsed correctly (12)."); + + setText(searchbox, "file#token:42#token:42"); + is(filterView.searchData.toSource(), '["#", ["file#token:42", "token:42"]]', + "The searchbox data wasn't parsed correctly (13)."); + + setText(searchbox, "file:42#token:42#token"); + is(filterView.searchData.toSource(), '["#", ["file:42#token:42", "token"]]', + "The searchbox data wasn't parsed correctly (14)."); + + + setText(searchbox, "!token"); + is(filterView.searchData.toSource(), '["!", ["token"]]', + "The searchbox data wasn't parsed correctly (15)."); + + setText(searchbox, "!token#global"); + is(filterView.searchData.toSource(), '["!", ["token#global"]]', + "The searchbox data wasn't parsed correctly (16)."); + + setText(searchbox, "!token#global:42"); + is(filterView.searchData.toSource(), '["!", ["token#global:42"]]', + "The searchbox data wasn't parsed correctly (17)."); + + setText(searchbox, "!token:42#global"); + is(filterView.searchData.toSource(), '["!", ["token:42#global"]]', + "The searchbox data wasn't parsed correctly (18)."); + + + setText(searchbox, "@token"); + is(filterView.searchData.toSource(), '["@", ["token"]]', + "The searchbox data wasn't parsed correctly (19)."); + + setText(searchbox, "@token#global"); + is(filterView.searchData.toSource(), '["@", ["token#global"]]', + "The searchbox data wasn't parsed correctly (20)."); + + setText(searchbox, "@token#global:42"); + is(filterView.searchData.toSource(), '["@", ["token#global:42"]]', + "The searchbox data wasn't parsed correctly (21)."); + + setText(searchbox, "@token:42#global"); + is(filterView.searchData.toSource(), '["@", ["token:42#global"]]', + "The searchbox data wasn't parsed correctly (22)."); + + + setText(searchbox, "*token"); + is(filterView.searchData.toSource(), '["*", ["token"]]', + "The searchbox data wasn't parsed correctly (23)."); + + setText(searchbox, "*token#global"); + is(filterView.searchData.toSource(), '["*", ["token#global"]]', + "The searchbox data wasn't parsed correctly (24)."); + + setText(searchbox, "*token#global:42"); + is(filterView.searchData.toSource(), '["*", ["token#global:42"]]', + "The searchbox data wasn't parsed correctly (25)."); + + setText(searchbox, "*token:42#global"); + is(filterView.searchData.toSource(), '["*", ["token:42#global"]]', + "The searchbox data wasn't parsed correctly (26)."); + + + closeDebuggerAndFinish(aPanel); + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_server-conditional-bp-01.js b/toolkit/devtools/debugger/test/browser_dbg_server-conditional-bp-01.js new file mode 100644 index 000000000..e6cf32590 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_server-conditional-bp-01.js @@ -0,0 +1,250 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Bug 740825: Test adding conditional breakpoints (with server-side support) + */ + +const TAB_URL = EXAMPLE_URL + "doc_conditional-breakpoints.html"; + +function test() { + // Linux debug test slaves are a bit slow at this test sometimes. + requestLongerTimeout(2); + + let gTab, gPanel, gDebugger; + let gEditor, gSources, gBreakpoints, gBreakpointsAdded, gBreakpointsRemoving; + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gSources = gDebugger.DebuggerView.Sources; + gBreakpoints = gDebugger.DebuggerController.Breakpoints; + gBreakpointsAdded = gBreakpoints._added; + gBreakpointsRemoving = gBreakpoints._removing; + + waitForSourceAndCaretAndScopes(gPanel, ".html", 17) + .then(() => addBreakpoints()) + .then(() => initialChecks()) + .then(() => resumeAndTestBreakpoint(20)) + .then(() => resumeAndTestBreakpoint(21)) + .then(() => resumeAndTestBreakpoint(22)) + .then(() => resumeAndTestBreakpoint(23)) + .then(() => resumeAndTestBreakpoint(24)) + .then(() => resumeAndTestBreakpoint(25)) + .then(() => resumeAndTestBreakpoint(27)) + .then(() => resumeAndTestBreakpoint(28)) + .then(() => resumeAndTestBreakpoint(29)) + .then(() => resumeAndTestNoBreakpoint()) + .then(() => { + return promise.all([ + reloadActiveTab(gPanel, gDebugger.EVENTS.BREAKPOINT_SHOWN_IN_EDITOR, 13), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_SHOWN_IN_PANE, 13) + ]); + }) + .then(() => testAfterReload()) + .then(() => closeDebuggerAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + + callInTab(gTab, "ermahgerd"); + }); + + function addBreakpoints() { + return promise.resolve(null) + .then(() => gPanel.addBreakpoint({ actor: gSources.selectedValue, + line: 18, + condition: "undefined" + })) + .then(() => gPanel.addBreakpoint({ actor: gSources.selectedValue, + line: 19, + condition: "null" + })) + .then(() => gPanel.addBreakpoint({ actor: gSources.selectedValue, + line: 20, + condition: "42" + })) + .then(() => gPanel.addBreakpoint({ actor: gSources.selectedValue, + line: 21, + condition: "true" + })) + .then(() => gPanel.addBreakpoint({ actor: gSources.selectedValue, + line: 22, + condition: "'nasu'" + })) + .then(() => gPanel.addBreakpoint({ actor: gSources.selectedValue, + line: 23, + condition: "/regexp/" + })) + .then(() => gPanel.addBreakpoint({ actor: gSources.selectedValue, + line: 24, + condition: "({})" + })) + .then(() => gPanel.addBreakpoint({ actor: gSources.selectedValue, + line: 25, + condition: "(function() {})" + })) + .then(() => gPanel.addBreakpoint({ actor: gSources.selectedValue, + line: 26, + condition: "(function() { return false; })()" + })) + .then(() => gPanel.addBreakpoint({ actor: gSources.selectedValue, + line: 27, + condition: "a" + })) + .then(() => gPanel.addBreakpoint({ actor: gSources.selectedValue, + line: 28, + condition: "a !== undefined" + })) + .then(() => gPanel.addBreakpoint({ actor: gSources.selectedValue, + line: 29, + condition: "a !== null" + })) + .then(() => gPanel.addBreakpoint({ actor: gSources.selectedValue, + line: 30, + condition: "b" + })); + } + + function initialChecks() { + is(gDebugger.gThreadClient.state, "paused", + "Should only be getting stack frames while paused."); + is(gSources.itemCount, 1, + "Found the expected number of sources."); + is(gEditor.getText().indexOf("ermahgerd"), 253, + "The correct source was loaded initially."); + is(gSources.selectedValue, gSources.values[0], + "The correct source is selected."); + + is(gBreakpointsAdded.size, 13, + "13 breakpoints currently added."); + is(gBreakpointsRemoving.size, 0, + "No breakpoints currently being removed."); + is(gEditor.getBreakpoints().length, 13, + "13 breakpoints currently shown in the editor."); + + ok(!gBreakpoints._getAdded({ url: "foo", line: 3 }), + "_getAdded('foo', 3) returns falsey."); + ok(!gBreakpoints._getRemoving({ url: "bar", line: 3 }), + "_getRemoving('bar', 3) returns falsey."); + } + + function resumeAndTestBreakpoint(aLine) { + let finished = waitForCaretUpdated(gPanel, aLine).then(() => testBreakpoint(aLine)); + + EventUtils.sendMouseEvent({ type: "mousedown" }, + gDebugger.document.getElementById("resume"), + gDebugger); + + return finished; + } + + function resumeAndTestNoBreakpoint() { + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.AFTER_FRAMES_CLEARED).then(() => { + is(gSources.itemCount, 1, + "Found the expected number of sources."); + is(gEditor.getText().indexOf("ermahgerd"), 253, + "The correct source was loaded initially."); + is(gSources.selectedValue, gSources.values[0], + "The correct source is selected."); + + ok(gSources.selectedItem, + "There should be a selected source in the sources pane.") + ok(!gSources._selectedBreakpointItem, + "There should be no selected breakpoint in the sources pane.") + is(gSources._conditionalPopupVisible, false, + "The breakpoint conditional expression popup should not be shown."); + + is(gDebugger.document.querySelectorAll(".dbg-stackframe").length, 0, + "There should be no visible stackframes."); + is(gDebugger.document.querySelectorAll(".dbg-breakpoint").length, 13, + "There should be thirteen visible breakpoints."); + }); + + gDebugger.gThreadClient.resume(); + + return finished; + } + + function testBreakpoint(aLine, aHighlightBreakpoint) { + // Highlight the breakpoint only if required. + if (aHighlightBreakpoint) { + let finished = waitForCaretUpdated(gPanel, aLine).then(() => testBreakpoint(aLine)); + gSources.highlightBreakpoint({ actor: gSources.selectedValue, line: aLine }); + return finished; + } + + let selectedActor = gSources.selectedValue; + let selectedBreakpoint = gSources._selectedBreakpointItem; + + ok(selectedActor, + "There should be a selected item in the sources pane."); + ok(selectedBreakpoint, + "There should be a selected brekapoint in the sources pane."); + + is(selectedBreakpoint.attachment.actor, selectedActor, + "The breakpoint on line " + aLine + " wasn't added on the correct source."); + is(selectedBreakpoint.attachment.line, aLine, + "The breakpoint on line " + aLine + " wasn't found."); + is(!!selectedBreakpoint.attachment.disabled, false, + "The breakpoint on line " + aLine + " should be enabled."); + is(!!selectedBreakpoint.attachment.openPopup, false, + "The breakpoint on line " + aLine + " should not have opened a popup."); + is(gSources._conditionalPopupVisible, false, + "The breakpoint conditional expression popup should not have been shown."); + + return gBreakpoints._getAdded(selectedBreakpoint.attachment).then(aBreakpointClient => { + is(aBreakpointClient.location.actor, selectedActor, + "The breakpoint's client url is correct"); + is(aBreakpointClient.location.line, aLine, + "The breakpoint's client line is correct"); + isnot(aBreakpointClient.condition, undefined, + "The breakpoint on line " + aLine + " should have a conditional expression."); + + ok(isCaretPos(gPanel, aLine), + "The editor caret position is not properly set."); + }); + } + + function testAfterReload() { + let selectedActor = getSelectedSourceURL(gSources); + let selectedBreakpoint = gSources._selectedBreakpointItem; + + ok(selectedActor, + "There should be a selected item in the sources pane after reload."); + ok(!selectedBreakpoint, + "There should be no selected brekapoint in the sources pane after reload."); + + return promise.resolve(null) + .then(() => testBreakpoint(18, true)) + .then(() => testBreakpoint(19, true)) + .then(() => testBreakpoint(20, true)) + .then(() => testBreakpoint(21, true)) + .then(() => testBreakpoint(22, true)) + .then(() => testBreakpoint(23, true)) + .then(() => testBreakpoint(24, true)) + .then(() => testBreakpoint(25, true)) + .then(() => testBreakpoint(26, true)) + .then(() => testBreakpoint(27, true)) + .then(() => testBreakpoint(28, true)) + .then(() => testBreakpoint(29, true)) + .then(() => testBreakpoint(30, true)) + .then(() => { + is(gSources.itemCount, 1, + "Found the expected number of sources."); + is(gEditor.getText().indexOf("ermahgerd"), 253, + "The correct source was loaded again."); + is(gSources.selectedValue, gSources.values[0], + "The correct source is selected."); + + ok(gSources.selectedItem, + "There should be a selected source in the sources pane.") + ok(gSources._selectedBreakpointItem, + "There should be a selected breakpoint in the sources pane.") + is(gSources._conditionalPopupVisible, false, + "The breakpoint conditional expression popup should not be shown."); + }); + } +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_server-conditional-bp-02.js b/toolkit/devtools/debugger/test/browser_dbg_server-conditional-bp-02.js new file mode 100644 index 000000000..1fad10177 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_server-conditional-bp-02.js @@ -0,0 +1,191 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Bug 812172: Test adding and modifying conditional breakpoints (with server-side support) + */ + +const TAB_URL = EXAMPLE_URL + "doc_conditional-breakpoints.html"; + +function test() { + let gTab, gPanel, gDebugger; + let gEditor, gSources, gBreakpoints, gBreakpointsAdded, gBreakpointsRemoving; + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gSources = gDebugger.DebuggerView.Sources; + gBreakpoints = gDebugger.DebuggerController.Breakpoints; + gBreakpointsAdded = gBreakpoints._added; + gBreakpointsRemoving = gBreakpoints._removing; + + waitForSourceAndCaretAndScopes(gPanel, ".html", 17) + .then(() => initialChecks()) + .then(() => addBreakpoint1()) + .then(() => testBreakpoint(18, false, false, undefined)) + .then(() => addBreakpoint2()) + .then(() => testBreakpoint(19, false, false, undefined)) + .then(() => modBreakpoint2()) + .then(() => testBreakpoint(19, false, true, undefined)) + .then(() => addBreakpoint3()) + .then(() => testBreakpoint(20, true, false, undefined)) + .then(() => modBreakpoint3()) + .then(() => testBreakpoint(20, true, false, "bamboocha")) + .then(() => addBreakpoint4()) + .then(() => testBreakpoint(21, false, false, undefined)) + .then(() => delBreakpoint4()) + .then(() => setCaretPosition(18)) + .then(() => testBreakpoint(18, false, false, undefined)) + .then(() => setCaretPosition(19)) + .then(() => testBreakpoint(19, false, false, undefined)) + .then(() => setCaretPosition(20)) + .then(() => testBreakpoint(20, true, false, "bamboocha")) + .then(() => setCaretPosition(17)) + .then(() => testNoBreakpoint(17)) + .then(() => setCaretPosition(21)) + .then(() => testNoBreakpoint(21)) + .then(() => clickOnBreakpoint(0)) + .then(() => testBreakpoint(18, false, false, undefined)) + .then(() => clickOnBreakpoint(1)) + .then(() => testBreakpoint(19, false, false, undefined)) + .then(() => clickOnBreakpoint(2)) + .then(() => testBreakpoint(20, true, true, "bamboocha")) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + + callInTab(gTab, "ermahgerd"); + }); + + function initialChecks() { + is(gDebugger.gThreadClient.state, "paused", + "Should only be getting stack frames while paused."); + is(gSources.itemCount, 1, + "Found the expected number of sources."); + is(gEditor.getText().indexOf("ermahgerd"), 253, + "The correct source was loaded initially."); + is(gSources.selectedValue, gSources.values[0], + "The correct source is selected."); + + is(gBreakpointsAdded.size, 0, + "No breakpoints currently added."); + is(gBreakpointsRemoving.size, 0, + "No breakpoints currently being removed."); + is(gEditor.getBreakpoints().length, 0, + "No breakpoints currently shown in the editor."); + + ok(!gBreakpoints._getAdded({ actor: "foo", line: 3 }), + "_getAdded('foo', 3) returns falsey."); + ok(!gBreakpoints._getRemoving({ actor: "bar", line: 3 }), + "_getRemoving('bar', 3) returns falsey."); + } + + function addBreakpoint1() { + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_ADDED); + gPanel.addBreakpoint({ actor: gSources.selectedValue, line: 18 }); + return finished; + } + + function addBreakpoint2() { + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_ADDED); + setCaretPosition(19); + gSources._onCmdAddBreakpoint(); + return finished; + } + + function modBreakpoint2() { + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.CONDITIONAL_BREAKPOINT_POPUP_SHOWING); + setCaretPosition(19); + gSources._onCmdAddConditionalBreakpoint(); + return finished; + } + + function addBreakpoint3() { + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_ADDED); + setCaretPosition(20); + gSources._onCmdAddConditionalBreakpoint(); + return finished; + } + + function modBreakpoint3() { + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.CONDITIONAL_BREAKPOINT_POPUP_HIDING); + typeText(gSources._cbTextbox, "bamboocha"); + EventUtils.sendKey("RETURN", gDebugger); + return finished; + } + + function addBreakpoint4() { + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_ADDED); + setCaretPosition(21); + gSources._onCmdAddBreakpoint(); + return finished; + } + + function delBreakpoint4() { + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_REMOVED); + setCaretPosition(21); + gSources._onCmdAddBreakpoint(); + return finished; + } + + function testBreakpoint(aLine, aOpenPopupFlag, aPopupVisible, aConditionalExpression) { + let selectedActor = gSources.selectedValue; + let selectedBreakpoint = gSources._selectedBreakpointItem; + + ok(selectedActor, + "There should be a selected item in the sources pane."); + ok(selectedBreakpoint, + "There should be a selected brekapoint in the sources pane."); + + is(selectedBreakpoint.attachment.actor, selectedActor, + "The breakpoint on line " + aLine + " wasn't added on the correct source."); + is(selectedBreakpoint.attachment.line, aLine, + "The breakpoint on line " + aLine + " wasn't found."); + is(!!selectedBreakpoint.attachment.disabled, false, + "The breakpoint on line " + aLine + " should be enabled."); + is(!!selectedBreakpoint.attachment.openPopup, aOpenPopupFlag, + "The breakpoint on line " + aLine + " should have a correct popup state (1)."); + is(gSources._conditionalPopupVisible, aPopupVisible, + "The breakpoint on line " + aLine + " should have a correct popup state (2)."); + + return gBreakpoints._getAdded(selectedBreakpoint.attachment).then(aBreakpointClient => { + is(aBreakpointClient.location.actor, selectedActor, + "The breakpoint's client url is correct"); + is(aBreakpointClient.location.line, aLine, + "The breakpoint's client line is correct"); + is(aBreakpointClient.condition, aConditionalExpression, + "The breakpoint on line " + aLine + " should have a correct conditional expression."); + is("condition" in aBreakpointClient, !!aConditionalExpression, + "The breakpoint on line " + aLine + " should have a correct conditional state."); + + ok(isCaretPos(gPanel, aLine), + "The editor caret position is not properly set."); + }); + } + + function testNoBreakpoint(aLine) { + let selectedUrl = getSelectedSourceURL(gSources); + let selectedBreakpoint = gSources._selectedBreakpointItem; + + ok(selectedUrl, + "There should be a selected item in the sources pane for line " + aLine + "."); + ok(!selectedBreakpoint, + "There should be no selected brekapoint in the sources pane for line " + aLine + "."); + + ok(isCaretPos(gPanel, aLine), + "The editor caret position is not properly set."); + } + + function setCaretPosition(aLine) { + gEditor.setCursor({ line: aLine - 1, ch: 0 }); + } + + function clickOnBreakpoint(aIndex) { + EventUtils.sendMouseEvent({ type: "click" }, + gDebugger.document.querySelectorAll(".dbg-breakpoint")[aIndex], + gDebugger); + } +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_server-conditional-bp-03.js b/toolkit/devtools/debugger/test/browser_dbg_server-conditional-bp-03.js new file mode 100644 index 000000000..604320be6 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_server-conditional-bp-03.js @@ -0,0 +1,82 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that conditional breakpoints survive disabled breakpoints + * (with server-side support) + */ + +const TAB_URL = EXAMPLE_URL + "doc_conditional-breakpoints.html"; + +function test() { + let gTab, gPanel, gDebugger; + let gSources, gBreakpoints, gLocation; + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gSources = gDebugger.DebuggerView.Sources; + gBreakpoints = gDebugger.DebuggerController.Breakpoints; + + gLocation = { actor: gSources.selectedValue, line: 18 }; + + waitForSourceAndCaretAndScopes(gPanel, ".html", 17) + .then(addBreakpoint) + .then(setConditional) + .then(() => { + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_REMOVED); + toggleBreakpoint(); + return finished; + }) + .then(() => { + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_ADDED); + toggleBreakpoint(); + return finished; + }) + .then(testConditionalExpressionOnClient) + .then(() => { + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.CONDITIONAL_BREAKPOINT_POPUP_SHOWING); + openConditionalPopup(); + return finished; + }) + .then(testConditionalExpressionInPopup) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + + callInTab(gTab, "ermahgerd"); + }); + + function addBreakpoint() { + return gPanel.addBreakpoint(gLocation); + } + + function setConditional(aClient) { + return gBreakpoints.updateCondition(aClient.location, "hello"); + } + + function toggleBreakpoint() { + EventUtils.sendMouseEvent({ type: "click" }, + gDebugger.document.querySelector(".dbg-breakpoint-checkbox"), + gDebugger); + } + + function openConditionalPopup() { + EventUtils.sendMouseEvent({ type: "click" }, + gDebugger.document.querySelector(".dbg-breakpoint"), + gDebugger); + } + + function testConditionalExpressionOnClient() { + return gBreakpoints._getAdded(gLocation).then(aClient => { + is(aClient.condition, "hello", "The expression is correct (1)."); + }); + } + + function testConditionalExpressionInPopup() { + let textbox = gDebugger.document.getElementById("conditional-breakpoint-panel-textbox"); + is(textbox.value, "hello", "The expression is correct (2).") + } +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_server-conditional-bp-04.js b/toolkit/devtools/debugger/test/browser_dbg_server-conditional-bp-04.js new file mode 100644 index 000000000..43fc2e1b2 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_server-conditional-bp-04.js @@ -0,0 +1,84 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that conditional breakpoints with undefined expressions + * are stored as plain breakpoints when re-enabling them (with + * server-side support) + */ + +const TAB_URL = EXAMPLE_URL + "doc_conditional-breakpoints.html"; + +function test() { + let gTab, gPanel, gDebugger; + let gSources, gBreakpoints, gLocation; + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gSources = gDebugger.DebuggerView.Sources; + gBreakpoints = gDebugger.DebuggerController.Breakpoints; + + gLocation = { actor: gSources.selectedValue, line: 18 }; + + waitForSourceAndCaretAndScopes(gPanel, ".html", 17) + .then(addBreakpoint) + .then(setDummyConditional) + .then(() => { + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_REMOVED); + toggleBreakpoint(); + return finished; + }) + .then(() => { + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_ADDED); + toggleBreakpoint(); + return finished; + }) + .then(testConditionalExpressionOnClient) + .then(() => { + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.CONDITIONAL_BREAKPOINT_POPUP_SHOWING); + openConditionalPopup(); + finished.then(() => ok(false, "The popup shouldn't have opened.")); + return waitForTime(1000); + }) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + + callInTab(gTab, "ermahgerd"); + }); + + function addBreakpoint() { + return gPanel.addBreakpoint(gLocation); + } + + function setDummyConditional(aClient) { + // This happens when a conditional expression input popup is shown + // but the user doesn't type anything into it. + return gBreakpoints.updateCondition(aClient.location, ''); + } + + function toggleBreakpoint() { + EventUtils.sendMouseEvent({ type: "click" }, + gDebugger.document.querySelector(".dbg-breakpoint-checkbox"), + gDebugger); + } + + function openConditionalPopup() { + EventUtils.sendMouseEvent({ type: "click" }, + gDebugger.document.querySelector(".dbg-breakpoint"), + gDebugger); + } + + function testConditionalExpressionOnClient() { + return gBreakpoints._getAdded(gLocation).then(aClient => { + if ("condition" in aClient) { + ok(false, "A conditional expression shouldn't have been set."); + } else { + ok(true, "The conditional expression wasn't set, as expected."); + } + }); + } +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_source-maps-01.js b/toolkit/devtools/debugger/test/browser_dbg_source-maps-01.js new file mode 100644 index 000000000..4bbbb14be --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_source-maps-01.js @@ -0,0 +1,167 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we can set breakpoints and step through source mapped + * coffee script. + */ + +const TAB_URL = EXAMPLE_URL + "doc_binary_search.html"; +const COFFEE_URL = EXAMPLE_URL + "code_binary_search.coffee"; + +let gTab, gPanel, gDebugger; +let gEditor, gSources; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gSources = gDebugger.DebuggerView.Sources; + + checkSourceMapsEnabled(); + + waitForSourceShown(gPanel, ".coffee") + .then(checkInitialSource) + .then(testSetBreakpoint) + .then(testSetBreakpointBlankLine) + .then(testHitBreakpoint) + .then(testStepping) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); +} + +function checkSourceMapsEnabled() { + is(Services.prefs.getBoolPref("devtools.debugger.source-maps-enabled"), true, + "The source maps functionality should be enabled by default."); + is(gDebugger.Prefs.sourceMapsEnabled, true, + "The source maps pref should be true from startup."); + is(gDebugger.DebuggerView.Options._showOriginalSourceItem.getAttribute("checked"), "true", + "Source maps should be enabled from startup.") +} + +function checkInitialSource() { + isnot(gSources.selectedItem.attachment.source.url.indexOf(".coffee"), -1, + "The debugger should show the source mapped coffee source file."); + is(gSources.selectedValue.indexOf(".js"), -1, + "The debugger should not show the generated js source file."); + is(gEditor.getText().indexOf("isnt"), 218, + "The debugger's editor should have the coffee source source displayed."); + is(gEditor.getText().indexOf("function"), -1, + "The debugger's editor should not have the JS source displayed."); +} + +function testSetBreakpoint() { + let deferred = promise.defer(); + let sourceForm = getSourceForm(gSources, COFFEE_URL); + + gDebugger.gThreadClient.interrupt(aResponse => { + let source = gDebugger.gThreadClient.source(sourceForm); + source.setBreakpoint({ line: 5 }, aResponse => { + ok(!aResponse.error, + "Should be able to set a breakpoint in a coffee source file."); + ok(!aResponse.actualLocation, + "Should be able to set a breakpoint on line 5."); + + deferred.resolve(); + }); + }); + + return deferred.promise; +} + +function testSetBreakpointBlankLine() { + let deferred = promise.defer(); + let sourceForm = getSourceForm(gSources, COFFEE_URL); + + let source = gDebugger.gThreadClient.source(sourceForm); + source.setBreakpoint({ line: 3 }, aResponse => { + ok(!aResponse.error, + "Should be able to set a breakpoint in a coffee source file on a blank line."); + ok(aResponse.actualLocation, + "Because 3 is empty, we should have an actualLocation."); + is(aResponse.actualLocation.source.url, COFFEE_URL, + "actualLocation.actor should be source mapped to the coffee file."); + is(aResponse.actualLocation.line, 2, + "actualLocation.line should be source mapped back to 2."); + + deferred.resolve(); + }); + + return deferred.promise; +} + +function testHitBreakpoint() { + let deferred = promise.defer(); + + gDebugger.gThreadClient.resume(aResponse => { + ok(!aResponse.error, "Shouldn't get an error resuming."); + is(aResponse.type, "resumed", "Type should be 'resumed'."); + + gDebugger.gThreadClient.addOneTimeListener("paused", (aEvent, aPacket) => { + is(aPacket.type, "paused", + "We should now be paused again."); + is(aPacket.why.type, "breakpoint", + "and the reason we should be paused is because we hit a breakpoint."); + + // Check that we stopped at the right place, by making sure that the + // environment is in the state that we expect. + is(aPacket.frame.environment.bindings.variables.start.value, 0, + "'start' is 0."); + is(aPacket.frame.environment.bindings.variables.stop.value.type, "undefined", + "'stop' hasn't been assigned to yet."); + is(aPacket.frame.environment.bindings.variables.pivot.value.type, "undefined", + "'pivot' hasn't been assigned to yet."); + + waitForCaretUpdated(gPanel, 5).then(deferred.resolve); + }); + + // This will cause the breakpoint to be hit, and put us back in the + // paused state. + callInTab(gTab, "binary_search", [0, 2, 3, 5, 7, 10], 5); + }); + + return deferred.promise; +} + +function testStepping() { + let deferred = promise.defer(); + + gDebugger.gThreadClient.stepIn(aResponse => { + ok(!aResponse.error, "Shouldn't get an error resuming."); + is(aResponse.type, "resumed", "Type should be 'resumed'."); + + // After stepping, we will pause again, so listen for that. + gDebugger.gThreadClient.addOneTimeListener("paused", (aEvent, aPacket) => { + is(aPacket.type, "paused", + "We should now be paused again."); + is(aPacket.why.type, "resumeLimit", + "and the reason we should be paused is because we hit the resume limit."); + + // Check that we stopped at the right place, by making sure that the + // environment is in the state that we expect. + is(aPacket.frame.environment.bindings.variables.start.value, 0, + "'start' is 0."); + is(aPacket.frame.environment.bindings.variables.stop.value, 5, + "'stop' hasn't been assigned to yet."); + is(aPacket.frame.environment.bindings.variables.pivot.value.type, "undefined", + "'pivot' hasn't been assigned to yet."); + + waitForCaretUpdated(gPanel, 6).then(deferred.resolve); + }); + }); + + return deferred.promise; +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gEditor = null; + gSources = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_source-maps-02.js b/toolkit/devtools/debugger/test/browser_dbg_source-maps-02.js new file mode 100644 index 000000000..433843475 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_source-maps-02.js @@ -0,0 +1,148 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we can toggle between the original and generated sources. + */ + +const TAB_URL = EXAMPLE_URL + "doc_binary_search.html"; +const JS_URL = EXAMPLE_URL + "code_binary_search.js"; + +let gTab, gPanel, gDebugger, gEditor; +let gSources, gFrames, gPrefs, gOptions; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gSources = gDebugger.DebuggerView.Sources; + gFrames = gDebugger.DebuggerView.StackFrames; + gPrefs = gDebugger.Prefs; + gOptions = gDebugger.DebuggerView.Options; + + waitForSourceShown(gPanel, ".coffee") + .then(testToggleGeneratedSource) + .then(testSetBreakpoint) + .then(testToggleOnPause) + .then(testResume) + .then(() => closeDebuggerAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); +} + +function testToggleGeneratedSource() { + let finished = waitForSourceShown(gPanel, ".js").then(() => { + is(gPrefs.sourceMapsEnabled, false, + "The source maps pref should have been set to false."); + is(gOptions._showOriginalSourceItem.getAttribute("checked"), "false", + "Source maps should now be disabled.") + + is(gSources.selectedItem.attachment.source.url.indexOf(".coffee"), -1, + "The debugger should not show the source mapped coffee source file."); + isnot(gSources.selectedItem.attachment.source.url.indexOf(".js"), -1, + "The debugger should show the generated js source file."); + + is(gEditor.getText().indexOf("isnt"), -1, + "The debugger's editor should not have the coffee source source displayed."); + is(gEditor.getText().indexOf("function"), 36, + "The debugger's editor should have the JS source displayed."); + }); + + gOptions._showOriginalSourceItem.setAttribute("checked", "false"); + gOptions._toggleShowOriginalSource(); + gOptions._onPopupHidden(); + + return finished; +} + +function testSetBreakpoint() { + let deferred = promise.defer(); + let sourceForm = getSourceForm(gSources, JS_URL); + let source = gDebugger.gThreadClient.source(sourceForm); + + source.setBreakpoint({ line: 7 }, aResponse => { + ok(!aResponse.error, + "Should be able to set a breakpoint in a js file."); + + gDebugger.gClient.addOneTimeListener("resumed", () => { + waitForCaretAndScopes(gPanel, 7).then(() => { + // Make sure that we have JavaScript stack frames. + is(gFrames.itemCount, 1, + "Should have only one frame."); + is(gFrames.getItemAtIndex(0).attachment.url.indexOf(".coffee"), -1, + "First frame should not be a coffee source frame."); + isnot(gFrames.getItemAtIndex(0).attachment.url.indexOf(".js"), -1, + "First frame should be a JS frame."); + + deferred.resolve(); + }); + + // This will cause the breakpoint to be hit, and put us back in the + // paused state. + callInTab(gTab, "binary_search", [0, 2, 3, 5, 7, 10], 5); + }); + }); + + return deferred.promise; +} + +function testToggleOnPause() { + let finished = waitForSourceAndCaretAndScopes(gPanel, ".coffee", 5).then(() => { + is(gPrefs.sourceMapsEnabled, true, + "The source maps pref should have been set to true."); + is(gOptions._showOriginalSourceItem.getAttribute("checked"), "true", + "Source maps should now be enabled.") + + isnot(gSources.selectedItem.attachment.source.url.indexOf(".coffee"), -1, + "The debugger should show the source mapped coffee source file."); + is(gSources.selectedItem.attachment.source.url.indexOf(".js"), -1, + "The debugger should not show the generated js source file."); + + is(gEditor.getText().indexOf("isnt"), 218, + "The debugger's editor should have the coffee source source displayed."); + is(gEditor.getText().indexOf("function"), -1, + "The debugger's editor should not have the JS source displayed."); + + // Make sure that we have coffee source stack frames. + is(gFrames.itemCount, 1, + "Should have only one frame."); + is(gFrames.getItemAtIndex(0).attachment.url.indexOf(".js"), -1, + "First frame should not be a JS frame."); + isnot(gFrames.getItemAtIndex(0).attachment.url.indexOf(".coffee"), -1, + "First frame should be a coffee source frame."); + }); + + gOptions._showOriginalSourceItem.setAttribute("checked", "true"); + gOptions._toggleShowOriginalSource(); + gOptions._onPopupHidden(); + + return finished; +} + +function testResume() { + let deferred = promise.defer(); + + gDebugger.gThreadClient.resume(aResponse => { + ok(!aResponse.error, "Shouldn't get an error resuming."); + is(aResponse.type, "resumed", "Type should be 'resumed'."); + + deferred.resolve(); + }); + + return deferred.promise; +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gEditor = null; + gSources = null; + gFrames = null; + gPrefs = null; + gOptions = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_source-maps-03.js b/toolkit/devtools/debugger/test/browser_dbg_source-maps-03.js new file mode 100644 index 000000000..a7a48f361 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_source-maps-03.js @@ -0,0 +1,83 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we can debug minified javascript with source maps. + */ + +const TAB_URL = EXAMPLE_URL + "doc_minified.html"; +const JS_URL = EXAMPLE_URL + "code_math.js"; + +let gTab, gPanel, gDebugger; +let gEditor, gSources, gFrames; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gSources = gDebugger.DebuggerView.Sources; + gFrames = gDebugger.DebuggerView.StackFrames; + + waitForSourceShown(gPanel, JS_URL) + .then(checkInitialSource) + .then(testSetBreakpoint) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); +} + +function checkInitialSource() { + isnot(gSources.selectedItem.attachment.source.url.indexOf(".js"), -1, + "The debugger should not show the minified js file."); + is(gSources.selectedItem.attachment.source.url.indexOf(".min.js"), -1, + "The debugger should show the original js file."); + is(gEditor.getText().split("\n").length, 46, + "The debugger's editor should have the original source displayed, " + + "not the whitespace stripped minified version."); +} + +function testSetBreakpoint() { + let deferred = promise.defer(); + let sourceForm = getSourceForm(gSources, JS_URL); + let source = gDebugger.gThreadClient.source(sourceForm); + + source.setBreakpoint({ line: 30, column: 21 }, aResponse => { + ok(!aResponse.error, + "Should be able to set a breakpoint in a js file."); + ok(!aResponse.actualLocation, + "Should be able to set a breakpoint on line 30 and column 10."); + + gDebugger.gClient.addOneTimeListener("resumed", () => { + waitForCaretAndScopes(gPanel, 30).then(() => { + // Make sure that we have the right stack frames. + is(gFrames.itemCount, 9, + "Should have nine frames."); + is(gFrames.getItemAtIndex(0).attachment.url.indexOf(".min.js"), -1, + "First frame should not be a minified JS frame."); + isnot(gFrames.getItemAtIndex(0).attachment.url.indexOf(".js"), -1, + "First frame should be a JS frame."); + + deferred.resolve(); + }); + + // This will cause the breakpoint to be hit, and put us back in the + // paused state. + callInTab(gTab, "arithmetic"); + }); + }); + + return deferred.promise; +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gEditor = null; + gSources = null; + gFrames = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_source-maps-04.js b/toolkit/devtools/debugger/test/browser_dbg_source-maps-04.js new file mode 100644 index 000000000..cc285c493 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_source-maps-04.js @@ -0,0 +1,183 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that bogus source maps don't break debugging. + */ + +const TAB_URL = EXAMPLE_URL + "doc_minified_bogus_map.html"; +const JS_URL = EXAMPLE_URL + "code_math_bogus_map.js"; + +// This test causes an error to be logged in the console, which appears in TBPL +// logs, so we are disabling that here. +let { DevToolsUtils } = Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm", {}); +DevToolsUtils.reportingDisabled = true; + +let gPanel, gDebugger, gFrames, gSources, gPrefs, gOptions; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gFrames = gDebugger.DebuggerView.StackFrames; + gSources = gDebugger.DebuggerView.Sources; + gPrefs = gDebugger.Prefs; + gOptions = gDebugger.DebuggerView.Options; + + is(gPrefs.pauseOnExceptions, false, + "The pause-on-exceptions pref should be disabled by default."); + isnot(gOptions._pauseOnExceptionsItem.getAttribute("checked"), "true", + "The pause-on-exceptions menu item should not be checked."); + + waitForSourceShown(gPanel, JS_URL) + .then(checkInitialSource) + .then(enablePauseOnExceptions) + .then(disableIgnoreCaughtExceptions) + .then(testSetBreakpoint) + .then(reloadPage) + .then(testHitBreakpoint) + .then(enableIgnoreCaughtExceptions) + .then(disablePauseOnExceptions) + .then(() => closeDebuggerAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); +} + +function checkInitialSource() { + isnot(gSources.selectedItem.attachment.source.url.indexOf("code_math_bogus_map.js"), -1, + "The debugger should show the minified js file."); +} + +function enablePauseOnExceptions() { + let deferred = promise.defer(); + + gDebugger.gThreadClient.addOneTimeListener("resumed", () => { + is(gPrefs.pauseOnExceptions, true, + "The pause-on-exceptions pref should now be enabled."); + + ok(true, "Pausing on exceptions was enabled."); + deferred.resolve(); + }); + + gOptions._pauseOnExceptionsItem.setAttribute("checked", "true"); + gOptions._togglePauseOnExceptions(); + + return deferred.promise; +} + +function disableIgnoreCaughtExceptions() { + let deferred = promise.defer(); + + gDebugger.gThreadClient.addOneTimeListener("resumed", () => { + is(gPrefs.ignoreCaughtExceptions, false, + "The ignore-caught-exceptions pref should now be disabled."); + + ok(true, "Ignore caught exceptions was disabled."); + deferred.resolve(); + }); + + gOptions._ignoreCaughtExceptionsItem.setAttribute("checked", "false"); + gOptions._toggleIgnoreCaughtExceptions(); + + return deferred.promise; +} + +function testSetBreakpoint() { + let deferred = promise.defer(); + let sourceForm = getSourceForm(gSources, JS_URL); + let source = gDebugger.gThreadClient.source(sourceForm); + + source.setBreakpoint({ line: 3, column: 61 }, aResponse => { + ok(!aResponse.error, + "Should be able to set a breakpoint in a js file."); + ok(!aResponse.actualLocation, + "Should be able to set a breakpoint on line 3 and column 61."); + + deferred.resolve(); + }); + + return deferred.promise; +} + +function reloadPage() { + let loaded = waitForSourceAndCaret(gPanel, ".js", 3); + gDebugger.DebuggerController._target.activeTab.reload(); + return loaded.then(() => ok(true, "Page was reloaded and execution resumed.")); +} + +function testHitBreakpoint() { + let deferred = promise.defer(); + + gDebugger.gThreadClient.resume(aResponse => { + ok(!aResponse.error, "Shouldn't get an error resuming."); + is(aResponse.type, "resumed", "Type should be 'resumed'."); + + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES).then(() => { + is(gFrames.itemCount, 2, "Should have two frames."); + + // This is weird, but we need to let the debugger a chance to + // update first + executeSoon(() => { + gDebugger.gThreadClient.resume(() => { + gDebugger.gThreadClient.addOneTimeListener("paused", () => { + gDebugger.gThreadClient.resume(() => { + // We also need to make sure the next step doesn't add a + // "resumed" handler until this is completely finished + executeSoon(() => { + deferred.resolve(); + }); + }); + }); + }); + }); + }); + }); + + return deferred.promise; +} + +function enableIgnoreCaughtExceptions() { + let deferred = promise.defer(); + + gDebugger.gThreadClient.addOneTimeListener("resumed", () => { + is(gPrefs.ignoreCaughtExceptions, true, + "The ignore-caught-exceptions pref should now be enabled."); + + ok(true, "Ignore caught exceptions was enabled."); + deferred.resolve(); + }); + + gOptions._ignoreCaughtExceptionsItem.setAttribute("checked", "true"); + gOptions._toggleIgnoreCaughtExceptions(); + + return deferred.promise; +} + +function disablePauseOnExceptions() { + let deferred = promise.defer(); + + gDebugger.gThreadClient.addOneTimeListener("resumed", () => { + is(gPrefs.pauseOnExceptions, false, + "The pause-on-exceptions pref should now be disabled."); + + ok(true, "Pausing on exceptions was disabled."); + deferred.resolve(); + }); + + gOptions._pauseOnExceptionsItem.setAttribute("checked", "false"); + gOptions._togglePauseOnExceptions(); + + return deferred.promise; +} + +registerCleanupFunction(function() { + gPanel = null; + gDebugger = null; + gFrames = null; + gSources = null; + gPrefs = null; + gOptions = null; + DevToolsUtils.reportingDisabled = false; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_sources-bookmarklet.js b/toolkit/devtools/debugger/test/browser_dbg_sources-bookmarklet.js new file mode 100644 index 000000000..e4a5841a4 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_sources-bookmarklet.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure javascript bookmarklet scripts appear and load correctly in the source list + */ + +const TAB_URL = EXAMPLE_URL + "doc_script-bookmarklet.html"; + +const BOOKMARKLET_SCRIPT_CODE = "console.log('bookmarklet executed');"; + +function test() { + let gTab, gPanel, gDebugger; + let gSources, gBreakpoints; + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gSources = gDebugger.DebuggerView.Sources; + gBreakpoints = gDebugger.DebuggerController.Breakpoints; + + return Task.spawn(function*() { + let waitForSource = waitForDebuggerEvents(gPanel, gPanel.panelWin.EVENTS.NEW_SOURCE, 1); + + // NOTE: devtools debugger panel needs to be already open, + // or the bookmarklet script will not be shown in the sources panel + callInTab(gTab, "injectBookmarklet", BOOKMARKLET_SCRIPT_CODE); + + yield waitForSource; + + is(gSources.values.length, 2, "Should have 2 source"); + + let item = gSources.getItemForAttachment(e => { + return e.label.indexOf("javascript:") === 0; + }); + ok(item, "Source label is incorrect."); + + let res = yield promiseInvoke(gDebugger.DebuggerController.client, + gDebugger.DebuggerController.client.request, + { to: item.value, type: "source"}); + + ok(res && res.source == BOOKMARKLET_SCRIPT_CODE, "SourceActor reply received"); + is(res.source, BOOKMARKLET_SCRIPT_CODE, "source is correct"); + is(res.contentType, "text/javascript", "contentType is correct"); + + yield closeDebuggerAndFinish(gPanel); + }); + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_sources-cache.js b/toolkit/devtools/debugger/test/browser_dbg_sources-cache.js new file mode 100644 index 000000000..b838abf8d --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_sources-cache.js @@ -0,0 +1,142 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the sources cache knows how to cache sources when prompted. + */ + +const TAB_URL = EXAMPLE_URL + "doc_function-search.html"; +const TOTAL_SOURCES = 4; + +let gTab, gDebuggee, gPanel, gDebugger; +let gEditor, gSources, gControllerSources; +let gPrevLabelsCache, gPrevGroupsCache; + +function test() { + initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => { + gTab = aTab; + gDebuggee = aDebuggee; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gSources = gDebugger.DebuggerView.Sources; + gControllerSources = gDebugger.DebuggerController.SourceScripts; + gPrevLabelsCache = gDebugger.SourceUtils._labelsCache; + gPrevGroupsCache = gDebugger.SourceUtils._groupsCache; + + waitForSourceShown(gPanel, "-01.js") + .then(initialChecks) + .then(getTextForSourcesAndCheckIntegrity) + .then(performReloadAndTestState) + .then(() => closeDebuggerAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); +} + +function initialChecks() { + ok(gEditor.getText().contains("First source!"), + "Editor text contents appears to be correct."); + is(gSources.selectedItem.attachment.label, "code_function-search-01.js", + "The currently selected label in the sources container is correct."); + ok(getSelectedSourceURL(gSources).contains("code_function-search-01.js"), + "The currently selected value in the sources container appears to be correct."); + + is(gSources.itemCount, TOTAL_SOURCES, + "There should be " + TOTAL_SOURCES + " sources present in the sources list."); + is(gSources.visibleItems.length, TOTAL_SOURCES, + "There should be " + TOTAL_SOURCES + " sources visible in the sources list."); + is(gSources.attachments.length, TOTAL_SOURCES, + "There should be " + TOTAL_SOURCES + " attachments stored in the sources container model.") + is(gSources.values.length, TOTAL_SOURCES, + "There should be " + TOTAL_SOURCES + " values stored in the sources container model.") + + info("Source labels: " + gSources.attachments.toSource()); + info("Source values: " + gSources.values.toSource()); + + is(gSources.attachments[0].label, "code_function-search-01.js", + "The first source label is correct."); + ok(gSources.attachments[0].source.url.contains("code_function-search-01.js"), + "The first source value appears to be correct."); + + is(gSources.attachments[1].label, "code_function-search-02.js", + "The second source label is correct."); + ok(gSources.attachments[1].source.url.contains("code_function-search-02.js"), + "The second source value appears to be correct."); + + is(gSources.attachments[2].label, "code_function-search-03.js", + "The third source label is correct."); + ok(gSources.attachments[2].source.url.contains("code_function-search-03.js"), + "The third source value appears to be correct."); + + is(gSources.attachments[3].label, "doc_function-search.html", + "The third source label is correct."); + ok(gSources.attachments[3].source.url.contains("doc_function-search.html"), + "The third source value appears to be correct."); + + is(gDebugger.SourceUtils._labelsCache.size, TOTAL_SOURCES, + "There should be " + TOTAL_SOURCES + " labels cached."); + is(gDebugger.SourceUtils._groupsCache.size, TOTAL_SOURCES, + "There should be " + TOTAL_SOURCES + " groups cached."); +} + +function getTextForSourcesAndCheckIntegrity() { + return gControllerSources.getTextForSources(gSources.values).then(testCacheIntegrity); +} + +function performReloadAndTestState() { + gDebugger.gTarget.once("will-navigate", testStateBeforeReload); + gDebugger.gTarget.once("navigate", testStateAfterReload); + return reloadActiveTab(gPanel, gDebugger.EVENTS.SOURCE_SHOWN); +} + +function testCacheIntegrity(aSources) { + for (let [actor, contents] of aSources) { + // Sources of a debugee don't always finish fetching consecutively. D'uh. + let index = gSources.values.indexOf(actor); + + ok(index >= 0 && index <= TOTAL_SOURCES, + "Found a source actor cached correctly (" + index + ")."); + ok(contents.contains( + ["First source!", "Second source!", "Third source!", "Peanut butter jelly time!"][index]), + "Found a source's text contents cached correctly (" + index + ")."); + + info("Cached source actor at " + index + ": " + actor); + info("Cached source text at " + index + ": " + contents); + } +} + +function testStateBeforeReload() { + is(gSources.itemCount, 0, + "There should be no sources present in the sources list during reload."); + is(gDebugger.SourceUtils._labelsCache, gPrevLabelsCache, + "The labels cache has been refreshed during reload and no new objects were created."); + is(gDebugger.SourceUtils._groupsCache, gPrevGroupsCache, + "The groups cache has been refreshed during reload and no new objects were created."); + is(gDebugger.SourceUtils._labelsCache.size, 0, + "There should be no labels cached during reload"); + is(gDebugger.SourceUtils._groupsCache.size, 0, + "There should be no groups cached during reload"); +} + +function testStateAfterReload() { + is(gSources.itemCount, TOTAL_SOURCES, + "There should be " + TOTAL_SOURCES + " sources present in the sources list."); + is(gDebugger.SourceUtils._labelsCache.size, TOTAL_SOURCES, + "There should be " + TOTAL_SOURCES + " labels cached after reload."); + is(gDebugger.SourceUtils._groupsCache.size, TOTAL_SOURCES, + "There should be " + TOTAL_SOURCES + " groups cached after reload."); +} + +registerCleanupFunction(function() { + gTab = null; + gDebuggee = null; + gPanel = null; + gDebugger = null; + gEditor = null; + gSources = null; + gControllerSources = null; + gPrevLabelsCache = null; + gPrevGroupsCache = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_sources-eval-01.js b/toolkit/devtools/debugger/test/browser_dbg_sources-eval-01.js new file mode 100644 index 000000000..c60ceb50f --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_sources-eval-01.js @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure eval scripts appear in the source list + */ + +const TAB_URL = EXAMPLE_URL + "doc_script-eval.html"; + +function test() { + let gTab, gPanel, gDebugger; + let gSources, gBreakpoints; + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gSources = gDebugger.DebuggerView.Sources; + gBreakpoints = gDebugger.DebuggerController.Breakpoints; + + return Task.spawn(function*() { + yield waitForSourceShown(gPanel, "-eval.js"); + is(gSources.values.length, 1, "Should have 1 source"); + + let newSource = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.NEW_SOURCE); + callInTab(gTab, "evalSource"); + yield newSource; + + is(gSources.values.length, 2, "Should have 2 sources"); + + let item = gSources.getItemForAttachment(e => e.label.indexOf("> eval") !== -1); + ok(item, "Source label is incorrect."); + is(item.attachment.group, gDebugger.L10N.getStr('evalGroupLabel'), + 'Source group is incorrect'); + + yield closeDebuggerAndFinish(gPanel); + }); + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_sources-eval-02.js b/toolkit/devtools/debugger/test/browser_dbg_sources-eval-02.js new file mode 100644 index 000000000..7d1ba7ebd --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_sources-eval-02.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure eval scripts with the sourceURL pragma are correctly + * displayed + */ + +const TAB_URL = EXAMPLE_URL + "doc_script-eval.html"; + +function test() { + let gTab, gPanel, gDebugger; + let gSources, gBreakpoints, gEditor; + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gSources = gDebugger.DebuggerView.Sources; + gBreakpoints = gDebugger.DebuggerController.Breakpoints; + gEditor = gDebugger.DebuggerView.editor; + + return Task.spawn(function*() { + yield waitForSourceShown(gPanel, "-eval.js"); + is(gSources.values.length, 1, "Should have 1 source"); + + let newSource = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.NEW_SOURCE); + callInTab(gTab, "evalSourceWithSourceURL"); + yield newSource; + + is(gSources.values.length, 2, "Should have 2 sources"); + + let item = gSources.getItemForAttachment(e => e.label == "bar.js"); + ok(item, "Source label is incorrect."); + is(item.attachment.group, 'http://example.com', + 'Source group is incorrect'); + + let shown = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN); + gSources.selectedItem = item; + yield shown; + + ok(gEditor.getText().indexOf('bar = function() {') === 0, + 'Correct source is shown'); + + yield closeDebuggerAndFinish(gPanel); + }); + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_sources-labels.js b/toolkit/devtools/debugger/test/browser_dbg_sources-labels.js new file mode 100644 index 000000000..31ff8e174 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_sources-labels.js @@ -0,0 +1,166 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that urls are correctly shortened to unique labels. + */ + +const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html"; + +function test() { + let gTab, gPanel, gDebugger; + let gSources, gUtils; + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gSources = gDebugger.DebuggerView.Sources; + gUtils = gDebugger.SourceUtils; + + let ellipsis = gPanel.panelWin.L10N.ellipsis; + let nananana = new Array(20).join(NaN); + + // Test trimming url queries. + + let someUrl = "a/b/c.d?test=1&random=4#reference"; + let shortenedUrl = "a/b/c.d"; + is(gUtils.trimUrlQuery(someUrl), shortenedUrl, + "Trimming the url query isn't done properly."); + + // Test trimming long urls with an ellipsis. + + let largeLabel = new Array(100).join("Beer can in Jamaican sounds like Bacon!"); + let trimmedLargeLabel = gUtils.trimUrlLength(largeLabel, 1234); + is(trimmedLargeLabel.length, 1235, + "Trimming large labels isn't done properly."); + ok(trimmedLargeLabel.endsWith(ellipsis), + "Trimming large labels should add an ellipsis at the end."); + + // Test the sources list behaviour with certain urls. + + let urls = [ + { href: "http://some.address.com/random/", leaf: "subrandom/" }, + { href: "http://some.address.com/random/", leaf: "suprandom/?a=1" }, + { href: "http://some.address.com/random/", leaf: "?a=1" }, + { href: "https://another.address.org/random/subrandom/", leaf: "page.html" }, + + { href: "ftp://interesting.address.org/random/", leaf: "script.js" }, + { href: "ftp://interesting.address.com/random/", leaf: "script.js" }, + { href: "ftp://interesting.address.com/random/", leaf: "x/script.js" }, + { href: "ftp://interesting.address.com/random/", leaf: "x/y/script.js?a=1" }, + { href: "ftp://interesting.address.com/random/x/", leaf: "y/script.js?a=1&b=2" }, + { href: "ftp://interesting.address.com/random/x/y/", leaf: "script.js?a=1&b=2&c=3" }, + { href: "ftp://interesting.address.com/random/", leaf: "x/y/script.js?a=2" }, + { href: "ftp://interesting.address.com/random/x/", leaf: "y/script.js?a=2&b=3" }, + { href: "ftp://interesting.address.com/random/x/y/", leaf: "script.js?a=2&b=3&c=4" }, + + { href: "file://random/", leaf: "script_t1.js&a=1&b=2&c=3" }, + { href: "file://random/", leaf: "script_t2_1.js#id" }, + { href: "file://random/", leaf: "script_t2_2.js?a" }, + { href: "file://random/", leaf: "script_t2_3.js&b" }, + { href: "resource://random/", leaf: "script_t3_1.js#id?a=1&b=2" }, + { href: "resource://random/", leaf: "script_t3_2.js?a=1&b=2#id" }, + { href: "resource://random/", leaf: "script_t3_3.js&a=1&b=2#id" }, + + { href: nananana, leaf: "Batman!" + "{trim me, now and forevermore}" } + ]; + + is(gSources.itemCount, 1, + "Should contain the original source label in the sources widget."); + is(gSources.selectedIndex, 0, + "The first item in the sources widget should be selected (1)."); + is(gSources.selectedItem.attachment.label, "doc_recursion-stack.html", + "The first item in the sources widget should be selected (2)."); + is(getSelectedSourceURL(gSources), TAB_URL, + "The first item in the sources widget should be selected (3)."); + + let id = 0; + for (let { href, leaf } of urls) { + let url = href + leaf; + let actor = 'actor' + id++; + let label = gUtils.trimUrlLength(gUtils.getSourceLabel(url)); + let group = gUtils.getSourceGroup(url); + let dummy = document.createElement("label"); + dummy.setAttribute('value', label); + + gSources.push([dummy, actor], { + attachment: { + source: { actor: actor, url: url }, + label: label, + group: group + } + }); + } + + info("Source locations:"); + info(gSources.values.toSource()); + + info("Source attachments:"); + info(gSources.attachments.toSource()); + + for (let { href, leaf, dupe } of urls) { + let url = href + leaf; + if (dupe) { + ok(!gSources.containsValue(getSourceActor(gSources, url)), "Shouldn't contain source: " + url); + } else { + ok(gSources.containsValue(getSourceActor(gSources, url)), "Should contain source: " + url); + } + } + + ok(gSources.getItemForAttachment(e => e.label == "random/subrandom/"), + "Source (0) label is incorrect."); + ok(gSources.getItemForAttachment(e => e.label == "random/suprandom/?a=1"), + "Source (1) label is incorrect."); + ok(gSources.getItemForAttachment(e => e.label == "random/?a=1"), + "Source (2) label is incorrect."); + ok(gSources.getItemForAttachment(e => e.label == "page.html"), + "Source (3) label is incorrect."); + + ok(gSources.getItemForAttachment(e => e.label == "script.js"), + "Source (4) label is incorrect."); + ok(gSources.getItemForAttachment(e => e.label == "random/script.js"), + "Source (5) label is incorrect."); + ok(gSources.getItemForAttachment(e => e.label == "random/x/script.js"), + "Source (6) label is incorrect."); + ok(gSources.getItemForAttachment(e => e.label == "script.js?a=1"), + "Source (7) label is incorrect."); + + ok(gSources.getItemForAttachment(e => e.label == "script_t1.js"), + "Source (8) label is incorrect."); + ok(gSources.getItemForAttachment(e => e.label == "script_t2_1.js"), + "Source (9) label is incorrect."); + ok(gSources.getItemForAttachment(e => e.label == "script_t2_2.js"), + "Source (10) label is incorrect."); + ok(gSources.getItemForAttachment(e => e.label == "script_t2_3.js"), + "Source (11) label is incorrect."); + ok(gSources.getItemForAttachment(e => e.label == "script_t3_1.js"), + "Source (12) label is incorrect."); + ok(gSources.getItemForAttachment(e => e.label == "script_t3_2.js"), + "Source (13) label is incorrect."); + ok(gSources.getItemForAttachment(e => e.label == "script_t3_3.js"), + "Source (14) label is incorrect."); + + ok(gSources.getItemForAttachment(e => e.label == nananana + "Batman!" + ellipsis), + "Source (15) label is incorrect."); + + is(gSources.itemCount, urls.filter(({ dupe }) => !dupe).length + 1, + "Didn't get the correct number of sources in the list."); + + is(gSources.getItemByValue(getSourceActor(gSources, "http://some.address.com/random/subrandom/")).attachment.label, + "random/subrandom/", + "gSources.getItemByValue isn't functioning properly (0)."); + is(gSources.getItemByValue(getSourceActor(gSources, "http://some.address.com/random/suprandom/?a=1")).attachment.label, + "random/suprandom/?a=1", + "gSources.getItemByValue isn't functioning properly (1)."); + + is(gSources.getItemForAttachment(e => e.label == "random/subrandom/").attachment.source.url, + "http://some.address.com/random/subrandom/", + "gSources.getItemForAttachment isn't functioning properly (0)."); + is(gSources.getItemForAttachment(e => e.label == "random/suprandom/?a=1").attachment.source.url, + "http://some.address.com/random/suprandom/?a=1", + "gSources.getItemForAttachment isn't functioning properly (1)."); + + closeDebuggerAndFinish(gPanel); + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_sources-sorting.js b/toolkit/devtools/debugger/test/browser_dbg_sources-sorting.js new file mode 100644 index 000000000..89ab6db51 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_sources-sorting.js @@ -0,0 +1,136 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that urls are correctly sorted when added to the sources widget. + */ + +const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html"; + +let gTab, gPanel, gDebugger; +let gSources, gUtils; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gSources = gDebugger.DebuggerView.Sources; + gUtils = gDebugger.SourceUtils; + + waitForSourceShown(gPanel, ".html") + .then(addSourceAndCheckOrder.bind(null, 1)) + .then(addSourceAndCheckOrder.bind(null, 2)) + .then(addSourceAndCheckOrder.bind(null, 3)) + .then(() => { closeDebuggerAndFinish(gPanel); }); + }); +} + +function addSourceAndCheckOrder(aMethod) { + gSources.empty(); + gSources.suppressSelectionEvents = true; + + let urls = [ + { href: "ici://some.address.com/random/", leaf: "subrandom/" }, + { href: "ni://another.address.org/random/subrandom/", leaf: "page.html" }, + { href: "san://interesting.address.gro/random/", leaf: "script.js" }, + { href: "si://interesting.address.moc/random/", leaf: "script.js" }, + { href: "si://interesting.address.moc/random/", leaf: "x/script.js" }, + { href: "si://interesting.address.moc/random/", leaf: "x/y/script.js?a=1" }, + { href: "si://interesting.address.moc/random/x/", leaf: "y/script.js?a=1&b=2" }, + { href: "si://interesting.address.moc/random/x/y/", leaf: "script.js?a=1&b=2&c=3" } + ]; + + urls.sort(function(a, b) { + return Math.random() - 0.5; + }); + + let id = 0; + + switch (aMethod) { + case 1: + for (let { href, leaf } of urls) { + let url = href + leaf; + let actor = 'actor' + id++; + let label = gUtils.getSourceLabel(url); + let dummy = document.createElement("label"); + gSources.push([dummy, actor], { + staged: true, + attachment: { + label: label + } + }); + } + gSources.commit({ sorted: true }); + break; + + case 2: + for (let { href, leaf } of urls) { + let url = href + leaf; + let actor = 'actor' + id++; + let label = gUtils.getSourceLabel(url); + let dummy = document.createElement("label"); + gSources.push([dummy, actor], { + staged: false, + attachment: { + label: label + } + }); + } + break; + + case 3: + let i = 0 + for (; i < urls.length / 2; i++) { + let { href, leaf } = urls[i]; + let url = href + leaf; + let actor = 'actor' + id++; + let label = gUtils.getSourceLabel(url); + let dummy = document.createElement("label"); + gSources.push([dummy, actor], { + staged: true, + attachment: { + label: label + } + }); + } + gSources.commit({ sorted: true }); + + for (; i < urls.length; i++) { + let { href, leaf } = urls[i]; + let url = href + leaf; + let actor = 'actor' + id++; + let label = gUtils.getSourceLabel(url); + let dummy = document.createElement("label"); + gSources.push([dummy, actor], { + staged: false, + attachment: { + label: label + } + }); + } + break; + } + + checkSourcesOrder(aMethod); +} + +function checkSourcesOrder(aMethod) { + let attachments = gSources.attachments; + + for (let i = 0; i < attachments.length - 1; i++) { + let first = attachments[i].label; + let second = attachments[i + 1].label; + ok(first < second, + "Using method " + aMethod + ", " + + "the sources weren't in the correct order: " + first + " vs. " + second); + } +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gSources = null; + gUtils = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_split-console-paused-reload.js b/toolkit/devtools/debugger/test/browser_dbg_split-console-paused-reload.js new file mode 100644 index 000000000..70037b94f --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_split-console-paused-reload.js @@ -0,0 +1,42 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Hitting ESC to open the split console when paused on reload should not stop + * the pending navigation. + */ + +function test() { + Task.spawn(runTests); +} + +function* runTests() { + let TAB_URL = EXAMPLE_URL + "doc_split-console-paused-reload.html"; + let [,, panel] = yield initDebugger(TAB_URL); + let dbgWin = panel.panelWin; + let sources = dbgWin.DebuggerView.Sources; + let frames = dbgWin.DebuggerView.StackFrames; + let toolbox = gDevTools.getToolbox(panel.target); + + yield panel.addBreakpoint({ actor: getSourceActor(sources, TAB_URL), line: 16 }); + info("Breakpoint was set."); + dbgWin.DebuggerController._target.activeTab.reload(); + info("Page reloaded."); + yield waitForSourceAndCaretAndScopes(panel, ".html", 16); + yield ensureThreadClientState(panel, "paused"); + info("Breakpoint was hit."); + EventUtils.sendMouseEvent({ type: "mousedown" }, + frames.selectedItem.target, + dbgWin); + info("The breadcrumb received focus."); + + // This is the meat of the test. + let result = toolbox.once("webconsole-ready", () => { + ok(toolbox.splitConsole, "Split console is shown."); + is(dbgWin.gThreadClient.state, "paused", "Execution is still paused."); + Services.prefs.clearUserPref("devtools.toolbox.splitconsoleEnabled"); + }); + EventUtils.synthesizeKey("VK_ESCAPE", {}, dbgWin); + yield result; + yield resumeDebuggerThenCloseAndFinish(panel); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_stack-01.js b/toolkit/devtools/debugger/test/browser_dbg_stack-01.js new file mode 100644 index 000000000..013c13d99 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_stack-01.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that stackframes are added when debugger is paused. + */ + +const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html"; + +let gTab, gPanel, gDebugger; +let gFrames, gClassicFrames; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gFrames = gDebugger.DebuggerView.StackFrames; + gClassicFrames = gDebugger.DebuggerView.StackFramesClassicList; + + waitForSourceAndCaretAndScopes(gPanel, ".html", 14).then(performTest); + callInTab(gTab, "simpleCall"); + }); +} + +function performTest() { + is(gDebugger.gThreadClient.state, "paused", + "Should only be getting stack frames while paused."); + is(gFrames.itemCount, 1, + "Should have only one frame."); + is(gClassicFrames.itemCount, 1, + "Should also have only one frame in the mirrored view."); + + resumeDebuggerThenCloseAndFinish(gPanel); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gFrames = null; + gClassicFrames = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_stack-02.js b/toolkit/devtools/debugger/test/browser_dbg_stack-02.js new file mode 100644 index 000000000..229f39139 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_stack-02.js @@ -0,0 +1,109 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that stackframes are added when debugger is paused in eval calls. + */ + +const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html"; + +let gTab, gPanel, gDebugger; +let gFrames, gClassicFrames; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gFrames = gDebugger.DebuggerView.StackFrames; + gClassicFrames = gDebugger.DebuggerView.StackFramesClassicList; + + waitForSourceAndCaretAndScopes(gPanel, ".html", 1).then(performTest); + callInTab(gTab, "evalCall"); + }); +} + +function performTest() { + is(gDebugger.gThreadClient.state, "paused", + "Should only be getting stack frames while paused."); + is(gFrames.itemCount, 2, + "Should have two frames."); + is(gClassicFrames.itemCount, 2, + "Should also have only two in the mirrored view."); + + is(gFrames.getItemAtIndex(0).attachment.title, + "evalCall", "Oldest frame name should be correct."); + is(gFrames.getItemAtIndex(0).attachment.url, + TAB_URL, "Oldest frame url should be correct."); + is(gClassicFrames.getItemAtIndex(0).attachment.depth, + 0, "Oldest frame name is mirrored correctly."); + + is(gFrames.getItemAtIndex(1).attachment.title, + "(eval)", "Newest frame name should be correct."); + is(gFrames.getItemAtIndex(1).attachment.url, + TAB_URL, "Newest frame url should be correct."); + is(gClassicFrames.getItemAtIndex(1).attachment.depth, + 1, "Newest frame name is mirrored correctly."); + + is(gFrames.selectedIndex, 1, + "Newest frame should be selected by default."); + is(gClassicFrames.selectedIndex, 0, + "Newest frame should be selected by default in the mirrored view."); + + isnot(gFrames.selectedIndex, 0, + "Oldest frame should not be selected."); + isnot(gClassicFrames.selectedIndex, 1, + "Oldest frame should not be selected in the mirrored view."); + + EventUtils.sendMouseEvent({ type: "mousedown" }, + gFrames.getItemAtIndex(0).target, + gDebugger); + + isnot(gFrames.selectedIndex, 1, + "Newest frame should not be selected after click."); + isnot(gClassicFrames.selectedIndex, 0, + "Newest frame in the mirrored view should not be selected."); + + is(gFrames.selectedIndex, 0, + "Oldest frame should be selected after click."); + is(gClassicFrames.selectedIndex, 1, + "Oldest frame in the mirrored view should be selected."); + + EventUtils.sendMouseEvent({ type: "mousedown" }, + gFrames.getItemAtIndex(1).target.querySelector(".dbg-stackframe-title"), + gDebugger); + + is(gFrames.selectedIndex, 1, + "Newest frame should be selected after click inside the newest frame."); + is(gClassicFrames.selectedIndex, 0, + "Newest frame in the mirrored view should be selected."); + + isnot(gFrames.selectedIndex, 0, + "Oldest frame should not be selected after click inside the newest frame."); + isnot(gClassicFrames.selectedIndex, 1, + "Oldest frame in the mirrored view should not be selected."); + + EventUtils.sendMouseEvent({ type: "mousedown" }, + gFrames.getItemAtIndex(0).target.querySelector(".dbg-stackframe-details"), + gDebugger); + + isnot(gFrames.selectedIndex, 1, + "Newest frame should not be selected after click inside the oldest frame."); + isnot(gClassicFrames.selectedIndex, 0, + "Newest frame in the mirrored view should not be selected."); + + is(gFrames.selectedIndex, 0, + "Oldest frame should be selected after click inside the oldest frame."); + is(gClassicFrames.selectedIndex, 1, + "Oldest frame in the mirrored view should be selected."); + + resumeDebuggerThenCloseAndFinish(gPanel); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gFrames = null; + gClassicFrames = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_stack-03.js b/toolkit/devtools/debugger/test/browser_dbg_stack-03.js new file mode 100644 index 000000000..37b0a411a --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_stack-03.js @@ -0,0 +1,73 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that stackframes are scrollable. + */ + +const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html"; + +let gTab, gDebuggee, gPanel, gDebugger; +let gFrames, gClassicFrames, gFramesScrollingInterval; + +function test() { + initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => { + gTab = aTab; + gDebuggee = aDebuggee; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gFrames = gDebugger.DebuggerView.StackFrames; + gClassicFrames = gDebugger.DebuggerView.StackFramesClassicList; + + + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.AFTER_FRAMES_REFILLED) + .then(performTest); + + gDebuggee.gRecurseLimit = (gDebugger.gCallStackPageSize * 2) + 1; + gDebuggee.recurse(); + }); +} + +function performTest() { + is(gDebugger.gThreadClient.state, "paused", + "Should only be getting stack frames while paused."); + is(gFrames.itemCount, gDebugger.gCallStackPageSize, + "Should have only the max limit of frames."); + is(gClassicFrames.itemCount, gDebugger.gCallStackPageSize, + "Should have only the max limit of frames in the mirrored view as well."); + + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.AFTER_FRAMES_REFILLED).then(() => { + is(gFrames.itemCount, gDebugger.gCallStackPageSize * 2, + "Should now have twice the max limit of frames."); + is(gClassicFrames.itemCount, gDebugger.gCallStackPageSize * 2, + "Should now have twice the max limit of frames in the mirrored view as well."); + + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.AFTER_FRAMES_REFILLED).then(() => { + is(gFrames.itemCount, gDebuggee.gRecurseLimit, + "Should have reached the recurse limit."); + is(gClassicFrames.itemCount, gDebuggee.gRecurseLimit, + "Should have reached the recurse limit in the mirrored view as well."); + + gDebugger.gThreadClient.resume(() => { + window.clearInterval(gFramesScrollingInterval); + closeDebuggerAndFinish(gPanel); + }); + }); + }); + + gFramesScrollingInterval = window.setInterval(() => { + gFrames.widget._list.scrollByIndex(-1); + }, 100); +} + +registerCleanupFunction(function() { + window.clearInterval(gFramesScrollingInterval); + gFramesScrollingInterval = null; + + gTab = null; + gDebuggee = null; + gPanel = null; + gDebugger = null; + gFrames = null; + gClassicFrames = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_stack-04.js b/toolkit/devtools/debugger/test/browser_dbg_stack-04.js new file mode 100644 index 000000000..dfcaab355 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_stack-04.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that stackframes are cleared after resume. + */ + +const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html"; + +let gTab, gPanel, gDebugger; +let gFrames, gClassicFrames; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gFrames = gDebugger.DebuggerView.StackFrames; + gClassicFrames = gDebugger.DebuggerView.StackFramesClassicList; + + waitForSourceAndCaretAndScopes(gPanel, ".html", 1).then(performTest); + callInTab(gTab, "evalCall"); + }); +} + +function performTest() { + is(gDebugger.gThreadClient.state, "paused", + "Should only be getting stack frames while paused."); + is(gFrames.itemCount, 2, + "Should have two frames."); + is(gClassicFrames.itemCount, 2, + "Should also have two frames in the mirrored view."); + + gDebugger.once(gDebugger.EVENTS.AFTER_FRAMES_CLEARED, () => { + is(gFrames.itemCount, 0, + "Should have no frames after resume."); + is(gClassicFrames.itemCount, 0, + "Should also have no frames in the mirrored view after resume."); + + closeDebuggerAndFinish(gPanel); + }, true); + + gDebugger.gThreadClient.resume(); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gFrames = null; + gClassicFrames = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_stack-05.js b/toolkit/devtools/debugger/test/browser_dbg_stack-05.js new file mode 100644 index 000000000..45f61bc18 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_stack-05.js @@ -0,0 +1,129 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that switching between stack frames properly sets the current debugger + * location in the source editor. + */ + +const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html"; + +let gTab, gPanel, gDebugger; +let gEditor, gSources, gFrames, gClassicFrames; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gSources = gDebugger.DebuggerView.Sources; + gFrames = gDebugger.DebuggerView.StackFrames; + gClassicFrames = gDebugger.DebuggerView.StackFramesClassicList; + + waitForSourceAndCaretAndScopes(gPanel, "-02.js", 1) + .then(initialChecks) + .then(testNewestFrame) + .then(testOldestFrame) + .then(testAfterResume) + .then(() => closeDebuggerAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + + callInTab(gTab, "firstCall"); + }); +} + +function initialChecks() { + is(gDebugger.gThreadClient.state, "paused", + "Should only be getting stack frames while paused."); + is(gFrames.itemCount, 2, + "Should have four frames."); + is(gClassicFrames.itemCount, 2, + "Should also have four frames in the mirrored view."); +} + +function testNewestFrame() { + let deferred = promise.defer(); + + is(gFrames.selectedIndex, 1, + "Newest frame should be selected by default."); + is(gClassicFrames.selectedIndex, 0, + "Newest frame should be selected in the mirrored view as well."); + is(gSources.selectedIndex, 1, + "The second source is selected in the widget."); + ok(isCaretPos(gPanel, 1), + "Editor caret location is correct (1)."); + + // The editor's debug location takes a tick to update. + executeSoon(() => { + is(gEditor.getDebugLocation(), 5, + "Editor debug location is correct."); + + deferred.resolve(); + }); + + return deferred.promise; +} + +function testOldestFrame() { + let deferred = promise.defer(); + + waitForSourceAndCaret(gPanel, "-01.js", 1).then(waitForTick).then(() => { + is(gFrames.selectedIndex, 0, + "Second frame should be selected after click."); + is(gClassicFrames.selectedIndex, 1, + "Second frame should be selected in the mirrored view as well."); + is(gSources.selectedIndex, 0, + "The first source is now selected in the widget."); + ok(isCaretPos(gPanel, 5), + "Editor caret location is correct (3)."); + + // The editor's debug location takes a tick to update. + executeSoon(() => { + is(gEditor.getDebugLocation(), 4, + "Editor debug location is correct."); + + deferred.resolve(); + }); + }); + + EventUtils.sendMouseEvent({ type: "mousedown" }, + gDebugger.document.querySelector("#stackframe-1"), + gDebugger); + + return deferred.promise; +} + +function testAfterResume() { + let deferred = promise.defer(); + + gDebugger.once(gDebugger.EVENTS.AFTER_FRAMES_CLEARED, () => { + is(gFrames.itemCount, 0, + "Should have no frames after resume."); + is(gClassicFrames.itemCount, 0, + "Should have no frames in the mirrored view as well."); + ok(isCaretPos(gPanel, 5), + "Editor caret location is correct after resume."); + is(gEditor.getDebugLocation(), null, + "Editor debug location is correct after resume."); + + deferred.resolve(); + }, true); + + gDebugger.gThreadClient.resume(); + + return deferred.promise; +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gEditor = null; + gSources = null; + gFrames = null; + gClassicFrames = null; +}); + diff --git a/toolkit/devtools/debugger/test/browser_dbg_stack-06.js b/toolkit/devtools/debugger/test/browser_dbg_stack-06.js new file mode 100644 index 000000000..6bf4ca7d0 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_stack-06.js @@ -0,0 +1,86 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that selecting a stack frame loads the right source in the editor + * pane and highlights the proper line. + */ + +const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html"; + +let gTab, gPanel, gDebugger; +let gEditor, gSources, gFrames, gClassicFrames; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gSources = gDebugger.DebuggerView.Sources; + gFrames = gDebugger.DebuggerView.StackFrames; + gClassicFrames = gDebugger.DebuggerView.StackFramesClassicList; + + waitForSourceAndCaretAndScopes(gPanel, "-02.js", 1).then(performTest); + callInTab(gTab, "firstCall"); + }); +} + +function performTest() { + is(gFrames.selectedIndex, 1, + "Newest frame should be selected by default."); + is(gClassicFrames.selectedIndex, 0, + "Newest frame should also be selected in the mirrored view."); + is(gSources.selectedIndex, 1, + "The second source is selected in the widget."); + is(gEditor.getText().search(/firstCall/), -1, + "The first source is not displayed."); + is(gEditor.getText().search(/debugger/), 166, + "The second source is displayed."); + + waitForSourceAndCaret(gPanel, "-01.js", 1).then(waitForTick).then(() => { + is(gFrames.selectedIndex, 0, + "Oldest frame should be selected after click."); + is(gClassicFrames.selectedIndex, 1, + "Oldest frame should also be selected in the mirrored view."); + is(gSources.selectedIndex, 0, + "The first source is now selected in the widget."); + is(gEditor.getText().search(/firstCall/), 118, + "The first source is displayed."); + is(gEditor.getText().search(/debugger/), -1, + "The second source is not displayed."); + + waitForSourceAndCaret(gPanel, "-02.js", 1).then(waitForTick).then(() => { + is(gFrames.selectedIndex, 1, + "Newest frame should be selected again after click."); + is(gClassicFrames.selectedIndex, 0, + "Newest frame should also be selected again in the mirrored view."); + is(gSources.selectedIndex, 1, + "The second source is selected in the widget."); + is(gEditor.getText().search(/firstCall/), -1, + "The first source is not displayed."); + is(gEditor.getText().search(/debugger/), 166, + "The second source is displayed."); + + resumeDebuggerThenCloseAndFinish(gPanel); + }); + + EventUtils.sendMouseEvent({ type: "mousedown" }, + gDebugger.document.querySelector("#classic-stackframe-0"), + gDebugger); + }); + + EventUtils.sendMouseEvent({ type: "mousedown" }, + gDebugger.document.querySelector("#stackframe-1"), + gDebugger); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gEditor = null; + gSources = null; + gFrames = null; + gClassicFrames = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_stack-07.js b/toolkit/devtools/debugger/test/browser_dbg_stack-07.js new file mode 100644 index 000000000..d17f958c5 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_stack-07.js @@ -0,0 +1,107 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that after selecting a different stack frame, resuming reselects + * the topmost stackframe, loads the right source in the editor pane and + * highlights the proper line. + */ + +const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html"; + +let gTab, gPanel, gDebugger; +let gEditor, gSources, gFrames, gClassicFrames, gToolbar; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gSources = gDebugger.DebuggerView.Sources; + gFrames = gDebugger.DebuggerView.StackFrames; + gClassicFrames = gDebugger.DebuggerView.StackFramesClassicList; + gToolbar = gDebugger.DebuggerView.Toolbar; + + waitForSourceAndCaretAndScopes(gPanel, "-02.js", 1).then(performTest); + callInTab(gTab, "firstCall"); + }); +} + +function performTest() { + return Task.spawn(function() { + yield selectBottomFrame(); + testBottomFrame(4); + + yield performStep("StepOver"); + testTopFrame(1); + + yield selectBottomFrame(); + testBottomFrame(4); + + yield performStep("StepIn"); + testTopFrame(1); + + yield selectBottomFrame(); + testBottomFrame(4); + + yield performStep("StepOut"); + testTopFrame(1); + + yield resumeDebuggerThenCloseAndFinish(gPanel); + }); + + function selectBottomFrame() { + let updated = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES); + gClassicFrames.selectedIndex = gClassicFrames.itemCount - 1; + return updated.then(waitForTick); + } + + function testBottomFrame(debugLocation) { + is(gFrames.selectedIndex, 0, + "Oldest frame should be selected after click."); + is(gClassicFrames.selectedIndex, gFrames.itemCount - 1, + "Oldest frame should also be selected in the mirrored view."); + is(gSources.selectedIndex, 0, + "The first source is now selected in the widget."); + is(gEditor.getText().search(/firstCall/), 118, + "The first source is displayed."); + is(gEditor.getText().search(/debugger/), -1, + "The second source is not displayed."); + + is(gEditor.getDebugLocation(), debugLocation, + "Editor debugger location is correct."); + ok(gEditor.hasLineClass(debugLocation, "debug-line"), + "The debugged line is highlighted appropriately."); + } + + function performStep(type) { + let updated = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES); + gToolbar["_on" + type + "Pressed"](); + return updated.then(waitForTick); + } + + function testTopFrame(frameIndex) { + is(gFrames.selectedIndex, frameIndex, + "Topmost frame should be selected after click."); + is(gClassicFrames.selectedIndex, gFrames.itemCount - frameIndex - 1, + "Topmost frame should also be selected in the mirrored view."); + is(gSources.selectedIndex, 1, + "The second source is now selected in the widget."); + is(gEditor.getText().search(/firstCall/), -1, + "The second source is displayed."); + is(gEditor.getText().search(/debugger/), 166, + "The first source is not displayed."); + } +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gEditor = null; + gSources = null; + gFrames = null; + gClassicFrames = null; + gToolbar = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_step-out.js b/toolkit/devtools/debugger/test/browser_dbg_step-out.js new file mode 100644 index 000000000..1d376caf0 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_step-out.js @@ -0,0 +1,79 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that stepping out of a function displays the right return value. + */ + +const TAB_URL = EXAMPLE_URL + "doc_step-out.html"; + +let gTab, gPanel, gDebugger; +let gVars; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gVars = gDebugger.DebuggerView.Variables; + + testNormalReturn(); + }); +} + +function testNormalReturn() { + waitForSourceAndCaretAndScopes(gPanel, ".html", 17).then(() => { + waitForCaretAndScopes(gPanel, 19).then(() => { + let innerScope = gVars.getScopeAtIndex(0); + let returnVar = innerScope.get("<return>"); + + is(returnVar.name, "<return>", + "Should have the right property name for the returned value."); + is(returnVar.value, 10, + "Should have the right property value for the returned value."); + + resumeDebuggee().then(() => testReturnWithException()); + }); + + EventUtils.sendMouseEvent({ type: "mousedown" }, + gDebugger.document.getElementById("step-out"), + gDebugger); + }); + + sendMouseClickToTab(gTab, content.document.getElementById("return")); +} + +function testReturnWithException() { + waitForCaretAndScopes(gPanel, 24).then(() => { + waitForCaretAndScopes(gPanel, 27).then(() => { + let innerScope = gVars.getScopeAtIndex(0); + let exceptionVar = innerScope.get("<exception>"); + + is(exceptionVar.name, "<exception>", + "Should have the right property name for the returned value."); + is(exceptionVar.value, "boom", + "Should have the right property value for the returned value."); + + resumeDebuggee().then(() => closeDebuggerAndFinish(gPanel)); + }); + + EventUtils.sendMouseEvent({ type: "mousedown" }, + gDebugger.document.getElementById("step-out"), + gDebugger); + }); + + sendMouseClickToTab(gTab, content.document.getElementById("throw")); +} + +function resumeDebuggee() { + let deferred = promise.defer(); + gDebugger.gThreadClient.resume(deferred.resolve); + return deferred.promise; +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gVars = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_tabactor-01.js b/toolkit/devtools/debugger/test/browser_dbg_tabactor-01.js new file mode 100644 index 000000000..8e0b92d8f --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_tabactor-01.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check extension-added tab actor lifetimes. + */ + +const CHROME_URL = "chrome://mochitests/content/browser/browser/devtools/debugger/test/" +const ACTORS_URL = CHROME_URL + "testactors.js"; +const TAB_URL = EXAMPLE_URL + "doc_empty-tab-01.html"; + +let gClient; + +function test() { + if (!DebuggerServer.initialized) { + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + } + + DebuggerServer.addActors(ACTORS_URL); + + let transport = DebuggerServer.connectPipe(); + gClient = new DebuggerClient(transport); + gClient.connect((aType, aTraits) => { + is(aType, "browser", + "Root actor should identify itself as a browser."); + + addTab(TAB_URL) + .then(() => attachTabActorForUrl(gClient, TAB_URL)) + .then(testTabActor) + .then(closeTab) + .then(closeConnection) + .then(finish) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); +} + +function testTabActor([aGrip, aResponse]) { + let deferred = promise.defer(); + + ok(aGrip.testTabActor1, + "Found the test tab actor."); + ok(aGrip.testTabActor1.contains("test_one"), + "testTabActor1's actorPrefix should be used."); + + gClient.request({ to: aGrip.testTabActor1, type: "ping" }, aResponse => { + is(aResponse.pong, "pong", + "Actor should respond to requests."); + + deferred.resolve(); + }); + + return deferred.promise; +} + +function closeTab() { + return removeTab(gBrowser.selectedTab); +} + +function closeConnection() { + let deferred = promise.defer(); + gClient.close(deferred.resolve); + return deferred.promise; +} + +registerCleanupFunction(function() { + gClient = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_tabactor-02.js b/toolkit/devtools/debugger/test/browser_dbg_tabactor-02.js new file mode 100644 index 000000000..c27b39bbd --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_tabactor-02.js @@ -0,0 +1,84 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check extension-added tab actor lifetimes. + */ + +const CHROME_URL = "chrome://mochitests/content/browser/browser/devtools/debugger/test/" +const ACTORS_URL = CHROME_URL + "testactors.js"; +const TAB_URL = EXAMPLE_URL + "doc_empty-tab-01.html"; + +let gClient; + +function test() { + if (!DebuggerServer.initialized) { + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + } + + DebuggerServer.addActors(ACTORS_URL); + + let transport = DebuggerServer.connectPipe(); + gClient = new DebuggerClient(transport); + gClient.connect((aType, aTraits) => { + is(aType, "browser", + "Root actor should identify itself as a browser."); + + addTab(TAB_URL) + .then(() => attachTabActorForUrl(gClient, TAB_URL)) + .then(testTabActor) + .then(closeTab) + .then(closeConnection) + .then(finish) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); +} + +function testTabActor([aGrip, aResponse]) { + let deferred = promise.defer(); + + ok(aGrip.testTabActor1, + "Found the test tab actor."); + ok(aGrip.testTabActor1.contains("test_one"), + "testTabActor1's actorPrefix should be used."); + + gClient.request({ to: aGrip.testTabActor1, type: "ping" }, aResponse => { + is(aResponse.pong, "pong", + "Actor should respond to requests."); + + deferred.resolve(aResponse.actor); + }); + + return deferred.promise; +} + +function closeTab(aTestActor) { + return removeTab(gBrowser.selectedTab).then(() => { + let deferred = promise.defer(); + + try { + gClient.request({ to: aTestActor, type: "ping" }, aResponse => { + ok(false, "testTabActor1 didn't go away with the tab."); + deferred.reject(aResponse); + }); + } catch(e) { + is(e.message, "'ping' request packet has no destination.", "testTabActor1 went away."); + deferred.resolve(); + } + + return deferred.promise; + }); +} + +function closeConnection() { + let deferred = promise.defer(); + gClient.close(deferred.resolve); + return deferred.promise; +} + +registerCleanupFunction(function() { + gClient = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_terminate-on-tab-close.js b/toolkit/devtools/debugger/test/browser_dbg_terminate-on-tab-close.js new file mode 100644 index 000000000..6b06f9db6 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_terminate-on-tab-close.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that debuggee scripts are terminated on tab closure. + */ + +const TAB_URL = EXAMPLE_URL + "doc_terminate-on-tab-close.html"; + +let gTab, gDebugger, gPanel; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + + testTerminate(); + }); +} + +function testTerminate() { + gDebugger.gThreadClient.addOneTimeListener("paused", () => { + resumeDebuggerThenCloseAndFinish(gPanel).then(function () { + ok(true, "should not throw after this point"); + }); + }); + + callInTab(gTab, "debuggerThenThrow"); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_tracing-01.js b/toolkit/devtools/debugger/test/browser_dbg_tracing-01.js new file mode 100644 index 000000000..116173621 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_tracing-01.js @@ -0,0 +1,105 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we get the expected frame enter/exit logs in the tracer view. + */ + +const TAB_URL = EXAMPLE_URL + "doc_tracing-01.html"; + +let gTab, gPanel, gDebugger; + +function test() { + SpecialPowers.pushPrefEnv({'set': [["devtools.debugger.tracer", true]]}, () => { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + + waitForSourceShown(gPanel, "code_tracing-01.js") + .then(() => startTracing(gPanel)) + .then(clickButton) + .then(() => waitForClientEvents(aPanel, "traces")) + .then(testTraceLogs) + .then(() => stopTracing(gPanel)) + .then(() => { + const deferred = promise.defer(); + SpecialPowers.popPrefEnv(deferred.resolve); + return deferred.promise; + }) + .then(() => closeDebuggerAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); + }); +} + +function clickButton() { + sendMouseClickToTab(gTab, content.document.querySelector("button")); +} + +function testTraceLogs() { + const onclickLogs = filterTraces(gPanel, + t => t.querySelector(".trace-name[value=onclick]")); + is(onclickLogs.length, 2, "Should have two logs from 'onclick'"); + ok(onclickLogs[0].querySelector(".trace-call"), + "The first 'onclick' log should be a call."); + ok(onclickLogs[1].querySelector(".trace-return"), + "The second 'onclick' log should be a return."); + for (let t of onclickLogs) { + ok(t.querySelector(".trace-item").getAttribute("tooltiptext") + .contains("doc_tracing-01.html")); + } + + const nonOnclickLogs = filterTraces(gPanel, + t => !t.querySelector(".trace-name[value=onclick]")); + for (let t of nonOnclickLogs) { + ok(t.querySelector(".trace-item").getAttribute("tooltiptext") + .contains("code_tracing-01.js")); + } + + const mainLogs = filterTraces(gPanel, + t => t.querySelector(".trace-name[value=main]")); + is(mainLogs.length, 2, "Should have an enter and an exit for 'main'"); + ok(mainLogs[0].querySelector(".trace-call"), + "The first 'main' log should be a call."); + ok(mainLogs[1].querySelector(".trace-return"), + "The second 'main' log should be a return."); + + const factorialLogs = filterTraces(gPanel, + t => t.querySelector(".trace-name[value=factorial]")); + is(factorialLogs.length, 10, "Should have 5 enter, and 5 exit frames for 'factorial'"); + ok(factorialLogs.slice(0, 5).every(t => t.querySelector(".trace-call")), + "The first five 'factorial' logs should be calls."); + ok(factorialLogs.slice(5).every(t => t.querySelector(".trace-return")), + "The second five 'factorial' logs should be returns.") + + // Test that the depth affects padding so that calls are indented properly. + let lastDepth = -Infinity; + for (let t of factorialLogs.slice(0, 5)) { + let depth = parseInt(t.querySelector(".trace-item").style.MozPaddingStart, 10); + ok(depth > lastDepth, "The depth should be increasing"); + lastDepth = depth; + } + lastDepth = Infinity; + for (let t of factorialLogs.slice(5)) { + let depth = parseInt(t.querySelector(".trace-item").style.MozPaddingStart, 10); + ok(depth < lastDepth, "The depth should be decreasing"); + lastDepth = depth; + } + + const throwerLogs = filterTraces(gPanel, + t => t.querySelector(".trace-name[value=thrower]")); + is(throwerLogs.length, 2, "Should have an enter and an exit for 'thrower'"); + ok(throwerLogs[0].querySelector(".trace-call"), + "The first 'thrower' log should be a call."); + ok(throwerLogs[1].querySelector(".trace-throw", + "The second 'thrower' log should be a throw.")); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_tracing-02.js b/toolkit/devtools/debugger/test/browser_dbg_tracing-02.js new file mode 100644 index 000000000..eb55db161 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_tracing-02.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we highlight matching calls and returns on hover. + */ + +const TAB_URL = EXAMPLE_URL + "doc_tracing-01.html"; + +let gTab, gPanel, gDebugger; + +function test() { + SpecialPowers.pushPrefEnv({'set': [["devtools.debugger.tracer", true]]}, () => { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + + waitForSourceShown(gPanel, "code_tracing-01.js") + .then(() => startTracing(gPanel)) + .then(clickButton) + .then(() => waitForClientEvents(aPanel, "traces")) + .then(highlightCall) + .then(testReturnHighlighted) + .then(unhighlightCall) + .then(testNoneHighlighted) + .then(() => stopTracing(gPanel)) + .then(() => { + const deferred = promise.defer(); + SpecialPowers.popPrefEnv(deferred.resolve); + return deferred.promise; + }) + .then(() => closeDebuggerAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); + }); +} + +function clickButton() { + sendMouseClickToTab(gTab, content.document.querySelector("button")); +} + +function highlightCall() { + const callTrace = filterTraces(gPanel, t => t.querySelector(".trace-name[value=main]"))[0]; + EventUtils.sendMouseEvent({ type: "mouseover" }, + callTrace, + gDebugger); +} + +function testReturnHighlighted() { + const returnTrace = filterTraces(gPanel, t => t.querySelector(".trace-name[value=main]"))[1]; + ok(Array.indexOf(returnTrace.querySelector(".trace-item").classList, "selected-matching") >= 0, + "The corresponding return log should be highlighted."); +} + +function unhighlightCall() { + const callTrace = filterTraces(gPanel, t => t.querySelector(".trace-name[value=main]"))[0]; + EventUtils.sendMouseEvent({ type: "mouseout" }, + callTrace, + gDebugger); +} + +function testNoneHighlighted() { + const highlightedTraces = filterTraces(gPanel, t => t.querySelector(".selected-matching")); + is(highlightedTraces.length, 0, "Shouldn't have any highlighted traces"); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_tracing-03.js b/toolkit/devtools/debugger/test/browser_dbg_tracing-03.js new file mode 100644 index 000000000..e8bcbe8f9 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_tracing-03.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +SimpleTest.requestCompleteLog(); + +/** + * Test that we can jump to function definitions by clicking on logs. + */ + +const TAB_URL = EXAMPLE_URL + "doc_tracing-01.html"; + +let gTab, gPanel, gDebugger, gSources; + +function test() { + SpecialPowers.pushPrefEnv({'set': [["devtools.debugger.tracer", true]]}, () => { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gSources = gDebugger.DebuggerView.Sources; + + waitForSourceShown(gPanel, "code_tracing-01.js") + .then(() => startTracing(gPanel)) + .then(() => clickButton()) + .then(() => waitForClientEvents(aPanel, "traces")) + .then(() => { + // Switch away from the JS file so we can make sure that clicking on a + // log will switch us back to the correct JS file. + gSources.selectedValue = getSourceActor(gSources, TAB_URL); + return ensureSourceIs(aPanel, getSourceActor(gSources, TAB_URL), true); + }) + .then(() => { + const finished = waitForSourceShown(gPanel, "code_tracing-01.js"); + clickTraceLog(); + return finished; + }) + .then(testCorrectLine) + .then(() => stopTracing(gPanel)) + .then(() => { + const deferred = promise.defer(); + SpecialPowers.popPrefEnv(deferred.resolve); + return deferred.promise; + }) + .then(() => closeDebuggerAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); + }); +} + +function clickButton() { + sendMouseClickToTab(gTab, content.document.querySelector("button")); +} + +function clickTraceLog() { + filterTraces(gPanel, t => t.querySelector(".trace-name[value=main]"))[0].click(); +} + +function testCorrectLine() { + is(gDebugger.DebuggerView.editor.getCursor().line, 18, + "The editor should have the function definition site's line selected."); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gSources = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_tracing-04.js b/toolkit/devtools/debugger/test/browser_dbg_tracing-04.js new file mode 100644 index 000000000..c5976c6cb --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_tracing-04.js @@ -0,0 +1,80 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that when we click on logs, we get the parameters/return value in the variables view. + */ + +const TAB_URL = EXAMPLE_URL + "doc_tracing-01.html"; + +let gTab, gPanel, gDebugger; + +function test() { + SpecialPowers.pushPrefEnv({'set': [["devtools.debugger.tracer", true]]}, () => { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + + waitForSourceShown(gPanel, "code_tracing-01.js") + .then(() => startTracing(gPanel)) + .then(clickButton) + .then(() => waitForClientEvents(aPanel, "traces")) + .then(clickTraceCall) + .then(testParams) + .then(clickTraceReturn) + .then(testReturn) + .then(() => stopTracing(gPanel)) + .then(() => { + const deferred = promise.defer(); + SpecialPowers.popPrefEnv(deferred.resolve); + return deferred.promise; + }) + .then(() => closeDebuggerAndFinish(gPanel)) + .then(null, aError => { + DevToolsUtils.reportException("browser_dbg_tracing-04.js", aError); + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); + }); +} + +function clickButton() { + sendMouseClickToTab(gTab, content.document.querySelector("button")); +} + +function clickTraceCall() { + filterTraces(gPanel, t => t.querySelector(".trace-name[value=factorial]"))[0] + .click(); +} + +function testParams() { + const name = gDebugger.document.querySelector(".variables-view-variable .name"); + ok(name, "Should have a variable name"); + is(name.getAttribute("value"), "n", "The variable name should be n"); + + const value = gDebugger.document.querySelector(".variables-view-variable .value.token-number"); + ok(value, "Should have a variable value"); + is(value.getAttribute("value"), "5", "The variable value should be 5"); +} + +function clickTraceReturn() { + filterTraces(gPanel, t => t.querySelector(".trace-name[value=factorial]")) + .pop().click(); +} + +function testReturn() { + const name = gDebugger.document.querySelector(".variables-view-variable .name"); + ok(name, "Should have a variable name"); + is(name.getAttribute("value"), "<return>", "The variable name should be <return>"); + + const value = gDebugger.document.querySelector(".variables-view-variable .value.token-number"); + ok(value, "Should have a variable value"); + is(value.getAttribute("value"), "120", "The variable value should be 120"); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_tracing-05.js b/toolkit/devtools/debugger/test/browser_dbg_tracing-05.js new file mode 100644 index 000000000..a51cc0ae1 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_tracing-05.js @@ -0,0 +1,81 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that text describing the tracing state is correctly displayed. + */ + +const TAB_URL = EXAMPLE_URL + "doc_tracing-01.html"; + +let gTab, gPanel, gDebugger; +let gTracer, gL10N; + +function test() { + SpecialPowers.pushPrefEnv({'set': [["devtools.debugger.tracer", true]]}, () => { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gTracer = gDebugger.DebuggerView.Tracer; + gL10N = gDebugger.L10N; + + waitForSourceShown(gPanel, "code_tracing-01.js") + .then(testTracingNotStartedText) + .then(() => gTracer._onStartTracing()) + .then(testFunctionCallsUnavailableText) + .then(clickButton) + .then(() => waitForClientEvents(aPanel, "traces")) + .then(testNoEmptyText) + .then(() => gTracer._onClear()) + .then(testFunctionCallsUnavailableText) + .then(() => gTracer._onStopTracing()) + .then(testTracingNotStartedText) + .then(() => gTracer._onClear()) + .then(testTracingNotStartedText) + .then(() => { + const deferred = promise.defer(); + SpecialPowers.popPrefEnv(deferred.resolve); + return deferred.promise; + }) + .then(() => closeDebuggerAndFinish(gPanel)) + .then(null, aError => { + DevToolsUtils.reportException("browser_dbg_tracing-05.js", aError); + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); + }); +} + +function testTracingNotStartedText() { + let label = gDebugger.document.querySelector("#tracer-tabpanel .fast-list-widget-empty-text"); + ok(label, + "A label is displayed in the tracer tabpanel."); + is(label.getAttribute("value"), gL10N.getStr("tracingNotStartedText"), + "The correct {{tracingNotStartedText}} is displayed in the tracer tabpanel."); +} + +function testFunctionCallsUnavailableText() { + let label = gDebugger.document.querySelector("#tracer-tabpanel .fast-list-widget-empty-text"); + ok(label, + "A label is displayed in the tracer tabpanel."); + is(label.getAttribute("value"), gL10N.getStr("noFunctionCallsText"), + "The correct {{noFunctionCallsText}} is displayed in the tracer tabpanel."); +} + +function testNoEmptyText() { + let label = gDebugger.document.querySelector("#tracer-tabpanel .fast-list-widget-empty-text"); + ok(!label, + "No label should be displayed in the tracer tabpanel."); +} + +function clickButton() { + sendMouseClickToTab(gTab, content.document.querySelector("button")); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gTracer = null; + gL10N = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_tracing-06.js b/toolkit/devtools/debugger/test/browser_dbg_tracing-06.js new file mode 100644 index 000000000..f1f836ef3 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_tracing-06.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that the tracer doesn't connect to the backend when tracing is disabled. + */ + +const TAB_URL = EXAMPLE_URL + "doc_tracing-01.html"; +const TRACER_PREF = "devtools.debugger.tracer"; + +let gTab, gPanel, gDebugger; +let gOriginalPref = Services.prefs.getBoolPref(TRACER_PREF); +Services.prefs.setBoolPref(TRACER_PREF, false); + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + + waitForSourceShown(gPanel, "code_tracing-01.js") + .then(() => { + ok(!gDebugger.DebuggerController.traceClient, "Should not have a trace client"); + closeDebuggerAndFinish(gPanel); + }) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + Services.prefs.setBoolPref(TRACER_PREF, gOriginalPref); +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_tracing-07.js b/toolkit/devtools/debugger/test/browser_dbg_tracing-07.js new file mode 100644 index 000000000..4aaba2c41 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_tracing-07.js @@ -0,0 +1,83 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Execute code both before and after blackboxing and test that we get + * appropriately styled traces. + */ + +const TAB_URL = EXAMPLE_URL + "doc_tracing-01.html"; + +let gTab, gPanel; + +function test() { + Task.async(function*() { + yield pushPref(); + + [gTab,, gPanel] = yield initDebugger(TAB_URL); + + yield startTracing(gPanel); + yield clickButton(); + yield waitForClientEvents(gPanel, "traces"); + + /** + * Test that there are some traces which are not blackboxed. + */ + const firstBbButton = getBlackBoxButton(gPanel); + ok(!firstBbButton.checked, "Should not be black boxed by default"); + + const blackBoxedTraces = + gPanel.panelWin.document.querySelectorAll(".trace-item.black-boxed"); + ok(blackBoxedTraces.length === 0, "There should no blackboxed traces."); + + const notBlackBoxedTraces = + gPanel.panelWin.document.querySelectorAll(".trace-item:not(.black-boxed)"); + ok(notBlackBoxedTraces.length > 0, + "There should be some traces which are not blackboxed."); + + yield toggleBlackBoxing(gPanel); + yield clickButton(); + yield waitForClientEvents(gPanel, "traces"); + + /** + * Test that there are some traces which are blackboxed. + */ + const secondBbButton = getBlackBoxButton(gPanel); + ok(secondBbButton.checked, "The checkbox should no longer be checked."); + const traces = + gPanel.panelWin.document.querySelectorAll(".trace-item.black-boxed"); + ok(traces.length > 0, "There should be some blackboxed traces."); + + yield stopTracing(gPanel); + yield popPref(); + yield closeDebuggerAndFinish(gPanel); + + finish(); + })().catch(e => { + ok(false, "Got an error: " + e.message + "\n" + e.stack); + finish(); + }); +} + +function clickButton() { + sendMouseClickToTab(gTab, content.document.querySelector("button")); +} + +function pushPref() { + let deferred = promise.defer(); + SpecialPowers.pushPrefEnv({'set': [["devtools.debugger.tracer", true]]}, + deferred.resolve); + return deferred.promise; +} + +function popPref() { + let deferred = promise.defer(); + SpecialPowers.popPrefEnv(deferred.resolve); + return deferred.promise; +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; +}); + diff --git a/toolkit/devtools/debugger/test/browser_dbg_tracing-08.js b/toolkit/devtools/debugger/test/browser_dbg_tracing-08.js new file mode 100644 index 000000000..eb20ffa9f --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_tracing-08.js @@ -0,0 +1,59 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that tracing about:config doesn't produce errors. + */ + +const TAB_URL = "about:config"; + +let gPanel, gDoneChecks; + +function test() { + gDoneChecks = promise.defer(); + const tracerPref = promise.defer(); + const configPref = promise.defer(); + SpecialPowers.pushPrefEnv({'set': [["devtools.debugger.tracer", true]]}, tracerPref.resolve); + SpecialPowers.pushPrefEnv({'set': [["general.warnOnAboutConfig", false]]}, configPref.resolve); + promise.all([tracerPref.promise, configPref.promise]).then(() => { + initDebugger(TAB_URL).then(([,, aPanel]) => { + gPanel = aPanel; + gPanel.panelWin.gClient.addOneTimeListener("traces", testTraceLogs); + }).then(() => startTracing(gPanel)) + .then(generateTrace) + .then(() => waitForClientEvents(gPanel, "traces")) + .then(() => gDoneChecks.promise) + .then(() => stopTracing(gPanel)) + .then(resetPreferences) + .then(() => closeDebuggerAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); +} + +function testTraceLogs(name, packet) { + info("Traces: " + packet.traces.length); + ok(packet.traces.length > 0, "Got some traces."); + ok(packet.traces.every(t => t.type != "enteredFrame" || !!t.location), + "All enteredFrame traces contain location."); + gDoneChecks.resolve(); +} + +function generateTrace(name, packet) { + // Interact with the page to cause JS execution. + let search = content.document.getElementById("textbox"); + info("Interacting with the page."); + search.value = "devtools"; +} + +function resetPreferences() { + const deferred = promise.defer(); + SpecialPowers.popPrefEnv(() => SpecialPowers.popPrefEnv(deferred.resolve)); + return deferred.promise; +} + +registerCleanupFunction(function() { + gPanel = null; + gDoneChecks = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-01.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-01.js new file mode 100644 index 000000000..a3591eb93 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-01.js @@ -0,0 +1,126 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that creating, collpasing and expanding scopes in the + * variables view works as expected. + */ + +const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html"; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + let variables = aPanel.panelWin.DebuggerView.Variables; + let testScope = variables.addScope("test"); + + ok(testScope, + "Should have created a scope."); + ok(testScope.id.contains("test"), + "The newly created scope should have the default id set."); + is(testScope.name, "test", + "The newly created scope should have the desired name set."); + + ok(!testScope.displayValue, + "The newly created scope should not have a displayed value (1)."); + ok(!testScope.displayValueClassName, + "The newly created scope should not have a displayed value (2)."); + + ok(testScope.target, + "The newly created scope should point to a target node."); + ok(testScope.target.id.contains("test"), + "Should have the correct scope id on the element."); + + is(testScope.target.querySelector(".name").getAttribute("value"), "test", + "Any new scope should have the designated name."); + is(testScope.target.querySelector(".variables-view-element-details.enum").childNodes.length, 0, + "Any new scope should have a container with no enumerable child nodes."); + is(testScope.target.querySelector(".variables-view-element-details.nonenum").childNodes.length, 0, + "Any new scope should have a container with no non-enumerable child nodes."); + + ok(!testScope.expanded, + "Any new created scope should be initially collapsed."); + ok(testScope.visible, + "Any new created scope should be initially visible."); + + let expandCallbackArg = null; + let collapseCallbackArg = null; + let toggleCallbackArg = null; + let hideCallbackArg = null; + let showCallbackArg = null; + + testScope.onexpand = aScope => expandCallbackArg = aScope; + testScope.oncollapse = aScope => collapseCallbackArg = aScope; + testScope.ontoggle = aScope => toggleCallbackArg = aScope; + testScope.onhide = aScope => hideCallbackArg = aScope; + testScope.onshow = aScope => showCallbackArg = aScope; + + testScope.expand(); + ok(testScope.expanded, + "The testScope shouldn't be collapsed anymore."); + is(expandCallbackArg, testScope, + "The expandCallback wasn't called as it should."); + + testScope.collapse(); + ok(!testScope.expanded, + "The testScope should be collapsed again."); + is(collapseCallbackArg, testScope, + "The collapseCallback wasn't called as it should."); + + testScope.expanded = true; + ok(testScope.expanded, + "The testScope shouldn't be collapsed anymore."); + + testScope.toggle(); + ok(!testScope.expanded, + "The testScope should be collapsed again."); + is(toggleCallbackArg, testScope, + "The toggleCallback wasn't called as it should."); + + testScope.hide(); + ok(!testScope.visible, + "The testScope should be invisible after hiding."); + is(hideCallbackArg, testScope, + "The hideCallback wasn't called as it should."); + + testScope.show(); + ok(testScope.visible, + "The testScope should be visible again."); + is(showCallbackArg, testScope, + "The showCallback wasn't called as it should."); + + testScope.visible = false; + ok(!testScope.visible, + "The testScope should be invisible after hiding."); + ok(!testScope.expanded, + "The testScope should remember it is collapsed even if it is hidden."); + + testScope.visible = true; + ok(testScope.visible, + "The testScope should be visible after reshowing."); + ok(!testScope.expanded, + "The testScope should remember it is collapsed after it is reshown."); + + EventUtils.sendMouseEvent({ type: "mousedown", button: 1 }, + testScope.target.querySelector(".title"), + aPanel.panelWin); + + ok(!testScope.expanded, + "Clicking the testScope title with the right mouse button should't expand it."); + + EventUtils.sendMouseEvent({ type: "mousedown" }, + testScope.target.querySelector(".title"), + aPanel.panelWin); + + ok(testScope.expanded, + "Clicking the testScope title should expand it."); + + EventUtils.sendMouseEvent({ type: "mousedown" }, + testScope.target.querySelector(".title"), + aPanel.panelWin); + + ok(!testScope.expanded, + "Clicking again the testScope title should collapse it."); + + closeDebuggerAndFinish(aPanel); + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-02.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-02.js new file mode 100644 index 000000000..438b07a0f --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-02.js @@ -0,0 +1,221 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that creating, collpasing and expanding variables in the + * variables view works as expected. + */ + +const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html"; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + let variables = aPanel.panelWin.DebuggerView.Variables; + let testScope = variables.addScope("test"); + let testVar = testScope.addItem("something"); + let duplVar = testScope.addItem("something"); + + info("Scope id: " + testScope.id); + info("Scope name: " + testScope.name); + info("Variable id: " + testVar.id); + info("Variable name: " + testVar.name); + + ok(testScope, + "Should have created a scope."); + is(duplVar, null, + "Shouldn't be able to duplicate variables in the same scope."); + + ok(testVar, + "Should have created a variable."); + ok(testVar.id.contains("something"), + "The newly created variable should have the default id set."); + is(testVar.name, "something", + "The newly created variable should have the desired name set."); + + ok(!testVar.displayValue, + "The newly created variable should not have a displayed value yet (1)."); + ok(!testVar.displayValueClassName, + "The newly created variable should not have a displayed value yet (2)."); + + ok(testVar.target, + "The newly created scope should point to a target node."); + ok(testVar.target.id.contains("something"), + "Should have the correct variable id on the element."); + + is(testVar.target.querySelector(".name").getAttribute("value"), "something", + "Any new variable should have the designated name."); + is(testVar.target.querySelector(".variables-view-element-details.enum").childNodes.length, 0, + "Any new variable should have a container with no enumerable child nodes."); + is(testVar.target.querySelector(".variables-view-element-details.nonenum").childNodes.length, 0, + "Any new variable should have a container with no non-enumerable child nodes."); + + ok(!testVar.expanded, + "Any new created scope should be initially collapsed."); + ok(testVar.visible, + "Any new created scope should be initially visible."); + + let expandCallbackArg = null; + let collapseCallbackArg = null; + let toggleCallbackArg = null; + let hideCallbackArg = null; + let showCallbackArg = null; + + testVar.onexpand = aScope => expandCallbackArg = aScope; + testVar.oncollapse = aScope => collapseCallbackArg = aScope; + testVar.ontoggle = aScope => toggleCallbackArg = aScope; + testVar.onhide = aScope => hideCallbackArg = aScope; + testVar.onshow = aScope => showCallbackArg = aScope; + + testVar.expand(); + ok(testVar.expanded, + "The testVar shouldn't be collapsed anymore."); + is(expandCallbackArg, testVar, + "The expandCallback wasn't called as it should."); + + testVar.collapse(); + ok(!testVar.expanded, + "The testVar should be collapsed again."); + is(collapseCallbackArg, testVar, + "The collapseCallback wasn't called as it should."); + + testVar.expanded = true; + ok(testVar.expanded, + "The testVar shouldn't be collapsed anymore."); + + testVar.toggle(); + ok(!testVar.expanded, + "The testVar should be collapsed again."); + is(toggleCallbackArg, testVar, + "The toggleCallback wasn't called as it should."); + + testVar.hide(); + ok(!testVar.visible, + "The testVar should be invisible after hiding."); + is(hideCallbackArg, testVar, + "The hideCallback wasn't called as it should."); + + testVar.show(); + ok(testVar.visible, + "The testVar should be visible again."); + is(showCallbackArg, testVar, + "The showCallback wasn't called as it should."); + + testVar.visible = false; + ok(!testVar.visible, + "The testVar should be invisible after hiding."); + ok(!testVar.expanded, + "The testVar should remember it is collapsed even if it is hidden."); + + testVar.visible = true; + ok(testVar.visible, + "The testVar should be visible after reshowing."); + ok(!testVar.expanded, + "The testVar should remember it is collapsed after it is reshown."); + + EventUtils.sendMouseEvent({ type: "mousedown" }, + testVar.target.querySelector(".name"), + aPanel.panelWin); + + ok(testVar.expanded, + "Clicking the testVar name should expand it."); + + EventUtils.sendMouseEvent({ type: "mousedown" }, + testVar.target.querySelector(".name"), + aPanel.panelWin); + + ok(!testVar.expanded, + "Clicking again the testVar name should collapse it."); + + EventUtils.sendMouseEvent({ type: "mousedown" }, + testVar.target.querySelector(".arrow"), + aPanel.panelWin); + + ok(testVar.expanded, + "Clicking the testVar arrow should expand it."); + + EventUtils.sendMouseEvent({ type: "mousedown" }, + testVar.target.querySelector(".arrow"), + aPanel.panelWin); + + ok(!testVar.expanded, + "Clicking again the testVar arrow should collapse it."); + + EventUtils.sendMouseEvent({ type: "mousedown" }, + testVar.target.querySelector(".title"), + aPanel.panelWin); + + ok(testVar.expanded, + "Clicking the testVar title should expand it again."); + + testVar.addItem("child", { + value: { + type: "object", + class: "Object" + } + }); + + let testChild = testVar.get("child"); + ok(testChild, + "Should have created a child property."); + ok(testChild.id.contains("child"), + "The newly created property should have the default id set."); + is(testChild.name, "child", + "The newly created property should have the desired name set."); + + is(testChild.displayValue, "Object", + "The newly created property should not have a displayed value yet (1)."); + is(testChild.displayValueClassName, "token-other", + "The newly created property should not have a displayed value yet (2)."); + + ok(testChild.target, + "The newly created scope should point to a target node."); + ok(testChild.target.id.contains("child"), + "Should have the correct property id on the element."); + + is(testChild.target.querySelector(".name").getAttribute("value"), "child", + "Any new property should have the designated name."); + is(testChild.target.querySelector(".variables-view-element-details.enum").childNodes.length, 0, + "Any new property should have a container with no enumerable child nodes."); + is(testChild.target.querySelector(".variables-view-element-details.nonenum").childNodes.length, 0, + "Any new property should have a container with no non-enumerable child nodes."); + + ok(!testChild.expanded, + "Any new created scope should be initially collapsed."); + ok(testChild.visible, + "Any new created scope should be initially visible."); + + EventUtils.sendMouseEvent({ type: "mousedown" }, + testChild.target.querySelector(".name"), + aPanel.panelWin); + + ok(testChild.expanded, + "Clicking the testChild name should expand it."); + + EventUtils.sendMouseEvent({ type: "mousedown" }, + testChild.target.querySelector(".name"), + aPanel.panelWin); + + ok(!testChild.expanded, + "Clicking again the testChild name should collapse it."); + + EventUtils.sendMouseEvent({ type: "mousedown" }, + testChild.target.querySelector(".arrow"), + aPanel.panelWin); + + ok(testChild.expanded, + "Clicking the testChild arrow should expand it."); + + EventUtils.sendMouseEvent({ type: "mousedown" }, + testChild.target.querySelector(".arrow"), + aPanel.panelWin); + + ok(!testChild.expanded, + "Clicking again the testChild arrow should collapse it."); + + EventUtils.sendMouseEvent({ type: "mousedown" }, + testChild.target.querySelector(".title"), + aPanel.panelWin); + + closeDebuggerAndFinish(aPanel); + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-03.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-03.js new file mode 100644 index 000000000..4a591ad96 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-03.js @@ -0,0 +1,151 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that recursively creating properties in the variables view works + * as expected. + */ + +const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html"; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + let variables = aPanel.panelWin.DebuggerView.Variables; + let testScope = variables.addScope("test"); + + is(testScope.target.querySelectorAll(".variables-view-element-details.enum").length, 1, + "One enumerable container should be present in the scope."); + is(testScope.target.querySelectorAll(".variables-view-element-details.nonenum").length, 1, + "One non-enumerable container should be present in the scope."); + is(testScope.target.querySelector(".variables-view-element-details.enum").childNodes.length, 0, + "No enumerable variables should be present in the scope."); + is(testScope.target.querySelector(".variables-view-element-details.nonenum").childNodes.length, 0, + "No non-enumerable variables should be present in the scope."); + + testScope.addItem("something", { + value: { + type: "object", + class: "Object" + }, + enumerable: true + }); + + is(testScope.target.querySelectorAll(".variables-view-element-details.enum").length, 2, + "Two enumerable containers should be present in the tree."); + is(testScope.target.querySelectorAll(".variables-view-element-details.nonenum").length, 2, + "Two non-enumerable containers should be present in the tree."); + + is(testScope.target.querySelector(".variables-view-element-details.enum").childNodes.length, 1, + "A new enumerable variable should have been added in the scope."); + is(testScope.target.querySelector(".variables-view-element-details.nonenum").childNodes.length, 0, + "No new non-enumerable variables should have been added in the scope."); + + let testVar = testScope.get("something"); + ok(testVar, + "The added variable should be accessible from the scope."); + + is(testVar.target.querySelectorAll(".variables-view-element-details.enum").length, 1, + "One enumerable container should be present in the variable."); + is(testVar.target.querySelectorAll(".variables-view-element-details.nonenum").length, 1, + "One non-enumerable container should be present in the variable."); + is(testVar.target.querySelector(".variables-view-element-details.enum").childNodes.length, 0, + "No enumerable properties should be present in the variable."); + is(testVar.target.querySelector(".variables-view-element-details.nonenum").childNodes.length, 0, + "No non-enumerable properties should be present in the variable."); + + testVar.addItem("child", { + value: { + type: "object", + class: "Object" + }, + enumerable: true + }); + + is(testScope.target.querySelectorAll(".variables-view-element-details.enum").length, 3, + "Three enumerable containers should be present in the tree."); + is(testScope.target.querySelectorAll(".variables-view-element-details.nonenum").length, 3, + "Three non-enumerable containers should be present in the tree."); + + is(testVar.target.querySelector(".variables-view-element-details.enum").childNodes.length, 1, + "A new enumerable property should have been added in the variable."); + is(testVar.target.querySelector(".variables-view-element-details.nonenum").childNodes.length, 0, + "No new non-enumerable properties should have been added in the variable."); + + let testChild = testVar.get("child"); + ok(testChild, + "The added property should be accessible from the variable."); + + is(testChild.target.querySelectorAll(".variables-view-element-details.enum").length, 1, + "One enumerable container should be present in the property."); + is(testChild.target.querySelectorAll(".variables-view-element-details.nonenum").length, 1, + "One non-enumerable container should be present in the property."); + is(testChild.target.querySelector(".variables-view-element-details.enum").childNodes.length, 0, + "No enumerable sub-properties should be present in the property."); + is(testChild.target.querySelector(".variables-view-element-details.nonenum").childNodes.length, 0, + "No non-enumerable sub-properties should be present in the property."); + + testChild.addItem("grandChild", { + value: { + type: "object", + class: "Object" + }, + enumerable: true + }); + + is(testScope.target.querySelectorAll(".variables-view-element-details.enum").length, 4, + "Four enumerable containers should be present in the tree."); + is(testScope.target.querySelectorAll(".variables-view-element-details.nonenum").length, 4, + "Four non-enumerable containers should be present in the tree."); + + is(testChild.target.querySelector(".variables-view-element-details.enum").childNodes.length, 1, + "A new enumerable sub-property should have been added in the property."); + is(testChild.target.querySelector(".variables-view-element-details.nonenum").childNodes.length, 0, + "No new non-enumerable sub-properties should have been added in the property."); + + let testGrandChild = testChild.get("grandChild"); + ok(testGrandChild, + "The added sub-property should be accessible from the property."); + + is(testGrandChild.target.querySelectorAll(".variables-view-element-details.enum").length, 1, + "One enumerable container should be present in the property."); + is(testGrandChild.target.querySelectorAll(".variables-view-element-details.nonenum").length, 1, + "One non-enumerable container should be present in the property."); + is(testGrandChild.target.querySelector(".variables-view-element-details.enum").childNodes.length, 0, + "No enumerable sub-properties should be present in the property."); + is(testGrandChild.target.querySelector(".variables-view-element-details.nonenum").childNodes.length, 0, + "No non-enumerable sub-properties should be present in the property."); + + testGrandChild.addItem("granderChild", { + value: { + type: "object", + class: "Object" + }, + enumerable: true + }); + + is(testScope.target.querySelectorAll(".variables-view-element-details.enum").length, 5, + "Five enumerable containers should be present in the tree."); + is(testScope.target.querySelectorAll(".variables-view-element-details.nonenum").length, 5, + "Five non-enumerable containers should be present in the tree."); + + is(testGrandChild.target.querySelector(".variables-view-element-details.enum").childNodes.length, 1, + "A new enumerable variable should have been added in the variable."); + is(testGrandChild.target.querySelector(".variables-view-element-details.nonenum").childNodes.length, 0, + "No new non-enumerable variables should have been added in the variable."); + + let testGranderChild = testGrandChild.get("granderChild"); + ok(testGranderChild, + "The added sub-property should be accessible from the property."); + + is(testGranderChild.target.querySelectorAll(".variables-view-element-details.enum").length, 1, + "One enumerable container should be present in the property."); + is(testGranderChild.target.querySelectorAll(".variables-view-element-details.nonenum").length, 1, + "One non-enumerable container should be present in the property."); + is(testGranderChild.target.querySelector(".variables-view-element-details.enum").childNodes.length, 0, + "No enumerable sub-properties should be present in the property."); + is(testGranderChild.target.querySelector(".variables-view-element-details.nonenum").childNodes.length, 0, + "No non-enumerable sub-properties should be present in the property."); + + closeDebuggerAndFinish(aPanel); + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-04.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-04.js new file mode 100644 index 000000000..1ad4ff10f --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-04.js @@ -0,0 +1,150 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that grips are correctly applied to variables. + */ + +const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html"; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + let variables = aPanel.panelWin.DebuggerView.Variables; + let testScope = variables.addScope("test"); + let testVar = testScope.addItem("something"); + + testVar.setGrip(1.618); + + is(testVar.target.querySelector(".value").getAttribute("value"), "1.618", + "The grip information for the variable wasn't set correctly (1)."); + is(testVar.target.querySelector(".variables-view-element-details.enum").childNodes.length, 0, + "Setting the grip alone shouldn't add any new tree nodes (1)."); + is(testVar.target.querySelector(".variables-view-element-details.nonenum").childNodes.length, 0, + "Setting the grip alone shouldn't add any new tree nodes (2)."); + + testVar.setGrip({ + type: "object", + class: "Window" + }); + + is(testVar.target.querySelector(".value").getAttribute("value"), "Window", + "The grip information for the variable wasn't set correctly (2)."); + is(testVar.target.querySelector(".variables-view-element-details.enum").childNodes.length, 0, + "Setting the grip alone shouldn't add any new tree nodes (3)."); + is(testVar.target.querySelector(".variables-view-element-details.nonenum").childNodes.length, 0, + "Setting the grip alone shouldn't add any new tree nodes (4)."); + + testVar.addItems({ + helloWorld: { + value: "hello world", + enumerable: true + } + }); + + is(testVar.target.querySelector(".variables-view-element-details").childNodes.length, 1, + "A new detail node should have been added in the variable tree."); + is(testVar.get("helloWorld").target.querySelector(".value").getAttribute("value"), "\"hello world\"", + "The grip information for the variable wasn't set correctly (3)."); + + testVar.addItems({ + helloWorld: { + value: "hello jupiter", + enumerable: true + } + }); + + is(testVar.target.querySelector(".variables-view-element-details").childNodes.length, 1, + "Shouldn't be able to duplicate nodes added in the variable tree."); + is(testVar.get("helloWorld").target.querySelector(".value").getAttribute("value"), "\"hello world\"", + "The grip information for the variable wasn't preserved correctly (4)."); + + testVar.addItems({ + someProp0: { + value: "random string", + enumerable: true + }, + someProp1: { + value: "another string", + enumerable: true + } + }); + + is(testVar.target.querySelector(".variables-view-element-details").childNodes.length, 3, + "Two new detail nodes should have been added in the variable tree."); + is(testVar.get("someProp0").target.querySelector(".value").getAttribute("value"), "\"random string\"", + "The grip information for the variable wasn't set correctly (5)."); + is(testVar.get("someProp1").target.querySelector(".value").getAttribute("value"), "\"another string\"", + "The grip information for the variable wasn't set correctly (6)."); + + testVar.addItems({ + someProp2: { + value: { + type: "null" + }, + enumerable: true + }, + someProp3: { + value: { + type: "undefined" + }, + enumerable: true + }, + someProp4: { + value: { + type: "object", + class: "Object" + }, + enumerable: true + } + }); + + is(testVar.target.querySelector(".variables-view-element-details").childNodes.length, 6, + "Three new detail nodes should have been added in the variable tree."); + is(testVar.get("someProp2").target.querySelector(".value").getAttribute("value"), "null", + "The grip information for the variable wasn't set correctly (7)."); + is(testVar.get("someProp3").target.querySelector(".value").getAttribute("value"), "undefined", + "The grip information for the variable wasn't set correctly (8)."); + is(testVar.get("someProp4").target.querySelector(".value").getAttribute("value"), "Object", + "The grip information for the variable wasn't set correctly (9)."); + + let parent = testVar.get("someProp2"); + let child = parent.addItem("child", { + value: { + type: "null" + } + }); + + is(variables.getItemForNode(parent.target), parent, + "VariablesView should have a record of the parent."); + is(variables.getItemForNode(child.target), child, + "VariablesView should have a record of the child."); + is([...parent].length, 1, + "Parent should have one child."); + + parent.remove(); + + is(variables.getItemForNode(parent.target), undefined, + "VariablesView should not have a record of the parent anymore."); + is(parent.target.parentNode, null, + "Parent element should not have a parent.") + is(variables.getItemForNode(child.target), undefined, + "VariablesView should not have a record of the child anymore."); + is(child.target.parentNode, null, + "Child element should not have a parent.") + is([...parent].length, 0, + "Parent should have zero children."); + + testScope.remove(); + + is([...variables].length, 0, + "VariablesView should have been emptied."); + is(Cu.nondeterministicGetWeakMapKeys(variables._itemsByElement).length, 0, + "VariablesView _itemsByElement map has been emptied."); + is(variables._currHierarchy.size, 0, + "VariablesView _currHierarchy map has been emptied."); + is(variables._list.children.length, 0, + "VariablesView element should have no children."); + + closeDebuggerAndFinish(aPanel); + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-05.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-05.js new file mode 100644 index 000000000..71c857fb6 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-05.js @@ -0,0 +1,228 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that grips are correctly applied to variables and properties. + */ + +const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html"; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + let variables = aPanel.panelWin.DebuggerView.Variables; + + let globalScope = variables.addScope("Test-Global"); + let localScope = variables.addScope("Test-Local"); + + ok(globalScope, "The globalScope hasn't been created correctly."); + ok(localScope, "The localScope hasn't been created correctly."); + + is(globalScope.target.querySelector(".separator"), null, + "No separator string should be created for scopes (1)."); + is(localScope.target.querySelector(".separator"), null, + "No separator string should be created for scopes (2)."); + + let windowVar = globalScope.addItem("window"); + let documentVar = globalScope.addItem("document"); + + ok(windowVar, "The windowVar hasn't been created correctly."); + ok(documentVar, "The documentVar hasn't been created correctly."); + + ok(windowVar.target.querySelector(".separator").hidden, + "No separator string should be shown for variables without a grip (1)."); + ok(documentVar.target.querySelector(".separator").hidden, + "No separator string should be shown for variables without a grip (2)."); + + windowVar.setGrip({ type: "object", class: "Window" }); + documentVar.setGrip({ type: "object", class: "HTMLDocument" }); + + is(windowVar.target.querySelector(".separator").hidden, false, + "A separator string should now be shown after setting the grip (1)."); + is(documentVar.target.querySelector(".separator").hidden, false, + "A separator string should now be shown after setting the grip (2)."); + + is(windowVar.target.querySelector(".separator").getAttribute("value"), ": ", + "The separator string label is correct (1)."); + is(documentVar.target.querySelector(".separator").getAttribute("value"), ": ", + "The separator string label is correct (2)."); + + let localVar0 = localScope.addItem("localVar0"); + let localVar1 = localScope.addItem("localVar1"); + let localVar2 = localScope.addItem("localVar2"); + let localVar3 = localScope.addItem("localVar3"); + let localVar4 = localScope.addItem("localVar4"); + let localVar5 = localScope.addItem("localVar5"); + + let localVar6 = localScope.addItem("localVar6"); + let localVar7 = localScope.addItem("localVar7"); + let localVar8 = localScope.addItem("localVar8"); + let localVar9 = localScope.addItem("localVar9"); + + ok(localVar0, "The localVar0 hasn't been created correctly."); + ok(localVar1, "The localVar1 hasn't been created correctly."); + ok(localVar2, "The localVar2 hasn't been created correctly."); + ok(localVar3, "The localVar3 hasn't been created correctly."); + ok(localVar4, "The localVar4 hasn't been created correctly."); + ok(localVar5, "The localVar5 hasn't been created correctly."); + ok(localVar6, "The localVar6 hasn't been created correctly."); + ok(localVar7, "The localVar7 hasn't been created correctly."); + ok(localVar8, "The localVar8 hasn't been created correctly."); + ok(localVar9, "The localVar9 hasn't been created correctly."); + + localVar0.setGrip(42); + localVar1.setGrip(true); + localVar2.setGrip("nasu"); + + localVar3.setGrip({ type: "undefined" }); + localVar4.setGrip({ type: "null" }); + localVar5.setGrip({ type: "object", class: "Object" }); + localVar6.setGrip({ type: "Infinity" }); + localVar7.setGrip({ type: "-Infinity" }); + localVar8.setGrip({ type: "NaN" }); + localVar9.setGrip({ type: "-0" }); + + localVar5.addItems({ + someProp0: { value: 42, enumerable: true }, + someProp1: { value: true, enumerable: true }, + someProp2: { value: "nasu", enumerable: true }, + someProp3: { value: { type: "undefined" }, enumerable: true }, + someProp4: { value: { type: "null" }, enumerable: true }, + someProp5: { value: { type: "object", class: "Object" }, enumerable: true }, + someProp6: { value: { type: "Infinity" }, enumerable: true }, + someProp7: { value: { type: "-Infinity" }, enumerable: true }, + someProp8: { value: { type: "NaN" }, enumerable: true }, + someProp9: { value: { type: "-0" }, enumerable: true }, + someUndefined: { + get: { type: "undefined" }, + set: { type: "undefined" }, + enumerable: true + }, + someAccessor: { + get: { type: "object", class: "Function" }, + set: { type: "undefined" }, + enumerable: true + } + }); + + localVar5.get("someProp5").addItems({ + someProp0: { value: 42, enumerable: true }, + someProp1: { value: true, enumerable: true }, + someProp2: { value: "nasu", enumerable: true }, + someProp3: { value: { type: "undefined" }, enumerable: true }, + someProp4: { value: { type: "null" }, enumerable: true }, + someProp5: { value: { type: "object", class: "Object" }, enumerable: true }, + someProp6: { value: { type: "Infinity" }, enumerable: true }, + someProp7: { value: { type: "-Infinity" }, enumerable: true }, + someProp8: { value: { type: "NaN" }, enumerable: true }, + someProp9: { value: { type: "-0" }, enumerable: true }, + someUndefined: { + get: { type: "undefined" }, + set: { type: "undefined" }, + enumerable: true + }, + someAccessor: { + get: { type: "object", class: "Function" }, + set: { type: "undefined" }, + enumerable: true + } + }); + + is(globalScope.target.querySelector(".enum").childNodes.length, 0, + "The globalScope doesn't contain all the created enumerable variable elements."); + is(globalScope.target.querySelector(".nonenum").childNodes.length, 2, + "The globalScope doesn't contain all the created non-enumerable variable elements."); + + is(localScope.target.querySelector(".enum").childNodes.length, 0, + "The localScope doesn't contain all the created enumerable variable elements."); + is(localScope.target.querySelector(".nonenum").childNodes.length, 10, + "The localScope doesn't contain all the created non-enumerable variable elements."); + + is(localVar5.target.querySelector(".enum").childNodes.length, 12, + "The localVar5 doesn't contain all the created enumerable properties."); + is(localVar5.target.querySelector(".nonenum").childNodes.length, 0, + "The localVar5 doesn't contain all the created non-enumerable properties."); + + is(localVar5.get("someProp5").target.querySelector(".enum").childNodes.length, 12, + "The localVar5.someProp5 doesn't contain all the created enumerable properties."); + is(localVar5.get("someProp5").target.querySelector(".nonenum").childNodes.length, 0, + "The localVar5.someProp5 doesn't contain all the created non-enumerable properties."); + + is(windowVar.target.querySelector(".value").getAttribute("value"), "Window", + "The grip information for the windowVar wasn't set correctly."); + is(documentVar.target.querySelector(".value").getAttribute("value"), "HTMLDocument", + "The grip information for the documentVar wasn't set correctly."); + + is(localVar0.target.querySelector(".value").getAttribute("value"), "42", + "The grip information for the localVar0 wasn't set correctly."); + is(localVar1.target.querySelector(".value").getAttribute("value"), "true", + "The grip information for the localVar1 wasn't set correctly."); + is(localVar2.target.querySelector(".value").getAttribute("value"), "\"nasu\"", + "The grip information for the localVar2 wasn't set correctly."); + is(localVar3.target.querySelector(".value").getAttribute("value"), "undefined", + "The grip information for the localVar3 wasn't set correctly."); + is(localVar4.target.querySelector(".value").getAttribute("value"), "null", + "The grip information for the localVar4 wasn't set correctly."); + is(localVar5.target.querySelector(".value").getAttribute("value"), "Object", + "The grip information for the localVar5 wasn't set correctly."); + is(localVar6.target.querySelector(".value").getAttribute("value"), "Infinity", + "The grip information for the localVar6 wasn't set correctly."); + is(localVar7.target.querySelector(".value").getAttribute("value"), "-Infinity", + "The grip information for the localVar7 wasn't set correctly."); + is(localVar8.target.querySelector(".value").getAttribute("value"), "NaN", + "The grip information for the localVar8 wasn't set correctly."); + is(localVar9.target.querySelector(".value").getAttribute("value"), "-0", + "The grip information for the localVar9 wasn't set correctly."); + + is(localVar5.get("someProp0").target.querySelector(".value").getAttribute("value"), "42", + "The grip information for the someProp0 wasn't set correctly."); + is(localVar5.get("someProp1").target.querySelector(".value").getAttribute("value"), "true", + "The grip information for the someProp1 wasn't set correctly."); + is(localVar5.get("someProp2").target.querySelector(".value").getAttribute("value"), "\"nasu\"", + "The grip information for the someProp2 wasn't set correctly."); + is(localVar5.get("someProp3").target.querySelector(".value").getAttribute("value"), "undefined", + "The grip information for the someProp3 wasn't set correctly."); + is(localVar5.get("someProp4").target.querySelector(".value").getAttribute("value"), "null", + "The grip information for the someProp4 wasn't set correctly."); + is(localVar5.get("someProp5").target.querySelector(".value").getAttribute("value"), "Object", + "The grip information for the someProp5 wasn't set correctly."); + is(localVar5.get("someProp6").target.querySelector(".value").getAttribute("value"), "Infinity", + "The grip information for the someProp6 wasn't set correctly."); + is(localVar5.get("someProp7").target.querySelector(".value").getAttribute("value"), "-Infinity", + "The grip information for the someProp7 wasn't set correctly."); + is(localVar5.get("someProp8").target.querySelector(".value").getAttribute("value"), "NaN", + "The grip information for the someProp8 wasn't set correctly."); + is(localVar5.get("someProp9").target.querySelector(".value").getAttribute("value"), "-0", + "The grip information for the someProp9 wasn't set correctly."); + is(localVar5.get("someUndefined").target.querySelector(".value").getAttribute("value"), "", + "The grip information for the someUndefined wasn't set correctly."); + is(localVar5.get("someAccessor").target.querySelector(".value").getAttribute("value"), "", + "The grip information for the someAccessor wasn't set correctly."); + + is(localVar5.get("someProp5").get("someProp0").target.querySelector(".value").getAttribute("value"), "42", + "The grip information for the sub-someProp0 wasn't set correctly."); + is(localVar5.get("someProp5").get("someProp1").target.querySelector(".value").getAttribute("value"), "true", + "The grip information for the sub-someProp1 wasn't set correctly."); + is(localVar5.get("someProp5").get("someProp2").target.querySelector(".value").getAttribute("value"), "\"nasu\"", + "The grip information for the sub-someProp2 wasn't set correctly."); + is(localVar5.get("someProp5").get("someProp3").target.querySelector(".value").getAttribute("value"), "undefined", + "The grip information for the sub-someProp3 wasn't set correctly."); + is(localVar5.get("someProp5").get("someProp4").target.querySelector(".value").getAttribute("value"), "null", + "The grip information for the sub-someProp4 wasn't set correctly."); + is(localVar5.get("someProp5").get("someProp5").target.querySelector(".value").getAttribute("value"), "Object", + "The grip information for the sub-someProp5 wasn't set correctly."); + is(localVar5.get("someProp5").get("someProp6").target.querySelector(".value").getAttribute("value"), "Infinity", + "The grip information for the sub-someProp6 wasn't set correctly."); + is(localVar5.get("someProp5").get("someProp7").target.querySelector(".value").getAttribute("value"), "-Infinity", + "The grip information for the sub-someProp7 wasn't set correctly."); + is(localVar5.get("someProp5").get("someProp8").target.querySelector(".value").getAttribute("value"), "NaN", + "The grip information for the sub-someProp8 wasn't set correctly."); + is(localVar5.get("someProp5").get("someProp9").target.querySelector(".value").getAttribute("value"), "-0", + "The grip information for the sub-someProp9 wasn't set correctly."); + is(localVar5.get("someProp5").get("someUndefined").target.querySelector(".value").getAttribute("value"), "", + "The grip information for the sub-someUndefined wasn't set correctly."); + is(localVar5.get("someProp5").get("someAccessor").target.querySelector(".value").getAttribute("value"), "", + "The grip information for the sub-someAccessor wasn't set correctly."); + + closeDebuggerAndFinish(aPanel); + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-06.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-06.js new file mode 100644 index 000000000..fa6901d08 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-06.js @@ -0,0 +1,122 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that Promises get their internal state added as psuedo properties. + */ + +const TAB_URL = EXAMPLE_URL + "doc_promise.html"; + +const test = Task.async(function* () { + const [tab,, panel] = yield initDebugger(TAB_URL); + yield ensureSourceIs(panel, "doc_promise.html", true); + + const scopes = waitForCaretAndScopes(panel, 21); + callInTab(tab, "doPause"); + yield scopes; + + const variables = panel.panelWin.DebuggerView.Variables; + ok(variables, "Should get the variables view."); + + const scope = [...variables][0]; + ok(scope, "Should get the current function's scope."); + + const promiseVariables = [...scope].filter(([name]) => + ["p", "f", "r"].indexOf(name) !== -1); + + is(promiseVariables.length, 3, + "Should have our 3 promise variables: p, f, r"); + + for (let [name, item] of promiseVariables) { + info("Expanding variable '" + name + "'"); + let expanded = once(variables, "fetched"); + item.expand(); + yield expanded; + + let foundState = false; + switch (name) { + case "p": + for (let [property, { value }] of item) { + if (property !== "<state>") { + isnot(property, "<value>", + "A pending promise shouldn't have a value"); + isnot(property, "<reason>", + "A pending promise shouldn't have a reason"); + continue; + } + + foundState = true; + is(value, "pending", "The state should be pending."); + } + ok(foundState, "We should have found the <state> property."); + break; + + case "f": + let foundValue = false; + for (let [property, value] of item) { + if (property === "<state>") { + foundState = true; + is(value.value, "fulfilled", "The state should be fulfilled."); + } else if (property === "<value>") { + foundValue = true; + + let expanded = once(variables, "fetched"); + value.expand(); + yield expanded; + + let expectedProps = new Map([["a", 1], ["b", 2], ["c", 3]]); + for (let [prop, val] of value) { + if (prop === "__proto__") { + continue; + } + ok(expectedProps.has(prop), "The property should be expected."); + is(val.value, expectedProps.get(prop), "The property value should be correct."); + expectedProps.delete(prop); + } + is(Object.keys(expectedProps).length, 0, + "Should have found all of the expected properties."); + } else { + isnot(property, "<reason>", + "A fulfilled promise shouldn't have a reason"); + } + } + ok(foundState, "We should have found the <state> property."); + ok(foundValue, "We should have found the <value> property."); + break; + + case "r": + let foundReason = false; + for (let [property, value] of item) { + if (property === "<state>") { + foundState = true; + is(value.value, "rejected", "The state should be rejected."); + } else if (property === "<reason>") { + foundReason = true; + + let expanded = once(variables, "fetched"); + value.expand(); + yield expanded; + + let foundMessage = false; + for (let [prop, val] of value) { + if (prop !== "message") { + continue; + } + foundMessage = true; + is(val.value, "uh oh", "Should have the correct error message."); + } + ok(foundMessage, "Should have found the error's message"); + } else { + isnot(property, "<value>", + "A rejected promise shouldn't have a value"); + } + } + ok(foundState, "We should have found the <state> property."); + break; + } + } + + debugger; + + resumeDebuggerThenCloseAndFinish(panel); +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-accessibility.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-accessibility.js new file mode 100644 index 000000000..7605b2cca --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-accessibility.js @@ -0,0 +1,555 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that the variables view is keyboard accessible. + */ + +let gTab, gPanel, gDebugger; +let gVariablesView; + +function test() { + initDebugger("about:blank").then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gVariablesView = gDebugger.DebuggerView.Variables; + + performTest().then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); +} + +function performTest() { + let arr = [ + 42, + true, + "nasu", + undefined, + null, + [0, 1, 2], + { prop1: 9, prop2: 8 } + ]; + + let obj = { + p0: 42, + p1: true, + p2: "nasu", + p3: undefined, + p4: null, + p5: [3, 4, 5], + p6: { prop1: 7, prop2: 6 }, + get p7() { return arr; }, + set p8(value) { arr[0] = value } + }; + + let test = { + someProp0: 42, + someProp1: true, + someProp2: "nasu", + someProp3: undefined, + someProp4: null, + someProp5: arr, + someProp6: obj, + get someProp7() { return arr; }, + set someProp7(value) { arr[0] = value } + }; + + gVariablesView.eval = function() {}; + gVariablesView.switch = function() {}; + gVariablesView.delete = function() {}; + gVariablesView.rawObject = test; + gVariablesView.scrollPageSize = 5; + + return Task.spawn(function() { + yield waitForTick(); + + // Part 0: Test generic focus methods on the variables view. + + gVariablesView.focusFirstVisibleItem(); + is(gVariablesView.getFocusedItem().name, "someProp0", + "The 'someProp0' item should be focused."); + + gVariablesView.focusNextItem(); + is(gVariablesView.getFocusedItem().name, "someProp1", + "The 'someProp1' item should be focused."); + + gVariablesView.focusPrevItem(); + is(gVariablesView.getFocusedItem().name, "someProp0", + "The 'someProp0' item should be focused."); + + // Part 1: Make sure that UP/DOWN keys don't scroll the variables view. + + yield synthesizeKeyAndWaitForTick("VK_DOWN", {}); + is(gVariablesView._parent.scrollTop, 0, + "The 'variables' view shouldn't scroll when pressing the DOWN key."); + + yield synthesizeKeyAndWaitForTick("VK_UP", {}); + is(gVariablesView._parent.scrollTop, 0, + "The 'variables' view shouldn't scroll when pressing the UP key."); + + // Part 2: Make sure that RETURN/ESCAPE toggle input elements. + + yield synthesizeKeyAndWaitForElement("VK_RETURN", {}, ".element-value-input", true); + yield synthesizeKeyAndWaitForElement("VK_ESCAPE", {}, ".element-value-input", false); + yield synthesizeKeyAndWaitForElement("VK_RETURN", { shiftKey: true }, ".element-name-input", true); + yield synthesizeKeyAndWaitForElement("VK_ESCAPE", {}, ".element-name-input", false); + + // Part 3: Test simple navigation. + + EventUtils.sendKey("DOWN", gDebugger); + is(gVariablesView.getFocusedItem().name, "someProp1", + "The 'someProp1' item should be focused."); + + EventUtils.sendKey("UP", gDebugger); + is(gVariablesView.getFocusedItem().name, "someProp0", + "The 'someProp0' item should be focused."); + + EventUtils.sendKey("PAGE_DOWN", gDebugger); + is(gVariablesView.getFocusedItem().name, "someProp5", + "The 'someProp5' item should be focused."); + + EventUtils.sendKey("PAGE_UP", gDebugger); + is(gVariablesView.getFocusedItem().name, "someProp0", + "The 'someProp0' item should be focused."); + + EventUtils.sendKey("END", gDebugger); + is(gVariablesView.getFocusedItem().name, "__proto__", + "The '__proto__' item should be focused."); + + EventUtils.sendKey("HOME", gDebugger); + is(gVariablesView.getFocusedItem().name, "someProp0", + "The 'someProp0' item should be focused."); + + // Part 4: Test if pressing the same navigation key twice works as expected. + + EventUtils.sendKey("DOWN", gDebugger); + is(gVariablesView.getFocusedItem().name, "someProp1", + "The 'someProp1' item should be focused."); + + EventUtils.sendKey("DOWN", gDebugger); + is(gVariablesView.getFocusedItem().name, "someProp2", + "The 'someProp2' item should be focused."); + + EventUtils.sendKey("UP", gDebugger); + is(gVariablesView.getFocusedItem().name, "someProp1", + "The 'someProp1' item should be focused."); + + EventUtils.sendKey("UP", gDebugger); + is(gVariablesView.getFocusedItem().name, "someProp0", + "The 'someProp0' item should be focused."); + + EventUtils.sendKey("PAGE_DOWN", gDebugger); + is(gVariablesView.getFocusedItem().name, "someProp5", + "The 'someProp5' item should be focused."); + + EventUtils.sendKey("PAGE_DOWN", gDebugger); + is(gVariablesView.getFocusedItem().name, "__proto__", + "The '__proto__' item should be focused."); + + EventUtils.sendKey("PAGE_UP", gDebugger); + is(gVariablesView.getFocusedItem().name, "someProp5", + "The 'someProp5' item should be focused."); + + EventUtils.sendKey("PAGE_UP", gDebugger); + is(gVariablesView.getFocusedItem().name, "someProp0", + "The 'someProp0' item should be focused."); + + // Part 5: Test that HOME/PAGE_UP/PAGE_DOWN are symmetrical. + + EventUtils.sendKey("HOME", gDebugger); + is(gVariablesView.getFocusedItem().name, "someProp0", + "The 'someProp0' item should be focused."); + + EventUtils.sendKey("HOME", gDebugger); + is(gVariablesView.getFocusedItem().name, "someProp0", + "The 'someProp0' item should be focused."); + + EventUtils.sendKey("PAGE_UP", gDebugger); + is(gVariablesView.getFocusedItem().name, "someProp0", + "The 'someProp0' item should be focused."); + + EventUtils.sendKey("HOME", gDebugger); + is(gVariablesView.getFocusedItem().name, "someProp0", + "The 'someProp0' item should be focused."); + + EventUtils.sendKey("END", gDebugger); + is(gVariablesView.getFocusedItem().name, "__proto__", + "The '__proto__' item should be focused."); + + EventUtils.sendKey("END", gDebugger); + is(gVariablesView.getFocusedItem().name, "__proto__", + "The '__proto__' item should be focused."); + + EventUtils.sendKey("PAGE_DOWN", gDebugger); + is(gVariablesView.getFocusedItem().name, "__proto__", + "The '__proto__' item should be focused."); + + EventUtils.sendKey("END", gDebugger); + is(gVariablesView.getFocusedItem().name, "__proto__", + "The '__proto__' item should be focused."); + + // Part 6: Test that focus doesn't leave the variables view. + + EventUtils.sendKey("PAGE_UP", gDebugger); + is(gVariablesView.getFocusedItem().name, "someProp5", + "The 'someProp5' item should be focused."); + + EventUtils.sendKey("PAGE_UP", gDebugger); + is(gVariablesView.getFocusedItem().name, "someProp0", + "The 'someProp0' item should be focused."); + + EventUtils.sendKey("UP", gDebugger); + is(gVariablesView.getFocusedItem().name, "someProp0", + "The 'someProp0' item should be focused."); + + EventUtils.sendKey("UP", gDebugger); + is(gVariablesView.getFocusedItem().name, "someProp0", + "The 'someProp0' item should be focused."); + + EventUtils.sendKey("PAGE_DOWN", gDebugger); + is(gVariablesView.getFocusedItem().name, "someProp5", + "The 'someProp5' item should be focused."); + + EventUtils.sendKey("PAGE_DOWN", gDebugger); + is(gVariablesView.getFocusedItem().name, "__proto__", + "The '__proto__' item should be focused."); + + EventUtils.sendKey("PAGE_DOWN", gDebugger); + is(gVariablesView.getFocusedItem().name, "__proto__", + "The '__proto__' item should be focused."); + + EventUtils.sendKey("PAGE_DOWN", gDebugger); + is(gVariablesView.getFocusedItem().name, "__proto__", + "The '__proto__' item should be focused."); + + // Part 7: Test that random offsets don't occur in tandem with HOME/END. + + EventUtils.sendKey("HOME", gDebugger); + is(gVariablesView.getFocusedItem().name, "someProp0", + "The 'someProp0' item should be focused."); + + EventUtils.sendKey("DOWN", gDebugger); + is(gVariablesView.getFocusedItem().name, "someProp1", + "The 'someProp1' item should be focused."); + + EventUtils.sendKey("END", gDebugger); + is(gVariablesView.getFocusedItem().name, "__proto__", + "The '__proto__' item should be focused."); + + EventUtils.sendKey("DOWN", gDebugger); + is(gVariablesView.getFocusedItem().name, "__proto__", + "The '__proto__' item should be focused."); + + // Part 8: Test that the RIGHT key expands elements as intended. + + EventUtils.sendKey("PAGE_UP", gDebugger); + is(gVariablesView.getFocusedItem().name, "someProp5", + "The 'someProp5' item should be focused."); + is(gVariablesView.getFocusedItem().expanded, false, + "The 'someProp5' item should not be expanded yet."); + + yield synthesizeKeyAndWaitForTick("VK_RIGHT", {}); + is(gVariablesView.getFocusedItem().name, "someProp5", + "The 'someProp5' item should be focused."); + is(gVariablesView.getFocusedItem().expanded, true, + "The 'someProp5' item should now be expanded."); + is(gVariablesView.getFocusedItem()._store.size, 9, + "There should be 9 properties in the selected variable."); + is(gVariablesView.getFocusedItem()._enumItems.length, 7, + "There should be 7 enumerable properties in the selected variable."); + is(gVariablesView.getFocusedItem()._nonEnumItems.length, 2, + "There should be 2 non-enumerable properties in the selected variable."); + + yield waitForChildNodes(gVariablesView.getFocusedItem()._enum, 7); + yield waitForChildNodes(gVariablesView.getFocusedItem()._nonenum, 2); + + EventUtils.sendKey("RIGHT", gDebugger); + is(gVariablesView.getFocusedItem().name, "0", + "The '0' item should be focused."); + + EventUtils.sendKey("RIGHT", gDebugger); + is(gVariablesView.getFocusedItem().name, "0", + "The '0' item should still be focused."); + + EventUtils.sendKey("PAGE_DOWN", gDebugger); + is(gVariablesView.getFocusedItem().name, "5", + "The '5' item should be focused."); + is(gVariablesView.getFocusedItem().expanded, false, + "The '5' item should not be expanded yet."); + + yield synthesizeKeyAndWaitForTick("VK_RIGHT", {}); + is(gVariablesView.getFocusedItem().name, "5", + "The '5' item should be focused."); + is(gVariablesView.getFocusedItem().expanded, true, + "The '5' item should now be expanded."); + is(gVariablesView.getFocusedItem()._store.size, 5, + "There should be 5 properties in the selected variable."); + is(gVariablesView.getFocusedItem()._enumItems.length, 3, + "There should be 3 enumerable properties in the selected variable."); + is(gVariablesView.getFocusedItem()._nonEnumItems.length, 2, + "There should be 2 non-enumerable properties in the selected variable."); + + yield waitForChildNodes(gVariablesView.getFocusedItem()._enum, 3); + yield waitForChildNodes(gVariablesView.getFocusedItem()._nonenum, 2); + + EventUtils.sendKey("RIGHT", gDebugger); + is(gVariablesView.getFocusedItem().name, "0", + "The '0' item should be focused."); + + EventUtils.sendKey("RIGHT", gDebugger); + is(gVariablesView.getFocusedItem().name, "0", + "The '0' item should still be focused."); + + EventUtils.sendKey("PAGE_DOWN", gDebugger); + is(gVariablesView.getFocusedItem().name, "6", + "The '6' item should be focused."); + is(gVariablesView.getFocusedItem().expanded, false, + "The '6' item should not be expanded yet."); + + yield synthesizeKeyAndWaitForTick("VK_RIGHT", {}); + is(gVariablesView.getFocusedItem().name, "6", + "The '6' item should be focused."); + is(gVariablesView.getFocusedItem().expanded, true, + "The '6' item should now be expanded."); + is(gVariablesView.getFocusedItem()._store.size, 3, + "There should be 3 properties in the selected variable."); + is(gVariablesView.getFocusedItem()._enumItems.length, 2, + "There should be 2 enumerable properties in the selected variable."); + is(gVariablesView.getFocusedItem()._nonEnumItems.length, 1, + "There should be 1 non-enumerable properties in the selected variable."); + + yield waitForChildNodes(gVariablesView.getFocusedItem()._enum, 2); + yield waitForChildNodes(gVariablesView.getFocusedItem()._nonenum, 1); + + EventUtils.sendKey("RIGHT", gDebugger); + is(gVariablesView.getFocusedItem().name, "prop1", + "The 'prop1' item should be focused."); + + EventUtils.sendKey("RIGHT", gDebugger); + is(gVariablesView.getFocusedItem().name, "prop1", + "The 'prop1' item should still be focused."); + + EventUtils.sendKey("PAGE_DOWN", gDebugger); + is(gVariablesView.getFocusedItem().name, "someProp6", + "The 'someProp6' item should be focused."); + is(gVariablesView.getFocusedItem().expanded, false, + "The 'someProp6' item should not be expanded yet."); + + // Part 9: Test that the RIGHT key collapses elements as intended. + + EventUtils.sendKey("LEFT", gDebugger); + is(gVariablesView.getFocusedItem().name, "someProp6", + "The 'someProp6' item should be focused."); + + EventUtils.sendKey("UP", gDebugger); + is(gVariablesView.getFocusedItem().name, "__proto__", + "The '__proto__' item should be focused."); + + EventUtils.sendKey("LEFT", gDebugger); + is(gVariablesView.getFocusedItem().name, "someProp5", + "The 'someProp5' item should be focused."); + is(gVariablesView.getFocusedItem().expanded, true, + "The '6' item should still be expanded."); + + EventUtils.sendKey("LEFT", gDebugger); + is(gVariablesView.getFocusedItem().name, "someProp5", + "The 'someProp5' item should still be focused."); + is(gVariablesView.getFocusedItem().expanded, false, + "The '6' item should still not be expanded anymore."); + + EventUtils.sendKey("LEFT", gDebugger); + is(gVariablesView.getFocusedItem().name, "someProp5", + "The 'someProp5' item should still be focused."); + + // Part 9: Test continuous navigation. + + EventUtils.sendKey("UP", gDebugger); + is(gVariablesView.getFocusedItem().name, "someProp4", + "The 'someProp4' item should be focused."); + + EventUtils.sendKey("UP", gDebugger); + is(gVariablesView.getFocusedItem().name, "someProp3", + "The 'someProp3' item should be focused."); + + EventUtils.sendKey("UP", gDebugger); + is(gVariablesView.getFocusedItem().name, "someProp2", + "The 'someProp2' item should be focused."); + + EventUtils.sendKey("UP", gDebugger); + is(gVariablesView.getFocusedItem().name, "someProp1", + "The 'someProp1' item should be focused."); + + EventUtils.sendKey("UP", gDebugger); + is(gVariablesView.getFocusedItem().name, "someProp0", + "The 'someProp0' item should be focused."); + + EventUtils.sendKey("PAGE_UP", gDebugger); + is(gVariablesView.getFocusedItem().name, "someProp0", + "The 'someProp0' item should be focused."); + + EventUtils.sendKey("PAGE_DOWN", gDebugger); + is(gVariablesView.getFocusedItem().name, "someProp5", + "The 'someProp5' item should be focused."); + + EventUtils.sendKey("DOWN", gDebugger); + is(gVariablesView.getFocusedItem().name, "someProp6", + "The 'someProp6' item should be focused."); + + EventUtils.sendKey("DOWN", gDebugger); + is(gVariablesView.getFocusedItem().name, "someProp7", + "The 'someProp7' item should be focused."); + + EventUtils.sendKey("DOWN", gDebugger); + is(gVariablesView.getFocusedItem().name, "get", + "The 'get' item should be focused."); + + EventUtils.sendKey("DOWN", gDebugger); + is(gVariablesView.getFocusedItem().name, "set", + "The 'set' item should be focused."); + + EventUtils.sendKey("DOWN", gDebugger); + is(gVariablesView.getFocusedItem().name, "__proto__", + "The '__proto__' item should be focused."); + + // Part 10: Test that BACKSPACE deletes items in the variables view. + + EventUtils.sendKey("BACK_SPACE", gDebugger); + is(gVariablesView.getFocusedItem().name, "__proto__", + "The '__proto__' variable should still be focused."); + is(gVariablesView.getFocusedItem().value, "[object Object]", + "The '__proto__' variable should not have an empty value."); + is(gVariablesView.getFocusedItem().visible, false, + "The '__proto__' variable should be hidden."); + + EventUtils.sendKey("UP", gDebugger); + is(gVariablesView.getFocusedItem().name, "set", + "The 'set' item should be focused."); + is(gVariablesView.getFocusedItem().value, "[object Object]", + "The 'set' item should not have an empty value."); + is(gVariablesView.getFocusedItem().visible, true, + "The 'set' item should be visible."); + + EventUtils.sendKey("BACK_SPACE", gDebugger); + is(gVariablesView.getFocusedItem().name, "set", + "The 'set' item should still be focused."); + is(gVariablesView.getFocusedItem().value, "[object Object]", + "The 'set' item should not have an empty value."); + is(gVariablesView.getFocusedItem().visible, true, + "The 'set' item should be visible."); + is(gVariablesView.getFocusedItem().twisty, false, + "The 'set' item should be disabled and have a hidden twisty."); + + EventUtils.sendKey("UP", gDebugger); + is(gVariablesView.getFocusedItem().name, "get", + "The 'get' item should be focused."); + is(gVariablesView.getFocusedItem().value, "[object Object]", + "The 'get' item should not have an empty value."); + is(gVariablesView.getFocusedItem().visible, true, + "The 'get' item should be visible."); + + EventUtils.sendKey("BACK_SPACE", gDebugger); + is(gVariablesView.getFocusedItem().name, "get", + "The 'get' item should still be focused."); + is(gVariablesView.getFocusedItem().value, "[object Object]", + "The 'get' item should not have an empty value."); + is(gVariablesView.getFocusedItem().visible, true, + "The 'get' item should be visible."); + is(gVariablesView.getFocusedItem().twisty, false, + "The 'get' item should be disabled and have a hidden twisty."); + + EventUtils.sendKey("UP", gDebugger); + is(gVariablesView.getFocusedItem().name, "someProp7", + "The 'someProp7' item should be focused."); + is(gVariablesView.getFocusedItem().value, undefined, + "The 'someProp7' variable should have an empty value."); + is(gVariablesView.getFocusedItem().visible, true, + "The 'someProp7' variable should be visible."); + + EventUtils.sendKey("BACK_SPACE", gDebugger); + is(gVariablesView.getFocusedItem().name, "someProp7", + "The 'someProp7' variable should still be focused."); + is(gVariablesView.getFocusedItem().value, undefined, + "The 'someProp7' variable should have an empty value."); + is(gVariablesView.getFocusedItem().visible, false, + "The 'someProp7' variable should be hidden."); + + // Part 11: Test that Ctrl-C copies the current item to the system clipboard + + gVariablesView.focusFirstVisibleItem(); + let copied = promise.defer(); + let expectedValue = gVariablesView.getFocusedItem().name + + gVariablesView.getFocusedItem().separatorStr + + gVariablesView.getFocusedItem().value; + + waitForClipboard(expectedValue, function setup() { + EventUtils.synthesizeKey("C", { metaKey: true }, gDebugger); + }, copied.resolve, copied.reject + ); + + try { + yield copied.promise; + ok(true, + "Ctrl-C copied the selected item to the clipboard."); + } catch (e) { + ok(false, + "Ctrl-C didn't copy the selected item to the clipboard."); + } + + yield closeDebuggerAndFinish(gPanel); + }); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gVariablesView = null; +}); + +function synthesizeKeyAndWaitForElement(aKey, aModifiers, aSelector, aExistence) { + EventUtils.synthesizeKey(aKey, aModifiers, gDebugger); + return waitForElement(aSelector, aExistence); +} + +function synthesizeKeyAndWaitForTick(aKey, aModifiers) { + EventUtils.synthesizeKey(aKey, aModifiers, gDebugger); + return waitForTick(); +} + +function waitForElement(aSelector, aExistence) { + return waitForPredicate(() => { + return !!gVariablesView._list.querySelector(aSelector) == aExistence; + }); +} + +function waitForChildNodes(aTarget, aCount) { + return waitForPredicate(() => { + return aTarget.childNodes.length == aCount; + }); +} + +function waitForPredicate(aPredicate, aInterval = 10) { + let deferred = promise.defer(); + + // Poll every few milliseconds until the element is retrieved. + let count = 0; + let intervalID = window.setInterval(() => { + // Make sure we don't wait for too long. + if (++count > 1000) { + deferred.reject("Timed out while polling for the element."); + window.clearInterval(intervalID); + return; + } + // Check if the predicate condition is fulfilled. + if (!aPredicate()) { + return; + } + // We got the element, it's safe to callback. + window.clearInterval(intervalID); + deferred.resolve(); + }, aInterval); + + return deferred.promise; +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-data.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-data.js new file mode 100644 index 000000000..be2446fb6 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-data.js @@ -0,0 +1,609 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that the variables view correctly populates itself + * when given some raw data. + */ + +let gTab, gPanel, gDebugger; +let gVariablesView, gScope, gVariable; + +function test() { + initDebugger("about:blank").then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gVariablesView = gDebugger.DebuggerView.Variables; + + performTest(); + }); +} + +function performTest() { + let arr = [ + 42, + true, + "nasu", + undefined, + null, + [0, 1, 2], + { prop1: 9, prop2: 8 } + ]; + + let obj = { + p0: 42, + p1: true, + p2: "nasu", + p3: undefined, + p4: null, + p5: [3, 4, 5], + p6: { prop1: 7, prop2: 6 }, + get p7() { return arr; }, + set p8(value) { arr[0] = value } + }; + + let test = { + someProp0: 42, + someProp1: true, + someProp2: "nasu", + someProp3: undefined, + someProp4: null, + someProp5: arr, + someProp6: obj, + get someProp7() { return arr; }, + set someProp7(value) { arr[0] = value } + }; + + gVariablesView.eval = function() {}; + gVariablesView.switch = function() {}; + gVariablesView.delete = function() {}; + gVariablesView.new = function() {}; + gVariablesView.rawObject = test; + + testHierarchy(); + testHeader(); + testFirstLevelContents(); + testSecondLevelContents(); + testThirdLevelContents(); + testOriginalRawDataIntegrity(arr, obj); + + let fooScope = gVariablesView.addScope("foo"); + let anonymousVar = fooScope.addItem(); + + let anonymousScope = gVariablesView.addScope(); + let barVar = anonymousScope.addItem("bar"); + let bazProperty = barVar.addItem("baz"); + + testAnonymousHeaders(fooScope, anonymousVar, anonymousScope, barVar, bazProperty); + testPropertyInheritance(fooScope, anonymousVar, anonymousScope, barVar, bazProperty); + + testClearHierarchy(); + closeDebuggerAndFinish(gPanel); +} + +function testHierarchy() { + is(gVariablesView._currHierarchy.size, 13, + "There should be 1 scope, 1 var, 1 proto, 8 props, 1 getter and 1 setter."); + + gScope = gVariablesView._currHierarchy.get(""); + gVariable = gVariablesView._currHierarchy.get("[\"\"]"); + + is(gVariablesView._store.length, 1, + "There should be only one scope in the view."); + is(gScope._store.size, 1, + "There should be only one variable in the scope."); + is(gVariable._store.size, 9, + "There should be 1 __proto__ and 8 properties in the variable."); +} + +function testHeader() { + is(gScope.header, false, + "The scope title header should be hidden."); + is(gVariable.header, false, + "The variable title header should be hidden."); + + gScope.showHeader(); + gVariable.showHeader(); + + is(gScope.header, false, + "The scope title header should still not be visible."); + is(gVariable.header, false, + "The variable title header should still not be visible."); + + gScope.hideHeader(); + gVariable.hideHeader(); + + is(gScope.header, false, + "The scope title header should now still be hidden."); + is(gVariable.header, false, + "The variable title header should now still be hidden."); +} + +function testFirstLevelContents() { + let someProp0 = gVariable.get("someProp0"); + let someProp1 = gVariable.get("someProp1"); + let someProp2 = gVariable.get("someProp2"); + let someProp3 = gVariable.get("someProp3"); + let someProp4 = gVariable.get("someProp4"); + let someProp5 = gVariable.get("someProp5"); + let someProp6 = gVariable.get("someProp6"); + let someProp7 = gVariable.get("someProp7"); + let __proto__ = gVariable.get("__proto__"); + + is(someProp0.visible, true, "The first property visible state is correct."); + is(someProp1.visible, true, "The second property visible state is correct."); + is(someProp2.visible, true, "The third property visible state is correct."); + is(someProp3.visible, true, "The fourth property visible state is correct."); + is(someProp4.visible, true, "The fifth property visible state is correct."); + is(someProp5.visible, true, "The sixth property visible state is correct."); + is(someProp6.visible, true, "The seventh property visible state is correct."); + is(someProp7.visible, true, "The eight property visible state is correct."); + is(__proto__.visible, true, "The __proto__ property visible state is correct."); + + is(someProp0.expanded, false, "The first property expanded state is correct."); + is(someProp1.expanded, false, "The second property expanded state is correct."); + is(someProp2.expanded, false, "The third property expanded state is correct."); + is(someProp3.expanded, false, "The fourth property expanded state is correct."); + is(someProp4.expanded, false, "The fifth property expanded state is correct."); + is(someProp5.expanded, false, "The sixth property expanded state is correct."); + is(someProp6.expanded, false, "The seventh property expanded state is correct."); + is(someProp7.expanded, true, "The eight property expanded state is correct."); + is(__proto__.expanded, false, "The __proto__ property expanded state is correct."); + + is(someProp0.header, true, "The first property header state is correct."); + is(someProp1.header, true, "The second property header state is correct."); + is(someProp2.header, true, "The third property header state is correct."); + is(someProp3.header, true, "The fourth property header state is correct."); + is(someProp4.header, true, "The fifth property header state is correct."); + is(someProp5.header, true, "The sixth property header state is correct."); + is(someProp6.header, true, "The seventh property header state is correct."); + is(someProp7.header, true, "The eight property header state is correct."); + is(__proto__.header, true, "The __proto__ property header state is correct."); + + is(someProp0.twisty, false, "The first property twisty state is correct."); + is(someProp1.twisty, false, "The second property twisty state is correct."); + is(someProp2.twisty, false, "The third property twisty state is correct."); + is(someProp3.twisty, false, "The fourth property twisty state is correct."); + is(someProp4.twisty, false, "The fifth property twisty state is correct."); + is(someProp5.twisty, true, "The sixth property twisty state is correct."); + is(someProp6.twisty, true, "The seventh property twisty state is correct."); + is(someProp7.twisty, true, "The eight property twisty state is correct."); + is(__proto__.twisty, true, "The __proto__ property twisty state is correct."); + + is(someProp0.name, "someProp0", "The first property name is correct."); + is(someProp1.name, "someProp1", "The second property name is correct."); + is(someProp2.name, "someProp2", "The third property name is correct."); + is(someProp3.name, "someProp3", "The fourth property name is correct."); + is(someProp4.name, "someProp4", "The fifth property name is correct."); + is(someProp5.name, "someProp5", "The sixth property name is correct."); + is(someProp6.name, "someProp6", "The seventh property name is correct."); + is(someProp7.name, "someProp7", "The eight property name is correct."); + is(__proto__.name, "__proto__", "The __proto__ property name is correct."); + + is(someProp0.value, 42, "The first property value is correct."); + is(someProp1.value, true, "The second property value is correct."); + is(someProp2.value, "nasu", "The third property value is correct."); + is(someProp3.value.type, "undefined", "The fourth property value is correct."); + is(someProp4.value.type, "null", "The fifth property value is correct."); + is(someProp5.value.type, "object", "The sixth property value type is correct."); + is(someProp5.value.class, "Array", "The sixth property value class is correct."); + is(someProp6.value.type, "object", "The seventh property value type is correct."); + is(someProp6.value.class, "Object", "The seventh property value class is correct."); + is(someProp7.value, null, "The eight property value is correct."); + isnot(someProp7.getter, null, "The eight property getter is correct."); + isnot(someProp7.setter, null, "The eight property setter is correct."); + is(someProp7.getter.type, "object", "The eight property getter type is correct."); + is(someProp7.getter.class, "Function", "The eight property getter class is correct."); + is(someProp7.setter.type, "object", "The eight property setter type is correct."); + is(someProp7.setter.class, "Function", "The eight property setter class is correct."); + is(__proto__.value.type, "object", "The __proto__ property value type is correct."); + is(__proto__.value.class, "Object", "The __proto__ property value class is correct."); + + someProp0.expand(); + someProp1.expand(); + someProp2.expand(); + someProp3.expand(); + someProp4.expand(); + someProp7.expand(); + + ok(!someProp0.get("__proto__"), "Number primitives should not have a prototype"); + ok(!someProp1.get("__proto__"), "Boolean primitives should not have a prototype"); + ok(!someProp2.get("__proto__"), "String literals should not have a prototype"); + ok(!someProp3.get("__proto__"), "Undefined values should not have a prototype"); + ok(!someProp4.get("__proto__"), "Null values should not have a prototype"); + ok(!someProp7.get("__proto__"), "Getter properties should not have a prototype"); +} + +function testSecondLevelContents() { + let someProp5 = gVariable.get("someProp5"); + let someProp6 = gVariable.get("someProp6"); + + is(someProp5._store.size, 0, "No properties should be in someProp5 before expanding"); + someProp5.expand(); + is(someProp5._store.size, 9, "Some properties should be in someProp5 before expanding"); + + let arrayItem0 = someProp5.get("0"); + let arrayItem1 = someProp5.get("1"); + let arrayItem2 = someProp5.get("2"); + let arrayItem3 = someProp5.get("3"); + let arrayItem4 = someProp5.get("4"); + let arrayItem5 = someProp5.get("5"); + let arrayItem6 = someProp5.get("6"); + let __proto__ = someProp5.get("__proto__"); + + is(arrayItem0.visible, true, "The first array item visible state is correct."); + is(arrayItem1.visible, true, "The second array item visible state is correct."); + is(arrayItem2.visible, true, "The third array item visible state is correct."); + is(arrayItem3.visible, true, "The fourth array item visible state is correct."); + is(arrayItem4.visible, true, "The fifth array item visible state is correct."); + is(arrayItem5.visible, true, "The sixth array item visible state is correct."); + is(arrayItem6.visible, true, "The seventh array item visible state is correct."); + is(__proto__.visible, true, "The __proto__ property visible state is correct."); + + is(arrayItem0.expanded, false, "The first array item expanded state is correct."); + is(arrayItem1.expanded, false, "The second array item expanded state is correct."); + is(arrayItem2.expanded, false, "The third array item expanded state is correct."); + is(arrayItem3.expanded, false, "The fourth array item expanded state is correct."); + is(arrayItem4.expanded, false, "The fifth array item expanded state is correct."); + is(arrayItem5.expanded, false, "The sixth array item expanded state is correct."); + is(arrayItem6.expanded, false, "The seventh array item expanded state is correct."); + is(__proto__.expanded, false, "The __proto__ property expanded state is correct."); + + is(arrayItem0.header, true, "The first array item header state is correct."); + is(arrayItem1.header, true, "The second array item header state is correct."); + is(arrayItem2.header, true, "The third array item header state is correct."); + is(arrayItem3.header, true, "The fourth array item header state is correct."); + is(arrayItem4.header, true, "The fifth array item header state is correct."); + is(arrayItem5.header, true, "The sixth array item header state is correct."); + is(arrayItem6.header, true, "The seventh array item header state is correct."); + is(__proto__.header, true, "The __proto__ property header state is correct."); + + is(arrayItem0.twisty, false, "The first array item twisty state is correct."); + is(arrayItem1.twisty, false, "The second array item twisty state is correct."); + is(arrayItem2.twisty, false, "The third array item twisty state is correct."); + is(arrayItem3.twisty, false, "The fourth array item twisty state is correct."); + is(arrayItem4.twisty, false, "The fifth array item twisty state is correct."); + is(arrayItem5.twisty, true, "The sixth array item twisty state is correct."); + is(arrayItem6.twisty, true, "The seventh array item twisty state is correct."); + is(__proto__.twisty, true, "The __proto__ property twisty state is correct."); + + is(arrayItem0.name, "0", "The first array item name is correct."); + is(arrayItem1.name, "1", "The second array item name is correct."); + is(arrayItem2.name, "2", "The third array item name is correct."); + is(arrayItem3.name, "3", "The fourth array item name is correct."); + is(arrayItem4.name, "4", "The fifth array item name is correct."); + is(arrayItem5.name, "5", "The sixth array item name is correct."); + is(arrayItem6.name, "6", "The seventh array item name is correct."); + is(__proto__.name, "__proto__", "The __proto__ property name is correct."); + + is(arrayItem0.value, 42, "The first array item value is correct."); + is(arrayItem1.value, true, "The second array item value is correct."); + is(arrayItem2.value, "nasu", "The third array item value is correct."); + is(arrayItem3.value.type, "undefined", "The fourth array item value is correct."); + is(arrayItem4.value.type, "null", "The fifth array item value is correct."); + is(arrayItem5.value.type, "object", "The sixth array item value type is correct."); + is(arrayItem5.value.class, "Array", "The sixth array item value class is correct."); + is(arrayItem6.value.type, "object", "The seventh array item value type is correct."); + is(arrayItem6.value.class, "Object", "The seventh array item value class is correct."); + is(__proto__.value.type, "object", "The __proto__ property value type is correct."); + is(__proto__.value.class, "Array", "The __proto__ property value class is correct."); + + is(someProp6._store.size, 0, "No properties should be in someProp6 before expanding"); + someProp6.expand(); + is(someProp6._store.size, 10, "Some properties should be in someProp6 before expanding"); + + let objectItem0 = someProp6.get("p0"); + let objectItem1 = someProp6.get("p1"); + let objectItem2 = someProp6.get("p2"); + let objectItem3 = someProp6.get("p3"); + let objectItem4 = someProp6.get("p4"); + let objectItem5 = someProp6.get("p5"); + let objectItem6 = someProp6.get("p6"); + let objectItem7 = someProp6.get("p7"); + let objectItem8 = someProp6.get("p8"); + __proto__ = someProp6.get("__proto__"); + + is(objectItem0.visible, true, "The first object item visible state is correct."); + is(objectItem1.visible, true, "The second object item visible state is correct."); + is(objectItem2.visible, true, "The third object item visible state is correct."); + is(objectItem3.visible, true, "The fourth object item visible state is correct."); + is(objectItem4.visible, true, "The fifth object item visible state is correct."); + is(objectItem5.visible, true, "The sixth object item visible state is correct."); + is(objectItem6.visible, true, "The seventh object item visible state is correct."); + is(objectItem7.visible, true, "The eight object item visible state is correct."); + is(objectItem8.visible, true, "The ninth object item visible state is correct."); + is(__proto__.visible, true, "The __proto__ property visible state is correct."); + + is(objectItem0.expanded, false, "The first object item expanded state is correct."); + is(objectItem1.expanded, false, "The second object item expanded state is correct."); + is(objectItem2.expanded, false, "The third object item expanded state is correct."); + is(objectItem3.expanded, false, "The fourth object item expanded state is correct."); + is(objectItem4.expanded, false, "The fifth object item expanded state is correct."); + is(objectItem5.expanded, false, "The sixth object item expanded state is correct."); + is(objectItem6.expanded, false, "The seventh object item expanded state is correct."); + is(objectItem7.expanded, true, "The eight object item expanded state is correct."); + is(objectItem8.expanded, true, "The ninth object item expanded state is correct."); + is(__proto__.expanded, false, "The __proto__ property expanded state is correct."); + + is(objectItem0.header, true, "The first object item header state is correct."); + is(objectItem1.header, true, "The second object item header state is correct."); + is(objectItem2.header, true, "The third object item header state is correct."); + is(objectItem3.header, true, "The fourth object item header state is correct."); + is(objectItem4.header, true, "The fifth object item header state is correct."); + is(objectItem5.header, true, "The sixth object item header state is correct."); + is(objectItem6.header, true, "The seventh object item header state is correct."); + is(objectItem7.header, true, "The eight object item header state is correct."); + is(objectItem8.header, true, "The ninth object item header state is correct."); + is(__proto__.header, true, "The __proto__ property header state is correct."); + + is(objectItem0.twisty, false, "The first object item twisty state is correct."); + is(objectItem1.twisty, false, "The second object item twisty state is correct."); + is(objectItem2.twisty, false, "The third object item twisty state is correct."); + is(objectItem3.twisty, false, "The fourth object item twisty state is correct."); + is(objectItem4.twisty, false, "The fifth object item twisty state is correct."); + is(objectItem5.twisty, true, "The sixth object item twisty state is correct."); + is(objectItem6.twisty, true, "The seventh object item twisty state is correct."); + is(objectItem7.twisty, true, "The eight object item twisty state is correct."); + is(objectItem8.twisty, true, "The ninth object item twisty state is correct."); + is(__proto__.twisty, true, "The __proto__ property twisty state is correct."); + + is(objectItem0.name, "p0", "The first object item name is correct."); + is(objectItem1.name, "p1", "The second object item name is correct."); + is(objectItem2.name, "p2", "The third object item name is correct."); + is(objectItem3.name, "p3", "The fourth object item name is correct."); + is(objectItem4.name, "p4", "The fifth object item name is correct."); + is(objectItem5.name, "p5", "The sixth object item name is correct."); + is(objectItem6.name, "p6", "The seventh object item name is correct."); + is(objectItem7.name, "p7", "The eight seventh object item name is correct."); + is(objectItem8.name, "p8", "The ninth seventh object item name is correct."); + is(__proto__.name, "__proto__", "The __proto__ property name is correct."); + + is(objectItem0.value, 42, "The first object item value is correct."); + is(objectItem1.value, true, "The second object item value is correct."); + is(objectItem2.value, "nasu", "The third object item value is correct."); + is(objectItem3.value.type, "undefined", "The fourth object item value is correct."); + is(objectItem4.value.type, "null", "The fifth object item value is correct."); + is(objectItem5.value.type, "object", "The sixth object item value type is correct."); + is(objectItem5.value.class, "Array", "The sixth object item value class is correct."); + is(objectItem6.value.type, "object", "The seventh object item value type is correct."); + is(objectItem6.value.class, "Object", "The seventh object item value class is correct."); + is(objectItem7.value, null, "The eight object item value is correct."); + isnot(objectItem7.getter, null, "The eight object item getter is correct."); + isnot(objectItem7.setter, null, "The eight object item setter is correct."); + is(objectItem7.setter.type, "undefined", "The eight object item setter type is correct."); + is(objectItem7.getter.type, "object", "The eight object item getter type is correct."); + is(objectItem7.getter.class, "Function", "The eight object item getter class is correct."); + is(objectItem8.value, null, "The ninth object item value is correct."); + isnot(objectItem8.getter, null, "The ninth object item getter is correct."); + isnot(objectItem8.setter, null, "The ninth object item setter is correct."); + is(objectItem8.getter.type, "undefined", "The eight object item getter type is correct."); + is(objectItem8.setter.type, "object", "The ninth object item setter type is correct."); + is(objectItem8.setter.class, "Function", "The ninth object item setter class is correct."); + is(__proto__.value.type, "object", "The __proto__ property value type is correct."); + is(__proto__.value.class, "Object", "The __proto__ property value class is correct."); +} + +function testThirdLevelContents() { + (function() { + let someProp5 = gVariable.get("someProp5"); + let arrayItem5 = someProp5.get("5"); + let arrayItem6 = someProp5.get("6"); + + is(arrayItem5._store.size, 0, "No properties should be in arrayItem5 before expanding"); + arrayItem5.expand(); + is(arrayItem5._store.size, 5, "Some properties should be in arrayItem5 before expanding"); + + is(arrayItem6._store.size, 0, "No properties should be in arrayItem6 before expanding"); + arrayItem6.expand(); + is(arrayItem6._store.size, 3, "Some properties should be in arrayItem6 before expanding"); + + let arraySubItem0 = arrayItem5.get("0"); + let arraySubItem1 = arrayItem5.get("1"); + let arraySubItem2 = arrayItem5.get("2"); + let objectSubItem0 = arrayItem6.get("prop1"); + let objectSubItem1 = arrayItem6.get("prop2"); + + is(arraySubItem0.value, 0, "The first array sub-item value is correct."); + is(arraySubItem1.value, 1, "The second array sub-item value is correct."); + is(arraySubItem2.value, 2, "The third array sub-item value is correct."); + + is(objectSubItem0.value, 9, "The first object sub-item value is correct."); + is(objectSubItem1.value, 8, "The second object sub-item value is correct."); + + let array__proto__ = arrayItem5.get("__proto__"); + let object__proto__ = arrayItem6.get("__proto__"); + + ok(array__proto__, "The array should have a __proto__ property."); + ok(object__proto__, "The object should have a __proto__ property."); + })(); + + (function() { + let someProp6 = gVariable.get("someProp6"); + let objectItem5 = someProp6.get("p5"); + let objectItem6 = someProp6.get("p6"); + + is(objectItem5._store.size, 0, "No properties should be in objectItem5 before expanding"); + objectItem5.expand(); + is(objectItem5._store.size, 5, "Some properties should be in objectItem5 before expanding"); + + is(objectItem6._store.size, 0, "No properties should be in objectItem6 before expanding"); + objectItem6.expand(); + is(objectItem6._store.size, 3, "Some properties should be in objectItem6 before expanding"); + + let arraySubItem0 = objectItem5.get("0"); + let arraySubItem1 = objectItem5.get("1"); + let arraySubItem2 = objectItem5.get("2"); + let objectSubItem0 = objectItem6.get("prop1"); + let objectSubItem1 = objectItem6.get("prop2"); + + is(arraySubItem0.value, 3, "The first array sub-item value is correct."); + is(arraySubItem1.value, 4, "The second array sub-item value is correct."); + is(arraySubItem2.value, 5, "The third array sub-item value is correct."); + + is(objectSubItem0.value, 7, "The first object sub-item value is correct."); + is(objectSubItem1.value, 6, "The second object sub-item value is correct."); + + let array__proto__ = objectItem5.get("__proto__"); + let object__proto__ = objectItem6.get("__proto__"); + + ok(array__proto__, "The array should have a __proto__ property."); + ok(object__proto__, "The object should have a __proto__ property."); + })(); +} + +function testOriginalRawDataIntegrity(arr, obj) { + is(arr[0], 42, "The first array item should not have changed."); + is(arr[1], true, "The second array item should not have changed."); + is(arr[2], "nasu", "The third array item should not have changed."); + is(arr[3], undefined, "The fourth array item should not have changed."); + is(arr[4], null, "The fifth array item should not have changed."); + ok(arr[5] instanceof Array, "The sixth array item should be an Array."); + is(arr[5][0], 0, "The sixth array item should not have changed."); + is(arr[5][1], 1, "The sixth array item should not have changed."); + is(arr[5][2], 2, "The sixth array item should not have changed."); + ok(arr[6] instanceof Object, "The seventh array item should be an Object."); + is(arr[6].prop1, 9, "The seventh array item should not have changed."); + is(arr[6].prop2, 8, "The seventh array item should not have changed."); + + is(obj.p0, 42, "The first object property should not have changed."); + is(obj.p1, true, "The first object property should not have changed."); + is(obj.p2, "nasu", "The first object property should not have changed."); + is(obj.p3, undefined, "The first object property should not have changed."); + is(obj.p4, null, "The first object property should not have changed."); + ok(obj.p5 instanceof Array, "The sixth object property should be an Array."); + is(obj.p5[0], 3, "The sixth object property should not have changed."); + is(obj.p5[1], 4, "The sixth object property should not have changed."); + is(obj.p5[2], 5, "The sixth object property should not have changed."); + ok(obj.p6 instanceof Object, "The seventh object property should be an Object."); + is(obj.p6.prop1, 7, "The seventh object property should not have changed."); + is(obj.p6.prop2, 6, "The seventh object property should not have changed."); +} + +function testAnonymousHeaders(fooScope, anonymousVar, anonymousScope, barVar, bazProperty) { + is(fooScope.header, true, + "A named scope should have a header visible."); + is(fooScope.target.hasAttribute("untitled"), false, + "The non-header attribute should not be applied to scopes with headers."); + + is(anonymousScope.header, false, + "An anonymous scope should have a header visible."); + is(anonymousScope.target.hasAttribute("untitled"), true, + "The non-header attribute should not be applied to scopes without headers."); + + is(barVar.header, true, + "A named variable should have a header visible."); + is(barVar.target.hasAttribute("untitled"), false, + "The non-header attribute should not be applied to variables with headers."); + + is(anonymousVar.header, false, + "An anonymous variable should have a header visible."); + is(anonymousVar.target.hasAttribute("untitled"), true, + "The non-header attribute should not be applied to variables without headers."); +} + +function testPropertyInheritance(fooScope, anonymousVar, anonymousScope, barVar, bazProperty) { + is(fooScope.preventDisableOnChange, gVariablesView.preventDisableOnChange, + "The preventDisableOnChange property should persist from the view to all scopes."); + is(fooScope.preventDescriptorModifiers, gVariablesView.preventDescriptorModifiers, + "The preventDescriptorModifiers property should persist from the view to all scopes."); + is(fooScope.editableNameTooltip, gVariablesView.editableNameTooltip, + "The editableNameTooltip property should persist from the view to all scopes."); + is(fooScope.editableValueTooltip, gVariablesView.editableValueTooltip, + "The editableValueTooltip property should persist from the view to all scopes."); + is(fooScope.editButtonTooltip, gVariablesView.editButtonTooltip, + "The editButtonTooltip property should persist from the view to all scopes."); + is(fooScope.deleteButtonTooltip, gVariablesView.deleteButtonTooltip, + "The deleteButtonTooltip property should persist from the view to all scopes."); + is(fooScope.contextMenuId, gVariablesView.contextMenuId, + "The contextMenuId property should persist from the view to all scopes."); + is(fooScope.separatorStr, gVariablesView.separatorStr, + "The separatorStr property should persist from the view to all scopes."); + is(fooScope.eval, gVariablesView.eval, + "The eval property should persist from the view to all scopes."); + is(fooScope.switch, gVariablesView.switch, + "The switch property should persist from the view to all scopes."); + is(fooScope.delete, gVariablesView.delete, + "The delete property should persist from the view to all scopes."); + is(fooScope.new, gVariablesView.new, + "The new property should persist from the view to all scopes."); + isnot(fooScope.eval, fooScope.switch, + "The eval and switch functions got mixed up in the scope."); + isnot(fooScope.switch, fooScope.delete, + "The eval and switch functions got mixed up in the scope."); + + is(barVar.preventDisableOnChange, gVariablesView.preventDisableOnChange, + "The preventDisableOnChange property should persist from the view to all variables."); + is(barVar.preventDescriptorModifiers, gVariablesView.preventDescriptorModifiers, + "The preventDescriptorModifiers property should persist from the view to all variables."); + is(barVar.editableNameTooltip, gVariablesView.editableNameTooltip, + "The editableNameTooltip property should persist from the view to all variables."); + is(barVar.editableValueTooltip, gVariablesView.editableValueTooltip, + "The editableValueTooltip property should persist from the view to all variables."); + is(barVar.editButtonTooltip, gVariablesView.editButtonTooltip, + "The editButtonTooltip property should persist from the view to all variables."); + is(barVar.deleteButtonTooltip, gVariablesView.deleteButtonTooltip, + "The deleteButtonTooltip property should persist from the view to all variables."); + is(barVar.contextMenuId, gVariablesView.contextMenuId, + "The contextMenuId property should persist from the view to all variables."); + is(barVar.separatorStr, gVariablesView.separatorStr, + "The separatorStr property should persist from the view to all variables."); + is(barVar.eval, gVariablesView.eval, + "The eval property should persist from the view to all variables."); + is(barVar.switch, gVariablesView.switch, + "The switch property should persist from the view to all variables."); + is(barVar.delete, gVariablesView.delete, + "The delete property should persist from the view to all variables."); + is(barVar.new, gVariablesView.new, + "The new property should persist from the view to all variables."); + isnot(barVar.eval, barVar.switch, + "The eval and switch functions got mixed up in the variable."); + isnot(barVar.switch, barVar.delete, + "The eval and switch functions got mixed up in the variable."); + + is(bazProperty.preventDisableOnChange, gVariablesView.preventDisableOnChange, + "The preventDisableOnChange property should persist from the view to all properties."); + is(bazProperty.preventDescriptorModifiers, gVariablesView.preventDescriptorModifiers, + "The preventDescriptorModifiers property should persist from the view to all properties."); + is(bazProperty.editableNameTooltip, gVariablesView.editableNameTooltip, + "The editableNameTooltip property should persist from the view to all properties."); + is(bazProperty.editableValueTooltip, gVariablesView.editableValueTooltip, + "The editableValueTooltip property should persist from the view to all properties."); + is(bazProperty.editButtonTooltip, gVariablesView.editButtonTooltip, + "The editButtonTooltip property should persist from the view to all properties."); + is(bazProperty.deleteButtonTooltip, gVariablesView.deleteButtonTooltip, + "The deleteButtonTooltip property should persist from the view to all properties."); + is(bazProperty.contextMenuId, gVariablesView.contextMenuId, + "The contextMenuId property should persist from the view to all properties."); + is(bazProperty.separatorStr, gVariablesView.separatorStr, + "The separatorStr property should persist from the view to all properties."); + is(bazProperty.eval, gVariablesView.eval, + "The eval property should persist from the view to all properties."); + is(bazProperty.switch, gVariablesView.switch, + "The switch property should persist from the view to all properties."); + is(bazProperty.delete, gVariablesView.delete, + "The delete property should persist from the view to all properties."); + is(bazProperty.new, gVariablesView.new, + "The new property should persist from the view to all properties."); + isnot(bazProperty.eval, bazProperty.switch, + "The eval and switch functions got mixed up in the property."); + isnot(bazProperty.switch, bazProperty.delete, + "The eval and switch functions got mixed up in the property."); +} + +function testClearHierarchy() { + gVariablesView.clearHierarchy(); + ok(!gVariablesView._prevHierarchy.size, + "The previous hierarchy should have been cleared."); + ok(!gVariablesView._currHierarchy.size, + "The current hierarchy should have been cleared."); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gVariablesView = null; + gScope = null; + gVariable = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-edit-cancel.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-edit-cancel.js new file mode 100644 index 000000000..7ba20aad1 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-edit-cancel.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Make sure that canceling a name change correctly unhides the separator and
+ * value elements.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_watch-expressions.html";
+
+function test() {
+ Task.spawn(function*() {
+ let [tab,, panel] = yield initDebugger(TAB_URL);
+ let win = panel.panelWin;
+ let vars = win.DebuggerView.Variables;
+
+ win.DebuggerView.WatchExpressions.addExpression("this");
+
+ callInTab(tab, "ermahgerd");
+ yield waitForDebuggerEvents(panel, win.EVENTS.FETCHED_WATCH_EXPRESSIONS);
+
+ let exprScope = vars.getScopeAtIndex(0);
+ let {target} = exprScope.get("this");
+
+ let name = target.querySelector(".title > .name");
+ let separator = target.querySelector(".separator");
+ let value = target.querySelector(".value");
+
+ is(separator.hidden, false,
+ "The separator element should not be hidden.");
+ is(value.hidden, false,
+ "The value element should not be hidden.");
+
+ for (let key of ["ESCAPE", "RETURN"]) {
+ EventUtils.sendMouseEvent({ type: "dblclick" }, name, win);
+
+ is(separator.hidden, true,
+ "The separator element should be hidden.");
+ is(value.hidden, true,
+ "The value element should be hidden.");
+
+ EventUtils.sendKey(key, win);
+
+ is(separator.hidden, false,
+ "The separator element should not be hidden.");
+ is(value.hidden, false,
+ "The value element should not be hidden.");
+ }
+
+ yield resumeDebuggerThenCloseAndFinish(panel);
+ });
+}
diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-edit-click.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-edit-click.js new file mode 100644 index 000000000..25aaf46af --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-edit-click.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check that the editing state of a Variable is correctly tracked. Clicking on + * the textbox while editing should not cancel editing. + */ + +const TAB_URL = EXAMPLE_URL + "doc_watch-expressions.html"; + +function test() { + Task.spawn(function*() { + let [tab, debuggee, panel] = yield initDebugger(TAB_URL); + let win = panel.panelWin; + let vars = win.DebuggerView.Variables; + + win.DebuggerView.WatchExpressions.addExpression("this"); + + // Allow this generator function to yield first. + executeSoon(() => debuggee.ermahgerd()); + yield waitForDebuggerEvents(panel, win.EVENTS.FETCHED_WATCH_EXPRESSIONS); + + let exprScope = vars.getScopeAtIndex(0); + let exprVar = exprScope.get("this"); + let name = exprVar.target.querySelector(".title > .name"); + + is(exprVar.editing, false, + "The expression should indicate it is not being edited."); + + EventUtils.sendMouseEvent({ type: "dblclick" }, name, win); + let input = exprVar.target.querySelector(".title > .element-name-input"); + is(exprVar.editing, true, + "The expression should indicate it is being edited."); + is(input.selectionStart !== input.selectionEnd, true, + "The expression text should be selected."); + + EventUtils.synthesizeMouse(input, 2, 2, {}, win); + is(exprVar.editing, true, + "The expression should indicate it is still being edited after a click."); + is(input.selectionStart === input.selectionEnd, true, + "The expression text should not be selected."); + + EventUtils.sendKey("ESCAPE", win); + is(exprVar.editing, false, + "The expression should indicate it is not being edited after cancelling."); + + // Why is this needed? + EventUtils.synthesizeMouse(vars.parentNode, 2, 2, {}, win); + + yield resumeDebuggerThenCloseAndFinish(panel); + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-edit-getset-01.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-edit-getset-01.js new file mode 100644 index 000000000..d15fd6b2a --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-edit-getset-01.js @@ -0,0 +1,294 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that the variables view knows how to edit getters and setters. + */ + +const TAB_URL = EXAMPLE_URL + "doc_frame-parameters.html"; + +let gTab, gPanel, gDebugger; +let gL10N, gEditor, gVars, gWatch; + +function test() { + // Debug test slaves are a bit slow at this test. + requestLongerTimeout(2); + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gL10N = gDebugger.L10N; + gEditor = gDebugger.DebuggerView.editor; + gVars = gDebugger.DebuggerView.Variables; + gWatch = gDebugger.DebuggerView.WatchExpressions; + + gVars.switch = function() {}; + gVars.delete = function() {}; + + waitForSourceAndCaretAndScopes(gPanel, ".html", 24) + .then(() => addWatchExpressions()) + .then(() => testEdit("set", "this._prop = value + ' BEER CAN'", { + "myVar.prop": "xlerb BEER CAN", + "myVar.prop + 42": "xlerb BEER CAN42", + "myVar.prop = 'xlerb'": "xlerb" + })) + .then(() => testEdit("set", "{ this._prop = value + ' BEACON' }", { + "myVar.prop": "xlerb BEACON", + "myVar.prop + 42": "xlerb BEACON42", + "myVar.prop = 'xlerb'": "xlerb" + })) + .then(() => testEdit("set", "{ this._prop = value + ' BEACON;'; }", { + "myVar.prop": "xlerb BEACON;", + "myVar.prop + 42": "xlerb BEACON;42", + "myVar.prop = 'xlerb'": "xlerb" + })) + .then(() => testEdit("set", "{ return this._prop = value + ' BEACON;;'; }", { + "myVar.prop": "xlerb BEACON;;", + "myVar.prop + 42": "xlerb BEACON;;42", + "myVar.prop = 'xlerb'": "xlerb" + })) + .then(() => testEdit("set", "function(value) { this._prop = value + ' BACON' }", { + "myVar.prop": "xlerb BACON", + "myVar.prop + 42": "xlerb BACON42", + "myVar.prop = 'xlerb'": "xlerb" + })) + .then(() => testEdit("get", "'brelx BEER CAN'", { + "myVar.prop": "brelx BEER CAN", + "myVar.prop + 42": "brelx BEER CAN42", + "myVar.prop = 'xlerb'": "xlerb" + })) + .then(() => testEdit("get", "{ 'brelx BEACON' }", { + "myVar.prop": undefined, + "myVar.prop + 42": NaN, + "myVar.prop = 'xlerb'": "xlerb" + })) + .then(() => testEdit("get", "{ 'brelx BEACON;'; }", { + "myVar.prop": undefined, + "myVar.prop + 42": NaN, + "myVar.prop = 'xlerb'": "xlerb" + })) + .then(() => testEdit("get", "{ return 'brelx BEACON;;'; }", { + "myVar.prop": "brelx BEACON;;", + "myVar.prop + 42": "brelx BEACON;;42", + "myVar.prop = 'xlerb'": "xlerb" + })) + .then(() => testEdit("get", "function() { return 'brelx BACON'; }", { + "myVar.prop": "brelx BACON", + "myVar.prop + 42": "brelx BACON42", + "myVar.prop = 'xlerb'": "xlerb" + })) + .then(() => testEdit("get", "bogus", { + "myVar.prop": "ReferenceError: bogus is not defined", + "myVar.prop + 42": "ReferenceError: bogus is not defined", + "myVar.prop = 'xlerb'": "xlerb" + })) + .then(() => testEdit("set", "sugob", { + "myVar.prop": "ReferenceError: bogus is not defined", + "myVar.prop + 42": "ReferenceError: bogus is not defined", + "myVar.prop = 'xlerb'": "ReferenceError: sugob is not defined" + })) + .then(() => testEdit("get", "", { + "myVar.prop": undefined, + "myVar.prop + 42": NaN, + "myVar.prop = 'xlerb'": "ReferenceError: sugob is not defined" + })) + .then(() => testEdit("set", "", { + "myVar.prop": "xlerb", + "myVar.prop + 42": NaN, + "myVar.prop = 'xlerb'": "xlerb" + })) + .then(() => deleteWatchExpression("myVar.prop = 'xlerb'")) + .then(() => testEdit("self", "2507", { + "myVar.prop": 2507, + "myVar.prop + 42": 2549 + })) + .then(() => deleteWatchExpression("myVar.prop + 42")) + .then(() => testEdit("self", "0910", { + "myVar.prop": 910 + })) + .then(() => deleteLastWatchExpression("myVar.prop")) + .then(() => testWatchExpressionsRemoved()) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + + sendMouseClickToTab(gTab, content.document.querySelector("button")); + }); +} + +function addWatchExpressions() { + return promise.resolve(null) + .then(() => { + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_WATCH_EXPRESSIONS); + gWatch.addExpression("myVar.prop"); + gEditor.focus(); + return finished; + }) + .then(() => { + let exprScope = gVars.getScopeAtIndex(0); + ok(exprScope, + "There should be a wach expressions scope in the variables view."); + is(exprScope.name, gL10N.getStr("watchExpressionsScopeLabel"), + "The scope's name should be marked as 'Watch Expressions'."); + is(exprScope._store.size, 1, + "There should be 1 evaluation available."); + + let w1 = exprScope.get("myVar.prop"); + let w2 = exprScope.get("myVar.prop + 42"); + let w3 = exprScope.get("myVar.prop = 'xlerb'"); + + ok(w1, "The first watch expression should be present in the scope."); + ok(!w2, "The second watch expression should not be present in the scope."); + ok(!w3, "The third watch expression should not be present in the scope."); + + is(w1.value, 42, "The first value is correct."); + }) + .then(() => { + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_WATCH_EXPRESSIONS); + gWatch.addExpression("myVar.prop + 42"); + gEditor.focus(); + return finished; + }) + .then(() => { + let exprScope = gVars.getScopeAtIndex(0); + ok(exprScope, + "There should be a wach expressions scope in the variables view."); + is(exprScope.name, gL10N.getStr("watchExpressionsScopeLabel"), + "The scope's name should be marked as 'Watch Expressions'."); + is(exprScope._store.size, 2, + "There should be 2 evaluations available."); + + let w1 = exprScope.get("myVar.prop"); + let w2 = exprScope.get("myVar.prop + 42"); + let w3 = exprScope.get("myVar.prop = 'xlerb'"); + + ok(w1, "The first watch expression should be present in the scope."); + ok(w2, "The second watch expression should be present in the scope."); + ok(!w3, "The third watch expression should not be present in the scope."); + + is(w1.value, "42", "The first expression value is correct."); + is(w2.value, "84", "The second expression value is correct."); + }) + .then(() => { + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_WATCH_EXPRESSIONS); + gWatch.addExpression("myVar.prop = 'xlerb'"); + gEditor.focus(); + return finished; + }) + .then(() => { + let exprScope = gVars.getScopeAtIndex(0); + ok(exprScope, + "There should be a wach expressions scope in the variables view."); + is(exprScope.name, gL10N.getStr("watchExpressionsScopeLabel"), + "The scope's name should be marked as 'Watch Expressions'."); + is(exprScope._store.size, 3, + "There should be 3 evaluations available."); + + let w1 = exprScope.get("myVar.prop"); + let w2 = exprScope.get("myVar.prop + 42"); + let w3 = exprScope.get("myVar.prop = 'xlerb'"); + + ok(w1, "The first watch expression should be present in the scope."); + ok(w2, "The second watch expression should be present in the scope."); + ok(w3, "The third watch expression should be present in the scope."); + + is(w1.value, "xlerb", "The first expression value is correct."); + is(w2.value, "xlerb42", "The second expression value is correct."); + is(w3.value, "xlerb", "The third expression value is correct."); + }); +} + +function deleteWatchExpression(aString) { + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_WATCH_EXPRESSIONS); + gWatch.deleteExpression({ name: aString }); + return finished; +} + +function deleteLastWatchExpression(aString) { + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES); + gWatch.deleteExpression({ name: aString }); + return finished; +} + +function testEdit(aWhat, aString, aExpected) { + let localScope = gVars.getScopeAtIndex(1); + let myVar = localScope.get("myVar"); + + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES).then(() => { + let propVar = myVar.get("prop"); + let getterOrSetterOrVar = aWhat != "self" ? propVar.get(aWhat) : propVar; + + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_WATCH_EXPRESSIONS).then(() => { + let exprScope = gVars.getScopeAtIndex(0); + ok(exprScope, + "There should be a wach expressions scope in the variables view."); + is(exprScope.name, gL10N.getStr("watchExpressionsScopeLabel"), + "The scope's name should be marked as 'Watch Expressions'."); + is(exprScope._store.size, Object.keys(aExpected).length, + "There should be a certain number of evaluations available."); + + function testExpression(aExpression) { + if (!aExpression) { + return; + } + let value = aExpected[aExpression.name]; + if (isNaN(value)) { + ok(isNaN(aExpression.value), + "The expression value is correct after the edit."); + } else if (value == null) { + is(aExpression.value.type, value + "", + "The expression value is correct after the edit."); + } else { + is(aExpression.value, value, + "The expression value is correct after the edit."); + } + } + + testExpression(exprScope.get(Object.keys(aExpected)[0])); + testExpression(exprScope.get(Object.keys(aExpected)[1])); + testExpression(exprScope.get(Object.keys(aExpected)[2])); + }); + + let editTarget = getterOrSetterOrVar.target; + + // Allow the target variable to get painted, so that clicking on + // its value would scroll the new textbox node into view. + executeSoon(() => { + let varValue = editTarget.querySelector(".title > .value"); + EventUtils.sendMouseEvent({ type: "mousedown" }, varValue, gDebugger); + + let varInput = editTarget.querySelector(".title > .element-value-input"); + setText(varInput, aString); + EventUtils.sendKey("RETURN", gDebugger); + }); + + return finished; + }); + + myVar.expand(); + gVars.clearHierarchy(); + + return finished; +} + +function testWatchExpressionsRemoved() { + let scope = gVars.getScopeAtIndex(0); + ok(scope, + "There should be a local scope in the variables view."); + isnot(scope.name, gL10N.getStr("watchExpressionsScopeLabel"), + "The scope's name should not be marked as 'Watch Expressions'."); + isnot(scope._store.size, 0, + "There should be some variables available."); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gL10N = null; + gEditor = null; + gVars = null; + gWatch = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-edit-getset-02.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-edit-getset-02.js new file mode 100644 index 000000000..7c760b1af --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-edit-getset-02.js @@ -0,0 +1,101 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that the variables view is able to override getter properties + * to plain value properties. + */ + +const TAB_URL = EXAMPLE_URL + "doc_frame-parameters.html"; + +let gTab, gPanel, gDebugger; +let gL10N, gEditor, gVars, gWatch; + +function test() { + // Debug test slaves are a bit slow at this test. + requestLongerTimeout(2); + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gL10N = gDebugger.L10N; + gEditor = gDebugger.DebuggerView.editor; + gVars = gDebugger.DebuggerView.Variables; + gWatch = gDebugger.DebuggerView.WatchExpressions; + + gVars.switch = function() {}; + gVars.delete = function() {}; + + waitForSourceAndCaretAndScopes(gPanel, ".html", 24) + .then(() => addWatchExpression()) + .then(() => testEdit("\"xlerb\"", "xlerb")) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + + sendMouseClickToTab(gTab, content.document.querySelector("button")); + }); +} + +function addWatchExpression() { + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_WATCH_EXPRESSIONS); + + gWatch.addExpression("myVar.prop"); + gEditor.focus(); + + return finished; +} + +function testEdit(aString, aExpected) { + let localScope = gVars.getScopeAtIndex(1); + let myVar = localScope.get("myVar"); + + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES).then(() => { + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_WATCH_EXPRESSIONS).then(() => { + let exprScope = gVars.getScopeAtIndex(0); + + ok(exprScope, + "There should be a wach expressions scope in the variables view."); + is(exprScope.name, gL10N.getStr("watchExpressionsScopeLabel"), + "The scope's name should be marked as 'Watch Expressions'."); + is(exprScope._store.size, 1, + "There should be one evaluation available."); + + is(exprScope.get("myVar.prop").value, aExpected, + "The expression value is correct after the edit."); + }); + + let editTarget = myVar.get("prop").target; + + // Allow the target variable to get painted, so that clicking on + // its value would scroll the new textbox node into view. + executeSoon(() => { + let varEdit = editTarget.querySelector(".title > .variables-view-edit"); + EventUtils.sendMouseEvent({ type: "mousedown" }, varEdit, gDebugger); + + let varInput = editTarget.querySelector(".title > .element-value-input"); + setText(varInput, aString); + EventUtils.sendKey("RETURN", gDebugger); + }); + + return finished; + }); + + myVar.expand(); + gVars.clearHierarchy(); + + return finished; +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gL10N = null; + gEditor = null; + gVars = null; + gWatch = null; +}); + diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-edit-value.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-edit-value.js new file mode 100644 index 000000000..c58e6c0da --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-edit-value.js @@ -0,0 +1,85 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that the editing variables or properties values works properly. + */ + +const TAB_URL = EXAMPLE_URL + "doc_frame-parameters.html"; + +let gTab, gPanel, gDebugger; +let gVars; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gVars = gDebugger.DebuggerView.Variables; + + waitForSourceAndCaretAndScopes(gPanel, ".html", 24) + .then(() => initialChecks()) + .then(() => testModification("a", "1")) + .then(() => testModification("{ a: 1 }", "Object")) + .then(() => testModification("[a]", "Array[1]")) + .then(() => testModification("b", "Object")) + .then(() => testModification("b.a", "1")) + .then(() => testModification("c.a", "1")) + .then(() => testModification("Infinity", "Infinity")) + .then(() => testModification("NaN", "NaN")) + .then(() => testModification("new Function", "anonymous()")) + .then(() => testModification("+0", "0")) + .then(() => testModification("-0", "-0")) + .then(() => testModification("Object.keys({})", "Array[0]")) + .then(() => testModification("document.title", '"Debugger test page"')) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + + sendMouseClickToTab(gTab, content.document.querySelector("button")); + }); +} + +function initialChecks() { + let localScope = gVars.getScopeAtIndex(0); + let aVar = localScope.get("a"); + + is(aVar.target.querySelector(".name").getAttribute("value"), "a", + "Should have the right name for 'a'."); + is(aVar.target.querySelector(".value").getAttribute("value"), "1", + "Should have the right initial value for 'a'."); +} + +function testModification(aNewValue, aNewResult) { + let localScope = gVars.getScopeAtIndex(0); + let aVar = localScope.get("a"); + + // Allow the target variable to get painted, so that clicking on + // its value would scroll the new textbox node into view. + executeSoon(() => { + let varValue = aVar.target.querySelector(".title > .value"); + EventUtils.sendMouseEvent({ type: "mousedown" }, varValue, gDebugger); + + let varInput = aVar.target.querySelector(".title > .element-value-input"); + setText(varInput, aNewValue); + EventUtils.sendKey("RETURN", gDebugger); + }); + + return waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES).then(() => { + let localScope = gVars.getScopeAtIndex(0); + let aVar = localScope.get("a"); + + is(aVar.target.querySelector(".name").getAttribute("value"), "a", + "Should have the right name for 'a'."); + is(aVar.target.querySelector(".value").getAttribute("value"), aNewResult, + "Should have the right new value for 'a'."); + }); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gVars = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-edit-watch.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-edit-watch.js new file mode 100644 index 000000000..9f4604af4 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-edit-watch.js @@ -0,0 +1,502 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that the editing or removing watch expressions works properly. + */ + +const TAB_URL = EXAMPLE_URL + "doc_watch-expressions.html"; + +let gTab, gPanel, gDebugger; +let gL10N, gEditor, gVars, gWatch; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gL10N = gDebugger.L10N; + gEditor = gDebugger.DebuggerView.editor; + gVars = gDebugger.DebuggerView.Variables; + gWatch = gDebugger.DebuggerView.WatchExpressions; + + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_WATCH_EXPRESSIONS) + .then(() => testInitialVariablesInScope()) + .then(() => testInitialExpressionsInScope()) + .then(() => testModification("document.title = 42", "document.title = 43", "43", "undefined")) + .then(() => testIntegrity1()) + .then(() => testModification("aArg", "aArg = 44", "44", "44")) + .then(() => testIntegrity2()) + .then(() => testModification("aArg = 44", "\ \t\r\ndocument.title\ \t\r\n", "\"43\"", "44")) + .then(() => testIntegrity3()) + .then(() => testModification("document.title = 43", "\ \t\r\ndocument.title\ \t\r\n", "\"43\"", "44")) + .then(() => testIntegrity4()) + .then(() => testModification("document.title", "\ \t\r\n", "\"43\"", "44")) + .then(() => testIntegrity5()) + .then(() => testExprDeletion("this", "44")) + .then(() => testIntegrity6()) + .then(() => testExprFinalDeletion("ermahgerd", "44")) + .then(() => testIntegrity7()) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + + addExpressions(); + callInTab(gTab, "ermahgerd"); + }); +} + +function addExpressions() { + addExpression("this"); + addExpression("ermahgerd"); + addExpression("aArg"); + addExpression("document.title"); + addCmdExpression("document.title = 42"); + + is(gWatch.itemCount, 5, + "There should be 5 items availalble in the watch expressions view."); + + is(gWatch.getItemAtIndex(4).attachment.initialExpression, "this", + "The first expression's initial value should be correct."); + is(gWatch.getItemAtIndex(3).attachment.initialExpression, "ermahgerd", + "The second expression's initial value should be correct."); + is(gWatch.getItemAtIndex(2).attachment.initialExpression, "aArg", + "The third expression's initial value should be correct."); + is(gWatch.getItemAtIndex(1).attachment.initialExpression, "document.title", + "The fourth expression's initial value should be correct."); + is(gWatch.getItemAtIndex(0).attachment.initialExpression, "document.title = 42", + "The fifth expression's initial value should be correct."); + + is(gWatch.getItemAtIndex(4).attachment.currentExpression, "this", + "The first expression's current value should be correct."); + is(gWatch.getItemAtIndex(3).attachment.currentExpression, "ermahgerd", + "The second expression's current value should be correct."); + is(gWatch.getItemAtIndex(2).attachment.currentExpression, "aArg", + "The third expression's current value should be correct."); + is(gWatch.getItemAtIndex(1).attachment.currentExpression, "document.title", + "The fourth expression's current value should be correct."); + is(gWatch.getItemAtIndex(0).attachment.currentExpression, "document.title = 42", + "The fifth expression's current value should be correct."); +} + +function testInitialVariablesInScope() { + let localScope = gVars.getScopeAtIndex(1); + let argVar = localScope.get("aArg"); + + is(argVar.visible, true, + "Should have the right visibility state for 'aArg'."); + is(argVar.name, "aArg", + "Should have the right name for 'aArg'."); + is(argVar.value.type, "undefined", + "Should have the right initial value for 'aArg'."); +} + +function testInitialExpressionsInScope() { + let exprScope = gVars.getScopeAtIndex(0); + let thisExpr = exprScope.get("this"); + let ermExpr = exprScope.get("ermahgerd"); + let argExpr = exprScope.get("aArg"); + let docExpr = exprScope.get("document.title"); + let docExpr2 = exprScope.get("document.title = 42"); + + ok(exprScope, + "There should be a wach expressions scope in the variables view."); + is(exprScope.name, gL10N.getStr("watchExpressionsScopeLabel"), + "The scope's name should be marked as 'Watch Expressions'."); + is(exprScope._store.size, 5, + "There should be 5 evaluations available."); + + is(thisExpr.visible, true, + "Should have the right visibility state for 'this'."); + is(thisExpr.target.querySelectorAll(".variables-view-delete").length, 1, + "Should have the one close button visible for 'this'."); + is(thisExpr.name, "this", + "Should have the right name for 'this'."); + is(thisExpr.value.type, "object", + "Should have the right value type for 'this'."); + is(thisExpr.value.class, "Window", + "Should have the right value type for 'this'."); + + is(ermExpr.visible, true, + "Should have the right visibility state for 'ermahgerd'."); + is(ermExpr.target.querySelectorAll(".variables-view-delete").length, 1, + "Should have the one close button visible for 'ermahgerd'."); + is(ermExpr.name, "ermahgerd", + "Should have the right name for 'ermahgerd'."); + is(ermExpr.value.type, "object", + "Should have the right value type for 'ermahgerd'."); + is(ermExpr.value.class, "Function", + "Should have the right value type for 'ermahgerd'."); + + is(argExpr.visible, true, + "Should have the right visibility state for 'aArg'."); + is(argExpr.target.querySelectorAll(".variables-view-delete").length, 1, + "Should have the one close button visible for 'aArg'."); + is(argExpr.name, "aArg", + "Should have the right name for 'aArg'."); + is(argExpr.value.type, "undefined", + "Should have the right value for 'aArg'."); + + is(docExpr.visible, true, + "Should have the right visibility state for 'document.title'."); + is(docExpr.target.querySelectorAll(".variables-view-delete").length, 1, + "Should have the one close button visible for 'document.title'."); + is(docExpr.name, "document.title", + "Should have the right name for 'document.title'."); + is(docExpr.value, "42", + "Should have the right value for 'document.title'."); + + is(docExpr2.visible, true, + "Should have the right visibility state for 'document.title = 42'."); + is(docExpr2.target.querySelectorAll(".variables-view-delete").length, 1, + "Should have the one close button visible for 'document.title = 42'."); + is(docExpr2.name, "document.title = 42", + "Should have the right name for 'document.title = 42'."); + is(docExpr2.value, 42, + "Should have the right value for 'document.title = 42'."); + + is(gDebugger.document.querySelectorAll(".dbg-expression[hidden=true]").length, 5, + "There should be 5 hidden nodes in the watch expressions container."); + is(gDebugger.document.querySelectorAll(".dbg-expression:not([hidden=true])").length, 0, + "There should be 0 visible nodes in the watch expressions container."); +} + +function testModification(aName, aNewValue, aNewResult, aArgResult) { + let exprScope = gVars.getScopeAtIndex(0); + let exprVar = exprScope.get(aName); + + let finished = promise.all([ + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_WATCH_EXPRESSIONS) + ]) + .then(() => { + let localScope = gVars.getScopeAtIndex(1); + let argVar = localScope.get("aArg"); + + is(argVar.visible, true, + "Should have the right visibility state for 'aArg'."); + is(argVar.target.querySelector(".name").getAttribute("value"), "aArg", + "Should have the right name for 'aArg'."); + is(argVar.target.querySelector(".value").getAttribute("value"), aArgResult, + "Should have the right new value for 'aArg'."); + + let exprScope = gVars.getScopeAtIndex(0); + let exprOldVar = exprScope.get(aName); + let exprNewVar = exprScope.get(aNewValue.trim()); + + if (!aNewValue.trim()) { + ok(!exprOldVar, + "The old watch expression should have been removed."); + ok(!exprNewVar, + "No new watch expression should have been added."); + } else { + ok(!exprOldVar, + "The old watch expression should have been removed."); + ok(exprNewVar, + "The new watch expression should have been added."); + + is(exprNewVar.visible, true, + "Should have the right visibility state for the watch expression."); + is(exprNewVar.target.querySelector(".name").getAttribute("value"), aNewValue.trim(), + "Should have the right name for the watch expression."); + is(exprNewVar.target.querySelector(".value").getAttribute("value"), aNewResult, + "Should have the right new value for the watch expression."); + } + }); + + let varValue = exprVar.target.querySelector(".title > .name"); + EventUtils.sendMouseEvent({ type: "dblclick" }, varValue, gDebugger); + + let varInput = exprVar.target.querySelector(".title > .element-name-input"); + setText(varInput, aNewValue); + EventUtils.sendKey("RETURN", gDebugger); + + return finished; +} + +function testExprDeletion(aName, aArgResult) { + let exprScope = gVars.getScopeAtIndex(0); + let exprVar = exprScope.get(aName); + + let finished = promise.all([ + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_WATCH_EXPRESSIONS) + ]) + .then(() => { + let localScope = gVars.getScopeAtIndex(1); + let argVar = localScope.get("aArg"); + + is(argVar.visible, true, + "Should have the right visibility state for 'aArg'."); + is(argVar.target.querySelector(".name").getAttribute("value"), "aArg", + "Should have the right name for 'aArg'."); + is(argVar.target.querySelector(".value").getAttribute("value"), aArgResult, + "Should have the right new value for 'aArg'."); + + let exprScope = gVars.getScopeAtIndex(0); + let exprOldVar = exprScope.get(aName); + + ok(!exprOldVar, + "The watch expression should have been deleted."); + }); + + let varDelete = exprVar.target.querySelector(".variables-view-delete"); + EventUtils.sendMouseEvent({ type: "click" }, varDelete, gDebugger); + + return finished; +} + +function testExprFinalDeletion(aName, aArgResult) { + let exprScope = gVars.getScopeAtIndex(0); + let exprVar = exprScope.get(aName); + + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES).then(() => { + let localScope = gVars.getScopeAtIndex(0); + let argVar = localScope.get("aArg"); + + is(argVar.visible, true, + "Should have the right visibility state for 'aArg'."); + is(argVar.target.querySelector(".name").getAttribute("value"), "aArg", + "Should have the right name for 'aArg'."); + is(argVar.target.querySelector(".value").getAttribute("value"), aArgResult, + "Should have the right new value for 'aArg'."); + + let exprScope = gVars.getScopeAtIndex(0); + let exprOldVar = exprScope.get(aName); + + ok(!exprOldVar, + "The watch expression should have been deleted."); + }); + + let varDelete = exprVar.target.querySelector(".variables-view-delete"); + EventUtils.sendMouseEvent({ type: "click" }, varDelete, gDebugger); + + return finished; +} + +function testIntegrity1() { + is(gDebugger.document.querySelectorAll(".dbg-expression[hidden=true]").length, 5, + "There should be 5 hidden nodes in the watch expressions container."); + is(gDebugger.document.querySelectorAll(".dbg-expression:not([hidden=true])").length, 0, + "There should be 0 visible nodes in the watch expressions container."); + + let exprScope = gVars.getScopeAtIndex(0); + ok(exprScope, + "There should be a wach expressions scope in the variables view."); + is(exprScope.name, gL10N.getStr("watchExpressionsScopeLabel"), + "The scope's name should be marked as 'Watch Expressions'."); + is(exprScope._store.size, 5, + "There should be 5 visible evaluations available."); + + is(gWatch.itemCount, 5, + "There should be 5 hidden expression input available."); + is(gWatch.getItemAtIndex(0).attachment.view.inputNode.value, "document.title = 43", + "The first textbox input value is not the correct one."); + is(gWatch.getItemAtIndex(0).attachment.currentExpression, "document.title = 43", + "The first textbox input value is not the correct one."); + is(gWatch.getItemAtIndex(1).attachment.view.inputNode.value, "document.title", + "The second textbox input value is not the correct one."); + is(gWatch.getItemAtIndex(1).attachment.currentExpression, "document.title", + "The second textbox input value is not the correct one."); + is(gWatch.getItemAtIndex(2).attachment.view.inputNode.value, "aArg", + "The third textbox input value is not the correct one."); + is(gWatch.getItemAtIndex(2).attachment.currentExpression, "aArg", + "The third textbox input value is not the correct one."); + is(gWatch.getItemAtIndex(3).attachment.view.inputNode.value, "ermahgerd", + "The fourth textbox input value is not the correct one."); + is(gWatch.getItemAtIndex(3).attachment.currentExpression, "ermahgerd", + "The fourth textbox input value is not the correct one."); + is(gWatch.getItemAtIndex(4).attachment.view.inputNode.value, "this", + "The fifth textbox input value is not the correct one."); + is(gWatch.getItemAtIndex(4).attachment.currentExpression, "this", + "The fifth textbox input value is not the correct one."); +} + +function testIntegrity2() { + is(gDebugger.document.querySelectorAll(".dbg-expression[hidden=true]").length, 5, + "There should be 5 hidden nodes in the watch expressions container."); + is(gDebugger.document.querySelectorAll(".dbg-expression:not([hidden=true])").length, 0, + "There should be 0 visible nodes in the watch expressions container."); + + let exprScope = gVars.getScopeAtIndex(0); + ok(exprScope, + "There should be a wach expressions scope in the variables view."); + is(exprScope.name, gL10N.getStr("watchExpressionsScopeLabel"), + "The scope's name should be marked as 'Watch Expressions'."); + is(exprScope._store.size, 5, + "There should be 5 visible evaluations available."); + + is(gWatch.itemCount, 5, + "There should be 5 hidden expression input available."); + is(gWatch.getItemAtIndex(0).attachment.view.inputNode.value, "document.title = 43", + "The first textbox input value is not the correct one."); + is(gWatch.getItemAtIndex(0).attachment.currentExpression, "document.title = 43", + "The first textbox input value is not the correct one."); + is(gWatch.getItemAtIndex(1).attachment.view.inputNode.value, "document.title", + "The second textbox input value is not the correct one."); + is(gWatch.getItemAtIndex(1).attachment.currentExpression, "document.title", + "The second textbox input value is not the correct one."); + is(gWatch.getItemAtIndex(2).attachment.view.inputNode.value, "aArg = 44", + "The third textbox input value is not the correct one."); + is(gWatch.getItemAtIndex(2).attachment.currentExpression, "aArg = 44", + "The third textbox input value is not the correct one."); + is(gWatch.getItemAtIndex(3).attachment.view.inputNode.value, "ermahgerd", + "The fourth textbox input value is not the correct one."); + is(gWatch.getItemAtIndex(3).attachment.currentExpression, "ermahgerd", + "The fourth textbox input value is not the correct one."); + is(gWatch.getItemAtIndex(4).attachment.view.inputNode.value, "this", + "The fifth textbox input value is not the correct one."); + is(gWatch.getItemAtIndex(4).attachment.currentExpression, "this", + "The fifth textbox input value is not the correct one."); +} + +function testIntegrity3() { + is(gDebugger.document.querySelectorAll(".dbg-expression[hidden=true]").length, 4, + "There should be 4 hidden nodes in the watch expressions container."); + is(gDebugger.document.querySelectorAll(".dbg-expression:not([hidden=true])").length, 0, + "There should be 0 visible nodes in the watch expressions container."); + + let exprScope = gVars.getScopeAtIndex(0); + ok(exprScope, + "There should be a wach expressions scope in the variables view."); + is(exprScope.name, gL10N.getStr("watchExpressionsScopeLabel"), + "The scope's name should be marked as 'Watch Expressions'."); + is(exprScope._store.size, 4, + "There should be 4 visible evaluations available."); + + is(gWatch.itemCount, 4, + "There should be 4 hidden expression input available."); + is(gWatch.getItemAtIndex(0).attachment.view.inputNode.value, "document.title = 43", + "The first textbox input value is not the correct one."); + is(gWatch.getItemAtIndex(0).attachment.currentExpression, "document.title = 43", + "The first textbox input value is not the correct one."); + is(gWatch.getItemAtIndex(1).attachment.view.inputNode.value, "document.title", + "The second textbox input value is not the correct one."); + is(gWatch.getItemAtIndex(1).attachment.currentExpression, "document.title", + "The second textbox input value is not the correct one."); + is(gWatch.getItemAtIndex(2).attachment.view.inputNode.value, "ermahgerd", + "The third textbox input value is not the correct one."); + is(gWatch.getItemAtIndex(2).attachment.currentExpression, "ermahgerd", + "The third textbox input value is not the correct one."); + is(gWatch.getItemAtIndex(3).attachment.view.inputNode.value, "this", + "The fourth textbox input value is not the correct one."); + is(gWatch.getItemAtIndex(3).attachment.currentExpression, "this", + "The fourth textbox input value is not the correct one."); +} + +function testIntegrity4() { + is(gDebugger.document.querySelectorAll(".dbg-expression[hidden=true]").length, 3, + "There should be 3 hidden nodes in the watch expressions container."); + is(gDebugger.document.querySelectorAll(".dbg-expression:not([hidden=true])").length, 0, + "There should be 0 visible nodes in the watch expressions container."); + + let exprScope = gVars.getScopeAtIndex(0); + ok(exprScope, + "There should be a wach expressions scope in the variables view."); + is(exprScope.name, gL10N.getStr("watchExpressionsScopeLabel"), + "The scope's name should be marked as 'Watch Expressions'."); + is(exprScope._store.size, 3, + "There should be 3 visible evaluations available."); + + is(gWatch.itemCount, 3, + "There should be 3 hidden expression input available."); + is(gWatch.getItemAtIndex(0).attachment.view.inputNode.value, "document.title", + "The first textbox input value is not the correct one."); + is(gWatch.getItemAtIndex(0).attachment.currentExpression, "document.title", + "The first textbox input value is not the correct one."); + is(gWatch.getItemAtIndex(1).attachment.view.inputNode.value, "ermahgerd", + "The second textbox input value is not the correct one."); + is(gWatch.getItemAtIndex(1).attachment.currentExpression, "ermahgerd", + "The second textbox input value is not the correct one."); + is(gWatch.getItemAtIndex(2).attachment.view.inputNode.value, "this", + "The third textbox input value is not the correct one."); + is(gWatch.getItemAtIndex(2).attachment.currentExpression, "this", + "The third textbox input value is not the correct one."); +} + +function testIntegrity5() { + is(gDebugger.document.querySelectorAll(".dbg-expression[hidden=true]").length, 2, + "There should be 2 hidden nodes in the watch expressions container."); + is(gDebugger.document.querySelectorAll(".dbg-expression:not([hidden=true])").length, 0, + "There should be 0 visible nodes in the watch expressions container."); + + let exprScope = gVars.getScopeAtIndex(0); + ok(exprScope, + "There should be a wach expressions scope in the variables view."); + is(exprScope.name, gL10N.getStr("watchExpressionsScopeLabel"), + "The scope's name should be marked as 'Watch Expressions'."); + is(exprScope._store.size, 2, + "There should be 2 visible evaluations available."); + + is(gWatch.itemCount, 2, + "There should be 2 hidden expression input available."); + is(gWatch.getItemAtIndex(0).attachment.view.inputNode.value, "ermahgerd", + "The first textbox input value is not the correct one."); + is(gWatch.getItemAtIndex(0).attachment.currentExpression, "ermahgerd", + "The first textbox input value is not the correct one."); + is(gWatch.getItemAtIndex(1).attachment.view.inputNode.value, "this", + "The second textbox input value is not the correct one."); + is(gWatch.getItemAtIndex(1).attachment.currentExpression, "this", + "The second textbox input value is not the correct one."); +} + +function testIntegrity6() { + is(gDebugger.document.querySelectorAll(".dbg-expression[hidden=true]").length, 1, + "There should be 1 hidden nodes in the watch expressions container."); + is(gDebugger.document.querySelectorAll(".dbg-expression:not([hidden=true])").length, 0, + "There should be 0 visible nodes in the watch expressions container."); + + let exprScope = gVars.getScopeAtIndex(0); + ok(exprScope, + "There should be a wach expressions scope in the variables view."); + is(exprScope.name, gL10N.getStr("watchExpressionsScopeLabel"), + "The scope's name should be marked as 'Watch Expressions'."); + is(exprScope._store.size, 1, + "There should be 1 visible evaluation available."); + + is(gWatch.itemCount, 1, + "There should be 1 hidden expression input available."); + is(gWatch.getItemAtIndex(0).attachment.view.inputNode.value, "ermahgerd", + "The first textbox input value is not the correct one."); + is(gWatch.getItemAtIndex(0).attachment.currentExpression, "ermahgerd", + "The first textbox input value is not the correct one."); +} + +function testIntegrity7() { + is(gDebugger.document.querySelectorAll(".dbg-expression[hidden=true]").length, 0, + "There should be 0 hidden nodes in the watch expressions container."); + is(gDebugger.document.querySelectorAll(".dbg-expression:not([hidden=true])").length, 0, + "There should be 0 visible nodes in the watch expressions container."); + + let localScope = gVars.getScopeAtIndex(0); + ok(localScope, + "There should be a local scope in the variables view."); + isnot(localScope.name, gL10N.getStr("watchExpressionsScopeLabel"), + "The scope's name should not be marked as 'Watch Expressions'."); + isnot(localScope._store.size, 0, + "There should be some variables available."); + + is(gWatch.itemCount, 0, + "The watch expressions container should be empty."); +} + +function addExpression(aString) { + gWatch.addExpression(aString); + gEditor.focus(); +} + +function addCmdExpression(aString) { + gWatch._onCmdAddExpression(aString); + gEditor.focus(); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gL10N = null; + gEditor = null; + gVars = null; + gWatch = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-filter-01.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-filter-01.js new file mode 100644 index 000000000..4555a0dad --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-filter-01.js @@ -0,0 +1,218 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that the variables view correctly filters nodes by name. + */ + +const TAB_URL = EXAMPLE_URL + "doc_with-frame.html"; + +let gTab, gPanel, gDebugger; +let gVariables, gSearchBox; + +function test() { + // Debug test slaves are quite slow at this test. + requestLongerTimeout(4); + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gVariables = gDebugger.DebuggerView.Variables; + + gVariables._enableSearch(); + gSearchBox = gVariables._searchboxNode; + + // The first 'with' scope should be expanded by default, but the + // variables haven't been fetched yet. This is how 'with' scopes work. + promise.all([ + waitForSourceAndCaret(gPanel, ".html", 22), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_VARIABLES) + ]).then(prepareVariablesAndProperties) + .then(testVariablesAndPropertiesFiltering) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + + sendMouseClickToTab(gTab, content.document.querySelector("button")); + }); +} + +function testVariablesAndPropertiesFiltering() { + let localScope = gVariables.getScopeAtIndex(0); + let withScope = gVariables.getScopeAtIndex(1); + let functionScope = gVariables.getScopeAtIndex(2); + let globalScope = gVariables.getScopeAtIndex(3); + let protoVar = localScope.get("__proto__"); + let constrVar = protoVar.get("constructor"); + let proto2Var = constrVar.get("__proto__"); + let constr2Var = proto2Var.get("constructor"); + + function testFiltered() { + is(localScope.expanded, true, + "The localScope should be expanded."); + is(withScope.expanded, true, + "The withScope should be expanded."); + is(functionScope.expanded, true, + "The functionScope should be expanded."); + is(globalScope.expanded, true, + "The globalScope should be expanded."); + + is(protoVar.expanded, true, + "The protoVar should be expanded."); + is(constrVar.expanded, true, + "The constrVar should be expanded."); + is(proto2Var.expanded, true, + "The proto2Var should be expanded."); + is(constr2Var.expanded, true, + "The constr2Var should be expanded."); + + is(localScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length, 1, + "There should be 1 variable displayed in the local scope."); + is(withScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length, 0, + "There should be 0 variables displayed in the with scope."); + is(functionScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length, 0, + "There should be 0 variables displayed in the function scope."); + is(globalScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length, 0, + "There should be 0 variables displayed in the global scope."); + + is(withScope.target.querySelectorAll(".variables-view-property:not([unmatched])").length, 0, + "There should be 0 properties displayed in the with scope."); + is(functionScope.target.querySelectorAll(".variables-view-property:not([unmatched])").length, 0, + "There should be 0 properties displayed in the function scope."); + is(globalScope.target.querySelectorAll(".variables-view-property:not([unmatched])").length, 0, + "There should be 0 properties displayed in the global scope."); + + is(localScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[0].getAttribute("value"), + "__proto__", "The only inner variable displayed should be '__proto__'"); + is(localScope.target.querySelectorAll(".variables-view-property:not([unmatched]) > .title > .name")[0].getAttribute("value"), + "constructor", "The first inner property displayed should be 'constructor'"); + is(localScope.target.querySelectorAll(".variables-view-property:not([unmatched]) > .title > .name")[1].getAttribute("value"), + "__proto__", "The second inner property displayed should be '__proto__'"); + is(localScope.target.querySelectorAll(".variables-view-property:not([unmatched]) > .title > .name")[2].getAttribute("value"), + "constructor", "The third inner property displayed should be 'constructor'"); + } + + function firstFilter() { + typeText(gSearchBox, "constructor"); + testFiltered(); + } + + function secondFilter() { + localScope.collapse(); + withScope.collapse(); + functionScope.collapse(); + globalScope.collapse(); + protoVar.collapse(); + constrVar.collapse(); + proto2Var.collapse(); + constr2Var.collapse(); + + is(localScope.expanded, false, + "The localScope should not be expanded."); + is(withScope.expanded, false, + "The withScope should not be expanded."); + is(functionScope.expanded, false, + "The functionScope should not be expanded."); + is(globalScope.expanded, false, + "The globalScope should not be expanded."); + + is(protoVar.expanded, false, + "The protoVar should not be expanded."); + is(constrVar.expanded, false, + "The constrVar should not be expanded."); + is(proto2Var.expanded, false, + "The proto2Var should not be expanded."); + is(constr2Var.expanded, false, + "The constr2Var should not be expanded."); + + clearText(gSearchBox); + typeText(gSearchBox, "constructor"); + testFiltered(); + } + + firstFilter(); + secondFilter(); +} + +function prepareVariablesAndProperties() { + let deferred = promise.defer(); + + let localScope = gVariables.getScopeAtIndex(0); + let withScope = gVariables.getScopeAtIndex(1); + let functionScope = gVariables.getScopeAtIndex(2); + let globalScope = gVariables.getScopeAtIndex(3); + + is(localScope.expanded, true, + "The localScope should be expanded."); + is(withScope.expanded, false, + "The withScope should not be expanded yet."); + is(functionScope.expanded, false, + "The functionScope should not be expanded yet."); + is(globalScope.expanded, false, + "The globalScope should not be expanded yet."); + + // Wait for only two events to be triggered, because the Function scope is + // an environment to which scope arguments and variables are already attached. + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_VARIABLES, 2).then(() => { + is(localScope.expanded, true, + "The localScope should now be expanded."); + is(withScope.expanded, true, + "The withScope should now be expanded."); + is(functionScope.expanded, true, + "The functionScope should now be expanded."); + is(globalScope.expanded, true, + "The globalScope should now be expanded."); + + let protoVar = localScope.get("__proto__"); + + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 1).then(() => { + let constrVar = protoVar.get("constructor"); + + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 1).then(() => { + let proto2Var = constrVar.get("__proto__"); + + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 1).then(() => { + let constr2Var = proto2Var.get("constructor"); + + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 1).then(() => { + is(protoVar.expanded, true, + "The local scope '__proto__' should be expanded."); + is(constrVar.expanded, true, + "The local scope '__proto__.constructor' should be expanded."); + is(proto2Var.expanded, true, + "The local scope '__proto__.constructor.__proto__' should be expanded."); + is(constr2Var.expanded, true, + "The local scope '__proto__.constructor.__proto__.constructor' should be expanded."); + + deferred.resolve(); + }); + + constr2Var.expand(); + }); + + proto2Var.expand(); + }); + + constrVar.expand(); + }); + + protoVar.expand(); + }); + + withScope.expand(); + functionScope.expand(); + globalScope.expand(); + + return deferred.promise; +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gVariables = null; + gSearchBox = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-filter-02.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-filter-02.js new file mode 100644 index 000000000..9590366f4 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-filter-02.js @@ -0,0 +1,225 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that the variables view correctly filters nodes by value. + */ + +const TAB_URL = EXAMPLE_URL + "doc_with-frame.html"; + +let gTab, gPanel, gDebugger; +let gVariables, gSearchBox; + +function test() { + // Debug test slaves are quite slow at this test. + requestLongerTimeout(4); + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gVariables = gDebugger.DebuggerView.Variables; + + gVariables._enableSearch(); + gSearchBox = gVariables._searchboxNode; + + // The first 'with' scope should be expanded by default, but the + // variables haven't been fetched yet. This is how 'with' scopes work. + promise.all([ + waitForSourceAndCaret(gPanel, ".html", 22), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_VARIABLES) + ]).then(prepareVariablesAndProperties) + .then(testVariablesAndPropertiesFiltering) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + + sendMouseClickToTab(gTab, content.document.querySelector("button")); + }); +} + +function testVariablesAndPropertiesFiltering() { + let localScope = gVariables.getScopeAtIndex(0); + let withScope = gVariables.getScopeAtIndex(1); + let functionScope = gVariables.getScopeAtIndex(2); + let globalScope = gVariables.getScopeAtIndex(3); + let protoVar = localScope.get("__proto__"); + let constrVar = protoVar.get("constructor"); + let proto2Var = constrVar.get("__proto__"); + let constr2Var = proto2Var.get("constructor"); + + function testFiltered() { + is(localScope.expanded, true, + "The localScope should be expanded."); + is(withScope.expanded, true, + "The withScope should be expanded."); + is(functionScope.expanded, true, + "The functionScope should be expanded."); + is(globalScope.expanded, true, + "The globalScope should be expanded."); + + is(protoVar.expanded, true, + "The protoVar should be expanded."); + is(constrVar.expanded, true, + "The constrVar should be expanded."); + is(proto2Var.expanded, true, + "The proto2Var should be expanded."); + is(constr2Var.expanded, true, + "The constr2Var should be expanded."); + + is(localScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length, 1, + "There should be 1 variable displayed in the local scope."); + is(withScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length, 0, + "There should be 0 variables displayed in the with scope."); + is(functionScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length, 0, + "There should be 0 variables displayed in the function scope."); + is(globalScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length, 0, + "There should be no variables displayed in the global scope."); + + is(localScope.target.querySelectorAll(".variables-view-property:not([unmatched])").length, 4, + "There should be 4 properties displayed in the local scope."); + is(withScope.target.querySelectorAll(".variables-view-property:not([unmatched])").length, 0, + "There should be 0 properties displayed in the with scope."); + is(functionScope.target.querySelectorAll(".variables-view-property:not([unmatched])").length, 0, + "There should be 0 properties displayed in the function scope."); + is(globalScope.target.querySelectorAll(".variables-view-property:not([unmatched])").length, 0, + "There should be 0 properties displayed in the global scope."); + + is(localScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[0].getAttribute("value"), + "__proto__", "The only inner variable displayed should be '__proto__'"); + is(localScope.target.querySelectorAll(".variables-view-property:not([unmatched]) > .title > .name")[0].getAttribute("value"), + "constructor", "The first inner property displayed should be 'constructor'"); + is(localScope.target.querySelectorAll(".variables-view-property:not([unmatched]) > .title > .name")[1].getAttribute("value"), + "__proto__", "The second inner property displayed should be '__proto__'"); + is(localScope.target.querySelectorAll(".variables-view-property:not([unmatched]) > .title > .name")[2].getAttribute("value"), + "constructor", "The third inner property displayed should be 'constructor'"); + + is(localScope.target.querySelectorAll(".variables-view-property:not([unmatched]) > .title > .name")[3].getAttribute("value"), + "name", "The fourth inner property displayed should be 'name'"); + is(localScope.target.querySelectorAll(".variables-view-property:not([unmatched]) > .title > .value")[3].getAttribute("value"), + "\"Function\"", "The fourth inner property displayed should be '\"Function\"'"); + } + + function firstFilter() { + typeText(gSearchBox, "\"Function\""); + testFiltered(); + } + + function secondFilter() { + localScope.collapse(); + withScope.collapse(); + functionScope.collapse(); + globalScope.collapse(); + protoVar.collapse(); + constrVar.collapse(); + proto2Var.collapse(); + constr2Var.collapse(); + + is(localScope.expanded, false, + "The localScope should not be expanded."); + is(withScope.expanded, false, + "The withScope should not be expanded."); + is(functionScope.expanded, false, + "The functionScope should not be expanded."); + is(globalScope.expanded, false, + "The globalScope should not be expanded."); + + is(protoVar.expanded, false, + "The protoVar should not be expanded."); + is(constrVar.expanded, false, + "The constrVar should not be expanded."); + is(proto2Var.expanded, false, + "The proto2Var should not be expanded."); + is(constr2Var.expanded, false, + "The constr2Var should not be expanded."); + + backspaceText(gSearchBox, 10); + typeText(gSearchBox, "\"Function\""); + testFiltered(); + } + + firstFilter(); + secondFilter(); +} + +function prepareVariablesAndProperties() { + let deferred = promise.defer(); + + let localScope = gVariables.getScopeAtIndex(0); + let withScope = gVariables.getScopeAtIndex(1); + let functionScope = gVariables.getScopeAtIndex(2); + let globalScope = gVariables.getScopeAtIndex(3); + + is(localScope.expanded, true, + "The localScope should be expanded."); + is(withScope.expanded, false, + "The withScope should not be expanded yet."); + is(functionScope.expanded, false, + "The functionScope should not be expanded yet."); + is(globalScope.expanded, false, + "The globalScope should not be expanded yet."); + + // Wait for only two events to be triggered, because the Function scope is + // an environment to which scope arguments and variables are already attached. + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_VARIABLES, 2).then(() => { + is(localScope.expanded, true, + "The localScope should now be expanded."); + is(withScope.expanded, true, + "The withScope should now be expanded."); + is(functionScope.expanded, true, + "The functionScope should now be expanded."); + is(globalScope.expanded, true, + "The globalScope should now be expanded."); + + let protoVar = localScope.get("__proto__"); + + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 1).then(() => { + let constrVar = protoVar.get("constructor"); + + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 1).then(() => { + let proto2Var = constrVar.get("__proto__"); + + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 1).then(() => { + let constr2Var = proto2Var.get("constructor"); + + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 1).then(() => { + is(protoVar.expanded, true, + "The local scope '__proto__' should be expanded."); + is(constrVar.expanded, true, + "The local scope '__proto__.constructor' should be expanded."); + is(proto2Var.expanded, true, + "The local scope '__proto__.constructor.__proto__' should be expanded."); + is(constr2Var.expanded, true, + "The local scope '__proto__.constructor.__proto__.constructor' should be expanded."); + + deferred.resolve(); + }); + + constr2Var.expand(); + }); + + proto2Var.expand(); + }); + + constrVar.expand(); + }); + + protoVar.expand(); + }); + + withScope.expand(); + functionScope.expand(); + globalScope.expand(); + + return deferred.promise; +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gVariables = null; + gSearchBox = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-filter-03.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-filter-03.js new file mode 100644 index 000000000..6ed1f2135 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-filter-03.js @@ -0,0 +1,157 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that the variables view correctly filters nodes when triggered + * from the debugger's searchbox via an operator. + */ + +const TAB_URL = EXAMPLE_URL + "doc_with-frame.html"; + +let gTab, gPanel, gDebugger; +let gVariables, gSearchBox; + +function test() { + // Debug test slaves are a bit slow at this test. + requestLongerTimeout(2); + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gVariables = gDebugger.DebuggerView.Variables; + gSearchBox = gDebugger.DebuggerView.Filtering._searchbox; + + // The first 'with' scope should be expanded by default, but the + // variables haven't been fetched yet. This is how 'with' scopes work. + promise.all([ + waitForSourceAndCaret(gPanel, ".html", 22), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_VARIABLES) + ]).then(prepareVariablesAndProperties) + .then(testVariablesAndPropertiesFiltering) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + + sendMouseClickToTab(gTab, content.document.querySelector("button")); + }); +} + +function testVariablesAndPropertiesFiltering() { + let localScope = gVariables.getScopeAtIndex(0); + let withScope = gVariables.getScopeAtIndex(1); + let functionScope = gVariables.getScopeAtIndex(2); + let globalScope = gVariables.getScopeAtIndex(3); + + function testFiltered() { + is(localScope.expanded, true, + "The localScope should be expanded."); + is(withScope.expanded, true, + "The withScope should be expanded."); + is(functionScope.expanded, true, + "The functionScope should be expanded."); + is(globalScope.expanded, true, + "The globalScope should be expanded."); + + is(localScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length, 1, + "There should be 1 variable displayed in the local scope."); + is(withScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length, 0, + "There should be 0 variables displayed in the with scope."); + is(functionScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length, 0, + "There should be 0 variables displayed in the function scope."); + is(globalScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length, 0, + "There should be 0 variables displayed in the global scope."); + + is(localScope.target.querySelectorAll(".variables-view-property:not([unmatched])").length, 0, + "There should be 0 properties displayed in the local scope."); + is(withScope.target.querySelectorAll(".variables-view-property:not([unmatched])").length, 0, + "There should be 0 properties displayed in the with scope."); + is(functionScope.target.querySelectorAll(".variables-view-property:not([unmatched])").length, 0, + "There should be 0 properties displayed in the function scope."); + is(globalScope.target.querySelectorAll(".variables-view-property:not([unmatched])").length, 0, + "There should be 0 properties displayed in the global scope."); + } + + function firstFilter() { + typeText(gSearchBox, "*alpha"); + testFiltered("alpha"); + + is(localScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[0].getAttribute("value"), + "alpha", "The only inner variable displayed should be 'alpha'"); + } + + function secondFilter() { + localScope.collapse(); + withScope.collapse(); + functionScope.collapse(); + globalScope.collapse(); + + is(localScope.expanded, false, + "The localScope should not be expanded."); + is(withScope.expanded, false, + "The withScope should not be expanded."); + is(functionScope.expanded, false, + "The functionScope should not be expanded."); + is(globalScope.expanded, false, + "The globalScope should not be expanded."); + + backspaceText(gSearchBox, 6); + typeText(gSearchBox, "*beta"); + testFiltered("beta"); + + is(localScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[0].getAttribute("value"), + "beta", "The only inner variable displayed should be 'beta'"); + } + + firstFilter(); + secondFilter(); +} + +function prepareVariablesAndProperties() { + let deferred = promise.defer(); + + let localScope = gVariables.getScopeAtIndex(0); + let withScope = gVariables.getScopeAtIndex(1); + let functionScope = gVariables.getScopeAtIndex(2); + let globalScope = gVariables.getScopeAtIndex(3); + + is(localScope.expanded, true, + "The localScope should be expanded."); + is(withScope.expanded, false, + "The withScope should not be expanded yet."); + is(functionScope.expanded, false, + "The functionScope should not be expanded yet."); + is(globalScope.expanded, false, + "The globalScope should not be expanded yet."); + + // Wait for only two events to be triggered, because the Function scope is + // an environment to which scope arguments and variables are already attached. + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_VARIABLES, 2).then(() => { + is(localScope.expanded, true, + "The localScope should now be expanded."); + is(withScope.expanded, true, + "The withScope should now be expanded."); + is(functionScope.expanded, true, + "The functionScope should now be expanded."); + is(globalScope.expanded, true, + "The globalScope should now be expanded."); + + deferred.resolve(); + }); + + withScope.expand(); + functionScope.expand(); + globalScope.expand(); + + return deferred.promise; +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gVariables = null; + gSearchBox = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-filter-04.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-filter-04.js new file mode 100644 index 000000000..26b99a93b --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-filter-04.js @@ -0,0 +1,225 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that the variables view correctly shows/hides nodes when various + * keyboard shortcuts are pressed in the debugger's searchbox. + */ + +const TAB_URL = EXAMPLE_URL + "doc_with-frame.html"; + +let gTab, gPanel, gDebugger; +let gEditor, gVariables, gSearchBox; + +function test() { + // Debug test slaves are a bit slow at this test. + requestLongerTimeout(2); + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gVariables = gDebugger.DebuggerView.Variables; + gSearchBox = gDebugger.DebuggerView.Filtering._searchbox; + + // The first 'with' scope should be expanded by default, but the + // variables haven't been fetched yet. This is how 'with' scopes work. + promise.all([ + waitForSourceAndCaret(gPanel, ".html", 22), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_VARIABLES) + ]).then(prepareVariablesAndProperties) + .then(testVariablesAndPropertiesFiltering) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + + sendMouseClickToTab(gTab, content.document.querySelector("button")); + }); +} + +function testVariablesAndPropertiesFiltering() { + let localScope = gVariables.getScopeAtIndex(0); + let withScope = gVariables.getScopeAtIndex(1); + let functionScope = gVariables.getScopeAtIndex(2); + let globalScope = gVariables.getScopeAtIndex(3); + let step = 0; + + let tests = [ + function() { + assertExpansion([true, false, false, false]); + EventUtils.sendKey("RETURN", gDebugger); + }, + function() { + assertExpansion([true, false, false, false]); + EventUtils.sendKey("RETURN", gDebugger); + }, + function() { + assertExpansion([true, false, false, false]); + gEditor.focus(); + }, + function() { + assertExpansion([true, false, false, false]); + typeText(gSearchBox, "*"); + }, + function() { + assertExpansion([true, true, true, true]); + EventUtils.sendKey("RETURN", gDebugger); + }, + function() { + assertExpansion([true, true, true, true]); + EventUtils.sendKey("RETURN", gDebugger); + }, + function() { + assertExpansion([true, true, true, true]); + gEditor.focus(); + }, + function() { + assertExpansion([true, true, true, true]); + backspaceText(gSearchBox, 1); + }, + function() { + assertExpansion([true, true, true, true]); + EventUtils.sendKey("RETURN", gDebugger); + }, + function() { + assertExpansion([true, true, true, true]); + EventUtils.sendKey("RETURN", gDebugger); + }, + function() { + assertExpansion([true, true, true, true]); + gEditor.focus(); + }, + function() { + assertExpansion([true, true, true, true]); + localScope.collapse(); + withScope.collapse(); + functionScope.collapse(); + globalScope.collapse(); + }, + function() { + assertExpansion([false, false, false, false]); + EventUtils.sendKey("RETURN", gDebugger); + }, + function() { + assertExpansion([false, false, false, false]); + EventUtils.sendKey("RETURN", gDebugger); + }, + function() { + assertExpansion([false, false, false, false]); + gEditor.focus(); + }, + function() { + assertExpansion([false, false, false, false]); + clearText(gSearchBox); + typeText(gSearchBox, "*"); + }, + function() { + assertExpansion([true, true, true, true]); + EventUtils.sendKey("RETURN", gDebugger); + }, + function() { + assertExpansion([true, true, true, true]); + EventUtils.sendKey("RETURN", gDebugger); + }, + function() { + assertExpansion([true, true, true, true]); + gEditor.focus(); + }, + function() { + assertExpansion([true, true, true, true]); + backspaceText(gSearchBox, 1); + }, + function() { + assertExpansion([true, true, true, true]); + EventUtils.sendKey("RETURN", gDebugger); + }, + function() { + assertExpansion([true, true, true, true]); + EventUtils.sendKey("RETURN", gDebugger); + }, + function() { + assertExpansion([true, true, true, true]); + gEditor.focus(); + }, + function() { + assertExpansion([true, true, true, true]); + } + ]; + + function assertExpansion(aFlags) { + is(localScope.expanded, aFlags[0], + "The localScope should " + (aFlags[0] ? "" : "not ") + + "be expanded at this point (" + step + ")."); + + is(withScope.expanded, aFlags[1], + "The withScope should " + (aFlags[1] ? "" : "not ") + + "be expanded at this point (" + step + ")."); + + is(functionScope.expanded, aFlags[2], + "The functionScope should " + (aFlags[2] ? "" : "not ") + + "be expanded at this point (" + step + ")."); + + is(globalScope.expanded, aFlags[3], + "The globalScope should " + (aFlags[3] ? "" : "not ") + + "be expanded at this point (" + step + ")."); + + step++; + } + + return promise.all(tests.map(f => f())); +} + +function prepareVariablesAndProperties() { + let deferred = promise.defer(); + + let localScope = gVariables.getScopeAtIndex(0); + let withScope = gVariables.getScopeAtIndex(1); + let functionScope = gVariables.getScopeAtIndex(2); + let globalScope = gVariables.getScopeAtIndex(3); + + is(localScope.expanded, true, + "The localScope should be expanded."); + is(withScope.expanded, false, + "The withScope should not be expanded yet."); + is(functionScope.expanded, false, + "The functionScope should not be expanded yet."); + is(globalScope.expanded, false, + "The globalScope should not be expanded yet."); + + // Wait for only two events to be triggered, because the Function scope is + // an environment to which scope arguments and variables are already attached. + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_VARIABLES, 2).then(() => { + is(localScope.expanded, true, + "The localScope should now be expanded."); + is(withScope.expanded, true, + "The withScope should now be expanded."); + is(functionScope.expanded, true, + "The functionScope should now be expanded."); + is(globalScope.expanded, true, + "The globalScope should now be expanded."); + + withScope.collapse(); + functionScope.collapse(); + globalScope.collapse(); + + deferred.resolve(); + }); + + withScope.expand(); + functionScope.expand(); + globalScope.expand(); + + return deferred.promise; +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gEditor = null; + gVariables = null; + gSearchBox = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-filter-05.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-filter-05.js new file mode 100644 index 000000000..0e5f0b040 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-filter-05.js @@ -0,0 +1,233 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that the variables view correctly shows/hides nodes when various + * keyboard shortcuts are pressed in the debugger's searchbox. + */ + +const TAB_URL = EXAMPLE_URL + "doc_with-frame.html"; + +let gTab, gPanel, gDebugger; +let gVariables, gSearchBox; + +function test() { + // Debug test slaves are a bit slow at this test. + requestLongerTimeout(2); + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gVariables = gDebugger.DebuggerView.Variables; + gSearchBox = gDebugger.DebuggerView.Filtering._searchbox; + + // The first 'with' scope should be expanded by default, but the + // variables haven't been fetched yet. This is how 'with' scopes work. + promise.all([ + waitForSourceAndCaret(gPanel, ".html", 22), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_VARIABLES) + ]).then(prepareVariablesAndProperties) + .then(testVariablesAndPropertiesFiltering) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + + sendMouseClickToTab(gTab, content.document.querySelector("button")); + }); +} + +function testVariablesAndPropertiesFiltering() { + let localScope = gVariables.getScopeAtIndex(0); + let withScope = gVariables.getScopeAtIndex(1); + let functionScope = gVariables.getScopeAtIndex(2); + let globalScope = gVariables.getScopeAtIndex(3); + let step = 0; + + let tests = [ + function() { + assertScopeExpansion([true, false, false, false]); + typeText(gSearchBox, "*arguments"); + }, + function() { + assertScopeExpansion([true, true, true, true]); + assertVariablesCountAtLeast([0, 0, 1, 0]); + + is(functionScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[0].getAttribute("value"), + "arguments", "The arguments pseudoarray should be visible."); + is(functionScope.get("arguments").expanded, false, + "The arguments pseudoarray in functionScope should not be expanded."); + + backspaceText(gSearchBox, 6); + }, + function() { + assertScopeExpansion([true, true, true, true]); + assertVariablesCountAtLeast([0, 0, 1, 1]); + + is(functionScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[0].getAttribute("value"), + "arguments", "The arguments pseudoarray should be visible."); + is(functionScope.get("arguments").expanded, false, + "The arguments pseudoarray in functionScope should not be expanded."); + + is(globalScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[0].getAttribute("value"), + "EventTarget", "The EventTarget object should be visible."); + is(globalScope.get("EventTarget").expanded, false, + "The EventTarget object in globalScope should not be expanded."); + + backspaceText(gSearchBox, 2); + }, + function() { + assertScopeExpansion([true, true, true, true]); + assertVariablesCountAtLeast([0, 1, 3, 1]); + + is(functionScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[0].getAttribute("value"), + "aNumber", "The aNumber param should be visible."); + is(functionScope.get("aNumber").expanded, false, + "The aNumber param in functionScope should not be expanded."); + + is(functionScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[1].getAttribute("value"), + "a", "The a variable should be visible."); + is(functionScope.get("a").expanded, false, + "The a variable in functionScope should not be expanded."); + + is(functionScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[2].getAttribute("value"), + "arguments", "The arguments pseudoarray should be visible."); + is(functionScope.get("arguments").expanded, false, + "The arguments pseudoarray in functionScope should not be expanded."); + + backspaceText(gSearchBox, 1); + }, + function() { + assertScopeExpansion([true, true, true, true]); + assertVariablesCountAtLeast([4, 1, 3, 1]); + + is(localScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[0].getAttribute("value"), + "this", "The this reference should be visible."); + is(localScope.get("this").expanded, false, + "The this reference in localScope should not be expanded."); + + is(localScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[1].getAttribute("value"), + "alpha", "The alpha variable should be visible."); + is(localScope.get("alpha").expanded, false, + "The alpha variable in localScope should not be expanded."); + + is(localScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[2].getAttribute("value"), + "beta", "The beta variable should be visible."); + is(localScope.get("beta").expanded, false, + "The beta variable in localScope should not be expanded."); + + is(localScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[3].getAttribute("value"), + "__proto__", "The __proto__ reference should be visible."); + is(localScope.get("__proto__").expanded, false, + "The __proto__ reference in localScope should not be expanded."); + + is(functionScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[0].getAttribute("value"), + "aNumber", "The aNumber param should be visible."); + is(functionScope.get("aNumber").expanded, false, + "The aNumber param in functionScope should not be expanded."); + + is(functionScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[1].getAttribute("value"), + "a", "The a variable should be visible."); + is(functionScope.get("a").expanded, false, + "The a variable in functionScope should not be expanded."); + + is(functionScope.target.querySelectorAll(".variables-view-variable:not([unmatched]) > .title > .name")[2].getAttribute("value"), + "arguments", "The arguments pseudoarray should be visible."); + is(functionScope.get("arguments").expanded, false, + "The arguments pseudoarray in functionScope should not be expanded."); + } + ]; + + function assertScopeExpansion(aFlags) { + is(localScope.expanded, aFlags[0], + "The localScope should " + (aFlags[0] ? "" : "not ") + + "be expanded at this point (" + step + ")."); + + is(withScope.expanded, aFlags[1], + "The withScope should " + (aFlags[1] ? "" : "not ") + + "be expanded at this point (" + step + ")."); + + is(functionScope.expanded, aFlags[2], + "The functionScope should " + (aFlags[2] ? "" : "not ") + + "be expanded at this point (" + step + ")."); + + is(globalScope.expanded, aFlags[3], + "The globalScope should " + (aFlags[3] ? "" : "not ") + + "be expanded at this point (" + step + ")."); + } + + function assertVariablesCountAtLeast(aCounts) { + ok(localScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length >= aCounts[0], + "There should be " + aCounts[0] + + " variable displayed in the local scope (" + step + ")."); + + ok(withScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length >= aCounts[1], + "There should be " + aCounts[1] + + " variable displayed in the with scope (" + step + ")."); + + ok(functionScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length >= aCounts[2], + "There should be " + aCounts[2] + + " variable displayed in the function scope (" + step + ")."); + + ok(globalScope.target.querySelectorAll(".variables-view-variable:not([unmatched])").length >= aCounts[3], + "There should be " + aCounts[3] + + " variable displayed in the global scope (" + step + ")."); + + step++; + } + + return promise.all(tests.map(f => f())); +} + +function prepareVariablesAndProperties() { + let deferred = promise.defer(); + + let localScope = gVariables.getScopeAtIndex(0); + let withScope = gVariables.getScopeAtIndex(1); + let functionScope = gVariables.getScopeAtIndex(2); + let globalScope = gVariables.getScopeAtIndex(3); + + is(localScope.expanded, true, + "The localScope should be expanded."); + is(withScope.expanded, false, + "The withScope should not be expanded yet."); + is(functionScope.expanded, false, + "The functionScope should not be expanded yet."); + is(globalScope.expanded, false, + "The globalScope should not be expanded yet."); + + // Wait for only two events to be triggered, because the Function scope is + // an environment to which scope arguments and variables are already attached. + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_VARIABLES, 2).then(() => { + is(localScope.expanded, true, + "The localScope should now be expanded."); + is(withScope.expanded, true, + "The withScope should now be expanded."); + is(functionScope.expanded, true, + "The functionScope should now be expanded."); + is(globalScope.expanded, true, + "The globalScope should now be expanded."); + + withScope.collapse(); + functionScope.collapse(); + globalScope.collapse(); + + deferred.resolve(); + }); + + withScope.expand(); + functionScope.expand(); + globalScope.expand(); + + return deferred.promise; +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gVariables = null; + gSearchBox = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-filter-pref.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-filter-pref.js new file mode 100644 index 000000000..364e22d58 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-filter-pref.js @@ -0,0 +1,79 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that the variables view filter prefs work properly. + */ + +const TAB_URL = EXAMPLE_URL + "doc_with-frame.html"; + +let gTab, gPanel, gDebugger; +let gPrefs, gOptions, gVariables; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gPrefs = gDebugger.Prefs; + gOptions = gDebugger.DebuggerView.Options; + gVariables = gDebugger.DebuggerView.Variables; + + waitForSourceShown(gPanel, ".html").then(performTest); + }); +} + +function performTest() { + ok(!gVariables._searchboxNode, + "There should not initially be a searchbox available in the variables view."); + ok(!gVariables._searchboxContainer, + "There should not initially be a searchbox container available in the variables view."); + ok(!gVariables._parent.parentNode.querySelector(".variables-view-searchinput"), + "The searchbox element should not be found."); + + is(gPrefs.variablesSearchboxVisible, false, + "The debugger searchbox should be preffed as hidden."); + isnot(gOptions._showVariablesFilterBoxItem.getAttribute("checked"), "true", + "The options menu item should not be checked."); + + gOptions._showVariablesFilterBoxItem.setAttribute("checked", "true"); + gOptions._toggleShowVariablesFilterBox(); + + ok(gVariables._searchboxNode, + "There should be a searchbox available in the variables view."); + ok(gVariables._searchboxContainer, + "There should be a searchbox container available in the variables view."); + ok(gVariables._parent.parentNode.querySelector(".variables-view-searchinput"), + "There searchbox element should be found."); + + is(gPrefs.variablesSearchboxVisible, true, + "The debugger searchbox should now be preffed as visible."); + is(gOptions._showVariablesFilterBoxItem.getAttribute("checked"), "true", + "The options menu item should now be checked."); + + gOptions._showVariablesFilterBoxItem.setAttribute("checked", "false"); + gOptions._toggleShowVariablesFilterBox(); + + ok(!gVariables._searchboxNode, + "There should not be a searchbox available in the variables view."); + ok(!gVariables._searchboxContainer, + "There should not be a searchbox container available in the variables view."); + ok(!gVariables._parent.parentNode.querySelector(".variables-view-searchinput"), + "There searchbox element should not be found."); + + is(gPrefs.variablesSearchboxVisible, false, + "The debugger searchbox should now be preffed as hidden."); + isnot(gOptions._showVariablesFilterBoxItem.getAttribute("checked"), "true", + "The options menu item should now be unchecked."); + + closeDebuggerAndFinish(gPanel); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gPrefs = null; + gOptions = null; + gVariables = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-filter-searchbox.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-filter-searchbox.js new file mode 100644 index 000000000..4cb1f7b78 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-filter-searchbox.js @@ -0,0 +1,144 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that the variables view correctly shows the searchbox + * when prompted. + */ + +const TAB_URL = EXAMPLE_URL + "doc_with-frame.html"; + +let gTab, gPanel, gDebugger; +let gVariables; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gVariables = gDebugger.DebuggerView.Variables; + + waitForSourceShown(gPanel, ".html").then(performTest); + }); +} + +function performTest() { + // Step 1: the searchbox shouldn't initially be shown. + + ok(!gVariables._searchboxNode, + "There should not initially be a searchbox available in the variables view."); + ok(!gVariables._searchboxContainer, + "There should not initially be a searchbox container available in the variables view."); + ok(!gVariables._parent.parentNode.querySelector(".variables-view-searchinput"), + "The searchbox element should not be found."); + + // Step 2: test enable/disable cycles. + + gVariables._enableSearch(); + ok(gVariables._searchboxNode, + "There should be a searchbox available after enabling."); + ok(gVariables._searchboxContainer, + "There should be a searchbox container available after enabling."); + ok(gVariables._searchboxContainer.hidden, + "The searchbox container should be hidden at this point."); + ok(gVariables._parent.parentNode.querySelector(".variables-view-searchinput"), + "The searchbox element should be found."); + + gVariables._disableSearch(); + ok(!gVariables._searchboxNode, + "There shouldn't be a searchbox available after disabling."); + ok(!gVariables._searchboxContainer, + "There shouldn't be a searchbox container available after disabling."); + ok(!gVariables._parent.parentNode.querySelector(".variables-view-searchinput"), + "The searchbox element should not be found."); + + // Step 3: add a placeholder while the searchbox is hidden. + + var placeholder = "not freshly squeezed mango juice"; + + gVariables.searchPlaceholder = placeholder; + is(gVariables.searchPlaceholder, placeholder, + "The placeholder getter didn't return the expected string"); + + // Step 4: enable search and check the placeholder. + + gVariables._enableSearch(); + ok(gVariables._searchboxNode, + "There should be a searchbox available after enabling."); + ok(gVariables._searchboxContainer, + "There should be a searchbox container available after enabling."); + ok(gVariables._searchboxContainer.hidden, + "The searchbox container should be hidden at this point."); + ok(gVariables._parent.parentNode.querySelector(".variables-view-searchinput"), + "The searchbox element should be found."); + + is(gVariables._searchboxNode.getAttribute("placeholder"), + placeholder, "There correct placeholder should be applied to the searchbox."); + + // Step 5: add a placeholder while the searchbox is visible and check wether + // it has been immediatey applied. + + var placeholder = "freshly squeezed mango juice"; + + gVariables.searchPlaceholder = placeholder; + is(gVariables.searchPlaceholder, placeholder, + "The placeholder getter didn't return the expected string"); + + is(gVariables._searchboxNode.getAttribute("placeholder"), + placeholder, "There correct placeholder should be applied to the searchbox."); + + // Step 4: disable, enable, then test the placeholder. + + gVariables._disableSearch(); + ok(!gVariables._searchboxNode, + "There shouldn't be a searchbox available after disabling again."); + ok(!gVariables._searchboxContainer, + "There shouldn't be a searchbox container available after disabling again."); + ok(!gVariables._parent.parentNode.querySelector(".variables-view-searchinput"), + "The searchbox element should not be found."); + + gVariables._enableSearch(); + ok(gVariables._searchboxNode, + "There should be a searchbox available after enabling again."); + ok(gVariables._searchboxContainer, + "There should be a searchbox container available after enabling again."); + ok(gVariables._searchboxContainer.hidden, + "The searchbox container should be hidden at this point."); + ok(gVariables._parent.parentNode.querySelector(".variables-view-searchinput"), + "The searchbox element should be found."); + + is(gVariables._searchboxNode.getAttribute("placeholder"), + placeholder, "There correct placeholder should be applied to the searchbox again."); + + // Step 5: alternate disable, enable, then test the placeholder. + + gVariables.searchEnabled = false; + ok(!gVariables._searchboxNode, + "There shouldn't be a searchbox available after disabling again."); + ok(!gVariables._searchboxContainer, + "There shouldn't be a searchbox container available after disabling again."); + ok(!gVariables._parent.parentNode.querySelector(".variables-view-searchinput"), + "The searchbox element should not be found."); + + gVariables.searchEnabled = true; + ok(gVariables._searchboxNode, + "There should be a searchbox available after enabling again."); + ok(gVariables._searchboxContainer, + "There should be a searchbox container available after enabling again."); + ok(gVariables._searchboxContainer.hidden, + "The searchbox container should be hidden at this point."); + ok(gVariables._parent.parentNode.querySelector(".variables-view-searchinput"), + "The searchbox element should be found."); + + is(gVariables._searchboxNode.getAttribute("placeholder"), + placeholder, "There correct placeholder should be applied to the searchbox again."); + + closeDebuggerAndFinish(gPanel); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gVariables = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-frame-parameters-01.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-frame-parameters-01.js new file mode 100644 index 000000000..b29977f89 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-frame-parameters-01.js @@ -0,0 +1,256 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that the variables view correctly displays the properties + * of objects when debugger is paused. + */ + +const TAB_URL = EXAMPLE_URL + "doc_frame-parameters.html"; + +let gTab, gPanel, gDebugger; +let gVariables; + +function test() { + // Debug test slaves are a bit slow at this test. + requestLongerTimeout(2); + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gVariables = gDebugger.DebuggerView.Variables; + + waitForSourceAndCaretAndScopes(gPanel, ".html", 24) + .then(initialChecks) + .then(testExpandVariables) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + + sendMouseClickToTab(gTab, content.document.querySelector("button")); + }); +} + +function initialChecks() { + let scopeNodes = gDebugger.document.querySelectorAll(".variables-view-scope"); + is(scopeNodes.length, 2, + "There should be 2 scopes available."); + + ok(scopeNodes[0].querySelector(".name").getAttribute("value").contains("[test]"), + "The local scope should be properly identified."); + ok(scopeNodes[1].querySelector(".name").getAttribute("value").contains("[Window]"), + "The global scope should be properly identified."); + + is(gVariables.getScopeAtIndex(0).target, scopeNodes[0], + "getScopeAtIndex(0) didn't return the expected scope."); + is(gVariables.getScopeAtIndex(1).target, scopeNodes[1], + "getScopeAtIndex(1) didn't return the expected scope."); + + is(gVariables.getItemForNode(scopeNodes[0]).target, scopeNodes[0], + "getItemForNode([0]) didn't return the expected scope."); + is(gVariables.getItemForNode(scopeNodes[1]).target, scopeNodes[1], + "getItemForNode([1]) didn't return the expected scope."); + + is(gVariables.getItemForNode(scopeNodes[0]).expanded, true, + "The local scope should be expanded by default."); + is(gVariables.getItemForNode(scopeNodes[1]).expanded, false, + "The global scope should not be collapsed by default."); +} + +function testExpandVariables() { + let deferred = promise.defer(); + + let localScope = gVariables.getScopeAtIndex(0); + let localEnums = localScope.target.querySelector(".variables-view-element-details.enum").childNodes; + + let thisVar = gVariables.getItemForNode(localEnums[0]); + let argsVar = gVariables.getItemForNode(localEnums[8]); + let cVar = gVariables.getItemForNode(localEnums[10]); + + is(thisVar.target.querySelector(".name").getAttribute("value"), "this", + "Should have the right property name for 'this'."); + is(argsVar.target.querySelector(".name").getAttribute("value"), "arguments", + "Should have the right property name for 'arguments'."); + is(cVar.target.querySelector(".name").getAttribute("value"), "c", + "Should have the right property name for 'c'."); + + is(thisVar.expanded, false, + "The thisVar should not be expanded at this point."); + is(argsVar.expanded, false, + "The argsVar should not be expanded at this point."); + is(cVar.expanded, false, + "The cVar should not be expanded at this point."); + + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 3).then(() => { + is(thisVar.get("window").target.querySelector(".name").getAttribute("value"), "window", + "Should have the right property name for 'window'."); + is(thisVar.get("window").target.querySelector(".value").getAttribute("value"), + "Window \u2192 doc_frame-parameters.html", + "Should have the right property value for 'window'."); + ok(thisVar.get("window").target.querySelector(".value").className.contains("token-other"), + "Should have the right token class for 'window'."); + + is(thisVar.get("document").target.querySelector(".name").getAttribute("value"), "document", + "Should have the right property name for 'document'."); + is(thisVar.get("document").target.querySelector(".value").getAttribute("value"), + "HTMLDocument \u2192 doc_frame-parameters.html", + "Should have the right property value for 'document'."); + ok(thisVar.get("document").target.querySelector(".value").className.contains("token-domnode"), + "Should have the right token class for 'document'."); + + let argsProps = argsVar.target.querySelectorAll(".variables-view-property"); + is(argsProps.length, 8, + "The 'arguments' variable should contain 5 enumerable and 3 non-enumerable properties"); + + is(argsProps[0].querySelector(".name").getAttribute("value"), "0", + "Should have the right property name for '0'."); + is(argsProps[0].querySelector(".value").getAttribute("value"), "Object", + "Should have the right property value for '0'."); + ok(argsProps[0].querySelector(".value").className.contains("token-other"), + "Should have the right token class for '0'."); + + is(argsProps[1].querySelector(".name").getAttribute("value"), "1", + "Should have the right property name for '1'."); + is(argsProps[1].querySelector(".value").getAttribute("value"), "\"beta\"", + "Should have the right property value for '1'."); + ok(argsProps[1].querySelector(".value").className.contains("token-string"), + "Should have the right token class for '1'."); + + is(argsProps[2].querySelector(".name").getAttribute("value"), "2", + "Should have the right property name for '2'."); + is(argsProps[2].querySelector(".value").getAttribute("value"), "3", + "Should have the right property name for '2'."); + ok(argsProps[2].querySelector(".value").className.contains("token-number"), + "Should have the right token class for '2'."); + + is(argsProps[3].querySelector(".name").getAttribute("value"), "3", + "Should have the right property name for '3'."); + is(argsProps[3].querySelector(".value").getAttribute("value"), "false", + "Should have the right property value for '3'."); + ok(argsProps[3].querySelector(".value").className.contains("token-boolean"), + "Should have the right token class for '3'."); + + is(argsProps[4].querySelector(".name").getAttribute("value"), "4", + "Should have the right property name for '4'."); + is(argsProps[4].querySelector(".value").getAttribute("value"), "null", + "Should have the right property name for '4'."); + ok(argsProps[4].querySelector(".value").className.contains("token-null"), + "Should have the right token class for '4'."); + + is(gVariables.getItemForNode(argsProps[0]).target, + argsVar.target.querySelectorAll(".variables-view-property")[0], + "getItemForNode([0]) didn't return the expected property."); + + is(gVariables.getItemForNode(argsProps[1]).target, + argsVar.target.querySelectorAll(".variables-view-property")[1], + "getItemForNode([1]) didn't return the expected property."); + + is(gVariables.getItemForNode(argsProps[2]).target, + argsVar.target.querySelectorAll(".variables-view-property")[2], + "getItemForNode([2]) didn't return the expected property."); + + is(argsVar.find(argsProps[0]).target, + argsVar.target.querySelectorAll(".variables-view-property")[0], + "find([0]) didn't return the expected property."); + + is(argsVar.find(argsProps[1]).target, + argsVar.target.querySelectorAll(".variables-view-property")[1], + "find([1]) didn't return the expected property."); + + is(argsVar.find(argsProps[2]).target, + argsVar.target.querySelectorAll(".variables-view-property")[2], + "find([2]) didn't return the expected property."); + + let cProps = cVar.target.querySelectorAll(".variables-view-property"); + is(cProps.length, 7, + "The 'c' variable should contain 6 enumerable and 1 non-enumerable properties"); + + is(cProps[0].querySelector(".name").getAttribute("value"), "a", + "Should have the right property name for 'a'."); + is(cProps[0].querySelector(".value").getAttribute("value"), "1", + "Should have the right property value for 'a'."); + ok(cProps[0].querySelector(".value").className.contains("token-number"), + "Should have the right token class for 'a'."); + + is(cProps[1].querySelector(".name").getAttribute("value"), "b", + "Should have the right property name for 'b'."); + is(cProps[1].querySelector(".value").getAttribute("value"), "\"beta\"", + "Should have the right property value for 'b'."); + ok(cProps[1].querySelector(".value").className.contains("token-string"), + "Should have the right token class for 'b'."); + + is(cProps[2].querySelector(".name").getAttribute("value"), "c", + "Should have the right property name for 'c'."); + is(cProps[2].querySelector(".value").getAttribute("value"), "3", + "Should have the right property value for 'c'."); + ok(cProps[2].querySelector(".value").className.contains("token-number"), + "Should have the right token class for 'c'."); + + is(cProps[3].querySelector(".name").getAttribute("value"), "d", + "Should have the right property name for 'd'."); + is(cProps[3].querySelector(".value").getAttribute("value"), "false", + "Should have the right property value for 'd'."); + ok(cProps[3].querySelector(".value").className.contains("token-boolean"), + "Should have the right token class for 'd'."); + + is(cProps[4].querySelector(".name").getAttribute("value"), "e", + "Should have the right property name for 'e'."); + is(cProps[4].querySelector(".value").getAttribute("value"), "null", + "Should have the right property value for 'e'."); + ok(cProps[4].querySelector(".value").className.contains("token-null"), + "Should have the right token class for 'e'."); + + is(cProps[5].querySelector(".name").getAttribute("value"), "f", + "Should have the right property name for 'f'."); + is(cProps[5].querySelector(".value").getAttribute("value"), "undefined", + "Should have the right property value for 'f'."); + ok(cProps[5].querySelector(".value").className.contains("token-undefined"), + "Should have the right token class for 'f'."); + + is(gVariables.getItemForNode(cProps[0]).target, + cVar.target.querySelectorAll(".variables-view-property")[0], + "getItemForNode([0]) didn't return the expected property."); + + is(gVariables.getItemForNode(cProps[1]).target, + cVar.target.querySelectorAll(".variables-view-property")[1], + "getItemForNode([1]) didn't return the expected property."); + + is(gVariables.getItemForNode(cProps[2]).target, + cVar.target.querySelectorAll(".variables-view-property")[2], + "getItemForNode([2]) didn't return the expected property."); + + is(cVar.find(cProps[0]).target, + cVar.target.querySelectorAll(".variables-view-property")[0], + "find([0]) didn't return the expected property."); + + is(cVar.find(cProps[1]).target, + cVar.target.querySelectorAll(".variables-view-property")[1], + "find([1]) didn't return the expected property."); + + is(cVar.find(cProps[2]).target, + cVar.target.querySelectorAll(".variables-view-property")[2], + "find([2]) didn't return the expected property."); + }); + + // Expand the 'this', 'arguments' and 'c' variables view nodes. This causes + // their properties to be retrieved and displayed. + thisVar.expand(); + argsVar.expand(); + cVar.expand(); + + is(thisVar.expanded, true, + "The thisVar should be immediately marked as expanded."); + is(argsVar.expanded, true, + "The argsVar should be immediately marked as expanded."); + is(cVar.expanded, true, + "The cVar should be immediately marked as expanded."); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gVariables = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-frame-parameters-02.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-frame-parameters-02.js new file mode 100644 index 000000000..b1f482317 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-frame-parameters-02.js @@ -0,0 +1,546 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that the variables view displays the right variables and + * properties when debugger is paused. + */ + +const TAB_URL = EXAMPLE_URL + "doc_frame-parameters.html"; + +let gTab, gPanel, gDebugger; +let gVariables; + +function test() { + // Debug test slaves are a bit slow at this test. + requestLongerTimeout(2); + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gVariables = gDebugger.DebuggerView.Variables; + + waitForSourceAndCaretAndScopes(gPanel, ".html", 24) + .then(testScopeVariables) + .then(testArgumentsProperties) + .then(testSimpleObject) + .then(testComplexObject) + .then(testArgumentObject) + .then(testInnerArgumentObject) + .then(testGetterSetterObject) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + + sendMouseClickToTab(gTab, content.document.querySelector("button")); + }); +} + +function testScopeVariables() { + let localScope = gVariables.getScopeAtIndex(0); + is(localScope.expanded, true, + "The local scope should be expanded by default."); + + let localEnums = localScope.target.querySelector(".variables-view-element-details.enum").childNodes; + let localNonEnums = localScope.target.querySelector(".variables-view-element-details.nonenum").childNodes; + + is(localEnums.length, 12, + "The local scope should contain all the created enumerable elements."); + is(localNonEnums.length, 0, + "The local scope should contain all the created non-enumerable elements."); + + is(localEnums[0].querySelector(".name").getAttribute("value"), "this", + "Should have the right property name for 'this'."); + is(localEnums[0].querySelector(".value").getAttribute("value"), + "Window \u2192 doc_frame-parameters.html", + "Should have the right property value for 'this'."); + ok(localEnums[0].querySelector(".value").className.contains("token-other"), + "Should have the right token class for 'this'."); + + is(localEnums[1].querySelector(".name").getAttribute("value"), "aArg", + "Should have the right property name for 'aArg'."); + is(localEnums[1].querySelector(".value").getAttribute("value"), "Object", + "Should have the right property value for 'aArg'."); + ok(localEnums[1].querySelector(".value").className.contains("token-other"), + "Should have the right token class for 'aArg'."); + + is(localEnums[2].querySelector(".name").getAttribute("value"), "bArg", + "Should have the right property name for 'bArg'."); + is(localEnums[2].querySelector(".value").getAttribute("value"), "\"beta\"", + "Should have the right property value for 'bArg'."); + ok(localEnums[2].querySelector(".value").className.contains("token-string"), + "Should have the right token class for 'bArg'."); + + is(localEnums[3].querySelector(".name").getAttribute("value"), "cArg", + "Should have the right property name for 'cArg'."); + is(localEnums[3].querySelector(".value").getAttribute("value"), "3", + "Should have the right property value for 'cArg'."); + ok(localEnums[3].querySelector(".value").className.contains("token-number"), + "Should have the right token class for 'cArg'."); + + is(localEnums[4].querySelector(".name").getAttribute("value"), "dArg", + "Should have the right property name for 'dArg'."); + is(localEnums[4].querySelector(".value").getAttribute("value"), "false", + "Should have the right property value for 'dArg'."); + ok(localEnums[4].querySelector(".value").className.contains("token-boolean"), + "Should have the right token class for 'dArg'."); + + is(localEnums[5].querySelector(".name").getAttribute("value"), "eArg", + "Should have the right property name for 'eArg'."); + is(localEnums[5].querySelector(".value").getAttribute("value"), "null", + "Should have the right property value for 'eArg'."); + ok(localEnums[5].querySelector(".value").className.contains("token-null"), + "Should have the right token class for 'eArg'."); + + is(localEnums[6].querySelector(".name").getAttribute("value"), "fArg", + "Should have the right property name for 'fArg'."); + is(localEnums[6].querySelector(".value").getAttribute("value"), "undefined", + "Should have the right property value for 'fArg'."); + ok(localEnums[6].querySelector(".value").className.contains("token-undefined"), + "Should have the right token class for 'fArg'."); + + is(localEnums[7].querySelector(".name").getAttribute("value"), "a", + "Should have the right property name for 'a'."); + is(localEnums[7].querySelector(".value").getAttribute("value"), "1", + "Should have the right property value for 'a'."); + ok(localEnums[7].querySelector(".value").className.contains("token-number"), + "Should have the right token class for 'a'."); + + is(localEnums[8].querySelector(".name").getAttribute("value"), "arguments", + "Should have the right property name for 'arguments'."); + is(localEnums[8].querySelector(".value").getAttribute("value"), "Arguments", + "Should have the right property value for 'arguments'."); + ok(localEnums[8].querySelector(".value").className.contains("token-other"), + "Should have the right token class for 'arguments'."); + + is(localEnums[9].querySelector(".name").getAttribute("value"), "b", + "Should have the right property name for 'b'."); + is(localEnums[9].querySelector(".value").getAttribute("value"), "Object", + "Should have the right property value for 'b'."); + ok(localEnums[9].querySelector(".value").className.contains("token-other"), + "Should have the right token class for 'b'."); + + is(localEnums[10].querySelector(".name").getAttribute("value"), "c", + "Should have the right property name for 'c'."); + is(localEnums[10].querySelector(".value").getAttribute("value"), "Object", + "Should have the right property value for 'c'."); + ok(localEnums[10].querySelector(".value").className.contains("token-other"), + "Should have the right token class for 'c'."); + + is(localEnums[11].querySelector(".name").getAttribute("value"), "myVar", + "Should have the right property name for 'myVar'."); + is(localEnums[11].querySelector(".value").getAttribute("value"), "Object", + "Should have the right property value for 'myVar'."); + ok(localEnums[11].querySelector(".value").className.contains("token-other"), + "Should have the right token class for 'myVar'."); +} + +function testArgumentsProperties() { + let deferred = promise.defer(); + + let argsVar = gVariables.getScopeAtIndex(0).get("arguments"); + is(argsVar.expanded, false, + "The 'arguments' variable should not be expanded by default."); + + let argsEnums = argsVar.target.querySelector(".variables-view-element-details.enum").childNodes; + let argsNonEnums = argsVar.target.querySelector(".variables-view-element-details.nonenum").childNodes; + + gDebugger.once(gDebugger.EVENTS.FETCHED_PROPERTIES, () => { + is(argsEnums.length, 5, + "The 'arguments' variable should contain all the created enumerable elements."); + is(argsNonEnums.length, 3, + "The 'arguments' variable should contain all the created non-enumerable elements."); + + is(argsEnums[0].querySelector(".name").getAttribute("value"), "0", + "Should have the right property name for '0'."); + is(argsEnums[0].querySelector(".value").getAttribute("value"), "Object", + "Should have the right property value for '0'."); + ok(argsEnums[0].querySelector(".value").className.contains("token-other"), + "Should have the right token class for '0'."); + + is(argsEnums[1].querySelector(".name").getAttribute("value"), "1", + "Should have the right property name for '1'."); + is(argsEnums[1].querySelector(".value").getAttribute("value"), "\"beta\"", + "Should have the right property value for '1'."); + ok(argsEnums[1].querySelector(".value").className.contains("token-string"), + "Should have the right token class for '1'."); + + is(argsEnums[2].querySelector(".name").getAttribute("value"), "2", + "Should have the right property name for '2'."); + is(argsEnums[2].querySelector(".value").getAttribute("value"), "3", + "Should have the right property name for '2'."); + ok(argsEnums[2].querySelector(".value").className.contains("token-number"), + "Should have the right token class for '2'."); + + is(argsEnums[3].querySelector(".name").getAttribute("value"), "3", + "Should have the right property name for '3'."); + is(argsEnums[3].querySelector(".value").getAttribute("value"), "false", + "Should have the right property value for '3'."); + ok(argsEnums[3].querySelector(".value").className.contains("token-boolean"), + "Should have the right token class for '3'."); + + is(argsEnums[4].querySelector(".name").getAttribute("value"), "4", + "Should have the right property name for '4'."); + is(argsEnums[4].querySelector(".value").getAttribute("value"), "null", + "Should have the right property name for '4'."); + ok(argsEnums[4].querySelector(".value").className.contains("token-null"), + "Should have the right token class for '4'."); + + is(argsNonEnums[0].querySelector(".name").getAttribute("value"), "callee", + "Should have the right property name for 'callee'."); + is(argsNonEnums[0].querySelector(".value").getAttribute("value"), + "test(aArg,bArg,cArg,dArg,eArg,fArg)", + "Should have the right property name for 'callee'."); + ok(argsNonEnums[0].querySelector(".value").className.contains("token-other"), + "Should have the right token class for 'callee'."); + + is(argsNonEnums[1].querySelector(".name").getAttribute("value"), "length", + "Should have the right property name for 'length'."); + is(argsNonEnums[1].querySelector(".value").getAttribute("value"), "5", + "Should have the right property value for 'length'."); + ok(argsNonEnums[1].querySelector(".value").className.contains("token-number"), + "Should have the right token class for 'length'."); + + is(argsNonEnums[2].querySelector(".name").getAttribute("value"), "__proto__", + "Should have the right property name for '__proto__'."); + is(argsNonEnums[2].querySelector(".value").getAttribute("value"), "Object", + "Should have the right property value for '__proto__'."); + ok(argsNonEnums[2].querySelector(".value").className.contains("token-other"), + "Should have the right token class for '__proto__'."); + + deferred.resolve(); + }); + + argsVar.expand(); + return deferred.promise; +} + +function testSimpleObject() { + let deferred = promise.defer(); + + let bVar = gVariables.getScopeAtIndex(0).get("b"); + is(bVar.expanded, false, + "The 'b' variable should not be expanded by default."); + + let bEnums = bVar.target.querySelector(".variables-view-element-details.enum").childNodes; + let bNonEnums = bVar.target.querySelector(".variables-view-element-details.nonenum").childNodes; + + gDebugger.once(gDebugger.EVENTS.FETCHED_PROPERTIES, () => { + is(bEnums.length, 1, + "The 'b' variable should contain all the created enumerable elements."); + is(bNonEnums.length, 1, + "The 'b' variable should contain all the created non-enumerable elements."); + + is(bEnums[0].querySelector(".name").getAttribute("value"), "a", + "Should have the right property name for 'a'."); + is(bEnums[0].querySelector(".value").getAttribute("value"), "1", + "Should have the right property value for 'a'."); + ok(bEnums[0].querySelector(".value").className.contains("token-number"), + "Should have the right token class for 'a'."); + + is(bNonEnums[0].querySelector(".name").getAttribute("value"), "__proto__", + "Should have the right property name for '__proto__'."); + is(bNonEnums[0].querySelector(".value").getAttribute("value"), "Object", + "Should have the right property value for '__proto__'."); + ok(bNonEnums[0].querySelector(".value").className.contains("token-other"), + "Should have the right token class for '__proto__'."); + + deferred.resolve(); + }); + + bVar.expand(); + return deferred.promise; +} + +function testComplexObject() { + let deferred = promise.defer(); + + let cVar = gVariables.getScopeAtIndex(0).get("c"); + is(cVar.expanded, false, + "The 'c' variable should not be expanded by default."); + + let cEnums = cVar.target.querySelector(".variables-view-element-details.enum").childNodes; + let cNonEnums = cVar.target.querySelector(".variables-view-element-details.nonenum").childNodes; + + gDebugger.once(gDebugger.EVENTS.FETCHED_PROPERTIES, () => { + is(cEnums.length, 6, + "The 'c' variable should contain all the created enumerable elements."); + is(cNonEnums.length, 1, + "The 'c' variable should contain all the created non-enumerable elements."); + + is(cEnums[0].querySelector(".name").getAttribute("value"), "a", + "Should have the right property name for 'a'."); + is(cEnums[0].querySelector(".value").getAttribute("value"), "1", + "Should have the right property value for 'a'."); + ok(cEnums[0].querySelector(".value").className.contains("token-number"), + "Should have the right token class for 'a'."); + + is(cEnums[1].querySelector(".name").getAttribute("value"), "b", + "Should have the right property name for 'b'."); + is(cEnums[1].querySelector(".value").getAttribute("value"), "\"beta\"", + "Should have the right property value for 'b'."); + ok(cEnums[1].querySelector(".value").className.contains("token-string"), + "Should have the right token class for 'b'."); + + is(cEnums[2].querySelector(".name").getAttribute("value"), "c", + "Should have the right property name for 'c'."); + is(cEnums[2].querySelector(".value").getAttribute("value"), "3", + "Should have the right property value for 'c'."); + ok(cEnums[2].querySelector(".value").className.contains("token-number"), + "Should have the right token class for 'c'."); + + is(cEnums[3].querySelector(".name").getAttribute("value"), "d", + "Should have the right property name for 'd'."); + is(cEnums[3].querySelector(".value").getAttribute("value"), "false", + "Should have the right property value for 'd'."); + ok(cEnums[3].querySelector(".value").className.contains("token-boolean"), + "Should have the right token class for 'd'."); + + is(cEnums[4].querySelector(".name").getAttribute("value"), "e", + "Should have the right property name for 'e'."); + is(cEnums[4].querySelector(".value").getAttribute("value"), "null", + "Should have the right property value for 'e'."); + ok(cEnums[4].querySelector(".value").className.contains("token-null"), + "Should have the right token class for 'e'."); + + is(cEnums[5].querySelector(".name").getAttribute("value"), "f", + "Should have the right property name for 'f'."); + is(cEnums[5].querySelector(".value").getAttribute("value"), "undefined", + "Should have the right property value for 'f'."); + ok(cEnums[5].querySelector(".value").className.contains("token-undefined"), + "Should have the right token class for 'f'."); + + is(cNonEnums[0].querySelector(".name").getAttribute("value"), "__proto__", + "Should have the right property name for '__proto__'."); + is(cNonEnums[0].querySelector(".value").getAttribute("value"), "Object", + "Should have the right property value for '__proto__'."); + ok(cNonEnums[0].querySelector(".value").className.contains("token-other"), + "Should have the right token class for '__proto__'."); + + deferred.resolve(); + }); + + cVar.expand(); + return deferred.promise; +} + +function testArgumentObject() { + let deferred = promise.defer(); + + let argVar = gVariables.getScopeAtIndex(0).get("aArg"); + is(argVar.expanded, false, + "The 'aArg' variable should not be expanded by default."); + + let argEnums = argVar.target.querySelector(".variables-view-element-details.enum").childNodes; + let argNonEnums = argVar.target.querySelector(".variables-view-element-details.nonenum").childNodes; + + gDebugger.once(gDebugger.EVENTS.FETCHED_PROPERTIES, () => { + is(argEnums.length, 6, + "The 'aArg' variable should contain all the created enumerable elements."); + is(argNonEnums.length, 1, + "The 'aArg' variable should contain all the created non-enumerable elements."); + + is(argEnums[0].querySelector(".name").getAttribute("value"), "a", + "Should have the right property name for 'a'."); + is(argEnums[0].querySelector(".value").getAttribute("value"), "1", + "Should have the right property value for 'a'."); + ok(argEnums[0].querySelector(".value").className.contains("token-number"), + "Should have the right token class for 'a'."); + + is(argEnums[1].querySelector(".name").getAttribute("value"), "b", + "Should have the right property name for 'b'."); + is(argEnums[1].querySelector(".value").getAttribute("value"), "\"beta\"", + "Should have the right property value for 'b'."); + ok(argEnums[1].querySelector(".value").className.contains("token-string"), + "Should have the right token class for 'b'."); + + is(argEnums[2].querySelector(".name").getAttribute("value"), "c", + "Should have the right property name for 'c'."); + is(argEnums[2].querySelector(".value").getAttribute("value"), "3", + "Should have the right property value for 'c'."); + ok(argEnums[2].querySelector(".value").className.contains("token-number"), + "Should have the right token class for 'c'."); + + is(argEnums[3].querySelector(".name").getAttribute("value"), "d", + "Should have the right property name for 'd'."); + is(argEnums[3].querySelector(".value").getAttribute("value"), "false", + "Should have the right property value for 'd'."); + ok(argEnums[3].querySelector(".value").className.contains("token-boolean"), + "Should have the right token class for 'd'."); + + is(argEnums[4].querySelector(".name").getAttribute("value"), "e", + "Should have the right property name for 'e'."); + is(argEnums[4].querySelector(".value").getAttribute("value"), "null", + "Should have the right property value for 'e'."); + ok(argEnums[4].querySelector(".value").className.contains("token-null"), + "Should have the right token class for 'e'."); + + is(argEnums[5].querySelector(".name").getAttribute("value"), "f", + "Should have the right property name for 'f'."); + is(argEnums[5].querySelector(".value").getAttribute("value"), "undefined", + "Should have the right property value for 'f'."); + ok(argEnums[5].querySelector(".value").className.contains("token-undefined"), + "Should have the right token class for 'f'."); + + is(argNonEnums[0].querySelector(".name").getAttribute("value"), "__proto__", + "Should have the right property name for '__proto__'."); + is(argNonEnums[0].querySelector(".value").getAttribute("value"), "Object", + "Should have the right property value for '__proto__'."); + ok(argNonEnums[0].querySelector(".value").className.contains("token-other"), + "Should have the right token class for '__proto__'."); + + deferred.resolve(); + }); + + argVar.expand(); + return deferred.promise; +} + +function testInnerArgumentObject() { + let deferred = promise.defer(); + + let argProp = gVariables.getScopeAtIndex(0).get("arguments").get("0"); + is(argProp.expanded, false, + "The 'arguments[0]' property should not be expanded by default."); + + let argEnums = argProp.target.querySelector(".variables-view-element-details.enum").childNodes; + let argNonEnums = argProp.target.querySelector(".variables-view-element-details.nonenum").childNodes; + + gDebugger.once(gDebugger.EVENTS.FETCHED_PROPERTIES, () => { + is(argEnums.length, 6, + "The 'arguments[0]' property should contain all the created enumerable elements."); + is(argNonEnums.length, 1, + "The 'arguments[0]' property should contain all the created non-enumerable elements."); + + is(argEnums[0].querySelector(".name").getAttribute("value"), "a", + "Should have the right property name for 'a'."); + is(argEnums[0].querySelector(".value").getAttribute("value"), "1", + "Should have the right property value for 'a'."); + ok(argEnums[0].querySelector(".value").className.contains("token-number"), + "Should have the right token class for 'a'."); + + is(argEnums[1].querySelector(".name").getAttribute("value"), "b", + "Should have the right property name for 'b'."); + is(argEnums[1].querySelector(".value").getAttribute("value"), "\"beta\"", + "Should have the right property value for 'b'."); + ok(argEnums[1].querySelector(".value").className.contains("token-string"), + "Should have the right token class for 'b'."); + + is(argEnums[2].querySelector(".name").getAttribute("value"), "c", + "Should have the right property name for 'c'."); + is(argEnums[2].querySelector(".value").getAttribute("value"), "3", + "Should have the right property value for 'c'."); + ok(argEnums[2].querySelector(".value").className.contains("token-number"), + "Should have the right token class for 'c'."); + + is(argEnums[3].querySelector(".name").getAttribute("value"), "d", + "Should have the right property name for 'd'."); + is(argEnums[3].querySelector(".value").getAttribute("value"), "false", + "Should have the right property value for 'd'."); + ok(argEnums[3].querySelector(".value").className.contains("token-boolean"), + "Should have the right token class for 'd'."); + + is(argEnums[4].querySelector(".name").getAttribute("value"), "e", + "Should have the right property name for 'e'."); + is(argEnums[4].querySelector(".value").getAttribute("value"), "null", + "Should have the right property value for 'e'."); + ok(argEnums[4].querySelector(".value").className.contains("token-null"), + "Should have the right token class for 'e'."); + + is(argEnums[5].querySelector(".name").getAttribute("value"), "f", + "Should have the right property name for 'f'."); + is(argEnums[5].querySelector(".value").getAttribute("value"), "undefined", + "Should have the right property value for 'f'."); + ok(argEnums[5].querySelector(".value").className.contains("token-undefined"), + "Should have the right token class for 'f'."); + + is(argNonEnums[0].querySelector(".name").getAttribute("value"), "__proto__", + "Should have the right property name for '__proto__'."); + is(argNonEnums[0].querySelector(".value").getAttribute("value"), "Object", + "Should have the right property value for '__proto__'."); + ok(argNonEnums[0].querySelector(".value").className.contains("token-other"), + "Should have the right token class for '__proto__'."); + + deferred.resolve(); + }); + + argProp.expand(); + return deferred.promise; +} + +function testGetterSetterObject() { + let deferred = promise.defer(); + + let myVar = gVariables.getScopeAtIndex(0).get("myVar"); + is(myVar.expanded, false, + "The myVar variable should not be expanded by default."); + + let myVarEnums = myVar.target.querySelector(".variables-view-element-details.enum").childNodes; + let myVarNonEnums = myVar.target.querySelector(".variables-view-element-details.nonenum").childNodes; + + gDebugger.once(gDebugger.EVENTS.FETCHED_PROPERTIES, () => { + is(myVarEnums.length, 2, + "The myVar should contain all the created enumerable elements."); + is(myVarNonEnums.length, 1, + "The myVar should contain all the created non-enumerable elements."); + + is(myVarEnums[0].querySelector(".name").getAttribute("value"), "_prop", + "Should have the right property name for '_prop'."); + is(myVarEnums[0].querySelector(".value").getAttribute("value"), "42", + "Should have the right property value for '_prop'."); + ok(myVarEnums[0].querySelector(".value").className.contains("token-number"), + "Should have the right token class for '_prop'."); + + is(myVarEnums[1].querySelector(".name").getAttribute("value"), "prop", + "Should have the right property name for 'prop'."); + is(myVarEnums[1].querySelector(".value").getAttribute("value"), "", + "Should have the right property value for 'prop'."); + ok(!myVarEnums[1].querySelector(".value").className.contains("token"), + "Should have no token class for 'prop'."); + + is(myVarNonEnums[0].querySelector(".name").getAttribute("value"), "__proto__", + "Should have the right property name for '__proto__'."); + is(myVarNonEnums[0].querySelector(".value").getAttribute("value"), "Object", + "Should have the right property value for '__proto__'."); + ok(myVarNonEnums[0].querySelector(".value").className.contains("token-other"), + "Should have the right token class for '__proto__'."); + + let propEnums = myVarEnums[1].querySelector(".variables-view-element-details.enum").childNodes; + let propNonEnums = myVarEnums[1].querySelector(".variables-view-element-details.nonenum").childNodes; + + is(propEnums.length, 0, + "The propEnums should contain all the created enumerable elements."); + is(propNonEnums.length, 2, + "The propEnums should contain all the created non-enumerable elements."); + + is(propNonEnums[0].querySelector(".name").getAttribute("value"), "get", + "Should have the right property name for 'get'."); + is(propNonEnums[0].querySelector(".value").getAttribute("value"), + "test/myVar.prop()", + "Should have the right property value for 'get'."); + ok(propNonEnums[0].querySelector(".value").className.contains("token-other"), + "Should have the right token class for 'get'."); + + is(propNonEnums[1].querySelector(".name").getAttribute("value"), "set", + "Should have the right property name for 'set'."); + is(propNonEnums[1].querySelector(".value").getAttribute("value"), + "test/myVar.prop(val)", + "Should have the right property value for 'set'."); + ok(propNonEnums[1].querySelector(".value").className.contains("token-other"), + "Should have the right token class for 'set'."); + + deferred.resolve(); + }); + + myVar.expand(); + return deferred.promise; +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gVariables = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-frame-parameters-03.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-frame-parameters-03.js new file mode 100644 index 000000000..d667ce673 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-frame-parameters-03.js @@ -0,0 +1,151 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that the variables view displays the right variables and + * properties in the global scope when debugger is paused. + */ + +const TAB_URL = EXAMPLE_URL + "doc_frame-parameters.html"; + +let gTab, gPanel, gDebugger; +let gVariables; + +function test() { + // Debug test slaves are a bit slow at this test. + requestLongerTimeout(2); + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gVariables = gDebugger.DebuggerView.Variables; + + waitForSourceAndCaretAndScopes(gPanel, ".html", 24) + .then(expandGlobalScope) + .then(testGlobalScope) + .then(expandWindowVariable) + .then(testWindowVariable) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + + sendMouseClickToTab(gTab, content.document.querySelector("button")); + }); +} + +function expandGlobalScope() { + let deferred = promise.defer(); + + let globalScope = gVariables.getScopeAtIndex(1); + is(globalScope.expanded, false, + "The global scope should not be expanded by default."); + + gDebugger.once(gDebugger.EVENTS.FETCHED_VARIABLES, deferred.resolve); + + EventUtils.sendMouseEvent({ type: "mousedown" }, + globalScope.target.querySelector(".name"), + gDebugger); + + return deferred.promise; +} + +function testGlobalScope() { + let globalScope = gVariables.getScopeAtIndex(1); + is(globalScope.expanded, true, + "The global scope should now be expanded."); + + is(globalScope.get("InstallTrigger").target.querySelector(".name").getAttribute("value"), "InstallTrigger", + "Should have the right property name for 'InstallTrigger'."); + is(globalScope.get("InstallTrigger").target.querySelector(".value").getAttribute("value"), "InstallTriggerImpl", + "Should have the right property value for 'InstallTrigger'."); + + is(globalScope.get("SpecialPowers").target.querySelector(".name").getAttribute("value"), "SpecialPowers", + "Should have the right property name for 'SpecialPowers'."); + is(globalScope.get("SpecialPowers").target.querySelector(".value").getAttribute("value"), "Object", + "Should have the right property value for 'SpecialPowers'."); + + is(globalScope.get("window").target.querySelector(".name").getAttribute("value"), "window", + "Should have the right property name for 'window'."); + is(globalScope.get("window").target.querySelector(".value").getAttribute("value"), + "Window \u2192 doc_frame-parameters.html", + "Should have the right property value for 'window'."); + + is(globalScope.get("document").target.querySelector(".name").getAttribute("value"), "document", + "Should have the right property name for 'document'."); + is(globalScope.get("document").target.querySelector(".value").getAttribute("value"), + "HTMLDocument \u2192 doc_frame-parameters.html", + "Should have the right property value for 'document'."); + + is(globalScope.get("undefined").target.querySelector(".name").getAttribute("value"), "undefined", + "Should have the right property name for 'undefined'."); + is(globalScope.get("undefined").target.querySelector(".value").getAttribute("value"), "undefined", + "Should have the right property value for 'undefined'."); + + is(globalScope.get("undefined").target.querySelector(".enum").childNodes.length, 0, + "Should have no child enumerable properties for 'undefined'."); + is(globalScope.get("undefined").target.querySelector(".nonenum").childNodes.length, 0, + "Should have no child non-enumerable properties for 'undefined'."); +} + +function expandWindowVariable() { + let deferred = promise.defer(); + + let windowVar = gVariables.getScopeAtIndex(1).get("window"); + is(windowVar.expanded, false, + "The window variable should not be expanded by default."); + + gDebugger.once(gDebugger.EVENTS.FETCHED_PROPERTIES, deferred.resolve); + + EventUtils.sendMouseEvent({ type: "mousedown" }, + windowVar.target.querySelector(".name"), + gDebugger); + + return deferred.promise; +} + +function testWindowVariable() { + let windowVar = gVariables.getScopeAtIndex(1).get("window"); + is(windowVar.expanded, true, + "The window variable should now be expanded."); + + is(windowVar.get("InstallTrigger").target.querySelector(".name").getAttribute("value"), "InstallTrigger", + "Should have the right property name for 'InstallTrigger'."); + is(windowVar.get("InstallTrigger").target.querySelector(".value").getAttribute("value"), "InstallTriggerImpl", + "Should have the right property value for 'InstallTrigger'."); + + is(windowVar.get("SpecialPowers").target.querySelector(".name").getAttribute("value"), "SpecialPowers", + "Should have the right property name for 'SpecialPowers'."); + is(windowVar.get("SpecialPowers").target.querySelector(".value").getAttribute("value"), "Object", + "Should have the right property value for 'SpecialPowers'."); + + is(windowVar.get("window").target.querySelector(".name").getAttribute("value"), "window", + "Should have the right property name for 'window'."); + is(windowVar.get("window").target.querySelector(".value").getAttribute("value"), + "Window \u2192 doc_frame-parameters.html", + "Should have the right property value for 'window'."); + + is(windowVar.get("document").target.querySelector(".name").getAttribute("value"), "document", + "Should have the right property name for 'document'."); + is(windowVar.get("document").target.querySelector(".value").getAttribute("value"), + "HTMLDocument \u2192 doc_frame-parameters.html", + "Should have the right property value for 'document'."); + + is(windowVar.get("undefined").target.querySelector(".name").getAttribute("value"), "undefined", + "Should have the right property name for 'undefined'."); + is(windowVar.get("undefined").target.querySelector(".value").getAttribute("value"), "undefined", + "Should have the right property value for 'undefined'."); + + is(windowVar.get("undefined").target.querySelector(".enum").childNodes.length, 0, + "Should have no child enumerable properties for 'undefined'."); + is(windowVar.get("undefined").target.querySelector(".nonenum").childNodes.length, 0, + "Should have no child non-enumerable properties for 'undefined'."); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gVariables = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-frame-with.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-frame-with.js new file mode 100644 index 000000000..b96032174 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-frame-with.js @@ -0,0 +1,207 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that the variables view is correctly populated in 'with' frames. + */ + +const TAB_URL = EXAMPLE_URL + "doc_with-frame.html"; + +let gTab, gPanel, gDebugger; +let gVariables; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gVariables = gDebugger.DebuggerView.Variables; + + // The first 'with' scope should be expanded by default, but the + // variables haven't been fetched yet. This is how 'with' scopes work. + promise.all([ + waitForSourceAndCaret(gPanel, ".html", 22), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_VARIABLES) + ]).then(testFirstWithScope) + .then(expandSecondWithScope) + .then(testSecondWithScope) + .then(expandFunctionScope) + .then(testFunctionScope) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + + sendMouseClickToTab(gTab, content.document.querySelector("button")); + }); +} + +function testFirstWithScope() { + let firstWithScope = gVariables.getScopeAtIndex(0); + is(firstWithScope.expanded, true, + "The first 'with' scope should be expanded by default."); + ok(firstWithScope.target.querySelector(".name").getAttribute("value").contains("[Object]"), + "The first 'with' scope should be properly identified."); + + let withEnums = firstWithScope._enum.childNodes; + let withNonEnums = firstWithScope._nonenum.childNodes; + + is(withEnums.length, 3, + "The first 'with' scope should contain all the created enumerable elements."); + is(withNonEnums.length, 1, + "The first 'with' scope should contain all the created non-enumerable elements."); + + is(withEnums[0].querySelector(".name").getAttribute("value"), "this", + "Should have the right property name for 'this'."); + is(withEnums[0].querySelector(".value").getAttribute("value"), + "Window \u2192 doc_with-frame.html", + "Should have the right property value for 'this'."); + ok(withEnums[0].querySelector(".value").className.contains("token-other"), + "Should have the right token class for 'this'."); + + is(withEnums[1].querySelector(".name").getAttribute("value"), "alpha", + "Should have the right property name for 'alpha'."); + is(withEnums[1].querySelector(".value").getAttribute("value"), "1", + "Should have the right property value for 'alpha'."); + ok(withEnums[1].querySelector(".value").className.contains("token-number"), + "Should have the right token class for 'alpha'."); + + is(withEnums[2].querySelector(".name").getAttribute("value"), "beta", + "Should have the right property name for 'beta'."); + is(withEnums[2].querySelector(".value").getAttribute("value"), "2", + "Should have the right property value for 'beta'."); + ok(withEnums[2].querySelector(".value").className.contains("token-number"), + "Should have the right token class for 'beta'."); + + is(withNonEnums[0].querySelector(".name").getAttribute("value"), "__proto__", + "Should have the right property name for '__proto__'."); + is(withNonEnums[0].querySelector(".value").getAttribute("value"), "Object", + "Should have the right property value for '__proto__'."); + ok(withNonEnums[0].querySelector(".value").className.contains("token-other"), + "Should have the right token class for '__proto__'."); +} + +function expandSecondWithScope() { + let deferred = promise.defer(); + + let secondWithScope = gVariables.getScopeAtIndex(1); + is(secondWithScope.expanded, false, + "The second 'with' scope should not be expanded by default."); + + gDebugger.once(gDebugger.EVENTS.FETCHED_VARIABLES, deferred.resolve); + + EventUtils.sendMouseEvent({ type: "mousedown" }, + secondWithScope.target.querySelector(".name"), + gDebugger); + + return deferred.promise; +} + +function testSecondWithScope() { + let secondWithScope = gVariables.getScopeAtIndex(1); + is(secondWithScope.expanded, true, + "The second 'with' scope should now be expanded."); + ok(secondWithScope.target.querySelector(".name").getAttribute("value").contains("[Math]"), + "The second 'with' scope should be properly identified."); + + let withEnums = secondWithScope._enum.childNodes; + let withNonEnums = secondWithScope._nonenum.childNodes; + + is(withEnums.length, 0, + "The second 'with' scope should contain all the created enumerable elements."); + isnot(withNonEnums.length, 0, + "The second 'with' scope should contain all the created non-enumerable elements."); + + is(secondWithScope.get("E").target.querySelector(".name").getAttribute("value"), "E", + "Should have the right property name for 'E'."); + is(secondWithScope.get("E").target.querySelector(".value").getAttribute("value"), "2.718281828459045", + "Should have the right property value for 'E'."); + ok(secondWithScope.get("E").target.querySelector(".value").className.contains("token-number"), + "Should have the right token class for 'E'."); + + is(secondWithScope.get("PI").target.querySelector(".name").getAttribute("value"), "PI", + "Should have the right property name for 'PI'."); + is(secondWithScope.get("PI").target.querySelector(".value").getAttribute("value"), "3.141592653589793", + "Should have the right property value for 'PI'."); + ok(secondWithScope.get("PI").target.querySelector(".value").className.contains("token-number"), + "Should have the right token class for 'PI'."); + + is(secondWithScope.get("random").target.querySelector(".name").getAttribute("value"), "random", + "Should have the right property name for 'random'."); + is(secondWithScope.get("random").target.querySelector(".value").getAttribute("value"), "random()", + "Should have the right property value for 'random'."); + ok(secondWithScope.get("random").target.querySelector(".value").className.contains("token-other"), + "Should have the right token class for 'random'."); + + is(secondWithScope.get("__proto__").target.querySelector(".name").getAttribute("value"), "__proto__", + "Should have the right property name for '__proto__'."); + is(secondWithScope.get("__proto__").target.querySelector(".value").getAttribute("value"), "Object", + "Should have the right property value for '__proto__'."); + ok(secondWithScope.get("__proto__").target.querySelector(".value").className.contains("token-other"), + "Should have the right token class for '__proto__'."); +} + +function expandFunctionScope() { + let funcScope = gVariables.getScopeAtIndex(2); + is(funcScope.expanded, false, + "The function scope shouldn't be expanded by default, but the " + + "variables have been already fetched. This is how local scopes work."); + + EventUtils.sendMouseEvent({ type: "mousedown" }, + funcScope.target.querySelector(".name"), + gDebugger); + + return promise.resolve(null); +} + +function testFunctionScope() { + let funcScope = gVariables.getScopeAtIndex(2); + is(funcScope.expanded, true, + "The function scope should now be expanded."); + ok(funcScope.target.querySelector(".name").getAttribute("value").contains("[test]"), + "The function scope should be properly identified."); + + let funcEnums = funcScope._enum.childNodes; + let funcNonEnums = funcScope._nonenum.childNodes; + + is(funcEnums.length, 6, + "The function scope should contain all the created enumerable elements."); + is(funcNonEnums.length, 0, + "The function scope should contain all the created non-enumerable elements."); + + is(funcScope.get("aNumber").target.querySelector(".name").getAttribute("value"), "aNumber", + "Should have the right property name for 'aNumber'."); + is(funcScope.get("aNumber").target.querySelector(".value").getAttribute("value"), "10", + "Should have the right property value for 'aNumber'."); + ok(funcScope.get("aNumber").target.querySelector(".value").className.contains("token-number"), + "Should have the right token class for 'aNumber'."); + + is(funcScope.get("a").target.querySelector(".name").getAttribute("value"), "a", + "Should have the right property name for 'a'."); + is(funcScope.get("a").target.querySelector(".value").getAttribute("value"), "314.1592653589793", + "Should have the right property value for 'a'."); + ok(funcScope.get("a").target.querySelector(".value").className.contains("token-number"), + "Should have the right token class for 'a'."); + + is(funcScope.get("r").target.querySelector(".name").getAttribute("value"), "r", + "Should have the right property name for 'r'."); + is(funcScope.get("r").target.querySelector(".value").getAttribute("value"), "10", + "Should have the right property value for 'r'."); + ok(funcScope.get("r").target.querySelector(".value").className.contains("token-number"), + "Should have the right token class for 'r'."); + + is(funcScope.get("foo").target.querySelector(".name").getAttribute("value"), "foo", + "Should have the right property name for 'foo'."); + is(funcScope.get("foo").target.querySelector(".value").getAttribute("value"), "6.283185307179586", + "Should have the right property value for 'foo'."); + ok(funcScope.get("foo").target.querySelector(".value").className.contains("token-number"), + "Should have the right token class for 'foo'."); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gVariables = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-frozen-sealed-nonext.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-frozen-sealed-nonext.js new file mode 100644 index 000000000..33eafc162 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-frozen-sealed-nonext.js @@ -0,0 +1,87 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This test checks that we properly set the frozen, sealed, and non-extensbile + * attributes on variables so that the F/S/N is shown in the variables view. + */ + +const TAB_URL = EXAMPLE_URL + "doc_frame-parameters.html"; + +let gTab, gPanel, gDebugger; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + + prepareTest(); + }); +} + +function prepareTest() { + gDebugger.once(gDebugger.EVENTS.FETCHED_SCOPES, runTest); + + evalInTab(gTab, "(" + function() { + var frozen = Object.freeze({}); + var sealed = Object.seal({}); + var nonExtensible = Object.preventExtensions({}); + var extensible = {}; + var string = "foo bar baz"; + + debugger; + } + "())"); +} + +function runTest() { + let hasNoneTester = function(aVariable) { + ok(!aVariable.hasAttribute("frozen"), + "The variable should not be frozen."); + ok(!aVariable.hasAttribute("sealed"), + "The variable should not be sealed."); + ok(!aVariable.hasAttribute("non-extensible"), + "The variable should be extensible."); + }; + + let testers = { + frozen: function (aVariable) { + ok(aVariable.hasAttribute("frozen"), + "The variable should be frozen."); + }, + sealed: function (aVariable) { + ok(aVariable.hasAttribute("sealed"), + "The variable should be sealed."); + }, + nonExtensible: function (aVariable) { + ok(aVariable.hasAttribute("non-extensible"), + "The variable should be non-extensible."); + }, + extensible: hasNoneTester, + string: hasNoneTester, + arguments: hasNoneTester, + this: hasNoneTester + }; + + let variables = gDebugger.document.querySelectorAll(".variable-or-property"); + + for (let variable of variables) { + let name = variable.querySelector(".name").getAttribute("value"); + let tester = testers[name]; + delete testers[name]; + + ok(tester, "We should have a tester for the '" + name + "' variable."); + tester(variable); + } + + is(Object.keys(testers).length, 0, + "We should have run and removed all the testers."); + + resumeDebuggerThenCloseAndFinish(gPanel); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-hide-non-enums.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-hide-non-enums.js new file mode 100644 index 000000000..2030a82f9 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-hide-non-enums.js @@ -0,0 +1,105 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that non-enumerable variables and properties can be hidden + * in the variables view. + */ + +const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html"; + +let gTab, gPanel, gDebugger; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + + waitForSourceAndCaretAndScopes(gPanel, ".html", 14).then(performTest); + callInTab(gTab, "simpleCall"); + }); +} + +function performTest() { + let testScope = gDebugger.DebuggerView.Variables.addScope("test-scope"); + let testVar = testScope.addItem("foo"); + + testVar.addItems({ + foo: { + value: "bar", + enumerable: true + }, + bar: { + value: "foo", + enumerable: false + } + }); + + // Expand the scope and variable. + testScope.expand(); + testVar.expand(); + + // Expanding the non-enumerable container is synchronously dispatched + // on the main thread, so wait for the next tick. + executeSoon(() => { + let details = testVar._enum; + let nonenum = testVar._nonenum; + + is(details.childNodes.length, 1, + "There should be just one property in the .details container."); + ok(details.hasAttribute("open"), + ".details container should be visible."); + ok(nonenum.hasAttribute("open"), + ".nonenum container should be visible."); + is(nonenum.childNodes.length, 1, + "There should be just one property in the .nonenum container."); + + // Uncheck 'show hidden properties'. + gDebugger.DebuggerView.Options._showVariablesOnlyEnumItem.setAttribute("checked", "true"); + gDebugger.DebuggerView.Options._toggleShowVariablesOnlyEnum(); + + ok(details.hasAttribute("open"), + ".details container should stay visible."); + ok(!nonenum.hasAttribute("open"), + ".nonenum container should become hidden."); + + // Check 'show hidden properties'. + gDebugger.DebuggerView.Options._showVariablesOnlyEnumItem.setAttribute("checked", "false"); + gDebugger.DebuggerView.Options._toggleShowVariablesOnlyEnum(); + + ok(details.hasAttribute("open"), + ".details container should stay visible."); + ok(nonenum.hasAttribute("open"), + ".nonenum container should become visible."); + + // Collapse the variable. This is done on the current tick. + testVar.collapse(); + + ok(!details.hasAttribute("open"), + ".details container should be hidden."); + ok(!nonenum.hasAttribute("open"), + ".nonenum container should be hidden."); + + // Uncheck 'show hidden properties'. + gDebugger.DebuggerView.Options._showVariablesOnlyEnumItem.setAttribute("checked", "true"); + gDebugger.DebuggerView.Options._toggleShowVariablesOnlyEnum(); + + ok(!details.hasAttribute("open"), + ".details container should stay hidden."); + ok(!nonenum.hasAttribute("open"), + ".nonenum container should stay hidden."); + + // Check 'show hidden properties'. + gDebugger.DebuggerView.Options._showVariablesOnlyEnumItem.setAttribute("checked", "false"); + gDebugger.DebuggerView.Options._toggleShowVariablesOnlyEnum(); + + resumeDebuggerThenCloseAndFinish(gPanel); + }); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-large-array-buffer.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-large-array-buffer.js new file mode 100644 index 000000000..d543b49a4 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-large-array-buffer.js @@ -0,0 +1,239 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that the variables view remains responsive when faced with + * huge ammounts of data. + */ + +const TAB_URL = EXAMPLE_URL + "doc_large-array-buffer.html"; + +let gTab, gPanel, gDebugger; +let gVariables, gEllipsis; + +function test() { + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gVariables = gDebugger.DebuggerView.Variables; + gEllipsis = Services.prefs.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString).data; + + waitForSourceAndCaretAndScopes(gPanel, ".html", 23) + .then(() => initialChecks()) + .then(() => verifyFirstLevel()) + .then(() => verifyNextLevels()) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + + sendMouseClickToTab(gTab, content.document.querySelector("button")); + }); +} + +function initialChecks() { + let localScope = gVariables.getScopeAtIndex(0); + let bufferVar = localScope.get("buffer"); + let arrayVar = localScope.get("largeArray"); + let objectVar = localScope.get("largeObject"); + + ok(bufferVar, "There should be a 'buffer' variable present in the scope."); + ok(arrayVar, "There should be a 'largeArray' variable present in the scope."); + ok(objectVar, "There should be a 'largeObject' variable present in the scope."); + + is(bufferVar.target.querySelector(".name").getAttribute("value"), "buffer", + "Should have the right property name for 'buffer'."); + is(bufferVar.target.querySelector(".value").getAttribute("value"), "ArrayBuffer", + "Should have the right property value for 'buffer'."); + ok(bufferVar.target.querySelector(".value").className.contains("token-other"), + "Should have the right token class for 'buffer'."); + + is(arrayVar.target.querySelector(".name").getAttribute("value"), "largeArray", + "Should have the right property name for 'largeArray'."); + is(arrayVar.target.querySelector(".value").getAttribute("value"), "Int8Array[10000]", + "Should have the right property value for 'largeArray'."); + ok(arrayVar.target.querySelector(".value").className.contains("token-other"), + "Should have the right token class for 'largeArray'."); + + is(objectVar.target.querySelector(".name").getAttribute("value"), "largeObject", + "Should have the right property name for 'largeObject'."); + is(objectVar.target.querySelector(".value").getAttribute("value"), "Object", + "Should have the right property value for 'largeObject'."); + ok(objectVar.target.querySelector(".value").className.contains("token-other"), + "Should have the right token class for 'largeObject'."); + + is(bufferVar.expanded, false, + "The 'buffer' variable shouldn't be expanded."); + is(arrayVar.expanded, false, + "The 'largeArray' variable shouldn't be expanded."); + is(objectVar.expanded, false, + "The 'largeObject' variable shouldn't be expanded."); + + let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 2); + arrayVar.expand(); + objectVar.expand(); + return finished; +} + +function verifyFirstLevel() { + let localScope = gVariables.getScopeAtIndex(0); + let arrayVar = localScope.get("largeArray"); + let objectVar = localScope.get("largeObject"); + + let arrayEnums = arrayVar.target.querySelector(".variables-view-element-details.enum").childNodes; + let arrayNonEnums = arrayVar.target.querySelector(".variables-view-element-details.nonenum").childNodes; + is(arrayEnums.length, 0, + "The 'largeArray' shouldn't contain any enumerable elements."); + is(arrayNonEnums.length, 9, + "The 'largeArray' should contain all the created non-enumerable elements."); + + let objectEnums = objectVar.target.querySelector(".variables-view-element-details.enum").childNodes; + let objectNonEnums = objectVar.target.querySelector(".variables-view-element-details.nonenum").childNodes; + is(objectEnums.length, 0, + "The 'largeObject' shouldn't contain any enumerable elements."); + is(objectNonEnums.length, 5, + "The 'largeObject' should contain all the created non-enumerable elements."); + + is(arrayVar.target.querySelectorAll(".variables-view-property .name")[0].getAttribute("value"), + 0 + gEllipsis + 1999, "The first page in the 'largeArray' is named correctly."); + is(arrayVar.target.querySelectorAll(".variables-view-property .value")[0].getAttribute("value"), + "", "The first page in the 'largeArray' should not have a corresponding value."); + is(arrayVar.target.querySelectorAll(".variables-view-property .name")[1].getAttribute("value"), + 2000 + gEllipsis + 3999, "The second page in the 'largeArray' is named correctly."); + is(arrayVar.target.querySelectorAll(".variables-view-property .value")[1].getAttribute("value"), + "", "The second page in the 'largeArray' should not have a corresponding value."); + is(arrayVar.target.querySelectorAll(".variables-view-property .name")[2].getAttribute("value"), + 4000 + gEllipsis + 5999, "The third page in the 'largeArray' is named correctly."); + is(arrayVar.target.querySelectorAll(".variables-view-property .value")[2].getAttribute("value"), + "", "The third page in the 'largeArray' should not have a corresponding value."); + is(arrayVar.target.querySelectorAll(".variables-view-property .name")[3].getAttribute("value"), + 6000 + gEllipsis + 9999, "The fourth page in the 'largeArray' is named correctly."); + is(arrayVar.target.querySelectorAll(".variables-view-property .value")[3].getAttribute("value"), + "", "The fourth page in the 'largeArray' should not have a corresponding value."); + + is(objectVar.target.querySelectorAll(".variables-view-property .name")[0].getAttribute("value"), + 0 + gEllipsis + 1999, "The first page in the 'largeObject' is named correctly."); + is(objectVar.target.querySelectorAll(".variables-view-property .value")[0].getAttribute("value"), + "", "The first page in the 'largeObject' should not have a corresponding value."); + is(objectVar.target.querySelectorAll(".variables-view-property .name")[1].getAttribute("value"), + 2000 + gEllipsis + 3999, "The second page in the 'largeObject' is named correctly."); + is(objectVar.target.querySelectorAll(".variables-view-property .value")[1].getAttribute("value"), + "", "The second page in the 'largeObject' should not have a corresponding value."); + is(objectVar.target.querySelectorAll(".variables-view-property .name")[2].getAttribute("value"), + 4000 + gEllipsis + 5999, "The thrid page in the 'largeObject' is named correctly."); + is(objectVar.target.querySelectorAll(".variables-view-property .value")[2].getAttribute("value"), + "", "The thrid page in the 'largeObject' should not have a corresponding value."); + is(objectVar.target.querySelectorAll(".variables-view-property .name")[3].getAttribute("value"), + 6000 + gEllipsis + 9999, "The fourth page in the 'largeObject' is named correctly."); + is(objectVar.target.querySelectorAll(".variables-view-property .value")[3].getAttribute("value"), + "", "The fourth page in the 'largeObject' should not have a corresponding value."); + + is(arrayVar.target.querySelectorAll(".variables-view-property .name")[4].getAttribute("value"), + "length", "The other properties 'largeArray' are named correctly."); + is(arrayVar.target.querySelectorAll(".variables-view-property .value")[4].getAttribute("value"), + "10000", "The other properties 'largeArray' have the correct value."); + is(arrayVar.target.querySelectorAll(".variables-view-property .name")[5].getAttribute("value"), + "buffer", "The other properties 'largeArray' are named correctly."); + is(arrayVar.target.querySelectorAll(".variables-view-property .value")[5].getAttribute("value"), + "ArrayBuffer", "The other properties 'largeArray' have the correct value."); + is(arrayVar.target.querySelectorAll(".variables-view-property .name")[6].getAttribute("value"), + "byteLength", "The other properties 'largeArray' are named correctly."); + is(arrayVar.target.querySelectorAll(".variables-view-property .value")[6].getAttribute("value"), + "10000", "The other properties 'largeArray' have the correct value."); + is(arrayVar.target.querySelectorAll(".variables-view-property .name")[7].getAttribute("value"), + "byteOffset", "The other properties 'largeArray' are named correctly."); + is(arrayVar.target.querySelectorAll(".variables-view-property .value")[7].getAttribute("value"), + "0", "The other properties 'largeArray' have the correct value."); + is(arrayVar.target.querySelectorAll(".variables-view-property .name")[8].getAttribute("value"), + "__proto__", "The other properties 'largeArray' are named correctly."); + is(arrayVar.target.querySelectorAll(".variables-view-property .value")[8].getAttribute("value"), + "Int8ArrayPrototype", "The other properties 'largeArray' have the correct value."); + + is(objectVar.target.querySelectorAll(".variables-view-property .name")[4].getAttribute("value"), + "__proto__", "The other properties 'largeObject' are named correctly."); + is(objectVar.target.querySelectorAll(".variables-view-property .value")[4].getAttribute("value"), + "Object", "The other properties 'largeObject' have the correct value."); +} + +function verifyNextLevels() { + let localScope = gVariables.getScopeAtIndex(0); + let objectVar = localScope.get("largeObject"); + + let lastPage1 = objectVar.get(6000 + gEllipsis + 9999); + ok(lastPage1, "The last page in the first level was retrieved successfully."); + lastPage1.expand(); + + let pageEnums1 = lastPage1.target.querySelector(".variables-view-element-details.enum").childNodes; + let pageNonEnums1 = lastPage1.target.querySelector(".variables-view-element-details.nonenum").childNodes; + is(pageEnums1.length, 0, + "The last page in the first level shouldn't contain any enumerable elements."); + is(pageNonEnums1.length, 4, + "The last page in the first level should contain all the created non-enumerable elements."); + + is(lastPage1._nonenum.querySelectorAll(".variables-view-property .name")[0].getAttribute("value"), + 6000 + gEllipsis + 6999, "The first page in this level named correctly (1)."); + is(lastPage1._nonenum.querySelectorAll(".variables-view-property .name")[1].getAttribute("value"), + 7000 + gEllipsis + 7999, "The second page in this level named correctly (1)."); + is(lastPage1._nonenum.querySelectorAll(".variables-view-property .name")[2].getAttribute("value"), + 8000 + gEllipsis + 8999, "The third page in this level named correctly (1)."); + is(lastPage1._nonenum.querySelectorAll(".variables-view-property .name")[3].getAttribute("value"), + 9000 + gEllipsis + 9999, "The fourth page in this level named correctly (1)."); + + let lastPage2 = lastPage1.get(9000 + gEllipsis + 9999); + ok(lastPage2, "The last page in the second level was retrieved successfully."); + lastPage2.expand(); + + let pageEnums2 = lastPage2.target.querySelector(".variables-view-element-details.enum").childNodes; + let pageNonEnums2 = lastPage2.target.querySelector(".variables-view-element-details.nonenum").childNodes; + is(pageEnums2.length, 0, + "The last page in the second level shouldn't contain any enumerable elements."); + is(pageNonEnums2.length, 4, + "The last page in the second level should contain all the created non-enumerable elements."); + + is(lastPage2._nonenum.querySelectorAll(".variables-view-property .name")[0].getAttribute("value"), + 9000 + gEllipsis + 9199, "The first page in this level named correctly (2)."); + is(lastPage2._nonenum.querySelectorAll(".variables-view-property .name")[1].getAttribute("value"), + 9200 + gEllipsis + 9399, "The second page in this level named correctly (2)."); + is(lastPage2._nonenum.querySelectorAll(".variables-view-property .name")[2].getAttribute("value"), + 9400 + gEllipsis + 9599, "The third page in this level named correctly (2)."); + is(lastPage2._nonenum.querySelectorAll(".variables-view-property .name")[3].getAttribute("value"), + 9600 + gEllipsis + 9999, "The fourth page in this level named correctly (2)."); + + let lastPage3 = lastPage2.get(9600 + gEllipsis + 9999); + ok(lastPage3, "The last page in the third level was retrieved successfully."); + lastPage3.expand(); + + let pageEnums3 = lastPage3.target.querySelector(".variables-view-element-details.enum").childNodes; + let pageNonEnums3 = lastPage3.target.querySelector(".variables-view-element-details.nonenum").childNodes; + is(pageEnums3.length, 400, + "The last page in the third level should contain all the created enumerable elements."); + is(pageNonEnums3.length, 0, + "The last page in the third level shouldn't contain any non-enumerable elements."); + + is(lastPage3._enum.querySelectorAll(".variables-view-property .name")[0].getAttribute("value"), + 9600, "The properties in this level are named correctly (3)."); + is(lastPage3._enum.querySelectorAll(".variables-view-property .name")[1].getAttribute("value"), + 9601, "The properties in this level are named correctly (3)."); + is(lastPage3._enum.querySelectorAll(".variables-view-property .name")[398].getAttribute("value"), + 9998, "The properties in this level are named correctly (3)."); + is(lastPage3._enum.querySelectorAll(".variables-view-property .name")[399].getAttribute("value"), + 9999, "The properties in this level are named correctly (3)."); + + is(lastPage3._enum.querySelectorAll(".variables-view-property .value")[0].getAttribute("value"), + 399, "The properties in this level have the correct value (3)."); + is(lastPage3._enum.querySelectorAll(".variables-view-property .value")[1].getAttribute("value"), + 398, "The properties in this level have the correct value (3)."); + is(lastPage3._enum.querySelectorAll(".variables-view-property .value")[398].getAttribute("value"), + 1, "The properties in this level have the correct value (3)."); + is(lastPage3._enum.querySelectorAll(".variables-view-property .value")[399].getAttribute("value"), + 0, "The properties in this level have the correct value (3)."); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gVariables = null; + gEllipsis = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-override-01.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-override-01.js new file mode 100644 index 000000000..7b2911c38 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-override-01.js @@ -0,0 +1,227 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that VariablesView methods responsible for styling variables + * as overridden work properly. + */ + +const TAB_URL = EXAMPLE_URL + "doc_scope-variable-2.html"; + +function test() { + Task.spawn(function() { + let [tab,, panel] = yield initDebugger(TAB_URL); + let win = panel.panelWin; + let events = win.EVENTS; + let variables = win.DebuggerView.Variables; + + callInTab(tab, "test"); + yield waitForSourceAndCaretAndScopes(panel, ".html", 23); + + let firstScope = variables.getScopeAtIndex(0); + let secondScope = variables.getScopeAtIndex(1); + let thirdScope = variables.getScopeAtIndex(2); + let globalScope = variables.getScopeAtIndex(3); + + ok(firstScope, "The first scope is available."); + ok(secondScope, "The second scope is available."); + ok(thirdScope, "The third scope is available."); + ok(globalScope, "The global scope is available."); + + is(firstScope.name, "Function scope [secondNest]", + "The first scope's name is correct."); + is(secondScope.name, "Function scope [firstNest]", + "The second scope's name is correct."); + is(thirdScope.name, "Function scope [test]", + "The third scope's name is correct."); + is(globalScope.name, "Global scope [Window]", + "The global scope's name is correct."); + + is(firstScope.expanded, true, + "The first scope's expansion state is correct."); + is(secondScope.expanded, false, + "The second scope's expansion state is correct."); + is(thirdScope.expanded, false, + "The third scope's expansion state is correct."); + is(globalScope.expanded, false, + "The global scope's expansion state is correct."); + + is(firstScope._store.size, 3, + "The first scope should have all the variables available."); + is(secondScope._store.size, 0, + "The second scope should have no variables available yet."); + is(thirdScope._store.size, 0, + "The third scope should have no variables available yet."); + is(globalScope._store.size, 0, + "The global scope should have no variables available yet."); + + // Test getOwnerScopeForVariableOrProperty with simple variables. + + let thisVar = firstScope.get("this"); + let thisOwner = variables.getOwnerScopeForVariableOrProperty(thisVar); + is(thisOwner, firstScope, + "The getOwnerScopeForVariableOrProperty method works properly (1)."); + + let someVar1 = firstScope.get("a"); + let someOwner1 = variables.getOwnerScopeForVariableOrProperty(someVar1); + is(someOwner1, firstScope, + "The getOwnerScopeForVariableOrProperty method works properly (2)."); + + // Test getOwnerScopeForVariableOrProperty with first-degree properties. + + let argsVar1 = firstScope.get("arguments"); + let fetched = waitForDebuggerEvents(panel, events.FETCHED_PROPERTIES); + argsVar1.expand(); + yield fetched; + + let calleeProp1 = argsVar1.get("callee"); + let calleeOwner1 = variables.getOwnerScopeForVariableOrProperty(calleeProp1); + is(calleeOwner1, firstScope, + "The getOwnerScopeForVariableOrProperty method works properly (3)."); + + // Test getOwnerScopeForVariableOrProperty with second-degree properties. + + let protoVar1 = argsVar1.get("__proto__"); + fetched = waitForDebuggerEvents(panel, events.FETCHED_PROPERTIES); + protoVar1.expand(); + yield fetched; + + let constrProp1 = protoVar1.get("constructor"); + let constrOwner1 = variables.getOwnerScopeForVariableOrProperty(constrProp1); + is(constrOwner1, firstScope, + "The getOwnerScopeForVariableOrProperty method works properly (4)."); + + // Test getOwnerScopeForVariableOrProperty with a simple variable + // from non-topmost scopes. + + // Only need to wait for a single FETCHED_VARIABLES event, just for the + // global scope, because the other local scopes already have the + // arguments and variables available as evironment bindings. + fetched = waitForDebuggerEvents(panel, events.FETCHED_VARIABLES); + secondScope.expand(); + thirdScope.expand(); + globalScope.expand(); + yield fetched; + + let someVar2 = secondScope.get("a"); + let someOwner2 = variables.getOwnerScopeForVariableOrProperty(someVar2); + is(someOwner2, secondScope, + "The getOwnerScopeForVariableOrProperty method works properly (5)."); + + let someVar3 = thirdScope.get("a"); + let someOwner3 = variables.getOwnerScopeForVariableOrProperty(someVar3); + is(someOwner3, thirdScope, + "The getOwnerScopeForVariableOrProperty method works properly (6)."); + + // Test getOwnerScopeForVariableOrProperty with first-degree properies + // from non-topmost scopes. + + let argsVar2 = secondScope.get("arguments"); + fetched = waitForDebuggerEvents(panel, events.FETCHED_PROPERTIES); + argsVar2.expand(); + yield fetched; + + let calleeProp2 = argsVar2.get("callee"); + let calleeOwner2 = variables.getOwnerScopeForVariableOrProperty(calleeProp2); + is(calleeOwner2, secondScope, + "The getOwnerScopeForVariableOrProperty method works properly (7)."); + + let argsVar3 = thirdScope.get("arguments"); + fetched = waitForDebuggerEvents(panel, events.FETCHED_PROPERTIES); + argsVar3.expand(); + yield fetched; + + let calleeProp3 = argsVar3.get("callee"); + let calleeOwner3 = variables.getOwnerScopeForVariableOrProperty(calleeProp3); + is(calleeOwner3, thirdScope, + "The getOwnerScopeForVariableOrProperty method works properly (8)."); + + // Test getOwnerScopeForVariableOrProperty with second-degree properties + // from non-topmost scopes. + + let protoVar2 = argsVar2.get("__proto__"); + fetched = waitForDebuggerEvents(panel, events.FETCHED_PROPERTIES); + protoVar2.expand(); + yield fetched; + + let constrProp2 = protoVar2.get("constructor"); + let constrOwner2 = variables.getOwnerScopeForVariableOrProperty(constrProp2); + is(constrOwner2, secondScope, + "The getOwnerScopeForVariableOrProperty method works properly (9)."); + + let protoVar3 = argsVar3.get("__proto__"); + fetched = waitForDebuggerEvents(panel, events.FETCHED_PROPERTIES); + protoVar3.expand(); + yield fetched; + + let constrProp3 = protoVar3.get("constructor"); + let constrOwner3 = variables.getOwnerScopeForVariableOrProperty(constrProp3); + is(constrOwner3, thirdScope, + "The getOwnerScopeForVariableOrProperty method works properly (10)."); + + // Test getParentScopesForVariableOrProperty with simple variables. + + let varOwners1 = variables.getParentScopesForVariableOrProperty(someVar1); + let varOwners2 = variables.getParentScopesForVariableOrProperty(someVar2); + let varOwners3 = variables.getParentScopesForVariableOrProperty(someVar3); + + is(varOwners1.length, 0, + "There should be no owner scopes for the first variable."); + + is(varOwners2.length, 1, + "There should be one owner scope for the second variable."); + is(varOwners2[0], firstScope, + "The only owner scope for the second variable is correct."); + + is(varOwners3.length, 2, + "There should be two owner scopes for the third variable."); + is(varOwners3[0], firstScope, + "The first owner scope for the third variable is correct."); + is(varOwners3[1], secondScope, + "The second owner scope for the third variable is correct."); + + // Test getParentScopesForVariableOrProperty with first-degree properties. + + let propOwners1 = variables.getParentScopesForVariableOrProperty(calleeProp1); + let propOwners2 = variables.getParentScopesForVariableOrProperty(calleeProp2); + let propOwners3 = variables.getParentScopesForVariableOrProperty(calleeProp3); + + is(propOwners1.length, 0, + "There should be no owner scopes for the first property."); + + is(propOwners2.length, 1, + "There should be one owner scope for the second property."); + is(propOwners2[0], firstScope, + "The only owner scope for the second property is correct."); + + is(propOwners3.length, 2, + "There should be two owner scopes for the third property."); + is(propOwners3[0], firstScope, + "The first owner scope for the third property is correct."); + is(propOwners3[1], secondScope, + "The second owner scope for the third property is correct."); + + // Test getParentScopesForVariableOrProperty with second-degree properties. + + let secPropOwners1 = variables.getParentScopesForVariableOrProperty(constrProp1); + let secPropOwners2 = variables.getParentScopesForVariableOrProperty(constrProp2); + let secPropOwners3 = variables.getParentScopesForVariableOrProperty(constrProp3); + + is(secPropOwners1.length, 0, + "There should be no owner scopes for the first inner property."); + + is(secPropOwners2.length, 1, + "There should be one owner scope for the second inner property."); + is(secPropOwners2[0], firstScope, + "The only owner scope for the second inner property is correct."); + + is(secPropOwners3.length, 2, + "There should be two owner scopes for the third inner property."); + is(secPropOwners3[0], firstScope, + "The first owner scope for the third inner property is correct."); + is(secPropOwners3[1], secondScope, + "The second owner scope for the third inner property is correct."); + + yield resumeDebuggerThenCloseAndFinish(panel); + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-override-02.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-override-02.js new file mode 100644 index 000000000..641293d11 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-override-02.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that overridden variables in the VariablesView are styled properly. + */ + +const TAB_URL = EXAMPLE_URL + "doc_scope-variable-2.html"; + +function test() { + Task.spawn(function() { + let [tab,, panel] = yield initDebugger(TAB_URL); + let win = panel.panelWin; + let events = win.EVENTS; + let variables = win.DebuggerView.Variables; + + // Wait for the hierarchy to be committed by the VariablesViewController. + let committedLocalScopeHierarchy = promise.defer(); + variables.oncommit = committedLocalScopeHierarchy.resolve; + + callInTab(tab, "test"); + yield waitForSourceAndCaretAndScopes(panel, ".html", 23); + yield committedLocalScopeHierarchy.promise; + + let firstScope = variables.getScopeAtIndex(0); + let secondScope = variables.getScopeAtIndex(1); + let thirdScope = variables.getScopeAtIndex(2); + + let someVar1 = firstScope.get("a"); + let argsVar1 = firstScope.get("arguments"); + + is(someVar1.target.hasAttribute("overridden"), false, + "The first 'a' variable should not be marked as being overridden."); + is(argsVar1.target.hasAttribute("overridden"), false, + "The first 'arguments' variable should not be marked as being overridden."); + + // Wait for the hierarchy to be committed by the VariablesViewController. + let committedSecondScopeHierarchy = promise.defer(); + variables.oncommit = committedSecondScopeHierarchy.resolve; + secondScope.expand(); + yield committedSecondScopeHierarchy.promise; + + let someVar2 = secondScope.get("a"); + let argsVar2 = secondScope.get("arguments"); + + is(someVar2.target.hasAttribute("overridden"), true, + "The second 'a' variable should be marked as being overridden."); + is(argsVar2.target.hasAttribute("overridden"), true, + "The second 'arguments' variable should be marked as being overridden."); + + // Wait for the hierarchy to be committed by the VariablesViewController. + let committedThirdScopeHierarchy = promise.defer(); + variables.oncommit = committedThirdScopeHierarchy.resolve; + thirdScope.expand(); + yield committedThirdScopeHierarchy.promise; + + let someVar3 = thirdScope.get("a"); + let argsVar3 = thirdScope.get("arguments"); + + is(someVar3.target.hasAttribute("overridden"), true, + "The third 'a' variable should be marked as being overridden."); + is(argsVar3.target.hasAttribute("overridden"), true, + "The third 'arguments' variable should be marked as being overridden."); + + yield resumeDebuggerThenCloseAndFinish(panel); + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-01.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-01.js new file mode 100644 index 000000000..5349643fd --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-01.js @@ -0,0 +1,59 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests opening the variable inspection popup on a variable which has a + * simple literal as the value. + */ + +const TAB_URL = EXAMPLE_URL + "doc_frame-parameters.html"; + +function test() { + Task.spawn(function() { + let [tab,, panel] = yield initDebugger(TAB_URL); + let win = panel.panelWin; + let bubble = win.DebuggerView.VariableBubble; + let tooltip = bubble._tooltip.panel; + + bubble._ignoreLiterals = false; + + function verifyContents(textContent, className) { + is(tooltip.querySelectorAll(".variables-view-container").length, 0, + "There should be no variables view containers added to the tooltip."); + is(tooltip.querySelectorAll(".devtools-tooltip-simple-text").length, 1, + "There should be a simple text node added to the tooltip instead."); + + is(tooltip.querySelector(".devtools-tooltip-simple-text").textContent, textContent, + "The inspected property's value is correct."); + ok(tooltip.querySelector(".devtools-tooltip-simple-text").className.contains(className), + "The inspected property's value is colorized correctly."); + } + + callInTab(tab, "start"); + yield waitForSourceAndCaretAndScopes(panel, ".html", 24); + + // Inspect variables. + yield openVarPopup(panel, { line: 15, ch: 12 }); + verifyContents("1", "token-number"); + + yield reopenVarPopup(panel, { line: 16, ch: 21 }); + verifyContents("1", "token-number"); + + yield reopenVarPopup(panel, { line: 17, ch: 21 }); + verifyContents("1", "token-number"); + + yield reopenVarPopup(panel, { line: 17, ch: 27 }); + verifyContents("\"beta\"", "token-string"); + + yield reopenVarPopup(panel, { line: 17, ch: 44 }); + verifyContents("false", "token-boolean"); + + yield reopenVarPopup(panel, { line: 17, ch: 54 }); + verifyContents("null", "token-null"); + + yield reopenVarPopup(panel, { line: 17, ch: 63 }); + verifyContents("undefined", "token-undefined"); + + yield resumeDebuggerThenCloseAndFinish(panel); + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-02.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-02.js new file mode 100644 index 000000000..9f8a3d750 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-02.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests opening the variable inspection popup on a variable which has a + * a property accessible via getters and setters. + */ + +const TAB_URL = EXAMPLE_URL + "doc_frame-parameters.html"; + +function test() { + Task.spawn(function() { + let [tab,, panel] = yield initDebugger(TAB_URL); + let win = panel.panelWin; + let bubble = win.DebuggerView.VariableBubble; + let tooltip = bubble._tooltip.panel; + + function verifyContents(textContent, className) { + is(tooltip.querySelectorAll(".variables-view-container").length, 0, + "There should be no variables view containers added to the tooltip."); + is(tooltip.querySelectorAll(".devtools-tooltip-simple-text").length, 1, + "There should be a simple text node added to the tooltip instead."); + + is(tooltip.querySelector(".devtools-tooltip-simple-text").textContent, textContent, + "The inspected property's value is correct."); + ok(tooltip.querySelector(".devtools-tooltip-simple-text").className.contains(className), + "The inspected property's value is colorized correctly."); + } + + callInTab(tab, "start"); + yield waitForSourceAndCaretAndScopes(panel, ".html", 24); + + // Inspect properties. + yield openVarPopup(panel, { line: 19, ch: 10 }); + verifyContents("42", "token-number"); + + yield reopenVarPopup(panel, { line: 20, ch: 14 }); + verifyContents("42", "token-number"); + + yield reopenVarPopup(panel, { line: 21, ch: 14 }); + verifyContents("42", "token-number"); + + yield resumeDebuggerThenCloseAndFinish(panel); + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-03.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-03.js new file mode 100644 index 000000000..4ed3375be --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-03.js @@ -0,0 +1,42 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that the inspected indentifier is highlighted. + */ + +const TAB_URL = EXAMPLE_URL + "doc_frame-parameters.html"; + +function test() { + Task.spawn(function() { + let [tab,, panel] = yield initDebugger(TAB_URL); + let win = panel.panelWin; + let bubble = win.DebuggerView.VariableBubble; + + callInTab(tab, "start"); + yield waitForSourceAndCaretAndScopes(panel, ".html", 24); + + // Inspect variable. + yield openVarPopup(panel, { line: 15, ch: 12 }); + + ok(bubble.contentsShown(), + "The variable should register as being shown."); + ok(!bubble._tooltip.isEmpty(), + "The variable inspection popup isn't empty."); + ok(bubble._markedText, + "There's some marked text in the editor."); + ok(bubble._markedText.clear, + "The marked text in the editor can be cleared."); + + yield hideVarPopup(panel); + + ok(!bubble.contentsShown(), + "The variable should register as being hidden."); + ok(bubble._tooltip.isEmpty(), + "The variable inspection popup is now empty."); + ok(!bubble._markedText, + "The marked text in the editor was removed."); + + yield resumeDebuggerThenCloseAndFinish(panel); + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-04.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-04.js new file mode 100644 index 000000000..3d4a43bd1 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-04.js @@ -0,0 +1,31 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that the variable inspection popup is hidden when the editor scrolls. + */ + +const TAB_URL = EXAMPLE_URL + "doc_frame-parameters.html"; + +function test() { + Task.spawn(function() { + let [tab,, panel] = yield initDebugger(TAB_URL); + let win = panel.panelWin; + let bubble = win.DebuggerView.VariableBubble; + + callInTab(tab, "start"); + yield waitForSourceAndCaretAndScopes(panel, ".html", 24); + + // Inspect variable. + yield openVarPopup(panel, { line: 15, ch: 12 }); + yield hideVarPopupByScrollingEditor(panel); + ok(true, "The variable inspection popup was hidden."); + + ok(bubble._tooltip.isEmpty(), + "The variable inspection popup is now empty."); + ok(!bubble._markedText, + "The marked text in the editor was removed."); + + yield resumeDebuggerThenCloseAndFinish(panel); + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-05.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-05.js new file mode 100644 index 000000000..846f063ec --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-05.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests opening the variable inspection popup on a variable which has a + * simple object as the value. + */ + +const TAB_URL = EXAMPLE_URL + "doc_frame-parameters.html"; + +function test() { + Task.spawn(function() { + let [tab,, panel] = yield initDebugger(TAB_URL); + let win = panel.panelWin; + let bubble = win.DebuggerView.VariableBubble; + let tooltip = bubble._tooltip.panel; + + function verifyContents() { + is(tooltip.querySelectorAll(".variables-view-container").length, 1, + "There should be one variables view container added to the tooltip."); + + is(tooltip.querySelectorAll(".variables-view-scope[untitled]").length, 1, + "There should be one scope with no header displayed."); + is(tooltip.querySelectorAll(".variables-view-variable[untitled]").length, 1, + "There should be one variable with no header displayed."); + + is(tooltip.querySelectorAll(".variables-view-property").length, 2, + "There should be 2 properties displayed."); + + is(tooltip.querySelectorAll(".variables-view-property .name")[0].getAttribute("value"), "a", + "The first property's name is correct."); + is(tooltip.querySelectorAll(".variables-view-property .value")[0].getAttribute("value"), "1", + "The first property's value is correct."); + + is(tooltip.querySelectorAll(".variables-view-property .name")[1].getAttribute("value"), "__proto__", + "The second property's name is correct."); + is(tooltip.querySelectorAll(".variables-view-property .value")[1].getAttribute("value"), "Object", + "The second property's value is correct."); + } + + callInTab(tab, "start"); + yield waitForSourceAndCaretAndScopes(panel, ".html", 24); + + // Inspect variable. + yield openVarPopup(panel, { line: 16, ch: 12 }, true); + verifyContents(); + + yield resumeDebuggerThenCloseAndFinish(panel); + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-06.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-06.js new file mode 100644 index 000000000..1855e1ba1 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-06.js @@ -0,0 +1,76 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests opening the variable inspection popup on a variable which has a + * complext object as the value. + */ + +const TAB_URL = EXAMPLE_URL + "doc_frame-parameters.html"; + +function test() { + requestLongerTimeout(2); + Task.spawn(function() { + let [tab,, panel] = yield initDebugger(TAB_URL); + let win = panel.panelWin; + let bubble = win.DebuggerView.VariableBubble; + let tooltip = bubble._tooltip.panel; + + function verifyContents() { + is(tooltip.querySelectorAll(".variables-view-container").length, 1, + "There should be one variables view container added to the tooltip."); + + is(tooltip.querySelectorAll(".variables-view-scope[untitled]").length, 1, + "There should be one scope with no header displayed."); + is(tooltip.querySelectorAll(".variables-view-variable[untitled]").length, 1, + "There should be one variable with no header displayed."); + + is(tooltip.querySelectorAll(".variables-view-property").length, 7, + "There should be 7 properties displayed."); + + is(tooltip.querySelectorAll(".variables-view-property .name")[0].getAttribute("value"), "a", + "The first property's name is correct."); + is(tooltip.querySelectorAll(".variables-view-property .value")[0].getAttribute("value"), "1", + "The first property's value is correct."); + + is(tooltip.querySelectorAll(".variables-view-property .name")[1].getAttribute("value"), "b", + "The second property's name is correct."); + is(tooltip.querySelectorAll(".variables-view-property .value")[1].getAttribute("value"), "\"beta\"", + "The second property's value is correct."); + + is(tooltip.querySelectorAll(".variables-view-property .name")[2].getAttribute("value"), "c", + "The third property's name is correct."); + is(tooltip.querySelectorAll(".variables-view-property .value")[2].getAttribute("value"), "3", + "The third property's value is correct."); + + is(tooltip.querySelectorAll(".variables-view-property .name")[3].getAttribute("value"), "d", + "The fourth property's name is correct."); + is(tooltip.querySelectorAll(".variables-view-property .value")[3].getAttribute("value"), "false", + "The fourth property's value is correct."); + + is(tooltip.querySelectorAll(".variables-view-property .name")[4].getAttribute("value"), "e", + "The fifth property's name is correct."); + is(tooltip.querySelectorAll(".variables-view-property .value")[4].getAttribute("value"), "null", + "The fifth property's value is correct."); + + is(tooltip.querySelectorAll(".variables-view-property .name")[5].getAttribute("value"), "f", + "The sixth property's name is correct."); + is(tooltip.querySelectorAll(".variables-view-property .value")[5].getAttribute("value"), "undefined", + "The sixth property's value is correct."); + + is(tooltip.querySelectorAll(".variables-view-property .name")[6].getAttribute("value"), "__proto__", + "The seventh property's name is correct."); + is(tooltip.querySelectorAll(".variables-view-property .value")[6].getAttribute("value"), "Object", + "The seventh property's value is correct."); + } + + callInTab(tab, "start"); + yield waitForSourceAndCaretAndScopes(panel, ".html", 24); + + // Inspect variable. + yield openVarPopup(panel, { line: 17, ch: 12 }, true); + verifyContents(); + + yield resumeDebuggerThenCloseAndFinish(panel); + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-07.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-07.js new file mode 100644 index 000000000..5fd51669b --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-07.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests the variable inspection popup behaves correctly when switching + * between simple and complex objects. + */ + +const TAB_URL = EXAMPLE_URL + "doc_frame-parameters.html"; + +function test() { + Task.spawn(function() { + let [tab,, panel] = yield initDebugger(TAB_URL); + let win = panel.panelWin; + let bubble = win.DebuggerView.VariableBubble; + let tooltip = bubble._tooltip.panel; + + function verifySimpleContents(textContent, className) { + is(tooltip.querySelectorAll(".variables-view-container").length, 0, + "There should be no variables view container added to the tooltip."); + is(tooltip.querySelectorAll(".devtools-tooltip-simple-text").length, 1, + "There should be one simple text node added to the tooltip."); + + is(tooltip.querySelector(".devtools-tooltip-simple-text").textContent, textContent, + "The inspected property's value is correct."); + ok(tooltip.querySelector(".devtools-tooltip-simple-text").className.contains(className), + "The inspected property's value is colorized correctly."); + } + + function verifyComplexContents(propertyCount) { + is(tooltip.querySelectorAll(".variables-view-container").length, 1, + "There should be one variables view container added to the tooltip."); + is(tooltip.querySelectorAll(".devtools-tooltip-simple-text").length, 0, + "There should be no simple text node added to the tooltip."); + + is(tooltip.querySelectorAll(".variables-view-scope[untitled]").length, 1, + "There should be one scope with no header displayed."); + is(tooltip.querySelectorAll(".variables-view-variable[untitled]").length, 1, + "There should be one variable with no header displayed."); + + ok(tooltip.querySelectorAll(".variables-view-property").length >= propertyCount, + "There should be some properties displayed."); + } + + callInTab(tab, "start"); + yield waitForSourceAndCaretAndScopes(panel, ".html", 24); + + // Inspect variables. + yield openVarPopup(panel, { line: 15, ch: 12 }); + verifySimpleContents("1", "token-number"); + + yield reopenVarPopup(panel, { line: 16, ch: 12 }, true); + verifyComplexContents(2); + + yield reopenVarPopup(panel, { line: 19, ch: 10 }); + verifySimpleContents("42", "token-number"); + + yield reopenVarPopup(panel, { line: 31, ch: 10 }, true); + verifyComplexContents(100); + + yield resumeDebuggerThenCloseAndFinish(panel); + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-08.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-08.js new file mode 100644 index 000000000..580b10090 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-08.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests opening inspecting variables works across scopes. + */ + +const TAB_URL = EXAMPLE_URL + "doc_scope-variable.html"; + +function test() { + Task.spawn(function() { + let [tab,, panel] = yield initDebugger(TAB_URL); + let win = panel.panelWin; + let events = win.EVENTS; + let editor = win.DebuggerView.editor; + let frames = win.DebuggerView.StackFrames; + let bubble = win.DebuggerView.VariableBubble; + let tooltip = bubble._tooltip.panel; + + function verifyContents(textContent, className) { + is(tooltip.querySelectorAll(".variables-view-container").length, 0, + "There should be no variables view containers added to the tooltip."); + is(tooltip.querySelectorAll(".devtools-tooltip-simple-text").length, 1, + "There should be a simple text node added to the tooltip instead."); + + is(tooltip.querySelector(".devtools-tooltip-simple-text").textContent, textContent, + "The inspected property's value is correct."); + ok(tooltip.querySelector(".devtools-tooltip-simple-text").className.contains(className), + "The inspected property's value is colorized correctly."); + } + + function checkView(selectedFrame, caretLine) { + is(win.gThreadClient.state, "paused", + "Should only be getting stack frames while paused."); + is(frames.itemCount, 2, + "Should have two frames."); + is(frames.selectedDepth, selectedFrame, + "The correct frame is selected in the widget."); + ok(isCaretPos(panel, caretLine), + "Editor caret location is correct."); + } + + callInTab(tab, "test"); + yield waitForSourceAndCaretAndScopes(panel, ".html", 20); + checkView(0, 20); + + // Inspect variable in topmost frame. + yield openVarPopup(panel, { line: 18, ch: 12 }); + verifyContents("\"second scope\"", "token-string"); + checkView(0, 20); + + // Hide the popup and change the frame. + yield hideVarPopup(panel); + + let updatedFrame = waitForDebuggerEvents(panel, events.FETCHED_SCOPES); + frames.selectedDepth = 1; + yield updatedFrame; + checkView(1, 15); + + // Inspect variable in oldest frame. + yield openVarPopup(panel, { line: 13, ch: 12 }); + verifyContents("\"first scope\"", "token-string"); + checkView(1, 15); + + yield resumeDebuggerThenCloseAndFinish(panel); + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-09.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-09.js new file mode 100644 index 000000000..a06587775 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-09.js @@ -0,0 +1,32 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests opening inspecting variables works across scopes. + */ + +const TAB_URL = EXAMPLE_URL + "doc_scope-variable-3.html"; + +function test() { + Task.spawn(function() { + let [tab,, panel] = yield initDebugger(TAB_URL); + let win = panel.panelWin; + let bubble = win.DebuggerView.VariableBubble; + let tooltip = bubble._tooltip.panel; + + callInTab(tab, "test"); + yield waitForSourceAndCaretAndScopes(panel, ".html", 15); + + yield openVarPopup(panel, { line: 12, ch: 10 }); + ok(true, "The variable inspection popup was shown for the real variable."); + + once(tooltip, "popupshown").then(() => { + ok(false, "The variable inspection popup shouldn't have been opened."); + }); + + reopenVarPopup(panel, { line: 18, ch: 10 }); + yield waitForTime(1000); + + yield resumeDebuggerThenCloseAndFinish(panel); + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-10.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-10.js new file mode 100644 index 000000000..0905a0551 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-10.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Makes sure the source editor's scroll location doesn't change when + * a variable inspection popup is opened and a watch expression is + * also evaluated at the same time. + */ + +const TAB_URL = EXAMPLE_URL + "doc_frame-parameters.html"; + +function test() { + Task.spawn(function() { + let [tab,, panel] = yield initDebugger(TAB_URL); + let win = panel.panelWin; + let events = win.EVENTS; + let editor = win.DebuggerView.editor; + let editorContainer = win.document.getElementById("editor"); + let bubble = win.DebuggerView.VariableBubble; + let expressions = win.DebuggerView.WatchExpressions; + let tooltip = bubble._tooltip.panel; + + callInTab(tab, "start"); + yield waitForSourceAndCaretAndScopes(panel, ".html", 24); + + let expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS); + expressions.addExpression("this"); + editor.focus(); + yield expressionsEvaluated; + + // Scroll to the top of the editor and inspect variables. + let breakpointScrollPosition = editor.getScrollInfo().top; + editor.setFirstVisibleLine(0); + let topmostScrollPosition = editor.getScrollInfo().top; + + ok(topmostScrollPosition < breakpointScrollPosition, + "The editor is now scrolled to the top (0)."); + is(editor.getFirstVisibleLine(), 0, + "The editor is now scrolled to the top (1)."); + + let failPopup = () => ok(false, "The popup has got unexpectedly hidden."); + let failScroll = () => ok(false, "The editor has got unexpectedly scrolled."); + tooltip.addEventListener("popuphiding", failPopup); + editorContainer.addEventListener("scroll", failScroll); + editor.on("scroll", () => { + if (editor.getScrollInfo().top > topmostScrollPosition) { + ok(false, "The editor scrolled back to the breakpoint location."); + } + }); + + expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS); + yield openVarPopup(panel, { line: 14, ch: 15 }); + yield expressionsEvaluated; + + tooltip.removeEventListener("popuphiding", failPopup); + editorContainer.removeEventListener("scroll", failScroll); + + yield resumeDebuggerThenCloseAndFinish(panel); + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-11.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-11.js new file mode 100644 index 000000000..5fc86988e --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-11.js @@ -0,0 +1,77 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that the watch expression button is added in variable view popup. + */ + +const TAB_URL = EXAMPLE_URL + "doc_watch-expression-button.html"; + +function test() { + Task.spawn(function() { + let [tab,, panel] = yield initDebugger(TAB_URL); + let win = panel.panelWin; + let events = win.EVENTS; + let watch = win.DebuggerView.WatchExpressions; + let bubble = win.DebuggerView.VariableBubble; + let tooltip = bubble._tooltip.panel; + + let label = win.L10N.getStr("addWatchExpressionButton"); + let className = "dbg-expression-button"; + + function testExpressionButton(aLabel, aClassName, aExpression) { + ok(tooltip.querySelector("button"), + "There should be a button available in variable view popup."); + is(tooltip.querySelector("button").label, aLabel, + "The button available is labeled correctly."); + is(tooltip.querySelector("button").className, aClassName, + "The button available is styled correctly."); + + tooltip.querySelector("button").click(); + + ok(!tooltip.querySelector("button"), + "There should be no button available in variable view popup."); + ok(watch.getItemAtIndex(0), + "The expression at index 0 should be available."); + is(watch.getItemAtIndex(0).attachment.initialExpression, aExpression, + "The expression at index 0 is correct."); + } + + callInTab(tab, "start"); + yield waitForSourceAndCaretAndScopes(panel, ".html", 19); + + // Inspect primitive value variable. + yield openVarPopup(panel, { line: 15, ch: 12 }); + let popupHiding = once(tooltip, "popuphiding"); + let expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS); + testExpressionButton(label, className, "a"); + yield promise.all([popupHiding, expressionsEvaluated]); + ok(true, "The new watch expressions were re-evaluated and the panel got hidden (1)."); + + // Inspect non primitive value variable. + expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS); + yield openVarPopup(panel, { line: 16, ch: 12 }, true); + yield expressionsEvaluated; + ok(true, "The watch expressions were re-evaluated when a new panel opened (1)."); + + popupHiding = once(tooltip, "popuphiding"); + expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS); + testExpressionButton(label, className, "b"); + yield promise.all([popupHiding, expressionsEvaluated]); + ok(true, "The new watch expressions were re-evaluated and the panel got hidden (2)."); + + // Inspect property of an object. + expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS); + yield openVarPopup(panel, { line: 17, ch: 10 }); + yield expressionsEvaluated; + ok(true, "The watch expressions were re-evaluated when a new panel opened (2)."); + + popupHiding = once(tooltip, "popuphiding"); + expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS); + testExpressionButton(label, className, "b.a"); + yield promise.all([popupHiding, expressionsEvaluated]); + ok(true, "The new watch expressions were re-evaluated and the panel got hidden (3)."); + + yield resumeDebuggerThenCloseAndFinish(panel); + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-12.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-12.js new file mode 100644 index 000000000..f4b290ae8 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-12.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that the clicking "Watch" button twice, for the same expression, only adds it + * once. + */ + +const TAB_URL = EXAMPLE_URL + "doc_watch-expression-button.html"; + +function test() { + Task.spawn(function() { + let [tab,, panel] = yield initDebugger(TAB_URL); + let win = panel.panelWin; + let events = win.EVENTS; + let watch = win.DebuggerView.WatchExpressions; + let bubble = win.DebuggerView.VariableBubble; + let tooltip = bubble._tooltip.panel; + + function verifyContent(aExpression, aItemCount) { + + ok(watch.getItemAtIndex(0), + "The expression at index 0 should be available."); + is(watch.getItemAtIndex(0).attachment.initialExpression, aExpression, + "The expression at index 0 is correct."); + is(watch.itemCount, aItemCount, + "The expression count is correct."); + } + + callInTab(tab, "start"); + yield waitForSourceAndCaretAndScopes(panel, ".html", 19); + + // Inspect primitive value variable. + yield openVarPopup(panel, { line: 15, ch: 12 }); + let popupHiding = once(tooltip, "popuphiding"); + let expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS); + tooltip.querySelector("button").click(); + verifyContent("a", 1); + yield promise.all([popupHiding, expressionsEvaluated]); + ok(true, "The new watch expressions were re-evaluated and the panel got hidden (1)."); + + // Inspect property of an object. + expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS); + yield openVarPopup(panel, { line: 17, ch: 10 }); + yield expressionsEvaluated; + ok(true, "The watch expressions were re-evaluated when a new panel opened (1)."); + + popupHiding = once(tooltip, "popuphiding"); + expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS); + tooltip.querySelector("button").click(); + verifyContent("b.a", 2); + yield promise.all([popupHiding, expressionsEvaluated]); + ok(true, "The new watch expressions were re-evaluated and the panel got hidden (2)."); + + // Re-inspect primitive value variable. + expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS); + yield openVarPopup(panel, { line: 15, ch: 12 }); + yield expressionsEvaluated; + ok(true, "The watch expressions were re-evaluated when a new panel opened (2)."); + + popupHiding = once(tooltip, "popuphiding"); + expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS); + tooltip.querySelector("button").click(); + verifyContent("b.a", 2); + yield promise.all([popupHiding, expressionsEvaluated]); + ok(true, "The new watch expressions were re-evaluated and the panel got hidden (3)."); + + yield resumeDebuggerThenCloseAndFinish(panel); + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-13.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-13.js new file mode 100644 index 000000000..fad68f92a --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-13.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that the variable inspection popup has inspector links for DOMNode + * properties and that the popup closes when the link is clicked + */ + +const TAB_URL = EXAMPLE_URL + "doc_domnode-variables.html"; + +function test() { + Task.spawn(function() { + let [tab,, panel] = yield initDebugger(TAB_URL); + let win = panel.panelWin; + let bubble = win.DebuggerView.VariableBubble; + let tooltip = bubble._tooltip.panel; + let toolbox = gDevTools.getToolbox(panel.target); + + function getDomNodeInTooltip(propertyName) { + let domNodeProperties = tooltip.querySelectorAll(".token-domnode"); + for (let prop of domNodeProperties) { + let propName = prop.parentNode.querySelector(".name"); + if (propName.getAttribute("value") === propertyName) { + ok(true, "DOMNode " + propertyName + " was found in the tooltip"); + return prop; + } + } + ok(false, "DOMNode " + propertyName + " wasn't found in the tooltip"); + } + + callInTab(tab, "start"); + yield waitForSourceAndCaretAndScopes(panel, ".html", 19); + + // Inspect the div DOM variable. + yield openVarPopup(panel, { line: 17, ch: 38 }, true); + let property = getDomNodeInTooltip("firstElementChild"); + + // Simulate mouseover on the property value + let highlighted = once(toolbox, "node-highlight"); + EventUtils.sendMouseEvent({ type: "mouseover" }, property, + property.ownerDocument.defaultView); + yield highlighted; + ok(true, "The node-highlight event was fired on hover of the DOMNode"); + + // Simulate a click on the "select in inspector" button + let button = property.parentNode.querySelector(".variables-view-open-inspector"); + ok(button, "The select-in-inspector button is present"); + let inspectorSelected = once(toolbox, "inspector-selected"); + EventUtils.sendMouseEvent({ type: "mousedown" }, button, + button.ownerDocument.defaultView); + yield inspectorSelected; + ok(true, "The inspector got selected when clicked on the select-in-inspector"); + + // Make sure the inspector's initialization is finalized before ending the test + // Listening to the event *after* triggering the switch to the inspector isn't + // a problem as the inspector is asynchronously loaded. + yield once(toolbox.getPanel("inspector"), "inspector-updated"); + + yield resumeDebuggerThenCloseAndFinish(panel); + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-14.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-14.js new file mode 100644 index 000000000..c70c6fd11 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-14.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that the variable inspection popup is hidden when + * selecting text in the editor. + */ + +const TAB_URL = EXAMPLE_URL + "doc_frame-parameters.html"; + +function test() { + Task.spawn(function*() { + let [tab,, panel] = yield initDebugger(TAB_URL); + let win = panel.panelWin; + let bubble = win.DebuggerView.VariableBubble; + + callInTab(tab, "start"); + yield waitForSourceAndCaretAndScopes(panel, ".html", 24); + + // Select some text. + let cursor = win.DebuggerView.editor.getOffset({ line: 15, ch: 12 }); + let [ anchor, head ] = win.DebuggerView.editor.getPosition( + cursor, + cursor + 3 + ); + win.DebuggerView.editor.setSelection(anchor, head); + + // Try to Inspect variable during selection. + let popupOpened = yield intendOpenVarPopup(panel, { line: 15, ch: 12 }, true); + + // Ensure the bubble is not there + ok(!popupOpened, + "The popup is not opened"); + ok(!bubble._markedText, + "The marked text in the editor is not there."); + + // Try to Inspect variable after selection. + popupOpened = yield intendOpenVarPopup(panel, { line: 15, ch: 12 }, false); + + // Ensure the bubble is not there + ok(popupOpened, + "The popup is opened"); + ok(bubble._markedText, + "The marked text in the editor is there."); + + yield resumeDebuggerThenCloseAndFinish(panel); + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-15.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-15.js new file mode 100644 index 000000000..1a9e40947 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-15.js @@ -0,0 +1,32 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests opening the variable inspection popup directly on literals. + */ + +const TAB_URL = EXAMPLE_URL + "doc_frame-parameters.html"; + +function test() { + Task.spawn(function() { + let [tab,, panel] = yield initDebugger(TAB_URL); + let win = panel.panelWin; + let bubble = win.DebuggerView.VariableBubble; + let tooltip = bubble._tooltip.panel; + + callInTab(tab, "start"); + yield waitForSourceAndCaretAndScopes(panel, ".html", 24); + + yield openVarPopup(panel, { line: 15, ch: 12 }); + ok(true, "The variable inspection popup was shown for the real variable."); + + once(tooltip, "popupshown").then(() => { + ok(false, "The variable inspection popup shouldn't have been opened."); + }); + + reopenVarPopup(panel, { line: 17, ch: 27 }); + yield waitForTime(1000); + + yield resumeDebuggerThenCloseAndFinish(panel); + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-16.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-16.js new file mode 100644 index 000000000..2f822a14e --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-popup-16.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if opening the variables inspection popup preserves the highlighting + * associated with the currently debugged line. + */ + +const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html"; + +function test() { + Task.spawn(function() { + let [tab,, panel] = yield initDebugger(TAB_URL); + let win = panel.panelWin; + let events = win.EVENTS; + let editor = win.DebuggerView.editor; + let frames = win.DebuggerView.StackFrames; + let variables = win.DebuggerView.Variables; + let bubble = win.DebuggerView.VariableBubble; + let tooltip = bubble._tooltip.panel; + + function checkView(selectedFrame, caretLine, debugLine = caretLine) { + let deferred = promise.defer(); + + is(win.gThreadClient.state, "paused", + "Should only be getting stack frames while paused."); + is(frames.itemCount, 25, + "Should have 25 frames."); + is(frames.selectedDepth, selectedFrame, + "The correct frame is selected in the widget."); + ok(isCaretPos(panel, caretLine), + "Editor caret location is correct."); + + // The editor's debug location takes a tick to update. + executeSoon(() => { + ok(isCaretPos(panel, caretLine), "Editor caret location is still correct."); + ok(isDebugPos(panel, debugLine), "Editor debug location is correct."); + deferred.resolve(); + }); + + return deferred.promise; + } + + function expandGlobalScope() { + let globalScope = variables.getScopeAtIndex(1); + is(globalScope.expanded, false, + "The globalScope should not be expanded yet."); + + let finished = waitForDebuggerEvents(panel, events.FETCHED_VARIABLES); + globalScope.expand(); + return finished; + } + + callInTab(tab, "recurse"); + yield waitForSourceAndCaretAndScopes(panel, ".html", 26); + yield checkView(0, 26); + + yield expandGlobalScope(); + yield checkView(0, 26); + + // Inspect variable in topmost frame. + yield openVarPopup(panel, { line: 26, ch: 11 }); + yield checkView(0, 26); + + yield resumeDebuggerThenCloseAndFinish(panel); + }); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-reexpand-01.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-reexpand-01.js new file mode 100644 index 000000000..ce9c74eef --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-reexpand-01.js @@ -0,0 +1,201 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that the variables view correctly re-expands nodes after pauses. + */ + +const TAB_URL = EXAMPLE_URL + "doc_with-frame.html"; + +let gTab, gPanel, gDebugger; +let gBreakpoints, gSources, gVariables; + +function test() { + // Debug test slaves are a bit slow at this test. + requestLongerTimeout(4); + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gBreakpoints = gDebugger.DebuggerController.Breakpoints; + gSources = gDebugger.DebuggerView.Sources; + gVariables = gDebugger.DebuggerView.Variables; + + // Always expand all items between pauses except 'window' variables. + gVariables.commitHierarchyIgnoredItems = Object.create(null, { window: { value: true } }); + + waitForSourceShown(gPanel, ".html") + .then(addBreakpoint) + .then(() => ensureThreadClientState(gPanel, "resumed")) + .then(pauseDebuggee) + .then(prepareVariablesAndProperties) + .then(stepInDebuggee) + .then(testVariablesExpand) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); +} + +function addBreakpoint() { + return gBreakpoints.addBreakpoint({ actor: gSources.selectedValue, line: 21 }); +} + +function pauseDebuggee() { + sendMouseClickToTab(gTab, content.document.querySelector("button")); + + // The first 'with' scope should be expanded by default, but the + // variables haven't been fetched yet. This is how 'with' scopes work. + return promise.all([ + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_VARIABLES) + ]); +} + +function stepInDebuggee() { + // Spin the event loop before causing the debuggee to pause, to allow + // this function to return first. + executeSoon(() => { + EventUtils.sendMouseEvent({ type: "mousedown" }, + gDebugger.document.querySelector("#step-in"), + gDebugger); + }); + + return promise.all([ + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES, 1), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_VARIABLES, 3), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 1), + ]); +} + +function testVariablesExpand() { + let localScope = gVariables.getScopeAtIndex(0); + let withScope = gVariables.getScopeAtIndex(1); + let functionScope = gVariables.getScopeAtIndex(2); + let globalScope = gVariables.getScopeAtIndex(3); + + let thisVar = localScope.get("this"); + let windowVar = thisVar.get("window"); + + is(localScope.target.querySelector(".arrow").hasAttribute("open"), true, + "The localScope arrow should still be expanded."); + is(withScope.target.querySelector(".arrow").hasAttribute("open"), true, + "The withScope arrow should still be expanded."); + is(functionScope.target.querySelector(".arrow").hasAttribute("open"), true, + "The functionScope arrow should still be expanded."); + is(globalScope.target.querySelector(".arrow").hasAttribute("open"), true, + "The globalScope arrow should still be expanded."); + is(thisVar.target.querySelector(".arrow").hasAttribute("open"), true, + "The thisVar arrow should still be expanded."); + is(windowVar.target.querySelector(".arrow").hasAttribute("open"), false, + "The windowVar arrow should not be expanded."); + + is(localScope.target.querySelector(".variables-view-element-details").hasAttribute("open"), true, + "The localScope enumerables should still be expanded."); + is(withScope.target.querySelector(".variables-view-element-details").hasAttribute("open"), true, + "The withScope enumerables should still be expanded."); + is(functionScope.target.querySelector(".variables-view-element-details").hasAttribute("open"), true, + "The functionScope enumerables should still be expanded."); + is(globalScope.target.querySelector(".variables-view-element-details").hasAttribute("open"), true, + "The globalScope enumerables should still be expanded."); + is(thisVar.target.querySelector(".variables-view-element-details").hasAttribute("open"), true, + "The thisVar enumerables should still be expanded."); + is(windowVar.target.querySelector(".variables-view-element-details").hasAttribute("open"), false, + "The windowVar enumerables should not be expanded."); + + is(localScope.expanded, true, + "The localScope expanded getter should return true."); + is(withScope.expanded, true, + "The withScope expanded getter should return true."); + is(functionScope.expanded, true, + "The functionScope expanded getter should return true."); + is(globalScope.expanded, true, + "The globalScope expanded getter should return true."); + is(thisVar.expanded, true, + "The thisVar expanded getter should return true."); + is(windowVar.expanded, false, + "The windowVar expanded getter should return true."); +} + +function prepareVariablesAndProperties() { + let deferred = promise.defer(); + + let localScope = gVariables.getScopeAtIndex(0); + let withScope = gVariables.getScopeAtIndex(1); + let functionScope = gVariables.getScopeAtIndex(2); + let globalScope = gVariables.getScopeAtIndex(3); + + is(localScope.expanded, true, + "The localScope should be expanded."); + is(withScope.expanded, false, + "The withScope should not be expanded yet."); + is(functionScope.expanded, false, + "The functionScope should not be expanded yet."); + is(globalScope.expanded, false, + "The globalScope should not be expanded yet."); + + // Wait for only two events to be triggered, because the Function scope is + // an environment to which scope arguments and variables are already attached. + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_VARIABLES, 2).then(() => { + is(localScope.expanded, true, + "The localScope should now be expanded."); + is(withScope.expanded, true, + "The withScope should now be expanded."); + is(functionScope.expanded, true, + "The functionScope should now be expanded."); + is(globalScope.expanded, true, + "The globalScope should now be expanded."); + + let thisVar = localScope.get("this"); + + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 1).then(() => { + let windowVar = thisVar.get("window"); + + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 1).then(() => { + let documentVar = windowVar.get("document"); + + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 1).then(() => { + let locationVar = documentVar.get("location"); + + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 1).then(() => { + is(thisVar.expanded, true, + "The local scope 'this' should be expanded."); + is(windowVar.expanded, true, + "The local scope 'this.window' should be expanded."); + is(documentVar.expanded, true, + "The local scope 'this.window.document' should be expanded."); + is(locationVar.expanded, true, + "The local scope 'this.window.document.location' should be expanded."); + + deferred.resolve(); + }); + + locationVar.expand(); + }); + + documentVar.expand(); + }); + + windowVar.expand(); + }); + + thisVar.expand(); + }); + + withScope.expand(); + functionScope.expand(); + globalScope.expand(); + + return deferred.promise; +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gBreakpoints = null; + gSources = null; + gVariables = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-reexpand-02.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-reexpand-02.js new file mode 100644 index 000000000..1afa7370f --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-reexpand-02.js @@ -0,0 +1,216 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that the variables view correctly re-expands nodes after pauses, + * with the caveat that there are no ignored items in the hierarchy. + */ + +const TAB_URL = EXAMPLE_URL + "doc_with-frame.html"; + +let gTab, gPanel, gDebugger; +let gBreakpoints, gSources, gVariables; + +function test() { + // Debug test slaves are a bit slow at this test. + requestLongerTimeout(4); + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gBreakpoints = gDebugger.DebuggerController.Breakpoints; + gSources = gDebugger.DebuggerView.Sources; + gVariables = gDebugger.DebuggerView.Variables; + + // Always expand all items between pauses. + gVariables.commitHierarchyIgnoredItems = Object.create(null); + + waitForSourceShown(gPanel, ".html") + .then(addBreakpoint) + .then(() => ensureThreadClientState(gPanel, "resumed")) + .then(pauseDebuggee) + .then(prepareVariablesAndProperties) + .then(stepInDebuggee) + .then(testVariablesExpand) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); +} + +function addBreakpoint() { + return gBreakpoints.addBreakpoint({ actor: gSources.selectedValue, line: 21 }); +} + +function pauseDebuggee() { + sendMouseClickToTab(gTab, content.document.querySelector("button")); + + // The first 'with' scope should be expanded by default, but the + // variables haven't been fetched yet. This is how 'with' scopes work. + return promise.all([ + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_VARIABLES) + ]); +} + +function stepInDebuggee() { + // Spin the event loop before causing the debuggee to pause, to allow + // this function to return first. + executeSoon(() => { + EventUtils.sendMouseEvent({ type: "mousedown" }, + gDebugger.document.querySelector("#step-in"), + gDebugger); + }); + + return promise.all([ + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES, 1), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_VARIABLES, 3), + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 4), + ]); +} + +function testVariablesExpand() { + let localScope = gVariables.getScopeAtIndex(0); + let withScope = gVariables.getScopeAtIndex(1); + let functionScope = gVariables.getScopeAtIndex(2); + let globalScope = gVariables.getScopeAtIndex(3); + + let thisVar = localScope.get("this"); + let windowVar = thisVar.get("window"); + let documentVar = windowVar.get("document"); + let locationVar = documentVar.get("location"); + + is(localScope.target.querySelector(".arrow").hasAttribute("open"), true, + "The localScope arrow should still be expanded."); + is(withScope.target.querySelector(".arrow").hasAttribute("open"), true, + "The withScope arrow should still be expanded."); + is(functionScope.target.querySelector(".arrow").hasAttribute("open"), true, + "The functionScope arrow should still be expanded."); + is(globalScope.target.querySelector(".arrow").hasAttribute("open"), true, + "The globalScope arrow should still be expanded."); + is(thisVar.target.querySelector(".arrow").hasAttribute("open"), true, + "The thisVar arrow should still be expanded."); + is(windowVar.target.querySelector(".arrow").hasAttribute("open"), true, + "The windowVar arrow should still be expanded."); + is(documentVar.target.querySelector(".arrow").hasAttribute("open"), true, + "The documentVar arrow should still be expanded."); + is(locationVar.target.querySelector(".arrow").hasAttribute("open"), true, + "The locationVar arrow should still be expanded."); + + is(localScope.target.querySelector(".variables-view-element-details").hasAttribute("open"), true, + "The localScope enumerables should still be expanded."); + is(withScope.target.querySelector(".variables-view-element-details").hasAttribute("open"), true, + "The withScope enumerables should still be expanded."); + is(functionScope.target.querySelector(".variables-view-element-details").hasAttribute("open"), true, + "The functionScope enumerables should still be expanded."); + is(globalScope.target.querySelector(".variables-view-element-details").hasAttribute("open"), true, + "The globalScope enumerables should still be expanded."); + is(thisVar.target.querySelector(".variables-view-element-details").hasAttribute("open"), true, + "The thisVar enumerables should still be expanded."); + is(windowVar.target.querySelector(".variables-view-element-details").hasAttribute("open"), true, + "The windowVar enumerables should still be expanded."); + is(documentVar.target.querySelector(".variables-view-element-details").hasAttribute("open"), true, + "The documentVar enumerables should still be expanded."); + is(locationVar.target.querySelector(".variables-view-element-details").hasAttribute("open"), true, + "The locationVar enumerables should still be expanded."); + + is(localScope.expanded, true, + "The localScope expanded getter should return true."); + is(withScope.expanded, true, + "The withScope expanded getter should return true."); + is(functionScope.expanded, true, + "The functionScope expanded getter should return true."); + is(globalScope.expanded, true, + "The globalScope expanded getter should return true."); + is(thisVar.expanded, true, + "The thisVar expanded getter should return true."); + is(windowVar.expanded, true, + "The windowVar expanded getter should return true."); + is(documentVar.expanded, true, + "The documentVar expanded getter should return true."); + is(locationVar.expanded, true, + "The locationVar expanded getter should return true."); +} + +function prepareVariablesAndProperties() { + let deferred = promise.defer(); + + let localScope = gVariables.getScopeAtIndex(0); + let withScope = gVariables.getScopeAtIndex(1); + let functionScope = gVariables.getScopeAtIndex(2); + let globalScope = gVariables.getScopeAtIndex(3); + + is(localScope.expanded, true, + "The localScope should be expanded."); + is(withScope.expanded, false, + "The withScope should not be expanded yet."); + is(functionScope.expanded, false, + "The functionScope should not be expanded yet."); + is(globalScope.expanded, false, + "The globalScope should not be expanded yet."); + + // Wait for only two events to be triggered, because the Function scope is + // an environment to which scope arguments and variables are already attached. + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_VARIABLES, 2).then(() => { + is(localScope.expanded, true, + "The localScope should now be expanded."); + is(withScope.expanded, true, + "The withScope should now be expanded."); + is(functionScope.expanded, true, + "The functionScope should now be expanded."); + is(globalScope.expanded, true, + "The globalScope should now be expanded."); + + let thisVar = localScope.get("this"); + + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 1).then(() => { + let windowVar = thisVar.get("window"); + + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 1).then(() => { + let documentVar = windowVar.get("document"); + + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 1).then(() => { + let locationVar = documentVar.get("location"); + + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 1).then(() => { + is(thisVar.expanded, true, + "The local scope 'this' should be expanded."); + is(windowVar.expanded, true, + "The local scope 'this.window' should be expanded."); + is(documentVar.expanded, true, + "The local scope 'this.window.document' should be expanded."); + is(locationVar.expanded, true, + "The local scope 'this.window.document.location' should be expanded."); + + deferred.resolve(); + }); + + locationVar.expand(); + }); + + documentVar.expand(); + }); + + windowVar.expand(); + }); + + thisVar.expand(); + }); + + withScope.expand(); + functionScope.expand(); + globalScope.expand(); + + return deferred.promise; +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gBreakpoints = null; + gSources = null; + gVariables = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-reexpand-03.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-reexpand-03.js new file mode 100644 index 000000000..2b94674eb --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-reexpand-03.js @@ -0,0 +1,123 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that the variables view correctly re-expands *scopes* after pauses. + */ + +const TAB_URL = EXAMPLE_URL + "doc_scope-variable-4.html"; + +let gTab, gPanel, gDebugger; +let gBreakpoints, gSources, gVariables; + +function test() { + // Debug test slaves are a bit slow at this test. + requestLongerTimeout(4); + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gBreakpoints = gDebugger.DebuggerController.Breakpoints; + gSources = gDebugger.DebuggerView.Sources; + gVariables = gDebugger.DebuggerView.Variables; + + // Always expand all items between pauses. + gVariables.commitHierarchyIgnoredItems = Object.create(null); + + waitForSourceShown(gPanel, ".html") + .then(addBreakpoint) + .then(() => ensureThreadClientState(gPanel, "resumed")) + .then(pauseDebuggee) + .then(prepareScopes) + .then(resumeDebuggee) + .then(testVariablesExpand) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); +} + +function addBreakpoint() { + return gBreakpoints.addBreakpoint({ actor: gSources.selectedValue, line: 18 }); +} + +function pauseDebuggee() { + callInTab(gTab, "test"); + + return waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES); +} + +function resumeDebuggee() { + // Spin the event loop before causing the debuggee to pause, to allow + // this function to return first. + executeSoon(() => { + EventUtils.sendMouseEvent({ type: "mousedown" }, + gDebugger.document.querySelector("#resume"), + gDebugger); + }); + + return waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES); +} + +function testVariablesExpand() { + let localScope = gVariables.getScopeAtIndex(0); + let functionScope = gVariables.getScopeAtIndex(1); + let globalScope = gVariables.getScopeAtIndex(2); + + is(localScope.target.querySelector(".arrow").hasAttribute("open"), true, + "The localScope arrow should still be expanded."); + is(functionScope.target.querySelector(".arrow").hasAttribute("open"), true, + "The functionScope arrow should still be expanded."); + is(globalScope.target.querySelector(".arrow").hasAttribute("open"), false, + "The globalScope arrow should not be expanded."); + + is(localScope.target.querySelector(".variables-view-element-details").hasAttribute("open"), true, + "The localScope enumerables should still be expanded."); + is(functionScope.target.querySelector(".variables-view-element-details").hasAttribute("open"), true, + "The functionScope enumerables should still be expanded."); + is(globalScope.target.querySelector(".variables-view-element-details").hasAttribute("open"), false, + "The globalScope enumerables should not be expanded."); + + is(localScope.expanded, true, + "The localScope expanded getter should return true."); + is(functionScope.expanded, true, + "The functionScope expanded getter should return true."); + is(globalScope.expanded, false, + "The globalScope expanded getter should return false."); +} + +function prepareScopes() { + let localScope = gVariables.getScopeAtIndex(0); + let functionScope = gVariables.getScopeAtIndex(1); + let globalScope = gVariables.getScopeAtIndex(2); + + is(localScope.expanded, true, + "The localScope should be expanded."); + is(functionScope.expanded, false, + "The functionScope should not be expanded yet."); + is(globalScope.expanded, false, + "The globalScope should not be expanded yet."); + + localScope.collapse(); + functionScope.expand(); + + // Don't for any events to be triggered, because the Function scope is + // an environment to which scope arguments and variables are already attached. + is(localScope.expanded, false, + "The localScope should not be expanded anymore."); + is(functionScope.expanded, true, + "The functionScope should now be expanded."); + is(globalScope.expanded, false, + "The globalScope should still not be expanded."); +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gBreakpoints = null; + gSources = null; + gVariables = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-webidl.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-webidl.js new file mode 100644 index 000000000..153fe7499 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-webidl.js @@ -0,0 +1,256 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that the variables view correctly displays WebIDL attributes in DOM + * objects. + */ + +const TAB_URL = EXAMPLE_URL + "doc_frame-parameters.html"; + +let gTab, gPanel, gDebugger; +let gVariables; + +function test() { + // Debug test slaves are a bit slow at this test. + requestLongerTimeout(2); + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gVariables = gDebugger.DebuggerView.Variables; + + waitForSourceAndCaretAndScopes(gPanel, ".html", 24) + .then(expandGlobalScope) + .then(performTest) + .then(() => resumeDebuggerThenCloseAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + + sendMouseClickToTab(gTab, content.document.querySelector("button")); + }); +} + +function expandGlobalScope() { + let deferred = promise.defer(); + + let globalScope = gVariables.getScopeAtIndex(1); + is(globalScope.expanded, false, + "The global scope should not be expanded by default."); + + gDebugger.once(gDebugger.EVENTS.FETCHED_VARIABLES, deferred.resolve); + + EventUtils.sendMouseEvent({ type: "mousedown" }, + globalScope.target.querySelector(".name"), + gDebugger); + + return deferred.promise; +} + +function performTest() { + let deferred = promise.defer(); + let globalScope = gVariables.getScopeAtIndex(1); + + let buttonVar = globalScope.get("button"); + let buttonAsProtoVar = globalScope.get("buttonAsProto"); + let documentVar = globalScope.get("document"); + + is(buttonVar.target.querySelector(".name").getAttribute("value"), "button", + "Should have the right property name for 'button'."); + is(buttonVar.target.querySelector(".value").getAttribute("value"), "<button>", + "Should have the right property value for 'button'."); + ok(buttonVar.target.querySelector(".value").className.contains("token-domnode"), + "Should have the right token class for 'button'."); + + is(buttonAsProtoVar.target.querySelector(".name").getAttribute("value"), "buttonAsProto", + "Should have the right property name for 'buttonAsProto'."); + is(buttonAsProtoVar.target.querySelector(".value").getAttribute("value"), "Object", + "Should have the right property value for 'buttonAsProto'."); + ok(buttonAsProtoVar.target.querySelector(".value").className.contains("token-other"), + "Should have the right token class for 'buttonAsProto'."); + + is(documentVar.target.querySelector(".name").getAttribute("value"), "document", + "Should have the right property name for 'document'."); + is(documentVar.target.querySelector(".value").getAttribute("value"), + "HTMLDocument \u2192 doc_frame-parameters.html", + "Should have the right property value for 'document'."); + ok(documentVar.target.querySelector(".value").className.contains("token-domnode"), + "Should have the right token class for 'document'."); + + is(buttonVar.expanded, false, + "The buttonVar should not be expanded at this point."); + is(buttonAsProtoVar.expanded, false, + "The buttonAsProtoVar should not be expanded at this point."); + is(documentVar.expanded, false, + "The documentVar should not be expanded at this point."); + + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 3).then(() => { + is(buttonVar.get("type").target.querySelector(".name").getAttribute("value"), "type", + "Should have the right property name for 'type'."); + is(buttonVar.get("type").target.querySelector(".value").getAttribute("value"), "\"submit\"", + "Should have the right property value for 'type'."); + ok(buttonVar.get("type").target.querySelector(".value").className.contains("token-string"), + "Should have the right token class for 'type'."); + + is(buttonVar.get("childNodes").target.querySelector(".name").getAttribute("value"), "childNodes", + "Should have the right property name for 'childNodes'."); + is(buttonVar.get("childNodes").target.querySelector(".value").getAttribute("value"), "NodeList[1]", + "Should have the right property value for 'childNodes'."); + ok(buttonVar.get("childNodes").target.querySelector(".value").className.contains("token-other"), + "Should have the right token class for 'childNodes'."); + + is(buttonVar.get("onclick").target.querySelector(".name").getAttribute("value"), "onclick", + "Should have the right property name for 'onclick'."); + is(buttonVar.get("onclick").target.querySelector(".value").getAttribute("value"), "onclick(event)", + "Should have the right property value for 'onclick'."); + ok(buttonVar.get("onclick").target.querySelector(".value").className.contains("token-other"), + "Should have the right token class for 'onclick'."); + + is(documentVar.get("title").target.querySelector(".name").getAttribute("value"), "title", + "Should have the right property name for 'title'."); + is(documentVar.get("title").target.querySelector(".value").getAttribute("value"), "\"Debugger test page\"", + "Should have the right property value for 'title'."); + ok(documentVar.get("title").target.querySelector(".value").className.contains("token-string"), + "Should have the right token class for 'title'."); + + is(documentVar.get("childNodes").target.querySelector(".name").getAttribute("value"), "childNodes", + "Should have the right property name for 'childNodes'."); + is(documentVar.get("childNodes").target.querySelector(".value").getAttribute("value"), "NodeList[3]", + "Should have the right property value for 'childNodes'."); + ok(documentVar.get("childNodes").target.querySelector(".value").className.contains("token-other"), + "Should have the right token class for 'childNodes'."); + + is(documentVar.get("onclick").target.querySelector(".name").getAttribute("value"), "onclick", + "Should have the right property name for 'onclick'."); + is(documentVar.get("onclick").target.querySelector(".value").getAttribute("value"), "null", + "Should have the right property value for 'onclick'."); + ok(documentVar.get("onclick").target.querySelector(".value").className.contains("token-null"), + "Should have the right token class for 'onclick'."); + + let buttonProtoVar = buttonVar.get("__proto__"); + let buttonAsProtoProtoVar = buttonAsProtoVar.get("__proto__"); + let documentProtoVar = documentVar.get("__proto__"); + + is(buttonProtoVar.target.querySelector(".name").getAttribute("value"), "__proto__", + "Should have the right property name for '__proto__'."); + is(buttonProtoVar.target.querySelector(".value").getAttribute("value"), "HTMLButtonElementPrototype", + "Should have the right property value for '__proto__'."); + ok(buttonProtoVar.target.querySelector(".value").className.contains("token-other"), + "Should have the right token class for '__proto__'."); + + is(buttonAsProtoProtoVar.target.querySelector(".name").getAttribute("value"), "__proto__", + "Should have the right property name for '__proto__'."); + is(buttonAsProtoProtoVar.target.querySelector(".value").getAttribute("value"), "<button>", + "Should have the right property value for '__proto__'."); + ok(buttonAsProtoProtoVar.target.querySelector(".value").className.contains("token-domnode"), + "Should have the right token class for '__proto__'."); + + is(documentProtoVar.target.querySelector(".name").getAttribute("value"), "__proto__", + "Should have the right property name for '__proto__'."); + is(documentProtoVar.target.querySelector(".value").getAttribute("value"), "HTMLDocumentPrototype", + "Should have the right property value for '__proto__'."); + ok(documentProtoVar.target.querySelector(".value").className.contains("token-other"), + "Should have the right token class for '__proto__'."); + + is(buttonProtoVar.expanded, false, + "The buttonProtoVar should not be expanded at this point."); + is(buttonAsProtoProtoVar.expanded, false, + "The buttonAsProtoProtoVar should not be expanded at this point."); + is(documentProtoVar.expanded, false, + "The documentProtoVar should not be expanded at this point."); + + waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 3).then(() => { + is(buttonAsProtoProtoVar.get("type").target.querySelector(".name").getAttribute("value"), "type", + "Should have the right property name for 'type'."); + is(buttonAsProtoProtoVar.get("type").target.querySelector(".value").getAttribute("value"), "\"submit\"", + "Should have the right property value for 'type'."); + ok(buttonAsProtoProtoVar.get("type").target.querySelector(".value").className.contains("token-string"), + "Should have the right token class for 'type'."); + + is(buttonAsProtoProtoVar.get("childNodes").target.querySelector(".name").getAttribute("value"), "childNodes", + "Should have the right property name for 'childNodes'."); + is(buttonAsProtoProtoVar.get("childNodes").target.querySelector(".value").getAttribute("value"), "NodeList[1]", + "Should have the right property value for 'childNodes'."); + ok(buttonAsProtoProtoVar.get("childNodes").target.querySelector(".value").className.contains("token-other"), + "Should have the right token class for 'childNodes'."); + + is(buttonAsProtoProtoVar.get("onclick").target.querySelector(".name").getAttribute("value"), "onclick", + "Should have the right property name for 'onclick'."); + is(buttonAsProtoProtoVar.get("onclick").target.querySelector(".value").getAttribute("value"), "onclick(event)", + "Should have the right property value for 'onclick'."); + ok(buttonAsProtoProtoVar.get("onclick").target.querySelector(".value").className.contains("token-other"), + "Should have the right token class for 'onclick'."); + + let buttonProtoProtoVar = buttonProtoVar.get("__proto__"); + let buttonAsProtoProtoProtoVar = buttonAsProtoProtoVar.get("__proto__"); + let documentProtoProtoVar = documentProtoVar.get("__proto__"); + + is(buttonProtoProtoVar.target.querySelector(".name").getAttribute("value"), "__proto__", + "Should have the right property name for '__proto__'."); + is(buttonProtoProtoVar.target.querySelector(".value").getAttribute("value"), "HTMLElementPrototype", + "Should have the right property value for '__proto__'."); + ok(buttonProtoProtoVar.target.querySelector(".value").className.contains("token-other"), + "Should have the right token class for '__proto__'."); + + is(buttonAsProtoProtoProtoVar.target.querySelector(".name").getAttribute("value"), "__proto__", + "Should have the right property name for '__proto__'."); + is(buttonAsProtoProtoProtoVar.target.querySelector(".value").getAttribute("value"), "HTMLButtonElementPrototype", + "Should have the right property value for '__proto__'."); + ok(buttonAsProtoProtoProtoVar.target.querySelector(".value").className.contains("token-other"), + "Should have the right token class for '__proto__'."); + + is(documentProtoProtoVar.target.querySelector(".name").getAttribute("value"), "__proto__", + "Should have the right property name for '__proto__'."); + is(documentProtoProtoVar.target.querySelector(".value").getAttribute("value"), "DocumentPrototype", + "Should have the right property value for '__proto__'."); + ok(documentProtoProtoVar.target.querySelector(".value").className.contains("token-other"), + "Should have the right token class for '__proto__'.") + + is(buttonAsProtoProtoProtoVar.expanded, false, + "The buttonAsProtoProtoProtoVar should not be expanded at this point."); + is(buttonAsProtoProtoProtoVar.expanded, false, + "The buttonAsProtoProtoProtoVar should not be expanded at this point."); + is(documentProtoProtoVar.expanded, false, + "The documentProtoProtoVar should not be expanded at this point."); + + deferred.resolve(); + }); + + // Similarly, expand the 'button.__proto__', 'buttonAsProto.__proto__' and + // 'document.__proto__' variables view nodes. + buttonProtoVar.expand(); + buttonAsProtoProtoVar.expand(); + documentProtoVar.expand(); + + is(buttonProtoVar.expanded, true, + "The buttonProtoVar should be immediately marked as expanded."); + is(buttonAsProtoProtoVar.expanded, true, + "The buttonAsProtoProtoVar should be immediately marked as expanded."); + is(documentProtoVar.expanded, true, + "The documentProtoVar should be immediately marked as expanded."); + }); + + // Expand the 'button', 'buttonAsProto' and 'document' variables view nodes. + // This causes their properties to be retrieved and displayed. + buttonVar.expand(); + buttonAsProtoVar.expand(); + documentVar.expand(); + + is(buttonVar.expanded, true, + "The buttonVar should be immediately marked as expanded."); + is(buttonAsProtoVar.expanded, true, + "The buttonAsProtoVar should be immediately marked as expanded."); + is(documentVar.expanded, true, + "The documentVar should be immediately marked as expanded."); + + return deferred.promise; +} + +registerCleanupFunction(function() { + gTab = null; + gPanel = null; + gDebugger = null; + gVariables = null; +}); diff --git a/toolkit/devtools/debugger/test/browser_dbg_watch-expressions-01.js b/toolkit/devtools/debugger/test/browser_dbg_watch-expressions-01.js new file mode 100644 index 000000000..51a6d775a --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_watch-expressions-01.js @@ -0,0 +1,227 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Bug 727429: Test the debugger watch expressions. + */ + +const TAB_URL = EXAMPLE_URL + "doc_watch-expressions.html"; + +function test() { + // Debug test slaves are a bit slow at this test. + requestLongerTimeout(2); + + let gTab, gPanel, gDebugger; + let gEditor, gWatch, gVariables; + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gEditor = gDebugger.DebuggerView.editor; + gWatch = gDebugger.DebuggerView.WatchExpressions; + gVariables = gDebugger.DebuggerView.Variables; + + gDebugger.DebuggerView.toggleInstrumentsPane({ visible: true, animated: false }); + + waitForSourceShown(gPanel, ".html") + .then(() => performTest()) + .then(() => closeDebuggerAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); + + function performTest() { + is(gWatch.getAllStrings().length, 0, + "There should initially be no watch expressions."); + + addAndCheckExpressions(1, 0, "a"); + addAndCheckExpressions(2, 0, "b"); + addAndCheckExpressions(3, 0, "c"); + + removeAndCheckExpression(2, 1, "a"); + removeAndCheckExpression(1, 0, "a"); + + addAndCheckExpressions(2, 0, "", true); + gEditor.focus(); + is(gWatch.getAllStrings().length, 1, + "Empty watch expressions are automatically removed."); + + addAndCheckExpressions(2, 0, "a", true); + gEditor.focus(); + is(gWatch.getAllStrings().length, 1, + "Duplicate watch expressions are automatically removed."); + + addAndCheckExpressions(2, 0, "a\t", true); + addAndCheckExpressions(2, 0, "a\r", true); + addAndCheckExpressions(2, 0, "a\n", true); + gEditor.focus(); + is(gWatch.getAllStrings().length, 1, + "Duplicate watch expressions are automatically removed."); + + addAndCheckExpressions(2, 0, "\ta", true); + addAndCheckExpressions(2, 0, "\ra", true); + addAndCheckExpressions(2, 0, "\na", true); + gEditor.focus(); + is(gWatch.getAllStrings().length, 1, + "Duplicate watch expressions are automatically removed."); + + addAndCheckCustomExpression(2, 0, "bazΩΩka"); + addAndCheckCustomExpression(3, 0, "bambøøcha"); + + EventUtils.sendMouseEvent({ type: "click" }, + gWatch.getItemAtIndex(0).attachment.view.closeNode, + gDebugger); + + is(gWatch.getAllStrings().length, 2, + "Watch expressions are removed when the close button is pressed."); + is(gWatch.getAllStrings()[0], "bazΩΩka", + "The expression at index " + 0 + " should be correct (1)."); + is(gWatch.getAllStrings()[1], "a", + "The expression at index " + 1 + " should be correct (2)."); + + EventUtils.sendMouseEvent({ type: "click" }, + gWatch.getItemAtIndex(0).attachment.view.closeNode, + gDebugger); + + is(gWatch.getAllStrings().length, 1, + "Watch expressions are removed when the close button is pressed."); + is(gWatch.getAllStrings()[0], "a", + "The expression at index " + 0 + " should be correct (3)."); + + EventUtils.sendMouseEvent({ type: "click" }, + gWatch.getItemAtIndex(0).attachment.view.closeNode, + gDebugger); + + is(gWatch.getAllStrings().length, 0, + "Watch expressions are removed when the close button is pressed."); + + EventUtils.sendMouseEvent({ type: "click" }, + gWatch.widget._parent, + gDebugger); + + is(gWatch.getAllStrings().length, 1, + "Watch expressions are added when the view container is pressed."); + } + + function addAndCheckCustomExpression(aTotal, aIndex, aString, noBlur) { + addAndCheckExpressions(aTotal, aIndex, "", true); + + for (let i = 0; i < aString.length; i++) { + EventUtils.sendChar(aString[i], gDebugger); + } + + gEditor.focus(); + + let element = gWatch.getItemAtIndex(aIndex).target; + + is(gWatch.getItemAtIndex(aIndex).attachment.initialExpression, "", + "The initial expression at index " + aIndex + " should be correct (1)."); + is(gWatch.getItemForElement(element).attachment.initialExpression, "", + "The initial expression at index " + aIndex + " should be correct (2)."); + + is(gWatch.getItemAtIndex(aIndex).attachment.currentExpression, aString, + "The expression at index " + aIndex + " should be correct (1)."); + is(gWatch.getItemForElement(element).attachment.currentExpression, aString, + "The expression at index " + aIndex + " should be correct (2)."); + + is(gWatch.getString(aIndex), aString, + "The expression at index " + aIndex + " should be correct (3)."); + is(gWatch.getAllStrings()[aIndex], aString, + "The expression at index " + aIndex + " should be correct (4)."); + } + + function addAndCheckExpressions(aTotal, aIndex, aString, noBlur) { + gWatch.addExpression(aString); + + is(gWatch.getAllStrings().length, aTotal, + "There should be " + aTotal + " watch expressions available (1)."); + is(gWatch.itemCount, aTotal, + "There should be " + aTotal + " watch expressions available (2)."); + + ok(gWatch.getItemAtIndex(aIndex), + "The expression at index " + aIndex + " should be available."); + is(gWatch.getItemAtIndex(aIndex).attachment.initialExpression, aString, + "The expression at index " + aIndex + " should have an initial expression."); + + let element = gWatch.getItemAtIndex(aIndex).target; + + ok(element, + "There should be a new expression item in the view."); + ok(gWatch.getItemForElement(element), + "The watch expression item should be accessible."); + is(gWatch.getItemForElement(element), gWatch.getItemAtIndex(aIndex), + "The correct watch expression item was accessed."); + + ok(gWatch.widget.getItemAtIndex(aIndex) instanceof XULElement, + "The correct watch expression element was accessed (1)."); + is(element, gWatch.widget.getItemAtIndex(aIndex), + "The correct watch expression element was accessed (2)."); + + is(gWatch.getItemForElement(element).attachment.view.arrowNode.hidden, false, + "The arrow node should be visible."); + is(gWatch.getItemForElement(element).attachment.view.closeNode.hidden, false, + "The close button should be visible."); + is(gWatch.getItemForElement(element).attachment.view.inputNode.getAttribute("focused"), "true", + "The textbox input should be focused."); + + is(gVariables.parentNode.scrollTop, 0, + "The variables view should be scrolled to top"); + + is(gWatch.items[0], gWatch.getItemAtIndex(aIndex), + "The correct watch expression was added to the cache (1)."); + is(gWatch.items[0], gWatch.getItemForElement(element), + "The correct watch expression was added to the cache (2)."); + + if (!noBlur) { + gEditor.focus(); + + is(gWatch.getItemAtIndex(aIndex).attachment.initialExpression, aString, + "The initial expression at index " + aIndex + " should be correct (1)."); + is(gWatch.getItemForElement(element).attachment.initialExpression, aString, + "The initial expression at index " + aIndex + " should be correct (2)."); + + is(gWatch.getItemAtIndex(aIndex).attachment.currentExpression, aString, + "The expression at index " + aIndex + " should be correct (1)."); + is(gWatch.getItemForElement(element).attachment.currentExpression, aString, + "The expression at index " + aIndex + " should be correct (2)."); + + is(gWatch.getString(aIndex), aString, + "The expression at index " + aIndex + " should be correct (3)."); + is(gWatch.getAllStrings()[aIndex], aString, + "The expression at index " + aIndex + " should be correct (4)."); + } + } + + function removeAndCheckExpression(aTotal, aIndex, aString) { + gWatch.removeAt(aIndex); + + is(gWatch.getAllStrings().length, aTotal, + "There should be " + aTotal + " watch expressions available (1)."); + is(gWatch.itemCount, aTotal, + "There should be " + aTotal + " watch expressions available (2)."); + + ok(gWatch.getItemAtIndex(aIndex), + "The expression at index " + aIndex + " should still be available."); + is(gWatch.getItemAtIndex(aIndex).attachment.initialExpression, aString, + "The expression at index " + aIndex + " should still have an initial expression."); + + let element = gWatch.getItemAtIndex(aIndex).target; + + is(gWatch.getItemAtIndex(aIndex).attachment.initialExpression, aString, + "The initial expression at index " + aIndex + " should be correct (1)."); + is(gWatch.getItemForElement(element).attachment.initialExpression, aString, + "The initial expression at index " + aIndex + " should be correct (2)."); + + is(gWatch.getItemAtIndex(aIndex).attachment.currentExpression, aString, + "The expression at index " + aIndex + " should be correct (1)."); + is(gWatch.getItemForElement(element).attachment.currentExpression, aString, + "The expression at index " + aIndex + " should be correct (2)."); + + is(gWatch.getString(aIndex), aString, + "The expression at index " + aIndex + " should be correct (3)."); + is(gWatch.getAllStrings()[aIndex], aString, + "The expression at index " + aIndex + " should be correct (4)."); + } +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_watch-expressions-02.js b/toolkit/devtools/debugger/test/browser_dbg_watch-expressions-02.js new file mode 100644 index 000000000..4d4b785b9 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_watch-expressions-02.js @@ -0,0 +1,371 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Bug 727429: Test the debugger watch expressions. + */ + +const TAB_URL = EXAMPLE_URL + "doc_watch-expressions.html"; + +function test() { + // Debug test slaves are a bit slow at this test. + requestLongerTimeout(2); + + let gTab, gPanel, gDebugger; + let gWatch, gVariables; + + initDebugger(TAB_URL).then(([aTab,, aPanel]) => { + gTab = aTab; + gPanel = aPanel; + gDebugger = gPanel.panelWin; + gWatch = gDebugger.DebuggerView.WatchExpressions; + gVariables = gDebugger.DebuggerView.Variables; + + gDebugger.DebuggerView.toggleInstrumentsPane({ visible: true, animated: false }); + + waitForSourceShown(gPanel, ".html", 1) + .then(addExpressions) + .then(performTest) + .then(finishTest) + .then(() => closeDebuggerAndFinish(gPanel)) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); + + function addExpressions() { + gWatch.addExpression("'a'"); + gWatch.addExpression("\"a\""); + gWatch.addExpression("'a\"\"'"); + gWatch.addExpression("\"a''\""); + gWatch.addExpression("?"); + gWatch.addExpression("a"); + gWatch.addExpression("this"); + gWatch.addExpression("this.canada"); + gWatch.addExpression("[1, 2, 3]"); + gWatch.addExpression("x = [1, 2, 3]"); + gWatch.addExpression("y = [1, 2, 3]; y.test = 4"); + gWatch.addExpression("z = [1, 2, 3]; z.test = 4; z"); + gWatch.addExpression("t = [1, 2, 3]; t.test = 4; !t"); + gWatch.addExpression("arguments[0]"); + gWatch.addExpression("encodeURI(\"\\\")"); + gWatch.addExpression("decodeURI(\"\\\")"); + gWatch.addExpression("decodeURIComponent(\"%\")"); + gWatch.addExpression("//"); + gWatch.addExpression("// 42"); + gWatch.addExpression("{}.foo"); + gWatch.addExpression("{}.foo()"); + gWatch.addExpression("({}).foo()"); + gWatch.addExpression("new Array(-1)"); + gWatch.addExpression("4.2.toExponential(-4.2)"); + gWatch.addExpression("throw new Error(\"bazinga\")"); + gWatch.addExpression("({ get error() { throw new Error(\"bazinga\") } }).error"); + gWatch.addExpression("throw { get name() { throw \"bazinga\" } }"); + } + + function performTest() { + let deferred = promise.defer(); + + is(gDebugger.document.querySelectorAll(".dbg-expression[hidden=true]").length, 0, + "There should be 0 hidden nodes in the watch expressions container"); + is(gDebugger.document.querySelectorAll(".dbg-expression:not([hidden=true])").length, 27, + "There should be 27 visible nodes in the watch expressions container"); + + test1(function() { + test2(function() { + test3(function() { + test4(function() { + test5(function() { + test6(function() { + test7(function() { + test8(function() { + test9(function() { + deferred.resolve(); + }); + }); + }); + }); + }); + }); + }); + }); + }); + + return deferred.promise; + } + + function finishTest() { + is(gDebugger.document.querySelectorAll(".dbg-expression[hidden=true]").length, 0, + "There should be 0 hidden nodes in the watch expressions container"); + is(gDebugger.document.querySelectorAll(".dbg-expression:not([hidden=true])").length, 27, + "There should be 27 visible nodes in the watch expressions container"); + } + + function test1(aCallback) { + gDebugger.once(gDebugger.EVENTS.FETCHED_WATCH_EXPRESSIONS, () => { + checkWatchExpressions(26, { + a: "ReferenceError: a is not defined", + this: { type: "object", class: "Object" }, + prop: { type: "object", class: "String" }, + args: { type: "undefined" } + }); + aCallback(); + }); + + callInTab(gTab, "test"); + } + + function test2(aCallback) { + gDebugger.once(gDebugger.EVENTS.FETCHED_WATCH_EXPRESSIONS, () => { + checkWatchExpressions(26, { + a: { type: "undefined" }, + this: { type: "object", class: "Window" }, + prop: { type: "undefined" }, + args: "sensational" + }); + aCallback(); + }); + + EventUtils.sendMouseEvent({ type: "mousedown" }, + gDebugger.document.getElementById("resume"), + gDebugger); + } + + function test3(aCallback) { + gDebugger.once(gDebugger.EVENTS.FETCHED_WATCH_EXPRESSIONS, () => { + checkWatchExpressions(26, { + a: { type: "object", class: "Object" }, + this: { type: "object", class: "Window" }, + prop: { type: "undefined" }, + args: "sensational" + }); + aCallback(); + }); + + EventUtils.sendMouseEvent({ type: "mousedown" }, + gDebugger.document.getElementById("resume"), + gDebugger); + } + + function test4(aCallback) { + gDebugger.once(gDebugger.EVENTS.FETCHED_WATCH_EXPRESSIONS, () => { + checkWatchExpressions(27, { + a: 5, + this: { type: "object", class: "Window" }, + prop: { type: "undefined" }, + args: "sensational" + }); + aCallback(); + }); + + gWatch.addExpression("a = 5"); + EventUtils.sendKey("RETURN", gDebugger); + } + + function test5(aCallback) { + gDebugger.once(gDebugger.EVENTS.FETCHED_WATCH_EXPRESSIONS, () => { + checkWatchExpressions(27, { + a: 5, + this: { type: "object", class: "Window" }, + prop: { type: "undefined" }, + args: "sensational" + }); + aCallback(); + }); + + gWatch.addExpression("encodeURI(\"\\\")"); + EventUtils.sendKey("RETURN", gDebugger); + } + + function test6(aCallback) { + gDebugger.once(gDebugger.EVENTS.FETCHED_WATCH_EXPRESSIONS, () => { + checkWatchExpressions(27, { + a: 5, + this: { type: "object", class: "Window" }, + prop: { type: "undefined" }, + args: "sensational" + }); + aCallback(); + }) + + gWatch.addExpression("decodeURI(\"\\\")"); + EventUtils.sendKey("RETURN", gDebugger); + } + + function test7(aCallback) { + gDebugger.once(gDebugger.EVENTS.FETCHED_WATCH_EXPRESSIONS, () => { + checkWatchExpressions(27, { + a: 5, + this: { type: "object", class: "Window" }, + prop: { type: "undefined" }, + args: "sensational" + }); + aCallback(); + }); + + gWatch.addExpression("?"); + EventUtils.sendKey("RETURN", gDebugger); + } + + function test8(aCallback) { + gDebugger.once(gDebugger.EVENTS.FETCHED_WATCH_EXPRESSIONS, () => { + checkWatchExpressions(27, { + a: 5, + this: { type: "object", class: "Window" }, + prop: { type: "undefined" }, + args: "sensational" + }); + aCallback(); + }); + + gWatch.addExpression("a"); + EventUtils.sendKey("RETURN", gDebugger); + } + + function test9(aCallback) { + gDebugger.once(gDebugger.EVENTS.AFTER_FRAMES_CLEARED, () => { + aCallback(); + }); + + EventUtils.sendMouseEvent({ type: "mousedown" }, + gDebugger.document.getElementById("resume"), + gDebugger); + } + + function checkWatchExpressions(aTotal, aExpectedExpressions) { + let { + a: expected_a, + this: expected_this, + prop: expected_prop, + args: expected_args + } = aExpectedExpressions; + + is(gDebugger.document.querySelectorAll(".dbg-expression[hidden=true]").length, aTotal, + "There should be " + aTotal + " hidden nodes in the watch expressions container."); + is(gDebugger.document.querySelectorAll(".dbg-expression:not([hidden=true])").length, 0, + "There should be 0 visible nodes in the watch expressions container."); + + let label = gDebugger.L10N.getStr("watchExpressionsScopeLabel"); + let scope = gVariables._currHierarchy.get(label); + + ok(scope, "There should be a wach expressions scope in the variables view."); + is(scope._store.size, aTotal, "There should be " + aTotal + " evaluations availalble."); + + let w1 = scope.get("'a'"); + let w2 = scope.get("\"a\""); + let w3 = scope.get("'a\"\"'"); + let w4 = scope.get("\"a''\""); + let w5 = scope.get("?"); + let w6 = scope.get("a"); + let w7 = scope.get("this"); + let w8 = scope.get("this.canada"); + let w9 = scope.get("[1, 2, 3]"); + let w10 = scope.get("x = [1, 2, 3]"); + let w11 = scope.get("y = [1, 2, 3]; y.test = 4"); + let w12 = scope.get("z = [1, 2, 3]; z.test = 4; z"); + let w13 = scope.get("t = [1, 2, 3]; t.test = 4; !t"); + let w14 = scope.get("arguments[0]"); + let w15 = scope.get("encodeURI(\"\\\")"); + let w16 = scope.get("decodeURI(\"\\\")"); + let w17 = scope.get("decodeURIComponent(\"%\")"); + let w18 = scope.get("//"); + let w19 = scope.get("// 42"); + let w20 = scope.get("{}.foo"); + let w21 = scope.get("{}.foo()"); + let w22 = scope.get("({}).foo()"); + let w23 = scope.get("new Array(-1)"); + let w24 = scope.get("4.2.toExponential(-4.2)"); + let w25 = scope.get("throw new Error(\"bazinga\")"); + let w26 = scope.get("({ get error() { throw new Error(\"bazinga\") } }).error"); + let w27 = scope.get("throw { get name() { throw \"bazinga\" } }"); + + ok(w1, "The first watch expression should be present in the scope."); + ok(w2, "The second watch expression should be present in the scope."); + ok(w3, "The third watch expression should be present in the scope."); + ok(w4, "The fourth watch expression should be present in the scope."); + ok(w5, "The fifth watch expression should be present in the scope."); + ok(w6, "The sixth watch expression should be present in the scope."); + ok(w7, "The seventh watch expression should be present in the scope."); + ok(w8, "The eight watch expression should be present in the scope."); + ok(w9, "The ninth watch expression should be present in the scope."); + ok(w10, "The tenth watch expression should be present in the scope."); + ok(w11, "The eleventh watch expression should be present in the scope."); + ok(w12, "The twelfth watch expression should be present in the scope."); + ok(w13, "The 13th watch expression should be present in the scope."); + ok(w14, "The 14th watch expression should be present in the scope."); + ok(w15, "The 15th watch expression should be present in the scope."); + ok(w16, "The 16th watch expression should be present in the scope."); + ok(w17, "The 17th watch expression should be present in the scope."); + ok(w18, "The 18th watch expression should be present in the scope."); + ok(w19, "The 19th watch expression should be present in the scope."); + ok(w20, "The 20th watch expression should be present in the scope."); + ok(w21, "The 21st watch expression should be present in the scope."); + ok(w22, "The 22nd watch expression should be present in the scope."); + ok(w23, "The 23nd watch expression should be present in the scope."); + ok(w24, "The 24th watch expression should be present in the scope."); + ok(w25, "The 25th watch expression should be present in the scope."); + ok(w26, "The 26th watch expression should be present in the scope."); + ok(!w27, "The 27th watch expression should not be present in the scope."); + + is(w1.value, "a", "The first value is correct."); + is(w2.value, "a", "The second value is correct."); + is(w3.value, "a\"\"", "The third value is correct."); + is(w4.value, "a''", "The fourth value is correct."); + is(w5.value, "SyntaxError: expected expression, got '?'", "The fifth value is correct."); + + if (typeof expected_a == "object") { + is(w6.value.type, expected_a.type, "The sixth value type is correct."); + is(w6.value.class, expected_a.class, "The sixth value class is correct."); + } else { + is(w6.value, expected_a, "The sixth value is correct."); + } + + if (typeof expected_this == "object") { + is(w7.value.type, expected_this.type, "The seventh value type is correct."); + is(w7.value.class, expected_this.class, "The seventh value class is correct."); + } else { + is(w7.value, expected_this, "The seventh value is correct."); + } + + if (typeof expected_prop == "object") { + is(w8.value.type, expected_prop.type, "The eighth value type is correct."); + is(w8.value.class, expected_prop.class, "The eighth value class is correct."); + } else { + is(w8.value, expected_prop, "The eighth value is correct."); + } + + is(w9.value.type, "object", "The ninth value type is correct."); + is(w9.value.class, "Array", "The ninth value class is correct."); + is(w10.value.type, "object", "The tenth value type is correct."); + is(w10.value.class, "Array", "The tenth value class is correct."); + is(w11.value, "4", "The eleventh value is correct."); + is(w12.value.type, "object", "The eleventh value type is correct."); + is(w12.value.class, "Array", "The twelfth value class is correct."); + is(w13.value, false, "The 13th value is correct."); + + if (typeof expected_args == "object") { + is(w14.value.type, expected_args.type, "The 14th value type is correct."); + is(w14.value.class, expected_args.class, "The 14th value class is correct."); + } else { + is(w14.value, expected_args, "The 14th value is correct."); + } + + is(w15.value, "SyntaxError: unterminated string literal", "The 15th value is correct."); + is(w16.value, "SyntaxError: unterminated string literal", "The 16th value is correct."); + is(w17.value, "URIError: malformed URI sequence", "The 17th value is correct."); + + is(w18.value.type, "undefined", "The 18th value type is correct."); + is(w18.value.class, undefined, "The 18th value class is correct."); + + is(w19.value.type, "undefined", "The 19th value type is correct."); + is(w19.value.class, undefined, "The 19th value class is correct."); + + is(w20.value, "SyntaxError: expected expression, got '.'", "The 20th value is correct."); + is(w21.value, "SyntaxError: expected expression, got '.'", "The 21th value is correct."); + is(w22.value, "TypeError: (intermediate value).foo is not a function", "The 22th value is correct."); + is(w23.value, "RangeError: invalid array length", "The 23th value is correct."); + is(w24.value, "RangeError: precision -4 out of range", "The 24th value is correct."); + is(w25.value, "Error: bazinga", "The 25th value is correct."); + is(w26.value, "Error: bazinga", "The 26th value is correct."); + } +} diff --git a/toolkit/devtools/debugger/test/code_binary_search.coffee b/toolkit/devtools/debugger/test/code_binary_search.coffee new file mode 100644 index 000000000..e3dacdaaa --- /dev/null +++ b/toolkit/devtools/debugger/test/code_binary_search.coffee @@ -0,0 +1,18 @@ +# Uses a binary search algorithm to locate a value in the specified array. +window.binary_search = (items, value) -> + + start = 0 + stop = items.length - 1 + pivot = Math.floor (start + stop) / 2 + + while items[pivot] isnt value and start < stop + + # Adjust the search area. + stop = pivot - 1 if value < items[pivot] + start = pivot + 1 if value > items[pivot] + + # Recalculate the pivot. + pivot = Math.floor (stop + start) / 2 + + # Make sure we've found the correct value. + if items[pivot] is value then pivot else -1
\ No newline at end of file diff --git a/toolkit/devtools/debugger/test/code_binary_search.js b/toolkit/devtools/debugger/test/code_binary_search.js new file mode 100644 index 000000000..c43848a60 --- /dev/null +++ b/toolkit/devtools/debugger/test/code_binary_search.js @@ -0,0 +1,29 @@ +// Generated by CoffeeScript 1.6.1 +(function() { + + window.binary_search = function(items, value) { + var pivot, start, stop; + start = 0; + stop = items.length - 1; + pivot = Math.floor((start + stop) / 2); + while (items[pivot] !== value && start < stop) { + if (value < items[pivot]) { + stop = pivot - 1; + } + if (value > items[pivot]) { + start = pivot + 1; + } + pivot = Math.floor((stop + start) / 2); + } + if (items[pivot] === value) { + return pivot; + } else { + return -1; + } + }; + +}).call(this); + +/* +//# sourceMappingURL=code_binary_search.map +*/ diff --git a/toolkit/devtools/debugger/test/code_binary_search.map b/toolkit/devtools/debugger/test/code_binary_search.map new file mode 100644 index 000000000..8d2251125 --- /dev/null +++ b/toolkit/devtools/debugger/test/code_binary_search.map @@ -0,0 +1,10 @@ +{ + "version": 3, + "file": "code_binary_search.js", + "sourceRoot": "", + "sources": [ + "code_binary_search.coffee" + ], + "names": [], + "mappings": ";AACA;CAAA;CAAA,CAAA,CAAuB,EAAA,CAAjB,GAAkB,IAAxB;CAEE,OAAA,UAAA;CAAA,EAAQ,CAAR,CAAA;CAAA,EACQ,CAAR,CAAa,CAAL;CADR,EAEQ,CAAR,CAAA;CAEA,EAA0C,CAAR,CAAtB,MAAN;CAGJ,EAA6B,CAAR,CAAA,CAArB;CAAA,EAAQ,CAAR,CAAQ,GAAR;QAAA;CACA,EAA6B,CAAR,CAAA,CAArB;CAAA,EAAQ,EAAR,GAAA;QADA;CAAA,EAIQ,CAAI,CAAZ,CAAA;CAXF,IAIA;CAUA,GAAA,CAAS;CAAT,YAA8B;MAA9B;AAA0C,CAAD,YAAA;MAhBpB;CAAvB,EAAuB;CAAvB" +} diff --git a/toolkit/devtools/debugger/test/code_blackboxing_blackboxme.js b/toolkit/devtools/debugger/test/code_blackboxing_blackboxme.js new file mode 100644 index 000000000..713b3d50d --- /dev/null +++ b/toolkit/devtools/debugger/test/code_blackboxing_blackboxme.js @@ -0,0 +1,9 @@ +function blackboxme(fn) { + (function one() { + (function two() { + (function three() { + fn(); + }()); + }()); + }()); +} diff --git a/toolkit/devtools/debugger/test/code_blackboxing_one.js b/toolkit/devtools/debugger/test/code_blackboxing_one.js new file mode 100644 index 000000000..7f37b02ad --- /dev/null +++ b/toolkit/devtools/debugger/test/code_blackboxing_one.js @@ -0,0 +1,4 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function one() { two(); } diff --git a/toolkit/devtools/debugger/test/code_blackboxing_three.js b/toolkit/devtools/debugger/test/code_blackboxing_three.js new file mode 100644 index 000000000..55ed6c4da --- /dev/null +++ b/toolkit/devtools/debugger/test/code_blackboxing_three.js @@ -0,0 +1,4 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function three() { doDebuggerStatement(); } diff --git a/toolkit/devtools/debugger/test/code_blackboxing_two.js b/toolkit/devtools/debugger/test/code_blackboxing_two.js new file mode 100644 index 000000000..4790ea4a7 --- /dev/null +++ b/toolkit/devtools/debugger/test/code_blackboxing_two.js @@ -0,0 +1,4 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function two() { three(); } diff --git a/toolkit/devtools/debugger/test/code_breakpoints-break-on-last-line-of-script-on-reload.js b/toolkit/devtools/debugger/test/code_breakpoints-break-on-last-line-of-script-on-reload.js new file mode 100644 index 000000000..a8e8a7973 --- /dev/null +++ b/toolkit/devtools/debugger/test/code_breakpoints-break-on-last-line-of-script-on-reload.js @@ -0,0 +1,6 @@ +debugger; +var a = (function(){ + var b = 9; + console.log("x", b); + return b; +})(); diff --git a/toolkit/devtools/debugger/test/code_breakpoints-other-tabs.js b/toolkit/devtools/debugger/test/code_breakpoints-other-tabs.js new file mode 100644 index 000000000..2cf53ba2d --- /dev/null +++ b/toolkit/devtools/debugger/test/code_breakpoints-other-tabs.js @@ -0,0 +1,4 @@ +function testCase() { + var foo = "break on me"; + debugger; +} diff --git a/toolkit/devtools/debugger/test/code_frame-script.js b/toolkit/devtools/debugger/test/code_frame-script.js new file mode 100644 index 000000000..c42803b10 --- /dev/null +++ b/toolkit/devtools/debugger/test/code_frame-script.js @@ -0,0 +1,33 @@ +"use strict"; + +const { classes: Cc, interfaces: Ci, utils: Cu } = Components; +const { loadSubScript } = Cc['@mozilla.org/moz/jssubscript-loader;1']. + getService(Ci.mozIJSSubScriptLoader); + +const EventUtils = {}; +loadSubScript("chrome://marionette/content/EventUtils.js", EventUtils); + +dump("Frame script loaded.\n"); + +addMessageListener("test:call", function (message) { + dump("Calling function with name " + message.data.name + ".\n"); + + let data = message.data; + XPCNativeWrapper.unwrap(content)[data.name].apply(undefined, data.args); + sendAsyncMessage("test:call"); +}); + +addMessageListener("test:click", function (message) { + dump("Sending mouse click.\n"); + + let target = message.objects.target; + EventUtils.synthesizeMouseAtCenter(target, {}, + target.ownerDocument.defaultView); +}); + +addMessageListener("test:eval", function (message) { + dump("Evalling string " + message.data.string + ".\n"); + + content.eval(message.data.string); + sendAsyncMessage("test:eval"); +}); diff --git a/toolkit/devtools/debugger/test/code_function-search-01.js b/toolkit/devtools/debugger/test/code_function-search-01.js new file mode 100644 index 000000000..b5d647cfe --- /dev/null +++ b/toolkit/devtools/debugger/test/code_function-search-01.js @@ -0,0 +1,42 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function test() { + // Blah! First source! +} + +test.prototype = { + anonymousExpression: function() { + }, + namedExpression: function NAME() { + }, + sub: { + sub: { + sub: { + } + } + } +}; + +var foo = { + a_test: function() { + }, + n_test: function x() { + }, + sub: { + a_test: function() { + }, + n_test: function y() { + }, + sub: { + a_test: function() { + }, + n_test: function z() { + }, + sub: { + test_SAME_NAME: function test_SAME_NAME() { + } + } + } + } +}; diff --git a/toolkit/devtools/debugger/test/code_function-search-02.js b/toolkit/devtools/debugger/test/code_function-search-02.js new file mode 100644 index 000000000..10b48518f --- /dev/null +++ b/toolkit/devtools/debugger/test/code_function-search-02.js @@ -0,0 +1,21 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +var test2 = function() { + // Blah! Second source! +} + +var test3 = function test3_NAME() { +} + +var test4_SAME_NAME = function test4_SAME_NAME() { +} + +test.prototype.x = function X() { +}; +test.prototype.sub.y = function Y() { +}; +test.prototype.sub.sub.z = function Z() { +}; +test.prototype.sub.sub.sub.t = this.x = this.y = this.z = function() { +}; diff --git a/toolkit/devtools/debugger/test/code_function-search-03.js b/toolkit/devtools/debugger/test/code_function-search-03.js new file mode 100644 index 000000000..e64292a92 --- /dev/null +++ b/toolkit/devtools/debugger/test/code_function-search-03.js @@ -0,0 +1,32 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +window.addEventListener("bogus", function namedEventListener() { + // Blah! Third source! +}); + +try { + var bar = foo.sub.sub.test({ + a: function A() { + } + }); + + bar.alpha = foo.sub.sub.test({ + b: function B() { + } + }); + + bar.alpha.beta = new X(Y(Z(foo.sub.sub.test({ + c: function C() { + } + })))); + + this.theta = new X(new Y(new Z(new foo.sub.sub.test({ + d: function D() { + } + })))); + + var fun = foo = bar = this.t_foo = window.w_bar = function baz() {}; + +} catch (e) { +} diff --git a/toolkit/devtools/debugger/test/code_location-changes.js b/toolkit/devtools/debugger/test/code_location-changes.js new file mode 100644 index 000000000..d164b8bdf --- /dev/null +++ b/toolkit/devtools/debugger/test/code_location-changes.js @@ -0,0 +1,7 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function myFunction() { + var a = 1; + debugger; +} diff --git a/toolkit/devtools/debugger/test/code_math.js b/toolkit/devtools/debugger/test/code_math.js new file mode 100644 index 000000000..f765817bb --- /dev/null +++ b/toolkit/devtools/debugger/test/code_math.js @@ -0,0 +1,45 @@ +function add(a, b, k) { + var result = a + b; + return k(result); +} + +function sub(a, b, k) { + var result = a - b; + return k(result); +} + +function mul(a, b, k) { + var result = a * b; + return k(result); +} + +function div(a, b, k) { + var result = a / b; + return k(result); +} + +function arithmetic() { + add(4, 4, function (a) { + // 8 + sub(a, 2, function (b) { + // 6 + mul(b, 3, function (c) { + // 18 + div(c, 2, function (d) { + // 9 + console.log(d); + }); + }); + }); + }); +} + +// Compile with closure compiler and the following flags: +// +// --compilation_level WHITESPACE_ONLY +// --source_map_format V3 +// --create_source_map code_math.map +// --js_output_file code_math.min.js +// +// And then append the sourceMappingURL comment directive to code_math.min.js +// manually. diff --git a/toolkit/devtools/debugger/test/code_math.map b/toolkit/devtools/debugger/test/code_math.map new file mode 100644 index 000000000..474304c39 --- /dev/null +++ b/toolkit/devtools/debugger/test/code_math.map @@ -0,0 +1,8 @@ +{ +"version":3, +"file":"code_math.min.js", +"lineCount":1, +"mappings":"AAAAA,QAASA,IAAG,CAACC,CAAD,CAAIC,CAAJ,CAAOC,CAAP,CAAU,CACpB,IAAIC,OAASH,CAATG,CAAaF,CACjB,OAAOC,EAAA,CAAEC,MAAF,CAFa,CAKtBC,QAASA,IAAG,CAACJ,CAAD,CAAIC,CAAJ,CAAOC,CAAP,CAAU,CACpB,IAAIC,OAASH,CAATG,CAAaF,CACjB,OAAOC,EAAA,CAAEC,MAAF,CAFa,CAKtBE,QAASA,IAAG,CAACL,CAAD,CAAIC,CAAJ,CAAOC,CAAP,CAAU,CACpB,IAAIC,OAASH,CAATG,CAAaF,CACjB,OAAOC,EAAA,CAAEC,MAAF,CAFa,CAKtBG,QAASA,IAAG,CAACN,CAAD,CAAIC,CAAJ,CAAOC,CAAP,CAAU,CACpB,IAAIC,OAASH,CAATG,CAAaF,CACjB,OAAOC,EAAA,CAAEC,MAAF,CAFa,CAKtBI,QAASA,WAAU,EAAG,CACpBR,GAAA,CAAI,CAAJ,CAAO,CAAP,CAAU,QAAS,CAACC,CAAD,CAAI,CAErBI,GAAA,CAAIJ,CAAJ,CAAO,CAAP,CAAU,QAAS,CAACC,CAAD,CAAI,CAErBI,GAAA,CAAIJ,CAAJ,CAAO,CAAP,CAAU,QAAS,CAACO,CAAD,CAAI,CAErBF,GAAA,CAAIE,CAAJ,CAAO,CAAP,CAAU,QAAS,CAACC,CAAD,CAAI,CAErBC,OAAAC,IAAA,CAAYF,CAAZ,CAFqB,CAAvB,CAFqB,CAAvB,CAFqB,CAAvB,CAFqB,CAAvB,CADoB;", +"sources":["code_math.js"], +"names":["add","a","b","k","result","sub","mul","div","arithmetic","c","d","console","log"] +} diff --git a/toolkit/devtools/debugger/test/code_math.min.js b/toolkit/devtools/debugger/test/code_math.min.js new file mode 100644 index 000000000..7d1fb48f0 --- /dev/null +++ b/toolkit/devtools/debugger/test/code_math.min.js @@ -0,0 +1,2 @@ +function add(a,b,k){var result=a+b;return k(result)}function sub(a,b,k){var result=a-b;return k(result)}function mul(a,b,k){var result=a*b;return k(result)}function div(a,b,k){var result=a/b;return k(result)}function arithmetic(){add(4,4,function(a){sub(a,2,function(b){mul(b,3,function(c){div(c,2,function(d){console.log(d)})})})})}; +//@ sourceMappingURL=code_math.map diff --git a/toolkit/devtools/debugger/test/code_math_bogus_map.js b/toolkit/devtools/debugger/test/code_math_bogus_map.js new file mode 100644 index 000000000..82e156b10 --- /dev/null +++ b/toolkit/devtools/debugger/test/code_math_bogus_map.js @@ -0,0 +1,4 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +function stopMe(){throw Error("boom");}try{stopMe();var a=1;a=a*2;}catch(e){}; +//# sourceMappingURL=bogus.map diff --git a/toolkit/devtools/debugger/test/code_same-line-functions.js b/toolkit/devtools/debugger/test/code_same-line-functions.js new file mode 100644 index 000000000..58643f59d --- /dev/null +++ b/toolkit/devtools/debugger/test/code_same-line-functions.js @@ -0,0 +1 @@ +function first() { var a = "first"; second(); function second() { var a = "second"; } }
\ No newline at end of file diff --git a/toolkit/devtools/debugger/test/code_script-eval.js b/toolkit/devtools/debugger/test/code_script-eval.js new file mode 100644 index 000000000..c7485ac7b --- /dev/null +++ b/toolkit/devtools/debugger/test/code_script-eval.js @@ -0,0 +1,10 @@ + +var bar; + +function evalSource() { + eval('bar = function() {\nvar x = 5;\n}'); +} + +function evalSourceWithSourceURL() { + eval('bar = function() {\nvar x = 6;\n} //# sourceURL=bar.js'); +} diff --git a/toolkit/devtools/debugger/test/code_script-switching-01.js b/toolkit/devtools/debugger/test/code_script-switching-01.js new file mode 100644 index 000000000..4ba2772de --- /dev/null +++ b/toolkit/devtools/debugger/test/code_script-switching-01.js @@ -0,0 +1,6 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function firstCall() { + secondCall(); +} diff --git a/toolkit/devtools/debugger/test/code_script-switching-02.js b/toolkit/devtools/debugger/test/code_script-switching-02.js new file mode 100644 index 000000000..feb74315f --- /dev/null +++ b/toolkit/devtools/debugger/test/code_script-switching-02.js @@ -0,0 +1,13 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function secondCall() { + // This comment is useful: ☺ + debugger; + function foo() {} + if (x) { + foo(); + } +} + +var x = true; diff --git a/toolkit/devtools/debugger/test/code_test-editor-mode b/toolkit/devtools/debugger/test/code_test-editor-mode new file mode 100644 index 000000000..ca8a90889 --- /dev/null +++ b/toolkit/devtools/debugger/test/code_test-editor-mode @@ -0,0 +1,6 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function secondCall() { + debugger; +} diff --git a/toolkit/devtools/debugger/test/code_tracing-01.js b/toolkit/devtools/debugger/test/code_tracing-01.js new file mode 100644 index 000000000..81fc9a7c6 --- /dev/null +++ b/toolkit/devtools/debugger/test/code_tracing-01.js @@ -0,0 +1,29 @@ +function factorial(n) { + if (n <= 1) { + return 1; + } else { + return n * factorial(n - 1); + } +} + +function* yielder(n) { + while (n-- >= 0) { + yield { value: n, squared: n * n }; + } +} + +function thrower() { + throw new Error("Curse your sudden but inevitable betrayal!"); +} + +function main() { + factorial(5); + + // XXX bug 923729: Can't test yielding yet. + // for (let x of yielder(5)) {} + + try { + thrower(); + } catch (e) { + } +} diff --git a/toolkit/devtools/debugger/test/code_ugly-2.js b/toolkit/devtools/debugger/test/code_ugly-2.js new file mode 100644 index 000000000..15fba0701 --- /dev/null +++ b/toolkit/devtools/debugger/test/code_ugly-2.js @@ -0,0 +1 @@ +function main2() { var a = 1 + 3; var b = a++; return b + a; } diff --git a/toolkit/devtools/debugger/test/code_ugly-3.js b/toolkit/devtools/debugger/test/code_ugly-3.js new file mode 100644 index 000000000..0424b288c --- /dev/null +++ b/toolkit/devtools/debugger/test/code_ugly-3.js @@ -0,0 +1 @@ +function main3() { var a = 1; debugger; noop(a); return 10; }; diff --git a/toolkit/devtools/debugger/test/code_ugly-4.js b/toolkit/devtools/debugger/test/code_ugly-4.js new file mode 100644 index 000000000..90c2eca64 --- /dev/null +++ b/toolkit/devtools/debugger/test/code_ugly-4.js @@ -0,0 +1,24 @@ +function a(){b()}function b(){debugger} +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYWJjLmpzIiwic291cmNlcyI6WyJkYXRhOnRleHQvamF2YXNjcmlwdCxmdW5jdGlvbiBhKCl7YigpfSIsImRhdGE6dGV4dC9qYXZhc2NyaXB0LGZ1bmN0aW9uIGIoKXtkZWJ1Z2dlcn0iXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsaUJDQUEsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMifQ== + +// Generate this file by evaluating the following in a browser-environment +// scratchpad: +// +// Components.utils.import('resource://gre/modules/devtools/SourceMap.jsm'); +// +// let dataUrl = s => "data:text/javascript," + s; +// +// let A = "function a(){b()}"; +// let A_URL = dataUrl(A); +// let B = "function b(){debugger}"; +// let B_URL = dataUrl(B); +// +// let result = (new SourceNode(null, null, null, [ +// new SourceNode(1, 0, A_URL, A), +// B.split("").map((ch, i) => new SourceNode(1, i, B_URL, ch)) +// ])).toStringWithSourceMap({ +// file: "abc.js" +// }); +// +// result.code + "\n//# " + "sourceMappingURL=data:application/json;base64," + btoa(JSON.stringify(result.map)); + diff --git a/toolkit/devtools/debugger/test/code_ugly-5.js b/toolkit/devtools/debugger/test/code_ugly-5.js new file mode 100644 index 000000000..248eb3bd5 --- /dev/null +++ b/toolkit/devtools/debugger/test/code_ugly-5.js @@ -0,0 +1,14 @@ +/*1385419625,181944095,JIT Construction: v1021776,en_US*/ +/** + * Copyright Test Inc. + * + * Licensed under the Apache License, Version 2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + */ +// Copyright Test Inc. +// +// etc... +// etc... +function foo(){var a=1;var b=2;bar(a,b);} +function bar(c,d){debugger;} +foo();
\ No newline at end of file diff --git a/toolkit/devtools/debugger/test/code_ugly-6.js b/toolkit/devtools/debugger/test/code_ugly-6.js new file mode 100644 index 000000000..0c678c140 --- /dev/null +++ b/toolkit/devtools/debugger/test/code_ugly-6.js @@ -0,0 +1,5 @@ +// Copyright Test Inc. +// +// etc... +// etc... +function main(){ return 0; }
\ No newline at end of file diff --git a/toolkit/devtools/debugger/test/code_ugly-7.js b/toolkit/devtools/debugger/test/code_ugly-7.js new file mode 100644 index 000000000..8ce53b305 --- /dev/null +++ b/toolkit/devtools/debugger/test/code_ugly-7.js @@ -0,0 +1,5 @@ +// Copyright Test Inc. +// +// etc... +// etc... +function foo(){}; foo();
\ No newline at end of file diff --git a/toolkit/devtools/debugger/test/code_ugly-8 b/toolkit/devtools/debugger/test/code_ugly-8 new file mode 100644 index 000000000..dc0d18500 --- /dev/null +++ b/toolkit/devtools/debugger/test/code_ugly-8 @@ -0,0 +1,3 @@ +function foo() { var a=1; var b=2; bar(a, b); } +function bar(c, d) { debugger; } +foo(); diff --git a/toolkit/devtools/debugger/test/code_ugly-8^headers^ b/toolkit/devtools/debugger/test/code_ugly-8^headers^ new file mode 100644 index 000000000..a17a9a3a1 --- /dev/null +++ b/toolkit/devtools/debugger/test/code_ugly-8^headers^ @@ -0,0 +1 @@ +Content-Type: application/javascript diff --git a/toolkit/devtools/debugger/test/code_ugly.js b/toolkit/devtools/debugger/test/code_ugly.js new file mode 100644 index 000000000..dc0d18500 --- /dev/null +++ b/toolkit/devtools/debugger/test/code_ugly.js @@ -0,0 +1,3 @@ +function foo() { var a=1; var b=2; bar(a, b); } +function bar(c, d) { debugger; } +foo(); diff --git a/toolkit/devtools/debugger/test/doc_auto-pretty-print-01.html b/toolkit/devtools/debugger/test/doc_auto-pretty-print-01.html new file mode 100644 index 000000000..dee2d52f2 --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_auto-pretty-print-01.html @@ -0,0 +1,14 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8" /> + <title>Auto Pretty Printing Test Page</title> + </head> + <body> + <script src="code_ugly-5.js"></script> + <script src="code_ugly-6.js"></script> + </body> +</html>
\ No newline at end of file diff --git a/toolkit/devtools/debugger/test/doc_auto-pretty-print-02.html b/toolkit/devtools/debugger/test/doc_auto-pretty-print-02.html new file mode 100644 index 000000000..e96a63d9e --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_auto-pretty-print-02.html @@ -0,0 +1,14 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8" /> + <title>Auto Pretty Printing Test Page</title> + </head> + <body> + <script src="code_ugly-6.js"></script> + <script src="code_ugly-7.js"></script> + </body> +</html>
\ No newline at end of file diff --git a/toolkit/devtools/debugger/test/doc_binary_search.html b/toolkit/devtools/debugger/test/doc_binary_search.html new file mode 100644 index 000000000..803106fc5 --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_binary_search.html @@ -0,0 +1,15 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Debugger test page</title> + </head> + + <body> + <script type="text/javascript" src="code_binary_search.js"></script> + </body> + +</html> diff --git a/toolkit/devtools/debugger/test/doc_blackboxing.html b/toolkit/devtools/debugger/test/doc_blackboxing.html new file mode 100644 index 000000000..a83b16de5 --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_blackboxing.html @@ -0,0 +1,26 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Debugger test page</title> + </head> + + <body> + <script type="text/javascript" src="code_blackboxing_blackboxme.js"></script> + <script type="text/javascript" src="code_blackboxing_one.js"></script> + <script type="text/javascript" src="code_blackboxing_two.js"></script> + <script type="text/javascript" src="code_blackboxing_three.js"></script> + <script> + function runTest() { + blackboxme(doDebuggerStatement); + } + function doDebuggerStatement() { + debugger; + } + </script> + </body> + +</html> diff --git a/toolkit/devtools/debugger/test/doc_breakpoint-move.html b/toolkit/devtools/debugger/test/doc_breakpoint-move.html new file mode 100644 index 000000000..5124bbbcf --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_breakpoint-move.html @@ -0,0 +1,25 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Debugger test page</title> + </head> + + <body> + <button onclick="ermahgerd()">Click me!</button> + + <script type="text/javascript"> + function ermahgerd() { + debugger; + // This is just a line + // and here we are + var x = 5; + return x; + } + </script> + </body> + +</html> diff --git a/toolkit/devtools/debugger/test/doc_breakpoints-break-on-last-line-of-script-on-reload.html b/toolkit/devtools/debugger/test/doc_breakpoints-break-on-last-line-of-script-on-reload.html new file mode 100644 index 000000000..c1730e506 --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_breakpoints-break-on-last-line-of-script-on-reload.html @@ -0,0 +1,8 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE html> +<head> + <meta charset="utf-8"/> + <title>Debugger Break on Last Line of Script on Reload Test Page</title> +</head> +<script src="code_breakpoints-break-on-last-line-of-script-on-reload.js"></script> diff --git a/toolkit/devtools/debugger/test/doc_breakpoints-other-tabs.html b/toolkit/devtools/debugger/test/doc_breakpoints-other-tabs.html new file mode 100644 index 000000000..4273dbdd8 --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_breakpoints-other-tabs.html @@ -0,0 +1,8 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE html> +<head> + <meta charset="utf-8"/> + <title>Debugger Breakpoints Other Tabs Test Page</title> +</head> +<script src="code_breakpoints-other-tabs.js"></script> diff --git a/toolkit/devtools/debugger/test/doc_breakpoints-reload.html b/toolkit/devtools/debugger/test/doc_breakpoints-reload.html new file mode 100644 index 000000000..0c6059c6d --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_breakpoints-reload.html @@ -0,0 +1,13 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE html> +<head> + <meta charset="utf-8"/> + <title>Debugger Breakpoints Other Tabs Test Page</title> +</head> +<script> + function theTest() { + window.foo = "break on me"; + } + theTest(); +</script> diff --git a/toolkit/devtools/debugger/test/doc_closure-optimized-out.html b/toolkit/devtools/debugger/test/doc_closure-optimized-out.html new file mode 100644 index 000000000..3ad4e8fc0 --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_closure-optimized-out.html @@ -0,0 +1,34 @@ +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'/> + <title>Debugger Test for Inspecting Optimized-Out Variables</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + <script type="text/javascript"> + window.addEventListener("load", function onload() { + window.removeEventListener("load", onload); + function clickHandler(event) { + button.removeEventListener("click", clickHandler, false); + function outer(arg) { + var upvar = arg * 2; + // The inner lambda only aliases arg, so the frontend alias analysis decides + // that upvar is not aliased and is not in the CallObject. + return function () { + arg += 2; + }; + } + + var f = outer(42); + f(); + } + var button = document.querySelector("button"); + button.addEventListener("click", clickHandler, false); + }); + </script> + + </head> + <body> + <button>Click me!</button> + </body> +</html> diff --git a/toolkit/devtools/debugger/test/doc_closures.html b/toolkit/devtools/debugger/test/doc_closures.html new file mode 100644 index 000000000..1ba91601a --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_closures.html @@ -0,0 +1,32 @@ +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'/> + <title>Debugger Test for Closure Inspection</title> + <!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + <script type="text/javascript"> + window.addEventListener("load", function onload() { + window.removeEventListener("load", onload); + function clickHandler(event) { + button.removeEventListener("click", clickHandler, false); + var PersonFactory = function _pfactory(name) { + var foo = 10; + return { + getName: function() { return name; }, + getFoo: function() { foo = Date.now(); return foo; } + }; + }; + var person = new PersonFactory("Bob"); + debugger; + } + var button = document.querySelector("button"); + button.addEventListener("click", clickHandler, false); + }); + </script> + + </head> + <body> + <button>Click me!</button> + </body> +</html> diff --git a/toolkit/devtools/debugger/test/doc_cmd-break.html b/toolkit/devtools/debugger/test/doc_cmd-break.html new file mode 100644 index 000000000..4f434746e --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_cmd-break.html @@ -0,0 +1,22 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Debugger test page</title> + </head> + + <body> + <script type="text/javascript"> + function firstCall() { + window.gLineNumber = Error().lineNumber; secondCall(); + } + function secondCall() { + debugger; + } + </script> + </body> + +</html> diff --git a/toolkit/devtools/debugger/test/doc_cmd-dbg.html b/toolkit/devtools/debugger/test/doc_cmd-dbg.html new file mode 100644 index 000000000..5ab41eb1b --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_cmd-dbg.html @@ -0,0 +1,40 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Debugger test page</title> + </head> + + <body> + <input type="text" value=""/> + <input type="button" value="Click me!" onclick="test()"/> + + <script type="application/javascript;version=1.7"> + let output = document.querySelector("input"); + output.value = ""; + + function test() { + debugger; + stepIntoMe(); // step in + + output.value = "dbg continue"; + debugger; + } + + function stepIntoMe() { + output.value = "step in"; // step in + stepOverMe(); // step over + let x = 0; // step out + output.value = "step out"; + } + + function stepOverMe() { + output.value = "step over"; + } + </script> + </body> + +</html> diff --git a/toolkit/devtools/debugger/test/doc_conditional-breakpoints.html b/toolkit/devtools/debugger/test/doc_conditional-breakpoints.html new file mode 100644 index 000000000..7adce7a18 --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_conditional-breakpoints.html @@ -0,0 +1,35 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Debugger test page</title> + </head> + + <body> + <button onclick="ermahgerd()">Click me!</button> + + <script type="text/javascript"> + function ermahgerd() { + var a = {}; + debugger; + a = "undefined"; + a = "null"; + a = "42"; + a = "true"; + a = "'nasu'"; + a = "/regexp/"; + a = "{}"; + a = "function() {}"; + a = "(function { return false; })()"; + a = "a"; + a = "a !== undefined"; + a = "a !== null"; + a = "b"; + } + </script> + </body> + +</html> diff --git a/toolkit/devtools/debugger/test/doc_domnode-variables.html b/toolkit/devtools/debugger/test/doc_domnode-variables.html new file mode 100644 index 000000000..9e7531036 --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_domnode-variables.html @@ -0,0 +1,24 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Debugger test page</title> + </head> + + <body> + <div>Look at this DIV! Just look at it!</div> + + <script type="text/javascript"> + function start() { + var theDiv = document.querySelector("div"); + var theBody = document.body; + var manyDomNodes = [theDiv, theBody]; + debugger; + } + </script> + </body> + +</html> diff --git a/toolkit/devtools/debugger/test/doc_editor-mode.html b/toolkit/devtools/debugger/test/doc_editor-mode.html new file mode 100644 index 000000000..8e3573cea --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_editor-mode.html @@ -0,0 +1,20 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Debugger test page</title> + </head> + + <body> + <script type="text/javascript" src="code_script-switching-01.js?a=b"></script> + <script type="text/javascript" src="code_test-editor-mode?c=d"></script> + <script type="text/javascript"> + function banana() { + } + </script> + </body> + +</html> diff --git a/toolkit/devtools/debugger/test/doc_empty-tab-01.html b/toolkit/devtools/debugger/test/doc_empty-tab-01.html new file mode 100644 index 000000000..28398f776 --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_empty-tab-01.html @@ -0,0 +1,14 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Empty test page 1</title> + </head> + + <body> + </body> + +</html> diff --git a/toolkit/devtools/debugger/test/doc_empty-tab-02.html b/toolkit/devtools/debugger/test/doc_empty-tab-02.html new file mode 100644 index 000000000..5db150844 --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_empty-tab-02.html @@ -0,0 +1,14 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Empty test page 2</title> + </head> + + <body> + </body> + +</html> diff --git a/toolkit/devtools/debugger/test/doc_event-listeners-01.html b/toolkit/devtools/debugger/test/doc_event-listeners-01.html new file mode 100644 index 000000000..b44400311 --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_event-listeners-01.html @@ -0,0 +1,43 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> +<html> + <head> + <meta charset="utf-8"/> + <title>Debugger test page</title> + </head> + + <body> + <button>Click me!</button> + <input type="text" onchange="changeHandler()"> + + <script type="text/javascript"> + window.addEventListener("load", function onload() { + window.removeEventListener("load", onload); + function initialSetup(event) { + debugger; + var button = document.querySelector("button"); + button.onclick = clickHandler; + } + function clickHandler(event) { + window.foobar = "clickHandler"; + } + function changeHandler(event) { + window.foobar = "changeHandler"; + } + function keyupHandler(event) { + window.foobar = "keyupHandler"; + } + + var button = document.querySelector("button"); + button.onclick = initialSetup; + + var input = document.querySelector("input"); + input.addEventListener("keyup", keyupHandler, true); + + window.changeHandler = changeHandler; + }); + </script> + </body> + +</html> diff --git a/toolkit/devtools/debugger/test/doc_event-listeners-02.html b/toolkit/devtools/debugger/test/doc_event-listeners-02.html new file mode 100644 index 000000000..6a4649de9 --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_event-listeners-02.html @@ -0,0 +1,53 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> +<html> + <head> + <meta charset="utf-8"/> + <title>Debugger test page</title> + </head> + + <body> + <button>Click me!</button> + <input type="text" onchange="changeHandler()"> + + <script type="text/javascript"> + window.addEventListener("load", function onload() { + window.removeEventListener("load", onload); + function initialSetup(event) { + debugger; + var button = document.querySelector("button"); + button.onclick = clickHandler; + } + function clickHandler(event) { + window.foobar = "clickHandler"; + } + function changeHandler(event) { + window.foobar = "changeHandler"; + } + function keyupHandler(event) { + window.foobar = "keyupHandler"; + } + function keydownHandler(event) { + window.foobar = "keydownHandler"; + } + + var button = document.querySelector("button"); + button.onclick = initialSetup; + + var input = document.querySelector("input"); + input.addEventListener("keyup", keyupHandler, true); + + window.addEventListener("keydown", keydownHandler, true); + document.body.addEventListener("keydown", keydownHandler, true); + + window.changeHandler = changeHandler; + }); + + function addBodyClickEventListener() { + document.body.addEventListener("click", function() { debugger; }); + } + </script> + </body> + +</html> diff --git a/toolkit/devtools/debugger/test/doc_event-listeners-03.html b/toolkit/devtools/debugger/test/doc_event-listeners-03.html new file mode 100644 index 000000000..b672a4360 --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_event-listeners-03.html @@ -0,0 +1,63 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> +<html> + <head> + <meta charset="utf-8"/> + <title>Bound event listeners test page</title> + </head> + + <body> + <button id="initialSetup">initialSetup</button> + <button id="clicker">clicker</button> + <button id="handleEventClick">handleEventClick</button> + <button id="boundHandleEventClick">boundHandleEventClick</button> + + <script type="text/javascript"> + window.addEventListener("load", function onload() { + window.removeEventListener("load", onload); + function initialSetup(event) { + var button = document.getElementById("initialSetup"); + button.removeEventListener("click", initialSetup); + debugger; + } + + function clicker(event) { + window.foobar = "clicker"; + } + + function handleEventClick() { + var button = document.getElementById("handleEventClick"); + // Create a long prototype chain to test for weird edge cases. + button.addEventListener("click", Object.create(Object.create(this))); + } + + handleEventClick.prototype.handleEvent = function() { + window.foobar = "handleEventClick"; + }; + + function boundHandleEventClick() { + var button = document.getElementById("boundHandleEventClick"); + this.handleEvent = this.handleEvent.bind(this); + button.addEventListener("click", this); + } + + boundHandleEventClick.prototype.handleEvent = function() { + window.foobar = "boundHandleEventClick"; + }; + + var button = document.getElementById("clicker"); + // Bind more than once to test for weird edge cases. + var boundClicker = clicker.bind(this).bind(this).bind(this); + button.addEventListener("click", boundClicker); + + new handleEventClick(); + new boundHandleEventClick(); + + var initButton = document.getElementById("initialSetup"); + initButton.addEventListener("click", initialSetup); + }); + </script> + </body> + +</html> diff --git a/toolkit/devtools/debugger/test/doc_frame-parameters.html b/toolkit/devtools/debugger/test/doc_frame-parameters.html new file mode 100644 index 000000000..b3108d6bf --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_frame-parameters.html @@ -0,0 +1,37 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Debugger test page</title> + </head> + + <body> + <button onclick="start()">Click me!</button> + + <script type="text/javascript"> + function test(aArg, bArg, cArg, dArg, eArg, fArg) { + var a = 1; + var b = { a: a }; + var c = { a: 1, b: "beta", c: 3, d: false, e: null, f: fArg }; + var myVar = { + _prop: 42, + get prop() { return this._prop; }, + set prop(val) { this._prop = val; } + }; + debugger; + } + + function start() { + var a = { a: 1, b: "beta", c: 3, d: false, e: null, f: undefined }; + var e = eval("test(a, 'beta', 3, false, null);"); + } + + var button = document.querySelector("button"); + var buttonAsProto = Object.create(button); + </script> + </body> + +</html> diff --git a/toolkit/devtools/debugger/test/doc_function-display-name.html b/toolkit/devtools/debugger/test/doc_function-display-name.html new file mode 100644 index 000000000..84e8ce6e1 --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_function-display-name.html @@ -0,0 +1,31 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Debugger test page</title> + </head> + + <body> + <script type="text/javascript"> + var a = function() { + return function() { + debugger; + } + } + + var anon = a(); + anon.displayName = "anonFunc"; + + var inferred = a(); + + function evalCall() { + eval("anon();"); + eval("inferred();"); + } + </script> + </body> + +</html> diff --git a/toolkit/devtools/debugger/test/doc_function-search.html b/toolkit/devtools/debugger/test/doc_function-search.html new file mode 100644 index 000000000..711a873ed --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_function-search.html @@ -0,0 +1,30 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Debugger test page</title> + </head> + + <body> + <p>Peanut butter jelly time!</p> + + <script type="text/javascript" src="code_function-search-01.js"></script> + <script type="text/javascript" src="code_function-search-02.js"></script> + <script type="text/javascript" src="code_function-search-03.js"></script> + + <script type="text/javascript;version=1.8"> + function inline() {} + let arrow = () => {} + + let foo = bar => {} + let foo2 = bar2 = baz2 => 42; + + setTimeout((foo, bar, baz) => {}); + setTimeout((foo, bar, baz) => 42); + </script> + </body> + +</html> diff --git a/toolkit/devtools/debugger/test/doc_global-method-override.html b/toolkit/devtools/debugger/test/doc_global-method-override.html new file mode 100644 index 000000000..d8cf750fc --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_global-method-override.html @@ -0,0 +1,16 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"> + <title>Debugger global method override test page</title> + </head> + <body> + <script type="text/javascript"> + console.log( "Error: " + toString( { x: 0, y: 0 } ) ); + function toString(v) { return "[ " + v.x + ", " + v.y + " ]"; } + </script> + </body> +</html> diff --git a/toolkit/devtools/debugger/test/doc_iframes.html b/toolkit/devtools/debugger/test/doc_iframes.html new file mode 100644 index 000000000..e5a76c280 --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_iframes.html @@ -0,0 +1,15 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Debugger test page</title> + </head> + + <body> + <iframe src="doc_inline-debugger-statement.html"></iframe> + </body> + +</html> diff --git a/toolkit/devtools/debugger/test/doc_included-script.html b/toolkit/devtools/debugger/test/doc_included-script.html new file mode 100644 index 000000000..8b134dd42 --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_included-script.html @@ -0,0 +1,22 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Debugger test page</title> + </head> + + <body> + <button onclick="myFunction()">Click me!</button> + + <script type="text/javascript" src="code_location-changes.js"></script> + <script type="text/javascript"> + function runDebuggerStatement() { + debugger; + } + </script> + </body> + +</html> diff --git a/toolkit/devtools/debugger/test/doc_inline-debugger-statement.html b/toolkit/devtools/debugger/test/doc_inline-debugger-statement.html new file mode 100644 index 000000000..406e9d9da --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_inline-debugger-statement.html @@ -0,0 +1,21 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Debugger test page</title> + </head> + + <body> + <button>Click me!</button> + + <script type="text/javascript"> + function runDebuggerStatement() { + debugger; + } + </script> + </body> + +</html> diff --git a/toolkit/devtools/debugger/test/doc_inline-script.html b/toolkit/devtools/debugger/test/doc_inline-script.html new file mode 100644 index 000000000..d071cc084 --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_inline-script.html @@ -0,0 +1,25 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Debugger test page</title> + </head> + + <body> + <button onclick="myFunction()">Click me!</button> + + <script type="text/javascript"> + function runDebuggerStatement() { + debugger; + } + function myFunction() { + var a = 1; + debugger; + } + </script> + </body> + +</html> diff --git a/toolkit/devtools/debugger/test/doc_large-array-buffer.html b/toolkit/devtools/debugger/test/doc_large-array-buffer.html new file mode 100644 index 000000000..b8545e57c --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_large-array-buffer.html @@ -0,0 +1,27 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Debugger test page</title> + </head> + + <body> + <button onclick="test(10000)">Click me!</button> + + <script type="text/javascript"> + function test(aNumber) { + var buffer = new ArrayBuffer(aNumber); + var largeArray = new Int8Array(buffer); + var largeObject = {}; + + for (var i = 0; i < aNumber; i++) { + largeObject[i] = aNumber - i - 1; + } + debugger; + } + </script> + </body> +</html> diff --git a/toolkit/devtools/debugger/test/doc_minified.html b/toolkit/devtools/debugger/test/doc_minified.html new file mode 100644 index 000000000..b229e079f --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_minified.html @@ -0,0 +1,14 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Debugger test page</title> + </head> + + <body> + <script src="code_math.min.js"></script> + </body> +</html> diff --git a/toolkit/devtools/debugger/test/doc_minified_bogus_map.html b/toolkit/devtools/debugger/test/doc_minified_bogus_map.html new file mode 100644 index 000000000..d6670a7e1 --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_minified_bogus_map.html @@ -0,0 +1,14 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Debugger test page</title> + </head> + + <body> + <script src="code_math_bogus_map.js"></script> + </body> +</html> diff --git a/toolkit/devtools/debugger/test/doc_native-event-handler.html b/toolkit/devtools/debugger/test/doc_native-event-handler.html new file mode 100644 index 000000000..cd2a656bf --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_native-event-handler.html @@ -0,0 +1,22 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>A video element with native event handlers</title> + <script type="text/javascript"> + function initialSetup(event) { + debugger; + } + + window.addEventListener("load", function() {}, false); + </script> + </head> + <body> + <button onclick="initialSetup()">Click me!</button> + <!-- the "controls" attribute ensures that there are extra event handlers in + the element. --> + <video controls></video> + </body> +</html> diff --git a/toolkit/devtools/debugger/test/doc_no-page-sources.html b/toolkit/devtools/debugger/test/doc_no-page-sources.html new file mode 100644 index 000000000..5131578ad --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_no-page-sources.html @@ -0,0 +1,11 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> +<html> + <head> + <meta charset="utf-8"/> + <title>This page has no sources</title> + </head> + <body> + </body> +</html>
\ No newline at end of file diff --git a/toolkit/devtools/debugger/test/doc_pause-exceptions.html b/toolkit/devtools/debugger/test/doc_pause-exceptions.html new file mode 100644 index 000000000..7766fb49d --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_pause-exceptions.html @@ -0,0 +1,35 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Debugger test page</title> + </head> + + <body> + <button>Click me!</button> + <ul></ul> + + <script type="text/javascript"> + window.addEventListener("load", function() { + function test() { + try { + throw new Error("boom"); + } catch (e) { + var list = document.querySelector("ul"); + var item = document.createElement("li"); + item.innerHTML = e.message; + list.appendChild(item); + } finally { + debugger; + } + } + var button = document.querySelector("button"); + button.addEventListener("click", test, false); + }); + </script> + </body> + +</html> diff --git a/toolkit/devtools/debugger/test/doc_pretty-print-2.html b/toolkit/devtools/debugger/test/doc_pretty-print-2.html new file mode 100644 index 000000000..509f57d6b --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_pretty-print-2.html @@ -0,0 +1,15 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE html> +<head> + <meta charset="utf-8"/> + <title>Debugger Pretty Printing Test Page</title> +</head> +<script src="code_ugly-2.js"></script> +<script src="code_ugly-3.js"></script> +<script src="code_ugly-4.js"></script> +<script> + function noop(x) { + return x; + } +</script> diff --git a/toolkit/devtools/debugger/test/doc_pretty-print-3.html b/toolkit/devtools/debugger/test/doc_pretty-print-3.html new file mode 100644 index 000000000..6192642f3 --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_pretty-print-3.html @@ -0,0 +1,8 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE html> +<head> + <meta charset="utf-8"/> + <title>Debugger Pretty Printing Test Page</title> +</head> +<script src="code_ugly-8"></script> diff --git a/toolkit/devtools/debugger/test/doc_pretty-print-on-paused.html b/toolkit/devtools/debugger/test/doc_pretty-print-on-paused.html new file mode 100644 index 000000000..a431d0898 --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_pretty-print-on-paused.html @@ -0,0 +1,14 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8" /> + <title>Pretty printing when debugger is paused Test Page</title> + </head> + <body> + <script src="code_ugly-2.js"></script> + <script src="code_script-switching-02.js"></script> + </body> +</html> diff --git a/toolkit/devtools/debugger/test/doc_pretty-print.html b/toolkit/devtools/debugger/test/doc_pretty-print.html new file mode 100644 index 000000000..dcf595a8d --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_pretty-print.html @@ -0,0 +1,8 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE html> +<head> + <meta charset="utf-8"/> + <title>Debugger Pretty Printing Test Page</title> +</head> +<script src="code_ugly.js"></script> diff --git a/toolkit/devtools/debugger/test/doc_promise.html b/toolkit/devtools/debugger/test/doc_promise.html new file mode 100644 index 000000000..fe6c1d807 --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_promise.html @@ -0,0 +1,30 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Debugger + Promise test page</title> + </head> + + <body> + <script> + window.pending = new Promise(function () {}); + window.fulfilled = Promise.resolve({ a: 1, b: 2, c: 3 }); + window.rejected = Promise.reject(new Error("uh oh")); + + window.doPause = function () { + var p = window.pending; + var f = window.fulfilled; + var r = window.rejected; + debugger; + }; + + // Attach an error handler so that the logs don't have a warning about an + // unhandled, rejected promise. + window.rejected.then(null, function () {}); + </script> + </body> + +</html> diff --git a/toolkit/devtools/debugger/test/doc_random-javascript.html b/toolkit/devtools/debugger/test/doc_random-javascript.html new file mode 100644 index 000000000..69269e409 --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_random-javascript.html @@ -0,0 +1,15 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Debugger test page</title> + </head> + + <body> + <script src="sjs_random-javascript.sjs"></script> + </body> + +</html> diff --git a/toolkit/devtools/debugger/test/doc_recursion-stack.html b/toolkit/devtools/debugger/test/doc_recursion-stack.html new file mode 100644 index 000000000..d68fb1d18 --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_recursion-stack.html @@ -0,0 +1,35 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Debugger test page</title> + </head> + + <body> + <script type="text/javascript"> + function simpleCall() { + debugger; + } + + function evalCall() { + eval("debugger;"); + } + + var gRecurseLimit = 100; + var gRecurseDepth = 0; + + function recurse() { + if (++gRecurseDepth == gRecurseLimit) { + debugger; + gRecurseDepth = 0; + return; + } + recurse(); + } + </script> + </body> + +</html> diff --git a/toolkit/devtools/debugger/test/doc_same-line-functions.html b/toolkit/devtools/debugger/test/doc_same-line-functions.html new file mode 100644 index 000000000..dbdf2644d --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_same-line-functions.html @@ -0,0 +1,15 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Debugger Tracer test page</title> + </head> + + <body> + <script src="code_same-line-functions.js"></script> + <button onclick="first()">Click me!</button> + </body> +</html> diff --git a/toolkit/devtools/debugger/test/doc_scope-variable-2.html b/toolkit/devtools/debugger/test/doc_scope-variable-2.html new file mode 100644 index 000000000..afbfd166a --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_scope-variable-2.html @@ -0,0 +1,30 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Debugger test page</title> + </head> + + <body> + <script type="text/javascript"> + function test() { + var a = "first scope"; + firstNest(); + + function firstNest() { + var a = "second scope"; + secondNest(); + + function secondNest() { + var a = "third scope"; + debugger; + } + } + } + </script> + </body> + +</html> diff --git a/toolkit/devtools/debugger/test/doc_scope-variable-3.html b/toolkit/devtools/debugger/test/doc_scope-variable-3.html new file mode 100644 index 000000000..fcd45cc0a --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_scope-variable-3.html @@ -0,0 +1,23 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Debugger test page</title> + </head> + + <body> + <script type="text/javascript"> + var trap = "first script"; + function test() { + debugger; + } + </script> + <script type="text/javascript">/* + trololol + */</script> + </body> + +</html> diff --git a/toolkit/devtools/debugger/test/doc_scope-variable-4.html b/toolkit/devtools/debugger/test/doc_scope-variable-4.html new file mode 100644 index 000000000..17b0e3b10 --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_scope-variable-4.html @@ -0,0 +1,25 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Debugger test page</title> + </head> + + <body> + <script type="text/javascript"> + function test() { + var a = "first scope"; + nest(); + + function nest() { + var a = "second scope"; + debugger; + } + } + </script> + </body> + +</html> diff --git a/toolkit/devtools/debugger/test/doc_scope-variable.html b/toolkit/devtools/debugger/test/doc_scope-variable.html new file mode 100644 index 000000000..3fa28fab9 --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_scope-variable.html @@ -0,0 +1,25 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Debugger test page</title> + </head> + + <body> + <script type="text/javascript"> + function test() { + var a = "first scope"; + nest(); + } + + function nest() { + var a = "second scope"; + debugger; + } + </script> + </body> + +</html> diff --git a/toolkit/devtools/debugger/test/doc_script-bookmarklet.html b/toolkit/devtools/debugger/test/doc_script-bookmarklet.html new file mode 100644 index 000000000..922010062 --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_script-bookmarklet.html @@ -0,0 +1,14 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Debugger test page</title> + </head> + + <body> + <script>function injectBookmarklet(bookmarklet) { setTimeout(function() { window.location = "javascript:" + bookmarklet; }, 0); }</script> + </body> +</html> diff --git a/toolkit/devtools/debugger/test/doc_script-eval.html b/toolkit/devtools/debugger/test/doc_script-eval.html new file mode 100644 index 000000000..7e3f253bb --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_script-eval.html @@ -0,0 +1,16 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Debugger test page</title> + </head> + + <body> + <button onclick="evalSource()">Click me!</button> + + <script type="text/javascript" src="code_script-eval.js"></script> + </body> +</html> diff --git a/toolkit/devtools/debugger/test/doc_script-switching-01.html b/toolkit/devtools/debugger/test/doc_script-switching-01.html new file mode 100644 index 000000000..afb4484b5 --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_script-switching-01.html @@ -0,0 +1,18 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Debugger test page</title> + </head> + + <body> + <button onclick="firstCall()">Click me!</button> + + <script type="text/javascript" src="code_script-switching-01.js"></script> + <script type="text/javascript" src="code_script-switching-02.js"></script> + </body> + +</html> diff --git a/toolkit/devtools/debugger/test/doc_script-switching-02.html b/toolkit/devtools/debugger/test/doc_script-switching-02.html new file mode 100644 index 000000000..cceeea2c8 --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_script-switching-02.html @@ -0,0 +1,18 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Debugger test page</title> + </head> + + <body> + <button onclick="firstCall()">Click me!</button> + + <script type="text/javascript" src="code_script-switching-01.js"></script> + <script type="text/javascript" src="code_script-switching-02.js?foo=bar,baz|lol"></script> + </body> + +</html> diff --git a/toolkit/devtools/debugger/test/doc_split-console-paused-reload.html b/toolkit/devtools/debugger/test/doc_split-console-paused-reload.html new file mode 100644 index 000000000..3848e7a5e --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_split-console-paused-reload.html @@ -0,0 +1,20 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Test page for opening a split-console when execution is paused</title> + </head> + + <body> + <script type="text/javascript"> + function runDebuggerStatement() { + debugger; + } + window.foobar = 1; + </script> + </body> + +</html> diff --git a/toolkit/devtools/debugger/test/doc_step-out.html b/toolkit/devtools/debugger/test/doc_step-out.html new file mode 100644 index 000000000..89eda2be1 --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_step-out.html @@ -0,0 +1,42 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Debugger test page</title> + </head> + + <body> + <button id="return">Return me!</button> + <button id="throw">Throw me!</button> + + <script type="text/javascript"> + function normal(aArg) { + debugger; + var r = 10; + return r; + } + + function error(aArg) { + function inner(aArg) { + debugger; + var r = 10; + throw "boom"; + return r; + } + try { + inner(aArg); + } catch (e) {} + } + + var normalBtn = document.getElementById("return"); + normalBtn.addEventListener("click", normal, false); + + var throwBtn = document.getElementById("throw"); + throwBtn.addEventListener("click", error, false); + </script> + </body> + +</html> diff --git a/toolkit/devtools/debugger/test/doc_terminate-on-tab-close.html b/toolkit/devtools/debugger/test/doc_terminate-on-tab-close.html new file mode 100644 index 000000000..2101b3103 --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_terminate-on-tab-close.html @@ -0,0 +1,20 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Debugger test page</title> + </head> + + <body> + <script type="text/javascript"> + function debuggerThenThrow() { + debugger; + throw "unreachable"; + } + </script> + </body> + +</html> diff --git a/toolkit/devtools/debugger/test/doc_tracing-01.html b/toolkit/devtools/debugger/test/doc_tracing-01.html new file mode 100644 index 000000000..be3c7af1b --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_tracing-01.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"/> + <title>Debugger Tracer test page</title> + </head> + + <body> + <script src="code_tracing-01.js"></script> + <button onclick="main()">Click me!</button> + + <script type="text/javascript"> + // Have an inline script to make sure the HTML file is listed + // in the sources. We want to switch between them. + function foo() { + return 5; + } + </script> + </body> +</html> diff --git a/toolkit/devtools/debugger/test/doc_watch-expression-button.html b/toolkit/devtools/debugger/test/doc_watch-expression-button.html new file mode 100644 index 000000000..a4a5be26e --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_watch-expression-button.html @@ -0,0 +1,31 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Debugger test page</title> + </head> + + <body> + <button onclick="start()">Click me!</button> + + <script type="text/javascript"> + function test() { + var a = 1; + var b = { a: a }; + b.a = 2; + debugger; + } + + function start() { + var e = eval('test();'); + } + + var button = document.querySelector("button"); + var buttonAsProto = Object.create(button); + </script> + </body> + +</html> diff --git a/toolkit/devtools/debugger/test/doc_watch-expressions.html b/toolkit/devtools/debugger/test/doc_watch-expressions.html new file mode 100644 index 000000000..487b5a5a5 --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_watch-expressions.html @@ -0,0 +1,29 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Debugger test page</title> + </head> + + <body> + <script type="text/javascript"> + function test() { + ermahgerd.call({ canada: new String("eh") }); + } + function ermahgerd(aArg) { + var t = document.title; + debugger; + (function() { + var a = undefined; + debugger; + var a = {}; + debugger; + }("sensational")); + } + </script> + </body> + +</html> diff --git a/toolkit/devtools/debugger/test/doc_with-frame.html b/toolkit/devtools/debugger/test/doc_with-frame.html new file mode 100644 index 000000000..8fa202b18 --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_with-frame.html @@ -0,0 +1,29 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Debugger test page</title> + </head> + + <body> + <button onclick="test(10)">Click me!</button> + + <script type="text/javascript"> + function test(aNumber) { + var a, obj = { alpha: 1, beta: 2 }; + var r = aNumber; + with (Math) { + a = PI * r * r; + with (obj) { + var foo = beta * PI; + debugger; + } + } + } + </script> + </body> + +</html> diff --git a/toolkit/devtools/debugger/test/head.js b/toolkit/devtools/debugger/test/head.js new file mode 100644 index 000000000..f510a56f7 --- /dev/null +++ b/toolkit/devtools/debugger/test/head.js @@ -0,0 +1,1019 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +let { Services } = Cu.import("resource://gre/modules/Services.jsm", {}); + +// Disable logging for faster test runs. Set this pref to true if you want to +// debug a test in your try runs. Both the debugger server and frontend will +// be affected by this pref. +let gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log"); +Services.prefs.setBoolPref("devtools.debugger.log", false); + +let { Task } = Cu.import("resource://gre/modules/Task.jsm", {}); +let { Promise: promise } = Cu.import("resource://gre/modules/devtools/deprecated-sync-thenables.js", {}); +let { gDevTools } = Cu.import("resource:///modules/devtools/gDevTools.jsm", {}); +let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); +let { require } = devtools; +let { DevToolsUtils } = Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm", {}); +let { BrowserToolboxProcess } = Cu.import("resource:///modules/devtools/ToolboxProcess.jsm", {}); +let { DebuggerServer } = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {}); +let { DebuggerClient } = Cu.import("resource://gre/modules/devtools/dbg-client.jsm", {}); +let { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm", {}); +let EventEmitter = require("devtools/toolkit/event-emitter"); +const { promiseInvoke } = require("devtools/async-utils"); +let TargetFactory = devtools.TargetFactory; +let Toolbox = devtools.Toolbox; + +const EXAMPLE_URL = "http://example.com/browser/browser/devtools/debugger/test/"; +const FRAME_SCRIPT_URL = getRootDirectory(gTestPath) + "code_frame-script.js"; + +gDevTools.testing = true; +SimpleTest.registerCleanupFunction(() => { + gDevTools.testing = false; +}); + +// All tests are asynchronous. +waitForExplicitFinish(); + +registerCleanupFunction(function* () { + info("finish() was called, cleaning up..."); + Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging); + + while (gBrowser && gBrowser.tabs && gBrowser.tabs.length > 1) { + info("Destroying toolbox."); + let target = TargetFactory.forTab(gBrowser.selectedTab); + yield gDevTools.closeToolbox(target); + + info("Removing tab."); + gBrowser.removeCurrentTab(); + } + + // Properly shut down the server to avoid memory leaks. + DebuggerServer.destroy(); + + // Debugger tests use a lot of memory, so force a GC to help fragmentation. + info("Forcing GC after debugger test."); + Cu.forceGC(); +}); + +// Import the GCLI test helper +let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/")); +testDir = testDir.replace(/\/\//g, '/'); +testDir = testDir.replace("chrome:/mochitest", "chrome://mochitest"); +let helpersjs = testDir + "/../../commandline/test/helpers.js"; +Services.scriptloader.loadSubScript(helpersjs, this); + +// Redeclare dbg_assert with a fatal behavior. +function dbg_assert(cond, e) { + if (!cond) { + throw e; + } +} + +function addWindow(aUrl) { + info("Adding window: " + aUrl); + return promise.resolve(getChromeWindow(window.open(aUrl))); +} + +function getChromeWindow(aWindow) { + return aWindow + .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem).rootTreeItem + .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow); +} + +function addTab(aUrl, aWindow) { + info("Adding tab: " + aUrl); + + let deferred = promise.defer(); + let targetWindow = aWindow || window; + let targetBrowser = targetWindow.gBrowser; + + targetWindow.focus(); + let tab = targetBrowser.selectedTab = targetBrowser.addTab(aUrl); + let linkedBrowser = tab.linkedBrowser; + + info("Loading frame script with url " + FRAME_SCRIPT_URL + "."); + linkedBrowser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false); + + linkedBrowser.addEventListener("load", function onLoad() { + linkedBrowser.removeEventListener("load", onLoad, true); + info("Tab added and finished loading: " + aUrl); + deferred.resolve(tab); + }, true); + + return deferred.promise; +} + +function removeTab(aTab, aWindow) { + info("Removing tab."); + + let deferred = promise.defer(); + let targetWindow = aWindow || window; + let targetBrowser = targetWindow.gBrowser; + let tabContainer = targetBrowser.tabContainer; + + tabContainer.addEventListener("TabClose", function onClose(aEvent) { + tabContainer.removeEventListener("TabClose", onClose, false); + info("Tab removed and finished closing."); + deferred.resolve(); + }, false); + + targetBrowser.removeTab(aTab); + return deferred.promise; +} + +function addAddon(aUrl) { + info("Installing addon: " + aUrl); + + let deferred = promise.defer(); + + AddonManager.getInstallForURL(aUrl, aInstaller => { + aInstaller.install(); + let listener = { + onInstallEnded: function(aAddon, aAddonInstall) { + aInstaller.removeListener(listener); + + // Wait for add-on's startup scripts to execute. See bug 997408 + executeSoon(function() { + deferred.resolve(aAddonInstall); + }); + } + }; + aInstaller.addListener(listener); + }, "application/x-xpinstall"); + + return deferred.promise; +} + +function removeAddon(aAddon) { + info("Removing addon."); + + let deferred = promise.defer(); + + let listener = { + onUninstalled: function(aUninstalledAddon) { + if (aUninstalledAddon != aAddon) { + return; + } + AddonManager.removeAddonListener(listener); + deferred.resolve(); + } + }; + AddonManager.addAddonListener(listener); + aAddon.uninstall(); + + return deferred.promise; +} + +function getTabActorForUrl(aClient, aUrl) { + let deferred = promise.defer(); + + aClient.listTabs(aResponse => { + let tabActor = aResponse.tabs.filter(aGrip => aGrip.url == aUrl).pop(); + deferred.resolve(tabActor); + }); + + return deferred.promise; +} + +function getAddonActorForUrl(aClient, aUrl) { + info("Get addon actor for URL: " + aUrl); + let deferred = promise.defer(); + + aClient.listAddons(aResponse => { + let addonActor = aResponse.addons.filter(aGrip => aGrip.url == aUrl).pop(); + info("got addon actor for URL: " + addonActor.actor); + deferred.resolve(addonActor); + }); + + return deferred.promise; +} + +function attachTabActorForUrl(aClient, aUrl) { + let deferred = promise.defer(); + + getTabActorForUrl(aClient, aUrl).then(aGrip => { + aClient.attachTab(aGrip.actor, aResponse => { + deferred.resolve([aGrip, aResponse]); + }); + }); + + return deferred.promise; +} + +function attachThreadActorForUrl(aClient, aUrl) { + let deferred = promise.defer(); + + attachTabActorForUrl(aClient, aUrl).then(([aGrip, aResponse]) => { + aClient.attachThread(aResponse.threadActor, (aResponse, aThreadClient) => { + aThreadClient.resume(aResponse => { + deferred.resolve(aThreadClient); + }); + }); + }); + + return deferred.promise; +} + +function once(aTarget, aEventName, aUseCapture = false) { + info("Waiting for event: '" + aEventName + "' on " + aTarget + "."); + + let deferred = promise.defer(); + + for (let [add, remove] of [ + ["addEventListener", "removeEventListener"], + ["addListener", "removeListener"], + ["on", "off"] + ]) { + if ((add in aTarget) && (remove in aTarget)) { + aTarget[add](aEventName, function onEvent(...aArgs) { + aTarget[remove](aEventName, onEvent, aUseCapture); + deferred.resolve.apply(deferred, aArgs); + }, aUseCapture); + break; + } + } + + return deferred.promise; +} + +function waitForTick() { + let deferred = promise.defer(); + executeSoon(deferred.resolve); + return deferred.promise; +} + +function waitForTime(aDelay) { + let deferred = promise.defer(); + setTimeout(deferred.resolve, aDelay); + return deferred.promise; +} + +function waitForSourceShown(aPanel, aUrl) { + return waitForDebuggerEvents(aPanel, aPanel.panelWin.EVENTS.SOURCE_SHOWN).then(aSource => { + let sourceUrl = aSource.url || aSource.introductionUrl; + info("Source shown: " + sourceUrl); + + if (!sourceUrl.contains(aUrl)) { + return waitForSourceShown(aPanel, aUrl); + } else { + ok(true, "The correct source has been shown."); + } + }); +} + +function waitForEditorLocationSet(aPanel) { + return waitForDebuggerEvents(aPanel, aPanel.panelWin.EVENTS.EDITOR_LOCATION_SET); +} + +function ensureSourceIs(aPanel, aUrlOrSource, aWaitFlag = false) { + let sources = aPanel.panelWin.DebuggerView.Sources; + + if (sources.selectedValue === aUrlOrSource || + sources.selectedItem.attachment.source.url.contains(aUrlOrSource)) { + ok(true, "Expected source is shown: " + aUrlOrSource); + return promise.resolve(null); + } + if (aWaitFlag) { + return waitForSourceShown(aPanel, aUrlOrSource); + } + ok(false, "Expected source was not already shown: " + aUrlOrSource); + return promise.reject(null); +} + +function waitForCaretUpdated(aPanel, aLine, aCol = 1) { + return waitForEditorEvents(aPanel, "cursorActivity").then(() => { + let cursor = aPanel.panelWin.DebuggerView.editor.getCursor(); + info("Caret updated: " + (cursor.line + 1) + ", " + (cursor.ch + 1)); + + if (!isCaretPos(aPanel, aLine, aCol)) { + return waitForCaretUpdated(aPanel, aLine, aCol); + } else { + ok(true, "The correct caret position has been set."); + } + }); +} + +function ensureCaretAt(aPanel, aLine, aCol = 1, aWaitFlag = false) { + if (isCaretPos(aPanel, aLine, aCol)) { + ok(true, "Expected caret position is set: " + aLine + "," + aCol); + return promise.resolve(null); + } + if (aWaitFlag) { + return waitForCaretUpdated(aPanel, aLine, aCol); + } + ok(false, "Expected caret position was not already set: " + aLine + "," + aCol); + return promise.reject(null); +} + +function isCaretPos(aPanel, aLine, aCol = 1) { + let editor = aPanel.panelWin.DebuggerView.editor; + let cursor = editor.getCursor(); + + // Source editor starts counting line and column numbers from 0. + info("Current editor caret position: " + (cursor.line + 1) + ", " + (cursor.ch + 1)); + return cursor.line == (aLine - 1) && cursor.ch == (aCol - 1); +} + +function isDebugPos(aPanel, aLine) { + let editor = aPanel.panelWin.DebuggerView.editor; + let location = editor.getDebugLocation(); + + // Source editor starts counting line and column numbers from 0. + info("Current editor debug position: " + (location + 1)); + return location != null && editor.hasLineClass(aLine - 1, "debug-line"); +} + +function isEditorSel(aPanel, [start, end]) { + let editor = aPanel.panelWin.DebuggerView.editor; + let range = { + start: editor.getOffset(editor.getCursor("start")), + end: editor.getOffset(editor.getCursor()) + }; + + // Source editor starts counting line and column numbers from 0. + info("Current editor selection: " + (range.start + 1) + ", " + (range.end + 1)); + return range.start == (start - 1) && range.end == (end - 1); +} + +function waitForSourceAndCaret(aPanel, aUrl, aLine, aCol) { + return promise.all([ + waitForSourceShown(aPanel, aUrl), + waitForCaretUpdated(aPanel, aLine, aCol) + ]); +} + +function waitForCaretAndScopes(aPanel, aLine, aCol) { + return promise.all([ + waitForCaretUpdated(aPanel, aLine, aCol), + waitForDebuggerEvents(aPanel, aPanel.panelWin.EVENTS.FETCHED_SCOPES) + ]); +} + +function waitForSourceAndCaretAndScopes(aPanel, aUrl, aLine, aCol) { + return promise.all([ + waitForSourceAndCaret(aPanel, aUrl, aLine, aCol), + waitForDebuggerEvents(aPanel, aPanel.panelWin.EVENTS.FETCHED_SCOPES) + ]); +} + +function waitForDebuggerEvents(aPanel, aEventName, aEventRepeat = 1) { + info("Waiting for debugger event: '" + aEventName + "' to fire: " + aEventRepeat + " time(s)."); + + let deferred = promise.defer(); + let panelWin = aPanel.panelWin; + let count = 0; + + panelWin.on(aEventName, function onEvent(aEventName, ...aArgs) { + info("Debugger event '" + aEventName + "' fired: " + (++count) + " time(s)."); + + if (count == aEventRepeat) { + ok(true, "Enough '" + aEventName + "' panel events have been fired."); + panelWin.off(aEventName, onEvent); + deferred.resolve.apply(deferred, aArgs); + } + }); + + return deferred.promise; +} + +function waitForEditorEvents(aPanel, aEventName, aEventRepeat = 1) { + info("Waiting for editor event: '" + aEventName + "' to fire: " + aEventRepeat + " time(s)."); + + let deferred = promise.defer(); + let editor = aPanel.panelWin.DebuggerView.editor; + let count = 0; + + editor.on(aEventName, function onEvent(...aArgs) { + info("Editor event '" + aEventName + "' fired: " + (++count) + " time(s)."); + + if (count == aEventRepeat) { + ok(true, "Enough '" + aEventName + "' editor events have been fired."); + editor.off(aEventName, onEvent); + deferred.resolve.apply(deferred, aArgs); + } + }); + + return deferred.promise; +} + +function waitForThreadEvents(aPanel, aEventName, aEventRepeat = 1) { + info("Waiting for thread event: '" + aEventName + "' to fire: " + aEventRepeat + " time(s)."); + + let deferred = promise.defer(); + let thread = aPanel.panelWin.gThreadClient; + let count = 0; + + thread.addListener(aEventName, function onEvent(aEventName, ...aArgs) { + info("Thread event '" + aEventName + "' fired: " + (++count) + " time(s)."); + + if (count == aEventRepeat) { + ok(true, "Enough '" + aEventName + "' thread events have been fired."); + thread.removeListener(aEventName, onEvent); + deferred.resolve.apply(deferred, aArgs); + } + }); + + return deferred.promise; +} + +function waitForClientEvents(aPanel, aEventName, aEventRepeat = 1) { + info("Waiting for client event: '" + aEventName + "' to fire: " + aEventRepeat + " time(s)."); + + let deferred = promise.defer(); + let client = aPanel.panelWin.gClient; + let count = 0; + + client.addListener(aEventName, function onEvent(aEventName, ...aArgs) { + info("Thread event '" + aEventName + "' fired: " + (++count) + " time(s)."); + + if (count == aEventRepeat) { + ok(true, "Enough '" + aEventName + "' thread events have been fired."); + client.removeListener(aEventName, onEvent); + deferred.resolve.apply(deferred, aArgs); + } + }); + + return deferred.promise; +} + +function ensureThreadClientState(aPanel, aState) { + let thread = aPanel.panelWin.gThreadClient; + let state = thread.state; + + info("Thread is: '" + state + "'."); + + if (state == aState) { + return promise.resolve(null); + } else { + return waitForThreadEvents(aPanel, aState); + } +} + +function navigateActiveTabTo(aPanel, aUrl, aWaitForEventName, aEventRepeat) { + let finished = waitForDebuggerEvents(aPanel, aWaitForEventName, aEventRepeat); + let activeTab = aPanel.panelWin.DebuggerController._target.activeTab; + aUrl ? activeTab.navigateTo(aUrl) : activeTab.reload(); + return finished; +} + +function navigateActiveTabInHistory(aPanel, aDirection, aWaitForEventName, aEventRepeat) { + let finished = waitForDebuggerEvents(aPanel, aWaitForEventName, aEventRepeat); + content.history[aDirection](); + return finished; +} + +function reloadActiveTab(aPanel, aWaitForEventName, aEventRepeat) { + return navigateActiveTabTo(aPanel, null, aWaitForEventName, aEventRepeat); +} + +function clearText(aElement) { + info("Clearing text..."); + aElement.focus(); + aElement.value = ""; +} + +function setText(aElement, aText) { + clearText(aElement); + info("Setting text: " + aText); + aElement.value = aText; +} + +function typeText(aElement, aText) { + info("Typing text: " + aText); + aElement.focus(); + EventUtils.sendString(aText, aElement.ownerDocument.defaultView); +} + +function backspaceText(aElement, aTimes) { + info("Pressing backspace " + aTimes + " times."); + for (let i = 0; i < aTimes; i++) { + aElement.focus(); + EventUtils.sendKey("BACK_SPACE", aElement.ownerDocument.defaultView); + } +} + +function getTab(aTarget, aWindow) { + if (aTarget instanceof XULElement) { + return promise.resolve(aTarget); + } else { + return addTab(aTarget, aWindow); + } +} + +function getSources(aClient) { + let deferred = promise.defer(); + + aClient.getSources(({sources}) => deferred.resolve(sources)); + + return deferred.promise; +} + +function initDebugger(aTarget, aWindow) { + info("Initializing a debugger panel."); + + return getTab(aTarget, aWindow).then(aTab => { + info("Debugee tab added successfully: " + aTarget); + + let deferred = promise.defer(); + let debuggee = aTab.linkedBrowser.contentWindow.wrappedJSObject; + let target = TargetFactory.forTab(aTab); + + gDevTools.showToolbox(target, "jsdebugger").then(aToolbox => { + info("Debugger panel shown successfully."); + + let debuggerPanel = aToolbox.getCurrentPanel(); + let panelWin = debuggerPanel.panelWin; + + // Wait for the initial resume... + panelWin.gClient.addOneTimeListener("resumed", () => { + info("Debugger client resumed successfully."); + + prepareDebugger(debuggerPanel); + deferred.resolve([aTab, debuggee, debuggerPanel, aWindow]); + }); + }); + + return deferred.promise; + }); +} + +// Creates an add-on debugger for a given add-on. The returned AddonDebugger +// object must be destroyed before finishing the test +function initAddonDebugger(aUrl) { + let addonDebugger = new AddonDebugger(); + return addonDebugger.init(aUrl).then(() => addonDebugger); +} + +function AddonDebugger() { + this._onMessage = this._onMessage.bind(this); + this._onConsoleAPICall = this._onConsoleAPICall.bind(this); + EventEmitter.decorate(this); +} + +AddonDebugger.prototype = { + init: Task.async(function*(aUrl) { + info("Initializing an addon debugger panel."); + + if (!DebuggerServer.initialized) { + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + } + + this.frame = document.createElement("iframe"); + this.frame.setAttribute("height", 400); + document.documentElement.appendChild(this.frame); + window.addEventListener("message", this._onMessage); + + let transport = DebuggerServer.connectPipe(); + this.client = new DebuggerClient(transport); + + let connected = promise.defer(); + this.client.connect(connected.resolve); + yield connected.promise; + + let addonActor = yield getAddonActorForUrl(this.client, aUrl); + + let targetOptions = { + form: addonActor, + client: this.client, + chrome: true + }; + + let toolboxOptions = { + customIframe: this.frame + }; + + this.target = devtools.TargetFactory.forTab(targetOptions); + let toolbox = yield gDevTools.showToolbox(this.target, "jsdebugger", devtools.Toolbox.HostType.CUSTOM, toolboxOptions); + + info("Addon debugger panel shown successfully."); + + this.debuggerPanel = toolbox.getCurrentPanel(); + + // Wait for the initial resume... + yield waitForClientEvents(this.debuggerPanel, "resumed"); + yield prepareDebugger(this.debuggerPanel); + yield this._attachConsole(); + }), + + destroy: Task.async(function*() { + let deferred = promise.defer(); + this.client.close(deferred.resolve); + yield deferred.promise; + yield this.debuggerPanel._toolbox.destroy(); + this.frame.remove(); + window.removeEventListener("message", this._onMessage); + }), + + _attachConsole: function() { + let deferred = promise.defer(); + this.client.attachConsole(this.target.form.consoleActor, ["ConsoleAPI"], (aResponse, aWebConsoleClient) => { + if (aResponse.error) { + deferred.reject(aResponse); + } + else { + this.webConsole = aWebConsoleClient; + this.client.addListener("consoleAPICall", this._onConsoleAPICall); + deferred.resolve(); + } + }); + return deferred.promise; + }, + + _onConsoleAPICall: function(aType, aPacket) { + if (aPacket.from != this.webConsole.actor) + return; + this.emit("console", aPacket.message); + }, + + /** + * Returns a list of the groups and sources in the UI. The returned array + * contains objects for each group with properties name and sources. The + * sources property contains an array with objects for each source for that + * group with properties label and url. + */ + getSourceGroups: Task.async(function*() { + let debuggerWin = this.debuggerPanel.panelWin; + let sources = yield getSources(debuggerWin.gThreadClient); + ok(sources.length, "retrieved sources"); + + // groups will be the return value, groupmap and the maps we put in it will + // be used as quick lookups to add the url information in below + let groups = []; + let groupmap = new Map(); + + let uigroups = this.debuggerPanel.panelWin.document.querySelectorAll(".side-menu-widget-group"); + for (let g of uigroups) { + let name = g.querySelector(".side-menu-widget-group-title .name").value; + let group = { + name: name, + sources: [] + }; + groups.push(group); + let labelmap = new Map(); + groupmap.set(name, labelmap); + + for (let l of g.querySelectorAll(".dbg-source-item")) { + let source = { + label: l.value, + url: null + }; + + labelmap.set(l.value, source); + group.sources.push(source); + } + } + + for (let source of sources) { + let { label, group } = debuggerWin.DebuggerView.Sources.getItemByValue(source.actor).attachment; + + if (!groupmap.has(group)) { + ok(false, "Saw a source group not in the UI: " + group); + continue; + } + + if (!groupmap.get(group).has(label)) { + ok(false, "Saw a source label not in the UI: " + label); + continue; + } + + groupmap.get(group).get(label).url = source.url.split(" -> ").pop(); + } + + return groups; + }), + + _onMessage: function(event) { + let json = JSON.parse(event.data); + switch (json.name) { + case "toolbox-title": + this.title = json.data.value; + break; + } + } +} + +function initChromeDebugger(aOnClose) { + info("Initializing a chrome debugger process."); + + let deferred = promise.defer(); + + // Wait for the toolbox process to start... + BrowserToolboxProcess.init(aOnClose, (aEvent, aProcess) => { + info("Browser toolbox process started successfully."); + + prepareDebugger(aProcess); + deferred.resolve(aProcess); + }); + + return deferred.promise; +} + +function prepareDebugger(aDebugger) { + if ("target" in aDebugger) { + let view = aDebugger.panelWin.DebuggerView; + view.Variables.lazyEmpty = false; + view.Variables.lazySearch = false; + view.FilteredSources._autoSelectFirstItem = true; + view.FilteredFunctions._autoSelectFirstItem = true; + } else { + // Nothing to do here yet. + } +} + +function teardown(aPanel, aFlags = {}) { + info("Destroying the specified debugger."); + + let toolbox = aPanel._toolbox; + let tab = aPanel.target.tab; + let debuggerRootActorDisconnected = once(window, "Debugger:Shutdown"); + let debuggerPanelDestroyed = once(aPanel, "destroyed"); + let devtoolsToolboxDestroyed = toolbox.destroy(); + + return promise.all([ + debuggerRootActorDisconnected, + debuggerPanelDestroyed, + devtoolsToolboxDestroyed + ]).then(() => aFlags.noTabRemoval ? null : removeTab(tab)); +} + +function closeDebuggerAndFinish(aPanel, aFlags = {}) { + let thread = aPanel.panelWin.gThreadClient; + if (thread.state == "paused" && !aFlags.whilePaused) { + ok(false, "You should use 'resumeDebuggerThenCloseAndFinish' instead, " + + "unless you're absolutely sure about what you're doing."); + } + return teardown(aPanel, aFlags).then(finish); +} + +function resumeDebuggerThenCloseAndFinish(aPanel, aFlags = {}) { + let deferred = promise.defer(); + let thread = aPanel.panelWin.gThreadClient; + thread.resume(() => closeDebuggerAndFinish(aPanel, aFlags).then(deferred.resolve)); + return deferred.promise; +} + +// Blackboxing helpers + +function getBlackBoxButton(aPanel) { + return aPanel.panelWin.document.getElementById("black-box"); +} + +/** + * Returns the node that has the black-boxed class applied to it. + */ +function getSelectedSourceElement(aPanel) { + return aPanel.panelWin.DebuggerView.Sources.selectedItem.prebuiltNode; +} + +function toggleBlackBoxing(aPanel, aSource = null) { + function clickBlackBoxButton() { + getBlackBoxButton(aPanel).click(); + } + + const blackBoxChanged = waitForThreadEvents(aPanel, "blackboxchange"); + + if (aSource) { + aPanel.panelWin.DebuggerView.Sources.selectedValue = aSource; + ensureSourceIs(aPanel, aSource, true).then(clickBlackBoxButton); + } else { + clickBlackBoxButton(); + } + + return blackBoxChanged; +} + +function selectSourceAndGetBlackBoxButton(aPanel, aUrl) { + function returnBlackboxButton() { + return getBlackBoxButton(aPanel); + } + + let sources = aPanel.panelWin.DebuggerView.Sources; + sources.selectedValue = getSourceActor(sources, aUrl); + return ensureSourceIs(aPanel, aUrl, true).then(returnBlackboxButton); +} + +// Variables view inspection popup helpers + +function openVarPopup(aPanel, aCoords, aWaitForFetchedProperties) { + let events = aPanel.panelWin.EVENTS; + let editor = aPanel.panelWin.DebuggerView.editor; + let bubble = aPanel.panelWin.DebuggerView.VariableBubble; + let tooltip = bubble._tooltip.panel; + + let popupShown = once(tooltip, "popupshown"); + let fetchedProperties = aWaitForFetchedProperties + ? waitForDebuggerEvents(aPanel, events.FETCHED_BUBBLE_PROPERTIES) + : promise.resolve(null); + let updatedFrame = waitForDebuggerEvents(aPanel, events.FETCHED_SCOPES); + + let { left, top } = editor.getCoordsFromPosition(aCoords); + bubble._findIdentifier(left, top); + return promise.all([popupShown, fetchedProperties, updatedFrame]).then(waitForTick); +} + +// Simulates the mouse hovering a variable in the debugger +// Takes in account the position of the cursor in the text, if the text is +// selected and if a button is currently pushed (aButtonPushed > 0). +// The function returns a promise which returns true if the popup opened or +// false if it didn't +function intendOpenVarPopup(aPanel, aPosition, aButtonPushed) { + let bubble = aPanel.panelWin.DebuggerView.VariableBubble; + let editor = aPanel.panelWin.DebuggerView.editor; + let tooltip = bubble._tooltip; + + let { left, top } = editor.getCoordsFromPosition(aPosition); + + const eventDescriptor = { + clientX: left, + clientY: top, + buttons: aButtonPushed + }; + + bubble._onMouseMove(eventDescriptor); + + const deferred = promise.defer(); + window.setTimeout( + function() { + if(tooltip.isEmpty()) { + deferred.resolve(false); + } else { + deferred.resolve(true); + } + }, + tooltip.defaultShowDelay + 1000 + ); + + return deferred.promise; +} + +function hideVarPopup(aPanel) { + let bubble = aPanel.panelWin.DebuggerView.VariableBubble; + let tooltip = bubble._tooltip.panel; + + let popupHiding = once(tooltip, "popuphiding"); + bubble.hideContents(); + return popupHiding.then(waitForTick); +} + +function hideVarPopupByScrollingEditor(aPanel) { + let editor = aPanel.panelWin.DebuggerView.editor; + let bubble = aPanel.panelWin.DebuggerView.VariableBubble; + let tooltip = bubble._tooltip.panel; + + let popupHiding = once(tooltip, "popuphiding"); + editor.setFirstVisibleLine(0); + return popupHiding.then(waitForTick); +} + +function reopenVarPopup(...aArgs) { + return hideVarPopup.apply(this, aArgs).then(() => openVarPopup.apply(this, aArgs)); +} + +// Tracing helpers + +function startTracing(aPanel) { + const deferred = promise.defer(); + aPanel.panelWin.DebuggerController.Tracer.startTracing(aResponse => { + if (aResponse.error) { + deferred.reject(aResponse); + } else { + deferred.resolve(aResponse); + } + }); + return deferred.promise; +} + +function stopTracing(aPanel) { + const deferred = promise.defer(); + aPanel.panelWin.DebuggerController.Tracer.stopTracing(aResponse => { + if (aResponse.error) { + deferred.reject(aResponse); + } else { + deferred.resolve(aResponse); + } + }); + return deferred.promise; +} + +function filterTraces(aPanel, f) { + const traces = aPanel.panelWin.document + .getElementById("tracer-traces") + .querySelector("scrollbox") + .children; + return Array.filter(traces, f); +} +function attachAddonActorForUrl(aClient, aUrl) { + let deferred = promise.defer(); + + getAddonActorForUrl(aClient, aUrl).then(aGrip => { + aClient.attachAddon(aGrip.actor, aResponse => { + deferred.resolve([aGrip, aResponse]); + }); + }); + + return deferred.promise; +} + +function rdpInvoke(aClient, aMethod, ...args) { + return promiseInvoke(aClient, aMethod, ...args) + .then(({error, message }) => { + if (error) { + throw new Error(error + ": " + message); + } + }); +} + +function doResume(aPanel) { + const threadClient = aPanel.panelWin.gThreadClient; + return rdpInvoke(threadClient, threadClient.resume); +} + +function doInterrupt(aPanel) { + const threadClient = aPanel.panelWin.gThreadClient; + return rdpInvoke(threadClient, threadClient.interrupt); +} + +function pushPrefs(...aPrefs) { + let deferred = promise.defer(); + SpecialPowers.pushPrefEnv({"set": aPrefs}, deferred.resolve); + return deferred.promise; +} + +function popPrefs() { + let deferred = promise.defer(); + SpecialPowers.popPrefEnv(deferred.resolve); + return deferred.promise; +} + +function sendMessageToTab(tab, name, data, objects) { + info("Sending message with name " + name + " to tab."); + + tab.linkedBrowser.messageManager.sendAsyncMessage(name, data, objects); +} + +function waitForMessageFromTab(tab, name) { + info("Waiting for message with name " + name + " from tab."); + + return new Promise(function (resolve) { + let messageManager = tab.linkedBrowser.messageManager; + messageManager.addMessageListener(name, function listener(message) { + messageManager.removeMessageListener(name, listener); + resolve(message); + }); + }); +} + +function callInTab(tab, name) { + info("Calling function with name " + name + " in tab."); + + sendMessageToTab(tab, "test:call", { + name: name, + args: Array.prototype.slice.call(arguments, 2) + }); + waitForMessageFromTab(tab, "test:call"); +} + +function evalInTab(tab, string) { + info("Evalling string " + string + " in tab."); + + sendMessageToTab(tab, "test:eval", { + string: string, + }); + waitForMessageFromTab(tab, "test:eval"); +} + +function sendMouseClickToTab(tab, target) { + info("Sending mouse click to tab."); + + sendMessageToTab(tab, "test:click", undefined, { + target: target + }); +} + +// Source helpers + +function getSelectedSourceURL(aSources) { + return (aSources.selectedItem && + aSources.selectedItem.attachment.source.url); +} + +function getSourceURL(aSources, aActor) { + let item = aSources.getItemByValue(aActor); + return item && item.attachment.source.url; +} + +function getSourceActor(aSources, aURL) { + let item = aSources.getItemForAttachment(a => a.source.url === aURL); + return item && item.value; +} + +function getSourceForm(aSources, aURL) { + let item = aSources.getItemByValue(getSourceActor(gSources, aURL)); + return item.attachment.source; +} diff --git a/toolkit/devtools/debugger/test/sjs_random-javascript.sjs b/toolkit/devtools/debugger/test/sjs_random-javascript.sjs new file mode 100644 index 000000000..3e0ea8e53 --- /dev/null +++ b/toolkit/devtools/debugger/test/sjs_random-javascript.sjs @@ -0,0 +1,11 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function handleRequest(request, response) { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "application/javascript; charset=utf-8", false); + response.write([ + "window.setInterval(function bacon() {", + " var x = '" + Math.random() + "';", + "}, 0);"].join("\n")); +} diff --git a/toolkit/devtools/debugger/test/testactors.js b/toolkit/devtools/debugger/test/testactors.js new file mode 100644 index 000000000..01d197927 --- /dev/null +++ b/toolkit/devtools/debugger/test/testactors.js @@ -0,0 +1,31 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function TestActor1(aConnection, aTab) +{ + this.conn = aConnection; + this.tab = aTab; +} + +TestActor1.prototype = { + actorPrefix: "test_one", + + grip: function TA1_grip() { + return { actor: this.actorID, + test: "TestActor1" }; + }, + + onPing: function TA1_onPing() { + return { pong: "pong" }; + } +}; + +TestActor1.prototype.requestTypes = { + "ping": TestActor1.prototype.onPing +}; + +DebuggerServer.removeTabActor(TestActor1); +DebuggerServer.removeGlobalActor(TestActor1); + +DebuggerServer.addTabActor(TestActor1, "testTabActor1"); +DebuggerServer.addGlobalActor(TestActor1, "testGlobalActor1"); |