mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-04 23:08:39 +09:00
import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo
This commit is contained in:
commit
dcd9973243
150858 changed files with 23884658 additions and 0 deletions
291
devtools/client/framework/ToolboxProcess.jsm
Normal file
291
devtools/client/framework/ToolboxProcess.jsm
Normal 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);
|
||||
61
devtools/client/framework/about-devtools-toolbox.js
Normal file
61
devtools/client/framework/about-devtools-toolbox.js
Normal 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);
|
||||
}
|
||||
};
|
||||
115
devtools/client/framework/attach-thread.js
Normal file
115
devtools/client/framework/attach-thread.js
Normal 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 };
|
||||
390
devtools/client/framework/browser-menus.js
Normal file
390
devtools/client/framework/browser-menus.js
Normal 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);
|
||||
};
|
||||
112
devtools/client/framework/connect/connect.css
Normal file
112
devtools/client/framework/connect/connect.css
Normal 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;
|
||||
}
|
||||
236
devtools/client/framework/connect/connect.js
Normal file
236
devtools/client/framework/connect/connect.js
Normal 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));
|
||||
}
|
||||
52
devtools/client/framework/connect/connect.xhtml
Normal file
52
devtools/client/framework/connect/connect.xhtml
Normal 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>
|
||||
BIN
devtools/client/framework/dev-edition-promo/dev-edition-logo.png
Normal file
BIN
devtools/client/framework/dev-edition-promo/dev-edition-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.6 KiB |
|
|
@ -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 */
|
||||
}
|
||||
|
|
@ -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>
|
||||
758
devtools/client/framework/devtools-browser.js
Normal file
758
devtools/client/framework/devtools-browser.js
Normal 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();
|
||||
});
|
||||
534
devtools/client/framework/devtools.js
Normal file
534
devtools/client/framework/devtools.js
Normal 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();
|
||||
});
|
||||
162
devtools/client/framework/gDevTools.jsm
Normal file
162
devtools/client/framework/gDevTools.jsm
Normal 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);
|
||||
};
|
||||
});
|
||||
103
devtools/client/framework/location-store.js
Normal file
103
devtools/client/framework/location-store.js
Normal 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 };
|
||||
};
|
||||
65
devtools/client/framework/menu-item.js
Normal file
65
devtools/client/framework/menu-item.js
Normal 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;
|
||||
173
devtools/client/framework/menu.js
Normal file
173
devtools/client/framework/menu.js
Normal 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;
|
||||
33
devtools/client/framework/moz.build
Normal file
33
devtools/client/framework/moz.build
Normal 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',
|
||||
)
|
||||
107
devtools/client/framework/options-panel.css
Normal file
107
devtools/client/framework/options-panel.css
Normal 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;
|
||||
}
|
||||
247
devtools/client/framework/selection.js
Normal file
247
devtools/client/framework/selection.js
Normal 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;
|
||||
},
|
||||
};
|
||||
592
devtools/client/framework/sidebar.js
Normal file
592
devtools/client/framework/sidebar.js
Normal 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;
|
||||
})
|
||||
};
|
||||
209
devtools/client/framework/source-map-service.js
Normal file
209
devtools/client/framework/source-map-service.js
Normal 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;
|
||||
}
|
||||
20
devtools/client/framework/source-map-util.js
Normal file
20
devtools/client/framework/source-map-util.js
Normal 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
|
||||
};
|
||||
220
devtools/client/framework/source-map-worker.js
Normal file
220
devtools/client/framework/source-map-worker.js
Normal 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 });
|
||||
}
|
||||
};
|
||||
84
devtools/client/framework/source-map.js
Normal file
84
devtools/client/framework/source-map.js
Normal 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
|
||||
};
|
||||
120
devtools/client/framework/target-from-url.js
Normal file
120
devtools/client/framework/target-from-url.js
Normal 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);
|
||||
}
|
||||
825
devtools/client/framework/target.js
Normal file
825
devtools/client/framework/target.js
Normal 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();
|
||||
}
|
||||
};
|
||||
6
devtools/client/framework/test/.eslintrc.js
Normal file
6
devtools/client/framework/test/.eslintrc.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
// Extend from the shared list of defined globals for mochitests.
|
||||
"extends": "../../../.eslintrc.mochitests.js"
|
||||
};
|
||||
95
devtools/client/framework/test/browser.ini
Normal file
95
devtools/client/framework/test/browser.ini
Normal 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]
|
||||
65
devtools/client/framework/test/browser_browser_toolbox.js
Normal file
65
devtools/client/framework/test/browser_browser_toolbox.js
Normal 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");
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
264
devtools/client/framework/test/browser_devtools_api.js
Normal file
264
devtools/client/framework/test/browser_devtools_api.js
Normal 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);
|
||||
},
|
||||
};
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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;
|
||||
});
|
||||
115
devtools/client/framework/test/browser_keybindings_01.js
Normal file
115
devtools/client/framework/test/browser_keybindings_01.js
Normal 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();
|
||||
}
|
||||
}
|
||||
65
devtools/client/framework/test/browser_keybindings_02.js
Normal file
65
devtools/client/framework/test/browser_keybindings_02.js
Normal 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");
|
||||
}
|
||||
53
devtools/client/framework/test/browser_keybindings_03.js
Normal file
53
devtools/client/framework/test/browser_keybindings_03.js
Normal 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();
|
||||
});
|
||||
181
devtools/client/framework/test/browser_menu_api.js
Normal file
181
devtools/client/framework/test/browser_menu_api.js
Normal 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");
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
115
devtools/client/framework/test/browser_source_map-01.js
Normal file
115
devtools/client/framework/test/browser_source_map-01.js
Normal 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;
|
||||
}
|
||||
113
devtools/client/framework/test/browser_source_map-02.js
Normal file
113
devtools/client/framework/test/browser_source_map-02.js
Normal 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;
|
||||
}
|
||||
56
devtools/client/framework/test/browser_target_events.js
Normal file
56
devtools/client/framework/test/browser_target_events.js
Normal 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();
|
||||
}
|
||||
133
devtools/client/framework/test/browser_target_from_url.js
Normal file
133
devtools/client/framework/test/browser_target_from_url.js
Normal 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);
|
||||
}
|
||||
25
devtools/client/framework/test/browser_target_remote.js
Normal file
25
devtools/client/framework/test/browser_target_remote.js
Normal 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();
|
||||
});
|
||||
});
|
||||
}
|
||||
74
devtools/client/framework/test/browser_target_support.js
Normal file
74
devtools/client/framework/test/browser_target_support.js
Normal 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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
81
devtools/client/framework/test/browser_toolbox_highlight.js
Normal file
81
devtools/client/framework/test/browser_toolbox_highlight.js
Normal 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");
|
||||
}
|
||||
139
devtools/client/framework/test/browser_toolbox_hosts.js
Normal file
139
devtools/client/framework/test/browser_toolbox_hosts.js
Normal 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");
|
||||
}
|
||||
69
devtools/client/framework/test/browser_toolbox_hosts_size.js
Normal file
69
devtools/client/framework/test/browser_toolbox_hosts_size.js
Normal 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();
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
|
|
@ -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");
|
||||
});
|
||||
106
devtools/client/framework/test/browser_toolbox_minimize.js
Normal file
106
devtools/client/framework/test/browser_toolbox_minimize.js
Normal 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;
|
||||
}
|
||||
297
devtools/client/framework/test/browser_toolbox_options.js
Normal file
297
devtools/client/framework/test/browser_toolbox_options.js
Normal 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;
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
81
devtools/client/framework/test/browser_toolbox_races.js
Normal file
81
devtools/client/framework/test/browser_toolbox_races.js
Normal 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", {});
|
||||
}
|
||||
78
devtools/client/framework/test/browser_toolbox_raise.js
Normal file
78
devtools/client/framework/test/browser_toolbox_raise.js
Normal 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();
|
||||
});
|
||||
}
|
||||
21
devtools/client/framework/test/browser_toolbox_ready.js
Normal file
21
devtools/client/framework/test/browser_toolbox_ready.js
Normal 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();
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
101
devtools/client/framework/test/browser_toolbox_select_event.js
Normal file
101
devtools/client/framework/test/browser_toolbox_select_event.js
Normal 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();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -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();
|
||||
});
|
||||
181
devtools/client/framework/test/browser_toolbox_sidebar.js
Normal file
181
devtools/client/framework/test/browser_toolbox_sidebar.js
Normal 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();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
60
devtools/client/framework/test/browser_toolbox_target.js
Normal file
60
devtools/client/framework/test/browser_toolbox_target.js
Normal 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();
|
||||
});
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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;
|
||||
});
|
||||
108
devtools/client/framework/test/browser_toolbox_toggle.js
Normal file
108
devtools/client/framework/test/browser_toolbox_toggle.js
Normal 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();
|
||||
}
|
||||
51
devtools/client/framework/test/browser_toolbox_tool_ready.js
Normal file
51
devtools/client/framework/test/browser_toolbox_tool_ready.js
Normal 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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
67
devtools/client/framework/test/browser_toolbox_zoom.js
Normal file
67
devtools/client/framework/test/browser_toolbox_zoom.js
Normal 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();
|
||||
});
|
||||
}
|
||||
149
devtools/client/framework/test/browser_two_tabs.js
Normal file
149
devtools/client/framework/test/browser_two_tabs.js
Normal 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);
|
||||
}
|
||||
18
devtools/client/framework/test/code_binary_search.coffee
Normal file
18
devtools/client/framework/test/code_binary_search.coffee
Normal 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
|
||||
29
devtools/client/framework/test/code_binary_search.js
Normal file
29
devtools/client/framework/test/code_binary_search.js
Normal 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
|
||||
*/
|
||||
10
devtools/client/framework/test/code_binary_search.map
Normal file
10
devtools/client/framework/test/code_binary_search.map
Normal 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
Loading…
Add table
Add a link
Reference in a new issue