mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 1005909 - Make URLs in console strings clickable. r=rcampbell
This commit is contained in:
parent
6cd5bee13d
commit
9dddfe29e9
@ -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.
|
||||
*
|
||||
|
@ -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]
|
||||
|
@ -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);
|
||||
}
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user