import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo

This commit is contained in:
Roy Tam 2018-01-19 03:59:58 +08:00
commit dcd9973243
150858 changed files with 23884658 additions and 0 deletions

View file

@ -0,0 +1,291 @@
/* -*- 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, results: Cr } = Components;
const DBG_XUL = "chrome://devtools/content/framework/toolbox-process-window.xul";
const CHROME_DEBUGGER_PROFILE_NAME = "chrome_debugger_profile";
const { require, DevToolsLoader } = Cu.import("resource://devtools/shared/Loader.jsm", {});
const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyGetter(this, "Telemetry", function () {
return require("devtools/client/shared/telemetry");
});
XPCOMUtils.defineLazyGetter(this, "EventEmitter", function () {
return require("devtools/shared/event-emitter");
});
const promise = require("promise");
const Services = require("Services");
this.EXPORTED_SYMBOLS = ["BrowserToolboxProcess"];
var processes = new Set();
/**
* Constructor for creating a process that will hold a chrome toolbox.
*
* @param function aOnClose [optional]
* A function called when the process stops running.
* @param function aOnRun [optional]
* A function called when the process starts running.
* @param object aOptions [optional]
* An object with properties for configuring BrowserToolboxProcess.
*/
this.BrowserToolboxProcess = function BrowserToolboxProcess(aOnClose, aOnRun, aOptions) {
let emitter = new EventEmitter();
this.on = emitter.on.bind(emitter);
this.off = emitter.off.bind(emitter);
this.once = emitter.once.bind(emitter);
// Forward any events to the shared emitter.
this.emit = function (...args) {
emitter.emit(...args);
BrowserToolboxProcess.emit(...args);
};
// If first argument is an object, use those properties instead of
// all three arguments
if (typeof aOnClose === "object") {
if (aOnClose.onClose) {
this.once("close", aOnClose.onClose);
}
if (aOnClose.onRun) {
this.once("run", aOnClose.onRun);
}
this._options = aOnClose;
} else {
if (aOnClose) {
this.once("close", aOnClose);
}
if (aOnRun) {
this.once("run", aOnRun);
}
this._options = aOptions || {};
}
this._telemetry = new Telemetry();
this.close = this.close.bind(this);
Services.obs.addObserver(this.close, "quit-application", false);
this._initServer();
this._initProfile();
this._create();
processes.add(this);
};
EventEmitter.decorate(BrowserToolboxProcess);
/**
* Initializes and starts a chrome toolbox process.
* @return object
*/
BrowserToolboxProcess.init = function (aOnClose, aOnRun, aOptions) {
return new BrowserToolboxProcess(aOnClose, aOnRun, aOptions);
};
/**
* Passes a set of options to the BrowserAddonActors for the given ID.
*
* @param aId string
* The ID of the add-on to pass the options to
* @param aOptions object
* The options.
* @return a promise that will be resolved when complete.
*/
BrowserToolboxProcess.setAddonOptions = function DSC_setAddonOptions(aId, aOptions) {
let promises = [];
for (let process of processes.values()) {
promises.push(process.debuggerServer.setAddonOptions(aId, aOptions));
}
return promise.all(promises);
};
BrowserToolboxProcess.prototype = {
/**
* Initializes the debugger server.
*/
_initServer: function () {
if (this.debuggerServer) {
dumpn("The chrome toolbox server is already running.");
return;
}
dumpn("Initializing the chrome toolbox server.");
// Create a separate loader instance, so that we can be sure to receive a
// separate instance of the DebuggingServer from the rest of the devtools.
// This allows us to safely use the tools against even the actors and
// DebuggingServer itself, especially since we can mark this loader as
// invisible to the debugger (unlike the usual loader settings).
this.loader = new DevToolsLoader();
this.loader.invisibleToDebugger = true;
let { DebuggerServer } = this.loader.require("devtools/server/main");
this.debuggerServer = DebuggerServer;
dumpn("Created a separate loader instance for the DebuggerServer.");
// Forward interesting events.
this.debuggerServer.on("connectionchange", this.emit);
this.debuggerServer.init();
this.debuggerServer.addBrowserActors();
this.debuggerServer.allowChromeProcess = true;
dumpn("initialized and added the browser actors for the DebuggerServer.");
let chromeDebuggingPort =
Services.prefs.getIntPref("devtools.debugger.chrome-debugging-port");
let chromeDebuggingWebSocket =
Services.prefs.getBoolPref("devtools.debugger.chrome-debugging-websocket");
let listener = this.debuggerServer.createListener();
listener.portOrPath = chromeDebuggingPort;
listener.webSocket = chromeDebuggingWebSocket;
listener.open();
dumpn("Finished initializing the chrome toolbox server.");
dumpn("Started listening on port: " + chromeDebuggingPort);
},
/**
* Initializes a profile for the remote debugger process.
*/
_initProfile: function () {
dumpn("Initializing the chrome toolbox user profile.");
let debuggingProfileDir = Services.dirsvc.get("ProfLD", Ci.nsIFile);
debuggingProfileDir.append(CHROME_DEBUGGER_PROFILE_NAME);
try {
debuggingProfileDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
} catch (ex) {
// Don't re-copy over the prefs again if this profile already exists
if (ex.result === Cr.NS_ERROR_FILE_ALREADY_EXISTS) {
this._dbgProfilePath = debuggingProfileDir.path;
} else {
dumpn("Error trying to create a profile directory, failing.");
dumpn("Error: " + (ex.message || ex));
}
return;
}
this._dbgProfilePath = debuggingProfileDir.path;
// We would like to copy prefs into this new profile...
let prefsFile = debuggingProfileDir.clone();
prefsFile.append("prefs.js");
// ... but unfortunately, when we run tests, it seems the starting profile
// clears out the prefs file before re-writing it, and in practice the
// file is empty when we get here. So just copying doesn't work in that
// case.
// We could force a sync pref flush and then copy it... but if we're doing
// that, we might as well just flush directly to the new profile, which
// always works:
Services.prefs.savePrefFile(prefsFile);
dumpn("Finished creating the chrome toolbox user profile at: " + this._dbgProfilePath);
},
/**
* Creates and initializes the profile & process for the remote debugger.
*/
_create: function () {
dumpn("Initializing chrome debugging process.");
let process = this._dbgProcess = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
process.init(Services.dirsvc.get("XREExeF", Ci.nsIFile));
let xulURI = DBG_XUL;
if (this._options.addonID) {
xulURI += "?addonID=" + this._options.addonID;
}
dumpn("Running chrome debugging process.");
let args = ["-no-remote", "-foreground", "-profile", this._dbgProfilePath, "-chrome", xulURI];
// During local development, incremental builds can trigger the main process
// to clear its startup cache with the "flag file" .purgecaches, but this
// file is removed during app startup time, so we aren't able to know if it
// was present in order to also clear the child profile's startup cache as
// well.
//
// As an approximation of "isLocalBuild", check for an unofficial build.
if (!Services.appinfo.isOfficial) {
args.push("-purgecaches");
}
// Disable safe mode for the new process in case this was opened via the
// keyboard shortcut.
let nsIEnvironment = Components.classes["@mozilla.org/process/environment;1"].getService(Components.interfaces.nsIEnvironment);
let originalValue = nsIEnvironment.get("MOZ_DISABLE_SAFE_MODE_KEY");
nsIEnvironment.set("MOZ_DISABLE_SAFE_MODE_KEY", "1");
process.runwAsync(args, args.length, { observe: () => this.close() });
// Now that the process has started, it's safe to reset the env variable.
nsIEnvironment.set("MOZ_DISABLE_SAFE_MODE_KEY", originalValue);
this._telemetry.toolOpened("jsbrowserdebugger");
dumpn("Chrome toolbox is now running...");
this.emit("run", this);
},
/**
* Closes the remote debugging server and kills the toolbox process.
*/
close: function () {
if (this.closed) {
return;
}
dumpn("Cleaning up the chrome debugging process.");
Services.obs.removeObserver(this.close, "quit-application");
if (this._dbgProcess.isRunning) {
this._dbgProcess.kill();
}
this._telemetry.toolClosed("jsbrowserdebugger");
if (this.debuggerServer) {
this.debuggerServer.off("connectionchange", this.emit);
this.debuggerServer.destroy();
this.debuggerServer = null;
}
dumpn("Chrome toolbox is now closed...");
this.closed = true;
this.emit("close", this);
processes.delete(this);
this._dbgProcess = null;
this._options = null;
if (this.loader) {
this.loader.destroy();
}
this.loader = null;
this._telemetry = null;
}
};
/**
* Helper method for debugging.
* @param string
*/
function dumpn(str) {
if (wantLogging) {
dump("DBG-FRONTEND: " + str + "\n");
}
}
var wantLogging = Services.prefs.getBoolPref("devtools.debugger.log");
Services.prefs.addObserver("devtools.debugger.log", {
observe: (...args) => wantLogging = Services.prefs.getBoolPref(args.pop())
}, false);
Services.obs.notifyObservers(null, "ToolboxProcessLoaded", null);

View file

@ -0,0 +1,61 @@
/* 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";
// Register about:devtools-toolbox which allows to open a devtools toolbox
// in a Firefox tab or a custom html iframe in browser.html
const { Ci, Cu, Cm, components } = require("chrome");
const registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
const Services = require("Services");
const { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
const { nsIAboutModule } = Ci;
function AboutURL() {}
AboutURL.prototype = {
uri: Services.io.newURI("chrome://devtools/content/framework/toolbox.xul",
null, null),
classDescription: "about:devtools-toolbox",
classID: components.ID("11342911-3135-45a8-8d71-737a2b0ad469"),
contractID: "@mozilla.org/network/protocol/about;1?what=devtools-toolbox",
QueryInterface: XPCOMUtils.generateQI([nsIAboutModule]),
newChannel: function (aURI, aLoadInfo) {
let chan = Services.io.newChannelFromURIWithLoadInfo(this.uri, aLoadInfo);
chan.owner = Services.scriptSecurityManager.getSystemPrincipal();
return chan;
},
getURIFlags: function (aURI) {
return nsIAboutModule.ALLOW_SCRIPT || nsIAboutModule.ENABLE_INDEXED_DB;
}
};
AboutURL.createInstance = function (outer, iid) {
if (outer) {
throw Cr.NS_ERROR_NO_AGGREGATION;
}
return new AboutURL();
};
exports.register = function () {
if (registrar.isCIDRegistered(AboutURL.prototype.classID)) {
console.error("Trying to register " + AboutURL.prototype.classDescription +
" more than once.");
} else {
registrar.registerFactory(AboutURL.prototype.classID,
AboutURL.prototype.classDescription,
AboutURL.prototype.contractID,
AboutURL);
}
};
exports.unregister = function () {
if (registrar.isCIDRegistered(AboutURL.prototype.classID)) {
registrar.unregisterFactory(AboutURL.prototype.classID, AboutURL);
}
};

View file

@ -0,0 +1,115 @@
/* -*- 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/. */
const {Cc, Ci, Cu} = require("chrome");
const Services = require("Services");
const defer = require("devtools/shared/defer");
const {LocalizationHelper} = require("devtools/shared/l10n");
const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
function handleThreadState(toolbox, event, packet) {
// Suppress interrupted events by default because the thread is
// paused/resumed a lot for various actions.
if (event !== "paused" || packet.why.type !== "interrupted") {
// TODO: Bug 1225492, we continue emitting events on the target
// like we used to, but we should emit these only on the
// threadClient now.
toolbox.target.emit("thread-" + event);
}
if (event === "paused") {
toolbox.highlightTool("jsdebugger");
if (packet.why.type === "debuggerStatement" ||
packet.why.type === "breakpoint" ||
packet.why.type === "exception") {
toolbox.raise();
toolbox.selectTool("jsdebugger");
}
} else if (event === "resumed") {
toolbox.unhighlightTool("jsdebugger");
}
}
function attachThread(toolbox) {
let deferred = defer();
let target = toolbox.target;
let { form: { chromeDebugger, actor } } = target;
// Sourcemaps are always turned off when using the new debugger
// frontend. This is because it does sourcemapping on the
// client-side, so the server should not do it. It also does not support
// blackboxing yet.
let useSourceMaps = false;
let autoBlackBox = false;
if(!Services.prefs.getBoolPref("devtools.debugger.new-debugger-frontend")) {
useSourceMaps = Services.prefs.getBoolPref("devtools.debugger.source-maps-enabled");
autoBlackBox = Services.prefs.getBoolPref("devtools.debugger.auto-black-box");
}
let threadOptions = { useSourceMaps, autoBlackBox };
let handleResponse = (res, threadClient) => {
if (res.error) {
deferred.reject(new Error("Couldn't attach to thread: " + res.error));
return;
}
threadClient.addListener("paused", handleThreadState.bind(null, toolbox));
threadClient.addListener("resumed", handleThreadState.bind(null, toolbox));
if (!threadClient.paused) {
deferred.reject(
new Error("Thread in wrong state when starting up, should be paused")
);
}
// These flags need to be set here because the client sends them
// with the `resume` request. We make sure to do this before
// resuming to avoid another interrupt. We can't pass it in with
// `threadOptions` because the resume request will override them.
threadClient.pauseOnExceptions(
Services.prefs.getBoolPref("devtools.debugger.pause-on-exceptions"),
Services.prefs.getBoolPref("devtools.debugger.ignore-caught-exceptions")
);
threadClient.resume(res => {
if (res.error === "wrongOrder") {
const box = toolbox.getNotificationBox();
box.appendNotification(
L10N.getStr("toolbox.resumeOrderWarning"),
"wrong-resume-order",
"",
box.PRIORITY_WARNING_HIGH
);
}
deferred.resolve(threadClient);
});
};
if (target.isTabActor) {
// Attaching a tab, a browser process, or a WebExtensions add-on.
target.activeTab.attachThread(threadOptions, handleResponse);
} else if (target.isAddon) {
// Attaching a legacy addon.
target.client.attachAddon(actor, res => {
target.client.attachThread(res.threadActor, handleResponse);
});
} else {
// Attaching an old browser debugger or a content process.
target.client.attachThread(chromeDebugger, handleResponse);
}
return deferred.promise;
}
function detachThread(threadClient) {
threadClient.removeListener("paused");
threadClient.removeListener("resumed");
}
module.exports = { attachThread, detachThread };

View file

@ -0,0 +1,390 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
/**
* This module inject dynamically menu items and key shortcuts into browser UI.
*
* Menu and shortcut definitions are fetched from:
* - devtools/client/menus for top level entires
* - devtools/client/definitions for tool-specifics entries
*/
const {LocalizationHelper} = require("devtools/shared/l10n");
const MENUS_L10N = new LocalizationHelper("devtools/client/locales/menus.properties");
loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true);
loader.lazyRequireGetter(this, "gDevToolsBrowser", "devtools/client/framework/devtools-browser", true);
// Keep list of inserted DOM Elements in order to remove them on unload
// Maps browser xul document => list of DOM Elements
const FragmentsCache = new Map();
function l10n(key) {
return MENUS_L10N.getStr(key);
}
/**
* Create a xul:key element
*
* @param {XULDocument} doc
* The document to which keys are to be added.
* @param {String} id
* key's id, automatically prefixed with "key_".
* @param {String} shortcut
* The key shortcut value.
* @param {String} keytext
* If `shortcut` refers to a function key, refers to the localized
* string to describe a non-character shortcut.
* @param {String} modifiers
* Space separated list of modifier names.
* @param {Function} oncommand
* The function to call when the shortcut is pressed.
*
* @return XULKeyElement
*/
function createKey({ doc, id, shortcut, keytext, modifiers, oncommand }) {
let k = doc.createElement("key");
k.id = "key_" + id;
if (shortcut.startsWith("VK_")) {
k.setAttribute("keycode", shortcut);
if (keytext) {
k.setAttribute("keytext", keytext);
}
} else {
k.setAttribute("key", shortcut);
}
if (modifiers) {
k.setAttribute("modifiers", modifiers);
}
// Bug 371900: command event is fired only if "oncommand" attribute is set.
k.setAttribute("oncommand", ";");
k.addEventListener("command", oncommand);
return k;
}
/**
* Create a xul:menuitem element
*
* @param {XULDocument} doc
* The document to which keys are to be added.
* @param {String} id
* Element id.
* @param {String} label
* Menu label.
* @param {String} accesskey (optional)
* Access key of the menuitem, used as shortcut while opening the menu.
* @param {Boolean} isCheckbox (optional)
* If true, the menuitem will act as a checkbox and have an optional
* tick on its left.
*
* @return XULMenuItemElement
*/
function createMenuItem({ doc, id, label, accesskey, isCheckbox }) {
let menuitem = doc.createElement("menuitem");
menuitem.id = id;
menuitem.setAttribute("label", label);
if (accesskey) {
menuitem.setAttribute("accesskey", accesskey);
}
if (isCheckbox) {
menuitem.setAttribute("type", "checkbox");
menuitem.setAttribute("autocheck", "false");
}
return menuitem;
}
/**
* Add a <key> to <keyset id="devtoolsKeyset">.
* Appending a <key> element is not always enough. The <keyset> needs
* to be detached and reattached to make sure the <key> is taken into
* account (see bug 832984).
*
* @param {XULDocument} doc
* The document to which keys are to be added
* @param {XULElement} or {DocumentFragment} keys
* Keys to add
*/
function attachKeybindingsToBrowser(doc, keys) {
let devtoolsKeyset = doc.getElementById("devtoolsKeyset");
if (!devtoolsKeyset) {
devtoolsKeyset = doc.createElement("keyset");
devtoolsKeyset.setAttribute("id", "devtoolsKeyset");
}
devtoolsKeyset.appendChild(keys);
let mainKeyset = doc.getElementById("mainKeyset");
mainKeyset.parentNode.insertBefore(devtoolsKeyset, mainKeyset);
}
/**
* Add a menu entry for a tool definition
*
* @param {Object} toolDefinition
* Tool definition of the tool to add a menu entry.
* @param {XULDocument} doc
* The document to which the tool menu item is to be added.
*/
function createToolMenuElements(toolDefinition, doc) {
let id = toolDefinition.id;
let menuId = "menuitem_" + id;
// Prevent multiple entries for the same tool.
if (doc.getElementById(menuId)) {
return;
}
let oncommand = function (id, event) {
let window = event.target.ownerDocument.defaultView;
gDevToolsBrowser.selectToolCommand(window.gBrowser, id);
}.bind(null, id);
let key = null;
if (toolDefinition.key) {
key = createKey({
doc,
id,
shortcut: toolDefinition.key,
modifiers: toolDefinition.modifiers,
oncommand: oncommand
});
}
let menuitem = createMenuItem({
doc,
id: "menuitem_" + id,
label: toolDefinition.menuLabel || toolDefinition.label,
accesskey: toolDefinition.accesskey
});
if (key) {
// Refer to the key in order to display the key shortcut at menu ends
menuitem.setAttribute("key", key.id);
}
menuitem.addEventListener("command", oncommand);
return {
key,
menuitem
};
}
/**
* Create xul menuitem, key elements for a given tool.
* And then insert them into browser DOM.
*
* @param {XULDocument} doc
* The document to which the tool is to be registered.
* @param {Object} toolDefinition
* Tool definition of the tool to register.
* @param {Object} prevDef
* The tool definition after which the tool menu item is to be added.
*/
function insertToolMenuElements(doc, toolDefinition, prevDef) {
let { key, menuitem } = createToolMenuElements(toolDefinition, doc);
if (key) {
attachKeybindingsToBrowser(doc, key);
}
let ref;
if (prevDef) {
let menuitem = doc.getElementById("menuitem_" + prevDef.id);
ref = menuitem && menuitem.nextSibling ? menuitem.nextSibling : null;
} else {
ref = doc.getElementById("menu_devtools_separator");
}
if (ref) {
ref.parentNode.insertBefore(menuitem, ref);
}
}
exports.insertToolMenuElements = insertToolMenuElements;
/**
* Remove a tool's menuitem from a window
*
* @param {string} toolId
* Id of the tool to add a menu entry for
* @param {XULDocument} doc
* The document to which the tool menu item is to be removed from
*/
function removeToolFromMenu(toolId, doc) {
let key = doc.getElementById("key_" + toolId);
if (key) {
key.remove();
}
let menuitem = doc.getElementById("menuitem_" + toolId);
if (menuitem) {
menuitem.remove();
}
}
exports.removeToolFromMenu = removeToolFromMenu;
/**
* Add all tools to the developer tools menu of a window.
*
* @param {XULDocument} doc
* The document to which the tool items are to be added.
*/
function addAllToolsToMenu(doc) {
let fragKeys = doc.createDocumentFragment();
let fragMenuItems = doc.createDocumentFragment();
for (let toolDefinition of gDevTools.getToolDefinitionArray()) {
if (!toolDefinition.inMenu) {
continue;
}
let elements = createToolMenuElements(toolDefinition, doc);
if (!elements) {
continue;
}
if (elements.key) {
fragKeys.appendChild(elements.key);
}
fragMenuItems.appendChild(elements.menuitem);
}
attachKeybindingsToBrowser(doc, fragKeys);
let mps = doc.getElementById("menu_devtools_separator");
if (mps) {
mps.parentNode.insertBefore(fragMenuItems, mps);
}
}
/**
* Add global menus and shortcuts that are not panel specific.
*
* @param {XULDocument} doc
* The document to which keys and menus are to be added.
*/
function addTopLevelItems(doc) {
let keys = doc.createDocumentFragment();
let menuItems = doc.createDocumentFragment();
let { menuitems } = require("../menus");
for (let item of menuitems) {
if (item.separator) {
let separator = doc.createElement("menuseparator");
separator.id = item.id;
menuItems.appendChild(separator);
} else {
let { id, l10nKey } = item;
// Create a <menuitem>
let menuitem = createMenuItem({
doc,
id,
label: l10n(l10nKey + ".label"),
accesskey: l10n(l10nKey + ".accesskey"),
isCheckbox: item.checkbox
});
menuitem.addEventListener("command", item.oncommand);
menuItems.appendChild(menuitem);
if (item.key && l10nKey) {
// Create a <key>
let shortcut = l10n(l10nKey + ".key");
let key = createKey({
doc,
id: item.key.id,
shortcut: shortcut,
keytext: shortcut.startsWith("VK_") ? l10n(l10nKey + ".keytext") : null,
modifiers: item.key.modifiers,
oncommand: item.oncommand
});
// Refer to the key in order to display the key shortcut at menu ends
menuitem.setAttribute("key", key.id);
keys.appendChild(key);
}
if (item.additionalKeys) {
// Create additional <key>
for (let key of item.additionalKeys) {
let shortcut = l10n(key.l10nKey + ".key");
let node = createKey({
doc,
id: key.id,
shortcut: shortcut,
keytext: shortcut.startsWith("VK_") ? l10n(key.l10nKey + ".keytext") : null,
modifiers: key.modifiers,
oncommand: item.oncommand
});
keys.appendChild(node);
}
}
}
}
// Cache all nodes before insertion to be able to remove them on unload
let nodes = [];
for (let node of keys.children) {
nodes.push(node);
}
for (let node of menuItems.children) {
nodes.push(node);
}
FragmentsCache.set(doc, nodes);
attachKeybindingsToBrowser(doc, keys);
let menu = doc.getElementById("menuWebDeveloperPopup");
menu.appendChild(menuItems);
// There is still "Page Source" menuitem hardcoded into browser.xul. Instead
// of manually inserting everything around it, move it to the expected
// position.
let pageSource = doc.getElementById("menu_pageSource");
let endSeparator = doc.getElementById("devToolsEndSeparator");
menu.insertBefore(pageSource, endSeparator);
}
/**
* Remove global menus and shortcuts that are not panel specific.
*
* @param {XULDocument} doc
* The document to which keys and menus are to be added.
*/
function removeTopLevelItems(doc) {
let nodes = FragmentsCache.get(doc);
if (!nodes) {
return;
}
FragmentsCache.delete(doc);
for (let node of nodes) {
node.remove();
}
}
/**
* Add menus and shortcuts to a browser document
*
* @param {XULDocument} doc
* The document to which keys and menus are to be added.
*/
exports.addMenus = function (doc) {
addTopLevelItems(doc);
addAllToolsToMenu(doc);
};
/**
* Remove menus and shortcuts from a browser document
*
* @param {XULDocument} doc
* The document to which keys and menus are to be removed.
*/
exports.removeMenus = function (doc) {
// We only remove top level entries. Per-tool entries are removed while
// unregistering each tool.
removeTopLevelItems(doc);
};

View file

@ -0,0 +1,112 @@
:root {
font: caption;
}
html {
background-color: #111;
}
body {
font-family: Arial, sans-serif;
color: white;
max-width: 600px;
margin: 30px auto 0;
box-shadow: 0 2px 3px black;
background-color: #3C3E40;
}
h1 {
margin: 0;
padding: 20px;
background-color: rgba(0,0,0,0.12);
background-image: radial-gradient(ellipse farthest-corner at center top , rgb(159, 223, 255), rgba(101, 203, 255, 0.3)), radial-gradient(ellipse farthest-side at center top , rgba(101, 203, 255, 0.4), rgba(101, 203, 255, 0));
background-size: 100% 2px, 100% 5px;
background-repeat: no-repeat;
border-bottom: 1px solid rgba(0,0,0,0.1);
}
form {
display: inline-block;
}
label {
display: block;
margin: 10px;
}
label > span {
display: inline-block;
min-width: 150px;
text-align: right;
margin-right: 10px;
}
#submit {
float: right;
}
input:invalid {
box-shadow: 0 0 2px 2px #F06;
}
section {
min-height: 160px;
margin: 60px 20px;
display: none; /* By default, hidden */
}
.error-message {
color: red;
}
.error-message:not(.active) {
display: none;
}
body:not(.actors-mode):not(.connecting) > #connection-form {
display: block;
}
body.actors-mode > #actors-list {
display: block;
}
body.connecting > #connecting {
display: block;
}
#connecting {
text-align: center;
}
#connecting > p > img {
vertical-align: top;
}
.actors {
padding-left: 0;
}
.actors > a {
display: block;
margin: 5px;
padding: 5px;
color: white;
}
.remote-process {
font-style: italic;
opacity: 0.8;
}
footer {
padding: 10px;
background-color: rgba(0,0,0,0.12);
border-top: 1px solid rgba(0,0,0,0.1);
font-size: small;
}
footer > a,
footer > a:visited {
color: white;
}

View file

@ -0,0 +1,236 @@
/* -*- 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";
var Cu = Components.utils;
var {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
var Services = require("Services");
var {gDevTools} = require("devtools/client/framework/devtools");
var {TargetFactory} = require("devtools/client/framework/target");
var {Toolbox} = require("devtools/client/framework/toolbox");
var {DebuggerClient} = require("devtools/shared/client/main");
var {Task} = require("devtools/shared/task");
var {LocalizationHelper} = require("devtools/shared/l10n");
var L10N = new LocalizationHelper("devtools/client/locales/connection-screen.properties");
var gClient;
var gConnectionTimeout;
/**
* Once DOM is ready, we prefil the host/port inputs with
* pref-stored values.
*/
window.addEventListener("DOMContentLoaded", function onDOMReady() {
window.removeEventListener("DOMContentLoaded", onDOMReady, true);
let host = Services.prefs.getCharPref("devtools.debugger.remote-host");
let port = Services.prefs.getIntPref("devtools.debugger.remote-port");
if (host) {
document.getElementById("host").value = host;
}
if (port) {
document.getElementById("port").value = port;
}
let form = document.querySelector("#connection-form form");
form.addEventListener("submit", function () {
window.submit().catch(e => {
console.error(e);
// Bug 921850: catch rare exception from DebuggerClient.socketConnect
showError("unexpected");
});
});
}, true);
/**
* Called when the "connect" button is clicked.
*/
var submit = Task.async(function* () {
// Show the "connecting" screen
document.body.classList.add("connecting");
let host = document.getElementById("host").value;
let port = document.getElementById("port").value;
// Save the host/port values
try {
Services.prefs.setCharPref("devtools.debugger.remote-host", host);
Services.prefs.setIntPref("devtools.debugger.remote-port", port);
} catch (e) {
// Fails in e10s mode, but not a critical feature.
}
// Initiate the connection
let transport = yield DebuggerClient.socketConnect({ host, port });
gClient = new DebuggerClient(transport);
let delay = Services.prefs.getIntPref("devtools.debugger.remote-timeout");
gConnectionTimeout = setTimeout(handleConnectionTimeout, delay);
let response = yield gClient.connect();
yield onConnectionReady(...response);
});
/**
* Connection is ready. List actors and build buttons.
*/
var onConnectionReady = Task.async(function* ([aType, aTraits]) {
clearTimeout(gConnectionTimeout);
let response = yield gClient.listAddons();
let parent = document.getElementById("addonActors");
if (!response.error && response.addons.length > 0) {
// Add one entry for each add-on.
for (let addon of response.addons) {
if (!addon.debuggable) {
continue;
}
buildAddonLink(addon, parent);
}
}
else {
// Hide the section when there are no add-ons
parent.previousElementSibling.remove();
parent.remove();
}
response = yield gClient.listTabs();
parent = document.getElementById("tabActors");
// Add Global Process debugging...
let globals = Cu.cloneInto(response, {});
delete globals.tabs;
delete globals.selected;
// ...only if there are appropriate actors (a 'from' property will always
// be there).
// Add one entry for each open tab.
for (let i = 0; i < response.tabs.length; i++) {
buildTabLink(response.tabs[i], parent, i == response.selected);
}
let gParent = document.getElementById("globalActors");
// Build the Remote Process button
// If Fx<39, tab actors were used to be exposed on RootActor
// but in Fx>=39, chrome is debuggable via getProcess() and ChromeActor
if (globals.consoleActor || gClient.mainRoot.traits.allowChromeProcess) {
let a = document.createElement("a");
a.onclick = function () {
if (gClient.mainRoot.traits.allowChromeProcess) {
gClient.getProcess()
.then(aResponse => {
openToolbox(aResponse.form, true);
});
} else if (globals.consoleActor) {
openToolbox(globals, true, "webconsole", false);
}
};
a.title = a.textContent = L10N.getStr("mainProcess");
a.className = "remote-process";
a.href = "#";
gParent.appendChild(a);
}
// Move the selected tab on top
let selectedLink = parent.querySelector("a.selected");
if (selectedLink) {
parent.insertBefore(selectedLink, parent.firstChild);
}
document.body.classList.remove("connecting");
document.body.classList.add("actors-mode");
// Ensure the first link is focused
let firstLink = parent.querySelector("a:first-of-type");
if (firstLink) {
firstLink.focus();
}
});
/**
* Build one button for an add-on actor.
*/
function buildAddonLink(addon, parent) {
let a = document.createElement("a");
a.onclick = function () {
openToolbox(addon, true, "jsdebugger", false);
};
a.textContent = addon.name;
a.title = addon.id;
a.href = "#";
parent.appendChild(a);
}
/**
* Build one button for a tab actor.
*/
function buildTabLink(tab, parent, selected) {
let a = document.createElement("a");
a.onclick = function () {
openToolbox(tab);
};
a.textContent = tab.title;
a.title = tab.url;
if (!a.textContent) {
a.textContent = tab.url;
}
a.href = "#";
if (selected) {
a.classList.add("selected");
}
parent.appendChild(a);
}
/**
* An error occured. Let's show it and return to the first screen.
*/
function showError(type) {
document.body.className = "error";
let activeError = document.querySelector(".error-message.active");
if (activeError) {
activeError.classList.remove("active");
}
activeError = document.querySelector(".error-" + type);
if (activeError) {
activeError.classList.add("active");
}
}
/**
* Connection timeout.
*/
function handleConnectionTimeout() {
showError("timeout");
}
/**
* The user clicked on one of the buttons.
* Opens the toolbox.
*/
function openToolbox(form, chrome = false, tool = "webconsole", isTabActor) {
let options = {
form: form,
client: gClient,
chrome: chrome,
isTabActor: isTabActor
};
TargetFactory.forRemoteTab(options).then((target) => {
let hostType = Toolbox.HostType.WINDOW;
gDevTools.showToolbox(target, tool, hostType).then((toolbox) => {
toolbox.once("destroyed", function () {
gClient.close();
});
}, console.error.bind(console));
window.close();
}, console.error.bind(console));
}

View file

@ -0,0 +1,52 @@
<?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/. -->
<!DOCTYPE html [
<!ENTITY % connectionDTD SYSTEM "chrome://devtools/locale/connection-screen.dtd" >
%connectionDTD;
]>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<head>
<title>&title;</title>
<link rel="stylesheet" href="chrome://devtools/skin/dark-theme.css" type="text/css"/>
<link rel="stylesheet" href="chrome://devtools/content/framework/connect/connect.css" type="text/css"/>
<script type="application/javascript;version=1.8" src="connect.js"></script>
</head>
<body>
<h1>&header;</h1>
<section id="connection-form">
<form validate="validate" action="#">
<label>
<span>&host;</span>
<input required="required" class="devtools-textinput" id="host" type="text"></input>
</label>
<label>
<span>&port;</span>
<input required="required" class="devtools-textinput" id="port" type="number" pattern="\d+"></input>
</label>
<label>
<input class="devtools-toolbarbutton" id="submit" standalone="true" type="submit" value="&connect;"></input>
</label>
</form>
<p class="error-message error-timeout">&errorTimeout;</p>
<p class="error-message error-refused">&errorRefused;</p>
<p class="error-message error-unexpected">&errorUnexpected;</p>
</section>
<section id="actors-list">
<p>&availableTabs;</p>
<ul class="actors" id="tabActors"></ul>
<p>&availableAddons;</p>
<ul class="actors" id="addonActors"></ul>
<p>&availableProcesses;</p>
<ul class="actors" id="globalActors"></ul>
</section>
<section id="connecting">
<p class="devtools-throbber">&connecting;</p>
</section>
<footer>&remoteHelp;<a target='_' href='https://developer.mozilla.org/docs/Tools/Remote_Debugging'>&remoteDocumentation;</a>&remoteHelpSuffix;</footer>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

View file

@ -0,0 +1,94 @@
/* 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/. */
window {
-moz-appearance: none;
background-color: transparent;
}
#doorhanger-container {
width: 450px;
}
#top-panel {
padding: 20px;
background: #343c45; /* toolbars */
color: #8fa1b2; /* body text */
/*
* Sloppy preprocessing since UNIX_BUT_NOT_MAC is only defined
* in `browser/app/profile/firefox.js`, which this file cannot
* depend on. Must style font-size to target linux.
*/
%ifdef XP_UNIX
%ifndef XP_MACOSX
font-size: 13px;
%else
font-size: 15px;
%endif
%else
font-size: 15px;
%endif
line-height: 19px;
min-height: 100px;
}
#top-panel h1 {
font-weight: bold;
font-family: Open Sans, sans-serif;
font-size: 1.1em;
}
#top-panel p {
font-family: Open Sans, sans-serif;
font-size: 0.9em;
width: 300px;
display: block;
margin: 5px 0px 0px 0px;
}
#icon {
background-image: url("chrome://devtools/content/framework/dev-edition-promo/dev-edition-logo.png");
background-size: 64px 64px;
background-repeat: no-repeat;
width: 64px;
height: 64px;
margin-right: 20px;
}
#lower-panel {
padding: 20px;
background-color: #252c33; /* tab toolbars */
min-height: 75px;
border-top: 1px solid #292e33; /* text high contrast (light) */
}
#button-container {
margin: auto 20px;
}
#button-container button {
font: message-box !important;
font-size: 16px !important;
cursor: pointer;
width: 125px;
opacity: 1;
position: static;
-moz-appearance: none;
border-radius: 5px;
height: 30px;
width: 450px;
/* Override embossed borders on Windows/Linux */
border: none;
}
#close {
background-color: transparent;
color: #8fa1b2; /* body text */
}
#go {
margin-left: 100px;
background-color: #70bf53; /* green */
color: #f5f7fa; /* selection text color */
}

View file

@ -0,0 +1,36 @@
<?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/. -->
<!DOCTYPE window [
<!ENTITY % toolboxDTD SYSTEM "chrome://devtools/locale/toolbox.dtd" >
%toolboxDTD;
]>
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
<?xml-stylesheet rel="stylesheet" href="chrome://devtools/content/framework/dev-edition-promo/dev-edition-promo.css" type="text/css"?>
<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" id="dev-edition-promo">
<vbox id="doorhanger-container">
<hbox flex="1" id="top-panel">
<image id="icon" />
<vbox id="info">
<h1>Using Developer Tools in your browser?</h1>
<p>Download Firefox Developer Edition, our first browser made just for you.</p>
</vbox>
</hbox>
<hbox id="lower-panel" flex="1">
<hbox id="button-container" flex="1">
<button id="close"
flex="1"
standalone="true"
label="No thanks">
</button>
<button id="go"
flex="1"
standalone="true"
label="Learn more »">
</button>
</hbox>
</hbox>
</vbox>
</window>

View file

@ -0,0 +1,758 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
/**
* This is the main module loaded in Firefox desktop that handles browser
* windows and coordinates devtools around each window.
*
* This module is loaded lazily by devtools-clhandler.js, once the first
* browser window is ready (i.e. fired browser-delayed-startup-finished event)
**/
const {Cc, Ci, Cu} = require("chrome");
const Services = require("Services");
const promise = require("promise");
const defer = require("devtools/shared/defer");
const Telemetry = require("devtools/client/shared/telemetry");
const { gDevTools } = require("./devtools");
const { when: unload } = require("sdk/system/unload");
// Load target and toolbox lazily as they need gDevTools to be fully initialized
loader.lazyRequireGetter(this, "TargetFactory", "devtools/client/framework/target", true);
loader.lazyRequireGetter(this, "Toolbox", "devtools/client/framework/toolbox", true);
loader.lazyRequireGetter(this, "DebuggerServer", "devtools/server/main", true);
loader.lazyRequireGetter(this, "DebuggerClient", "devtools/shared/client/main", true);
loader.lazyRequireGetter(this, "BrowserMenus", "devtools/client/framework/browser-menus");
loader.lazyImporter(this, "CustomizableUI", "resource:///modules/CustomizableUI.jsm");
loader.lazyImporter(this, "AppConstants", "resource://gre/modules/AppConstants.jsm");
const {LocalizationHelper} = require("devtools/shared/l10n");
const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
const TABS_OPEN_PEAK_HISTOGRAM = "DEVTOOLS_TABS_OPEN_PEAK_LINEAR";
const TABS_OPEN_AVG_HISTOGRAM = "DEVTOOLS_TABS_OPEN_AVERAGE_LINEAR";
const TABS_PINNED_PEAK_HISTOGRAM = "DEVTOOLS_TABS_PINNED_PEAK_LINEAR";
const TABS_PINNED_AVG_HISTOGRAM = "DEVTOOLS_TABS_PINNED_AVERAGE_LINEAR";
/**
* gDevToolsBrowser exposes functions to connect the gDevTools instance with a
* Firefox instance.
*/
var gDevToolsBrowser = exports.gDevToolsBrowser = {
/**
* A record of the windows whose menus we altered, so we can undo the changes
* as the window is closed
*/
_trackedBrowserWindows: new Set(),
_telemetry: new Telemetry(),
_tabStats: {
peakOpen: 0,
peakPinned: 0,
histOpen: [],
histPinned: []
},
/**
* This function is for the benefit of Tools:DevToolbox in
* browser/base/content/browser-sets.inc and should not be used outside
* of there
*/
// used by browser-sets.inc, command
toggleToolboxCommand: function (gBrowser) {
let target = TargetFactory.forTab(gBrowser.selectedTab);
let toolbox = gDevTools.getToolbox(target);
// If a toolbox exists, using toggle from the Main window :
// - should close a docked toolbox
// - should focus a windowed toolbox
let isDocked = toolbox && toolbox.hostType != Toolbox.HostType.WINDOW;
isDocked ? gDevTools.closeToolbox(target) : gDevTools.showToolbox(target);
},
/**
* This function ensures the right commands are enabled in a window,
* depending on their relevant prefs. It gets run when a window is registered,
* or when any of the devtools prefs change.
*/
updateCommandAvailability: function (win) {
let doc = win.document;
function toggleMenuItem(id, isEnabled) {
let cmd = doc.getElementById(id);
if (isEnabled) {
cmd.removeAttribute("disabled");
cmd.removeAttribute("hidden");
} else {
cmd.setAttribute("disabled", "true");
cmd.setAttribute("hidden", "true");
}
}
// Enable developer toolbar?
let devToolbarEnabled = Services.prefs.getBoolPref("devtools.toolbar.enabled");
toggleMenuItem("menu_devToolbar", devToolbarEnabled);
let focusEl = doc.getElementById("menu_devToolbar");
if (devToolbarEnabled) {
focusEl.removeAttribute("disabled");
} else {
focusEl.setAttribute("disabled", "true");
}
if (devToolbarEnabled && Services.prefs.getBoolPref("devtools.toolbar.visible")) {
win.DeveloperToolbar.show(false).catch(console.error);
}
// Enable WebIDE?
let webIDEEnabled = Services.prefs.getBoolPref("devtools.webide.enabled");
toggleMenuItem("menu_webide", webIDEEnabled);
let showWebIDEWidget = Services.prefs.getBoolPref("devtools.webide.widget.enabled");
if (webIDEEnabled && showWebIDEWidget) {
gDevToolsBrowser.installWebIDEWidget();
} else {
gDevToolsBrowser.uninstallWebIDEWidget();
}
// Enable Browser Toolbox?
let chromeEnabled = Services.prefs.getBoolPref("devtools.chrome.enabled");
let devtoolsRemoteEnabled = Services.prefs.getBoolPref("devtools.debugger.remote-enabled");
let remoteEnabled = chromeEnabled && devtoolsRemoteEnabled;
toggleMenuItem("menu_browserToolbox", remoteEnabled);
toggleMenuItem("menu_browserContentToolbox", remoteEnabled && win.gMultiProcessBrowser);
// Enable DevTools connection screen, if the preference allows this.
toggleMenuItem("menu_devtools_connect", devtoolsRemoteEnabled);
},
observe: function (subject, topic, prefName) {
switch (topic) {
case "browser-delayed-startup-finished":
this._registerBrowserWindow(subject);
break;
case "nsPref:changed":
if (prefName.endsWith("enabled")) {
for (let win of this._trackedBrowserWindows) {
this.updateCommandAvailability(win);
}
}
break;
}
},
_prefObserverRegistered: false,
ensurePrefObserver: function () {
if (!this._prefObserverRegistered) {
this._prefObserverRegistered = true;
Services.prefs.addObserver("devtools.", this, false);
}
},
/**
* This function is for the benefit of Tools:{toolId} commands,
* triggered from the WebDeveloper menu and keyboard shortcuts.
*
* selectToolCommand's behavior:
* - if the toolbox is closed,
* we open the toolbox and select the tool
* - if the toolbox is open, and the targeted tool is not selected,
* we select it
* - if the toolbox is open, and the targeted tool is selected,
* and the host is NOT a window, we close the toolbox
* - if the toolbox is open, and the targeted tool is selected,
* and the host is a window, we raise the toolbox window
*/
// Used when: - registering a new tool
// - new xul window, to add menu items
selectToolCommand: function (gBrowser, toolId) {
let target = TargetFactory.forTab(gBrowser.selectedTab);
let toolbox = gDevTools.getToolbox(target);
let toolDefinition = gDevTools.getToolDefinition(toolId);
if (toolbox &&
(toolbox.currentToolId == toolId ||
(toolId == "webconsole" && toolbox.splitConsole)))
{
toolbox.fireCustomKey(toolId);
if (toolDefinition.preventClosingOnKey || toolbox.hostType == Toolbox.HostType.WINDOW) {
toolbox.raise();
} else {
gDevTools.closeToolbox(target);
}
gDevTools.emit("select-tool-command", toolId);
} else {
gDevTools.showToolbox(target, toolId).then(() => {
let target = TargetFactory.forTab(gBrowser.selectedTab);
let toolbox = gDevTools.getToolbox(target);
toolbox.fireCustomKey(toolId);
gDevTools.emit("select-tool-command", toolId);
});
}
},
/**
* Open a tab on "about:debugging", optionally pre-select a given tab.
*/
// Used by browser-sets.inc, command
openAboutDebugging: function (gBrowser, hash) {
let url = "about:debugging" + (hash ? "#" + hash : "");
gBrowser.selectedTab = gBrowser.addTab(url);
},
/**
* Open a tab to allow connects to a remote browser
*/
// Used by browser-sets.inc, command
openConnectScreen: function (gBrowser) {
gBrowser.selectedTab = gBrowser.addTab("chrome://devtools/content/framework/connect/connect.xhtml");
},
/**
* Open WebIDE
*/
// Used by browser-sets.inc, command
// itself, webide widget
openWebIDE: function () {
let win = Services.wm.getMostRecentWindow("devtools:webide");
if (win) {
win.focus();
} else {
Services.ww.openWindow(null, "chrome://webide/content/", "webide", "chrome,centerscreen,resizable", null);
}
},
_getContentProcessTarget: function (processId) {
// Create a DebuggerServer in order to connect locally to it
if (!DebuggerServer.initialized) {
DebuggerServer.init();
DebuggerServer.addBrowserActors();
}
DebuggerServer.allowChromeProcess = true;
let transport = DebuggerServer.connectPipe();
let client = new DebuggerClient(transport);
let deferred = defer();
client.connect().then(() => {
client.getProcess(processId)
.then(response => {
let options = {
form: response.form,
client: client,
chrome: true,
isTabActor: false
};
return TargetFactory.forRemoteTab(options);
})
.then(target => {
// Ensure closing the connection in order to cleanup
// the debugger client and also the server created in the
// content process
target.on("close", () => {
client.close();
});
deferred.resolve(target);
});
});
return deferred.promise;
},
// Used by menus.js
openContentProcessToolbox: function (gBrowser) {
let { childCount } = Services.ppmm;
// Get the process message manager for the current tab
let mm = gBrowser.selectedBrowser.messageManager.processMessageManager;
let processId = null;
for (let i = 1; i < childCount; i++) {
let child = Services.ppmm.getChildAt(i);
if (child == mm) {
processId = i;
break;
}
}
if (processId) {
this._getContentProcessTarget(processId)
.then(target => {
// Display a new toolbox, in a new window, with debugger by default
return gDevTools.showToolbox(target, "jsdebugger",
Toolbox.HostType.WINDOW);
});
} else {
let msg = L10N.getStr("toolbox.noContentProcessForTab.message");
Services.prompt.alert(null, "", msg);
}
},
/**
* Install Developer widget
*/
installDeveloperWidget: function () {
let id = "developer-button";
let widget = CustomizableUI.getWidget(id);
if (widget && widget.provider == CustomizableUI.PROVIDER_API) {
return;
}
CustomizableUI.createWidget({
id: id,
type: "view",
viewId: "PanelUI-developer",
shortcutId: "key_devToolboxMenuItem",
tooltiptext: "developer-button.tooltiptext2",
defaultArea: AppConstants.MOZ_DEV_EDITION ?
CustomizableUI.AREA_NAVBAR :
CustomizableUI.AREA_PANEL,
onViewShowing: function (aEvent) {
// Populate the subview with whatever menuitems are in the developer
// menu. We skip menu elements, because the menu panel has no way
// of dealing with those right now.
let doc = aEvent.target.ownerDocument;
let win = doc.defaultView;
let menu = doc.getElementById("menuWebDeveloperPopup");
let itemsToDisplay = [...menu.children];
// Hardcode the addition of the "work offline" menuitem at the bottom:
itemsToDisplay.push({localName: "menuseparator", getAttribute: () => {}});
itemsToDisplay.push(doc.getElementById("goOfflineMenuitem"));
let developerItems = doc.getElementById("PanelUI-developerItems");
// Import private helpers from CustomizableWidgets
let { clearSubview, fillSubviewFromMenuItems } =
Cu.import("resource:///modules/CustomizableWidgets.jsm", {});
clearSubview(developerItems);
fillSubviewFromMenuItems(itemsToDisplay, developerItems);
},
onBeforeCreated: function (doc) {
// Bug 1223127, CUI should make this easier to do.
if (doc.getElementById("PanelUI-developerItems")) {
return;
}
let view = doc.createElement("panelview");
view.id = "PanelUI-developerItems";
let panel = doc.createElement("vbox");
panel.setAttribute("class", "panel-subview-body");
view.appendChild(panel);
doc.getElementById("PanelUI-multiView").appendChild(view);
}
});
},
/**
* Install WebIDE widget
*/
// Used by itself
installWebIDEWidget: function () {
if (this.isWebIDEWidgetInstalled()) {
return;
}
let defaultArea;
if (Services.prefs.getBoolPref("devtools.webide.widget.inNavbarByDefault")) {
defaultArea = CustomizableUI.AREA_NAVBAR;
} else {
defaultArea = CustomizableUI.AREA_PANEL;
}
CustomizableUI.createWidget({
id: "webide-button",
shortcutId: "key_webide",
label: "devtools-webide-button2.label",
tooltiptext: "devtools-webide-button2.tooltiptext",
defaultArea: defaultArea,
onCommand: function (aEvent) {
gDevToolsBrowser.openWebIDE();
}
});
},
isWebIDEWidgetInstalled: function () {
let widgetWrapper = CustomizableUI.getWidget("webide-button");
return !!(widgetWrapper && widgetWrapper.provider == CustomizableUI.PROVIDER_API);
},
/**
* The deferred promise will be resolved by WebIDE's UI.init()
*/
isWebIDEInitialized: defer(),
/**
* Uninstall WebIDE widget
*/
uninstallWebIDEWidget: function () {
if (this.isWebIDEWidgetInstalled()) {
CustomizableUI.removeWidgetFromArea("webide-button");
}
CustomizableUI.destroyWidget("webide-button");
},
/**
* Move WebIDE widget to the navbar
*/
// Used by webide.js
moveWebIDEWidgetInNavbar: function () {
CustomizableUI.addWidgetToArea("webide-button", CustomizableUI.AREA_NAVBAR);
},
/**
* Add this DevTools's presence to a browser window's document
*
* @param {XULDocument} doc
* The document to which devtools should be hooked to.
*/
_registerBrowserWindow: function (win) {
if (gDevToolsBrowser._trackedBrowserWindows.has(win)) {
return;
}
gDevToolsBrowser._trackedBrowserWindows.add(win);
BrowserMenus.addMenus(win.document);
// Register the Developer widget in the Hamburger menu or navbar
// only once menus are registered as it depends on it.
gDevToolsBrowser.installDeveloperWidget();
// Inject lazily DeveloperToolbar on the chrome window
loader.lazyGetter(win, "DeveloperToolbar", function () {
let { DeveloperToolbar } = require("devtools/client/shared/developer-toolbar");
return new DeveloperToolbar(win);
});
this.updateCommandAvailability(win);
this.ensurePrefObserver();
win.addEventListener("unload", this);
let tabContainer = win.gBrowser.tabContainer;
tabContainer.addEventListener("TabSelect", this, false);
tabContainer.addEventListener("TabOpen", this, false);
tabContainer.addEventListener("TabClose", this, false);
tabContainer.addEventListener("TabPinned", this, false);
tabContainer.addEventListener("TabUnpinned", this, false);
},
/**
* Hook the JS debugger tool to the "Debug Script" button of the slow script
* dialog.
*/
setSlowScriptDebugHandler: function DT_setSlowScriptDebugHandler() {
let debugService = Cc["@mozilla.org/dom/slow-script-debug;1"]
.getService(Ci.nsISlowScriptDebug);
let tm = Cc["@mozilla.org/thread-manager;1"].getService(Ci.nsIThreadManager);
function slowScriptDebugHandler(aTab, aCallback) {
let target = TargetFactory.forTab(aTab);
gDevTools.showToolbox(target, "jsdebugger").then(toolbox => {
let threadClient = toolbox.getCurrentPanel().panelWin.gThreadClient;
// Break in place, which means resuming the debuggee thread and pausing
// right before the next step happens.
switch (threadClient.state) {
case "paused":
// When the debugger is already paused.
threadClient.resumeThenPause();
aCallback();
break;
case "attached":
// When the debugger is already open.
threadClient.interrupt(() => {
threadClient.resumeThenPause();
aCallback();
});
break;
case "resuming":
// The debugger is newly opened.
threadClient.addOneTimeListener("resumed", () => {
threadClient.interrupt(() => {
threadClient.resumeThenPause();
aCallback();
});
});
break;
default:
throw Error("invalid thread client state in slow script debug handler: " +
threadClient.state);
}
});
}
debugService.activationHandler = function (aWindow) {
let chromeWindow = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShellTreeItem)
.rootTreeItem
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindow)
.QueryInterface(Ci.nsIDOMChromeWindow);
let setupFinished = false;
slowScriptDebugHandler(chromeWindow.gBrowser.selectedTab,
() => { setupFinished = true; });
// Don't return from the interrupt handler until the debugger is brought
// up; no reason to continue executing the slow script.
let utils = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
utils.enterModalState();
while (!setupFinished) {
tm.currentThread.processNextEvent(true);
}
utils.leaveModalState();
};
debugService.remoteActivationHandler = function (aBrowser, aCallback) {
let chromeWindow = aBrowser.ownerDocument.defaultView;
let tab = chromeWindow.gBrowser.getTabForBrowser(aBrowser);
chromeWindow.gBrowser.selected = tab;
function callback() {
aCallback.finishDebuggerStartup();
}
slowScriptDebugHandler(tab, callback);
};
},
/**
* Unset the slow script debug handler.
*/
unsetSlowScriptDebugHandler: function DT_unsetSlowScriptDebugHandler() {
let debugService = Cc["@mozilla.org/dom/slow-script-debug;1"]
.getService(Ci.nsISlowScriptDebug);
debugService.activationHandler = undefined;
},
/**
* Add the menuitem for a tool to all open browser windows.
*
* @param {object} toolDefinition
* properties of the tool to add
*/
_addToolToWindows: function DT_addToolToWindows(toolDefinition) {
// No menu item or global shortcut is required for options panel.
if (!toolDefinition.inMenu) {
return;
}
// Skip if the tool is disabled.
try {
if (toolDefinition.visibilityswitch &&
!Services.prefs.getBoolPref(toolDefinition.visibilityswitch)) {
return;
}
} catch (e) {}
// We need to insert the new tool in the right place, which means knowing
// the tool that comes before the tool that we're trying to add
let allDefs = gDevTools.getToolDefinitionArray();
let prevDef;
for (let def of allDefs) {
if (!def.inMenu) {
continue;
}
if (def === toolDefinition) {
break;
}
prevDef = def;
}
for (let win of gDevToolsBrowser._trackedBrowserWindows) {
BrowserMenus.insertToolMenuElements(win.document, toolDefinition, prevDef);
}
if (toolDefinition.id === "jsdebugger") {
gDevToolsBrowser.setSlowScriptDebugHandler();
}
},
hasToolboxOpened: function (win) {
let tab = win.gBrowser.selectedTab;
for (let [target, toolbox] of gDevTools._toolboxes) {
if (target.tab == tab) {
return true;
}
}
return false;
},
/**
* Update the "Toggle Tools" checkbox in the developer tools menu. This is
* called when a toolbox is created or destroyed.
*/
_updateMenuCheckbox: function DT_updateMenuCheckbox() {
for (let win of gDevToolsBrowser._trackedBrowserWindows) {
let hasToolbox = gDevToolsBrowser.hasToolboxOpened(win);
let menu = win.document.getElementById("menu_devToolbox");
if (hasToolbox) {
menu.setAttribute("checked", "true");
} else {
menu.removeAttribute("checked");
}
}
},
/**
* Remove the menuitem for a tool to all open browser windows.
*
* @param {string} toolId
* id of the tool to remove
*/
_removeToolFromWindows: function DT_removeToolFromWindows(toolId) {
for (let win of gDevToolsBrowser._trackedBrowserWindows) {
BrowserMenus.removeToolFromMenu(toolId, win.document);
}
if (toolId === "jsdebugger") {
gDevToolsBrowser.unsetSlowScriptDebugHandler();
}
},
/**
* Called on browser unload to remove menu entries, toolboxes and event
* listeners from the closed browser window.
*
* @param {XULWindow} win
* The window containing the menu entry
*/
_forgetBrowserWindow: function (win) {
if (!gDevToolsBrowser._trackedBrowserWindows.has(win)) {
return;
}
gDevToolsBrowser._trackedBrowserWindows.delete(win);
win.removeEventListener("unload", this);
BrowserMenus.removeMenus(win.document);
// Destroy toolboxes for closed window
for (let [target, toolbox] of gDevTools._toolboxes) {
if (toolbox.win.top == win) {
toolbox.destroy();
}
}
// Destroy the Developer toolbar if it has been accessed
let desc = Object.getOwnPropertyDescriptor(win, "DeveloperToolbar");
if (desc && !desc.get) {
win.DeveloperToolbar.destroy();
}
let tabContainer = win.gBrowser.tabContainer;
tabContainer.removeEventListener("TabSelect", this, false);
tabContainer.removeEventListener("TabOpen", this, false);
tabContainer.removeEventListener("TabClose", this, false);
tabContainer.removeEventListener("TabPinned", this, false);
tabContainer.removeEventListener("TabUnpinned", this, false);
},
handleEvent: function (event) {
switch (event.type) {
case "TabOpen":
case "TabClose":
case "TabPinned":
case "TabUnpinned":
let open = 0;
let pinned = 0;
for (let win of this._trackedBrowserWindows) {
let tabContainer = win.gBrowser.tabContainer;
let numPinnedTabs = win.gBrowser._numPinnedTabs || 0;
let numTabs = tabContainer.itemCount - numPinnedTabs;
open += numTabs;
pinned += numPinnedTabs;
}
this._tabStats.histOpen.push(open);
this._tabStats.histPinned.push(pinned);
this._tabStats.peakOpen = Math.max(open, this._tabStats.peakOpen);
this._tabStats.peakPinned = Math.max(pinned, this._tabStats.peakPinned);
break;
case "TabSelect":
gDevToolsBrowser._updateMenuCheckbox();
break;
case "unload":
// top-level browser window unload
gDevToolsBrowser._forgetBrowserWindow(event.target.defaultView);
break;
}
},
_pingTelemetry: function () {
let mean = function (arr) {
if (arr.length === 0) {
return 0;
}
let total = arr.reduce((a, b) => a + b);
return Math.ceil(total / arr.length);
};
let tabStats = gDevToolsBrowser._tabStats;
this._telemetry.log(TABS_OPEN_PEAK_HISTOGRAM, tabStats.peakOpen);
this._telemetry.log(TABS_OPEN_AVG_HISTOGRAM, mean(tabStats.histOpen));
this._telemetry.log(TABS_PINNED_PEAK_HISTOGRAM, tabStats.peakPinned);
this._telemetry.log(TABS_PINNED_AVG_HISTOGRAM, mean(tabStats.histPinned));
},
/**
* All browser windows have been closed, tidy up remaining objects.
*/
destroy: function () {
Services.prefs.removeObserver("devtools.", gDevToolsBrowser);
Services.obs.removeObserver(gDevToolsBrowser, "browser-delayed-startup-finished");
Services.obs.removeObserver(gDevToolsBrowser.destroy, "quit-application");
gDevToolsBrowser._pingTelemetry();
gDevToolsBrowser._telemetry = null;
for (let win of gDevToolsBrowser._trackedBrowserWindows) {
gDevToolsBrowser._forgetBrowserWindow(win);
}
},
};
// Handle all already registered tools,
gDevTools.getToolDefinitionArray()
.forEach(def => gDevToolsBrowser._addToolToWindows(def));
// and the new ones.
gDevTools.on("tool-registered", function (ev, toolId) {
let toolDefinition = gDevTools._tools.get(toolId);
gDevToolsBrowser._addToolToWindows(toolDefinition);
});
gDevTools.on("tool-unregistered", function (ev, toolId) {
if (typeof toolId != "string") {
toolId = toolId.id;
}
gDevToolsBrowser._removeToolFromWindows(toolId);
});
gDevTools.on("toolbox-ready", gDevToolsBrowser._updateMenuCheckbox);
gDevTools.on("toolbox-destroyed", gDevToolsBrowser._updateMenuCheckbox);
Services.obs.addObserver(gDevToolsBrowser.destroy, "quit-application", false);
Services.obs.addObserver(gDevToolsBrowser, "browser-delayed-startup-finished", false);
// Fake end of browser window load event for all already opened windows
// that is already fully loaded.
let enumerator = Services.wm.getEnumerator(gDevTools.chromeWindowType);
while (enumerator.hasMoreElements()) {
let win = enumerator.getNext();
if (win.gBrowserInit && win.gBrowserInit.delayedStartupFinished) {
gDevToolsBrowser._registerBrowserWindow(win);
}
}
// Watch for module loader unload. Fires when the tools are reloaded.
unload(function () {
gDevToolsBrowser.destroy();
});

View file

@ -0,0 +1,534 @@
/* 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 Services = require("Services");
const promise = require("promise");
const defer = require("devtools/shared/defer");
// Load gDevToolsBrowser toolbox lazily as they need gDevTools to be fully initialized
loader.lazyRequireGetter(this, "Toolbox", "devtools/client/framework/toolbox", true);
loader.lazyRequireGetter(this, "ToolboxHostManager", "devtools/client/framework/toolbox-host-manager", true);
loader.lazyRequireGetter(this, "gDevToolsBrowser", "devtools/client/framework/devtools-browser", true);
const {defaultTools: DefaultTools, defaultThemes: DefaultThemes} =
require("devtools/client/definitions");
const EventEmitter = require("devtools/shared/event-emitter");
const {JsonView} = require("devtools/client/jsonview/main");
const AboutDevTools = require("devtools/client/framework/about-devtools-toolbox");
const {when: unload} = require("sdk/system/unload");
const {Task} = require("devtools/shared/task");
const FORBIDDEN_IDS = new Set(["toolbox", ""]);
const MAX_ORDINAL = 99;
/**
* DevTools is a class that represents a set of developer tools, it holds a
* set of tools and keeps track of open toolboxes in the browser.
*/
this.DevTools = function DevTools() {
this._tools = new Map(); // Map<toolId, tool>
this._themes = new Map(); // Map<themeId, theme>
this._toolboxes = new Map(); // Map<target, toolbox>
// List of toolboxes that are still in process of creation
this._creatingToolboxes = new Map(); // Map<target, toolbox Promise>
// destroy() is an observer's handler so we need to preserve context.
this.destroy = this.destroy.bind(this);
// JSON Viewer for 'application/json' documents.
JsonView.initialize();
AboutDevTools.register();
EventEmitter.decorate(this);
Services.obs.addObserver(this.destroy, "quit-application", false);
// This is important step in initialization codepath where we are going to
// start registering all default tools and themes: create menuitems, keys, emit
// related events.
this.registerDefaults();
};
DevTools.prototype = {
// The windowtype of the main window, used in various tools. This may be set
// to something different by other gecko apps.
chromeWindowType: "navigator:browser",
registerDefaults() {
// Ensure registering items in the sorted order (getDefault* functions
// return sorted lists)
this.getDefaultTools().forEach(definition => this.registerTool(definition));
this.getDefaultThemes().forEach(definition => this.registerTheme(definition));
},
unregisterDefaults() {
for (let definition of this.getToolDefinitionArray()) {
this.unregisterTool(definition.id);
}
for (let definition of this.getThemeDefinitionArray()) {
this.unregisterTheme(definition.id);
}
},
/**
* Register a new developer tool.
*
* A definition is a light object that holds different information about a
* developer tool. This object is not supposed to have any operational code.
* See it as a "manifest".
* The only actual code lives in the build() function, which will be used to
* start an instance of this tool.
*
* Each toolDefinition has the following properties:
* - id: Unique identifier for this tool (string|required)
* - visibilityswitch: Property name to allow us to hide this tool from the
* DevTools Toolbox.
* A falsy value indicates that it cannot be hidden.
* - icon: URL pointing to a graphic which will be used as the src for an
* 16x16 img tag (string|required)
* - invertIconForLightTheme: The icon can automatically have an inversion
* filter applied (default is false). All builtin tools are true, but
* addons may omit this to prevent unwanted changes to the `icon`
* image. filter: invert(1) is applied to the image (boolean|optional)
* - url: URL pointing to a XUL/XHTML document containing the user interface
* (string|required)
* - label: Localized name for the tool to be displayed to the user
* (string|required)
* - hideInOptions: Boolean indicating whether or not this tool should be
shown in toolbox options or not. Defaults to false.
* (boolean)
* - build: Function that takes an iframe, which has been populated with the
* markup from |url|, and also the toolbox containing the panel.
* And returns an instance of ToolPanel (function|required)
*/
registerTool: function DT_registerTool(toolDefinition) {
let toolId = toolDefinition.id;
if (!toolId || FORBIDDEN_IDS.has(toolId)) {
throw new Error("Invalid definition.id");
}
// Make sure that additional tools will always be able to be hidden.
// When being called from main.js, defaultTools has not yet been exported.
// But, we can assume that in this case, it is a default tool.
if (DefaultTools.indexOf(toolDefinition) == -1) {
toolDefinition.visibilityswitch = "devtools." + toolId + ".enabled";
}
this._tools.set(toolId, toolDefinition);
this.emit("tool-registered", toolId);
},
/**
* Removes all tools that match the given |toolId|
* Needed so that add-ons can remove themselves when they are deactivated
*
* @param {string|object} tool
* Definition or the id of the tool to unregister. Passing the
* tool id should be avoided as it is a temporary measure.
* @param {boolean} isQuitApplication
* true to indicate that the call is due to app quit, so we should not
* cause a cascade of costly events
*/
unregisterTool: function DT_unregisterTool(tool, isQuitApplication) {
let toolId = null;
if (typeof tool == "string") {
toolId = tool;
tool = this._tools.get(tool);
}
else {
toolId = tool.id;
}
this._tools.delete(toolId);
if (!isQuitApplication) {
this.emit("tool-unregistered", tool);
}
},
/**
* Sorting function used for sorting tools based on their ordinals.
*/
ordinalSort: function DT_ordinalSort(d1, d2) {
let o1 = (typeof d1.ordinal == "number") ? d1.ordinal : MAX_ORDINAL;
let o2 = (typeof d2.ordinal == "number") ? d2.ordinal : MAX_ORDINAL;
return o1 - o2;
},
getDefaultTools: function DT_getDefaultTools() {
return DefaultTools.sort(this.ordinalSort);
},
getAdditionalTools: function DT_getAdditionalTools() {
let tools = [];
for (let [key, value] of this._tools) {
if (DefaultTools.indexOf(value) == -1) {
tools.push(value);
}
}
return tools.sort(this.ordinalSort);
},
getDefaultThemes() {
return DefaultThemes.sort(this.ordinalSort);
},
/**
* Get a tool definition if it exists and is enabled.
*
* @param {string} toolId
* The id of the tool to show
*
* @return {ToolDefinition|null} tool
* The ToolDefinition for the id or null.
*/
getToolDefinition: function DT_getToolDefinition(toolId) {
let tool = this._tools.get(toolId);
if (!tool) {
return null;
} else if (!tool.visibilityswitch) {
return tool;
}
let enabled;
try {
enabled = Services.prefs.getBoolPref(tool.visibilityswitch);
} catch (e) {
enabled = true;
}
return enabled ? tool : null;
},
/**
* Allow ToolBoxes to get at the list of tools that they should populate
* themselves with.
*
* @return {Map} tools
* A map of the the tool definitions registered in this instance
*/
getToolDefinitionMap: function DT_getToolDefinitionMap() {
let tools = new Map();
for (let [id, definition] of this._tools) {
if (this.getToolDefinition(id)) {
tools.set(id, definition);
}
}
return tools;
},
/**
* Tools have an inherent ordering that can't be represented in a Map so
* getToolDefinitionArray provides an alternative representation of the
* definitions sorted by ordinal value.
*
* @return {Array} tools
* A sorted array of the tool definitions registered in this instance
*/
getToolDefinitionArray: function DT_getToolDefinitionArray() {
let definitions = [];
for (let [id, definition] of this._tools) {
if (this.getToolDefinition(id)) {
definitions.push(definition);
}
}
return definitions.sort(this.ordinalSort);
},
/**
* Register a new theme for developer tools toolbox.
*
* A definition is a light object that holds various information about a
* theme.
*
* Each themeDefinition has the following properties:
* - id: Unique identifier for this theme (string|required)
* - label: Localized name for the theme to be displayed to the user
* (string|required)
* - stylesheets: Array of URLs pointing to a CSS document(s) containing
* the theme style rules (array|required)
* - classList: Array of class names identifying the theme within a document.
* These names are set to document element when applying
* the theme (array|required)
* - onApply: Function that is executed by the framework when the theme
* is applied. The function takes the current iframe window
* and the previous theme id as arguments (function)
* - onUnapply: Function that is executed by the framework when the theme
* is unapplied. The function takes the current iframe window
* and the new theme id as arguments (function)
*/
registerTheme: function DT_registerTheme(themeDefinition) {
let themeId = themeDefinition.id;
if (!themeId) {
throw new Error("Invalid theme id");
}
if (this._themes.get(themeId)) {
throw new Error("Theme with the same id is already registered");
}
this._themes.set(themeId, themeDefinition);
this.emit("theme-registered", themeId);
},
/**
* Removes an existing theme from the list of registered themes.
* Needed so that add-ons can remove themselves when they are deactivated
*
* @param {string|object} theme
* Definition or the id of the theme to unregister.
*/
unregisterTheme: function DT_unregisterTheme(theme) {
let themeId = null;
if (typeof theme == "string") {
themeId = theme;
theme = this._themes.get(theme);
}
else {
themeId = theme.id;
}
let currTheme = Services.prefs.getCharPref("devtools.theme");
// Note that we can't check if `theme` is an item
// of `DefaultThemes` as we end up reloading definitions
// module and end up with different theme objects
let isCoreTheme = DefaultThemes.some(t => t.id === themeId);
// Reset the theme if an extension theme that's currently applied
// is being removed.
// Ignore shutdown since addons get disabled during that time.
if (!Services.startup.shuttingDown &&
!isCoreTheme &&
theme.id == currTheme) {
Services.prefs.setCharPref("devtools.theme", "light");
let data = {
pref: "devtools.theme",
newValue: "light",
oldValue: currTheme
};
this.emit("pref-changed", data);
this.emit("theme-unregistered", theme);
}
this._themes.delete(themeId);
},
/**
* Get a theme definition if it exists.
*
* @param {string} themeId
* The id of the theme
*
* @return {ThemeDefinition|null} theme
* The ThemeDefinition for the id or null.
*/
getThemeDefinition: function DT_getThemeDefinition(themeId) {
let theme = this._themes.get(themeId);
if (!theme) {
return null;
}
return theme;
},
/**
* Get map of registered themes.
*
* @return {Map} themes
* A map of the the theme definitions registered in this instance
*/
getThemeDefinitionMap: function DT_getThemeDefinitionMap() {
let themes = new Map();
for (let [id, definition] of this._themes) {
if (this.getThemeDefinition(id)) {
themes.set(id, definition);
}
}
return themes;
},
/**
* Get registered themes definitions sorted by ordinal value.
*
* @return {Array} themes
* A sorted array of the theme definitions registered in this instance
*/
getThemeDefinitionArray: function DT_getThemeDefinitionArray() {
let definitions = [];
for (let [id, definition] of this._themes) {
if (this.getThemeDefinition(id)) {
definitions.push(definition);
}
}
return definitions.sort(this.ordinalSort);
},
/**
* Show a Toolbox for a target (either by creating a new one, or if a toolbox
* already exists for the target, by bring to the front the existing one)
* If |toolId| is specified then the displayed toolbox will have the
* specified tool selected.
* If |hostType| is specified then the toolbox will be displayed using the
* specified HostType.
*
* @param {Target} target
* The target the toolbox will debug
* @param {string} toolId
* The id of the tool to show
* @param {Toolbox.HostType} hostType
* The type of host (bottom, window, side)
* @param {object} hostOptions
* Options for host specifically
*
* @return {Toolbox} toolbox
* The toolbox that was opened
*/
showToolbox: Task.async(function* (target, toolId, hostType, hostOptions) {
let toolbox = this._toolboxes.get(target);
if (toolbox) {
if (hostType != null && toolbox.hostType != hostType) {
yield toolbox.switchHost(hostType);
}
if (toolId != null && toolbox.currentToolId != toolId) {
yield toolbox.selectTool(toolId);
}
toolbox.raise();
} else {
// As toolbox object creation is async, we have to be careful about races
// Check for possible already in process of loading toolboxes before
// actually trying to create a new one.
let promise = this._creatingToolboxes.get(target);
if (promise) {
return yield promise;
}
let toolboxPromise = this.createToolbox(target, toolId, hostType, hostOptions);
this._creatingToolboxes.set(target, toolboxPromise);
toolbox = yield toolboxPromise;
this._creatingToolboxes.delete(target);
}
return toolbox;
}),
createToolbox: Task.async(function* (target, toolId, hostType, hostOptions) {
let manager = new ToolboxHostManager(target, hostType, hostOptions);
let toolbox = yield manager.create(toolId);
this._toolboxes.set(target, toolbox);
this.emit("toolbox-created", toolbox);
toolbox.once("destroy", () => {
this.emit("toolbox-destroy", target);
});
toolbox.once("destroyed", () => {
this._toolboxes.delete(target);
this.emit("toolbox-destroyed", target);
});
yield toolbox.open();
this.emit("toolbox-ready", toolbox);
return toolbox;
}),
/**
* Return the toolbox for a given target.
*
* @param {object} target
* Target value e.g. the target that owns this toolbox
*
* @return {Toolbox} toolbox
* The toolbox that is debugging the given target
*/
getToolbox: function DT_getToolbox(target) {
return this._toolboxes.get(target);
},
/**
* Close the toolbox for a given target
*
* @return promise
* This promise will resolve to false if no toolbox was found
* associated to the target. true, if the toolbox was successfully
* closed.
*/
closeToolbox: Task.async(function* (target) {
let toolbox = yield this._creatingToolboxes.get(target);
if (!toolbox) {
toolbox = this._toolboxes.get(target);
}
if (!toolbox) {
return false;
}
yield toolbox.destroy();
return true;
}),
/**
* Called to tear down a tools provider.
*/
_teardown: function DT_teardown() {
for (let [target, toolbox] of this._toolboxes) {
toolbox.destroy();
}
AboutDevTools.unregister();
},
/**
* All browser windows have been closed, tidy up remaining objects.
*/
destroy: function () {
Services.obs.removeObserver(this.destroy, "quit-application");
for (let [key, tool] of this.getToolDefinitionMap()) {
this.unregisterTool(key, true);
}
JsonView.destroy();
gDevTools.unregisterDefaults();
// Cleaning down the toolboxes: i.e.
// for (let [target, toolbox] of this._toolboxes) toolbox.destroy();
// Is taken care of by the gDevToolsBrowser.forgetBrowserWindow
},
/**
* Iterator that yields each of the toolboxes.
*/
*[Symbol.iterator ]() {
for (let toolbox of this._toolboxes) {
yield toolbox;
}
}
};
const gDevTools = exports.gDevTools = new DevTools();
// Watch for module loader unload. Fires when the tools are reloaded.
unload(function () {
gDevTools._teardown();
});

View file

@ -0,0 +1,162 @@
/* -*- 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";
/**
* This JSM is here to keep some compatibility with existing add-ons.
* Please now use the modules:
* - devtools/client/framework/devtools for gDevTools
* - devtools/client/framework/devtools-browser for gDevToolsBrowser
*/
this.EXPORTED_SYMBOLS = [ "gDevTools", "gDevToolsBrowser" ];
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
const { loader } = Cu.import("resource://devtools/shared/Loader.jsm", {});
/**
* Do not directly map to the commonjs modules so that callsites of
* gDevTools.jsm do not have to do anything to access to the very last version
* of the module. The `devtools` and `browser` getter are always going to
* retrieve the very last version of the modules.
*/
Object.defineProperty(this, "require", {
get() {
let { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
return require;
}
});
Object.defineProperty(this, "devtools", {
get() {
return require("devtools/client/framework/devtools").gDevTools;
}
});
Object.defineProperty(this, "browser", {
get() {
return require("devtools/client/framework/devtools-browser").gDevToolsBrowser;
}
});
/**
* gDevTools is a singleton that controls the Firefox Developer Tools.
*
* It is an instance of a DevTools class that holds a set of tools. It has the
* same lifetime as the browser.
*/
let gDevToolsMethods = [
// Used by the reload addon.
// Force reloading dependencies if the loader happens to have reloaded.
"reload",
// Used by: - b2g desktop.js
// - nsContextMenu
// - /devtools code
"showToolbox",
// Used by Addon SDK and /devtools
"closeToolbox",
"getToolbox",
// Used by Addon SDK, main.js and tests:
"registerTool",
"registerTheme",
"unregisterTool",
"unregisterTheme",
// Used by main.js and test
"getToolDefinitionArray",
"getThemeDefinitionArray",
// Used by theme-switching.js
"getThemeDefinition",
"emit",
// Used by /devtools
"on",
"off",
"once",
// Used by tests
"getToolDefinitionMap",
"getThemeDefinitionMap",
"getDefaultTools",
"getAdditionalTools",
"getToolDefinition",
];
this.gDevTools = {
// Used by tests
get _toolboxes() {
return devtools._toolboxes;
},
get _tools() {
return devtools._tools;
},
*[Symbol.iterator ]() {
for (let toolbox of this._toolboxes) {
yield toolbox;
}
}
};
gDevToolsMethods.forEach(name => {
this.gDevTools[name] = (...args) => {
return devtools[name].apply(devtools, args);
};
});
/**
* gDevToolsBrowser exposes functions to connect the gDevTools instance with a
* Firefox instance.
*/
let gDevToolsBrowserMethods = [
// used by browser-sets.inc, command
"toggleToolboxCommand",
// Used by browser.js itself, by setting a oncommand string...
"selectToolCommand",
// Used by browser-sets.inc, command
"openAboutDebugging",
// Used by browser-sets.inc, command
"openConnectScreen",
// Used by browser-sets.inc, command
// itself, webide widget
"openWebIDE",
// Used by browser-sets.inc, command
"openContentProcessToolbox",
// Used by webide.js
"moveWebIDEWidgetInNavbar",
// Used by browser.js
"registerBrowserWindow",
// Used by reload addon
"hasToolboxOpened",
// Used by browser.js
"forgetBrowserWindow"
];
this.gDevToolsBrowser = {
// Used by webide.js
get isWebIDEInitialized() {
return browser.isWebIDEInitialized;
},
// Used by a test (should be removed)
get _trackedBrowserWindows() {
return browser._trackedBrowserWindows;
}
};
gDevToolsBrowserMethods.forEach(name => {
this.gDevToolsBrowser[name] = (...args) => {
return browser[name].apply(browser, args);
};
});

View file

@ -0,0 +1,103 @@
/* 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_TOKEN = "<:>";
function LocationStore (store) {
this._store = store || new Map();
}
/**
* Method to get a promised location from the Store.
* @param location
* @returns Promise<Object>
*/
LocationStore.prototype.get = function (location) {
this._safeAccessInit(location.url);
return this._store.get(location.url).get(location);
};
/**
* Method to set a promised location to the Store
* @param location
* @param promisedLocation
*/
LocationStore.prototype.set = function (location, promisedLocation = null) {
this._safeAccessInit(location.url);
this._store.get(location.url).set(serialize(location), promisedLocation);
};
/**
* Utility method to verify if key exists in Store before accessing it.
* If not, initializing it.
* @param url
* @private
*/
LocationStore.prototype._safeAccessInit = function (url) {
if (!this._store.has(url)) {
this._store.set(url, new Map());
}
};
/**
* Utility proxy method to Map.clear() method
*/
LocationStore.prototype.clear = function () {
this._store.clear();
};
/**
* Retrieves an object containing all locations to be resolved when `source-updated`
* event is triggered.
* @param url
* @returns {Array<String>}
*/
LocationStore.prototype.getByURL = function (url){
if (this._store.has(url)) {
return [...this._store.get(url).keys()];
}
return [];
};
/**
* Invalidates the stale location promises from the store when `source-updated`
* event is triggered, and when FrameView unsubscribes from a location.
* @param url
*/
LocationStore.prototype.clearByURL = function (url) {
this._safeAccessInit(url);
this._store.set(url, new Map());
};
exports.LocationStore = LocationStore;
exports.serialize = serialize;
exports.deserialize = deserialize;
/**
* Utility method to serialize the source
* @param source
* @returns {string}
*/
function serialize(source) {
let { url, line, column } = source;
line = line || 0;
column = column || 0;
return `${url}${SOURCE_TOKEN}${line}${SOURCE_TOKEN}${column}`;
};
/**
* Utility method to serialize the source
* @param source
* @returns Object
*/
function deserialize(source) {
let [ url, line, column ] = source.split(SOURCE_TOKEN);
line = parseInt(line);
column = parseInt(column);
if (column === 0) {
return { url, line };
}
return { url, line, column };
};

View file

@ -0,0 +1,65 @@
/* -*- 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 partial implementation of the MenuItem API provided by electron:
* https://github.com/electron/electron/blob/master/docs/api/menu-item.md.
*
* Missing features:
* - id String - Unique within a single menu. If defined then it can be used
* as a reference to this item by the position attribute.
* - role String - Define the action of the menu item; when specified the
* click property will be ignored
* - sublabel String
* - accelerator Accelerator
* - icon NativeImage
* - position String - This field allows fine-grained definition of the
* specific location within a given menu.
*
* Implemented features:
* @param Object options
* Function click
* Will be called with click(menuItem, browserWindow) when the menu item
* is clicked
* String type
* Can be normal, separator, submenu, checkbox or radio
* String label
* Boolean enabled
* If false, the menu item will be greyed out and unclickable.
* Boolean checked
* Should only be specified for checkbox or radio type menu items.
* Menu submenu
* Should be specified for submenu type menu items. If submenu is specified,
* the type: 'submenu' can be omitted. If the value is not a Menu then it
* will be automatically converted to one using Menu.buildFromTemplate.
* Boolean visible
* If false, the menu item will be entirely hidden.
*/
function MenuItem({
accesskey = null,
checked = false,
click = () => {},
disabled = false,
label = "",
id = null,
submenu = null,
type = "normal",
visible = true,
} = { }) {
this.accesskey = accesskey;
this.checked = checked;
this.click = click;
this.disabled = disabled;
this.id = id;
this.label = label;
this.submenu = submenu;
this.type = type;
this.visible = visible;
}
module.exports = MenuItem;

View file

@ -0,0 +1,173 @@
/* -*- 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 EventEmitter = require("devtools/shared/event-emitter");
/**
* A partial implementation of the Menu API provided by electron:
* https://github.com/electron/electron/blob/master/docs/api/menu.md.
*
* Extra features:
* - Emits an 'open' and 'close' event when the menu is opened/closed
* @param String id (non standard)
* Needed so tests can confirm the XUL implementation is working
*/
function Menu({ id = null } = {}) {
this.menuitems = [];
this.id = id;
Object.defineProperty(this, "items", {
get() {
return this.menuitems;
}
});
EventEmitter.decorate(this);
}
/**
* Add an item to the end of the Menu
*
* @param {MenuItem} menuItem
*/
Menu.prototype.append = function (menuItem) {
this.menuitems.push(menuItem);
};
/**
* Add an item to a specified position in the menu
*
* @param {int} pos
* @param {MenuItem} menuItem
*/
Menu.prototype.insert = function (pos, menuItem) {
throw Error("Not implemented");
};
/**
* Show the Menu at a specified location on the screen
*
* Missing features:
* - browserWindow - BrowserWindow (optional) - Default is null.
* - positioningItem Number - (optional) OS X
*
* @param {int} screenX
* @param {int} screenY
* @param Toolbox toolbox (non standard)
* Needed so we in which window to inject XUL
*/
Menu.prototype.popup = function (screenX, screenY, toolbox) {
let doc = toolbox.doc;
let popupset = doc.querySelector("popupset");
// See bug 1285229, on Windows, opening the same popup multiple times in a
// row ends up duplicating the popup. The newly inserted popup doesn't
// dismiss the old one. So remove any previously displayed popup before
// opening a new one.
let popup = popupset.querySelector("menupopup[menu-api=\"true\"]");
if (popup) {
popup.hidePopup();
}
popup = doc.createElement("menupopup");
popup.setAttribute("menu-api", "true");
if (this.id) {
popup.id = this.id;
}
this._createMenuItems(popup);
// Remove the menu from the DOM once it's hidden.
popup.addEventListener("popuphidden", (e) => {
if (e.target === popup) {
popup.remove();
this.emit("close");
}
});
popup.addEventListener("popupshown", (e) => {
if (e.target === popup) {
this.emit("open");
}
});
popupset.appendChild(popup);
popup.openPopupAtScreen(screenX, screenY, true);
};
Menu.prototype._createMenuItems = function (parent) {
let doc = parent.ownerDocument;
this.menuitems.forEach(item => {
if (!item.visible) {
return;
}
if (item.submenu) {
let menupopup = doc.createElement("menupopup");
item.submenu._createMenuItems(menupopup);
let menu = doc.createElement("menu");
menu.appendChild(menupopup);
menu.setAttribute("label", item.label);
if (item.disabled) {
menu.setAttribute("disabled", "true");
}
if (item.accesskey) {
menu.setAttribute("accesskey", item.accesskey);
}
if (item.id) {
menu.id = item.id;
}
parent.appendChild(menu);
} else if (item.type === "separator") {
let menusep = doc.createElement("menuseparator");
parent.appendChild(menusep);
} else {
let menuitem = doc.createElement("menuitem");
menuitem.setAttribute("label", item.label);
menuitem.addEventListener("command", () => {
item.click();
});
if (item.type === "checkbox") {
menuitem.setAttribute("type", "checkbox");
}
if (item.type === "radio") {
menuitem.setAttribute("type", "radio");
}
if (item.disabled) {
menuitem.setAttribute("disabled", "true");
}
if (item.checked) {
menuitem.setAttribute("checked", "true");
}
if (item.accesskey) {
menuitem.setAttribute("accesskey", item.accesskey);
}
if (item.id) {
menuitem.id = item.id;
}
parent.appendChild(menuitem);
}
});
};
Menu.setApplicationMenu = () => {
throw Error("Not implemented");
};
Menu.sendActionToFirstResponder = () => {
throw Error("Not implemented");
};
Menu.buildFromTemplate = () => {
throw Error("Not implemented");
};
module.exports = Menu;

View file

@ -0,0 +1,33 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
TEST_HARNESS_FILES.xpcshell.devtools.client.framework.test += [
'test/shared-redux-head.js',
]
DevToolsModules(
'about-devtools-toolbox.js',
'attach-thread.js',
'browser-menus.js',
'devtools-browser.js',
'devtools.js',
'gDevTools.jsm',
'location-store.js',
'menu-item.js',
'menu.js',
'selection.js',
'sidebar.js',
'source-map-service.js',
'target-from-url.js',
'target.js',
'toolbox-highlighter-utils.js',
'toolbox-host-manager.js',
'toolbox-hosts.js',
'toolbox-options.js',
'toolbox.js',
'ToolboxProcess.jsm',
)

View file

@ -0,0 +1,107 @@
/* 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/. */
:root{
-moz-user-select: none;
}
#options-panel-container {
overflow: auto;
}
#options-panel {
display: block;
}
.options-vertical-pane {
display: inline;
float: left;
}
.options-vertical-pane {
margin: 5px;
width: calc(100%/3 - 10px);
min-width: 320px;
padding-inline-start: 5px;
box-sizing: border-box;
}
/* Snap to 50% width once there is not room for 3 columns anymore.
This prevents having 2 columns showing in a row, but taking up
only ~66% of the available space. */
@media (max-width: 1000px) {
.options-vertical-pane {
width: calc(100%/2 - 10px);
}
}
.options-vertical-pane fieldset {
border: none;
}
.options-vertical-pane fieldset legend {
font-size: 1.4rem;
margin-inline-start: -15px;
margin-bottom: 3px;
cursor: default;
}
.options-vertical-pane fieldset + fieldset {
margin-top: 1rem;
}
.options-groupbox {
margin-inline-start: 15px;
padding: 2px;
}
.options-groupbox label {
display: flex;
padding: 4px 0;
align-items: center;
}
/* Add padding for label of select inputs in order to
align it with surrounding checkboxes */
.options-groupbox label span:first-child {
padding-inline-start: 5px;
}
.options-groupbox label span + select {
margin-inline-start: 4px;
}
.options-groupbox.horizontal-options-groupbox label {
display: inline-flex;
align-items: flex-end;
}
.options-groupbox.horizontal-options-groupbox label + label {
margin-inline-start: 4px;
}
.options-groupbox > *,
.options-groupbox > .hidden-labels-box > checkbox {
padding: 2px;
}
.options-groupbox > .hidden-labels-box {
padding: 0;
}
.options-citation-label {
display: inline-block;
font-size: 1rem;
font-style: italic;
/* To align it with the checkbox */
padding: 4px 0 0;
padding-inline-end: 4px;
}
#devtools-sourceeditor-keybinding-select {
min-width: 130px;
}
#devtools-sourceeditor-tabsize-select {
min-width: 80px;
}

View file

@ -0,0 +1,247 @@
/* -*- 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 nodeConstants = require("devtools/shared/dom-node-constants");
var EventEmitter = require("devtools/shared/event-emitter");
/**
* API
*
* new Selection(walker=null)
* destroy()
* node (readonly)
* setNode(node, origin="unknown")
*
* Helpers:
*
* window
* document
* isRoot()
* isNode()
* isHTMLNode()
*
* Check the nature of the node:
*
* isElementNode()
* isAttributeNode()
* isTextNode()
* isCDATANode()
* isEntityRefNode()
* isEntityNode()
* isProcessingInstructionNode()
* isCommentNode()
* isDocumentNode()
* isDocumentTypeNode()
* isDocumentFragmentNode()
* isNotationNode()
*
* Events:
* "new-node-front" when the inner node changed
* "attribute-changed" when an attribute is changed
* "detached-front" when the node (or one of its parents) is removed from
* the document
* "reparented" when the node (or one of its parents) is moved under
* a different node
*/
/**
* A Selection object. Hold a reference to a node.
* Includes some helpers, fire some helpful events.
*/
function Selection(walker) {
EventEmitter.decorate(this);
this._onMutations = this._onMutations.bind(this);
this.setWalker(walker);
}
exports.Selection = Selection;
Selection.prototype = {
_walker: null,
_onMutations: function (mutations) {
let attributeChange = false;
let pseudoChange = false;
let detached = false;
let parentNode = null;
for (let m of mutations) {
if (!attributeChange && m.type == "attributes") {
attributeChange = true;
}
if (m.type == "childList") {
if (!detached && !this.isConnected()) {
if (this.isNode()) {
parentNode = m.target;
}
detached = true;
}
}
if (m.type == "pseudoClassLock") {
pseudoChange = true;
}
}
// Fire our events depending on what changed in the mutations array
if (attributeChange) {
this.emit("attribute-changed");
}
if (pseudoChange) {
this.emit("pseudoclass");
}
if (detached) {
this.emit("detached-front", parentNode);
}
},
destroy: function () {
this.setWalker(null);
},
setWalker: function (walker) {
if (this._walker) {
this._walker.off("mutations", this._onMutations);
}
this._walker = walker;
if (this._walker) {
this._walker.on("mutations", this._onMutations);
}
},
setNodeFront: function (value, reason = "unknown") {
this.reason = reason;
// If an inlineTextChild text node is being set, then set it's parent instead.
let parentNode = value && value.parentNode();
if (value && parentNode && parentNode.inlineTextChild === value) {
value = parentNode;
}
this._nodeFront = value;
this.emit("new-node-front", value, this.reason);
},
get documentFront() {
return this._walker.document(this._nodeFront);
},
get nodeFront() {
return this._nodeFront;
},
isRoot: function () {
return this.isNode() &&
this.isConnected() &&
this._nodeFront.isDocumentElement;
},
isNode: function () {
return !!this._nodeFront;
},
isConnected: function () {
let node = this._nodeFront;
if (!node || !node.actorID) {
return false;
}
while (node) {
if (node === this._walker.rootNode) {
return true;
}
node = node.parentNode();
}
return false;
},
isHTMLNode: function () {
let xhtmlNs = "http://www.w3.org/1999/xhtml";
return this.isNode() && this.nodeFront.namespaceURI == xhtmlNs;
},
// Node type
isElementNode: function () {
return this.isNode() && this.nodeFront.nodeType == nodeConstants.ELEMENT_NODE;
},
isPseudoElementNode: function () {
return this.isNode() && this.nodeFront.isPseudoElement;
},
isAnonymousNode: function () {
return this.isNode() && this.nodeFront.isAnonymous;
},
isAttributeNode: function () {
return this.isNode() && this.nodeFront.nodeType == nodeConstants.ATTRIBUTE_NODE;
},
isTextNode: function () {
return this.isNode() && this.nodeFront.nodeType == nodeConstants.TEXT_NODE;
},
isCDATANode: function () {
return this.isNode() && this.nodeFront.nodeType == nodeConstants.CDATA_SECTION_NODE;
},
isEntityRefNode: function () {
return this.isNode() &&
this.nodeFront.nodeType == nodeConstants.ENTITY_REFERENCE_NODE;
},
isEntityNode: function () {
return this.isNode() && this.nodeFront.nodeType == nodeConstants.ENTITY_NODE;
},
isProcessingInstructionNode: function () {
return this.isNode() &&
this.nodeFront.nodeType == nodeConstants.PROCESSING_INSTRUCTION_NODE;
},
isCommentNode: function () {
return this.isNode() &&
this.nodeFront.nodeType == nodeConstants.PROCESSING_INSTRUCTION_NODE;
},
isDocumentNode: function () {
return this.isNode() && this.nodeFront.nodeType == nodeConstants.DOCUMENT_NODE;
},
/**
* @returns true if the selection is the <body> HTML element.
*/
isBodyNode: function () {
return this.isHTMLNode() &&
this.isConnected() &&
this.nodeFront.nodeName === "BODY";
},
/**
* @returns true if the selection is the <head> HTML element.
*/
isHeadNode: function () {
return this.isHTMLNode() &&
this.isConnected() &&
this.nodeFront.nodeName === "HEAD";
},
isDocumentTypeNode: function () {
return this.isNode() && this.nodeFront.nodeType == nodeConstants.DOCUMENT_TYPE_NODE;
},
isDocumentFragmentNode: function () {
return this.isNode() &&
this.nodeFront.nodeType == nodeConstants.DOCUMENT_FRAGMENT_NODE;
},
isNotationNode: function () {
return this.isNode() && this.nodeFront.nodeType == nodeConstants.NOTATION_NODE;
},
};

View file

@ -0,0 +1,592 @@
/* 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";
var Services = require("Services");
var {Task} = require("devtools/shared/task");
var EventEmitter = require("devtools/shared/event-emitter");
var Telemetry = require("devtools/client/shared/telemetry");
const {LocalizationHelper} = require("devtools/shared/l10n");
const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
/**
* ToolSidebar provides methods to register tabs in the sidebar.
* It's assumed that the sidebar contains a xul:tabbox.
* Typically, you'll want the tabbox parameter to be a XUL tabbox like this:
*
* <tabbox id="inspector-sidebar" handleCtrlTab="false" class="devtools-sidebar-tabs">
* <tabs/>
* <tabpanels flex="1"/>
* </tabbox>
*
* The ToolSidebar API has a method to add new tabs, so the tabs and tabpanels
* nodes can be empty. But they can also already contain items before the
* ToolSidebar is created.
*
* Tabs added through the addTab method are only identified by an ID and a URL
* which is used as the href of an iframe node that is inserted in the newly
* created tabpanel.
* Tabs already present before the ToolSidebar is created may contain anything.
* However, these tabs must have ID attributes if it is required for the various
* methods that accept an ID as argument to work here.
*
* @param {Node} tabbox
* <tabbox> node;
* @param {ToolPanel} panel
* Related ToolPanel instance;
* @param {String} uid
* Unique ID
* @param {Object} options
* - hideTabstripe: Should the tabs be hidden. Defaults to false
* - showAllTabsMenu: Should a drop-down menu be displayed in case tabs
* become hidden. Defaults to false.
* - disableTelemetry: By default, switching tabs on and off in the sidebar
* will record tool usage in telemetry, pass this option to true to avoid it.
*
* Events raised:
* - new-tab-registered : After a tab has been added via addTab. The tab ID
* is passed with the event. This however, is raised before the tab iframe
* is fully loaded.
* - <tabid>-ready : After the tab iframe has been loaded
* - <tabid>-selected : After tab <tabid> was selected
* - select : Same as above, but for any tab, the ID is passed with the event
* - <tabid>-unselected : After tab <tabid> is unselected
*/
function ToolSidebar(tabbox, panel, uid, options = {}) {
EventEmitter.decorate(this);
this._tabbox = tabbox;
this._uid = uid;
this._panelDoc = this._tabbox.ownerDocument;
this._toolPanel = panel;
this._options = options;
this._onTabBoxOverflow = this._onTabBoxOverflow.bind(this);
this._onTabBoxUnderflow = this._onTabBoxUnderflow.bind(this);
try {
this._width = Services.prefs.getIntPref("devtools.toolsidebar-width." + this._uid);
} catch (e) {}
if (!options.disableTelemetry) {
this._telemetry = new Telemetry();
}
this._tabbox.tabpanels.addEventListener("select", this, true);
this._tabs = new Map();
// Check for existing tabs in the DOM and add them.
this.addExistingTabs();
if (this._options.hideTabstripe) {
this._tabbox.setAttribute("hidetabs", "true");
}
if (this._options.showAllTabsMenu) {
this.addAllTabsMenu();
}
this._toolPanel.emit("sidebar-created", this);
}
exports.ToolSidebar = ToolSidebar;
ToolSidebar.prototype = {
TAB_ID_PREFIX: "sidebar-tab-",
TABPANEL_ID_PREFIX: "sidebar-panel-",
/**
* Add a "…" button at the end of the tabstripe that toggles a dropdown menu
* containing the list of all tabs if any become hidden due to lack of room.
*
* If the ToolSidebar was created with the "showAllTabsMenu" option set to
* true, this is already done automatically. If not, you may call this
* function at any time to add the menu.
*/
addAllTabsMenu: function () {
if (this._allTabsBtn) {
return;
}
let tabs = this._tabbox.tabs;
// Create a container and insert it first in the tabbox
let allTabsContainer = this._panelDoc.createElementNS(XULNS, "stack");
this._tabbox.insertBefore(allTabsContainer, tabs);
// Move the tabs inside and make them flex
allTabsContainer.appendChild(tabs);
tabs.setAttribute("flex", "1");
// Create the dropdown menu next to the tabs
this._allTabsBtn = this._panelDoc.createElementNS(XULNS, "toolbarbutton");
this._allTabsBtn.setAttribute("class", "devtools-sidebar-alltabs");
this._allTabsBtn.setAttribute("end", "0");
this._allTabsBtn.setAttribute("top", "0");
this._allTabsBtn.setAttribute("width", "15");
this._allTabsBtn.setAttribute("type", "menu");
this._allTabsBtn.setAttribute("tooltiptext",
L10N.getStr("sidebar.showAllTabs.tooltip"));
this._allTabsBtn.setAttribute("hidden", "true");
allTabsContainer.appendChild(this._allTabsBtn);
let menuPopup = this._panelDoc.createElementNS(XULNS, "menupopup");
this._allTabsBtn.appendChild(menuPopup);
// Listening to tabs overflow event to toggle the alltabs button
tabs.addEventListener("overflow", this._onTabBoxOverflow, false);
tabs.addEventListener("underflow", this._onTabBoxUnderflow, false);
// Add menuitems to the alltabs menu if there are already tabs in the
// sidebar
for (let [id, tab] of this._tabs) {
let item = this._addItemToAllTabsMenu(id, tab, {
selected: tab.hasAttribute("selected")
});
if (tab.hidden) {
item.hidden = true;
}
}
},
removeAllTabsMenu: function () {
if (!this._allTabsBtn) {
return;
}
let tabs = this._tabbox.tabs;
tabs.removeEventListener("overflow", this._onTabBoxOverflow, false);
tabs.removeEventListener("underflow", this._onTabBoxUnderflow, false);
// Moving back the tabs as a first child of the tabbox
this._tabbox.insertBefore(tabs, this._tabbox.tabpanels);
this._tabbox.querySelector("stack").remove();
this._allTabsBtn = null;
},
_onTabBoxOverflow: function () {
this._allTabsBtn.removeAttribute("hidden");
},
_onTabBoxUnderflow: function () {
this._allTabsBtn.setAttribute("hidden", "true");
},
/**
* Add an item in the allTabs menu for a given tab.
*/
_addItemToAllTabsMenu: function (id, tab, options) {
if (!this._allTabsBtn) {
return;
}
let item = this._panelDoc.createElementNS(XULNS, "menuitem");
let idPrefix = "sidebar-alltabs-item-";
item.setAttribute("id", idPrefix + id);
item.setAttribute("label", tab.getAttribute("label"));
item.setAttribute("type", "checkbox");
if (options.selected) {
item.setAttribute("checked", true);
}
// The auto-checking of menuitems in this menu doesn't work, so let's do
// it manually
item.setAttribute("autocheck", false);
let menu = this._allTabsBtn.querySelector("menupopup");
if (options.insertBefore) {
let referenceItem = menu.querySelector(`#${idPrefix}${options.insertBefore}`);
menu.insertBefore(item, referenceItem);
} else {
menu.appendChild(item);
}
item.addEventListener("click", () => {
this._tabbox.selectedTab = tab;
}, false);
tab.allTabsMenuItem = item;
return item;
},
/**
* Register a tab. A tab is a document.
* The document must have a title, which will be used as the name of the tab.
*
* @param {string} id The unique id for this tab.
* @param {string} url The URL of the document to load in this new tab.
* @param {Object} options A set of options for this new tab:
* - {Boolean} selected Set to true to make this new tab selected by default.
* - {String} insertBefore By default, the new tab is appended at the end of the
* tabbox, pass the ID of an existing tab to insert it before that tab instead.
*/
addTab: function (id, url, options = {}) {
let iframe = this._panelDoc.createElementNS(XULNS, "iframe");
iframe.className = "iframe-" + id;
iframe.setAttribute("flex", "1");
iframe.setAttribute("src", url);
iframe.tooltip = "aHTMLTooltip";
// Creating the tab and adding it to the tabbox
let tab = this._panelDoc.createElementNS(XULNS, "tab");
tab.setAttribute("id", this.TAB_ID_PREFIX + id);
tab.setAttribute("crop", "end");
// Avoid showing "undefined" while the tab is loading
tab.setAttribute("label", "");
if (options.insertBefore) {
let referenceTab = this.getTab(options.insertBefore);
this._tabbox.tabs.insertBefore(tab, referenceTab);
} else {
this._tabbox.tabs.appendChild(tab);
}
// Add the tab to the allTabs menu if exists
let allTabsItem = this._addItemToAllTabsMenu(id, tab, options);
let onIFrameLoaded = (event) => {
let doc = event.target;
let win = doc.defaultView;
tab.setAttribute("label", doc.title);
if (allTabsItem) {
allTabsItem.setAttribute("label", doc.title);
}
iframe.removeEventListener("load", onIFrameLoaded, true);
if ("setPanel" in win) {
win.setPanel(this._toolPanel, iframe);
}
this.emit(id + "-ready");
};
iframe.addEventListener("load", onIFrameLoaded, true);
let tabpanel = this._panelDoc.createElementNS(XULNS, "tabpanel");
tabpanel.setAttribute("id", this.TABPANEL_ID_PREFIX + id);
tabpanel.appendChild(iframe);
if (options.insertBefore) {
let referenceTabpanel = this.getTabPanel(options.insertBefore);
this._tabbox.tabpanels.insertBefore(tabpanel, referenceTabpanel);
} else {
this._tabbox.tabpanels.appendChild(tabpanel);
}
this._tooltip = this._panelDoc.createElementNS(XULNS, "tooltip");
this._tooltip.id = "aHTMLTooltip";
tabpanel.appendChild(this._tooltip);
this._tooltip.page = true;
tab.linkedPanel = this.TABPANEL_ID_PREFIX + id;
// We store the index of this tab.
this._tabs.set(id, tab);
if (options.selected) {
this._selectTabSoon(id);
}
this.emit("new-tab-registered", id);
},
untitledTabsIndex: 0,
/**
* Search for existing tabs in the markup that aren't know yet and add them.
*/
addExistingTabs: function () {
let knownTabs = [...this._tabs.values()];
for (let tab of this._tabbox.tabs.querySelectorAll("tab")) {
if (knownTabs.indexOf(tab) !== -1) {
continue;
}
// Find an ID for this unknown tab
let id = tab.getAttribute("id") || "untitled-tab-" + (this.untitledTabsIndex++);
// If the existing tab contains the tab ID prefix, extract the ID of the
// tab
if (id.startsWith(this.TAB_ID_PREFIX)) {
id = id.split(this.TAB_ID_PREFIX).pop();
}
// Register the tab
this._tabs.set(id, tab);
this.emit("new-tab-registered", id);
}
},
/**
* Remove an existing tab.
* @param {String} tabId The ID of the tab that was used to register it, or
* the tab id attribute value if the tab existed before the sidebar got created.
* @param {String} tabPanelId Optional. If provided, this ID will be used
* instead of the tabId to retrieve and remove the corresponding <tabpanel>
*/
removeTab: Task.async(function* (tabId, tabPanelId) {
// Remove the tab if it can be found
let tab = this.getTab(tabId);
if (!tab) {
return;
}
let win = this.getWindowForTab(tabId);
if (win && ("destroy" in win)) {
yield win.destroy();
}
tab.remove();
// Also remove the tabpanel
let panel = this.getTabPanel(tabPanelId || tabId);
if (panel) {
panel.remove();
}
this._tabs.delete(tabId);
this.emit("tab-unregistered", tabId);
}),
/**
* Show or hide a specific tab.
* @param {Boolean} isVisible True to show the tab/tabpanel, False to hide it.
* @param {String} id The ID of the tab to be hidden.
*/
toggleTab: function (isVisible, id) {
// Toggle the tab.
let tab = this.getTab(id);
if (!tab) {
return;
}
tab.hidden = !isVisible;
// Toggle the item in the allTabs menu.
if (this._allTabsBtn) {
this._allTabsBtn.querySelector("#sidebar-alltabs-item-" + id).hidden = !isVisible;
}
},
/**
* Select a specific tab.
*/
select: function (id) {
let tab = this.getTab(id);
if (tab) {
this._tabbox.selectedTab = tab;
}
},
/**
* Hack required to select a tab right after it was created.
*
* @param {String} id
* The sidebar tab id to select.
*/
_selectTabSoon: function (id) {
this._panelDoc.defaultView.setTimeout(() => {
this.select(id);
}, 0);
},
/**
* Return the id of the selected tab.
*/
getCurrentTabID: function () {
let currentID = null;
for (let [id, tab] of this._tabs) {
if (this._tabbox.tabs.selectedItem == tab) {
currentID = id;
break;
}
}
return currentID;
},
/**
* Returns the requested tab panel based on the id.
* @param {String} id
* @return {DOMNode}
*/
getTabPanel: function (id) {
// Search with and without the ID prefix as there might have been existing
// tabpanels by the time the sidebar got created
return this._tabbox.tabpanels.querySelector("#" + this.TABPANEL_ID_PREFIX + id + ", #" + id);
},
/**
* Return the tab based on the provided id, if one was registered with this id.
* @param {String} id
* @return {DOMNode}
*/
getTab: function (id) {
return this._tabs.get(id);
},
/**
* Event handler.
*/
handleEvent: function (event) {
if (event.type !== "select" || this._destroyed) {
return;
}
if (this._currentTool == this.getCurrentTabID()) {
// Tool hasn't changed.
return;
}
let previousTool = this._currentTool;
this._currentTool = this.getCurrentTabID();
if (previousTool) {
if (this._telemetry) {
this._telemetry.toolClosed(previousTool);
}
this.emit(previousTool + "-unselected");
}
if (this._telemetry) {
this._telemetry.toolOpened(this._currentTool);
}
this.emit(this._currentTool + "-selected");
this.emit("select", this._currentTool);
// Handlers for "select"/"...-selected"/"...-unselected" events might have
// destroyed the sidebar in the meantime.
if (this._destroyed) {
return;
}
// Handle menuitem selection if the allTabsMenu is there by unchecking all
// items except the selected one.
let tab = this._tabbox.selectedTab;
if (tab.allTabsMenuItem) {
for (let otherItem of this._allTabsBtn.querySelectorAll("menuitem")) {
otherItem.removeAttribute("checked");
}
tab.allTabsMenuItem.setAttribute("checked", true);
}
},
/**
* Toggle sidebar's visibility state.
*/
toggle: function () {
if (this._tabbox.hasAttribute("hidden")) {
this.show();
} else {
this.hide();
}
},
/**
* Show the sidebar.
*
* @param {String} id
* The sidebar tab id to select.
*/
show: function (id) {
if (this._width) {
this._tabbox.width = this._width;
}
this._tabbox.removeAttribute("hidden");
// If an id is given, select the corresponding sidebar tab and record the
// tool opened.
if (id) {
this._currentTool = id;
if (this._telemetry) {
this._telemetry.toolOpened(this._currentTool);
}
this._selectTabSoon(id);
}
this.emit("show");
},
/**
* Show the sidebar.
*/
hide: function () {
Services.prefs.setIntPref("devtools.toolsidebar-width." + this._uid, this._tabbox.width);
this._tabbox.setAttribute("hidden", "true");
this._panelDoc.activeElement.blur();
this.emit("hide");
},
/**
* Return the window containing the tab content.
*/
getWindowForTab: function (id) {
if (!this._tabs.has(id)) {
return null;
}
// Get the tabpanel and make sure it contains an iframe
let panel = this.getTabPanel(id);
if (!panel || !panel.firstChild || !panel.firstChild.contentWindow) {
return;
}
return panel.firstChild.contentWindow;
},
/**
* Clean-up.
*/
destroy: Task.async(function* () {
if (this._destroyed) {
return;
}
this._destroyed = true;
Services.prefs.setIntPref("devtools.toolsidebar-width." + this._uid, this._tabbox.width);
if (this._allTabsBtn) {
this.removeAllTabsMenu();
}
this._tabbox.tabpanels.removeEventListener("select", this, true);
// Note that we check for the existence of this._tabbox.tabpanels at each
// step as the container window may have been closed by the time one of the
// panel's destroy promise resolves.
while (this._tabbox.tabpanels && this._tabbox.tabpanels.hasChildNodes()) {
let panel = this._tabbox.tabpanels.firstChild;
let win = panel.firstChild.contentWindow;
if (win && ("destroy" in win)) {
yield win.destroy();
}
panel.remove();
}
while (this._tabbox.tabs && this._tabbox.tabs.hasChildNodes()) {
this._tabbox.tabs.removeChild(this._tabbox.tabs.firstChild);
}
if (this._currentTool && this._telemetry) {
this._telemetry.toolClosed(this._currentTool);
}
this._toolPanel.emit("sidebar-destroyed", this);
this._tabs = null;
this._tabbox = null;
this._panelDoc = null;
this._toolPanel = null;
})
};

View file

@ -0,0 +1,209 @@
/* 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 { Task } = require("devtools/shared/task");
const EventEmitter = require("devtools/shared/event-emitter");
const { LocationStore, serialize, deserialize } = require("./location-store");
/**
* A manager class that wraps a TabTarget and listens to source changes
* from source maps and resolves non-source mapped locations to the source mapped
* versions and back and forth, and creating smart elements with a location that
* auto-update when the source changes (from pretty printing, source maps loading, etc)
*
* @param {TabTarget} target
*/
function SourceMapService(target) {
this._target = target;
this._locationStore = new LocationStore();
this._isNotSourceMapped = new Map();
EventEmitter.decorate(this);
this._onSourceUpdated = this._onSourceUpdated.bind(this);
this._resolveLocation = this._resolveLocation.bind(this);
this._resolveAndUpdate = this._resolveAndUpdate.bind(this);
this.subscribe = this.subscribe.bind(this);
this.unsubscribe = this.unsubscribe.bind(this);
this.reset = this.reset.bind(this);
this.destroy = this.destroy.bind(this);
target.on("source-updated", this._onSourceUpdated);
target.on("navigate", this.reset);
target.on("will-navigate", this.reset);
}
/**
* Clears the store containing the cached promised locations
*/
SourceMapService.prototype.reset = function () {
// Guard to prevent clearing the store when it is not initialized yet.
if (!this._locationStore) {
return;
}
this._locationStore.clear();
this._isNotSourceMapped.clear();
};
SourceMapService.prototype.destroy = function () {
this.reset();
this._target.off("source-updated", this._onSourceUpdated);
this._target.off("navigate", this.reset);
this._target.off("will-navigate", this.reset);
this._target.off("close", this.destroy);
this._target = this._locationStore = this._isNotSourceMapped = null;
};
/**
* Sets up listener for the callback to update the FrameView
* and tries to resolve location, if it is source-mappable
* @param location
* @param callback
*/
SourceMapService.prototype.subscribe = function (location, callback) {
// A valid candidate location for source-mapping should have a url and line.
// Abort if there's no `url`, which means it's unsourcemappable anyway,
// like an eval script.
// From previous attempts to source-map locations, we also determine if a location
// is not source-mapped.
if (!location.url || !location.line || this._isNotSourceMapped.get(location.url)) {
return;
}
this.on(serialize(location), callback);
this._locationStore.set(location);
this._resolveAndUpdate(location);
};
/**
* Removes the listener for the location and clears cached locations
* @param location
* @param callback
*/
SourceMapService.prototype.unsubscribe = function (location, callback) {
this.off(serialize(location), callback);
// Check to see if the store exists before attempting to clear a location
// Sometimes un-subscribe happens during the destruction cascades and this
// condition is to protect against that. Could be looked into in the future.
if (!this._locationStore) {
return;
}
this._locationStore.clearByURL(location.url);
};
/**
* Tries to resolve the location and if successful,
* emits the resolved location
* @param location
* @private
*/
SourceMapService.prototype._resolveAndUpdate = function (location) {
this._resolveLocation(location).then(resolvedLocation => {
// We try to source map the first console log to initiate the source-updated
// event from target. The isSameLocation check is to make sure we don't update
// the frame, if the location is not source-mapped.
if (resolvedLocation && !isSameLocation(location, resolvedLocation)) {
this.emit(serialize(location), location, resolvedLocation);
}
});
};
/**
* Checks if there is existing promise to resolve location, if so returns cached promise
* if not, tries to resolve location and returns a promised location
* @param location
* @return Promise<Object>
* @private
*/
SourceMapService.prototype._resolveLocation = Task.async(function* (location) {
let resolvedLocation;
const cachedLocation = this._locationStore.get(location);
if (cachedLocation) {
resolvedLocation = cachedLocation;
} else {
const promisedLocation = resolveLocation(this._target, location);
if (promisedLocation) {
this._locationStore.set(location, promisedLocation);
resolvedLocation = promisedLocation;
}
}
return resolvedLocation;
});
/**
* Checks if the `source-updated` event is fired from the target.
* Checks to see if location store has the source url in its cache,
* if so, tries to update each stale location in the store.
* Determines if the source should be source-mapped or not.
* @param _
* @param sourceEvent
* @private
*/
SourceMapService.prototype._onSourceUpdated = function (_, sourceEvent) {
let { type, source } = sourceEvent;
// If we get a new source, and it's not a source map, abort;
// we can have no actionable updates as this is just a new normal source.
// Check Source Actor for sourceMapURL property (after Firefox 48)
// If not present, utilize isSourceMapped and isPrettyPrinted properties
// to estimate if a source is not source-mapped.
const isNotSourceMapped = !(source.sourceMapURL ||
source.isSourceMapped || source.isPrettyPrinted);
if (type === "newSource" && isNotSourceMapped) {
this._isNotSourceMapped.set(source.url, true);
return;
}
let sourceUrl = null;
if (source.generatedUrl && source.isSourceMapped) {
sourceUrl = source.generatedUrl;
} else if (source.url && source.isPrettyPrinted) {
sourceUrl = source.url;
}
const locationsToResolve = this._locationStore.getByURL(sourceUrl);
if (locationsToResolve.length) {
this._locationStore.clearByURL(sourceUrl);
for (let location of locationsToResolve) {
this._resolveAndUpdate(deserialize(location));
}
}
};
exports.SourceMapService = SourceMapService;
/**
* Take a TabTarget and a location, containing a `url`, `line`, and `column`, resolve
* the location to the latest location (so a source mapped location, or if pretty print
* status has been updated)
*
* @param {TabTarget} target
* @param {Object} location
* @return {Promise<Object>}
*/
function resolveLocation(target, location) {
return Task.spawn(function* () {
let newLocation = yield target.resolveLocation({
url: location.url,
line: location.line,
column: location.column || Infinity
});
// Source or mapping not found, so don't do anything
if (newLocation.error) {
return null;
}
return newLocation;
});
}
/**
* Returns true if the original location and resolved location are the same
* @param location
* @param resolvedLocation
* @returns {boolean}
*/
function isSameLocation(location, resolvedLocation) {
return location.url === resolvedLocation.url &&
location.line === resolvedLocation.line &&
location.column === resolvedLocation.column;
}

View file

@ -0,0 +1,20 @@
function originalToGeneratedId(originalId) {
const match = originalId.match(/(.*)\/originalSource/);
return match ? match[1] : "";
}
function generatedToOriginalId(generatedId, url) {
return generatedId + "/originalSource-" + url.replace(/ \//, '-');
}
function isOriginalId(id) {
return !!id.match(/\/originalSource/);
}
function isGeneratedId(id) {
return !isOriginalId(id);
}
module.exports = {
originalToGeneratedId, generatedToOriginalId, isOriginalId, isGeneratedId
};

View file

@ -0,0 +1,220 @@
const { fetch, assert } = require("devtools/shared/DevToolsUtils");
const { joinURI } = require("devtools/shared/path");
const path = require("sdk/fs/path");
const { SourceMapConsumer, SourceMapGenerator } = require("source-map");
const { isJavaScript } = require("./source");
const {
originalToGeneratedId,
generatedToOriginalId,
isGeneratedId,
isOriginalId
} = require("./source-map-util");
let sourceMapRequests = new Map();
let sourceMapsEnabled = false;
function clearSourceMaps() {
sourceMapRequests.clear();
}
function enableSourceMaps() {
sourceMapsEnabled = true;
}
function _resolveSourceMapURL(source) {
const { url = "", sourceMapURL = "" } = source;
if (path.isURL(sourceMapURL) || url == "") {
// If it's already a full URL or the source doesn't have a URL,
// don't resolve anything.
return sourceMapURL;
} else if (path.isAbsolute(sourceMapURL)) {
// If it's an absolute path, it should be resolved relative to the
// host of the source.
const { protocol = "", host = "" } = parse(url);
return `${protocol}//${host}${sourceMapURL}`;
}
// Otherwise, it's a relative path and should be resolved relative
// to the source.
return dirname(url) + "/" + sourceMapURL;
}
/**
* Sets the source map's sourceRoot to be relative to the source map url.
* @memberof utils/source-map-worker
* @static
*/
function _setSourceMapRoot(sourceMap, absSourceMapURL, source) {
// No need to do this fiddling if we won't be fetching any sources over the
// wire.
if (sourceMap.hasContentsOfAllSources()) {
return;
}
const base = dirname(
(absSourceMapURL.indexOf("data:") === 0 && source.url) ?
source.url :
absSourceMapURL
);
if (sourceMap.sourceRoot) {
sourceMap.sourceRoot = joinURI(base, sourceMap.sourceRoot);
} else {
sourceMap.sourceRoot = base;
}
return sourceMap;
}
function _getSourceMap(generatedSourceId)
: ?Promise<SourceMapConsumer> {
return sourceMapRequests.get(generatedSourceId);
}
async function _resolveAndFetch(generatedSource) : SourceMapConsumer {
// Fetch the sourcemap over the network and create it.
const sourceMapURL = _resolveSourceMapURL(generatedSource);
const fetched = await fetch(
sourceMapURL, { loadFromCache: false }
);
// Create the source map and fix it up.
const map = new SourceMapConsumer(fetched.content);
_setSourceMapRoot(map, sourceMapURL, generatedSource);
return map;
}
function _fetchSourceMap(generatedSource) {
const existingRequest = sourceMapRequests.get(generatedSource.id);
if (existingRequest) {
// If it has already been requested, return the request. Make sure
// to do this even if sourcemapping is turned off, because
// pretty-printing uses sourcemaps.
//
// An important behavior here is that if it's in the middle of
// requesting it, all subsequent calls will block on the initial
// request.
return existingRequest;
} else if (!generatedSource.sourceMapURL || !sourceMapsEnabled) {
return Promise.resolve(null);
}
// Fire off the request, set it in the cache, and return it.
// Suppress any errors and just return null (ignores bogus
// sourcemaps).
const req = _resolveAndFetch(generatedSource).catch(() => null);
sourceMapRequests.set(generatedSource.id, req);
return req;
}
async function getOriginalURLs(generatedSource) {
const map = await _fetchSourceMap(generatedSource);
return map && map.sources;
}
async function getGeneratedLocation(location: Location, originalSource: Source)
: Promise<Location> {
if (!isOriginalId(location.sourceId)) {
return location;
}
const generatedSourceId = originalToGeneratedId(location.sourceId);
const map = await _getSourceMap(generatedSourceId);
if (!map) {
return location;
}
const { line, column } = map.generatedPositionFor({
source: originalSource.url,
line: location.line,
column: location.column == null ? 0 : location.column
});
return {
sourceId: generatedSourceId,
line: line,
// Treat 0 as no column so that line breakpoints work correctly.
column: column === 0 ? undefined : column
};
}
async function getOriginalLocation(location) {
if (!isGeneratedId(location.sourceId)) {
return location;
}
const map = await _getSourceMap(location.sourceId);
if (!map) {
return location;
}
const { source: url, line, column } = map.originalPositionFor({
line: location.line,
column: location.column == null ? Infinity : location.column
});
if (url == null) {
// No url means the location didn't map.
return location;
}
return {
sourceId: generatedToOriginalId(location.sourceId, url),
line,
column
};
}
async function getOriginalSourceText(originalSource) {
assert(isOriginalId(originalSource.id),
"Source is not an original source");
const generatedSourceId = originalToGeneratedId(originalSource.id);
const map = await _getSourceMap(generatedSourceId);
if (!map) {
return null;
}
let text = map.sourceContentFor(originalSource.url);
if (!text) {
text = (await fetch(
originalSource.url, { loadFromCache: false }
)).content;
}
return {
text,
contentType: isJavaScript(originalSource.url || "") ?
"text/javascript" :
"text/plain"
};
}
function applySourceMap(generatedId, url, code, mappings) {
const generator = new SourceMapGenerator({ file: url });
mappings.forEach(mapping => generator.addMapping(mapping));
generator.setSourceContent(url, code);
const map = SourceMapConsumer(generator.toJSON());
sourceMapRequests.set(generatedId, Promise.resolve(map));
}
const publicInterface = {
getOriginalURLs,
getGeneratedLocation,
getOriginalLocation,
getOriginalSourceText,
enableSourceMaps,
applySourceMap,
clearSourceMaps
};
self.onmessage = function(msg) {
const { id, method, args } = msg.data;
const response = publicInterface[method].apply(undefined, args);
if (response instanceof Promise) {
response.then(val => self.postMessage({ id, response: val }),
err => self.postMessage({ id, error: err }));
} else {
self.postMessage({ id, response });
}
};

View file

@ -0,0 +1,84 @@
// @flow
const {
originalToGeneratedId,
generatedToOriginalId,
isGeneratedId,
isOriginalId
} = require("./source-map-util");
function workerTask(worker, method) {
return function(...args: any) {
return new Promise((resolve, reject) => {
const id = msgId++;
worker.postMessage({ id, method, args });
const listener = ({ data: result }) => {
if (result.id !== id) {
return;
}
worker.removeEventListener("message", listener);
if (result.error) {
reject(result.error);
} else {
resolve(result.response);
}
};
worker.addEventListener("message", listener);
});
};
}
let sourceMapWorker;
function restartWorker() {
if (sourceMapWorker) {
sourceMapWorker.terminate();
}
sourceMapWorker = new Worker(
"resource://devtools/client/framework/source-map-worker.js"
);
if (Services.prefs.getBoolPref("devtools.debugger.client-source-maps-enabled")) {
sourceMapWorker.postMessage({ id: 0, method: "enableSourceMaps" });
}
}
restartWorker();
function destroyWorker() {
if (sourceMapWorker) {
sourceMapWorker.terminate();
sourceMapWorker = null;
}
}
function shouldSourceMap() {
return Services.prefs.getBoolPref("devtools.debugger.client-source-maps-enabled");
}
const getOriginalURLs = workerTask(sourceMapWorker, "getOriginalURLs");
const getGeneratedLocation = workerTask(sourceMapWorker,
"getGeneratedLocation");
const getOriginalLocation = workerTask(sourceMapWorker,
"getOriginalLocation");
const getOriginalSourceText = workerTask(sourceMapWorker,
"getOriginalSourceText");
const applySourceMap = workerTask(sourceMapWorker, "applySourceMap");
const clearSourceMaps = workerTask(sourceMapWorker, "clearSourceMaps");
module.exports = {
originalToGeneratedId,
generatedToOriginalId,
isGeneratedId,
isOriginalId,
getOriginalURLs,
getGeneratedLocation,
getOriginalLocation,
getOriginalSourceText,
applySourceMap,
clearSourceMaps,
destroyWorker,
shouldSourceMap
};

View file

@ -0,0 +1,120 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { Cu, Ci } = require("chrome");
const { TargetFactory } = require("devtools/client/framework/target");
const { DebuggerServer } = require("devtools/server/main");
const { DebuggerClient } = require("devtools/shared/client/main");
const { Task } = require("devtools/shared/task");
/**
* Construct a Target for a given URL object having various query parameters:
*
* host:
* {String} The hostname or IP address to connect to.
* port:
* {Number} The TCP port to connect to, to use with `host` argument.
* ws:
* {Boolean} If true, connect via websocket instread of regular TCP connection.
*
* type: tab, process
* {String} The type of target to connect to. Currently tabs and processes are supported types.
*
* If type="tab":
* id:
* {Number} the tab outerWindowID
* chrome: Optional
* {Boolean} Force the creation of a chrome target. Gives more privileges to the tab
* actor. Allows chrome execution in the webconsole and see chrome files in
* the debugger. (handy when contributing to firefox)
*
* If type="process":
* id:
* {Number} the process id to debug. Default to 0, which is the parent process.
*
* @param {URL} url
* The url to fetch query params from.
*
* @return A target object
*/
exports.targetFromURL = Task.async(function* (url) {
let params = url.searchParams;
let type = params.get("type");
if (!type) {
throw new Error("targetFromURL, missing type parameter");
}
let id = params.get("id");
// Allows to spawn a chrome enabled target for any context
// (handy to debug chrome stuff in a child process)
let chrome = params.has("chrome");
let client = yield createClient(params);
yield client.connect();
let form, isTabActor;
if (type === "tab") {
// Fetch target for a remote tab
id = parseInt(id);
if (isNaN(id)) {
throw new Error("targetFromURL, wrong tab id:'" + id + "', should be a number");
}
try {
let response = yield client.getTab({ outerWindowID: id });
form = response.tab;
} catch (ex) {
if (ex.error == "noTab") {
throw new Error("targetFromURL, tab with outerWindowID:'" + id + "' doesn't exist");
}
throw ex;
}
} else if (type == "process") {
// Fetch target for a remote chrome actor
DebuggerServer.allowChromeProcess = true;
try {
id = parseInt(id);
if (isNaN(id)) {
id = 0;
}
let response = yield client.getProcess(id);
form = response.form;
chrome = true;
if (id != 0) {
// Child process are not exposing tab actors and only support debugger+console
isTabActor = false;
}
} catch (ex) {
if (ex.error == "noProcess") {
throw new Error("targetFromURL, process with id:'" + id + "' doesn't exist");
}
throw ex;
}
} else {
throw new Error("targetFromURL, unsupported type='" + type + "' parameter");
}
return TargetFactory.forRemoteTab({ client, form, chrome, isTabActor });
});
function* createClient(params) {
let host = params.get("host");
let port = params.get("port");
let webSocket = !!params.get("ws");
let transport;
if (port) {
transport = yield DebuggerClient.socketConnect({ host, port, webSocket });
} else {
// Setup a server if we don't have one already running
if (!DebuggerServer.initialized) {
DebuggerServer.init();
DebuggerServer.addBrowserActors();
}
transport = DebuggerServer.connectPipe()
}
return new DebuggerClient(transport);
}

View file

@ -0,0 +1,825 @@
/* 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 { Ci } = require("chrome");
const promise = require("promise");
const defer = require("devtools/shared/defer");
const EventEmitter = require("devtools/shared/event-emitter");
const Services = require("Services");
const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
loader.lazyRequireGetter(this, "DebuggerServer", "devtools/server/main", true);
loader.lazyRequireGetter(this, "DebuggerClient",
"devtools/shared/client/main", true);
loader.lazyRequireGetter(this, "gDevTools",
"devtools/client/framework/devtools", true);
const targets = new WeakMap();
const promiseTargets = new WeakMap();
/**
* Functions for creating Targets
*/
const TargetFactory = exports.TargetFactory = {
/**
* Construct a Target
* @param {XULTab} tab
* The tab to use in creating a new target.
*
* @return A target object
*/
forTab: function (tab) {
let target = targets.get(tab);
if (target == null) {
target = new TabTarget(tab);
targets.set(tab, target);
}
return target;
},
/**
* Return a promise of a Target for a remote tab.
* @param {Object} options
* The options object has the following properties:
* {
* form: the remote protocol form of a tab,
* client: a DebuggerClient instance
* (caller owns this and is responsible for closing),
* chrome: true if the remote target is the whole process
* }
*
* @return A promise of a target object
*/
forRemoteTab: function (options) {
let targetPromise = promiseTargets.get(options);
if (targetPromise == null) {
let target = new TabTarget(options);
targetPromise = target.makeRemote().then(() => target);
promiseTargets.set(options, targetPromise);
}
return targetPromise;
},
forWorker: function (workerClient) {
let target = targets.get(workerClient);
if (target == null) {
target = new WorkerTarget(workerClient);
targets.set(workerClient, target);
}
return target;
},
/**
* Creating a target for a tab that is being closed is a problem because it
* allows a leak as a result of coming after the close event which normally
* clears things up. This function allows us to ask if there is a known
* target for a tab without creating a target
* @return true/false
*/
isKnownTab: function (tab) {
return targets.has(tab);
},
};
/**
* A Target represents something that we can debug. Targets are generally
* read-only. Any changes that you wish to make to a target should be done via
* a Tool that attaches to the target. i.e. a Target is just a pointer saying
* "the thing to debug is over there".
*
* Providing a generalized abstraction of a web-page or web-browser (available
* either locally or remotely) is beyond the scope of this class (and maybe
* also beyond the scope of this universe) However Target does attempt to
* abstract some common events and read-only properties common to many Tools.
*
* Supported read-only properties:
* - name, isRemote, url
*
* Target extends EventEmitter and provides support for the following events:
* - close: The target window has been closed. All tools attached to this
* target should close. This event is not currently cancelable.
* - navigate: The target window has navigated to a different URL
*
* Optional events:
* - will-navigate: The target window will navigate to a different URL
* - hidden: The target is not visible anymore (for TargetTab, another tab is
* selected)
* - visible: The target is visible (for TargetTab, tab is selected)
*
* Comparing Targets: 2 instances of a Target object can point at the same
* thing, so t1 !== t2 and t1 != t2 even when they represent the same object.
* To compare to targets use 't1.equals(t2)'.
*/
/**
* A TabTarget represents a page living in a browser tab. Generally these will
* be web pages served over http(s), but they don't have to be.
*/
function TabTarget(tab) {
EventEmitter.decorate(this);
this.destroy = this.destroy.bind(this);
this.activeTab = this.activeConsole = null;
// Only real tabs need initialization here. Placeholder objects for remote
// targets will be initialized after a makeRemote method call.
if (tab && !["client", "form", "chrome"].every(tab.hasOwnProperty, tab)) {
this._tab = tab;
this._setupListeners();
} else {
this._form = tab.form;
this._url = this._form.url;
this._title = this._form.title;
this._client = tab.client;
this._chrome = tab.chrome;
}
// Default isTabActor to true if not explicitly specified
if (typeof tab.isTabActor == "boolean") {
this._isTabActor = tab.isTabActor;
} else {
this._isTabActor = true;
}
}
TabTarget.prototype = {
_webProgressListener: null,
/**
* Returns a promise for the protocol description from the root actor. Used
* internally with `target.actorHasMethod`. Takes advantage of caching if
* definition was fetched previously with the corresponding actor information.
* Actors are lazily loaded, so not only must the tool using a specific actor
* be in use, the actors are only registered after invoking a method (for
* performance reasons, added in bug 988237), so to use these actor detection
* methods, one must already be communicating with a specific actor of that
* type.
*
* Must be a remote target.
*
* @return {Promise}
* {
* "category": "actor",
* "typeName": "longstractor",
* "methods": [{
* "name": "substring",
* "request": {
* "type": "substring",
* "start": {
* "_arg": 0,
* "type": "primitive"
* },
* "end": {
* "_arg": 1,
* "type": "primitive"
* }
* },
* "response": {
* "substring": {
* "_retval": "primitive"
* }
* }
* }],
* "events": {}
* }
*/
getActorDescription: function (actorName) {
if (!this.client) {
throw new Error("TabTarget#getActorDescription() can only be called on " +
"remote tabs.");
}
let deferred = defer();
if (this._protocolDescription &&
this._protocolDescription.types[actorName]) {
deferred.resolve(this._protocolDescription.types[actorName]);
} else {
this.client.mainRoot.protocolDescription(description => {
this._protocolDescription = description;
deferred.resolve(description.types[actorName]);
});
}
return deferred.promise;
},
/**
* Returns a boolean indicating whether or not the specific actor
* type exists. Must be a remote target.
*
* @param {String} actorName
* @return {Boolean}
*/
hasActor: function (actorName) {
if (!this.client) {
throw new Error("TabTarget#hasActor() can only be called on remote " +
"tabs.");
}
if (this.form) {
return !!this.form[actorName + "Actor"];
}
return false;
},
/**
* Queries the protocol description to see if an actor has
* an available method. The actor must already be lazily-loaded (read
* the restrictions in the `getActorDescription` comments),
* so this is for use inside of tool. Returns a promise that
* resolves to a boolean. Must be a remote target.
*
* @param {String} actorName
* @param {String} methodName
* @return {Promise}
*/
actorHasMethod: function (actorName, methodName) {
if (!this.client) {
throw new Error("TabTarget#actorHasMethod() can only be called on " +
"remote tabs.");
}
return this.getActorDescription(actorName).then(desc => {
if (desc && desc.methods) {
return !!desc.methods.find(method => method.name === methodName);
}
return false;
});
},
/**
* Returns a trait from the root actor.
*
* @param {String} traitName
* @return {Mixed}
*/
getTrait: function (traitName) {
if (!this.client) {
throw new Error("TabTarget#getTrait() can only be called on remote " +
"tabs.");
}
// If the targeted actor exposes traits and has a defined value for this
// traits, override the root actor traits
if (this.form.traits && traitName in this.form.traits) {
return this.form.traits[traitName];
}
return this.client.traits[traitName];
},
get tab() {
return this._tab;
},
get form() {
return this._form;
},
// Get a promise of the root form returned by a listTabs request. This promise
// is cached.
get root() {
if (!this._root) {
this._root = this._getRoot();
}
return this._root;
},
_getRoot: function () {
return new Promise((resolve, reject) => {
this.client.listTabs(response => {
if (response.error) {
reject(new Error(response.error + ": " + response.message));
return;
}
resolve(response);
});
});
},
get client() {
return this._client;
},
// Tells us if we are debugging content document
// or if we are debugging chrome stuff.
// Allows to controls which features are available against
// a chrome or a content document.
get chrome() {
return this._chrome;
},
// Tells us if the related actor implements TabActor interface
// and requires to call `attach` request before being used
// and `detach` during cleanup
get isTabActor() {
return this._isTabActor;
},
get window() {
// XXX - this is a footgun for e10s - there .contentWindow will be null,
// and even though .contentWindowAsCPOW *might* work, it will not work
// in all contexts. Consumers of .window need to be refactored to not
// rely on this.
if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) {
console.error("The .window getter on devtools' |target| object isn't " +
"e10s friendly!\n" + Error().stack);
}
// Be extra careful here, since this may be called by HS_getHudByWindow
// during shutdown.
if (this._tab && this._tab.linkedBrowser) {
return this._tab.linkedBrowser.contentWindow;
}
return null;
},
get name() {
if (this.isAddon) {
return this._form.name;
}
return this._title;
},
get url() {
return this._url;
},
get isRemote() {
return !this.isLocalTab;
},
get isAddon() {
return !!(this._form && this._form.actor && (
this._form.actor.match(/conn\d+\.addon\d+/) ||
this._form.actor.match(/conn\d+\.webExtension\d+/)
));
},
get isWebExtension() {
return !!(this._form && this._form.actor &&
this._form.actor.match(/conn\d+\.webExtension\d+/));
},
get isLocalTab() {
return !!this._tab;
},
get isMultiProcess() {
return !this.window;
},
/**
* Adds remote protocol capabilities to the target, so that it can be used
* for tools that support the Remote Debugging Protocol even for local
* connections.
*/
makeRemote: function () {
if (this._remote) {
return this._remote.promise;
}
this._remote = defer();
if (this.isLocalTab) {
// Since a remote protocol connection will be made, let's start the
// DebuggerServer here, once and for all tools.
if (!DebuggerServer.initialized) {
DebuggerServer.init();
DebuggerServer.addBrowserActors();
}
this._client = new DebuggerClient(DebuggerServer.connectPipe());
// A local TabTarget will never perform chrome debugging.
this._chrome = false;
}
this._setupRemoteListeners();
let attachTab = () => {
this._client.attachTab(this._form.actor, (response, tabClient) => {
if (!tabClient) {
this._remote.reject("Unable to attach to the tab");
return;
}
this.activeTab = tabClient;
this.threadActor = response.threadActor;
attachConsole();
});
};
let onConsoleAttached = (response, consoleClient) => {
if (!consoleClient) {
this._remote.reject("Unable to attach to the console");
return;
}
this.activeConsole = consoleClient;
this._remote.resolve(null);
};
let attachConsole = () => {
this._client.attachConsole(this._form.consoleActor,
[ "NetworkActivity" ],
onConsoleAttached);
};
if (this.isLocalTab) {
this._client.connect()
.then(() => this._client.getTab({ tab: this.tab }))
.then(response => {
this._form = response.tab;
this._url = this._form.url;
this._title = this._form.title;
attachTab();
}, e => this._remote.reject(e));
} else if (this.isTabActor) {
// In the remote debugging case, the protocol connection will have been
// already initialized in the connection screen code.
attachTab();
} else {
// AddonActor and chrome debugging on RootActor doesn't inherits from
// TabActor and doesn't need to be attached.
attachConsole();
}
return this._remote.promise;
},
/**
* Listen to the different events.
*/
_setupListeners: function () {
this._webProgressListener = new TabWebProgressListener(this);
this.tab.linkedBrowser.addProgressListener(this._webProgressListener);
this.tab.addEventListener("TabClose", this);
this.tab.parentNode.addEventListener("TabSelect", this);
this.tab.ownerDocument.defaultView.addEventListener("unload", this);
this.tab.addEventListener("TabRemotenessChange", this);
},
/**
* Teardown event listeners.
*/
_teardownListeners: function () {
if (this._webProgressListener) {
this._webProgressListener.destroy();
}
this._tab.ownerDocument.defaultView.removeEventListener("unload", this);
this._tab.removeEventListener("TabClose", this);
this._tab.parentNode.removeEventListener("TabSelect", this);
this._tab.removeEventListener("TabRemotenessChange", this);
},
/**
* Setup listeners for remote debugging, updating existing ones as necessary.
*/
_setupRemoteListeners: function () {
this.client.addListener("closed", this.destroy);
this._onTabDetached = (aType, aPacket) => {
// We have to filter message to ensure that this detach is for this tab
if (aPacket.from == this._form.actor) {
this.destroy();
}
};
this.client.addListener("tabDetached", this._onTabDetached);
this._onTabNavigated = (aType, aPacket) => {
let event = Object.create(null);
event.url = aPacket.url;
event.title = aPacket.title;
event.nativeConsoleAPI = aPacket.nativeConsoleAPI;
event.isFrameSwitching = aPacket.isFrameSwitching;
if (!aPacket.isFrameSwitching) {
// Update the title and url unless this is a frame switch.
this._url = aPacket.url;
this._title = aPacket.title;
}
// Send any stored event payload (DOMWindow or nsIRequest) for backwards
// compatibility with non-remotable tools.
if (aPacket.state == "start") {
event._navPayload = this._navRequest;
this.emit("will-navigate", event);
this._navRequest = null;
} else {
event._navPayload = this._navWindow;
this.emit("navigate", event);
this._navWindow = null;
}
};
this.client.addListener("tabNavigated", this._onTabNavigated);
this._onFrameUpdate = (aType, aPacket) => {
this.emit("frame-update", aPacket);
};
this.client.addListener("frameUpdate", this._onFrameUpdate);
this._onSourceUpdated = (event, packet) => this.emit("source-updated", packet);
this.client.addListener("newSource", this._onSourceUpdated);
this.client.addListener("updatedSource", this._onSourceUpdated);
},
/**
* Teardown listeners for remote debugging.
*/
_teardownRemoteListeners: function () {
this.client.removeListener("closed", this.destroy);
this.client.removeListener("tabNavigated", this._onTabNavigated);
this.client.removeListener("tabDetached", this._onTabDetached);
this.client.removeListener("frameUpdate", this._onFrameUpdate);
this.client.removeListener("newSource", this._onSourceUpdated);
this.client.removeListener("updatedSource", this._onSourceUpdated);
},
/**
* Handle tabs events.
*/
handleEvent: function (event) {
switch (event.type) {
case "TabClose":
case "unload":
this.destroy();
break;
case "TabSelect":
if (this.tab.selected) {
this.emit("visible", event);
} else {
this.emit("hidden", event);
}
break;
case "TabRemotenessChange":
this.onRemotenessChange();
break;
}
},
// Automatically respawn the toolbox when the tab changes between being
// loaded within the parent process and loaded from a content process.
// Process change can go in both ways.
onRemotenessChange: function () {
// Responsive design do a crazy dance around tabs and triggers
// remotenesschange events. But we should ignore them as at the end
// the content doesn't change its remoteness.
if (this._tab.isResponsiveDesignMode) {
return;
}
// Save a reference to the tab as it will be nullified on destroy
let tab = this._tab;
let onToolboxDestroyed = (event, target) => {
if (target != this) {
return;
}
gDevTools.off("toolbox-destroyed", target);
// Recreate a fresh target instance as the current one is now destroyed
let newTarget = TargetFactory.forTab(tab);
gDevTools.showToolbox(newTarget);
};
gDevTools.on("toolbox-destroyed", onToolboxDestroyed);
},
/**
* Target is not alive anymore.
*/
destroy: function () {
// If several things call destroy then we give them all the same
// destruction promise so we're sure to destroy only once
if (this._destroyer) {
return this._destroyer.promise;
}
this._destroyer = defer();
// Before taking any action, notify listeners that destruction is imminent.
this.emit("close");
if (this._tab) {
this._teardownListeners();
}
let cleanupAndResolve = () => {
this._cleanup();
this._destroyer.resolve(null);
};
// If this target was not remoted, the promise will be resolved before the
// function returns.
if (this._tab && !this._client) {
cleanupAndResolve();
} else if (this._client) {
// If, on the other hand, this target was remoted, the promise will be
// resolved after the remote connection is closed.
this._teardownRemoteListeners();
if (this.isLocalTab) {
// We started with a local tab and created the client ourselves, so we
// should close it.
this._client.close().then(cleanupAndResolve);
} else if (this.activeTab) {
// The client was handed to us, so we are not responsible for closing
// it. We just need to detach from the tab, if already attached.
// |detach| may fail if the connection is already dead, so proceed with
// cleanup directly after this.
this.activeTab.detach();
cleanupAndResolve();
} else {
cleanupAndResolve();
}
}
return this._destroyer.promise;
},
/**
* Clean up references to what this target points to.
*/
_cleanup: function () {
if (this._tab) {
targets.delete(this._tab);
} else {
promiseTargets.delete(this._form);
}
this.activeTab = null;
this.activeConsole = null;
this._client = null;
this._tab = null;
this._form = null;
this._remote = null;
this._root = null;
this._title = null;
this._url = null;
this.threadActor = null;
},
toString: function () {
let id = this._tab ? this._tab : (this._form && this._form.actor);
return `TabTarget:${id}`;
},
/**
* @see TabActor.prototype.onResolveLocation
*/
resolveLocation(loc) {
let deferred = defer();
this.client.request(Object.assign({
to: this._form.actor,
type: "resolveLocation",
}, loc), deferred.resolve);
return deferred.promise;
},
};
/**
* WebProgressListener for TabTarget.
*
* @param object aTarget
* The TabTarget instance to work with.
*/
function TabWebProgressListener(aTarget) {
this.target = aTarget;
}
TabWebProgressListener.prototype = {
target: null,
QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
Ci.nsISupportsWeakReference]),
onStateChange: function (progress, request, flag) {
let isStart = flag & Ci.nsIWebProgressListener.STATE_START;
let isDocument = flag & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT;
let isNetwork = flag & Ci.nsIWebProgressListener.STATE_IS_NETWORK;
let isRequest = flag & Ci.nsIWebProgressListener.STATE_IS_REQUEST;
// Skip non-interesting states.
if (!isStart || !isDocument || !isRequest || !isNetwork) {
return;
}
// emit event if the top frame is navigating
if (progress.isTopLevel) {
// Emit the event if the target is not remoted or store the payload for
// later emission otherwise.
if (this.target._client) {
this.target._navRequest = request;
} else {
this.target.emit("will-navigate", request);
}
}
},
onProgressChange: function () {},
onSecurityChange: function () {},
onStatusChange: function () {},
onLocationChange: function (webProgress, request, URI, flags) {
if (this.target &&
!(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)) {
let window = webProgress.DOMWindow;
// Emit the event if the target is not remoted or store the payload for
// later emission otherwise.
if (this.target._client) {
this.target._navWindow = window;
} else {
this.target.emit("navigate", window);
}
}
},
/**
* Destroy the progress listener instance.
*/
destroy: function () {
if (this.target.tab) {
try {
this.target.tab.linkedBrowser.removeProgressListener(this);
} catch (ex) {
// This can throw when a tab crashes in e10s.
}
}
this.target._webProgressListener = null;
this.target._navRequest = null;
this.target._navWindow = null;
this.target = null;
}
};
function WorkerTarget(workerClient) {
EventEmitter.decorate(this);
this._workerClient = workerClient;
}
/**
* A WorkerTarget represents a worker. Unlike TabTarget, which can represent
* either a local or remote tab, WorkerTarget always represents a remote worker.
* Moreover, unlike TabTarget, which is constructed with a placeholder object
* for remote tabs (from which a TabClient can then be lazily obtained),
* WorkerTarget is constructed with a WorkerClient directly.
*
* WorkerClient is designed to mimic the interface of TabClient as closely as
* possible. This allows us to debug workers as if they were ordinary tabs,
* requiring only minimal changes to the rest of the frontend.
*/
WorkerTarget.prototype = {
get isRemote() {
return true;
},
get isTabActor() {
return true;
},
get name() {
return "Worker";
},
get url() {
return this._workerClient.url;
},
get isWorkerTarget() {
return true;
},
get form() {
return {
consoleActor: this._workerClient.consoleActor
};
},
get activeTab() {
return this._workerClient;
},
get client() {
return this._workerClient.client;
},
destroy: function () {
this._workerClient.detach();
},
hasActor: function (name) {
// console is the only one actor implemented by WorkerActor
if (name == "console") {
return true;
}
return false;
},
getTrait: function () {
return undefined;
},
makeRemote: function () {
return Promise.resolve();
}
};

View file

@ -0,0 +1,6 @@
"use strict";
module.exports = {
// Extend from the shared list of defined globals for mochitests.
"extends": "../../../.eslintrc.mochitests.js"
};

View file

@ -0,0 +1,95 @@
[DEFAULT]
tags = devtools
subsuite = devtools
support-files =
browser_toolbox_options_disable_js.html
browser_toolbox_options_disable_js_iframe.html
browser_toolbox_options_disable_cache.sjs
browser_toolbox_sidebar_tool.xul
browser_toolbox_window_title_changes_page.html
browser_toolbox_window_title_frame_select_page.html
code_binary_search.coffee
code_binary_search.js
code_binary_search.map
code_math.js
code_ugly.js
doc_empty-tab-01.html
head.js
shared-head.js
shared-redux-head.js
helper_disable_cache.js
doc_theme.css
doc_viewsource.html
browser_toolbox_options_enable_serviceworkers_testing_frame_script.js
browser_toolbox_options_enable_serviceworkers_testing.html
serviceworker.js
[browser_browser_toolbox.js]
[browser_browser_toolbox_debugger.js]
[browser_devtools_api.js]
[browser_devtools_api_destroy.js]
[browser_dynamic_tool_enabling.js]
[browser_ignore_toolbox_network_requests.js]
[browser_keybindings_01.js]
[browser_keybindings_02.js]
[browser_keybindings_03.js]
[browser_menu_api.js]
[browser_new_activation_workflow.js]
[browser_source_map-01.js]
[browser_source_map-02.js]
[browser_target_from_url.js]
[browser_target_events.js]
[browser_target_remote.js]
[browser_target_support.js]
[browser_toolbox_custom_host.js]
[browser_toolbox_dynamic_registration.js]
[browser_toolbox_getpanelwhenready.js]
[browser_toolbox_highlight.js]
[browser_toolbox_hosts.js]
[browser_toolbox_hosts_size.js]
[browser_toolbox_hosts_telemetry.js]
[browser_toolbox_keyboard_navigation.js]
skip-if = os == "mac" # Full keyboard navigation on OSX only works if Full Keyboard Access setting is set to All Control in System Keyboard Preferences
[browser_toolbox_minimize.js]
skip-if = true # Bug 1177463 - Temporarily hide the minimize button
[browser_toolbox_options.js]
[browser_toolbox_options_disable_buttons.js]
[browser_toolbox_options_disable_cache-01.js]
[browser_toolbox_options_disable_cache-02.js]
[browser_toolbox_options_disable_js.js]
[browser_toolbox_options_enable_serviceworkers_testing.js]
# [browser_toolbox_raise.js] # Bug 962258
# skip-if = os == "win"
[browser_toolbox_races.js]
[browser_toolbox_ready.js]
[browser_toolbox_remoteness_change.js]
run-if = e10s
[browser_toolbox_select_event.js]
skip-if = e10s # Bug 1069044 - destroyInspector may hang during shutdown
[browser_toolbox_selected_tool_unavailable.js]
[browser_toolbox_sidebar.js]
[browser_toolbox_sidebar_events.js]
[browser_toolbox_sidebar_existing_tabs.js]
[browser_toolbox_sidebar_overflow_menu.js]
[browser_toolbox_split_console.js]
[browser_toolbox_target.js]
[browser_toolbox_tabsswitch_shortcuts.js]
[browser_toolbox_textbox_context_menu.js]
[browser_toolbox_theme_registration.js]
[browser_toolbox_toggle.js]
[browser_toolbox_tool_ready.js]
[browser_toolbox_tool_remote_reopen.js]
[browser_toolbox_transport_events.js]
[browser_toolbox_view_source_01.js]
[browser_toolbox_view_source_02.js]
[browser_toolbox_view_source_03.js]
[browser_toolbox_view_source_04.js]
[browser_toolbox_window_reload_target.js]
[browser_toolbox_window_shortcuts.js]
skip-if = os == "mac" && os_version == "10.8" || os == "win" && os_version == "5.1" # Bug 851129 - Re-enable browser_toolbox_window_shortcuts.js test after leaks are fixed
[browser_toolbox_window_title_changes.js]
[browser_toolbox_window_title_frame_select.js]
[browser_toolbox_zoom.js]
[browser_two_tabs.js]
# We want this test to run for mochitest-dt as well, so we include it here:
[../../../../browser/base/content/test/general/browser_parsable_css.js]

View file

@ -0,0 +1,65 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// On debug test slave, it takes about 50s to run the test.
requestLongerTimeout(4);
add_task(function* runTest() {
yield new Promise(done => {
let options = {"set": [
["devtools.debugger.prompt-connection", false],
["devtools.debugger.remote-enabled", true],
["devtools.chrome.enabled", true],
// Test-only pref to allow passing `testScript` argument to the browser
// toolbox
["devtools.browser-toolbox.allow-unsafe-script", true],
// On debug test slave, it takes more than the default time (20s)
// to get a initialized console
["devtools.debugger.remote-timeout", 120000]
]};
SpecialPowers.pushPrefEnv(options, done);
});
// Wait for a notification sent by a script evaluated in the webconsole
// of the browser toolbox.
let onCustomMessage = new Promise(done => {
Services.obs.addObserver(function listener() {
Services.obs.removeObserver(listener, "browser-toolbox-console-works");
done();
}, "browser-toolbox-console-works", false);
});
// Be careful, this JS function is going to be executed in the addon toolbox,
// which lives in another process. So do not try to use any scope variable!
let env = Components.classes["@mozilla.org/process/environment;1"].getService(Components.interfaces.nsIEnvironment);
let testScript = function () {
toolbox.selectTool("webconsole")
.then(console => {
let { jsterm } = console.hud;
let js = "Services.obs.notifyObservers(null, 'browser-toolbox-console-works', null);";
return jsterm.execute(js);
})
.then(() => toolbox.destroy());
};
env.set("MOZ_TOOLBOX_TEST_SCRIPT", "new " + testScript);
registerCleanupFunction(() => {
env.set("MOZ_TOOLBOX_TEST_SCRIPT", "");
});
let { BrowserToolboxProcess } = Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", {});
let closePromise;
yield new Promise(onRun => {
closePromise = new Promise(onClose => {
info("Opening the browser toolbox\n");
BrowserToolboxProcess.init(onClose, onRun);
});
});
ok(true, "Browser toolbox started\n");
yield onCustomMessage;
ok(true, "Received the custom message");
yield closePromise;
ok(true, "Browser toolbox process just closed");
});

View file

@ -0,0 +1,131 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// On debug test runner, it takes about 50s to run the test.
requestLongerTimeout(4);
const { setInterval, clearInterval } = require("sdk/timers");
add_task(function* runTest() {
yield new Promise(done => {
let options = {"set": [
["devtools.debugger.prompt-connection", false],
["devtools.debugger.remote-enabled", true],
["devtools.chrome.enabled", true],
// Test-only pref to allow passing `testScript` argument to the browser
// toolbox
["devtools.browser-toolbox.allow-unsafe-script", true],
// On debug test runner, it takes more than the default time (20s)
// to get a initialized console
["devtools.debugger.remote-timeout", 120000]
]};
SpecialPowers.pushPrefEnv(options, done);
});
let s = Cu.Sandbox("http://mozilla.org");
// Pass a fake URL to evalInSandbox. If we just pass a filename,
// Debugger is going to fail and only display root folder (`/`) listing.
// But it won't try to fetch this url and use sandbox content as expected.
let testUrl = "http://mozilla.org/browser-toolbox-test.js";
Cu.evalInSandbox("(" + function () {
this.plop = function plop() {
return 1;
};
} + ").call(this)", s, "1.8", testUrl, 0);
// Execute the function every second in order to trigger the breakpoint
let interval = setInterval(s.plop, 1000);
// Be careful, this JS function is going to be executed in the browser toolbox,
// which lives in another process. So do not try to use any scope variable!
let env = Components.classes["@mozilla.org/process/environment;1"]
.getService(Components.interfaces.nsIEnvironment);
let testScript = function () {
const { Task } = Components.utils.import("resource://gre/modules/Task.jsm", {});
dump("Opening the browser toolbox and debugger panel\n");
let window, document;
let testUrl = "http://mozilla.org/browser-toolbox-test.js";
Task.spawn(function* () {
dump("Waiting for debugger load\n");
let panel = yield toolbox.selectTool("jsdebugger");
let window = panel.panelWin;
let document = window.document;
yield window.once(window.EVENTS.SOURCE_SHOWN);
dump("Loaded, selecting the test script to debug\n");
let item = document.querySelector(`.dbg-source-item[tooltiptext="${testUrl}"]`);
let onSourceShown = window.once(window.EVENTS.SOURCE_SHOWN);
item.click();
yield onSourceShown;
dump("Selected, setting a breakpoint\n");
let { Sources, editor } = window.DebuggerView;
let onBreak = window.once(window.EVENTS.FETCHED_SCOPES);
editor.emit("gutterClick", 1);
yield onBreak;
dump("Paused, asserting breakpoint position\n");
let url = Sources.selectedItem.attachment.source.url;
if (url != testUrl) {
throw new Error("Breaking on unexpected script: " + url);
}
let cursor = editor.getCursor();
if (cursor.line != 1) {
throw new Error("Breaking on unexpected line: " + cursor.line);
}
dump("Now, stepping over\n");
let stepOver = window.document.querySelector("#step-over");
let onFetchedScopes = window.once(window.EVENTS.FETCHED_SCOPES);
stepOver.click();
yield onFetchedScopes;
dump("Stepped, asserting step position\n");
url = Sources.selectedItem.attachment.source.url;
if (url != testUrl) {
throw new Error("Stepping on unexpected script: " + url);
}
cursor = editor.getCursor();
if (cursor.line != 2) {
throw new Error("Stepping on unexpected line: " + cursor.line);
}
dump("Resume script execution\n");
let resume = window.document.querySelector("#resume");
let onResume = toolbox.target.once("thread-resumed");
resume.click();
yield onResume;
dump("Close the browser toolbox\n");
toolbox.destroy();
}).catch(error => {
dump("Error while running code in the browser toolbox process:\n");
dump(error + "\n");
dump("stack:\n" + error.stack + "\n");
});
};
env.set("MOZ_TOOLBOX_TEST_SCRIPT", "new " + testScript);
registerCleanupFunction(() => {
env.set("MOZ_TOOLBOX_TEST_SCRIPT", "");
});
let { BrowserToolboxProcess } = Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", {});
// Use two promises, one for each BrowserToolboxProcess.init callback
// arguments, to ensure that we wait for toolbox run and close events.
let closePromise;
yield new Promise(onRun => {
closePromise = new Promise(onClose => {
info("Opening the browser toolbox\n");
BrowserToolboxProcess.init(onClose, onRun);
});
});
ok(true, "Browser toolbox started\n");
yield closePromise;
ok(true, "Browser toolbox process just closed");
clearInterval(interval);
});

View file

@ -0,0 +1,264 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
//
// Whitelisting this test.
// As part of bug 1077403, the leaking uncaught rejections should be fixed.
//
thisTestLeaksUncaughtRejectionsAndShouldBeFixed("TypeError: this.docShell is null");
// When running in a standalone directory, we get this error
thisTestLeaksUncaughtRejectionsAndShouldBeFixed("TypeError: this.doc is undefined");
// Tests devtools API
const toolId1 = "test-tool-1";
const toolId2 = "test-tool-2";
var EventEmitter = require("devtools/shared/event-emitter");
function test() {
addTab("about:blank").then(runTests1);
}
// Test scenario 1: the tool definition build method returns a promise.
function runTests1(aTab) {
let toolDefinition = {
id: toolId1,
isTargetSupported: () => true,
visibilityswitch: "devtools.test-tool.enabled",
url: "about:blank",
label: "someLabel",
build: function (iframeWindow, toolbox) {
let panel = new DevToolPanel(iframeWindow, toolbox);
return panel.open();
},
};
ok(gDevTools, "gDevTools exists");
ok(!gDevTools.getToolDefinitionMap().has(toolId1),
"The tool is not registered");
gDevTools.registerTool(toolDefinition);
ok(gDevTools.getToolDefinitionMap().has(toolId1),
"The tool is registered");
let target = TargetFactory.forTab(gBrowser.selectedTab);
let events = {};
// Check events on the gDevTools and toolbox objects.
gDevTools.once(toolId1 + "-init", (event, toolbox, iframe) => {
ok(iframe, "iframe argument available");
toolbox.once(toolId1 + "-init", (event, iframe) => {
ok(iframe, "iframe argument available");
events["init"] = true;
});
});
gDevTools.once(toolId1 + "-ready", (event, toolbox, panel) => {
ok(panel, "panel argument available");
toolbox.once(toolId1 + "-ready", (event, panel) => {
ok(panel, "panel argument available");
events["ready"] = true;
});
});
gDevTools.showToolbox(target, toolId1).then(function (toolbox) {
is(toolbox.target, target, "toolbox target is correct");
is(toolbox.target.tab, gBrowser.selectedTab, "targeted tab is correct");
ok(events["init"], "init event fired");
ok(events["ready"], "ready event fired");
gDevTools.unregisterTool(toolId1);
// Wait for unregisterTool to select the next tool before calling runTests2,
// otherwise we will receive the wrong select event when waiting for
// unregisterTool to select the next tool in continueTests below.
toolbox.once("select", runTests2);
});
}
// Test scenario 2: the tool definition build method returns panel instance.
function runTests2() {
let toolDefinition = {
id: toolId2,
isTargetSupported: () => true,
visibilityswitch: "devtools.test-tool.enabled",
url: "about:blank",
label: "someLabel",
build: function (iframeWindow, toolbox) {
return new DevToolPanel(iframeWindow, toolbox);
},
};
ok(!gDevTools.getToolDefinitionMap().has(toolId2),
"The tool is not registered");
gDevTools.registerTool(toolDefinition);
ok(gDevTools.getToolDefinitionMap().has(toolId2),
"The tool is registered");
let target = TargetFactory.forTab(gBrowser.selectedTab);
let events = {};
// Check events on the gDevTools and toolbox objects.
gDevTools.once(toolId2 + "-init", (event, toolbox, iframe) => {
ok(iframe, "iframe argument available");
toolbox.once(toolId2 + "-init", (event, iframe) => {
ok(iframe, "iframe argument available");
events["init"] = true;
});
});
gDevTools.once(toolId2 + "-build", (event, toolbox, panel, iframe) => {
ok(panel, "panel argument available");
toolbox.once(toolId2 + "-build", (event, panel, iframe) => {
ok(panel, "panel argument available");
events["build"] = true;
});
});
gDevTools.once(toolId2 + "-ready", (event, toolbox, panel) => {
ok(panel, "panel argument available");
toolbox.once(toolId2 + "-ready", (event, panel) => {
ok(panel, "panel argument available");
events["ready"] = true;
});
});
gDevTools.showToolbox(target, toolId2).then(function (toolbox) {
is(toolbox.target, target, "toolbox target is correct");
is(toolbox.target.tab, gBrowser.selectedTab, "targeted tab is correct");
ok(events["init"], "init event fired");
ok(events["build"], "build event fired");
ok(events["ready"], "ready event fired");
continueTests(toolbox);
});
}
var continueTests = Task.async(function* (toolbox, panel) {
ok(toolbox.getCurrentPanel(), "panel value is correct");
is(toolbox.currentToolId, toolId2, "toolbox _currentToolId is correct");
ok(!toolbox.doc.getElementById("toolbox-tab-" + toolId2).hasAttribute("icon-invertable"),
"The tool tab does not have the invertable attribute");
ok(toolbox.doc.getElementById("toolbox-tab-inspector").hasAttribute("icon-invertable"),
"The builtin tool tabs do have the invertable attribute");
let toolDefinitions = gDevTools.getToolDefinitionMap();
ok(toolDefinitions.has(toolId2), "The tool is in gDevTools");
let toolDefinition = toolDefinitions.get(toolId2);
is(toolDefinition.id, toolId2, "toolDefinition id is correct");
info("Testing toolbox tool-unregistered event");
let toolSelected = toolbox.once("select");
let unregisteredTool = yield new Promise(resolve => {
toolbox.once("tool-unregistered", (e, id) => resolve(id));
gDevTools.unregisterTool(toolId2);
});
yield toolSelected;
is(unregisteredTool, toolId2, "Event returns correct id");
ok(!toolbox.isToolRegistered(toolId2),
"Toolbox: The tool is not registered");
ok(!gDevTools.getToolDefinitionMap().has(toolId2),
"The tool is no longer registered");
info("Testing toolbox tool-registered event");
let registeredTool = yield new Promise(resolve => {
toolbox.once("tool-registered", (e, id) => resolve(id));
gDevTools.registerTool(toolDefinition);
});
is(registeredTool, toolId2, "Event returns correct id");
ok(toolbox.isToolRegistered(toolId2),
"Toolbox: The tool is registered");
ok(gDevTools.getToolDefinitionMap().has(toolId2),
"The tool is registered");
info("Unregistering tool");
gDevTools.unregisterTool(toolId2);
destroyToolbox(toolbox);
});
function destroyToolbox(toolbox) {
toolbox.destroy().then(function () {
let target = TargetFactory.forTab(gBrowser.selectedTab);
ok(gDevTools._toolboxes.get(target) == null, "gDevTools doesn't know about target");
ok(toolbox.target == null, "toolbox doesn't know about target.");
finishUp();
});
}
function finishUp() {
gBrowser.removeCurrentTab();
finish();
}
/**
* When a Toolbox is started it creates a DevToolPanel for each of the tools
* by calling toolDefinition.build(). The returned object should
* at least implement these functions. They will be used by the ToolBox.
*
* There may be no benefit in doing this as an abstract type, but if nothing
* else gives us a place to write documentation.
*/
function DevToolPanel(iframeWindow, toolbox) {
EventEmitter.decorate(this);
this._toolbox = toolbox;
/* let doc = iframeWindow.document
let label = doc.createElement("label");
let textNode = doc.createTextNode("Some Tool");
label.appendChild(textNode);
doc.body.appendChild(label);*/
}
DevToolPanel.prototype = {
open: function () {
let deferred = defer();
executeSoon(() => {
this._isReady = true;
this.emit("ready");
deferred.resolve(this);
});
return deferred.promise;
},
get target() {
return this._toolbox.target;
},
get toolbox() {
return this._toolbox;
},
get isReady() {
return this._isReady;
},
_isReady: false,
destroy: function DTI_destroy() {
return defer(null);
},
};

View file

@ -0,0 +1,71 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
// Tests devtools API
function test() {
addTab("about:blank").then(runTests);
}
function runTests(aTab) {
let toolDefinition = {
id: "testTool",
visibilityswitch: "devtools.testTool.enabled",
isTargetSupported: () => true,
url: "about:blank",
label: "someLabel",
build: function (iframeWindow, toolbox) {
let deferred = defer();
executeSoon(() => {
deferred.resolve({
target: toolbox.target,
toolbox: toolbox,
isReady: true,
destroy: function () {},
});
});
return deferred.promise;
},
};
gDevTools.registerTool(toolDefinition);
let collectedEvents = [];
let target = TargetFactory.forTab(aTab);
gDevTools.showToolbox(target, toolDefinition.id).then(function (toolbox) {
let panel = toolbox.getPanel(toolDefinition.id);
ok(panel, "Tool open");
gDevTools.once("toolbox-destroy", (event, toolbox, iframe) => {
collectedEvents.push(event);
});
gDevTools.once(toolDefinition.id + "-destroy", (event, toolbox, iframe) => {
collectedEvents.push("gDevTools-" + event);
});
toolbox.once("destroy", (event) => {
collectedEvents.push(event);
});
toolbox.once(toolDefinition.id + "-destroy", (event) => {
collectedEvents.push("toolbox-" + event);
});
toolbox.destroy().then(function () {
is(collectedEvents.join(":"),
"toolbox-destroy:destroy:gDevTools-testTool-destroy:toolbox-testTool-destroy",
"Found the right amount of collected events.");
gDevTools.unregisterTool(toolDefinition.id);
gBrowser.removeCurrentTab();
executeSoon(function () {
finish();
});
});
});
}

View file

@ -0,0 +1,41 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
// Tests that toggling prefs immediately (de)activates the relevant menuitem
var gItemsToTest = {
"menu_devToolbar": "devtools.toolbar.enabled",
"menu_browserToolbox": ["devtools.chrome.enabled", "devtools.debugger.remote-enabled"],
"menu_devtools_connect": "devtools.debugger.remote-enabled",
};
function expectedAttributeValueFromPrefs(prefs) {
return prefs.every((pref) => Services.prefs.getBoolPref(pref)) ?
"" : "true";
}
function checkItem(el, prefs) {
let expectedValue = expectedAttributeValueFromPrefs(prefs);
is(el.getAttribute("disabled"), expectedValue, "disabled attribute should match current pref state");
is(el.getAttribute("hidden"), expectedValue, "hidden attribute should match current pref state");
}
function test() {
for (let k in gItemsToTest) {
let el = document.getElementById(k);
let prefs = gItemsToTest[k];
if (typeof prefs == "string") {
prefs = [prefs];
}
checkItem(el, prefs);
for (let pref of prefs) {
Services.prefs.setBoolPref(pref, !Services.prefs.getBoolPref(pref));
checkItem(el, prefs);
Services.prefs.setBoolPref(pref, !Services.prefs.getBoolPref(pref));
checkItem(el, prefs);
}
}
finish();
}

View file

@ -0,0 +1,33 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that network requests originating from the toolbox don't get recorded in
// the network panel.
add_task(function* () {
// TODO: This test tries to verify the normal behavior of the netmonitor and
// therefore needs to avoid the explicit check for tests. Bug 1167188 will
// allow us to remove this workaround.
let isTesting = flags.testing;
flags.testing = false;
let tab = yield addTab(URL_ROOT + "doc_viewsource.html");
let target = TargetFactory.forTab(tab);
let toolbox = yield gDevTools.showToolbox(target, "styleeditor");
let panel = toolbox.getPanel("styleeditor");
is(panel.UI.editors.length, 1, "correct number of editors opened");
let monitor = yield toolbox.selectTool("netmonitor");
let { RequestsMenu } = monitor.panelWin.NetMonitorView;
is(RequestsMenu.itemCount, 0, "No network requests appear in the network panel");
yield gDevTools.closeToolbox(target);
tab = target = toolbox = panel = null;
gBrowser.removeCurrentTab();
flags.testing = isTesting;
});

View file

@ -0,0 +1,115 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
// Tests that the keybindings for opening and closing the inspector work as expected
// Can probably make this a shared test that tests all of the tools global keybindings
const TEST_URL = "data:text/html,<html><head><title>Test for the " +
"highlighter keybindings</title></head><body>" +
"<h1>Keybindings!</h1></body></html>"
function test()
{
waitForExplicitFinish();
let doc;
let node;
let inspector;
let keysetMap = { };
addTab(TEST_URL).then(function () {
doc = content.document;
node = doc.querySelector("h1");
waitForFocus(setupKeyBindingsTest);
});
function buildDevtoolsKeysetMap(keyset) {
[].forEach.call(keyset.querySelectorAll("key"), function (key) {
if (!key.getAttribute("key")) {
return;
}
let modifiers = key.getAttribute("modifiers");
keysetMap[key.id.split("_")[1]] = {
key: key.getAttribute("key"),
modifiers: modifiers,
modifierOpt: {
shiftKey: modifiers.match("shift"),
ctrlKey: modifiers.match("ctrl"),
altKey: modifiers.match("alt"),
metaKey: modifiers.match("meta"),
accelKey: modifiers.match("accel")
},
synthesizeKey: function () {
EventUtils.synthesizeKey(this.key, this.modifierOpt);
}
};
});
}
function setupKeyBindingsTest()
{
for (let win of gDevToolsBrowser._trackedBrowserWindows) {
buildDevtoolsKeysetMap(win.document.getElementById("devtoolsKeyset"));
}
gDevTools.once("toolbox-ready", (e, toolbox) => {
inspectorShouldBeOpenAndHighlighting(toolbox.getCurrentPanel(), toolbox);
});
keysetMap.inspector.synthesizeKey();
}
function inspectorShouldBeOpenAndHighlighting(aInspector, aToolbox)
{
is(aToolbox.currentToolId, "inspector", "Correct tool has been loaded");
aToolbox.once("picker-started", () => {
ok(true, "picker-started event received, highlighter started");
keysetMap.inspector.synthesizeKey();
aToolbox.once("picker-stopped", () => {
ok(true, "picker-stopped event received, highlighter stopped");
gDevTools.once("select-tool-command", () => {
webconsoleShouldBeSelected(aToolbox);
});
keysetMap.webconsole.synthesizeKey();
});
});
}
function webconsoleShouldBeSelected(aToolbox)
{
is(aToolbox.currentToolId, "webconsole", "webconsole should be selected.");
gDevTools.once("select-tool-command", () => {
jsdebuggerShouldBeSelected(aToolbox);
});
keysetMap.jsdebugger.synthesizeKey();
}
function jsdebuggerShouldBeSelected(aToolbox)
{
is(aToolbox.currentToolId, "jsdebugger", "jsdebugger should be selected.");
gDevTools.once("select-tool-command", () => {
netmonitorShouldBeSelected(aToolbox);
});
keysetMap.netmonitor.synthesizeKey();
}
function netmonitorShouldBeSelected(aToolbox, panel)
{
is(aToolbox.currentToolId, "netmonitor", "netmonitor should be selected.");
finishUp();
}
function finishUp() {
doc = node = inspector = keysetMap = null;
gBrowser.removeCurrentTab();
finish();
}
}

View file

@ -0,0 +1,65 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that the toolbox keybindings still work after the host is changed.
const URL = "data:text/html;charset=utf8,test page";
var {Toolbox} = require("devtools/client/framework/toolbox");
const {LocalizationHelper} = require("devtools/shared/l10n");
const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
function getZoomValue() {
return parseFloat(Services.prefs.getCharPref("devtools.toolbox.zoomValue"));
}
add_task(function* () {
info("Create a test tab and open the toolbox");
let tab = yield addTab(URL);
let target = TargetFactory.forTab(tab);
let toolbox = yield gDevTools.showToolbox(target, "webconsole");
let {SIDE, BOTTOM} = Toolbox.HostType;
for (let type of [SIDE, BOTTOM, SIDE]) {
info("Switch to host type " + type);
yield toolbox.switchHost(type);
info("Try to use the toolbox shortcuts");
yield checkKeyBindings(toolbox);
}
Services.prefs.clearUserPref("devtools.toolbox.zoomValue");
Services.prefs.setCharPref("devtools.toolbox.host", BOTTOM);
yield toolbox.destroy();
gBrowser.removeCurrentTab();
});
function zoomWithKey(toolbox, key) {
let shortcut = L10N.getStr(key);
if (!shortcut) {
info("Key was empty, skipping zoomWithKey");
return;
}
info("Zooming with key: " + key);
let currentZoom = getZoomValue();
synthesizeKeyShortcut(shortcut, toolbox.win);
isnot(getZoomValue(), currentZoom, "The zoom level was changed in the toolbox");
}
function* checkKeyBindings(toolbox) {
zoomWithKey(toolbox, "toolbox.zoomIn.key");
zoomWithKey(toolbox, "toolbox.zoomIn2.key");
zoomWithKey(toolbox, "toolbox.zoomIn3.key");
zoomWithKey(toolbox, "toolbox.zoomReset.key");
zoomWithKey(toolbox, "toolbox.zoomOut.key");
zoomWithKey(toolbox, "toolbox.zoomOut2.key");
zoomWithKey(toolbox, "toolbox.zoomReset2.key");
}

View file

@ -0,0 +1,53 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that the toolbox 'switch to previous host' feature works.
// Pressing ctrl/cmd+shift+d should switch to the last used host.
const URL = "data:text/html;charset=utf8,test page for toolbox switching";
var {Toolbox} = require("devtools/client/framework/toolbox");
const {LocalizationHelper} = require("devtools/shared/l10n");
const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
add_task(function* () {
info("Create a test tab and open the toolbox");
let tab = yield addTab(URL);
let target = TargetFactory.forTab(tab);
let toolbox = yield gDevTools.showToolbox(target, "webconsole");
let shortcut = L10N.getStr("toolbox.toggleHost.key");
let {SIDE, BOTTOM, WINDOW} = Toolbox.HostType;
checkHostType(toolbox, BOTTOM, SIDE);
info("Switching from bottom to side");
let onHostChanged = toolbox.once("host-changed");
synthesizeKeyShortcut(shortcut, toolbox.win);
yield onHostChanged;
checkHostType(toolbox, SIDE, BOTTOM);
info("Switching from side to bottom");
onHostChanged = toolbox.once("host-changed");
synthesizeKeyShortcut(shortcut, toolbox.win);
yield onHostChanged;
checkHostType(toolbox, BOTTOM, SIDE);
info("Switching to window");
yield toolbox.switchHost(WINDOW);
checkHostType(toolbox, WINDOW, BOTTOM);
info("Switching from window to bottom");
onHostChanged = toolbox.once("host-changed");
synthesizeKeyShortcut(shortcut, toolbox.win);
yield onHostChanged;
checkHostType(toolbox, BOTTOM, WINDOW);
yield toolbox.destroy();
gBrowser.removeCurrentTab();
});

View file

@ -0,0 +1,181 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that the Menu API works
const URL = "data:text/html;charset=utf8,test page for menu api";
const Menu = require("devtools/client/framework/menu");
const MenuItem = require("devtools/client/framework/menu-item");
add_task(function* () {
info("Create a test tab and open the toolbox");
let tab = yield addTab(URL);
let target = TargetFactory.forTab(tab);
let toolbox = yield gDevTools.showToolbox(target, "webconsole");
yield testMenuItems();
yield testMenuPopup(toolbox);
yield testSubmenu(toolbox);
});
function* testMenuItems() {
let menu = new Menu();
let menuItem1 = new MenuItem();
let menuItem2 = new MenuItem();
menu.append(menuItem1);
menu.append(menuItem2);
is(menu.items.length, 2, "Correct number of 'items'");
is(menu.items[0], menuItem1, "Correct reference to MenuItem");
is(menu.items[1], menuItem2, "Correct reference to MenuItem");
}
function* testMenuPopup(toolbox) {
let clickFired = false;
let menu = new Menu({
id: "menu-popup",
});
menu.append(new MenuItem({ type: "separator" }));
let MENU_ITEMS = [
new MenuItem({
id: "menu-item-1",
label: "Normal Item",
click: () => {
info("Click callback has fired for menu item");
clickFired = true;
},
}),
new MenuItem({
label: "Checked Item",
type: "checkbox",
checked: true,
}),
new MenuItem({
label: "Radio Item",
type: "radio",
}),
new MenuItem({
label: "Disabled Item",
disabled: true,
}),
];
for (let item of MENU_ITEMS) {
menu.append(item);
}
// Append an invisible MenuItem, which shouldn't show up in the DOM
menu.append(new MenuItem({
label: "Invisible",
visible: false,
}));
menu.popup(0, 0, toolbox);
ok(toolbox.doc.querySelector("#menu-popup"), "A popup is in the DOM");
let menuSeparators =
toolbox.doc.querySelectorAll("#menu-popup > menuseparator");
is(menuSeparators.length, 1, "A separator is in the menu");
let menuItems = toolbox.doc.querySelectorAll("#menu-popup > menuitem");
is(menuItems.length, MENU_ITEMS.length, "Correct number of menuitems");
is(menuItems[0].id, MENU_ITEMS[0].id, "Correct id for menuitem");
is(menuItems[0].getAttribute("label"), MENU_ITEMS[0].label, "Correct label");
is(menuItems[1].getAttribute("label"), MENU_ITEMS[1].label, "Correct label");
is(menuItems[1].getAttribute("type"), "checkbox", "Correct type attr");
is(menuItems[1].getAttribute("checked"), "true", "Has checked attr");
is(menuItems[2].getAttribute("label"), MENU_ITEMS[2].label, "Correct label");
is(menuItems[2].getAttribute("type"), "radio", "Correct type attr");
ok(!menuItems[2].hasAttribute("checked"), "Doesn't have checked attr");
is(menuItems[3].getAttribute("label"), MENU_ITEMS[3].label, "Correct label");
is(menuItems[3].getAttribute("disabled"), "true", "disabled attr menuitem");
yield once(menu, "open");
let closed = once(menu, "close");
EventUtils.synthesizeMouseAtCenter(menuItems[0], {}, toolbox.win);
yield closed;
ok(clickFired, "Click has fired");
ok(!toolbox.doc.querySelector("#menu-popup"), "Popup removed from the DOM");
}
function* testSubmenu(toolbox) {
let clickFired = false;
let menu = new Menu({
id: "menu-popup",
});
let submenu = new Menu({
id: "submenu-popup",
});
submenu.append(new MenuItem({
label: "Submenu item",
click: () => {
info("Click callback has fired for submenu item");
clickFired = true;
},
}));
menu.append(new MenuItem({
label: "Submenu parent",
submenu: submenu,
}));
menu.append(new MenuItem({
label: "Submenu parent with attributes",
id: "submenu-parent-with-attrs",
submenu: submenu,
accesskey: "A",
disabled: true,
}));
menu.popup(0, 0, toolbox);
ok(toolbox.doc.querySelector("#menu-popup"), "A popup is in the DOM");
is(toolbox.doc.querySelectorAll("#menu-popup > menuitem").length, 0,
"No menuitem children");
let menus = toolbox.doc.querySelectorAll("#menu-popup > menu");
is(menus.length, 2, "Correct number of menus");
is(menus[0].getAttribute("label"), "Submenu parent", "Correct label");
ok(!menus[0].hasAttribute("disabled"), "Correct disabled state");
is(menus[1].getAttribute("accesskey"), "A", "Correct accesskey");
ok(menus[1].hasAttribute("disabled"), "Correct disabled state");
ok(menus[1].id, "submenu-parent-with-attrs", "Correct id");
let subMenuItems = menus[0].querySelectorAll("menupopup > menuitem");
is(subMenuItems.length, 1, "Correct number of submenu items");
is(subMenuItems[0].getAttribute("label"), "Submenu item", "Correct label");
yield once(menu, "open");
let closed = once(menu, "close");
info("Using keyboard navigation to open, close, and reopen the submenu");
let shown = once(menus[0], "popupshown");
EventUtils.synthesizeKey("VK_DOWN", {});
EventUtils.synthesizeKey("VK_RIGHT", {});
yield shown;
let hidden = once(menus[0], "popuphidden");
EventUtils.synthesizeKey("VK_LEFT", {});
yield hidden;
shown = once(menus[0], "popupshown");
EventUtils.synthesizeKey("VK_RIGHT", {});
yield shown;
info("Clicking the submenu item");
EventUtils.synthesizeMouseAtCenter(subMenuItems[0], {}, toolbox.win);
yield closed;
ok(clickFired, "Click has fired");
}

View file

@ -0,0 +1,69 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
// Tests devtools API
var toolbox, target;
var tempScope = {};
function test() {
addTab("about:blank").then(function (aTab) {
target = TargetFactory.forTab(gBrowser.selectedTab);
loadWebConsole(aTab).then(function () {
console.log("loaded");
});
});
}
function loadWebConsole(aTab) {
ok(gDevTools, "gDevTools exists");
return gDevTools.showToolbox(target, "webconsole").then(function (aToolbox) {
toolbox = aToolbox;
checkToolLoading();
});
}
function checkToolLoading() {
is(toolbox.currentToolId, "webconsole", "The web console is selected");
ok(toolbox.isReady, "toolbox is ready");
selectAndCheckById("jsdebugger").then(function () {
selectAndCheckById("styleeditor").then(function () {
testToggle();
});
});
}
function selectAndCheckById(id) {
return toolbox.selectTool(id).then(function () {
let tab = toolbox.doc.getElementById("toolbox-tab-" + id);
is(tab.hasAttribute("selected"), true, "The " + id + " tab is selected");
});
}
function testToggle() {
toolbox.once("destroyed", () => {
// Cannot reuse a target after it's destroyed.
target = TargetFactory.forTab(gBrowser.selectedTab);
gDevTools.showToolbox(target, "styleeditor").then(function (aToolbox) {
toolbox = aToolbox;
is(toolbox.currentToolId, "styleeditor", "The style editor is selected");
finishUp();
});
});
toolbox.destroy();
}
function finishUp() {
toolbox.destroy().then(function () {
toolbox = null;
target = null;
gBrowser.removeCurrentTab();
finish();
});
}

View file

@ -0,0 +1,115 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Whitelisting this test.
// As part of bug 1077403, the leaking uncaught rejections should be fixed.
thisTestLeaksUncaughtRejectionsAndShouldBeFixed("[object Object]");
thisTestLeaksUncaughtRejectionsAndShouldBeFixed(
"TypeError: this.transport is null");
/**
* Tests the SourceMapService updates generated sources when source maps
* are subsequently found. Also checks when no column is provided, and
* when tagging an already source mapped location initially.
*/
// Force the old debugger UI since it's directly used (see Bug 1301705)
Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
registerCleanupFunction(function* () {
Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
});
const DEBUGGER_ROOT = "http://example.com/browser/devtools/client/debugger/test/mochitest/";
// Empty page
const PAGE_URL = `${DEBUGGER_ROOT}doc_empty-tab-01.html`;
const JS_URL = `${URL_ROOT}code_binary_search.js`;
const COFFEE_URL = `${URL_ROOT}code_binary_search.coffee`;
const { SourceMapService } = require("devtools/client/framework/source-map-service");
const { serialize } = require("devtools/client/framework/location-store");
add_task(function* () {
const toolbox = yield openNewTabAndToolbox(PAGE_URL, "jsdebugger");
const service = new SourceMapService(toolbox.target);
let aggregator = new Map();
function onUpdate(e, oldLoc, newLoc) {
if (oldLoc.line === 6) {
checkLoc1(oldLoc, newLoc);
} else if (oldLoc.line === 8) {
checkLoc2(oldLoc, newLoc);
} else {
throw new Error(`Unexpected location update: ${JSON.stringify(oldLoc)}`);
}
aggregator.set(serialize(oldLoc), newLoc);
}
let loc1 = { url: JS_URL, line: 6 };
let loc2 = { url: JS_URL, line: 8, column: 3 };
service.subscribe(loc1, onUpdate);
service.subscribe(loc2, onUpdate);
// Inject JS script
let sourceShown = waitForSourceShown(toolbox.getCurrentPanel(), "code_binary_search");
yield createScript(JS_URL);
yield sourceShown;
yield waitUntil(() => aggregator.size === 2);
aggregator = Array.from(aggregator.values());
ok(aggregator.find(i => i.url === COFFEE_URL && i.line === 4), "found first updated location");
ok(aggregator.find(i => i.url === COFFEE_URL && i.line === 6), "found second updated location");
yield toolbox.destroy();
gBrowser.removeCurrentTab();
finish();
});
function checkLoc1(oldLoc, newLoc) {
is(oldLoc.line, 6, "Correct line for JS:6");
is(oldLoc.column, null, "Correct column for JS:6");
is(oldLoc.url, JS_URL, "Correct url for JS:6");
is(newLoc.line, 4, "Correct line for JS:6 -> COFFEE");
is(newLoc.column, 2, "Correct column for JS:6 -> COFFEE -- handles falsy column entries");
is(newLoc.url, COFFEE_URL, "Correct url for JS:6 -> COFFEE");
}
function checkLoc2(oldLoc, newLoc) {
is(oldLoc.line, 8, "Correct line for JS:8:3");
is(oldLoc.column, 3, "Correct column for JS:8:3");
is(oldLoc.url, JS_URL, "Correct url for JS:8:3");
is(newLoc.line, 6, "Correct line for JS:8:3 -> COFFEE");
is(newLoc.column, 10, "Correct column for JS:8:3 -> COFFEE");
is(newLoc.url, COFFEE_URL, "Correct url for JS:8:3 -> COFFEE");
}
function createScript(url) {
info(`Creating script: ${url}`);
let mm = getFrameScript();
let command = `
let script = document.createElement("script");
script.setAttribute("src", "${url}");
document.body.appendChild(script);
null;
`;
return evalInDebuggee(mm, command);
}
function waitForSourceShown(debuggerPanel, url) {
let { panelWin } = debuggerPanel;
let deferred = defer();
info(`Waiting for source ${url} to be shown in the debugger...`);
panelWin.on(panelWin.EVENTS.SOURCE_SHOWN, function onSourceShown(_, source) {
let sourceUrl = source.url || source.generatedUrl;
if (sourceUrl.includes(url)) {
panelWin.off(panelWin.EVENTS.SOURCE_SHOWN, onSourceShown);
info(`Source shown for ${url}`);
deferred.resolve(source);
}
});
return deferred.promise;
}

View file

@ -0,0 +1,113 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests the SourceMapService updates generated sources when pretty printing
* and un pretty printing.
*/
// Force the old debugger UI since it's directly used (see Bug 1301705)
Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
registerCleanupFunction(function* () {
Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
});
const DEBUGGER_ROOT = "http://example.com/browser/devtools/client/debugger/test/mochitest/";
// Empty page
const PAGE_URL = `${DEBUGGER_ROOT}doc_empty-tab-01.html`;
const JS_URL = `${URL_ROOT}code_ugly.js`;
const { SourceMapService } = require("devtools/client/framework/source-map-service");
add_task(function* () {
let toolbox = yield openNewTabAndToolbox(PAGE_URL, "jsdebugger");
let service = new SourceMapService(toolbox.target);
let checkedPretty = false;
let checkedUnpretty = false;
function onUpdate(e, oldLoc, newLoc) {
if (oldLoc.line === 3) {
checkPrettified(oldLoc, newLoc);
checkedPretty = true;
} else if (oldLoc.line === 9) {
checkUnprettified(oldLoc, newLoc);
checkedUnpretty = true;
} else {
throw new Error(`Unexpected location update: ${JSON.stringify(oldLoc)}`);
}
}
const loc1 = { url: JS_URL, line: 3 };
service.subscribe(loc1, onUpdate);
// Inject JS script
let sourceShown = waitForSourceShown(toolbox.getCurrentPanel(), "code_ugly.js");
yield createScript(JS_URL);
yield sourceShown;
let ppButton = toolbox.getCurrentPanel().panelWin.document.getElementById("pretty-print");
sourceShown = waitForSourceShown(toolbox.getCurrentPanel(), "code_ugly.js");
ppButton.click();
yield sourceShown;
yield waitUntil(() => checkedPretty);
// TODO check unprettified change once bug 1177446 fixed
// info("Testing un-pretty printing.");
// sourceShown = waitForSourceShown(toolbox.getCurrentPanel(), "code_ugly.js");
// ppButton.click();
// yield sourceShown;
// yield waitUntil(() => checkedUnpretty);
yield toolbox.destroy();
gBrowser.removeCurrentTab();
finish();
});
function checkPrettified(oldLoc, newLoc) {
is(oldLoc.line, 3, "Correct line for JS:3");
is(oldLoc.column, null, "Correct column for JS:3");
is(oldLoc.url, JS_URL, "Correct url for JS:3");
is(newLoc.line, 9, "Correct line for JS:3 -> PRETTY");
is(newLoc.column, 0, "Correct column for JS:3 -> PRETTY");
is(newLoc.url, JS_URL, "Correct url for JS:3 -> PRETTY");
}
function checkUnprettified(oldLoc, newLoc) {
is(oldLoc.line, 9, "Correct line for JS:3 -> PRETTY");
is(oldLoc.column, 0, "Correct column for JS:3 -> PRETTY");
is(oldLoc.url, JS_URL, "Correct url for JS:3 -> PRETTY");
is(newLoc.line, 3, "Correct line for JS:3 -> UNPRETTIED");
is(newLoc.column, null, "Correct column for JS:3 -> UNPRETTIED");
is(newLoc.url, JS_URL, "Correct url for JS:3 -> UNPRETTIED");
}
function createScript(url) {
info(`Creating script: ${url}`);
let mm = getFrameScript();
let command = `
let script = document.createElement("script");
script.setAttribute("src", "${url}");
document.body.appendChild(script);
`;
return evalInDebuggee(mm, command);
}
function waitForSourceShown(debuggerPanel, url) {
let { panelWin } = debuggerPanel;
let deferred = defer();
info(`Waiting for source ${url} to be shown in the debugger...`);
panelWin.on(panelWin.EVENTS.SOURCE_SHOWN, function onSourceShown(_, source) {
let sourceUrl = source.url || source.introductionUrl;
if (sourceUrl.includes(url)) {
panelWin.off(panelWin.EVENTS.SOURCE_SHOWN, onSourceShown);
info(`Source shown for ${url}`);
deferred.resolve(source);
}
});
return deferred.promise;
}

View file

@ -0,0 +1,56 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
var target;
function test()
{
waitForExplicitFinish();
gBrowser.selectedTab = gBrowser.addTab();
BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(onLoad);
}
function onLoad() {
target = TargetFactory.forTab(gBrowser.selectedTab);
is(target.tab, gBrowser.selectedTab, "Target linked to the right tab.");
target.once("hidden", onHidden);
gBrowser.selectedTab = gBrowser.addTab();
}
function onHidden() {
ok(true, "Hidden event received");
target.once("visible", onVisible);
gBrowser.removeCurrentTab();
}
function onVisible() {
ok(true, "Visible event received");
target.once("will-navigate", onWillNavigate);
let mm = getFrameScript();
mm.sendAsyncMessage("devtools:test:navigate", { location: "data:text/html,<meta charset='utf8'/>test navigation" });
}
function onWillNavigate(event, request) {
ok(true, "will-navigate event received");
// Wait for navigation handling to complete before removing the tab, in order
// to avoid triggering assertions.
target.once("navigate", executeSoon.bind(null, onNavigate));
}
function onNavigate() {
ok(true, "navigate event received");
target.once("close", onClose);
gBrowser.removeCurrentTab();
}
function onClose() {
ok(true, "close event received");
target = null;
finish();
}

View file

@ -0,0 +1,133 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const TEST_URI = "data:text/html;charset=utf-8," +
"<p>browser_target-from-url.js</p>";
const { DevToolsLoader } = Cu.import("resource://devtools/shared/Loader.jsm", {});
const { targetFromURL } = require("devtools/client/framework/target-from-url");
Services.prefs.setBoolPref("devtools.debugger.remote-enabled", true);
Services.prefs.setBoolPref("devtools.debugger.prompt-connection", false);
SimpleTest.registerCleanupFunction(() => {
Services.prefs.clearUserPref("devtools.debugger.remote-enabled");
Services.prefs.clearUserPref("devtools.debugger.prompt-connection");
});
function assertIsTabTarget(target, url, chrome = false) {
is(target.url, url);
is(target.isLocalTab, false);
is(target.chrome, chrome);
is(target.isTabActor, true);
is(target.isRemote, true);
}
add_task(function* () {
let tab = yield addTab(TEST_URI);
let browser = tab.linkedBrowser;
let target;
info("Test invalid type");
try {
yield targetFromURL(new URL("http://foo?type=x"));
ok(false, "Shouldn't pass");
} catch (e) {
is(e.message, "targetFromURL, unsupported type='x' parameter");
}
info("Test tab");
let windowId = browser.outerWindowID;
target = yield targetFromURL(new URL("http://foo?type=tab&id=" + windowId));
assertIsTabTarget(target, TEST_URI);
info("Test tab with chrome privileges");
target = yield targetFromURL(new URL("http://foo?type=tab&id=" + windowId + "&chrome"));
assertIsTabTarget(target, TEST_URI, true);
info("Test invalid tab id");
try {
yield targetFromURL(new URL("http://foo?type=tab&id=10000"));
ok(false, "Shouldn't pass");
} catch (e) {
is(e.message, "targetFromURL, tab with outerWindowID:'10000' doesn't exist");
}
info("Test parent process");
target = yield targetFromURL(new URL("http://foo?type=process"));
let topWindow = Services.wm.getMostRecentWindow("navigator:browser");
assertIsTabTarget(target, topWindow.location.href, true);
yield testRemoteTCP();
yield testRemoteWebSocket();
gBrowser.removeCurrentTab();
});
function* setupDebuggerServer(websocket) {
info("Create a separate loader instance for the DebuggerServer.");
let loader = new DevToolsLoader();
let { DebuggerServer } = loader.require("devtools/server/main");
DebuggerServer.init();
DebuggerServer.addBrowserActors();
DebuggerServer.allowChromeProcess = true;
let listener = DebuggerServer.createListener();
ok(listener, "Socket listener created");
// Pass -1 to automatically choose an available port
listener.portOrPath = -1;
listener.webSocket = websocket;
yield listener.open();
is(DebuggerServer.listeningSockets, 1, "1 listening socket");
return { DebuggerServer, listener };
}
function teardownDebuggerServer({ DebuggerServer, listener }) {
info("Close the listener socket");
listener.close();
is(DebuggerServer.listeningSockets, 0, "0 listening sockets");
info("Destroy the temporary debugger server");
DebuggerServer.destroy();
}
function* testRemoteTCP() {
info("Test remote process via TCP Connection");
let server = yield setupDebuggerServer(false);
let { port } = server.listener;
let target = yield targetFromURL(new URL("http://foo?type=process&host=127.0.0.1&port=" + port));
let topWindow = Services.wm.getMostRecentWindow("navigator:browser");
assertIsTabTarget(target, topWindow.location.href, true);
let settings = target.client._transport.connectionSettings;
is(settings.host, "127.0.0.1");
is(settings.port, port);
is(settings.webSocket, false);
yield target.client.close();
teardownDebuggerServer(server);
}
function* testRemoteWebSocket() {
info("Test remote process via WebSocket Connection");
let server = yield setupDebuggerServer(true);
let { port } = server.listener;
let target = yield targetFromURL(new URL("http://foo?type=process&host=127.0.0.1&port=" + port + "&ws=true"));
let topWindow = Services.wm.getMostRecentWindow("navigator:browser");
assertIsTabTarget(target, topWindow.location.href, true);
let settings = target.client._transport.connectionSettings;
is(settings.host, "127.0.0.1");
is(settings.port, port);
is(settings.webSocket, true);
yield target.client.close();
teardownDebuggerServer(server);
}

View file

@ -0,0 +1,25 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
// Ensure target is closed if client is closed directly
function test() {
waitForExplicitFinish();
getChromeActors((client, response) => {
let options = {
form: response,
client: client,
chrome: true
};
TargetFactory.forRemoteTab(options).then(target => {
target.on("close", () => {
ok(true, "Target was closed");
finish();
});
client.close();
});
});
}

View file

@ -0,0 +1,74 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
// Test support methods on Target, such as `hasActor`, `getActorDescription`,
// `actorHasMethod` and `getTrait`.
var { WebAudioFront } =
require("devtools/shared/fronts/webaudio");
function* testTarget(client, target) {
yield target.makeRemote();
is(target.hasActor("timeline"), true, "target.hasActor() true when actor exists.");
is(target.hasActor("webaudio"), true, "target.hasActor() true when actor exists.");
is(target.hasActor("notreal"), false, "target.hasActor() false when actor does not exist.");
// Create a front to ensure the actor is loaded
let front = new WebAudioFront(target.client, target.form);
let desc = yield target.getActorDescription("webaudio");
is(desc.typeName, "webaudio",
"target.getActorDescription() returns definition data for corresponding actor");
is(desc.events["start-context"]["type"], "startContext",
"target.getActorDescription() returns event data for corresponding actor");
desc = yield target.getActorDescription("nope");
is(desc, undefined, "target.getActorDescription() returns undefined for non-existing actor");
desc = yield target.getActorDescription();
is(desc, undefined, "target.getActorDescription() returns undefined for undefined actor");
let hasMethod = yield target.actorHasMethod("audionode", "getType");
is(hasMethod, true,
"target.actorHasMethod() returns true for existing actor with method");
hasMethod = yield target.actorHasMethod("audionode", "nope");
is(hasMethod, false,
"target.actorHasMethod() returns false for existing actor with no method");
hasMethod = yield target.actorHasMethod("nope", "nope");
is(hasMethod, false,
"target.actorHasMethod() returns false for non-existing actor with no method");
hasMethod = yield target.actorHasMethod();
is(hasMethod, false,
"target.actorHasMethod() returns false for undefined params");
is(target.getTrait("customHighlighters"), true,
"target.getTrait() returns boolean when trait exists");
is(target.getTrait("giddyup"), undefined,
"target.getTrait() returns undefined when trait does not exist");
close(target, client);
}
// Ensure target is closed if client is closed directly
function test() {
waitForExplicitFinish();
getChromeActors((client, response) => {
let options = {
form: response,
client: client,
chrome: true
};
TargetFactory.forRemoteTab(options).then(Task.async(testTarget).bind(null, client));
});
}
function close(target, client) {
target.on("close", () => {
ok(true, "Target was closed");
finish();
});
client.close();
}

View file

@ -0,0 +1,57 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
const TEST_URL = "data:text/html,test custom host";
function test() {
let {Toolbox} = require("devtools/client/framework/toolbox");
let toolbox, iframe, target;
window.addEventListener("message", onMessage);
iframe = document.createElement("iframe");
document.documentElement.appendChild(iframe);
addTab(TEST_URL).then(function (tab) {
target = TargetFactory.forTab(tab);
let options = {customIframe: iframe};
gDevTools.showToolbox(target, null, Toolbox.HostType.CUSTOM, options)
.then(testCustomHost, console.error)
.then(null, console.error);
});
function onMessage(event) {
if (typeof(event.data) !== "string") {
return;
}
info("onMessage: " + event.data);
let json = JSON.parse(event.data);
if (json.name == "toolbox-close") {
ok("Got the `toolbox-close` message");
window.removeEventListener("message", onMessage);
cleanup();
}
}
function testCustomHost(t) {
toolbox = t;
is(toolbox.win.top, window, "Toolbox is included in browser.xul");
is(toolbox.doc, iframe.contentDocument, "Toolbox is in the custom iframe");
executeSoon(() => gBrowser.removeCurrentTab());
}
function cleanup() {
iframe.remove();
// Even if we received "toolbox-close", the toolbox may still be destroying
// toolbox.destroy() returns a singleton promise that ensures
// everything is cleaned up before proceeding.
toolbox.destroy().then(() => {
toolbox = iframe = target = null;
finish();
});
}
}

View file

@ -0,0 +1,105 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
const TEST_URL = "data:text/html,test for dynamically registering and unregistering tools";
var toolbox;
function test()
{
addTab(TEST_URL).then(tab => {
let target = TargetFactory.forTab(tab);
gDevTools.showToolbox(target).then(testRegister);
});
}
function testRegister(aToolbox)
{
toolbox = aToolbox;
gDevTools.once("tool-registered", toolRegistered);
gDevTools.registerTool({
id: "test-tool",
label: "Test Tool",
inMenu: true,
isTargetSupported: () => true,
build: function () {},
key: "t"
});
}
function toolRegistered(event, toolId)
{
is(toolId, "test-tool", "tool-registered event handler sent tool id");
ok(gDevTools.getToolDefinitionMap().has(toolId), "tool added to map");
// test that it appeared in the UI
let doc = toolbox.doc;
let tab = doc.getElementById("toolbox-tab-" + toolId);
ok(tab, "new tool's tab exists in toolbox UI");
let panel = doc.getElementById("toolbox-panel-" + toolId);
ok(panel, "new tool's panel exists in toolbox UI");
for (let win of getAllBrowserWindows()) {
let key = win.document.getElementById("key_" + toolId);
ok(key, "key for new tool added to every browser window");
let menuitem = win.document.getElementById("menuitem_" + toolId);
ok(menuitem, "menu item of new tool added to every browser window");
}
// then unregister it
testUnregister();
}
function getAllBrowserWindows() {
let wins = [];
let enumerator = Services.wm.getEnumerator("navigator:browser");
while (enumerator.hasMoreElements()) {
wins.push(enumerator.getNext());
}
return wins;
}
function testUnregister()
{
gDevTools.once("tool-unregistered", toolUnregistered);
gDevTools.unregisterTool("test-tool");
}
function toolUnregistered(event, toolDefinition)
{
let toolId = toolDefinition.id;
is(toolId, "test-tool", "tool-unregistered event handler sent tool id");
ok(!gDevTools.getToolDefinitionMap().has(toolId), "tool removed from map");
// test that it disappeared from the UI
let doc = toolbox.doc;
let tab = doc.getElementById("toolbox-tab-" + toolId);
ok(!tab, "tool's tab was removed from the toolbox UI");
let panel = doc.getElementById("toolbox-panel-" + toolId);
ok(!panel, "tool's panel was removed from toolbox UI");
for (let win of getAllBrowserWindows()) {
let key = win.document.getElementById("key_" + toolId);
ok(!key, "key removed from every browser window");
let menuitem = win.document.getElementById("menuitem_" + toolId);
ok(!menuitem, "menu item removed from every browser window");
}
cleanup();
}
function cleanup()
{
toolbox.destroy();
toolbox = null;
gBrowser.removeCurrentTab();
finish();
}

View file

@ -0,0 +1,36 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
// Tests that getPanelWhenReady returns the correct panel in promise
// resolutions regardless of whether it has opened first.
var toolbox = null;
const URL = "data:text/html;charset=utf8,test for getPanelWhenReady";
add_task(function* () {
let tab = yield addTab(URL);
let target = TargetFactory.forTab(tab);
toolbox = yield gDevTools.showToolbox(target);
let debuggerPanelPromise = toolbox.getPanelWhenReady("jsdebugger");
yield toolbox.selectTool("jsdebugger");
let debuggerPanel = yield debuggerPanelPromise;
is(debuggerPanel, toolbox.getPanel("jsdebugger"),
"The debugger panel from getPanelWhenReady before loading is the actual panel");
let debuggerPanel2 = yield toolbox.getPanelWhenReady("jsdebugger");
is(debuggerPanel2, toolbox.getPanel("jsdebugger"),
"The debugger panel from getPanelWhenReady after loading is the actual panel");
yield cleanup();
});
function* cleanup() {
yield toolbox.destroy();
gBrowser.removeCurrentTab();
toolbox = null;
}

View file

@ -0,0 +1,81 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
var {Toolbox} = require("devtools/client/framework/toolbox");
var toolbox = null;
function test() {
const URL = "data:text/plain;charset=UTF-8,Nothing to see here, move along";
const TOOL_ID_1 = "jsdebugger";
const TOOL_ID_2 = "webconsole";
addTab(URL).then(() => {
let target = TargetFactory.forTab(gBrowser.selectedTab);
gDevTools.showToolbox(target, TOOL_ID_1, Toolbox.HostType.BOTTOM)
.then(aToolbox => {
toolbox = aToolbox;
// select tool 2
toolbox.selectTool(TOOL_ID_2)
// and highlight the first one
.then(highlightTab.bind(null, TOOL_ID_1))
// to see if it has the proper class.
.then(checkHighlighted.bind(null, TOOL_ID_1))
// Now switch back to first tool
.then(() => toolbox.selectTool(TOOL_ID_1))
// to check again. But there is no easy way to test if
// it is showing orange or not.
.then(checkNoHighlightWhenSelected.bind(null, TOOL_ID_1))
// Switch to tool 2 again
.then(() => toolbox.selectTool(TOOL_ID_2))
// and check again.
.then(checkHighlighted.bind(null, TOOL_ID_1))
// Now unhighlight the tool
.then(unhighlightTab.bind(null, TOOL_ID_1))
// to see the classes gone.
.then(checkNoHighlight.bind(null, TOOL_ID_1))
// Now close the toolbox and exit.
.then(() => executeSoon(() => {
toolbox.destroy()
.then(() => {
toolbox = null;
gBrowser.removeCurrentTab();
finish();
});
}));
});
});
}
function highlightTab(toolId) {
info("Highlighting tool " + toolId + "'s tab.");
toolbox.highlightTool(toolId);
}
function unhighlightTab(toolId) {
info("Unhighlighting tool " + toolId + "'s tab.");
toolbox.unhighlightTool(toolId);
}
function checkHighlighted(toolId) {
let tab = toolbox.doc.getElementById("toolbox-tab-" + toolId);
ok(tab.hasAttribute("highlighted"), "The highlighted attribute is present");
ok(!tab.hasAttribute("selected") || tab.getAttribute("selected") != "true",
"The tab is not selected");
}
function checkNoHighlightWhenSelected(toolId) {
let tab = toolbox.doc.getElementById("toolbox-tab-" + toolId);
ok(tab.hasAttribute("highlighted"), "The highlighted attribute is present");
ok(tab.hasAttribute("selected") && tab.getAttribute("selected") == "true",
"and the tab is selected, so the orange glow will not be present.");
}
function checkNoHighlight(toolId) {
let tab = toolbox.doc.getElementById("toolbox-tab-" + toolId);
ok(!tab.hasAttribute("highlighted"),
"The highlighted attribute is not present");
}

View file

@ -0,0 +1,139 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
var {Toolbox} = require("devtools/client/framework/toolbox");
var {SIDE, BOTTOM, WINDOW} = Toolbox.HostType;
var toolbox, target;
const URL = "data:text/html;charset=utf8,test for opening toolbox in different hosts";
add_task(function* runTest() {
info("Create a test tab and open the toolbox");
let tab = yield addTab(URL);
target = TargetFactory.forTab(tab);
toolbox = yield gDevTools.showToolbox(target, "webconsole");
yield testBottomHost();
yield testSidebarHost();
yield testWindowHost();
yield testToolSelect();
yield testDestroy();
yield testRememberHost();
yield testPreviousHost();
yield toolbox.destroy();
toolbox = target = null;
gBrowser.removeCurrentTab();
});
function* testBottomHost() {
checkHostType(toolbox, BOTTOM);
// test UI presence
let nbox = gBrowser.getNotificationBox();
let iframe = document.getAnonymousElementByAttribute(nbox, "class", "devtools-toolbox-bottom-iframe");
ok(iframe, "toolbox bottom iframe exists");
checkToolboxLoaded(iframe);
}
function* testSidebarHost() {
yield toolbox.switchHost(SIDE);
checkHostType(toolbox, SIDE);
// test UI presence
let nbox = gBrowser.getNotificationBox();
let bottom = document.getAnonymousElementByAttribute(nbox, "class", "devtools-toolbox-bottom-iframe");
ok(!bottom, "toolbox bottom iframe doesn't exist");
let iframe = document.getAnonymousElementByAttribute(nbox, "class", "devtools-toolbox-side-iframe");
ok(iframe, "toolbox side iframe exists");
checkToolboxLoaded(iframe);
}
function* testWindowHost() {
yield toolbox.switchHost(WINDOW);
checkHostType(toolbox, WINDOW);
let nbox = gBrowser.getNotificationBox();
let sidebar = document.getAnonymousElementByAttribute(nbox, "class", "devtools-toolbox-side-iframe");
ok(!sidebar, "toolbox sidebar iframe doesn't exist");
let win = Services.wm.getMostRecentWindow("devtools:toolbox");
ok(win, "toolbox separate window exists");
let iframe = win.document.getElementById("toolbox-iframe");
checkToolboxLoaded(iframe);
}
function* testToolSelect() {
// make sure we can load a tool after switching hosts
yield toolbox.selectTool("inspector");
}
function* testDestroy() {
yield toolbox.destroy();
target = TargetFactory.forTab(gBrowser.selectedTab);
toolbox = yield gDevTools.showToolbox(target);
}
function* testRememberHost() {
// last host was the window - make sure it's the same when re-opening
is(toolbox.hostType, WINDOW, "host remembered");
let win = Services.wm.getMostRecentWindow("devtools:toolbox");
ok(win, "toolbox separate window exists");
}
function* testPreviousHost() {
// last host was the window - make sure it's the same when re-opening
is(toolbox.hostType, WINDOW, "host remembered");
info("Switching to side");
yield toolbox.switchHost(SIDE);
checkHostType(toolbox, SIDE, WINDOW);
info("Switching to bottom");
yield toolbox.switchHost(BOTTOM);
checkHostType(toolbox, BOTTOM, SIDE);
info("Switching from bottom to side");
yield toolbox.switchToPreviousHost();
checkHostType(toolbox, SIDE, BOTTOM);
info("Switching from side to bottom");
yield toolbox.switchToPreviousHost();
checkHostType(toolbox, BOTTOM, SIDE);
info("Switching to window");
yield toolbox.switchHost(WINDOW);
checkHostType(toolbox, WINDOW, BOTTOM);
info("Switching from window to bottom");
yield toolbox.switchToPreviousHost();
checkHostType(toolbox, BOTTOM, WINDOW);
info("Forcing the previous host to match the current (bottom)");
Services.prefs.setCharPref("devtools.toolbox.previousHost", BOTTOM);
info("Switching from bottom to side (since previous=current=bottom");
yield toolbox.switchToPreviousHost();
checkHostType(toolbox, SIDE, BOTTOM);
info("Forcing the previous host to match the current (side)");
Services.prefs.setCharPref("devtools.toolbox.previousHost", SIDE);
info("Switching from side to bottom (since previous=current=side");
yield toolbox.switchToPreviousHost();
checkHostType(toolbox, BOTTOM, SIDE);
}
function checkToolboxLoaded(iframe) {
let tabs = iframe.contentDocument.getElementById("toolbox-tabs");
ok(tabs, "toolbox UI has been loaded into iframe");
}

View file

@ -0,0 +1,69 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
// Tests that getPanelWhenReady returns the correct panel in promise
// resolutions regardless of whether it has opened first.
const URL = "data:text/html;charset=utf8,test for host sizes";
var {Toolbox} = require("devtools/client/framework/toolbox");
add_task(function* () {
// Set size prefs to make the hosts way too big, so that the size has
// to be clamped to fit into the browser window.
Services.prefs.setIntPref("devtools.toolbox.footer.height", 10000);
Services.prefs.setIntPref("devtools.toolbox.sidebar.width", 10000);
let tab = yield addTab(URL);
let nbox = gBrowser.getNotificationBox();
let {clientHeight: nboxHeight, clientWidth: nboxWidth} = nbox;
let toolbox = yield gDevTools.showToolbox(TargetFactory.forTab(tab));
is(nbox.clientHeight, nboxHeight, "Opening the toolbox hasn't changed the height of the nbox");
is(nbox.clientWidth, nboxWidth, "Opening the toolbox hasn't changed the width of the nbox");
let iframe = document.getAnonymousElementByAttribute(nbox, "class", "devtools-toolbox-bottom-iframe");
is(iframe.clientHeight, nboxHeight - 25, "The iframe fits within the available space");
yield toolbox.switchHost(Toolbox.HostType.SIDE);
iframe = document.getAnonymousElementByAttribute(nbox, "class", "devtools-toolbox-side-iframe");
iframe.style.minWidth = "1px"; // Disable the min width set in css
is(iframe.clientWidth, nboxWidth - 25, "The iframe fits within the available space");
yield cleanup(toolbox);
});
add_task(function* () {
// Set size prefs to something reasonable, so we can check to make sure
// they are being set properly.
Services.prefs.setIntPref("devtools.toolbox.footer.height", 100);
Services.prefs.setIntPref("devtools.toolbox.sidebar.width", 100);
let tab = yield addTab(URL);
let nbox = gBrowser.getNotificationBox();
let {clientHeight: nboxHeight, clientWidth: nboxWidth} = nbox;
let toolbox = yield gDevTools.showToolbox(TargetFactory.forTab(tab));
is(nbox.clientHeight, nboxHeight, "Opening the toolbox hasn't changed the height of the nbox");
is(nbox.clientWidth, nboxWidth, "Opening the toolbox hasn't changed the width of the nbox");
let iframe = document.getAnonymousElementByAttribute(nbox, "class", "devtools-toolbox-bottom-iframe");
is(iframe.clientHeight, 100, "The iframe is resized properly");
yield toolbox.switchHost(Toolbox.HostType.SIDE);
iframe = document.getAnonymousElementByAttribute(nbox, "class", "devtools-toolbox-side-iframe");
iframe.style.minWidth = "1px"; // Disable the min width set in css
is(iframe.clientWidth, 100, "The iframe is resized properly");
yield cleanup(toolbox);
});
function* cleanup(toolbox) {
Services.prefs.clearUserPref("devtools.toolbox.host");
Services.prefs.clearUserPref("devtools.toolbox.footer.height");
Services.prefs.clearUserPref("devtools.toolbox.sidebar.width");
yield toolbox.destroy();
gBrowser.removeCurrentTab();
}

View file

@ -0,0 +1,50 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const {Toolbox} = require("devtools/client/framework/toolbox");
const {SIDE, BOTTOM, WINDOW} = Toolbox.HostType;
const URL = "data:text/html;charset=utf8,browser_toolbox_hosts_telemetry.js";
function getHostHistogram() {
return Services.telemetry.getHistogramById("DEVTOOLS_TOOLBOX_HOST");
}
add_task(function* () {
// Reset it to make counting easier
getHostHistogram().clear();
info("Create a test tab and open the toolbox");
let tab = yield addTab(URL);
let target = TargetFactory.forTab(tab);
let toolbox = yield gDevTools.showToolbox(target, "webconsole");
yield changeToolboxHost(toolbox);
yield checkResults();
yield toolbox.destroy();
toolbox = target = null;
gBrowser.removeCurrentTab();
// Cleanup
getHostHistogram().clear();
});
function* changeToolboxHost(toolbox) {
info("Switch toolbox host");
yield toolbox.switchHost(SIDE);
yield toolbox.switchHost(WINDOW);
yield toolbox.switchHost(BOTTOM);
yield toolbox.switchHost(SIDE);
yield toolbox.switchHost(WINDOW);
yield toolbox.switchHost(BOTTOM);
}
function checkResults() {
let counts = getHostHistogram().snapshot().counts;
is(counts[0], 3, "Toolbox HostType bottom has 3 successful entries");
is(counts[1], 2, "Toolbox HostType side has 2 successful entries");
is(counts[2], 2, "Toolbox HostType window has 2 successful entries");
}

View file

@ -0,0 +1,81 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Tests keyboard navigation of devtools tabbar.
const TEST_URL =
"data:text/html;charset=utf8,test page for toolbar keyboard navigation";
function containsFocus(aDoc, aElm) {
let elm = aDoc.activeElement;
while (elm) {
if (elm === aElm) { return true; }
elm = elm.parentNode;
}
return false;
}
add_task(function* () {
info("Create a test tab and open the toolbox");
let toolbox = yield openNewTabAndToolbox(TEST_URL, "webconsole");
let doc = toolbox.doc;
let toolbar = doc.querySelector(".devtools-tabbar");
let toolbarControls = [...toolbar.querySelectorAll(
".devtools-tab, button")].filter(elm =>
!elm.hidden && doc.defaultView.getComputedStyle(elm).getPropertyValue(
"display") !== "none");
// Put the keyboard focus onto the first toolbar control.
toolbarControls[0].focus();
ok(containsFocus(doc, toolbar), "Focus is within the toolbar");
// Move the focus away from toolbar to a next focusable element.
EventUtils.synthesizeKey("VK_TAB", {});
ok(!containsFocus(doc, toolbar), "Focus is outside of the toolbar");
// Move the focus back to the toolbar.
EventUtils.synthesizeKey("VK_TAB", { shiftKey: true });
ok(containsFocus(doc, toolbar), "Focus is within the toolbar again");
// Move through the toolbar forward using the right arrow key.
for (let i = 0; i < toolbarControls.length; ++i) {
is(doc.activeElement.id, toolbarControls[i].id, "New control is focused");
if (i < toolbarControls.length - 1) {
EventUtils.synthesizeKey("VK_RIGHT", {});
}
}
// Move the focus away from toolbar to a next focusable element.
EventUtils.synthesizeKey("VK_TAB", {});
ok(!containsFocus(doc, toolbar), "Focus is outside of the toolbar");
// Move the focus back to the toolbar.
EventUtils.synthesizeKey("VK_TAB", { shiftKey: true });
ok(containsFocus(doc, toolbar), "Focus is within the toolbar again");
// Move through the toolbar backward using the left arrow key.
for (let i = toolbarControls.length - 1; i >= 0; --i) {
is(doc.activeElement.id, toolbarControls[i].id, "New control is focused");
if (i > 0) { EventUtils.synthesizeKey("VK_LEFT", {}); }
}
// Move focus to the 3rd (non-first) toolbar control.
let expectedFocusedControl = toolbarControls[2];
EventUtils.synthesizeKey("VK_RIGHT", {});
EventUtils.synthesizeKey("VK_RIGHT", {});
is(doc.activeElement.id, expectedFocusedControl.id, "New control is focused");
// Move the focus away from toolbar to a next focusable element.
EventUtils.synthesizeKey("VK_TAB", {});
ok(!containsFocus(doc, toolbar), "Focus is outside of the toolbar");
// Move the focus back to the toolbar, ensure we land on the last active
// descendant control.
EventUtils.synthesizeKey("VK_TAB", { shiftKey: true });
is(doc.activeElement.id, expectedFocusedControl.id, "New control is focused");
});

View file

@ -0,0 +1,106 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that when the toolbox is displayed in a bottom host, that host can be
// minimized to just the tabbar height, and maximized again.
// Also test that while minimized, switching to a tool, clicking on the
// settings, or clicking on the selected tool's tab maximizes the toolbox again.
// Finally test that the minimize button doesn't exist in other host types.
const URL = "data:text/html;charset=utf8,test page";
const {Toolbox} = require("devtools/client/framework/toolbox");
add_task(function* () {
info("Create a test tab and open the toolbox");
let tab = yield addTab(URL);
let target = TargetFactory.forTab(tab);
let toolbox = yield gDevTools.showToolbox(target, "webconsole");
let button = toolbox.doc.querySelector("#toolbox-dock-bottom-minimize");
ok(button, "The minimize button exists in the default bottom host");
info("Try to minimize the toolbox");
yield minimize(toolbox);
ok(parseInt(toolbox._host.frame.style.marginBottom, 10) < 0,
"The toolbox host has been hidden away with a negative-margin");
info("Try to maximize again the toolbox");
yield maximize(toolbox);
ok(parseInt(toolbox._host.frame.style.marginBottom, 10) == 0,
"The toolbox host is shown again");
info("Try to minimize again using the keyboard shortcut");
yield minimizeWithShortcut(toolbox);
ok(parseInt(toolbox._host.frame.style.marginBottom, 10) < 0,
"The toolbox host has been hidden away with a negative-margin");
info("Try to maximize again using the keyboard shortcut");
yield maximizeWithShortcut(toolbox);
ok(parseInt(toolbox._host.frame.style.marginBottom, 10) == 0,
"The toolbox host is shown again");
info("Minimize again and switch to another tool");
yield minimize(toolbox);
let onMaximized = toolbox._host.once("maximized");
yield toolbox.selectTool("inspector");
yield onMaximized;
info("Minimize again and click on the tab of the current tool");
yield minimize(toolbox);
onMaximized = toolbox._host.once("maximized");
let tabButton = toolbox.doc.querySelector("#toolbox-tab-inspector");
EventUtils.synthesizeMouseAtCenter(tabButton, {}, toolbox.win);
yield onMaximized;
info("Minimize again and click on the settings tab");
yield minimize(toolbox);
onMaximized = toolbox._host.once("maximized");
let settingsButton = toolbox.doc.querySelector("#toolbox-tab-options");
EventUtils.synthesizeMouseAtCenter(settingsButton, {}, toolbox.win);
yield onMaximized;
info("Switch to a different host");
yield toolbox.switchHost(Toolbox.HostType.SIDE);
button = toolbox.doc.querySelector("#toolbox-dock-bottom-minimize");
ok(!button, "The minimize button doesn't exist in the side host");
Services.prefs.clearUserPref("devtools.toolbox.host");
yield toolbox.destroy();
gBrowser.removeCurrentTab();
});
function* minimize(toolbox) {
let button = toolbox.doc.querySelector("#toolbox-dock-bottom-minimize");
let onMinimized = toolbox._host.once("minimized");
EventUtils.synthesizeMouseAtCenter(button, {}, toolbox.win);
yield onMinimized;
}
function* minimizeWithShortcut(toolbox) {
let key = toolbox.doc.getElementById("toolbox-minimize-key")
.getAttribute("key");
let onMinimized = toolbox._host.once("minimized");
EventUtils.synthesizeKey(key, {accelKey: true, shiftKey: true},
toolbox.win);
yield onMinimized;
}
function* maximize(toolbox) {
let button = toolbox.doc.querySelector("#toolbox-dock-bottom-minimize");
let onMaximized = toolbox._host.once("maximized");
EventUtils.synthesizeMouseAtCenter(button, {}, toolbox.win);
yield onMaximized;
}
function* maximizeWithShortcut(toolbox) {
let key = toolbox.doc.getElementById("toolbox-minimize-key")
.getAttribute("key");
let onMaximized = toolbox._host.once("maximized");
EventUtils.synthesizeKey(key, {accelKey: true, shiftKey: true},
toolbox.win);
yield onMaximized;
}

View file

@ -0,0 +1,297 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* import-globals-from shared-head.js */
"use strict";
// Tests that changing preferences in the options panel updates the prefs
// and toggles appropriate things in the toolbox.
var doc = null, toolbox = null, panelWin = null, modifiedPrefs = [];
const {LocalizationHelper} = require("devtools/shared/l10n");
const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
add_task(function* () {
const URL = "data:text/html;charset=utf8,test for dynamically registering " +
"and unregistering tools";
registerNewTool();
let tab = yield addTab(URL);
let target = TargetFactory.forTab(tab);
toolbox = yield gDevTools.showToolbox(target);
doc = toolbox.doc;
yield testSelectTool();
yield testOptionsShortcut();
yield testOptions();
yield testToggleTools();
yield cleanup();
});
function registerNewTool() {
let toolDefinition = {
id: "test-tool",
isTargetSupported: () => true,
visibilityswitch: "devtools.test-tool.enabled",
url: "about:blank",
label: "someLabel"
};
ok(gDevTools, "gDevTools exists");
ok(!gDevTools.getToolDefinitionMap().has("test-tool"),
"The tool is not registered");
gDevTools.registerTool(toolDefinition);
ok(gDevTools.getToolDefinitionMap().has("test-tool"),
"The tool is registered");
}
function* testSelectTool() {
info("Checking to make sure that the options panel can be selected.");
let onceSelected = toolbox.once("options-selected");
toolbox.selectTool("options");
yield onceSelected;
ok(true, "Toolbox selected via selectTool method");
}
function* testOptionsShortcut() {
info("Selecting another tool, then reselecting options panel with keyboard.");
yield toolbox.selectTool("webconsole");
is(toolbox.currentToolId, "webconsole", "webconsole is selected");
synthesizeKeyShortcut(L10N.getStr("toolbox.options.key"));
is(toolbox.currentToolId, "options", "Toolbox selected via shortcut key (1)");
synthesizeKeyShortcut(L10N.getStr("toolbox.options.key"));
is(toolbox.currentToolId, "webconsole", "webconsole is selected (1)");
yield toolbox.selectTool("webconsole");
is(toolbox.currentToolId, "webconsole", "webconsole is selected");
synthesizeKeyShortcut(L10N.getStr("toolbox.help.key"));
is(toolbox.currentToolId, "options", "Toolbox selected via shortcut key (2)");
synthesizeKeyShortcut(L10N.getStr("toolbox.options.key"));
is(toolbox.currentToolId, "webconsole", "webconsole is reselected (2)");
synthesizeKeyShortcut(L10N.getStr("toolbox.help.key"));
is(toolbox.currentToolId, "options", "Toolbox selected via shortcut key (2)");
}
function* testOptions() {
let tool = toolbox.getPanel("options");
panelWin = tool.panelWin;
let prefNodes = tool.panelDoc.querySelectorAll(
"input[type=checkbox][data-pref]");
// Store modified pref names so that they can be cleared on error.
for (let node of tool.panelDoc.querySelectorAll("[data-pref]")) {
let pref = node.getAttribute("data-pref");
modifiedPrefs.push(pref);
}
for (let node of prefNodes) {
let prefValue = GetPref(node.getAttribute("data-pref"));
// Test clicking the checkbox for each options pref
yield testMouseClick(node, prefValue);
// Do again with opposite values to reset prefs
yield testMouseClick(node, !prefValue);
}
let prefSelects = tool.panelDoc.querySelectorAll("select[data-pref]");
for (let node of prefSelects) {
yield testSelect(node);
}
}
function* testSelect(select) {
let pref = select.getAttribute("data-pref");
let options = Array.from(select.options);
info("Checking select for: " + pref);
is(select.options[select.selectedIndex].value, GetPref(pref),
"select starts out selected");
for (let option of options) {
if (options.indexOf(option) === select.selectedIndex) {
continue;
}
let deferred = defer();
gDevTools.once("pref-changed", (event, data) => {
if (data.pref == pref) {
ok(true, "Correct pref was changed");
is(GetPref(pref), option.value, "Preference been switched for " + pref);
} else {
ok(false, "Pref " + pref + " was not changed correctly");
}
deferred.resolve();
});
select.selectedIndex = options.indexOf(option);
let changeEvent = new Event("change");
select.dispatchEvent(changeEvent);
yield deferred.promise;
}
}
function* testMouseClick(node, prefValue) {
let deferred = defer();
let pref = node.getAttribute("data-pref");
gDevTools.once("pref-changed", (event, data) => {
if (data.pref == pref) {
ok(true, "Correct pref was changed");
is(data.oldValue, prefValue, "Previous value is correct for " + pref);
is(data.newValue, !prefValue, "New value is correct for " + pref);
} else {
ok(false, "Pref " + pref + " was not changed correctly");
}
deferred.resolve();
});
node.scrollIntoView();
// We use executeSoon here to ensure that the element is in view and
// clickable.
executeSoon(function () {
info("Click event synthesized for pref " + pref);
EventUtils.synthesizeMouseAtCenter(node, {}, panelWin);
});
yield deferred.promise;
}
function* testToggleTools() {
let toolNodes = panelWin.document.querySelectorAll(
"#default-tools-box input[type=checkbox]:not([data-unsupported])," +
"#additional-tools-box input[type=checkbox]:not([data-unsupported])");
let enabledTools = [...toolNodes].filter(node => node.checked);
let toggleableTools = gDevTools.getDefaultTools().filter(tool => {
return tool.visibilityswitch;
}).concat(gDevTools.getAdditionalTools());
for (let node of toolNodes) {
let id = node.getAttribute("id");
ok(toggleableTools.some(tool => tool.id === id),
"There should be a toggle checkbox for: " + id);
}
// Store modified pref names so that they can be cleared on error.
for (let tool of toggleableTools) {
let pref = tool.visibilityswitch;
modifiedPrefs.push(pref);
}
// Toggle each tool
for (let node of toolNodes) {
yield toggleTool(node);
}
// Toggle again to reset tool enablement state
for (let node of toolNodes) {
yield toggleTool(node);
}
// Test that a tool can still be added when no tabs are present:
// Disable all tools
for (let node of enabledTools) {
yield toggleTool(node);
}
// Re-enable the tools which are enabled by default
for (let node of enabledTools) {
yield toggleTool(node);
}
// Toggle first, middle, and last tools to ensure that toolbox tabs are
// inserted in order
let firstTool = toolNodes[0];
let middleTool = toolNodes[(toolNodes.length / 2) | 0];
let lastTool = toolNodes[toolNodes.length - 1];
yield toggleTool(firstTool);
yield toggleTool(firstTool);
yield toggleTool(middleTool);
yield toggleTool(middleTool);
yield toggleTool(lastTool);
yield toggleTool(lastTool);
}
function* toggleTool(node) {
let deferred = defer();
let toolId = node.getAttribute("id");
if (node.checked) {
gDevTools.once("tool-unregistered",
checkUnregistered.bind(null, toolId, deferred));
} else {
gDevTools.once("tool-registered",
checkRegistered.bind(null, toolId, deferred));
}
node.scrollIntoView();
EventUtils.synthesizeMouseAtCenter(node, {}, panelWin);
yield deferred.promise;
}
function checkUnregistered(toolId, deferred, event, data) {
if (data.id == toolId) {
ok(true, "Correct tool removed");
// checking tab on the toolbox
ok(!doc.getElementById("toolbox-tab-" + toolId),
"Tab removed for " + toolId);
} else {
ok(false, "Something went wrong, " + toolId + " was not unregistered");
}
deferred.resolve();
}
function checkRegistered(toolId, deferred, event, data) {
if (data == toolId) {
ok(true, "Correct tool added back");
// checking tab on the toolbox
let radio = doc.getElementById("toolbox-tab-" + toolId);
ok(radio, "Tab added back for " + toolId);
if (radio.previousSibling) {
ok(+radio.getAttribute("ordinal") >=
+radio.previousSibling.getAttribute("ordinal"),
"Inserted tab's ordinal is greater than equal to its previous tab." +
"Expected " + radio.getAttribute("ordinal") + " >= " +
radio.previousSibling.getAttribute("ordinal"));
}
if (radio.nextSibling) {
ok(+radio.getAttribute("ordinal") <
+radio.nextSibling.getAttribute("ordinal"),
"Inserted tab's ordinal is less than its next tab. Expected " +
radio.getAttribute("ordinal") + " < " +
radio.nextSibling.getAttribute("ordinal"));
}
} else {
ok(false, "Something went wrong, " + toolId + " was not registered");
}
deferred.resolve();
}
function GetPref(name) {
let type = Services.prefs.getPrefType(name);
switch (type) {
case Services.prefs.PREF_STRING:
return Services.prefs.getCharPref(name);
case Services.prefs.PREF_INT:
return Services.prefs.getIntPref(name);
case Services.prefs.PREF_BOOL:
return Services.prefs.getBoolPref(name);
default:
throw new Error("Unknown type");
}
}
function* cleanup() {
gDevTools.unregisterTool("test-tool");
yield toolbox.destroy();
gBrowser.removeCurrentTab();
for (let pref of modifiedPrefs) {
Services.prefs.clearUserPref(pref);
}
toolbox = doc = panelWin = modifiedPrefs = null;
}

View file

@ -0,0 +1,163 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* import-globals-from shared-head.js */
"use strict";
const TEST_URL = "data:text/html;charset=utf8,test for dynamically " +
"registering and unregistering tools";
var doc = null, toolbox = null, panelWin = null, modifiedPrefs = [];
function test() {
addTab(TEST_URL).then(tab => {
let target = TargetFactory.forTab(tab);
gDevTools.showToolbox(target)
.then(testSelectTool)
.then(testToggleToolboxButtons)
.then(testPrefsAreRespectedWhenReopeningToolbox)
.then(cleanup, errorHandler);
});
}
function testPrefsAreRespectedWhenReopeningToolbox() {
let deferred = defer();
let target = TargetFactory.forTab(gBrowser.selectedTab);
info("Closing toolbox to test after reopening");
gDevTools.closeToolbox(target).then(() => {
let tabTarget = TargetFactory.forTab(gBrowser.selectedTab);
gDevTools.showToolbox(tabTarget)
.then(testSelectTool)
.then(() => {
info("Toolbox has been reopened. Checking UI state.");
testPreferenceAndUIStateIsConsistent();
deferred.resolve();
});
});
return deferred.promise;
}
function testSelectTool(devtoolsToolbox) {
let deferred = defer();
info("Selecting the options panel");
toolbox = devtoolsToolbox;
doc = toolbox.doc;
toolbox.once("options-selected", (event, tool) => {
ok(true, "Options panel selected via selectTool method");
panelWin = tool.panelWin;
deferred.resolve();
});
toolbox.selectTool("options");
return deferred.promise;
}
function testPreferenceAndUIStateIsConsistent() {
let checkNodes = [...panelWin.document.querySelectorAll(
"#enabled-toolbox-buttons-box input[type=checkbox]")];
let toolboxButtonNodes = [...doc.querySelectorAll(".command-button")];
toolboxButtonNodes.push(doc.getElementById("command-button-frames"));
let toggleableTools = toolbox.toolboxButtons;
// The noautohide button is only displayed in the browser toolbox
toggleableTools = toggleableTools.filter(
tool => tool.id != "command-button-noautohide");
for (let tool of toggleableTools) {
let isVisible = getBoolPref(tool.visibilityswitch);
let button = toolboxButtonNodes.filter(
toolboxButton => toolboxButton.id === tool.id)[0];
is(!button.hasAttribute("hidden"), isVisible,
"Button visibility matches pref for " + tool.id);
let check = checkNodes.filter(node => node.id === tool.id)[0];
is(check.checked, isVisible,
"Checkbox should be selected based on current pref for " + tool.id);
}
}
function testToggleToolboxButtons() {
let checkNodes = [...panelWin.document.querySelectorAll(
"#enabled-toolbox-buttons-box input[type=checkbox]")];
let toolboxButtonNodes = [...doc.querySelectorAll(".command-button")];
let toggleableTools = toolbox.toolboxButtons;
// The noautohide button is only displayed in the browser toolbox, and the element
// picker button is not toggleable.
toggleableTools = toggleableTools.filter(
tool => tool.id != "command-button-noautohide" && tool.id != "command-button-pick");
toolboxButtonNodes = toolboxButtonNodes.filter(
btn => btn.id != "command-button-noautohide" && btn.id != "command-button-pick");
is(checkNodes.length, toggleableTools.length,
"All of the buttons are toggleable.");
is(checkNodes.length, toolboxButtonNodes.length,
"All of the DOM buttons are toggleable.");
for (let tool of toggleableTools) {
let id = tool.id;
let matchedCheckboxes = checkNodes.filter(node => node.id === id);
let matchedButtons = toolboxButtonNodes.filter(button => button.id === id);
is(matchedCheckboxes.length, 1,
"There should be a single toggle checkbox for: " + id);
is(matchedButtons.length, 1,
"There should be a DOM button for: " + id);
is(matchedButtons[0], tool.button,
"DOM buttons should match for: " + id);
is(matchedCheckboxes[0].nextSibling.textContent, tool.label,
"The label for checkbox matches the tool definition.");
is(matchedButtons[0].getAttribute("title"), tool.label,
"The tooltip for button matches the tool definition.");
}
// Store modified pref names so that they can be cleared on error.
for (let tool of toggleableTools) {
let pref = tool.visibilityswitch;
modifiedPrefs.push(pref);
}
// Try checking each checkbox, making sure that it changes the preference
for (let node of checkNodes) {
let tool = toggleableTools.filter(
toggleableTool => toggleableTool.id === node.id)[0];
let isVisible = getBoolPref(tool.visibilityswitch);
testPreferenceAndUIStateIsConsistent();
node.click();
testPreferenceAndUIStateIsConsistent();
let isVisibleAfterClick = getBoolPref(tool.visibilityswitch);
is(isVisible, !isVisibleAfterClick,
"Clicking on the node should have toggled visibility preference for " +
tool.visibilityswitch);
}
return promise.resolve();
}
function getBoolPref(key) {
return Services.prefs.getBoolPref(key);
}
function cleanup() {
toolbox.destroy().then(function () {
gBrowser.removeCurrentTab();
for (let pref of modifiedPrefs) {
Services.prefs.clearUserPref(pref);
}
toolbox = doc = panelWin = modifiedPrefs = null;
finish();
});
}
function errorHandler(error) {
ok(false, "Unexpected error: " + error);
cleanup();
}

View file

@ -0,0 +1,34 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
requestLongerTimeout(2);
// Tests that disabling the cache for a tab works as it should when toolboxes
// are not toggled.
loadHelperScript("helper_disable_cache.js");
add_task(function* () {
// Ensure that the setting is cleared after the test.
registerCleanupFunction(() => {
info("Resetting devtools.cache.disabled to false.");
Services.prefs.setBoolPref("devtools.cache.disabled", false);
});
// Initialise tabs: 1 and 2 with a toolbox, 3 and 4 without.
for (let tab of tabs) {
yield initTab(tab, tab.startToolbox);
}
// Ensure cache is enabled for all tabs.
yield checkCacheStateForAllTabs([true, true, true, true]);
// Check the checkbox in tab 0 and ensure cache is disabled for tabs 0 and 1.
yield setDisableCacheCheckboxChecked(tabs[0], true);
yield checkCacheStateForAllTabs([false, false, true, true]);
yield finishUp();
});

View file

@ -0,0 +1,47 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
requestLongerTimeout(2);
// Tests that disabling the cache for a tab works as it should when toolboxes
// are toggled.
loadHelperScript("helper_disable_cache.js");
add_task(function* () {
// Ensure that the setting is cleared after the test.
registerCleanupFunction(() => {
info("Resetting devtools.cache.disabled to false.");
Services.prefs.setBoolPref("devtools.cache.disabled", false);
});
// Initialise tabs: 1 and 2 with a toolbox, 3 and 4 without.
for (let tab of tabs) {
yield initTab(tab, tab.startToolbox);
}
// Disable cache in tab 0
yield setDisableCacheCheckboxChecked(tabs[0], true);
// Open toolbox in tab 2 and ensure the cache is then disabled.
tabs[2].toolbox = yield gDevTools.showToolbox(tabs[2].target, "options");
yield checkCacheEnabled(tabs[2], false);
// Close toolbox in tab 2 and ensure the cache is enabled again
yield tabs[2].toolbox.destroy();
tabs[2].target = TargetFactory.forTab(tabs[2].tab);
yield checkCacheEnabled(tabs[2], true);
// Open toolbox in tab 2 and ensure the cache is then disabled.
tabs[2].toolbox = yield gDevTools.showToolbox(tabs[2].target, "options");
yield checkCacheEnabled(tabs[2], false);
// Check the checkbox in tab 2 and ensure cache is enabled for all tabs.
yield setDisableCacheCheckboxChecked(tabs[2], false);
yield checkCacheStateForAllTabs([true, true, true, true]);
yield finishUp();
});

View file

@ -0,0 +1,28 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
function handleRequest(request, response) {
let Etag = '"4d881ab-b03-435f0a0f9ef00"';
let IfNoneMatch = request.hasHeader("If-None-Match")
? request.getHeader("If-None-Match")
: "";
let guid = 'xxxxxxxx-xxxx-xxxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
let r = Math.random() * 16 | 0;
let v = c === "x" ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
let page = "<!DOCTYPE html><html><body><h1>" + guid + "</h1></body></html>";
response.setHeader("Etag", Etag, false);
if (IfNoneMatch === Etag) {
response.setStatusLine(request.httpVersion, "304", "Not Modified");
} else {
response.setHeader("Content-Type", "text/html; charset=utf-8", false);
response.setHeader("Content-Length", page.length + "", false);
response.write(page);
}
}

View file

@ -0,0 +1,46 @@
<!DOCTYPE html>
<html>
<head>
<title>browser_toolbox_options_disablejs.html</title>
<meta charset="UTF-8">
<style>
div {
width: 260px;
height: 24px;
border: 1px solid #000;
margin-top: 10px;
}
iframe {
height: 90px;
border: 1px solid #000;
}
h1 {
font-size: 20px
}
</style>
<script type="application/javascript;version=1.8">
function log(msg) {
let output = document.getElementById("output");
output.innerHTML = msg;
}
</script>
</head>
<body>
<h1>Test in page</h1>
<input id="logJSEnabled"
type="button"
value="Log JS Enabled"
onclick="log('JavaScript Enabled')"/>
<input id="logJSDisabled"
type="button"
value="Log JS Disabled"
onclick="log('JavaScript Disabled')"/>
<br>
<div id="output">No output</div>
<h1>Test in iframe</h1>
<iframe src="browser_toolbox_options_disable_js_iframe.html"></iframe>
</body>
</html>

View file

@ -0,0 +1,119 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
// Tests that disabling JavaScript for a tab works as it should.
const TEST_URI = URL_ROOT + "browser_toolbox_options_disable_js.html";
function test() {
addTab(TEST_URI).then(tab => {
let target = TargetFactory.forTab(tab);
gDevTools.showToolbox(target).then(testSelectTool);
});
}
function testSelectTool(toolbox) {
toolbox.once("options-selected", () => testToggleJS(toolbox));
toolbox.selectTool("options");
}
let testToggleJS = Task.async(function* (toolbox) {
ok(true, "Toolbox selected via selectTool method");
yield testJSEnabled();
yield testJSEnabledIframe();
// Disable JS.
yield toggleJS(toolbox);
yield testJSDisabled();
yield testJSDisabledIframe();
// Re-enable JS.
yield toggleJS(toolbox);
yield testJSEnabled();
yield testJSEnabledIframe();
finishUp(toolbox);
});
function* testJSEnabled() {
info("Testing that JS is enabled");
// We use waitForTick here because switching docShell.allowJavascript to true
// takes a while to become live.
yield waitForTick();
yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function () {
let doc = content.document;
let output = doc.getElementById("output");
doc.querySelector("#logJSEnabled").click();
is(output.textContent, "JavaScript Enabled", 'Output is "JavaScript Enabled"');
});
}
function* testJSEnabledIframe() {
info("Testing that JS is enabled in the iframe");
yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function () {
let doc = content.document;
let iframe = doc.querySelector("iframe");
let iframeDoc = iframe.contentDocument;
let output = iframeDoc.getElementById("output");
iframeDoc.querySelector("#logJSEnabled").click();
is(output.textContent, "JavaScript Enabled",
'Output is "JavaScript Enabled" in iframe');
});
}
function* toggleJS(toolbox) {
let panel = toolbox.getCurrentPanel();
let cbx = panel.panelDoc.getElementById("devtools-disable-javascript");
if (cbx.checked) {
info("Clearing checkbox to re-enable JS");
} else {
info("Checking checkbox to disable JS");
}
let browserLoaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
cbx.click();
yield browserLoaded;
}
function* testJSDisabled() {
info("Testing that JS is disabled");
yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function () {
let doc = content.document;
let output = doc.getElementById("output");
doc.querySelector("#logJSDisabled").click();
ok(output.textContent !== "JavaScript Disabled",
'output is not "JavaScript Disabled"');
});
}
function* testJSDisabledIframe() {
info("Testing that JS is disabled in the iframe");
yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function () {
let doc = content.document;
let iframe = doc.querySelector("iframe");
let iframeDoc = iframe.contentDocument;
let output = iframeDoc.getElementById("output");
iframeDoc.querySelector("#logJSDisabled").click();
ok(output.textContent !== "JavaScript Disabled",
'output is not "JavaScript Disabled" in iframe');
});
}
function finishUp(toolbox) {
toolbox.destroy().then(function () {
gBrowser.removeCurrentTab();
finish();
});
}

View file

@ -0,0 +1,33 @@
<html>
<head>
<title>browser_toolbox_options_disablejs.html</title>
<meta charset="UTF-8">
<style>
div {
width: 260px;
height: 24px;
border: 1px solid #000;
margin-top: 10px;
}
</style>
<script type="application/javascript;version=1.8">
function log(msg) {
let output = document.getElementById("output");
output.innerHTML = msg;
}
</script>
</head>
<body>
<input id="logJSEnabled"
type="button"
value="Log JS Enabled"
onclick="log('JavaScript Enabled')"/>
<input id="logJSDisabled"
type="button"
value="Log JS Disabled"
onclick="log('JavaScript Disabled')"/>
<br>
<div id="output">No output</div>
</body>
</html>

View file

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<title>browser_toolbox_options_enable_serviceworkers_testing.html</title>
<meta charset="UTF-8">
</head>
<body>
<h1>SW-test</h1>
</body>
</html>

View file

@ -0,0 +1,126 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
// Test that enabling Service Workers testing option enables the
// mServiceWorkersTestingEnabled attribute added to nsPIDOMWindow.
const COMMON_FRAME_SCRIPT_URL =
"chrome://devtools/content/shared/frame-script-utils.js";
const ROOT_TEST_DIR =
getRootDirectory(gTestPath);
const FRAME_SCRIPT_URL =
ROOT_TEST_DIR +
"browser_toolbox_options_enable_serviceworkers_testing_frame_script.js";
const TEST_URI = URL_ROOT +
"browser_toolbox_options_enable_serviceworkers_testing.html";
const ELEMENT_ID = "devtools-enable-serviceWorkersTesting";
var toolbox;
function test() {
// Note: Pref dom.serviceWorkers.testing.enabled is false since we are testing
// the same capabilities are enabled with the devtool pref.
SpecialPowers.pushPrefEnv({"set": [
["dom.serviceWorkers.exemptFromPerDomainMax", true],
["dom.serviceWorkers.enabled", true],
["dom.serviceWorkers.testing.enabled", false]
]}, init);
}
function init() {
addTab(TEST_URI).then(tab => {
let target = TargetFactory.forTab(tab);
let linkedBrowser = tab.linkedBrowser;
linkedBrowser.messageManager.loadFrameScript(COMMON_FRAME_SCRIPT_URL, false);
linkedBrowser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false);
gDevTools.showToolbox(target).then(testSelectTool);
});
}
function testSelectTool(aToolbox) {
toolbox = aToolbox;
toolbox.once("options-selected", start);
toolbox.selectTool("options");
}
function register() {
return executeInContent("devtools:sw-test:register");
}
function unregister(swr) {
return executeInContent("devtools:sw-test:unregister");
}
function registerAndUnregisterInFrame() {
return executeInContent("devtools:sw-test:iframe:register-and-unregister");
}
function testRegisterFails(data) {
is(data.success, false, "Register should fail with security error");
return promise.resolve();
}
function toggleServiceWorkersTestingCheckbox() {
let panel = toolbox.getCurrentPanel();
let cbx = panel.panelDoc.getElementById(ELEMENT_ID);
cbx.scrollIntoView();
if (cbx.checked) {
info("Clearing checkbox to disable service workers testing");
} else {
info("Checking checkbox to enable service workers testing");
}
cbx.click();
return promise.resolve();
}
function reload() {
let deferred = defer();
gBrowser.selectedBrowser.addEventListener("load", function onLoad(evt) {
gBrowser.selectedBrowser.removeEventListener(evt.type, onLoad, true);
deferred.resolve();
}, true);
executeInContent("devtools:test:reload", {}, {}, false);
return deferred.promise;
}
function testRegisterSuccesses(data) {
is(data.success, true, "Register should success");
return promise.resolve();
}
function start() {
register()
.then(testRegisterFails)
.then(toggleServiceWorkersTestingCheckbox)
.then(reload)
.then(register)
.then(testRegisterSuccesses)
.then(unregister)
.then(registerAndUnregisterInFrame)
.then(testRegisterSuccesses)
// Workers should be turned back off when we closes the toolbox
.then(toolbox.destroy.bind(toolbox))
.then(reload)
.then(register)
.then(testRegisterFails)
.catch(function (e) {
ok(false, "Some test failed with error " + e);
}).then(finishUp);
}
function finishUp() {
gBrowser.removeCurrentTab();
toolbox = null;
finish();
}

View file

@ -0,0 +1,46 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
// A helper frame-script for devtools/client/framework service worker tests.
"use strict";
addMessageListener("devtools:sw-test:register", function (msg) {
content.navigator.serviceWorker.register("serviceworker.js")
.then(swr => {
sendAsyncMessage("devtools:sw-test:register", {success: true});
}, error => {
sendAsyncMessage("devtools:sw-test:register", {success: false});
});
});
addMessageListener("devtools:sw-test:unregister", function (msg) {
content.navigator.serviceWorker.getRegistration().then(swr => {
swr.unregister().then(result => {
sendAsyncMessage("devtools:sw-test:unregister",
{success: result ? true : false});
});
});
});
addMessageListener("devtools:sw-test:iframe:register-and-unregister", function (msg) {
var frame = content.document.createElement("iframe");
frame.addEventListener("load", function onLoad() {
frame.removeEventListener("load", onLoad);
frame.contentWindow.navigator.serviceWorker.register("serviceworker.js")
.then(swr => {
return swr.unregister();
}).then(_ => {
frame.remove();
sendAsyncMessage("devtools:sw-test:iframe:register-and-unregister",
{success: true});
}).catch(error => {
sendAsyncMessage("devtools:sw-test:iframe:register-and-unregister",
{success: false});
});
});
frame.src = "browser_toolbox_options_enabled_serviceworkers_testing.html";
content.document.body.appendChild(frame);
});

View file

@ -0,0 +1,81 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test toggling the toolbox quickly and see if there is any race breaking it.
const URL = "data:text/html;charset=utf-8,Toggling devtools quickly";
add_task(function* () {
// Make sure this test starts with the selectedTool pref cleared. Previous
// tests select various tools, and that sets this pref.
Services.prefs.clearUserPref("devtools.toolbox.selectedTool");
let tab = yield addTab(URL);
let created = 0, ready = 0, destroy = 0, destroyed = 0;
let onCreated = () => {
created++;
};
let onReady = () => {
ready++;
};
let onDestroy = () => {
destroy++;
};
let onDestroyed = () => {
destroyed++;
};
gDevTools.on("toolbox-created", onCreated);
gDevTools.on("toolbox-ready", onReady);
gDevTools.on("toolbox-destroy", onDestroy);
gDevTools.on("toolbox-destroyed", onDestroyed);
// The current implementation won't toggle the toolbox many times,
// instead it will ignore toggles that happens while the toolbox is still
// creating or still destroying.
// Toggle the toolbox at least 3 times.
info("Trying to toggle the toolbox 3 times");
while (created < 3) {
// Sent multiple event to try to race the code during toolbox creation and destruction
toggle();
toggle();
toggle();
// Release the event loop to let a chance to actually create or destroy the toolbox!
yield wait(50);
}
info("Toggled the toolbox 3 times");
// Now wait for the 3rd toolbox to be fully ready before closing it.
// We close the last toolbox manually, out of the first while() loop to
// avoid races and be sure we end up we no toolbox and waited for all the
// requests to be done.
while (ready != 3) {
yield wait(100);
}
toggle();
while (destroyed != 3) {
yield wait(100);
}
is(created, 3, "right number of created events");
is(ready, 3, "right number of ready events");
is(destroy, 3, "right number of destroy events");
is(destroyed, 3, "right number of destroyed events");
gDevTools.off("toolbox-created", onCreated);
gDevTools.off("toolbox-ready", onReady);
gDevTools.off("toolbox-destroy", onDestroy);
gDevTools.off("toolbox-destroyed", onDestroyed);
gBrowser.removeCurrentTab();
});
function toggle() {
EventUtils.synthesizeKey("VK_F12", {});
}

View file

@ -0,0 +1,78 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
const TEST_URL = "data:text/html,test for opening toolbox in different hosts";
var {Toolbox} = require("devtools/client/framework/toolbox");
var toolbox, tab1, tab2;
function test() {
addTab(TEST_URL).then(tab => {
tab2 = gBrowser.addTab();
let target = TargetFactory.forTab(tab);
gDevTools.showToolbox(target)
.then(testBottomHost, console.error)
.then(null, console.error);
});
}
function testBottomHost(aToolbox) {
toolbox = aToolbox;
// switch to another tab and test toolbox.raise()
gBrowser.selectedTab = tab2;
executeSoon(function () {
is(gBrowser.selectedTab, tab2, "Correct tab is selected before calling raise");
toolbox.raise();
executeSoon(function () {
is(gBrowser.selectedTab, tab1, "Correct tab was selected after calling raise");
toolbox.switchHost(Toolbox.HostType.WINDOW).then(testWindowHost).then(null, console.error);
});
});
}
function testWindowHost() {
// Make sure toolbox is not focused.
window.addEventListener("focus", onFocus, true);
// Need to wait for focus as otherwise window.focus() is overridden by
// toolbox window getting focused first on Linux and Mac.
let onToolboxFocus = () => {
toolbox.win.parent.removeEventListener("focus", onToolboxFocus, true);
info("focusing main window.");
window.focus();
};
// Need to wait for toolbox window to get focus.
toolbox.win.parent.addEventListener("focus", onToolboxFocus, true);
}
function onFocus() {
info("Main window is focused before calling toolbox.raise()");
window.removeEventListener("focus", onFocus, true);
// Check if toolbox window got focus.
let onToolboxFocusAgain = () => {
toolbox.win.parent.removeEventListener("focus", onToolboxFocusAgain, false);
ok(true, "Toolbox window is the focused window after calling toolbox.raise()");
cleanup();
};
toolbox.win.parent.addEventListener("focus", onToolboxFocusAgain, false);
// Now raise toolbox.
toolbox.raise();
}
function cleanup() {
Services.prefs.setCharPref("devtools.toolbox.host", Toolbox.HostType.BOTTOM);
toolbox.destroy().then(function () {
toolbox = null;
gBrowser.removeCurrentTab();
gBrowser.removeCurrentTab();
finish();
});
}

View file

@ -0,0 +1,21 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
const TEST_URL = "data:text/html,test for toolbox being ready";
add_task(function* () {
let tab = yield addTab(TEST_URL);
let target = TargetFactory.forTab(tab);
const toolbox = yield gDevTools.showToolbox(target, "webconsole");
ok(toolbox.isReady, "toolbox isReady is set");
ok(toolbox.threadClient, "toolbox has a thread client");
const toolbox2 = yield gDevTools.showToolbox(toolbox.target, toolbox.toolId);
is(toolbox2, toolbox, "same toolbox");
yield toolbox.destroy();
gBrowser.removeCurrentTab();
});

View file

@ -0,0 +1,43 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
var {Toolbox} = require("devtools/client/framework/toolbox");
const URL_1 = "about:robots";
const URL_2 = "data:text/html;charset=UTF-8," +
encodeURIComponent("<div id=\"remote-page\">foo</div>");
add_task(function* () {
info("Open a tab on a URL supporting only running in parent process");
let tab = yield addTab(URL_1);
is(tab.linkedBrowser.currentURI.spec, URL_1, "We really are on the expected document");
is(tab.linkedBrowser.getAttribute("remote"), "", "And running in parent process");
let toolbox = yield openToolboxForTab(tab);
let onToolboxDestroyed = toolbox.once("destroyed");
let onToolboxCreated = gDevTools.once("toolbox-created");
info("Navigate to a URL supporting remote process");
let onLoaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
gBrowser.loadURI(URL_2);
yield onLoaded;
is(tab.linkedBrowser.getAttribute("remote"), "true", "Navigated to a data: URI and switching to remote");
info("Waiting for the toolbox to be destroyed");
yield onToolboxDestroyed;
info("Waiting for a new toolbox to be created");
toolbox = yield onToolboxCreated;
info("Waiting for the new toolbox to be ready");
yield toolbox.once("ready");
info("Veryify we are inspecting the new document");
let console = yield toolbox.selectTool("webconsole");
let { jsterm } = console.hud;
let url = yield jsterm.execute("document.location.href");
// Uses includes as the old console frontend prints a timestamp
ok(url.textContent.includes(URL_2), "The console inspects the second document");
});

View file

@ -0,0 +1,101 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const PAGE_URL = "data:text/html;charset=utf-8,test select events";
requestLongerTimeout(2);
add_task(function* () {
let tab = yield addTab(PAGE_URL);
let toolbox = yield openToolboxForTab(tab, "webconsole", "bottom");
yield testSelectEvent("inspector");
yield testSelectEvent("webconsole");
yield testSelectEvent("styleeditor");
yield testSelectEvent("inspector");
yield testSelectEvent("webconsole");
yield testSelectEvent("styleeditor");
yield testToolSelectEvent("inspector");
yield testToolSelectEvent("webconsole");
yield testToolSelectEvent("styleeditor");
yield toolbox.destroy();
toolbox = yield openToolboxForTab(tab, "webconsole", "side");
yield testSelectEvent("inspector");
yield testSelectEvent("webconsole");
yield testSelectEvent("styleeditor");
yield testSelectEvent("inspector");
yield testSelectEvent("webconsole");
yield testSelectEvent("styleeditor");
yield toolbox.destroy();
toolbox = yield openToolboxForTab(tab, "webconsole", "window");
yield testSelectEvent("inspector");
yield testSelectEvent("webconsole");
yield testSelectEvent("styleeditor");
yield testSelectEvent("inspector");
yield testSelectEvent("webconsole");
yield testSelectEvent("styleeditor");
yield toolbox.destroy();
yield testSelectToolRace();
/**
* Assert that selecting the given toolId raises a select event
* @param {toolId} Id of the tool to test
*/
function* testSelectEvent(toolId) {
let onSelect = toolbox.once("select");
toolbox.selectTool(toolId);
let id = yield onSelect;
is(id, toolId, toolId + " selected");
}
/**
* Assert that selecting the given toolId raises its corresponding
* selected event
* @param {toolId} Id of the tool to test
*/
function* testToolSelectEvent(toolId) {
let onSelected = toolbox.once(toolId + "-selected");
toolbox.selectTool(toolId);
yield onSelected;
is(toolbox.currentToolId, toolId, toolId + " tool selected");
}
/**
* Assert that two calls to selectTool won't race
*/
function* testSelectToolRace() {
let toolbox = yield openToolboxForTab(tab, "webconsole");
let selected = false;
let onSelect = (event, id) => {
if (selected) {
ok(false, "Got more than one 'select' event");
} else {
selected = true;
}
};
toolbox.once("select", onSelect);
let p1 = toolbox.selectTool("inspector")
let p2 = toolbox.selectTool("inspector");
// Check that both promises don't resolve too early
let checkSelectToolResolution = panel => {
ok(selected, "selectTool resolves only after 'select' event is fired");
let inspector = toolbox.getPanel("inspector");
is(panel, inspector, "selecTool resolves to the panel instance");
};
p1.then(checkSelectToolResolution);
p2.then(checkSelectToolResolution);
yield p1;
yield p2;
yield toolbox.destroy();
}
});

View file

@ -0,0 +1,48 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that opening the toolbox doesn't throw when the previously selected
// tool is not supported.
const testToolDefinition = {
id: "test-tool",
isTargetSupported: () => true,
visibilityswitch: "devtools.test-tool.enabled",
url: "about:blank",
label: "someLabel",
build: (iframeWindow, toolbox) => {
return {
target: toolbox.target,
toolbox: toolbox,
isReady: true,
destroy: () => {},
panelDoc: iframeWindow.document
};
}
};
add_task(function* () {
gDevTools.registerTool(testToolDefinition);
let tab = yield addTab("about:blank");
let target = TargetFactory.forTab(tab);
let toolbox = yield gDevTools.showToolbox(target, testToolDefinition.id);
is(toolbox.currentToolId, "test-tool", "test-tool was selected");
yield gDevTools.closeToolbox(target);
// Make the previously selected tool unavailable.
testToolDefinition.isTargetSupported = () => false;
target = TargetFactory.forTab(tab);
toolbox = yield gDevTools.showToolbox(target);
is(toolbox.currentToolId, "webconsole", "web console was selected");
yield gDevTools.closeToolbox(target);
gDevTools.unregisterTool(testToolDefinition.id);
tab = toolbox = target = null;
gBrowser.removeCurrentTab();
});

View file

@ -0,0 +1,181 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
function test() {
const Cu = Components.utils;
let {ToolSidebar} = require("devtools/client/framework/sidebar");
const toolURL = "data:text/xml;charset=utf8,<?xml version='1.0'?>" +
"<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'>" +
"<hbox flex='1'><description flex='1'>foo</description><splitter class='devtools-side-splitter'/>" +
"<tabbox flex='1' id='sidebar' class='devtools-sidebar-tabs'><tabs/><tabpanels flex='1'/></tabbox>" +
"</hbox>" +
"</window>";
const tab1URL = "data:text/html;charset=utf8,<title>1</title><p>1</p>";
const tab2URL = "data:text/html;charset=utf8,<title>2</title><p>2</p>";
const tab3URL = "data:text/html;charset=utf8,<title>3</title><p>3</p>";
let panelDoc;
let tab1Selected = false;
let registeredTabs = {};
let readyTabs = {};
let toolDefinition = {
id: "fakeTool4242",
visibilityswitch: "devtools.fakeTool4242.enabled",
url: toolURL,
label: "FAKE TOOL!!!",
isTargetSupported: () => true,
build: function (iframeWindow, toolbox) {
let deferred = defer();
executeSoon(() => {
deferred.resolve({
target: toolbox.target,
toolbox: toolbox,
isReady: true,
destroy: function () {},
panelDoc: iframeWindow.document,
});
});
return deferred.promise;
},
};
gDevTools.registerTool(toolDefinition);
addTab("about:blank").then(function (aTab) {
let target = TargetFactory.forTab(aTab);
gDevTools.showToolbox(target, toolDefinition.id).then(function (toolbox) {
let panel = toolbox.getPanel(toolDefinition.id);
panel.toolbox = toolbox;
ok(true, "Tool open");
let tabbox = panel.panelDoc.getElementById("sidebar");
panel.sidebar = new ToolSidebar(tabbox, panel, "testbug865688", true);
panel.sidebar.on("new-tab-registered", function (event, id) {
registeredTabs[id] = true;
});
panel.sidebar.once("tab1-ready", function (event) {
info(event);
readyTabs.tab1 = true;
allTabsReady(panel);
});
panel.sidebar.once("tab2-ready", function (event) {
info(event);
readyTabs.tab2 = true;
allTabsReady(panel);
});
panel.sidebar.once("tab3-ready", function (event) {
info(event);
readyTabs.tab3 = true;
allTabsReady(panel);
});
panel.sidebar.once("tab1-selected", function (event) {
info(event);
tab1Selected = true;
allTabsReady(panel);
});
panel.sidebar.addTab("tab1", tab1URL, {selected: true});
panel.sidebar.addTab("tab2", tab2URL);
panel.sidebar.addTab("tab3", tab3URL);
panel.sidebar.show();
}).then(null, console.error);
});
function allTabsReady(panel) {
if (!tab1Selected || !readyTabs.tab1 || !readyTabs.tab2 || !readyTabs.tab3) {
return;
}
ok(registeredTabs.tab1, "tab1 registered");
ok(registeredTabs.tab2, "tab2 registered");
ok(registeredTabs.tab3, "tab3 registered");
ok(readyTabs.tab1, "tab1 ready");
ok(readyTabs.tab2, "tab2 ready");
ok(readyTabs.tab3, "tab3 ready");
let tabs = panel.sidebar._tabbox.querySelectorAll("tab");
let panels = panel.sidebar._tabbox.querySelectorAll("tabpanel");
let label = 1;
for (let tab of tabs) {
is(tab.getAttribute("label"), label++, "Tab has the right title");
}
is(label, 4, "Found the right amount of tabs.");
is(panel.sidebar._tabbox.selectedPanel, panels[0], "First tab is selected");
is(panel.sidebar.getCurrentTabID(), "tab1", "getCurrentTabID() is correct");
panel.sidebar.once("tab1-unselected", function () {
ok(true, "received 'unselected' event");
panel.sidebar.once("tab2-selected", function () {
ok(true, "received 'selected' event");
tabs[1].focus();
is(panel.sidebar._panelDoc.activeElement, tabs[1],
"Focus is set to second tab");
panel.sidebar.hide();
isnot(panel.sidebar._panelDoc.activeElement, tabs[1],
"Focus is reset for sidebar");
is(panel.sidebar._tabbox.getAttribute("hidden"), "true", "Sidebar hidden");
is(panel.sidebar.getWindowForTab("tab1").location.href, tab1URL, "Window is accessible");
testRemoval(panel);
});
});
panel.sidebar.select("tab2");
}
function testRemoval(panel) {
panel.sidebar.once("tab-unregistered", function (event, id) {
info(event);
registeredTabs[id] = false;
is(id, "tab3", "The right tab must be removed");
let tabs = panel.sidebar._tabbox.querySelectorAll("tab");
let panels = panel.sidebar._tabbox.querySelectorAll("tabpanel");
is(tabs.length, 2, "There is the right number of tabs");
is(panels.length, 2, "There is the right number of panels");
testWidth(panel);
});
panel.sidebar.removeTab("tab3");
}
function testWidth(panel) {
let tabbox = panel.panelDoc.getElementById("sidebar");
tabbox.width = 420;
panel.sidebar.destroy().then(function () {
tabbox.width = 0;
panel.sidebar = new ToolSidebar(tabbox, panel, "testbug865688", true);
panel.sidebar.show();
is(panel.panelDoc.getElementById("sidebar").width, 420, "Width restored");
finishUp(panel);
});
}
function finishUp(panel) {
panel.sidebar.destroy();
panel.toolbox.destroy().then(function () {
gDevTools.unregisterTool(toolDefinition.id);
gBrowser.removeCurrentTab();
executeSoon(function () {
finish();
});
});
}
}

View file

@ -0,0 +1,93 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
function test() {
const Cu = Components.utils;
const { ToolSidebar } = require("devtools/client/framework/sidebar");
const toolURL = "data:text/xml;charset=utf8,<?xml version='1.0'?>" +
"<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'>" +
"<hbox flex='1'><description flex='1'>foo</description><splitter class='devtools-side-splitter'/>" +
"<tabbox flex='1' id='sidebar' class='devtools-sidebar-tabs'><tabs/><tabpanels flex='1'/></tabbox>" +
"</hbox>" +
"</window>";
const tab1URL = "data:text/html;charset=utf8,<title>1</title><p>1</p>";
let collectedEvents = [];
let toolDefinition = {
id: "testTool1072208",
visibilityswitch: "devtools.testTool1072208.enabled",
url: toolURL,
label: "Test tool",
isTargetSupported: () => true,
build: function (iframeWindow, toolbox) {
let deferred = defer();
executeSoon(() => {
deferred.resolve({
target: toolbox.target,
toolbox: toolbox,
isReady: true,
destroy: function () {},
panelDoc: iframeWindow.document,
});
});
return deferred.promise;
},
};
gDevTools.registerTool(toolDefinition);
addTab("about:blank").then(function (aTab) {
let target = TargetFactory.forTab(aTab);
gDevTools.showToolbox(target, toolDefinition.id).then(function (toolbox) {
let panel = toolbox.getPanel(toolDefinition.id);
ok(true, "Tool open");
panel.once("sidebar-created", function (event, id) {
collectedEvents.push(event);
});
panel.once("sidebar-destroyed", function (event, id) {
collectedEvents.push(event);
});
let tabbox = panel.panelDoc.getElementById("sidebar");
panel.sidebar = new ToolSidebar(tabbox, panel, "testbug1072208", true);
panel.sidebar.once("show", function (event, id) {
collectedEvents.push(event);
});
panel.sidebar.once("hide", function (event, id) {
collectedEvents.push(event);
});
panel.sidebar.once("tab1-selected", () => finishUp(panel));
panel.sidebar.addTab("tab1", tab1URL, {selected: true});
panel.sidebar.show();
}).then(null, console.error);
});
function finishUp(panel) {
panel.sidebar.hide();
panel.sidebar.destroy();
let events = collectedEvents.join(":");
is(events, "sidebar-created:show:hide:sidebar-destroyed",
"Found the right amount of collected events.");
panel.toolbox.destroy().then(function () {
gDevTools.unregisterTool(toolDefinition.id);
gBrowser.removeCurrentTab();
executeSoon(function () {
finish();
});
});
}
}

View file

@ -0,0 +1,78 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that the sidebar widget auto-registers existing tabs.
const {ToolSidebar} = require("devtools/client/framework/sidebar");
const testToolURL = "data:text/xml;charset=utf8,<?xml version='1.0'?>" +
"<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'>" +
"<hbox flex='1'><description flex='1'>test tool</description>" +
"<splitter class='devtools-side-splitter'/>" +
"<tabbox flex='1' id='sidebar' class='devtools-sidebar-tabs'>" +
"<tabs><tab id='tab1' label='tab 1'></tab><tab id='tab2' label='tab 2'></tab></tabs>" +
"<tabpanels flex='1'><tabpanel id='tabpanel1'>tab 1</tabpanel><tabpanel id='tabpanel2'>tab 2</tabpanel></tabpanels>" +
"</tabbox></hbox></window>";
const testToolDefinition = {
id: "testTool",
url: testToolURL,
label: "Test Tool",
isTargetSupported: () => true,
build: (iframeWindow, toolbox) => {
return promise.resolve({
target: toolbox.target,
toolbox: toolbox,
isReady: true,
destroy: () => {},
panelDoc: iframeWindow.document,
});
}
};
add_task(function* () {
let tab = yield addTab("about:blank");
let target = TargetFactory.forTab(tab);
gDevTools.registerTool(testToolDefinition);
let toolbox = yield gDevTools.showToolbox(target, testToolDefinition.id);
let toolPanel = toolbox.getPanel(testToolDefinition.id);
let tabbox = toolPanel.panelDoc.getElementById("sidebar");
info("Creating the sidebar widget");
let sidebar = new ToolSidebar(tabbox, toolPanel, "bug1101569");
info("Checking that existing tabs have been registered");
ok(sidebar.getTab("tab1"), "Existing tab 1 was found");
ok(sidebar.getTab("tab2"), "Existing tab 2 was found");
ok(sidebar.getTabPanel("tabpanel1"), "Existing tabpanel 1 was found");
ok(sidebar.getTabPanel("tabpanel2"), "Existing tabpanel 2 was found");
info("Checking that the sidebar API works with existing tabs");
sidebar.select("tab2");
is(tabbox.selectedTab, tabbox.querySelector("#tab2"),
"Existing tabs can be selected");
sidebar.select("tab1");
is(tabbox.selectedTab, tabbox.querySelector("#tab1"),
"Existing tabs can be selected");
is(sidebar.getCurrentTabID(), "tab1", "getCurrentTabID returns the expected id");
info("Removing a tab");
sidebar.removeTab("tab2", "tabpanel2");
ok(!sidebar.getTab("tab2"), "Tab 2 was removed correctly");
ok(!sidebar.getTabPanel("tabpanel2"), "Tabpanel 2 was removed correctly");
sidebar.destroy();
yield toolbox.destroy();
gDevTools.unregisterTool(testToolDefinition.id);
gBrowser.removeCurrentTab();
});

View file

@ -0,0 +1,80 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that the sidebar widget correctly displays the "all tabs..." button
// when the tabs overflow.
const {ToolSidebar} = require("devtools/client/framework/sidebar");
const testToolDefinition = {
id: "testTool",
url: CHROME_URL_ROOT + "browser_toolbox_sidebar_tool.xul",
label: "Test Tool",
isTargetSupported: () => true,
build: (iframeWindow, toolbox) => {
return {
target: toolbox.target,
toolbox: toolbox,
isReady: true,
destroy: () => {},
panelDoc: iframeWindow.document,
};
}
};
add_task(function* () {
let tab = yield addTab("about:blank");
let target = TargetFactory.forTab(tab);
gDevTools.registerTool(testToolDefinition);
let toolbox = yield gDevTools.showToolbox(target, testToolDefinition.id);
let toolPanel = toolbox.getPanel(testToolDefinition.id);
let tabbox = toolPanel.panelDoc.getElementById("sidebar");
info("Creating the sidebar widget");
let sidebar = new ToolSidebar(tabbox, toolPanel, "bug1101569", {
showAllTabsMenu: true
});
let allTabsMenu = toolPanel.panelDoc.querySelector(".devtools-sidebar-alltabs");
ok(allTabsMenu, "The all-tabs menu is available");
is(allTabsMenu.getAttribute("hidden"), "true", "The menu is hidden for now");
info("Adding 10 tabs to the sidebar widget");
for (let nb = 0; nb < 10; nb++) {
let url = `data:text/html;charset=utf8,<title>tab ${nb}</title><p>Test tab ${nb}</p>`;
sidebar.addTab("tab" + nb, url, {selected: nb === 0});
}
info("Fake an overflow event so that the all-tabs menu is visible");
sidebar._onTabBoxOverflow();
ok(!allTabsMenu.hasAttribute("hidden"), "The all-tabs menu is now shown");
info("Select each tab, one by one");
for (let nb = 0; nb < 10; nb++) {
let id = "tab" + nb;
info("Found tab item nb " + nb);
let item = allTabsMenu.querySelector("#sidebar-alltabs-item-" + id);
info("Click on the tab");
EventUtils.sendMouseEvent({type: "click"}, item, toolPanel.panelDoc.defaultView);
is(tabbox.selectedTab.id, "sidebar-tab-" + id,
"The selected tab is now nb " + nb);
}
info("Fake an underflow event so that the all-tabs menu gets hidden");
sidebar._onTabBoxUnderflow();
is(allTabsMenu.getAttribute("hidden"), "true", "The all-tabs menu is hidden");
yield sidebar.destroy();
yield toolbox.destroy();
gDevTools.unregisterTool(testToolDefinition.id);
gBrowser.removeCurrentTab();
});

View file

@ -0,0 +1,18 @@
<?xml version="1.0"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
<?xml-stylesheet href="chrome://devtools/content/shared/widgets/widgets.css" type="text/css"?>
<?xml-stylesheet href="chrome://devtools/skin/widgets.css" type="text/css"?>
<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<script type="application/javascript;version=1.8" src="chrome://devtools/content/shared/theme-switching.js"/>
<box flex="1" class="devtools-responsive-container theme-body">
<vbox flex="1" class="devtools-main-content" id="content">test</vbox>
<splitter class="devtools-side-splitter"/>
<tabbox flex="1" id="sidebar" class="devtools-sidebar-tabs">
<tabs/>
<tabpanels flex="1"/>
</tabbox>
</box>
</window>

View file

@ -0,0 +1,85 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Tests that these toolbox split console APIs work:
// * toolbox.useKeyWithSplitConsole()
// * toolbox.isSplitConsoleFocused
let gToolbox = null;
let panelWin = null;
const URL = "data:text/html;charset=utf8,test split console key delegation";
// Force the old debugger UI since it's directly used (see Bug 1301705)
Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
registerCleanupFunction(function* () {
Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
});
add_task(function* () {
let tab = yield addTab(URL);
let target = TargetFactory.forTab(tab);
gToolbox = yield gDevTools.showToolbox(target, "jsdebugger");
panelWin = gToolbox.getPanel("jsdebugger").panelWin;
yield gToolbox.openSplitConsole();
yield testIsSplitConsoleFocused();
yield testUseKeyWithSplitConsole();
yield testUseKeyWithSplitConsoleWrongTool();
yield cleanup();
});
function* testIsSplitConsoleFocused() {
yield gToolbox.openSplitConsole();
// The newly opened split console should have focus
ok(gToolbox.isSplitConsoleFocused(), "Split console is focused");
panelWin.focus();
ok(!gToolbox.isSplitConsoleFocused(), "Split console is no longer focused");
}
// A key bound to the selected tool should trigger it's command
function* testUseKeyWithSplitConsole() {
let commandCalled = false;
info("useKeyWithSplitConsole on debugger while debugger is focused");
gToolbox.useKeyWithSplitConsole("F3", () => {
commandCalled = true;
}, "jsdebugger");
info("synthesizeKey with the console focused");
let consoleInput = gToolbox.getPanel("webconsole").hud.jsterm.inputNode;
consoleInput.focus();
synthesizeKeyShortcut("F3", panelWin);
ok(commandCalled, "Shortcut key should trigger the command");
}
// A key bound to a *different* tool should not trigger it's command
function* testUseKeyWithSplitConsoleWrongTool() {
let commandCalled = false;
info("useKeyWithSplitConsole on inspector while debugger is focused");
gToolbox.useKeyWithSplitConsole("F4", () => {
commandCalled = true;
}, "inspector");
info("synthesizeKey with the console focused");
let consoleInput = gToolbox.getPanel("webconsole").hud.jsterm.inputNode;
consoleInput.focus();
synthesizeKeyShortcut("F4", panelWin);
ok(!commandCalled, "Shortcut key shouldn't trigger the command");
}
function* cleanup() {
// We don't want the open split console to confuse other tests..
Services.prefs.clearUserPref("devtools.toolbox.splitconsoleEnabled");
yield gToolbox.destroy();
gBrowser.removeCurrentTab();
gToolbox = panelWin = null;
}

View file

@ -0,0 +1,68 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
requestLongerTimeout(2);
var {Toolbox} = require("devtools/client/framework/toolbox");
const {LocalizationHelper} = require("devtools/shared/l10n");
const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
add_task(function* () {
let tab = yield addTab("about:blank");
let target = TargetFactory.forTab(tab);
yield target.makeRemote();
let toolIDs = gDevTools.getToolDefinitionArray()
.filter(def => def.isTargetSupported(target))
.map(def => def.id);
let toolbox = yield gDevTools.showToolbox(target, toolIDs[0], Toolbox.HostType.BOTTOM);
let nextShortcut = L10N.getStr("toolbox.nextTool.key");
let prevShortcut = L10N.getStr("toolbox.previousTool.key");
// Iterate over all tools, starting from options to netmonitor, in normal
// order.
for (let i = 1; i < toolIDs.length; i++) {
yield testShortcuts(toolbox, i, nextShortcut, toolIDs);
}
// Iterate again, in the same order, starting from netmonitor (so next one is
// 0: options).
for (let i = 0; i < toolIDs.length; i++) {
yield testShortcuts(toolbox, i, nextShortcut, toolIDs);
}
// Iterate over all tools in reverse order, starting from netmonitor to
// options.
for (let i = toolIDs.length - 2; i >= 0; i--) {
yield testShortcuts(toolbox, i, prevShortcut, toolIDs);
}
// Iterate again, in reverse order again, starting from options (so next one
// is length-1: netmonitor).
for (let i = toolIDs.length - 1; i >= 0; i--) {
yield testShortcuts(toolbox, i, prevShortcut, toolIDs);
}
yield toolbox.destroy();
gBrowser.removeCurrentTab();
});
function* testShortcuts(toolbox, index, shortcut, toolIDs) {
info("Testing shortcut to switch to tool " + index + ":" + toolIDs[index] +
" using shortcut " + shortcut);
let onToolSelected = toolbox.once("select");
synthesizeKeyShortcut(shortcut);
let id = yield onToolSelected;
info("toolbox-select event from " + id);
is(toolIDs.indexOf(id), index,
"Correct tool is selected on pressing the shortcut for " + id);
}

View file

@ -0,0 +1,60 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
// Test about:devtools-toolbox?target which allows opening a toolbox in an
// iframe while defining which document to debug by setting a `target`
// attribute refering to the document to debug.
add_task(function *() {
// iframe loads the document to debug
let iframe = document.createElement("browser");
iframe.setAttribute("type", "content");
document.documentElement.appendChild(iframe);
let onLoad = once(iframe, "load", true);
iframe.setAttribute("src", "data:text/html,document to debug");
yield onLoad;
is(iframe.contentWindow.document.body.innerHTML, "document to debug");
// toolbox loads the toolbox document
let toolboxIframe = document.createElement("iframe");
document.documentElement.appendChild(toolboxIframe);
// Important step to define which target to debug
toolboxIframe.target = iframe;
let onToolboxReady = gDevTools.once("toolbox-ready");
onLoad = once(toolboxIframe, "load", true);
toolboxIframe.setAttribute("src", "about:devtools-toolbox?target");
yield onLoad;
// Also wait for toolbox-ready, as toolbox document load isn't enough, there
// is plenty of asynchronous steps during toolbox load
info("Waiting for toolbox-ready");
let toolbox = yield onToolboxReady;
let onToolboxDestroyed = gDevTools.once("toolbox-destroyed");
let onTabActorDetached = once(toolbox.target.client, "tabDetached");
info("Removing the iframes");
toolboxIframe.remove();
// And wait for toolbox-destroyed as toolbox unload is also full of
// asynchronous operation that outlast unload event
info("Waiting for toolbox-destroyed");
yield onToolboxDestroyed;
info("Toolbox destroyed");
// Also wait for tabDetached. Toolbox destroys the Target which calls
// TabActor.detach(). But Target doesn't wait for detach's end to resolve.
// Whereas it is quite important as it is a significant part of toolbox
// cleanup. If we do not wait for it and starts removing debugged document,
// the actor is still considered as being attached and continues processing
// events.
yield onTabActorDetached;
iframe.remove();
});

View file

@ -0,0 +1,55 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
const URL = "data:text/html;charset=utf8,test for textbox context menu";
add_task(function* () {
let toolbox = yield openNewTabAndToolbox(URL, "inspector");
let textboxContextMenu = toolbox.textBoxContextMenuPopup;
emptyClipboard();
// Make sure the focus is predictable.
let inspector = toolbox.getPanel("inspector");
let onFocus = once(inspector.searchBox, "focus");
inspector.searchBox.focus();
yield onFocus;
ok(textboxContextMenu, "The textbox context menu is loaded in the toolbox");
let cmdUndo = textboxContextMenu.querySelector("[command=cmd_undo]");
let cmdDelete = textboxContextMenu.querySelector("[command=cmd_delete]");
let cmdSelectAll = textboxContextMenu.querySelector("[command=cmd_selectAll]");
let cmdCut = textboxContextMenu.querySelector("[command=cmd_cut]");
let cmdCopy = textboxContextMenu.querySelector("[command=cmd_copy]");
let cmdPaste = textboxContextMenu.querySelector("[command=cmd_paste]");
info("Opening context menu");
let onContextMenuPopup = once(textboxContextMenu, "popupshowing");
textboxContextMenu.openPopupAtScreen(0, 0, true);
yield onContextMenuPopup;
is(cmdUndo.getAttribute("disabled"), "true", "cmdUndo is disabled");
is(cmdDelete.getAttribute("disabled"), "true", "cmdDelete is disabled");
is(cmdSelectAll.getAttribute("disabled"), "true", "cmdSelectAll is disabled");
// Cut/Copy items are enabled in context menu even if there
// is no selection. See also Bug 1303033
is(cmdCut.getAttribute("disabled"), "", "cmdCut is enabled");
is(cmdCopy.getAttribute("disabled"), "", "cmdCopy is enabled");
if (isWindows()) {
// emptyClipboard only works on Windows (666254), assert paste only for this OS.
is(cmdPaste.getAttribute("disabled"), "true", "cmdPaste is disabled");
}
yield cleanup(toolbox);
});
function* cleanup(toolbox) {
yield toolbox.destroy();
gBrowser.removeCurrentTab();
}

View file

@ -0,0 +1,102 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* import-globals-from shared-head.js */
"use strict";
// Test for dynamically registering and unregistering themes
const CHROME_URL = "chrome://mochitests/content/browser/devtools/client/framework/test/";
var toolbox;
add_task(function* themeRegistration() {
let tab = yield addTab("data:text/html,test");
let target = TargetFactory.forTab(tab);
toolbox = yield gDevTools.showToolbox(target, "options");
let themeId = yield new Promise(resolve => {
gDevTools.once("theme-registered", (e, registeredThemeId) => {
resolve(registeredThemeId);
});
gDevTools.registerTheme({
id: "test-theme",
label: "Test theme",
stylesheets: [CHROME_URL + "doc_theme.css"],
classList: ["theme-test"],
});
});
is(themeId, "test-theme", "theme-registered event handler sent theme id");
ok(gDevTools.getThemeDefinitionMap().has(themeId), "theme added to map");
});
add_task(function* themeInOptionsPanel() {
let panelWin = toolbox.getCurrentPanel().panelWin;
let doc = panelWin.frameElement.contentDocument;
let themeBox = doc.getElementById("devtools-theme-box");
let testThemeOption = themeBox.querySelector(
"input[type=radio][value=test-theme]");
ok(testThemeOption, "new theme exists in the Options panel");
let lightThemeOption = themeBox.querySelector(
"input[type=radio][value=light]");
let color = panelWin.getComputedStyle(themeBox).color;
isnot(color, "rgb(255, 0, 0)", "style unapplied");
let onThemeSwithComplete = once(panelWin, "theme-switch-complete");
// Select test theme.
testThemeOption.click();
info("Waiting for theme to finish loading");
yield onThemeSwithComplete;
color = panelWin.getComputedStyle(themeBox).color;
is(color, "rgb(255, 0, 0)", "style applied");
onThemeSwithComplete = once(panelWin, "theme-switch-complete");
// Select light theme
lightThemeOption.click();
info("Waiting for theme to finish loading");
yield onThemeSwithComplete;
color = panelWin.getComputedStyle(themeBox).color;
isnot(color, "rgb(255, 0, 0)", "style unapplied");
onThemeSwithComplete = once(panelWin, "theme-switch-complete");
// Select test theme again.
testThemeOption.click();
yield onThemeSwithComplete;
});
add_task(function* themeUnregistration() {
let panelWin = toolbox.getCurrentPanel().panelWin;
let onUnRegisteredTheme = once(gDevTools, "theme-unregistered");
let onThemeSwitchComplete = once(panelWin, "theme-switch-complete");
gDevTools.unregisterTheme("test-theme");
yield onUnRegisteredTheme;
yield onThemeSwitchComplete;
ok(!gDevTools.getThemeDefinitionMap().has("test-theme"),
"theme removed from map");
let doc = panelWin.frameElement.contentDocument;
let themeBox = doc.getElementById("devtools-theme-box");
// The default light theme must be selected now.
is(themeBox.querySelector("#devtools-theme-box [value=light]").checked, true,
"light theme must be selected");
});
add_task(function* cleanup() {
yield toolbox.destroy();
toolbox = null;
});

View file

@ -0,0 +1,108 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test toggling the toolbox with ACCEL+SHIFT+I / ACCEL+ALT+I and F12 in docked
// and detached (window) modes.
const URL = "data:text/html;charset=utf-8,Toggling devtools using shortcuts";
var {Toolbox} = require("devtools/client/framework/toolbox");
add_task(function* () {
// Make sure this test starts with the selectedTool pref cleared. Previous
// tests select various tools, and that sets this pref.
Services.prefs.clearUserPref("devtools.toolbox.selectedTool");
// Test with ACCEL+SHIFT+I / ACCEL+ALT+I (MacOSX) ; modifiers should match :
// - toolbox-key-toggle in devtools/client/framework/toolbox-window.xul
// - key_devToolboxMenuItem in browser/base/content/browser.xul
info("Test toggle using CTRL+SHIFT+I/CMD+ALT+I");
yield testToggle("I", {
accelKey: true,
shiftKey: !navigator.userAgent.match(/Mac/),
altKey: navigator.userAgent.match(/Mac/)
});
// Test with F12 ; no modifiers
info("Test toggle using F12");
yield testToggle("VK_F12", {});
});
function* testToggle(key, modifiers) {
let tab = yield addTab(URL + " ; key : '" + key + "'");
yield gDevTools.showToolbox(TargetFactory.forTab(tab));
yield testToggleDockedToolbox(tab, key, modifiers);
yield testToggleDetachedToolbox(tab, key, modifiers);
yield cleanup();
}
function* testToggleDockedToolbox(tab, key, modifiers) {
let toolbox = getToolboxForTab(tab);
isnot(toolbox.hostType, Toolbox.HostType.WINDOW,
"Toolbox is docked in the main window");
info("verify docked toolbox is destroyed when using toggle key");
let onToolboxDestroyed = once(gDevTools, "toolbox-destroyed");
EventUtils.synthesizeKey(key, modifiers);
yield onToolboxDestroyed;
ok(true, "Docked toolbox is destroyed when using a toggle key");
info("verify new toolbox is created when using toggle key");
let onToolboxReady = once(gDevTools, "toolbox-ready");
EventUtils.synthesizeKey(key, modifiers);
yield onToolboxReady;
ok(true, "Toolbox is created by using when toggle key");
}
function* testToggleDetachedToolbox(tab, key, modifiers) {
let toolbox = getToolboxForTab(tab);
info("change the toolbox hostType to WINDOW");
yield toolbox.switchHost(Toolbox.HostType.WINDOW);
is(toolbox.hostType, Toolbox.HostType.WINDOW,
"Toolbox opened on separate window");
info("Wait for focus on the toolbox window");
yield new Promise(res => waitForFocus(res, toolbox.win));
info("Focus main window to put the toolbox window in the background");
let onMainWindowFocus = once(window, "focus");
window.focus();
yield onMainWindowFocus;
ok(true, "Main window focused");
info("Verify windowed toolbox is focused instead of closed when using " +
"toggle key from the main window");
let toolboxWindow = toolbox.win.top;
let onToolboxWindowFocus = once(toolboxWindow, "focus", true);
EventUtils.synthesizeKey(key, modifiers);
yield onToolboxWindowFocus;
ok(true, "Toolbox focused and not destroyed");
info("Verify windowed toolbox is destroyed when using toggle key from its " +
"own window");
let onToolboxDestroyed = once(gDevTools, "toolbox-destroyed");
EventUtils.synthesizeKey(key, modifiers, toolboxWindow);
yield onToolboxDestroyed;
ok(true, "Toolbox destroyed");
}
function getToolboxForTab(tab) {
return gDevTools.getToolbox(TargetFactory.forTab(tab));
}
function* cleanup() {
Services.prefs.setCharPref("devtools.toolbox.host",
Toolbox.HostType.BOTTOM);
gBrowser.removeCurrentTab();
}

View file

@ -0,0 +1,51 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
requestLongerTimeout(5);
/**
* Whitelisting this test.
* As part of bug 1077403, the leaking uncaught rejection should be fixed.
*/
thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Shader Editor is " +
"still waiting for a WebGL context to be created.");
function performChecks(target) {
return Task.spawn(function* () {
let toolIds = gDevTools.getToolDefinitionArray()
.filter(def => def.isTargetSupported(target))
.map(def => def.id);
let toolbox;
for (let index = 0; index < toolIds.length; index++) {
let toolId = toolIds[index];
info("About to open " + index + "/" + toolId);
toolbox = yield gDevTools.showToolbox(target, toolId);
ok(toolbox, "toolbox exists for " + toolId);
is(toolbox.currentToolId, toolId, "currentToolId should be " + toolId);
let panel = toolbox.getCurrentPanel();
ok(panel.isReady, toolId + " panel should be ready");
}
yield toolbox.destroy();
});
}
function test() {
Task.spawn(function* () {
toggleAllTools(true);
let tab = yield addTab("about:blank");
let target = TargetFactory.forTab(tab);
yield target.makeRemote();
yield performChecks(target);
gBrowser.removeCurrentTab();
toggleAllTools(false);
finish();
}, console.error);
}

View file

@ -0,0 +1,135 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Whitelisting this test.
* As part of bug 1077403, the leaking uncaught rejection should be fixed.
*/
thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Shader Editor is " +
"still waiting for a WebGL context to be created.");
const { DebuggerServer } = require("devtools/server/main");
const { DebuggerClient } = require("devtools/shared/client/main");
// Bug 1277805: Too slow for debug runs
requestLongerTimeout(2);
/**
* Bug 979536: Ensure fronts are destroyed after toolbox close.
*
* The fronts need to be destroyed manually to unbind their onPacket handlers.
*
* When you initialize a front and call |this.manage|, it adds a client actor
* pool that the DebuggerClient uses to route packet replies to that actor.
*
* Most (all?) tools create a new front when they are opened. When the destroy
* step is skipped and the tool is reopened, a second front is created and also
* added to the client actor pool. When a packet reply is received, is ends up
* being routed to the first (now unwanted) front that is still in the client
* actor pool. Since this is not the same front that was used to make the
* request, an error occurs.
*
* This problem does not occur with the toolbox for a local tab because the
* toolbox target creates its own DebuggerClient for the local tab, and the
* client is destroyed when the toolbox is closed, which removes the client
* actor pools, and avoids this issue.
*
* In WebIDE, we do not destroy the DebuggerClient on toolbox close because it
* is still used for other purposes like managing apps, etc. that aren't part of
* a toolbox. Thus, the same client gets reused across multiple toolboxes,
* which leads to the tools failing if they don't destroy their fronts.
*/
function runTools(target) {
return Task.spawn(function* () {
let toolIds = gDevTools.getToolDefinitionArray()
.filter(def => def.isTargetSupported(target))
.map(def => def.id);
let toolbox;
for (let index = 0; index < toolIds.length; index++) {
let toolId = toolIds[index];
info("About to open " + index + "/" + toolId);
toolbox = yield gDevTools.showToolbox(target, toolId, "window");
ok(toolbox, "toolbox exists for " + toolId);
is(toolbox.currentToolId, toolId, "currentToolId should be " + toolId);
let panel = toolbox.getCurrentPanel();
ok(panel.isReady, toolId + " panel should be ready");
}
yield toolbox.destroy();
});
}
function getClient() {
let deferred = defer();
if (!DebuggerServer.initialized) {
DebuggerServer.init();
DebuggerServer.addBrowserActors();
}
let transport = DebuggerServer.connectPipe();
let client = new DebuggerClient(transport);
return client.connect().then(() => client);
}
function getTarget(client) {
let deferred = defer();
client.listTabs(tabList => {
let target = TargetFactory.forRemoteTab({
client: client,
form: tabList.tabs[tabList.selected],
chrome: false
});
deferred.resolve(target);
});
return deferred.promise;
}
function test() {
Task.spawn(function* () {
toggleAllTools(true);
yield addTab("about:blank");
let client = yield getClient();
let target = yield getTarget(client);
yield runTools(target);
// Actor fronts should be destroyed now that the toolbox has closed, but
// look for any that remain.
for (let pool of client.__pools) {
if (!pool.__poolMap) {
continue;
}
for (let actor of pool.__poolMap.keys()) {
// Bug 1056342: Profiler fails today because of framerate actor, but
// this appears more complex to rework, so leave it for that bug to
// resolve.
if (actor.includes("framerateActor")) {
todo(false, "Front for " + actor + " still held in pool!");
continue;
}
// gcliActor is for the commandline which is separate to the toolbox
if (actor.includes("gcliActor")) {
continue;
}
ok(false, "Front for " + actor + " still held in pool!");
}
}
gBrowser.removeCurrentTab();
DebuggerServer.destroy();
toggleAllTools(false);
finish();
}, console.error);
}

View file

@ -0,0 +1,108 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const { on, off } = require("sdk/event/core");
const { DebuggerClient } = require("devtools/shared/client/main");
function test() {
gDevTools.on("toolbox-created", onToolboxCreated);
on(DebuggerClient, "connect", onDebuggerClientConnect);
addTab("about:blank").then(function () {
let target = TargetFactory.forTab(gBrowser.selectedTab);
gDevTools.showToolbox(target, "webconsole").then(testResults);
});
}
function testResults(toolbox) {
testPackets(sent1, received1);
testPackets(sent2, received2);
cleanUp(toolbox);
}
function cleanUp(toolbox) {
gDevTools.off("toolbox-created", onToolboxCreated);
off(DebuggerClient, "connect", onDebuggerClientConnect);
toolbox.destroy().then(function () {
gBrowser.removeCurrentTab();
executeSoon(function () {
finish();
});
});
}
function testPackets(sent, received) {
ok(sent.length > 0, "There must be at least one sent packet");
ok(received.length > 0, "There must be at leaset one received packet");
if (!sent.length || received.length) {
return;
}
let sentPacket = sent[0];
let receivedPacket = received[0];
is(receivedPacket.from, "root",
"The first received packet is from the root");
is(receivedPacket.applicationType, "browser",
"The first received packet has browser type");
is(sentPacket.type, "listTabs",
"The first sent packet is for list of tabs");
}
// Listen to the transport object that is associated with the
// default Toolbox debugger client
var sent1 = [];
var received1 = [];
function send1(eventId, packet) {
sent1.push(packet);
}
function onPacket1(eventId, packet) {
received1.push(packet);
}
function onToolboxCreated(eventId, toolbox) {
toolbox.target.makeRemote();
let client = toolbox.target.client;
let transport = client._transport;
transport.on("send", send1);
transport.on("packet", onPacket1);
client.addOneTimeListener("closed", event => {
transport.off("send", send1);
transport.off("packet", onPacket1);
});
}
// Listen to all debugger client object protocols.
var sent2 = [];
var received2 = [];
function send2(eventId, packet) {
sent2.push(packet);
}
function onPacket2(eventId, packet) {
received2.push(packet);
}
function onDebuggerClientConnect(client) {
let transport = client._transport;
transport.on("send", send2);
transport.on("packet", onPacket2);
client.addOneTimeListener("closed", event => {
transport.off("send", send2);
transport.off("packet", onPacket2);
});
}

View file

@ -0,0 +1,46 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests that Toolbox#viewSourceInDebugger works when debugger is not
* yet opened.
*/
var URL = `${URL_ROOT}doc_viewsource.html`;
var JS_URL = `${URL_ROOT}code_math.js`;
// Force the old debugger UI since it's directly used (see Bug 1301705)
Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
registerCleanupFunction(function* () {
Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
});
function* viewSource() {
let toolbox = yield openNewTabAndToolbox(URL);
yield toolbox.viewSourceInDebugger(JS_URL, 2);
let debuggerPanel = toolbox.getPanel("jsdebugger");
ok(debuggerPanel, "The debugger panel was opened.");
is(toolbox.currentToolId, "jsdebugger", "The debugger panel was selected.");
let { DebuggerView } = debuggerPanel.panelWin;
let Sources = DebuggerView.Sources;
is(Sources.selectedValue, getSourceActor(Sources, JS_URL),
"The correct source is shown in the debugger.");
is(DebuggerView.editor.getCursor().line + 1, 2,
"The correct line is highlighted in the debugger's source editor.");
yield closeToolboxAndTab(toolbox);
finish();
}
function test() {
Task.spawn(viewSource).then(finish, (aError) => {
ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
finish();
});
}

View file

@ -0,0 +1,54 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests that Toolbox#viewSourceInDebugger works when debugger is already loaded.
*/
var URL = `${URL_ROOT}doc_viewsource.html`;
var JS_URL = `${URL_ROOT}code_math.js`;
// Force the old debugger UI since it's directly used (see Bug 1301705)
Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
registerCleanupFunction(function* () {
Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
});
function* viewSource() {
let toolbox = yield openNewTabAndToolbox(URL);
let { panelWin: debuggerWin } = yield toolbox.selectTool("jsdebugger");
let debuggerEvents = debuggerWin.EVENTS;
let { DebuggerView } = debuggerWin;
let Sources = DebuggerView.Sources;
yield debuggerWin.once(debuggerEvents.SOURCE_SHOWN);
ok("A source was shown in the debugger.");
is(Sources.selectedValue, getSourceActor(Sources, JS_URL),
"The correct source is initially shown in the debugger.");
is(DebuggerView.editor.getCursor().line, 0,
"The correct line is initially highlighted in the debugger's source editor.");
yield toolbox.viewSourceInDebugger(JS_URL, 2);
let debuggerPanel = toolbox.getPanel("jsdebugger");
ok(debuggerPanel, "The debugger panel was opened.");
is(toolbox.currentToolId, "jsdebugger", "The debugger panel was selected.");
is(Sources.selectedValue, getSourceActor(Sources, JS_URL),
"The correct source is shown in the debugger.");
is(DebuggerView.editor.getCursor().line + 1, 2,
"The correct line is highlighted in the debugger's source editor.");
yield closeToolboxAndTab(toolbox);
finish();
}
function test() {
Task.spawn(viewSource).then(finish, (aError) => {
ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
finish();
});
}

View file

@ -0,0 +1,40 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests that Toolbox#viewSourceInStyleEditor works when style editor is not
* yet opened.
*/
var URL = `${URL_ROOT}doc_viewsource.html`;
var CSS_URL = `${URL_ROOT}doc_theme.css`;
function* viewSource() {
let toolbox = yield openNewTabAndToolbox(URL);
let fileFound = yield toolbox.viewSourceInStyleEditor(CSS_URL, 2);
ok(fileFound, "viewSourceInStyleEditor should resolve to true if source found.");
let stylePanel = toolbox.getPanel("styleeditor");
ok(stylePanel, "The style editor panel was opened.");
is(toolbox.currentToolId, "styleeditor", "The style editor panel was selected.");
let { UI } = stylePanel;
is(UI.selectedEditor.styleSheet.href, CSS_URL,
"The correct source is shown in the style editor.");
is(UI.selectedEditor.sourceEditor.getCursor().line + 1, 2,
"The correct line is highlighted in the style editor's source editor.");
yield closeToolboxAndTab(toolbox);
finish();
}
function test() {
Task.spawn(viewSource).then(finish, (aError) => {
ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
finish();
});
}

View file

@ -0,0 +1,39 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests that Toolbox#viewSourceInScratchpad works.
*/
var URL = `${URL_ROOT}doc_viewsource.html`;
function* viewSource() {
let toolbox = yield openNewTabAndToolbox(URL);
let win = yield openScratchpadWindow();
let { Scratchpad: scratchpad } = win;
// Brahm's Cello Sonata No.1, Op.38 now in the scratchpad
scratchpad.setText("E G B C B\nA B A G A B\nG E");
let scratchpadURL = scratchpad.uniqueName;
// Now select another tool for focus
yield toolbox.selectTool("webconsole");
yield toolbox.viewSourceInScratchpad(scratchpadURL, 2);
is(scratchpad.editor.getCursor().line, 2,
"The correct line is highlighted in scratchpad's editor.");
win.close();
yield closeToolboxAndTab(toolbox);
finish();
}
function test() {
Task.spawn(viewSource).then(finish, (aError) => {
ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
finish();
});
}

View file

@ -0,0 +1,100 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
requestLongerTimeout(10);
const TEST_URL = "data:text/html;charset=utf-8," +
"<html><head><title>Test reload</title></head>" +
"<body><h1>Testing reload from devtools</h1></body></html>";
var {Toolbox} = require("devtools/client/framework/toolbox");
const {LocalizationHelper} = require("devtools/shared/l10n");
const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
var target, toolbox, description, reloadsSent, toolIDs;
function test() {
addTab(TEST_URL).then(() => {
target = TargetFactory.forTab(gBrowser.selectedTab);
target.makeRemote().then(() => {
toolIDs = gDevTools.getToolDefinitionArray()
.filter(def => def.isTargetSupported(target))
.map(def => def.id);
gDevTools.showToolbox(target, toolIDs[0], Toolbox.HostType.BOTTOM)
.then(startReloadTest);
});
});
}
function startReloadTest(aToolbox) {
getFrameScript(); // causes frame-script-utils to be loaded into the child.
toolbox = aToolbox;
reloadsSent = 0;
let reloads = 0;
let reloadCounter = (msg) => {
reloads++;
info("Detected reload #" + reloads);
is(reloads, reloadsSent, "Reloaded from devtools window once and only for " + description + "");
};
gBrowser.selectedBrowser.messageManager.addMessageListener("devtools:test:load", reloadCounter);
testAllTheTools("docked", () => {
let origHostType = toolbox.hostType;
toolbox.switchHost(Toolbox.HostType.WINDOW).then(() => {
toolbox.win.focus();
testAllTheTools("undocked", () => {
toolbox.switchHost(origHostType).then(() => {
gBrowser.selectedBrowser.messageManager.removeMessageListener("devtools:test:load", reloadCounter);
// If we finish too early, the inspector breaks promises:
toolbox.getPanel("inspector").once("new-root", finishUp);
});
});
});
}, toolIDs.length - 1 /* only test 1 tool in docked mode, to cut down test time */);
}
function testAllTheTools(docked, callback, toolNum = 0) {
if (toolNum >= toolIDs.length) {
return callback();
}
toolbox.selectTool(toolIDs[toolNum]).then(() => {
testReload("toolbox.reload.key", docked, toolIDs[toolNum], () => {
testReload("toolbox.reload2.key", docked, toolIDs[toolNum], () => {
testReload("toolbox.forceReload.key", docked, toolIDs[toolNum], () => {
testReload("toolbox.forceReload2.key", docked, toolIDs[toolNum], () => {
testAllTheTools(docked, callback, toolNum + 1);
});
});
});
});
});
}
function testReload(shortcut, docked, toolID, callback) {
let complete = () => {
gBrowser.selectedBrowser.messageManager.removeMessageListener("devtools:test:load", complete);
return callback();
};
gBrowser.selectedBrowser.messageManager.addMessageListener("devtools:test:load", complete);
description = docked + " devtools with tool " + toolID + ", shortcut #" + shortcut;
info("Testing reload in " + description);
toolbox.win.focus();
synthesizeKeyShortcut(L10N.getStr(shortcut), toolbox.win);
reloadsSent++;
}
function finishUp() {
toolbox.destroy().then(() => {
gBrowser.removeCurrentTab();
target = toolbox = description = reloadsSent = toolIDs = null;
finish();
});
}

View file

@ -0,0 +1,84 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
var {Toolbox} = require("devtools/client/framework/toolbox");
var toolbox, toolIDs, idIndex, modifiedPrefs = [];
function test() {
addTab("about:blank").then(function () {
toolIDs = [];
for (let [id, definition] of gDevTools._tools) {
if (definition.key) {
toolIDs.push(id);
// Enable disabled tools
let pref = definition.visibilityswitch, prefValue;
try {
prefValue = Services.prefs.getBoolPref(pref);
} catch (e) {
continue;
}
if (!prefValue) {
modifiedPrefs.push(pref);
Services.prefs.setBoolPref(pref, true);
}
}
}
let target = TargetFactory.forTab(gBrowser.selectedTab);
idIndex = 0;
gDevTools.showToolbox(target, toolIDs[0], Toolbox.HostType.WINDOW)
.then(testShortcuts);
});
}
function testShortcuts(aToolbox, aIndex) {
if (aIndex === undefined) {
aIndex = 1;
} else if (aIndex == toolIDs.length) {
tidyUp();
return;
}
toolbox = aToolbox;
info("Toolbox fired a `ready` event");
toolbox.once("select", selectCB);
let key = gDevTools._tools.get(toolIDs[aIndex]).key;
let toolModifiers = gDevTools._tools.get(toolIDs[aIndex]).modifiers;
let modifiers = {
accelKey: toolModifiers.includes("accel"),
altKey: toolModifiers.includes("alt"),
shiftKey: toolModifiers.includes("shift"),
};
idIndex = aIndex;
info("Testing shortcut for tool " + aIndex + ":" + toolIDs[aIndex] +
" using key " + key);
EventUtils.synthesizeKey(key, modifiers, toolbox.win.parent);
}
function selectCB(event, id) {
info("toolbox-select event from " + id);
is(toolIDs.indexOf(id), idIndex,
"Correct tool is selected on pressing the shortcut for " + id);
testShortcuts(toolbox, idIndex + 1);
}
function tidyUp() {
toolbox.destroy().then(function () {
gBrowser.removeCurrentTab();
for (let pref of modifiedPrefs) {
Services.prefs.clearUserPref(pref);
}
toolbox = toolIDs = idIndex = modifiedPrefs = Toolbox = null;
finish();
});
}

View file

@ -0,0 +1,108 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
requestLongerTimeout(5);
var {Toolbox} = require("devtools/client/framework/toolbox");
function test() {
const URL_1 = "data:text/plain;charset=UTF-8,abcde";
const URL_2 = "data:text/plain;charset=UTF-8,12345";
const URL_3 = URL_ROOT + "browser_toolbox_window_title_changes_page.html";
const TOOL_ID_1 = "webconsole";
const TOOL_ID_2 = "jsdebugger";
const NAME_1 = "";
const NAME_2 = "";
const NAME_3 = "Toolbox test for title update";
let toolbox;
addTab(URL_1).then(function () {
let target = TargetFactory.forTab(gBrowser.selectedTab);
gDevTools.showToolbox(target, null, Toolbox.HostType.BOTTOM)
.then(function (aToolbox) { toolbox = aToolbox; })
.then(() => toolbox.selectTool(TOOL_ID_1))
// undock toolbox and check title
.then(() => {
// We have to first switch the host in order to spawn the new top level window
// on which we are going to listen from title change event
return toolbox.switchHost(Toolbox.HostType.WINDOW)
.then(() => waitForTitleChange(toolbox));
})
.then(checkTitle.bind(null, NAME_1, URL_1, "toolbox undocked"))
// switch to different tool and check title
.then(() => {
let onTitleChanged = waitForTitleChange(toolbox);
toolbox.selectTool(TOOL_ID_2);
return onTitleChanged;
})
.then(checkTitle.bind(null, NAME_1, URL_1, "tool changed"))
// navigate to different local url and check title
.then(function () {
let onTitleChanged = waitForTitleChange(toolbox);
gBrowser.loadURI(URL_2);
return onTitleChanged;
})
.then(checkTitle.bind(null, NAME_2, URL_2, "url changed"))
// navigate to a real url and check title
.then(() => {
let onTitleChanged = waitForTitleChange(toolbox);
gBrowser.loadURI(URL_3);
return onTitleChanged;
})
.then(checkTitle.bind(null, NAME_3, URL_3, "url changed"))
// destroy toolbox, create new one hosted in a window (with a
// different tool id), and check title
.then(function () {
// Give the tools a chance to handle the navigation event before
// destroying the toolbox.
executeSoon(function () {
toolbox.destroy()
.then(function () {
// After destroying the toolbox, a fresh target is required.
target = TargetFactory.forTab(gBrowser.selectedTab);
return gDevTools.showToolbox(target, null, Toolbox.HostType.WINDOW);
})
.then(function (aToolbox) { toolbox = aToolbox; })
.then(() => {
let onTitleChanged = waitForTitleChange(toolbox);
toolbox.selectTool(TOOL_ID_1);
return onTitleChanged;
})
.then(checkTitle.bind(null, NAME_3, URL_3,
"toolbox destroyed and recreated"))
// clean up
.then(() => toolbox.destroy())
.then(function () {
toolbox = null;
gBrowser.removeCurrentTab();
Services.prefs.clearUserPref("devtools.toolbox.host");
Services.prefs.clearUserPref("devtools.toolbox.selectedTool");
Services.prefs.clearUserPref("devtools.toolbox.sideEnabled");
finish();
});
});
});
});
}
function checkTitle(name, url, context) {
let win = Services.wm.getMostRecentWindow("devtools:toolbox");
let expectedTitle;
if (name) {
expectedTitle = `Developer Tools - ${name} - ${url}`;
} else {
expectedTitle = `Developer Tools - ${url}`;
}
is(win.document.title, expectedTitle, context);
}

View file

@ -0,0 +1,10 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>Toolbox test for title update</title>
<!-- Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ -->
</head>
<body></body>
</html>

View file

@ -0,0 +1,94 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* import-globals-from shared-head.js */
"use strict";
/**
* Check that the detached devtools window title is not updated when switching
* the selected frame. Also check that frames command button has 'open'
* attribute set when the list of frames is opened.
*/
var {Toolbox} = require("devtools/client/framework/toolbox");
const URL = URL_ROOT + "browser_toolbox_window_title_frame_select_page.html";
const IFRAME_URL = URL_ROOT + "browser_toolbox_window_title_changes_page.html";
add_task(function* () {
Services.prefs.setBoolPref("devtools.command-button-frames.enabled", true);
yield addTab(URL);
let target = TargetFactory.forTab(gBrowser.selectedTab);
let toolbox = yield gDevTools.showToolbox(target, null,
Toolbox.HostType.BOTTOM);
let onTitleChanged = waitForTitleChange(toolbox);
yield toolbox.selectTool("inspector");
yield onTitleChanged;
yield toolbox.switchHost(Toolbox.HostType.WINDOW);
// Wait for title change event *after* switch host, in order to listen
// for the event on the WINDOW host window, which only exists after switchHost
yield waitForTitleChange(toolbox);
is(getTitle(), `Developer Tools - Page title - ${URL}`,
"Devtools title correct after switching to detached window host");
// Wait for tick to avoid unexpected 'popuphidden' event, which
// blocks the frame popup menu opened below. See also bug 1276873
yield waitForTick();
// Open frame menu and wait till it's available on the screen.
// Also check 'open' attribute on the command button.
let btn = toolbox.doc.getElementById("command-button-frames");
ok(!btn.getAttribute("open"), "The open attribute must not be present");
let menu = toolbox.showFramesMenu({target: btn});
yield once(menu, "open");
is(btn.getAttribute("open"), "true", "The open attribute must be set");
// Verify that the frame list menu is populated
let frames = menu.items;
is(frames.length, 2, "We have both frames in the list");
let topFrameBtn = frames.filter(b => b.label == URL)[0];
let iframeBtn = frames.filter(b => b.label == IFRAME_URL)[0];
ok(topFrameBtn, "Got top level document in the list");
ok(iframeBtn, "Got iframe document in the list");
// Listen to will-navigate to check if the view is empty
let willNavigate = toolbox.target.once("will-navigate");
onTitleChanged = waitForTitleChange(toolbox);
// Only select the iframe after we are able to select an element from the top
// level document.
let newRoot = toolbox.getPanel("inspector").once("new-root");
info("Select the iframe");
iframeBtn.click();
yield willNavigate;
yield newRoot;
yield onTitleChanged;
info("Navigation to the iframe is done, the inspector should be back up");
is(getTitle(), `Developer Tools - Page title - ${URL}`,
"Devtools title was not updated after changing inspected frame");
info("Cleanup toolbox and test preferences.");
yield toolbox.destroy();
toolbox = null;
gBrowser.removeCurrentTab();
Services.prefs.clearUserPref("devtools.toolbox.host");
Services.prefs.clearUserPref("devtools.toolbox.selectedTool");
Services.prefs.clearUserPref("devtools.toolbox.sideEnabled");
Services.prefs.clearUserPref("devtools.command-button-frames.enabled");
finish();
});
function getTitle() {
return Services.wm.getMostRecentWindow("devtools:toolbox").document.title;
}

View file

@ -0,0 +1,11 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>Page title</title>
<!-- Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ -->
<iframe src="browser_toolbox_window_title_changes_page.html"></iframe>
</head>
<body></body>
</html>

View file

@ -0,0 +1,67 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
var toolbox;
const {LocalizationHelper} = require("devtools/shared/l10n");
const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
function test() {
addTab("about:blank").then(openToolbox);
}
function openToolbox() {
let target = TargetFactory.forTab(gBrowser.selectedTab);
gDevTools.showToolbox(target).then((aToolbox) => {
toolbox = aToolbox;
toolbox.selectTool("styleeditor").then(testZoom);
});
}
function testZoom() {
info("testing zoom keys");
testZoomLevel("In", 2, 1.2);
testZoomLevel("Out", 3, 0.9);
testZoomLevel("Reset", 1, 1);
tidyUp();
}
function testZoomLevel(type, times, expected) {
sendZoomKey("toolbox.zoom" + type + ".key", times);
let zoom = getCurrentZoom(toolbox);
is(zoom.toFixed(2), expected, "zoom level correct after zoom " + type);
let savedZoom = parseFloat(Services.prefs.getCharPref(
"devtools.toolbox.zoomValue"));
is(savedZoom.toFixed(2), expected,
"saved zoom level is correct after zoom " + type);
}
function sendZoomKey(shortcut, times) {
for (let i = 0; i < times; i++) {
synthesizeKeyShortcut(L10N.getStr(shortcut));
}
}
function getCurrentZoom() {
let windowUtils = toolbox.win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
return windowUtils.fullZoom;
}
function tidyUp() {
toolbox.destroy().then(function () {
gBrowser.removeCurrentTab();
toolbox = null;
finish();
});
}

View file

@ -0,0 +1,149 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Check regression when opening two tabs
*/
var { DebuggerServer } = require("devtools/server/main");
var { DebuggerClient } = require("devtools/shared/client/main");
const TAB_URL_1 = "data:text/html;charset=utf-8,foo";
const TAB_URL_2 = "data:text/html;charset=utf-8,bar";
var gClient;
var gTab1, gTab2;
var gTabActor1, gTabActor2;
function test() {
waitForExplicitFinish();
if (!DebuggerServer.initialized) {
DebuggerServer.init();
DebuggerServer.addBrowserActors();
}
openTabs();
}
function openTabs() {
// Open two tabs, select the second
addTab(TAB_URL_1).then(tab1 => {
gTab1 = tab1;
addTab(TAB_URL_2).then(tab2 => {
gTab2 = tab2;
connect();
});
});
}
function connect() {
// Connect to debugger server to fetch the two tab actors
gClient = new DebuggerClient(DebuggerServer.connectPipe());
gClient.connect()
.then(() => gClient.listTabs())
.then(response => {
// Fetch the tab actors for each tab
gTabActor1 = response.tabs.filter(a => a.url === TAB_URL_1)[0];
gTabActor2 = response.tabs.filter(a => a.url === TAB_URL_2)[0];
checkGetTab();
});
}
function checkGetTab() {
gClient.getTab({tab: gTab1})
.then(response => {
is(JSON.stringify(gTabActor1), JSON.stringify(response.tab),
"getTab returns the same tab grip for first tab");
})
.then(() => {
let filter = {};
// Filter either by tabId or outerWindowID,
// if we are running tests OOP or not.
if (gTab1.linkedBrowser.frameLoader.tabParent) {
filter.tabId = gTab1.linkedBrowser.frameLoader.tabParent.tabId;
} else {
let windowUtils = gTab1.linkedBrowser.contentWindow
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
filter.outerWindowID = windowUtils.outerWindowID;
}
return gClient.getTab(filter);
})
.then(response => {
is(JSON.stringify(gTabActor1), JSON.stringify(response.tab),
"getTab returns the same tab grip when filtering by tabId/outerWindowID");
})
.then(() => gClient.getTab({tab: gTab2}))
.then(response => {
is(JSON.stringify(gTabActor2), JSON.stringify(response.tab),
"getTab returns the same tab grip for second tab");
})
.then(checkGetTabFailures);
}
function checkGetTabFailures() {
gClient.getTab({ tabId: -999 })
.then(
response => ok(false, "getTab unexpectedly succeed with a wrong tabId"),
response => {
is(response.error, "noTab");
is(response.message, "Unable to find tab with tabId '-999'");
}
)
.then(() => gClient.getTab({ outerWindowID: -999 }))
.then(
response => ok(false, "getTab unexpectedly succeed with a wrong outerWindowID"),
response => {
is(response.error, "noTab");
is(response.message, "Unable to find tab with outerWindowID '-999'");
}
)
.then(checkSelectedTabActor);
}
function checkSelectedTabActor() {
// Send a naive request to the second tab actor
// to check if it works
gClient.request({ to: gTabActor2.consoleActor, type: "startListeners", listeners: [] }, aResponse => {
ok("startedListeners" in aResponse, "Actor from the selected tab should respond to the request.");
closeSecondTab();
});
}
function closeSecondTab() {
// Close the second tab, currently selected
let container = gBrowser.tabContainer;
container.addEventListener("TabClose", function onTabClose() {
container.removeEventListener("TabClose", onTabClose);
checkFirstTabActor();
});
gBrowser.removeTab(gTab2);
}
function checkFirstTabActor() {
// then send a request to the first tab actor
// to check if it still works
gClient.request({ to: gTabActor1.consoleActor, type: "startListeners", listeners: [] }, aResponse => {
ok("startedListeners" in aResponse, "Actor from the first tab should still respond.");
cleanup();
});
}
function cleanup() {
let container = gBrowser.tabContainer;
container.addEventListener("TabClose", function onTabClose() {
container.removeEventListener("TabClose", onTabClose);
gClient.close().then(finish);
});
gBrowser.removeTab(gTab1);
}

View file

@ -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

View file

@ -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
*/

View file

@ -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"
}

Some files were not shown because too many files have changed in this diff Show more