Bug 762996 - Add errors count to the Web Console button in the developer toolbar; r=paul

This commit is contained in:
Mihai Sucan 2012-06-14 14:36:48 +03:00
Родитель 0195bfa381
Коммит f37e168b92
7 изменённых файлов: 576 добавлений и 48 удалений

Просмотреть файл

@ -8,6 +8,9 @@ const EXPORTED_SYMBOLS = [ "DeveloperToolbar" ];
const NS_XHTML = "http://www.w3.org/1999/xhtml";
const WEBCONSOLE_CONTENT_SCRIPT_URL =
"chrome://browser/content/devtools/HUDService-content.js";
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
Components.utils.import("resource://gre/modules/Services.jsm");
@ -36,6 +39,10 @@ function DeveloperToolbar(aChromeWindow, aToolbarElement)
this._lastState = NOTIFICATIONS.HIDE;
this._pendingShowCallback = undefined;
this._pendingHide = false;
this._errorsCount = {};
this._webConsoleButton = this._doc
.getElementById("developer-toolbar-webconsole");
this._webConsoleButtonLabel = this._webConsoleButton.label;
}
/**
@ -58,6 +65,9 @@ const NOTIFICATIONS = {
*/
DeveloperToolbar.prototype.NOTIFICATIONS = NOTIFICATIONS;
DeveloperToolbar.prototype._contentMessageListeners =
["WebConsole:CachedMessages", "WebConsole:PageError"];
/**
* Is the toolbar open?
*/
@ -68,6 +78,18 @@ Object.defineProperty(DeveloperToolbar.prototype, 'visible', {
enumerable: true
});
var _gSequenceId = 0;
/**
* Getter for a unique ID.
*/
Object.defineProperty(DeveloperToolbar.prototype, 'sequenceId', {
get: function DT_visible() {
return _gSequenceId++;
},
enumerable: true
});
/**
* Called from browser.xul in response to menu-click or keyboard shortcut to
* toggle the toolbar
@ -152,9 +174,13 @@ DeveloperToolbar.prototype._onload = function DT_onload()
this.display.onOutput.add(this.outputPanel._outputChanged, this.outputPanel);
this._chromeWindow.getBrowser().tabContainer.addEventListener("TabSelect", this, false);
this._chromeWindow.getBrowser().tabContainer.addEventListener("TabClose", this, false);
this._chromeWindow.getBrowser().addEventListener("load", this, true);
this._chromeWindow.getBrowser().addEventListener("beforeunload", this, true);
this._chromeWindow.addEventListener("resize", this, false);
this._initErrorsCount(this._chromeWindow.getBrowser().selectedTab);
this._element.hidden = false;
this._input.focus();
@ -178,6 +204,67 @@ DeveloperToolbar.prototype._onload = function DT_onload()
}
};
/**
* Initialize the listeners needed for tracking the number of errors for a given
* tab.
*
* @private
* @param nsIDOMNode aTab the xul:tab for which you want to track the number of
* errors.
*/
DeveloperToolbar.prototype._initErrorsCount = function DT__initErrorsCount(aTab)
{
let tabId = aTab.linkedPanel;
if (tabId in this._errorsCount) {
this._updateErrorsCount();
return;
}
let messageManager = aTab.linkedBrowser.messageManager;
messageManager.loadFrameScript(WEBCONSOLE_CONTENT_SCRIPT_URL, true);
this._errorsCount[tabId] = 0;
this._contentMessageListeners.forEach(function(aName) {
messageManager.addMessageListener(aName, this);
}, this);
let message = {
features: ["PageError"],
cachedMessages: ["PageError"],
};
this.sendMessageToTab(aTab, "WebConsole:Init", message);
this._updateErrorsCount();
};
/**
* Stop the listeners needed for tracking the number of errors for a given
* tab.
*
* @private
* @param nsIDOMNode aTab the xul:tab for which you want to stop tracking the
* number of errors.
*/
DeveloperToolbar.prototype._stopErrorsCount = function DT__stopErrorsCount(aTab)
{
let tabId = aTab.linkedPanel;
if (!(tabId in this._errorsCount)) {
this._updateErrorsCount();
return;
}
this.sendMessageToTab(aTab, "WebConsole:Destroy", {});
let messageManager = aTab.linkedBrowser.messageManager;
this._contentMessageListeners.forEach(function(aName) {
messageManager.removeMessageListener(aName, this);
}, this);
delete this._errorsCount[tabId];
this._updateErrorsCount();
};
/**
* Hide the developer toolbar.
*/
@ -207,6 +294,11 @@ DeveloperToolbar.prototype.destroy = function DT_destroy()
{
this._chromeWindow.getBrowser().tabContainer.removeEventListener("TabSelect", this, false);
this._chromeWindow.getBrowser().removeEventListener("load", this, true);
this._chromeWindow.getBrowser().removeEventListener("beforeunload", this, true);
this._chromeWindow.removeEventListener("resize", this, false);
let tabs = this._chromeWindow.getBrowser().tabs;
Array.prototype.forEach.call(tabs, this._stopErrorsCount, this);
this.display.onVisibilityChange.remove(this.outputPanel._visibilityChanged, this.outputPanel);
this.display.onVisibilityChange.remove(this.tooltipPanel._visibilityChanged, this.tooltipPanel);
@ -261,11 +353,148 @@ DeveloperToolbar.prototype.handleEvent = function DT_handleEvent(aEvent)
contentDocument: contentDocument
},
});
if (aEvent.type == "TabSelect") {
this._initErrorsCount(aEvent.target);
}
}
}
else if (aEvent.type == "resize") {
this.outputPanel._resize();
}
else if (aEvent.type == "TabClose") {
this._stopErrorsCount(aEvent.target);
}
else if (aEvent.type == "beforeunload") {
this._onPageBeforeUnload(aEvent);
}
};
/**
* The handler of messages received from the nsIMessageManager.
*
* @param object aMessage the message received from the content process.
*/
DeveloperToolbar.prototype.receiveMessage = function DT_receiveMessage(aMessage)
{
if (!aMessage.json || !(aMessage.json.hudId in this._errorsCount)) {
return;
}
let tabId = aMessage.json.hudId;
let errors = this._errorsCount[tabId];
switch (aMessage.name) {
case "WebConsole:PageError":
this._onPageError(tabId, aMessage.json.pageError);
break;
case "WebConsole:CachedMessages":
aMessage.json.messages.forEach(this._onPageError.bind(this, tabId));
break;
}
if (errors != this._errorsCount[tabId]) {
this._updateErrorsCount(tabId);
}
};
/**
* Send a message to the content process using the nsIMessageManager of the
* given tab.
*
* @param nsIDOMNode aTab the tab you want to send a message to.
* @param string aName the name of the message you want to send.
* @param object aMessage the message to send.
*/
DeveloperToolbar.prototype.sendMessageToTab =
function DT_sendMessageToTab(aTab, aName, aMessage)
{
let tabId = aTab.linkedPanel;
aMessage.hudId = tabId;
if (!("id" in aMessage)) {
aMessage.id = "DevToolbar-" + this.sequenceId;
}
aTab.linkedBrowser.messageManager.sendAsyncMessage(aName, aMessage);
};
/**
* Process a "WebConsole:PageError" message received from the given tab. This
* method counts the JavaScript exceptions received.
*
* @private
* @param string aTabId the ID of the tab from where the page error comes.
* @param object aPageError the page error object received from the content
* process.
*/
DeveloperToolbar.prototype._onPageError =
function DT__onPageError(aTabId, aPageError)
{
if (aPageError.category == "CSS Parser" ||
aPageError.category == "CSS Loader" ||
(aPageError.flags & aPageError.warningFlag) ||
(aPageError.flags & aPageError.strictFlag)) {
return; // just a CSS or JS warning
}
this._errorsCount[aTabId]++;
};
/**
* The |beforeunload| event handler. This function resets the errors count when
* a different page starts loading.
*
* @private
* @param nsIDOMEvent aEvent the beforeunload DOM event.
*/
DeveloperToolbar.prototype._onPageBeforeUnload =
function DT__onPageBeforeUnload(aEvent)
{
let window = aEvent.target.defaultView;
if (window.top !== window) {
return;
}
let tabs = this._chromeWindow.getBrowser().tabs;
Array.prototype.some.call(tabs, function(aTab) {
if (aTab.linkedBrowser.contentWindow === window) {
let tabId = aTab.linkedPanel;
if (tabId in this._errorsCount) {
this._errorsCount[tabId] = 0;
this._updateErrorsCount(tabId);
}
return true;
}
return false;
}, this);
};
/**
* Update the page errors count displayed in the Web Console button for the
* currently selected tab.
*
* @private
* @param string [aChangedTabId] Optional. The tab ID that had its page errors
* count changed. If this is provided and it doesn't match the currently
* selected tab, then the button is not updated.
*/
DeveloperToolbar.prototype._updateErrorsCount =
function DT__updateErrorsCount(aChangedTabId)
{
let tabId = this._chromeWindow.getBrowser().selectedTab.linkedPanel;
if (aChangedTabId && tabId != aChangedTabId) {
return;
}
let errors = this._errorsCount[tabId];
if (errors) {
this._webConsoleButton.label =
this._webConsoleButtonLabel + " (" + errors + ")";
}
else {
this._webConsoleButton.label = this._webConsoleButtonLabel;
}
};
/**

Просмотреть файл

@ -19,12 +19,14 @@ _BROWSER_TEST_FILES = \
browser_templater_basic.js \
browser_toolbar_basic.js \
browser_toolbar_tooltip.js \
browser_toolbar_webconsole_errors_count.js \
head.js \
$(NULL)
_BROWSER_TEST_PAGES = \
browser_templater_basic.html \
browser_toolbar_basic.html \
browser_toolbar_webconsole_errors_count.html \
$(NULL)
libs:: $(_BROWSER_TEST_FILES)

Просмотреть файл

@ -0,0 +1,29 @@
<!DOCTYPE HTML>
<!-- Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ -->
<html>
<head>
<meta charset="UTF-8">
<title>Developer Toolbar Tests - errors count in the Web Console button</title>
<script type="text/javascript">
console.log("foobarBug762996consoleLog");
window.onload = function() {
window.foobarBug762996load();
};
window.foobarBug762996a();
</script>
<script type="text/javascript">
window.foobarBug762996b();
</script>
</head>
<body>
<p>Hello world! Test for errors count in the Web Console button (developer
toolbar).</p>
<p style="color: foobarBug762996css"><button>click me</button></p>
<script type="text/javascript">
document.querySelector("button").onclick = function() {
window.foobarBug762996click();
};
</script>
</body>
</html>

Просмотреть файл

@ -0,0 +1,208 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Tests that the developer toolbar errors count works properly.
function test() {
const TEST_URI = "http://example.com/browser/browser/devtools/shared/test/browser_toolbar_webconsole_errors_count.html";
let imported = {};
Components.utils.import("resource:///modules/HUDService.jsm", imported);
let HUDService = imported.HUDService;
let webconsole = document.getElementById("developer-toolbar-webconsole");
let toolbar = document.getElementById("Tools:DevToolbar");
let tab1, tab2;
function openToolbar(browser, tab) {
tab1 = tab;
ignoreAllUncaughtExceptions(false);
ok(!DeveloperToolbar.visible, "DeveloperToolbar is not visible");
expectUncaughtException();
oneTimeObserve(DeveloperToolbar.NOTIFICATIONS.SHOW, onOpenToolbar);
toolbar.doCommand();
}
ignoreAllUncaughtExceptions();
addTab(TEST_URI, openToolbar);
function getErrorsCount() {
let match = webconsole.label.match(/\((\d+)\)$/);
return (match || [])[1];
}
function onOpenToolbar() {
ok(DeveloperToolbar.visible, "DeveloperToolbar is visible");
waitForValue({
name: "web console button shows page errors",
validator: getErrorsCount,
value: 3,
success: addErrors,
failure: finish,
});
}
function addErrors() {
expectUncaughtException();
let button = content.document.querySelector("button");
EventUtils.synthesizeMouse(button, 2, 2, {}, content);
waitForValue({
name: "button shows one more error after click in page",
validator: getErrorsCount,
value: 4,
success: function() {
ignoreAllUncaughtExceptions();
addTab(TEST_URI, onOpenSecondTab);
},
failure: finish,
});
}
function onOpenSecondTab(browser, tab) {
tab2 = tab;
ignoreAllUncaughtExceptions(false);
expectUncaughtException();
waitForValue({
name: "button shows correct number of errors after new tab is open",
validator: getErrorsCount,
value: 3,
success: switchToTab1,
failure: finish,
});
}
function switchToTab1() {
gBrowser.selectedTab = tab1;
waitForValue({
name: "button shows the page errors from tab 1",
validator: getErrorsCount,
value: 4,
success: function() {
openWebConsole(tab1, onWebConsoleOpen);
},
failure: finish,
});
}
function openWebConsole(tab, callback)
{
function _onWebConsoleOpen(subject)
{
subject.QueryInterface(Ci.nsISupportsString);
let hud = HUDService.getHudReferenceById(subject.data);
executeSoon(callback.bind(null, hud));
}
oneTimeObserve("web-console-created", _onWebConsoleOpen);
HUDService.activateHUDForContext(tab);
}
function onWebConsoleOpen(hud) {
waitForValue({
name: "web console shows the page errors",
validator: function() {
return hud.outputNode.querySelectorAll(".hud-exception").length;
},
value: 4,
success: checkConsoleOutput.bind(null, hud),
failure: finish,
});
}
function checkConsoleOutput(hud) {
let errors = ["foobarBug762996a", "foobarBug762996b", "foobarBug762996load",
"foobarBug762996click", "foobarBug762996consoleLog",
"foobarBug762996css"];
errors.forEach(function(error) {
isnot(hud.outputNode.textContent.indexOf(error), -1,
error + " found in the Web Console output");
});
hud.jsterm.clearOutput();
is(hud.outputNode.textContent.indexOf("foobarBug762996color"), -1,
"clearOutput() worked");
expectUncaughtException();
let button = content.document.querySelector("button");
EventUtils.synthesizeMouse(button, 2, 2, {}, content);
waitForValue({
name: "button shows one more error after another click in page",
validator: getErrorsCount,
value: 5,
success: function() {
waitForValue(waitForNewError);
},
failure: finish,
});
let waitForNewError = {
name: "the Web Console displays the new error",
validator: function() {
return hud.outputNode.textContent.indexOf("foobarBug762996click") > -1;
},
success: doPageReload.bind(null, hud),
failure: finish,
};
}
function doPageReload(hud) {
tab1.linkedBrowser.addEventListener("load", function _onReload() {
tab1.linkedBrowser.removeEventListener("load", _onReload, true);
ignoreAllUncaughtExceptions(false);
expectUncaughtException();
}, true);
ignoreAllUncaughtExceptions();
content.location.reload();
waitForValue({
name: "the Web Console button count has been reset after page reload",
validator: getErrorsCount,
value: 3,
success: function() {
waitForValue(waitForConsoleOutputAfterReload);
},
failure: finish,
});
let waitForConsoleOutputAfterReload = {
name: "the Web Console displays the correct number of errors after reload",
validator: function() {
return hud.outputNode.querySelectorAll(".hud-exception").length;
},
value: 4,
success: function() {
isnot(hud.outputNode.textContent.indexOf("foobarBug762996load"), -1,
"foobarBug762996load found in console output after page reload");
testEnd();
},
failure: testEnd,
};
}
function testEnd() {
document.getElementById("developer-toolbar-closebutton").doCommand();
HUDService.deactivateHUDForContext(tab1);
gBrowser.removeTab(tab1);
gBrowser.removeTab(tab2);
finish();
}
function oneTimeObserve(name, callback) {
function _onObserve(aSubject, aTopic, aData) {
Services.obs.removeObserver(_onObserve, name);
callback(aSubject, aTopic, aData);
};
Services.obs.addObserver(_onObserve, name, false);
}
}

Просмотреть файл

@ -131,3 +131,67 @@ function catchFail(func) {
}
};
}
/**
* Polls a given function waiting for the given value.
*
* @param object aOptions
* Options object with the following properties:
* - validator
* A validator function that should return the expected value. This is
* called every few milliseconds to check if the result is the expected
* one. When the returned result is the expected one, then the |success|
* function is called and polling stops. If |validator| never returns
* the expected value, then polling timeouts after several tries and
* a failure is recorded - the given |failure| function is invoked.
* - success
* A function called when the validator function returns the expected
* value.
* - failure
* A function called if the validator function timeouts - fails to return
* the expected value in the given time.
* - name
* Name of test. This is used to generate the success and failure
* messages.
* - timeout
* Timeout for validator function, in milliseconds. Default is 5000 ms.
* - value
* The expected value. If this option is omitted then the |validator|
* function must return a trueish value.
* Each of the provided callback functions will receive two arguments:
* the |aOptions| object and the last value returned by |validator|.
*/
function waitForValue(aOptions)
{
let start = Date.now();
let timeout = aOptions.timeout || 5000;
let lastValue;
function wait(validatorFn, successFn, failureFn)
{
if ((Date.now() - start) > timeout) {
// Log the failure.
ok(false, "Timed out while waiting for: " + aOptions.name);
let expected = "value" in aOptions ?
"'" + aOptions.value + "'" :
"a trueish value";
info("timeout info :: got '" + lastValue + "', expected " + expected);
failureFn(aOptions, lastValue);
return;
}
lastValue = validatorFn(aOptions, lastValue);
let successful = "value" in aOptions ?
lastValue == aOptions.value :
lastValue;
if (successful) {
ok(true, aOptions.name);
successFn(aOptions, lastValue);
}
else {
setTimeout(function() wait(validatorFn, successFn, failureFn), 100);
}
}
wait(aOptions.validator, aOptions.success, aOptions.failure);
}

Просмотреть файл

@ -88,21 +88,19 @@ let Manager = {
*/
receiveMessage: function Manager_receiveMessage(aMessage)
{
if (!_alive) {
if (!_alive || !aMessage.json) {
return;
}
if (!aMessage.json || (aMessage.name != "WebConsole:Init" &&
aMessage.json.hudId != this.hudId)) {
Cu.reportError("Web Console content script: received message " +
aMessage.name + " from wrong hudId!");
if (aMessage.name == "WebConsole:Init" && !this.hudId) {
this._onInit(aMessage.json);
return;
}
if (aMessage.json.hudId != this.hudId) {
return;
}
switch (aMessage.name) {
case "WebConsole:Init":
this._onInit(aMessage.json);
break;
case "WebConsole:EnableFeature":
this.enableFeature(aMessage.json.feature, aMessage.json);
break;
@ -1286,17 +1284,8 @@ let ConsoleListener = {
return;
}
switch (aScriptError.category) {
// We ignore chrome-originating errors as we only care about content.
case "XPConnect JavaScript":
case "component javascript":
case "chrome javascript":
case "chrome registration":
case "XBL":
case "XBL Prototype Handler":
case "XBL Content Sink":
case "xbl javascript":
return;
if (!this.isCategoryAllowed(aScriptError.category)) {
return;
}
let errorWindow =
@ -1309,6 +1298,33 @@ let ConsoleListener = {
Manager.sendMessage("WebConsole:PageError", { pageError: aScriptError });
},
/**
* Check if the given script error category is allowed to be tracked or not.
* We ignore chrome-originating errors as we only care about content.
*
* @param string aCategory
* The nsIScriptError category you want to check.
* @return boolean
* True if the category is allowed to be logged, false otherwise.
*/
isCategoryAllowed: function CL_isCategoryAllowed(aCategory)
{
switch (aCategory) {
case "XPConnect JavaScript":
case "component javascript":
case "chrome javascript":
case "chrome registration":
case "XBL":
case "XBL Prototype Handler":
case "XBL Content Sink":
case "xbl javascript":
return false;
}
return true;
},
/**
* Get the cached page errors for the current inner window.
*
@ -1326,14 +1342,15 @@ let ConsoleListener = {
(errors.value || []).forEach(function(aError) {
if (!(aError instanceof Ci.nsIScriptError) ||
aError.innerWindowID != innerWindowId) {
aError.innerWindowID != innerWindowId ||
!this.isCategoryAllowed(aError.category)) {
return;
}
let remoteMessage = WebConsoleUtils.cloneObject(aError);
remoteMessage._type = "PageError";
result.push(remoteMessage);
});
}, this);
return result;
},

Просмотреть файл

@ -1412,10 +1412,8 @@ HeadsUpDisplay.prototype = {
switch (aMessage._type) {
case "PageError": {
let category = this.categoryForScriptError(aMessage.category);
if (category != -1) {
this.outputMessage(category, this.reportPageError,
[category, aMessage]);
}
this.outputMessage(category, this.reportPageError,
[category, aMessage]);
break;
}
case "ConsoleAPI":
@ -2053,10 +2051,6 @@ HeadsUpDisplay.prototype = {
*/
reportPageError: function HUD_reportPageError(aCategory, aScriptError)
{
if (!aScriptError.outerWindowID) {
return;
}
// Warnings and legacy strict errors become warnings; other types become
// errors.
let severity = SEVERITY_ERROR;
@ -2083,24 +2077,13 @@ HeadsUpDisplay.prototype = {
*
* @param nsIScriptError aScriptError
* The script error you want to determine the category for.
* @return CATEGORY_JS|CATEGORY_CSS|-1
* @return CATEGORY_JS|CATEGORY_CSS
* Depending on the script error CATEGORY_JS or CATEGORY_CSS can be
* returned. If the category is unknown -1 is returned.
* returned.
*/
categoryForScriptError: function HUD_categoryForScriptError(aScriptError)
{
switch (aScriptError.category) {
// We ignore chrome-originating errors as we only care about content.
case "XPConnect JavaScript":
case "component javascript":
case "chrome javascript":
case "chrome registration":
case "XBL":
case "XBL Prototype Handler":
case "XBL Content Sink":
case "xbl javascript":
return -1;
case "CSS Parser":
case "CSS Loader":
return CATEGORY_CSS;
@ -2282,8 +2265,6 @@ HeadsUpDisplay.prototype = {
receiveMessage: function HUD_receiveMessage(aMessage)
{
if (!aMessage.json || aMessage.json.hudId != this.hudId) {
Cu.reportError("JSTerm: received message " + aMessage.name +
" from wrong hudId.");
return;
}
@ -2306,10 +2287,8 @@ HeadsUpDisplay.prototype = {
case "WebConsole:PageError": {
let pageError = aMessage.json.pageError;
let category = this.categoryForScriptError(pageError);
if (category != -1) {
this.outputMessage(category, this.reportPageError,
[category, pageError]);
}
this.outputMessage(category, this.reportPageError,
[category, pageError]);
break;
}
case "WebConsole:CachedMessages":