Bug 1005909 - Make URLs in console strings clickable. r=rcampbell

This commit is contained in:
Connor Brem 2014-06-05 16:12:00 -04:00
parent 6cd5bee13d
commit 9dddfe29e9
6 changed files with 285 additions and 23 deletions

View File

@ -13,6 +13,7 @@ loader.lazyImporter(this, "gDevTools", "resource:///modules/devtools/gDevTools.j
loader.lazyImporter(this, "Task","resource://gre/modules/Task.jsm");
const Heritage = require("sdk/core/heritage");
const URI = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
const XHTML_NS = "http://www.w3.org/1999/xhtml";
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties";
@ -1081,6 +1082,11 @@ Messages.Extended.prototype = Heritage.extend(Messages.Simple.prototype,
let result = this.document.createElementNS(XHTML_NS, "span");
if (isPrimitive) {
if (Widgets.URLString.prototype.containsURL.call(Widgets.URLString.prototype, grip)) {
let widget = new Widgets.URLString(this, grip, options).render();
return widget.element;
}
let className = this.getClassNameForValueGrip(grip);
if (className) {
result.className = className;
@ -1757,6 +1763,125 @@ Widgets.MessageTimestamp.prototype = Heritage.extend(Widgets.BaseWidget.prototyp
}); // Widgets.MessageTimestamp.prototype
/**
* The URLString widget, for rendering strings where at least one token is a
* URL.
*
* @constructor
* @param object message
* The owning message.
* @param string str
* The string, which contains at least one valid URL.
*/
Widgets.URLString = function(message, str)
{
Widgets.BaseWidget.call(this, message);
this.str = str;
};
Widgets.URLString.prototype = Heritage.extend(Widgets.BaseWidget.prototype,
{
/**
* The string to format, which contains at least one valid URL.
* @type string
*/
str: "",
render: function()
{
if (this.element) {
return this;
}
// The rendered URLString will be a <span> containing a number of text
// <spans> for non-URL tokens and <a>'s for URL tokens.
this.element = this.el("span", {
class: "console-string"
});
this.element.appendChild(this._renderText("\""));
// As we walk through the tokens of the source string, we make sure to preserve
// the original whitespace that seperated the tokens.
let tokens = this.str.split(/\s+/);
let textStart = 0;
let tokenStart;
for (let token of tokens) {
tokenStart = this.str.indexOf(token, textStart);
if (this._isURL(token)) {
this.element.appendChild(this._renderText(this.str.slice(textStart, tokenStart)));
textStart = tokenStart + token.length;
this.element.appendChild(this._renderURL(token));
}
}
// Clean up any non-URL text at the end of the source string.
this.element.appendChild(this._renderText(this.str.slice(textStart, this.str.length)));
this.element.appendChild(this._renderText("\""));
return this;
},
/**
* Determines whether a grip is a string containing a URL.
*
* @param string grip
* The grip, which may contain a URL.
* @return boolean
* Whether the grip is a string containing a URL.
*/
containsURL: function(grip)
{
if (typeof grip != "string") {
return false;
}
let tokens = grip.split(/\s+/);
return tokens.some(this._isURL);
},
/**
* Determines whether a string token is a valid URL.
*
* @param string token
* The token.
* @return boolean
* Whenther the token is a URL.
*/
_isURL: function(token) {
try {
let uri = URI.newURI(token, null, null);
let url = uri.QueryInterface(Ci.nsIURL);
return true;
} catch (e) {
return false;
}
},
/**
* Renders a string as a URL.
*
* @param string url
* The string to be rendered as a url.
* @return DOMElement
* An element containing the rendered string.
*/
_renderURL: function(url)
{
let result = this.el("a", {
class: "url",
title: url,
href: url,
draggable: false
}, url);
this.message._addLinkCallback(result);
return result;
},
_renderText: function(text) {
return this.el("span", text);
},
}); // Widgets.URLString.prototype
/**
* Widget used for displaying ObjectActors that have no specialised renderers.
*

View File

@ -245,6 +245,7 @@ run-if = os == "mac"
[browser_webconsole_cached_autocomplete.js]
[browser_webconsole_change_font_size.js]
[browser_webconsole_chrome.js]
[browser_webconsole_clickable_urls.js]
[browser_webconsole_closure_inspection.js]
[browser_webconsole_completion.js]
[browser_webconsole_console_extras.js]

View File

@ -0,0 +1,83 @@
/*
* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
// When strings containing URLs are entered into the webconsole,
// check its output and ensure that the output can be clicked to open those URLs.
const TEST_URI = "data:text/html;charset=utf8,Bug 1005909 - Clickable URLS";
let inputTests = [
// 0: URL opens page when clicked.
{
input: "'http://example.com'",
output: "http://example.com",
expectedTab: "http://example.com/",
},
// 1: URL opens page using https when clicked.
{
input: "'https://example.com'",
output: "https://example.com",
expectedTab: "https://example.com/",
},
// 2: URL with port opens page when clicked.
{
input: "'https://example.com:443'",
output: "https://example.com:443",
expectedTab: "https://example.com/",
},
// 3: URL containing non-empty path opens page when clicked.
{
input: "'http://example.com/foo'",
output: "http://example.com/foo",
expectedTab: "http://example.com/foo",
},
// 4: URL opens page when clicked, even when surrounded by non-URL tokens.
{
input: "'foo http://example.com bar'",
output: "foo http://example.com bar",
expectedTab: "http://example.com/",
},
// 5: URL opens page when clicked, and whitespace is be preserved.
{
input: "'foo\\nhttp://example.com\\nbar'",
output: "foo\nhttp://example.com\nbar",
expectedTab: "http://example.com/",
},
// 6: URL opens page when clicked when multiple links are present.
{
input: "'http://example.com http://example.com'",
output: "http://example.com http://example.com",
expectedTab: "http://example.com/",
},
// 7: URL without scheme does not open page when clicked.
{
input: "'example.com'",
output: "example.com",
},
// 8: URL with invalid scheme does not open page when clicked.
{
input: "'foo://example.com'",
output: "foo://example.com",
},
];
function test() {
Task.spawn(function*() {
let {tab} = yield loadTab(TEST_URI);
let hud = yield openConsole(tab);
yield checkOutputForInputs(hud, inputTests);
inputTests = null;
}).then(finishTest);
}

View File

@ -8,6 +8,7 @@
const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console-output-03.html";
let inputTests = [
// 0
{
input: "document",
@ -57,6 +58,7 @@ let inputTests = [
{
input: "window.location.href",
output: '"' + TEST_URI + '"',
noClick: true,
},
// 6

View File

@ -110,11 +110,10 @@ let inputTests = [
];
function test() {
addTab(TEST_URI);
browser.addEventListener("load", function onLoad() {
browser.removeEventListener("load", onLoad, true);
openConsole().then((hud) => {
return checkOutputForInputs(hud, inputTests);
}).then(finishTest);
}, true);
Task.spawn(function*() {
const {tab} = yield loadTab(TEST_URI);
const hud = yield openConsole(tab);
yield checkOutputForInputs(hud, inputTests);
inputTests = null;
}).then(finishTest);
}

View File

@ -84,6 +84,32 @@ function loadTab(url) {
return deferred.promise;
}
function loadBrowser(browser) {
let deferred = promise.defer();
browser.addEventListener("load", function onLoad() {
browser.removeEventListener("load", onLoad, true);
deferred.resolve(null)
}, true);
return deferred.promise;
}
function closeTab(tab) {
let deferred = promise.defer();
let container = gBrowser.tabContainer;
container.addEventListener("TabClose", function onTabClose() {
container.removeEventListener("TabClose", onTabClose, true);
deferred.resolve(null);
}, true);
gBrowser.removeTab(tab);
return deferred.promise;
}
function afterAllTabsLoaded(callback, win) {
win = win || window;
@ -1382,10 +1408,14 @@ function whenDelayedStartupFinished(aWindow, aCallback)
* - inspectorIcon: boolean, when true, the test runner expects the
* result widget to contain an inspectorIcon element (className
* open-inspector).
*
* - expectedTab: string, optional, the full URL of the new tab which must
* open. If this is not provided, any new tabs that open will cause a test
* failure.
*/
function checkOutputForInputs(hud, inputTests)
{
let eventHandlers = new Set();
let container = gBrowser.tabContainer;
function* runner()
{
@ -1393,10 +1423,7 @@ function checkOutputForInputs(hud, inputTests)
info("checkInput(" + i + "): " + entry.input);
yield checkInput(entry);
}
for (let fn of eventHandlers) {
hud.jsterm.off("variablesview-open", fn);
}
container = null;
}
function* checkInput(entry)
@ -1467,27 +1494,39 @@ function checkOutputForInputs(hud, inputTests)
}
}
function checkObjectClick(entry, msg)
function* checkObjectClick(entry, msg)
{
let body = msg.querySelector(".message-body a") ||
msg.querySelector(".message-body");
ok(body, "the message body");
let deferred = promise.defer();
entry._onVariablesViewOpen = onVariablesViewOpen.bind(null, entry, deferred);
let deferredVariablesView = promise.defer();
entry._onVariablesViewOpen = onVariablesViewOpen.bind(null, entry, deferredVariablesView);
hud.jsterm.on("variablesview-open", entry._onVariablesViewOpen);
eventHandlers.add(entry._onVariablesViewOpen);
let deferredTab = promise.defer();
entry._onTabOpen = onTabOpen.bind(null, entry, deferredTab);
container.addEventListener("TabOpen", entry._onTabOpen, true);
body.scrollIntoView();
EventUtils.synthesizeMouse(body, 2, 2, {}, hud.iframeWindow);
if (entry.inspectable) {
info("message body tagName '" + body.tagName + "' className '" + body.className + "'");
return deferred.promise; // wait for the panel to open if we need to.
yield deferredVariablesView.promise;
} else {
hud.jsterm.off("variablesview-open", entry._onVariablesView);
entry._onVariablesView = null;
}
return promise.resolve(null);
if (entry.expectedTab) {
yield deferredTab.promise;
} else {
container.removeEventListener("TabOpen", entry._onTabOpen, true);
entry._onTabOpen = null;
}
yield promise.resolve(null);
}
function checkLinkToInspector(entry, msg)
@ -1513,7 +1552,7 @@ function checkOutputForInputs(hud, inputTests)
});
}
function onVariablesViewOpen(entry, deferred, event, view, options)
function onVariablesViewOpen(entry, {resolve, reject}, event, view, options)
{
let label = entry.variablesViewLabel || entry.output;
if (typeof label == "string" && options.label != label) {
@ -1524,12 +1563,25 @@ function checkOutputForInputs(hud, inputTests)
}
hud.jsterm.off("variablesview-open", entry._onVariablesViewOpen);
eventHandlers.delete(entry._onVariablesViewOpen);
entry._onVariablesViewOpen = null;
ok(entry.inspectable, "variables view was shown");
deferred.resolve(null);
resolve(null);
}
function onTabOpen(entry, {resolve, reject}, event)
{
container.removeEventListener("TabOpen", entry._onTabOpen, true);
entry._onTabOpen = null;
let tab = event.target;
let browser = gBrowser.getBrowserForTab(tab);
loadBrowser(browser).then(() => {
let uri = content.location.href;
ok(entry.expectedTab && entry.expectedTab == uri,
"opened tab '" + uri + "', expected tab '" + entry.expectedTab + "'");
return closeTab(tab);
}).then(resolve, reject);
}
return Task.spawn(runner);