Bug 1251033 - Part 4 - Integrate Frame component in webconsole. r=linclark

This commit is contained in:
Jordan Santell 2016-03-08 23:43:24 -08:00
parent aa9ca8ef90
commit f636eb87f1
13 changed files with 140 additions and 132 deletions

View File

@ -102,14 +102,8 @@ a {
text-decoration: underline;
}
.message-location > .filename {
text-overflow: ellipsis;
text-align: end;
overflow: hidden;
}
.message-location > .line-number {
flex: none;
.message-location > .frame-link {
width: 10em;
}
.message-flex-body {

View File

@ -3667,8 +3667,7 @@ Widgets.Stacktrace.prototype = Heritage.extend(Widgets.BaseWidget.prototype,
}
let location = this.output.owner.createLocationNode({url: frame.filename,
line: frame.lineNumber},
"jsdebugger");
line: frame.lineNumber});
// .devtools-monospace sets font-size to 80%, however .body already has
// .devtools-monospace. If we keep it here, the location would be rendered

View File

@ -57,12 +57,12 @@ function test() {
let msg = [...result.matched][0];
ok(msg, "message element found");
let locationNode = msg.querySelector(".message > .message-location");
let locationNode = msg.querySelector(".message > .message-location > .frame-link");
ok(locationNode, "message location element found");
let title = locationNode.getAttribute("title");
info("location node title: " + title);
isnot(title.indexOf(" -> "), -1, "error comes from a subscript");
let url = locationNode.getAttribute("data-url");
info("location node url: " + url);
ok(url.indexOf("resource://") === 0, "error comes from a subscript");
let viewSource = browserconsole.viewSource;
let URL = null;

View File

@ -61,7 +61,7 @@ function test() {
let msg = [...results[0].matched][0];
ok(msg, "message element found for: " + result.text);
let locationNode = msg.querySelector(".message > .message-location");
let locationNode = msg.querySelector(".message > .message-location .frame-link-filename");
ok(locationNode, "message location element found");
EventUtils.synthesizeMouse(locationNode, 2, 2, {}, hud.iframeWindow);

View File

@ -54,15 +54,17 @@ function consoleOpened(aHud) {
.getControllerForCommand("cmd_copy");
is(controller.isCommandEnabled("cmd_copy"), true, "cmd_copy is enabled");
// Remove new lines since getSelection() includes one between message and
// line number, but the clipboard doesn't (see bug 1119503)
// Remove new lines and whitespace since getSelection() includes
// a new line between message and line number, but the clipboard doesn't
// @see bug 1119503
let selection = (HUD.iframeWindow.getSelection() + "")
.replace(/\r?\n|\r/g, " ");
.replace(/\r?\n|\r| /g, "");
isnot(selection.indexOf("bug587617"), -1,
"selection text includes 'bug587617'");
waitForClipboard((str) => {
return selection.trim() == str.trim();
// Strip out spaces for comparison ease
return selection.trim() == str.trim().replace(/ /g, "");
}, () => {
goDoCommand("cmd_copy");
}, deferred.resolve, deferred.resolve);
@ -82,15 +84,17 @@ function testContextMenuCopy() {
let copyItem = contextMenu.querySelector("*[command='cmd_copy']");
ok(copyItem, "the context menu on the output node has a \"Copy\" item");
// Remove new lines since getSelection() includes one between message and line
// number, but the clipboard doesn't (see bug 1119503)
// Remove new lines and whitespace since getSelection() includes
// a new line between message and line number, but the clipboard doesn't
// @see bug 1119503
let selection = (HUD.iframeWindow.getSelection() + "")
.replace(/\r?\n|\r/g, " ");
.replace(/\r?\n|\r| /g, "");
copyItem.doCommand();
waitForClipboard((str) => {
return selection.trim() == str.trim();
// Strip out spaces for comparison ease
return selection.trim() == str.trim().replace(/ /g, "");
}, () => {
goDoCommand("cmd_copy");
}, deferred.resolve, deferred.resolve);

View File

@ -68,14 +68,15 @@ function performTest(HUD, [result]) {
.getControllerForCommand("cmd_copy");
is(controller.isCommandEnabled("cmd_copy"), true, "cmd_copy is enabled");
// Remove new lines since getSelection() includes one between message and line
// number, but the clipboard doesn't (see bug 1119503)
// Remove new lines and whitespace since getSelection() includes
// a new line between message and line number, but the clipboard doesn't
// @see bug 1119503
let selectionText = (HUD.iframeWindow.getSelection() + "")
.replace(/\r?\n|\r/g, " ");
.replace(/\r?\n|\r| /g, "");
isnot(selectionText.indexOf("foobarBazBug613280"), -1,
"selection text includes 'foobarBazBug613280'");
waitForClipboard((str) => {
return str.trim() == selectionText.trim();
return selectionText.trim() === str.trim().replace(/ /g, "");
}, clipboardSetup, clipboardCopyDone, clipboardCopyDone);
}

View File

@ -43,8 +43,8 @@ function test() {
let exceptionMsg = [...exceptionRule.matched][0];
let consoleMsg = [...consoleRule.matched][0];
let nodes = [exceptionMsg.querySelector(".message-location"),
consoleMsg.querySelector(".message-location")];
let nodes = [exceptionMsg.querySelector(".message-location > .frame-link"),
consoleMsg.querySelector(".message-location > .frame-link")];
ok(nodes[0], ".location node for the exception message");
ok(nodes[1], ".location node for the console message");
@ -60,14 +60,14 @@ function test() {
function* checkClickOnNode(index, node) {
info("checking click on node index " + index);
let url = node.getAttribute("title");
let url = node.getAttribute("data-url");
ok(url, "source url found for index " + index);
let line = node.sourceLine;
let line = node.getAttribute("data-line");
ok(line, "found source line for index " + index);
executeSoon(() => {
EventUtils.sendMouseEvent({ type: "click" }, node);
EventUtils.sendMouseEvent({ type: "click" }, node.querySelector(".frame-link-filename"));
});
yield hud.ui.once("source-in-debugger-opened");

View File

@ -39,10 +39,10 @@ function testViewSource() {
}).then(([error1Rule, error2Rule]) => {
let error1Msg = [...error1Rule.matched][0];
let error2Msg = [...error2Rule.matched][0];
nodes = [error1Msg.querySelector(".message-location"),
error2Msg.querySelector(".message-location")];
ok(nodes[0], ".message-location node for the first error");
ok(nodes[1], ".message-location node for the second error");
nodes = [error1Msg.querySelector(".message-location .frame-link"),
error2Msg.querySelector(".message-location .frame-link")];
ok(nodes[0], ".frame-link node for the first error");
ok(nodes[1], ".frame-link node for the second error");
let target = TargetFactory.forTab(gBrowser.selectedTab);
let toolbox = gDevTools.getToolbox(target);
@ -51,7 +51,7 @@ function testViewSource() {
deferred.resolve(panel);
});
EventUtils.sendMouseEvent({ type: "click" }, nodes[0]);
EventUtils.sendMouseEvent({ type: "click" }, nodes[0].querySelector(".frame-link-filename"));
});
return deferred.promise;
@ -71,14 +71,14 @@ function onStyleEditorReady(panel) {
checkStyleEditorForSheetAndLine(href, line - 1).then(deferred.resolve);
});
EventUtils.sendMouseEvent({ type: "click" }, nodes[1]);
EventUtils.sendMouseEvent({ type: "click" }, nodes[1].querySelector(".frame-link-filename"));
}
waitForFocus(function() {
info("style editor window focused");
let href = nodes[0].getAttribute("title");
let line = nodes[0].sourceLine;
let href = nodes[0].getAttribute("data-url");
let line = nodes[0].getAttribute("data-line");
ok(line, "found source line");
checkStyleEditorForSheetAndLine(href, line - 1).then(function() {
@ -87,8 +87,8 @@ function onStyleEditorReady(panel) {
let target = TargetFactory.forTab(gBrowser.selectedTab);
let toolbox = gDevTools.getToolbox(target);
href = nodes[1].getAttribute("title");
line = nodes[1].sourceLine;
href = nodes[1].getAttribute("data-url");
line = nodes[1].getAttribute("data-line");
ok(line, "found source line");
toolbox.selectTool("webconsole").then(function() {

View File

@ -58,7 +58,7 @@ add_task(function*() {
let [matched] = [...messages[0].matched];
ok(matched, "Found logged message from Scratchpad");
let anchor = matched.querySelector("a.message-location");
let anchor = matched.querySelector(".message-location .frame-link-filename");
toolbox.on("scratchpad-selected", function selected() {
toolbox.off("scratchpad-selected", selected);

View File

@ -11,10 +11,6 @@
const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
"test/test-error.html";
var getItemForAttachment;
var Sources;
var getItemInvoked = false;
add_task(function*() {
yield loadTab(TEST_URI);
let hud = yield openConsole(null);
@ -30,12 +26,6 @@ add_task(function*() {
}
EventUtils.sendMouseEvent({ type: "click" }, button, content);
let { panelWin: { DebuggerView } } = yield openDebugger();
info("debugger opened");
Sources = DebuggerView.Sources;
hud = yield openConsole();
info("console opened again");
let [result] = yield waitForMessages({
webconsole: hud,
messages: [{
@ -47,25 +37,15 @@ add_task(function*() {
let msg = [...result.matched][0];
ok(msg, "error message");
let locationNode = msg.querySelector(".message-location");
let locationNode = msg.querySelector(".message-location .frame-link-filename");
ok(locationNode, "location node");
let onTabOpen = waitForTab();
getItemForAttachment = Sources.getItemForAttachment;
Sources.getItemForAttachment = () => {
getItemInvoked = true;
return false;
};
EventUtils.sendMouseEvent({ type: "click" }, locationNode);
let tab = yield onTabOpen;
ok(true, "the view source tab was opened in response to clicking " +
"the location node");
gBrowser.removeTab(tab);
ok(getItemInvoked, "custom getItemForAttachment() was invoked");
Sources.getItemForAttachment = getItemForAttachment;
Sources = getItemForAttachment = null;
});

View File

@ -1070,16 +1070,16 @@ function waitForMessages(options) {
}
function checkSource(rule, element) {
let location = element.querySelector(".message-location");
let location = getRenderedSource(element);
if (!location) {
return false;
}
if (!checkText(rule.source.url, location.getAttribute("title"))) {
if (!checkText(rule.source.url, location.url)) {
return false;
}
if ("line" in rule.source && location.sourceLine != rule.source.line) {
if ("line" in rule.source && location.line === rule.source.line) {
return false;
}
@ -1111,10 +1111,10 @@ function waitForMessages(options) {
}
if (expected.file) {
let file = frame.querySelector(".message-location").title;
if (!checkText(expected.file, file)) {
let url = getRenderedSource(frame).url;
if (!checkText(expected.file, url)) {
ok(false, "frame #" + i + " does not match file name: " +
expected.file + " != " + file);
expected.file + " != " + url);
displayErrorContext(rule, element);
return false;
}
@ -1131,7 +1131,7 @@ function waitForMessages(options) {
}
if (expected.line) {
let line = frame.querySelector(".message-location").sourceLine;
let line = getRenderedSource(frame).line;
if (!checkText(expected.line, line)) {
ok(false, "frame #" + i + " does not match the line number: " +
expected.line + " != " + line);
@ -1317,9 +1317,9 @@ function waitForMessages(options) {
function onMessagesAdded(event, newMessages) {
for (let msg of newMessages) {
let elem = msg.node;
let location = elem.querySelector(".message-location");
if (location) {
let url = location.title;
let location = getRenderedSource(elem);
if (location && location.url) {
let url = location.url;
// Prevent recursion with the browser console and any potential
// messages coming from head.js.
if (url.indexOf("devtools/client/webconsole/test/head.js") != -1) {
@ -1753,3 +1753,12 @@ function simulateMessageLinkClick(element, expectedLink) {
return deferred.promise;
}
function getRenderedSource (root) {
let location = root.querySelector(".message-location .frame-link");
return location ? {
url: location.getAttribute("data-url"),
line: location.getAttribute("data-line"),
column: location.getAttribute("data-column"),
} : null;
}

View File

@ -11,6 +11,9 @@ const {Cc, Ci, Cu} = require("chrome");
const {Utils: WebConsoleUtils, CONSOLE_WORKER_IDS} =
require("devtools/shared/webconsole/utils");
const { getSourceNames } = require("devtools/client/shared/source-utils");
const BrowserLoaderModule = {};
Cu.import("resource://devtools/client/shared/browser-loader.js", BrowserLoaderModule);
const promise = require("promise");
const Debugger = require("Debugger");
const Services = require("Services");
@ -219,6 +222,7 @@ function WebConsoleFrame(webConsoleOwner) {
this.output = new ConsoleOutput(this);
this.unmountMessage = this.unmountMessage.bind(this);
this._toggleFilter = this._toggleFilter.bind(this);
this.resize = this.resize.bind(this);
this._onPanelSelected = this._onPanelSelected.bind(this);
@ -229,6 +233,15 @@ function WebConsoleFrame(webConsoleOwner) {
this._outputTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
this._outputTimerInitialized = false;
let require = BrowserLoaderModule.BrowserLoader({
window: this.window,
useOnlyShared: true
}).require;
this.React = require("devtools/client/shared/vendor/react");
this.ReactDOM = require("devtools/client/shared/vendor/react-dom");
this.FrameView = this.React.createFactory(require("devtools/client/shared/components/frame"));
EventEmitter.decorate(this);
}
exports.WebConsoleFrame = WebConsoleFrame;
@ -1162,6 +1175,10 @@ WebConsoleFrame.prototype = {
if (dupeNode) {
this.mergeFilteredMessageNode(dupeNode);
// Even though this node was never rendered, we create the location
// nodes before rendering, so we still have to clean up any
// React components
this.unmountMessage(node);
return dupeNode;
}
@ -2270,6 +2287,22 @@ WebConsoleFrame.prototype = {
}
},
/**
* Cleans up a message via a node that may or may not
* have actually been rendered in the DOM. Currently, only
* cleans up React components.
*
* @param nsIDOMNode node
* The message node you want to clean up.
*/
unmountMessage(node) {
// Select all `.message-location` within this node to ensure we get
// messages of stacktraces, which contain multiple location nodes.
for (let locationNode of node.querySelectorAll(".message-location")) {
this.ReactDOM.unmountComponentAtNode(locationNode);
}
},
/**
* Ensures that the number of message nodes of type category don't exceed that
* category's line limit by removing old messages as needed.
@ -2324,6 +2357,8 @@ WebConsoleFrame.prototype = {
node._variablesView = null;
}
this.unmountMessage(node);
node.remove();
},
@ -2478,80 +2513,56 @@ WebConsoleFrame.prototype = {
* Creates the anchor that displays the textual location of an incoming
* message.
*
* @param object aLocation
* @param {Object} aLocation
* An object containing url, line and column number of the message
* source (destructured).
* @param string target [optional]
* Tells which tool to open the link with, on click. Supported tools:
* jsdebugger, styleeditor, scratchpad.
* @return nsIDOMNode
* @return {Element}
* The new anchor element, ready to be added to the message node.
*/
createLocationNode: function({url, line, column}, target) {
createLocationNode: function({url, line, column}) {
if (!url) {
url = "";
}
let fullURL = url.split(" -> ").pop();
let locationNode = this.document.createElementNS(XHTML_NS, "a");
let filenameNode = this.document.createElementNS(XHTML_NS, "span");
// Create the text, which consists of an abbreviated version of the URL
// Scratchpad URLs should not be abbreviated.
let filename;
let fullURL;
let isScratchpad = false;
if (/^Scratchpad\/\d+$/.test(url)) {
filename = url;
fullURL = url;
isScratchpad = true;
} else {
fullURL = url.split(" -> ").pop();
filename = getSourceNames(fullURL).short;
}
filenameNode.className = "filename";
filenameNode.textContent = ` ${filename}`;
locationNode.appendChild(filenameNode);
locationNode.href = isScratchpad || !fullURL ? "#" : fullURL;
locationNode.draggable = false;
if (target) {
locationNode.target = target;
}
locationNode.setAttribute("title", url);
locationNode.className = "message-location theme-link devtools-monospace";
locationNode.className = "message-location devtools-monospace";
// Make the location clickable.
let onClick = () => {
let nodeTarget = locationNode.target;
if (nodeTarget == "scratchpad" || isScratchpad) {
let category = locationNode.parentNode.category;
let target = category === CATEGORY_CSS ? "styleeditor" :
category === CATEGORY_JS ? "jsdebugger" :
category === CATEGORY_WEBDEV ? "jsdebugger" :
/^Scratchpad\/\d+$/.test(url) ? "scratchpad" :
// If it ends in .js, let's attempt to open in debugger
// anyway, as this falls back to normal view-source.
/\.js$/.test(fullURL) ? "jsdebugger" : null;
switch (target) {
case "scratchpad":
this.owner.viewSourceInScratchpad(url, line);
return;
}
let category = locationNode.parentNode.category;
if (nodeTarget == "styleeditor" || category == CATEGORY_CSS) {
this.owner.viewSourceInStyleEditor(fullURL, line);
} else if (nodeTarget == "jsdebugger" ||
category == CATEGORY_JS || category == CATEGORY_WEBDEV) {
case "jsdebugger":
this.owner.viewSourceInDebugger(fullURL, line);
} else {
this.owner.viewSource(fullURL, line);
return;
case "styleeditor":
this.owner.viewSourceInStyleEditor(fullURL, line);
return;
}
// No matching tool found; use old school view-source
this.owner.viewSource(fullURL, line);
};
if (fullURL) {
this._addMessageLinkCallback(locationNode, onClick);
}
if (line) {
let lineNumberNode = this.document.createElementNS(XHTML_NS, "span");
lineNumberNode.className = "line-number";
lineNumberNode.textContent =
":" + line + (column >= 0 ? ":" + column : "");
locationNode.appendChild(lineNumberNode);
locationNode.sourceLine = line;
}
this.ReactDOM.render(this.FrameView({
frame: {
source: fullURL,
line,
column,
},
onClick
}), locationNode);
return locationNode;
},
@ -2788,6 +2799,12 @@ WebConsoleFrame.prototype = {
this._pruneCategoriesQueue = {};
this.webConsoleClient.clearNetworkRequests();
// Unmount any currently living frame components in DOM, since
// currently we only clean up messages in `this.removeOutputMessage`,
// via `this.pruneOutputIfNecessary`.
let liveMessages = this.outputNode.querySelectorAll(".message");
Array.prototype.forEach.call(liveMessages, this.unmountMessage);
if (this._outputTimerInitialized) {
this._outputTimerInitialized = false;
this._outputTimer.cancel();
@ -2802,6 +2819,8 @@ WebConsoleFrame.prototype = {
this.output.destroy();
this.output = null;
this.React = this.ReactDOM = this.FrameView = null;
if (this._contextMenuHandler) {
this._contextMenuHandler.destroy();
this._contextMenuHandler = null;

View File

@ -11,6 +11,8 @@
type="text/css"?>
<?xml-stylesheet href="chrome://devtools/skin/webconsole.css"
type="text/css"?>
<?xml-stylesheet href="chrome://devtools/skin/components-frame.css"
type="text/css"?>
<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
id="devtools-webconsole"