# HG changeset patch # Parent dd161d0840c60b98f0ea4dcb4c28ecccc80cfa54 # User Dennis Schubert Bug 922208 - Add console.count diff --git a/browser/devtools/webconsole/console-output.js b/browser/devtools/webconsole/console-output.js --- a/browser/devtools/webconsole/console-output.js +++ b/browser/devtools/webconsole/console-output.js @@ -77,17 +77,18 @@ const CONSOLE_API_LEVELS_TO_SEVERITIES = log: "log", trace: "log", debug: "log", dir: "log", group: "log", groupCollapsed: "log", groupEnd: "log", time: "log", - timeEnd: "log" + timeEnd: "log", + count: "log" }; // Array of known message source URLs we need to hide from output. const IGNORED_SOURCE_URLS = ["debugger eval code", "self-hosted"]; // The maximum length of strings to be displayed by the Web Console. const MAX_LONG_STRING_LENGTH = 200000; @@ -1072,17 +1073,34 @@ Messages.ConsoleGeneric = function(packe severity: CONSOLE_API_LEVELS_TO_SEVERITIES[packet.level], private: packet.private, filterDuplicates: true, location: { url: packet.filename, line: packet.lineNumber, }, }; - Messages.Extended.call(this, packet.arguments, options); + switch (packet.level) { + case "count": { + let counter = packet.counter; + if (counter) { + let label = counter.label; + if (!label) { + label = l10n.getStr("noCounterLabel"); + } + Messages.Extended.call(this, + [label+ ": " + counter.count], options); + } + break; + } + default: + Messages.Extended.call(this, packet.arguments, options); + break; + } + this._repeatID.consoleApiLevel = packet.level; }; Messages.ConsoleGeneric.prototype = Heritage.extend(Messages.Extended.prototype, { _renderBodyPieceSeparator: function() { return this.document.createTextNode(" "); diff --git a/browser/devtools/webconsole/test/browser.ini b/browser/devtools/webconsole/test/browser.ini --- a/browser/devtools/webconsole/test/browser.ini +++ b/browser/devtools/webconsole/test/browser.ini @@ -56,16 +56,18 @@ support-files = test-bug-837351-security-errors.html test-bug-846918-hsts-invalid-headers.html test-bug-846918-hsts-invalid-headers.html^headers^ test-bug-859170-longstring-hang.html test-bug-869003-iframe.html test-bug-869003-top-window.html test-closures.html test-console-assert.html + test-console-count.html + test-console-count-external-file.js test-console-extras.html test-console-replaced-api.html test-console.html test-console-output-02.html test-console-output-03.html test-console-output-04.html test-console-output-events.html test-consoleiframes.html @@ -226,16 +228,17 @@ run-if = os == "mac" [browser_webconsole_bug_846918_hsts_invalid-headers.js] [browser_webconsole_cached_autocomplete.js] [browser_webconsole_change_font_size.js] [browser_webconsole_chrome.js] [browser_webconsole_closure_inspection.js] [browser_webconsole_completion.js] [browser_webconsole_console_extras.js] [browser_webconsole_console_logging_api.js] +[browser_webconsole_count.js] [browser_webconsole_execution_scope.js] [browser_webconsole_for_of.js] [browser_webconsole_history.js] [browser_webconsole_input_field_focus_on_panel_select.js] [browser_webconsole_js_input_expansion.js] [browser_webconsole_jsterm.js] [browser_webconsole_live_filtering_of_message_types.js] [browser_webconsole_live_filtering_on_search_strings.js] diff --git a/browser/devtools/webconsole/test/browser_webconsole_count.js b/browser/devtools/webconsole/test/browser_webconsole_count.js new file mode 100644 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_count.js @@ -0,0 +1,84 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that console.count() counts as expected. See bug 922208. + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console-count.html"; + +function test() +{ + addTab(TEST_URI); + browser.addEventListener("load", function onLoad() { + browser.removeEventListener("load", onLoad, true); + Task.spawn(runner); + }, true); + + function* runner() + { + let hud = yield openConsole(); + + let button = content.document.querySelector("#local"); + ok(button, "we have the local-tests button"); + EventUtils.sendMouseEvent({ type: "click" }, button, content); + let messages = []; + [ + "start", + ": 2", + "console.count() testcounter: 1", + "console.count() testcounter: 2", + "console.count() testcounter: 3", + "console.count() testcounter: 4", + "end" + ].forEach(function (msg) { + messages.push({ + text: msg, + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG + }); + }); + messages.push({ + name: "Three local counts with no label and count=1", + text: ": 1", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + count: 3 + }); + yield waitForMessages({ + webconsole: hud, + messages: messages + }); + + hud.jsterm.clearOutput(); + + button = content.document.querySelector("#external"); + ok(button, "we have the external-tests button"); + EventUtils.sendMouseEvent({ type: "click" }, button, content); + messages = []; + [ + "start", + "console.count() testcounter: 5", + "console.count() testcounter: 6", + "end" + ].forEach(function (msg) { + messages.push({ + text: msg, + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG + }); + }); + messages.push({ + name: "Two external counts with no label and count=1", + text: ": 1", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + count: 2 + }); + yield waitForMessages({ + webconsole: hud, + messages: messages + }); + + finishTest(); + } +} diff --git a/browser/devtools/webconsole/test/test-console-count-external-file.js b/browser/devtools/webconsole/test/test-console-count-external-file.js new file mode 100644 --- /dev/null +++ b/browser/devtools/webconsole/test/test-console-count-external-file.js @@ -0,0 +1,7 @@ +function counterExternalFile() { + console.count("console.count() testcounter"); +} +function externalCountersWithoutLabel() { + console.count(); + console.count(); +} diff --git a/browser/devtools/webconsole/test/test-console-count.html b/browser/devtools/webconsole/test/test-console-count.html new file mode 100644 --- /dev/null +++ b/browser/devtools/webconsole/test/test-console-count.html @@ -0,0 +1,56 @@ + + + + + + console.count() test + + + + + +

test console.count()

+ + + + diff --git a/browser/devtools/webconsole/test/test-console-extras.html b/browser/devtools/webconsole/test/test-console-extras.html --- a/browser/devtools/webconsole/test/test-console-extras.html +++ b/browser/devtools/webconsole/test/test-console-extras.html @@ -4,17 +4,16 @@ Console extended API test

Heads Up Display Demo

diff --git a/browser/devtools/webconsole/webconsole.js b/browser/devtools/webconsole/webconsole.js --- a/browser/devtools/webconsole/webconsole.js +++ b/browser/devtools/webconsole/webconsole.js @@ -124,17 +124,18 @@ const LEVELS = { log: SEVERITY_LOG, trace: SEVERITY_LOG, debug: SEVERITY_LOG, dir: SEVERITY_LOG, group: SEVERITY_LOG, groupCollapsed: SEVERITY_LOG, groupEnd: SEVERITY_LOG, time: SEVERITY_LOG, - timeEnd: SEVERITY_LOG + timeEnd: SEVERITY_LOG, + count: SEVERITY_LOG }; // The lowest HTTP response code (inclusive) that is considered an error. const MIN_HTTP_ERROR_CODE = 400; // The highest HTTP response code (inclusive) that is considered an error. const MAX_HTTP_ERROR_CODE = 599; // Constants used for defining the direction of JSTerm input history navigation. @@ -1239,16 +1240,30 @@ WebConsoleFrame.prototype = { return null; } let duration = Math.round(timer.duration * 100) / 100; body = l10n.getFormatStr("timeEnd", [timer.name, duration]); clipboardText = body; break; } + case "count": { + let counter = aMessage.counter; + if (!counter) { + return null; + } + if (counter.error) { + Cu.reportError(l10n.getStr(counter.error)); + return null; + } + let msg = new Messages.ConsoleGeneric(aMessage); + node = msg.init(this.output).render().element; + break; + } + default: Cu.reportError("Unknown Console API log level: " + level); return null; } // Release object actors for arguments coming from console API methods that // we ignore their arguments. switch (level) { diff --git a/browser/locales/en-US/chrome/browser/devtools/webconsole.properties b/browser/locales/en-US/chrome/browser/devtools/webconsole.properties --- a/browser/locales/en-US/chrome/browser/devtools/webconsole.properties +++ b/browser/locales/en-US/chrome/browser/devtools/webconsole.properties @@ -114,22 +114,28 @@ unknownLocation= # of the console.time() call. Parameters: %S is the name of the timer. timerStarted=%S: timer started # LOCALIZATION NOTE (timeEnd): this string is used to display the result of # the console.timeEnd() call. Parameters: %1$S is the name of the timer, %2$S # is the number of milliseconds. timeEnd=%1$S: %2$Sms +# LOCALIZATION NOTE (noCounterLabel): this string is used to display +# count-messages with no label provided +noCounterLabel= + # LOCALIZATION NOTE (Autocomplete.blank): this string is used when inputnode # string containing anchor doesn't matches to any property in the content. Autocomplete.blank= <- no result maxTimersExceeded=The maximum allowed number of timers in this page was exceeded. +maxCountersExceeded=The maximum allowed number of counters in this page was exceeded. + # LOCALIZATION NOTE (JSTerm.updateNotInspectable): this string is used when # the user inspects an evaluation result in the Web Console and tries the # Update button, but the new result no longer returns an object that can be # inspected. JSTerm.updateNotInspectable=After your input has been re-evaluated the result is no longer inspectable. # LOCALIZATION NOTE (remoteWebConsolePromptTitle): the title displayed on the # Web Console prompt asking for the remote host and port to connect to. diff --git a/dom/base/ConsoleAPI.js b/dom/base/ConsoleAPI.js --- a/dom/base/ConsoleAPI.js +++ b/dom/base/ConsoleAPI.js @@ -6,16 +6,19 @@ let Cu = Components.utils; let Ci = Components.interfaces; let Cc = Components.classes; // The maximum allowed number of concurrent timers per page. const MAX_PAGE_TIMERS = 10000; +// The maximum allowed number of concurrent counters per page. +const MAX_PAGE_COUNTERS = 10000; + // The regular expression used to parse %s/%d and other placeholders for // variables in strings that need to be interpolated. const ARGUMENT_PATTERN = /%\d*\.?\d*([osdif])\b/g; // The maximum stacktrace depth when populating the stacktrace array used for // console.trace(). const DEFAULT_MAX_STACKTRACE_DEPTH = 200; @@ -133,33 +136,37 @@ ConsoleAPI.prototype = { null); }, assert: function CA_assert() { let args = Array.prototype.slice.call(arguments); if(!args.shift()) { self.queueCall("assert", args); } }, + count: function CA_count() { + self.queueCall("count", arguments); + }, __exposedProps__: { log: "r", info: "r", warn: "r", error: "r", exception: "r", debug: "r", trace: "r", dir: "r", group: "r", groupCollapsed: "r", groupEnd: "r", time: "r", timeEnd: "r", profile: "r", profileEnd: "r", - assert: "r" + assert: "r", + count: "r" } }; // We need to return an actual content object here, instead of a wrapped // chrome object. This allows things like console.log.bind() to work. let contentObj = Cu.createObjectIn(aWindow); function genPropDesc(fun) { return { enumerable: true, configurable: true, writable: true, @@ -177,28 +184,30 @@ ConsoleAPI.prototype = { group: genPropDesc('group'), groupCollapsed: genPropDesc('groupCollapsed'), groupEnd: genPropDesc('groupEnd'), time: genPropDesc('time'), timeEnd: genPropDesc('timeEnd'), profile: genPropDesc('profile'), profileEnd: genPropDesc('profileEnd'), assert: genPropDesc('assert'), + count: genPropDesc('count'), __noSuchMethod__: { enumerable: true, configurable: true, writable: true, value: function() {} }, __mozillaConsole__: { value: true } }; Object.defineProperties(contentObj, properties); Cu.makeObjectPropsNormal(contentObj); this._queuedCalls = []; this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); this._window = Cu.getWeakReference(aWindow); this.timerRegistry = new Map(); + this.counterRegistry = new Map(); return contentObj; }, observe: function CA_observe(aSubject, aTopic, aData) { if (aTopic == "inner-window-destroyed") { let innerWindowID = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data; @@ -327,16 +336,19 @@ ConsoleAPI.prototype = { case "dir": break; case "time": consoleEvent.timer = this.startTimer(args[0], meta.monotonicTimer); break; case "timeEnd": consoleEvent.timer = this.stopTimer(args[0], meta.monotonicTimer); break; + case "count": + consoleEvent.counter = this.increaseCounter(frame, args[0]); + break; default: // unknown console API method! return; } this.notifyObservers(method, consoleEvent); }, @@ -495,12 +507,54 @@ ConsoleAPI.prototype = { } let key = aName.toString(); if (!this.timerRegistry.has(key)) { return; } let duration = aTimestamp - this.timerRegistry.get(key); this.timerRegistry.delete(key); return { name: aName, duration: duration }; + }, + + /* + * A registry of counsole.count() counters. + * @type Map + */ + counterRegistry: null, + + /** + * Increases the given counter by one or creates a new counter if the label + * is not known so far. + * + * @param object aFrame + * The current stack frame to extract the filename and linenumber + * from the console.count() invocation. + * @param string aLabel + * The label of the counter. If no label is provided, the script url + * and line number is used for associating the counters + * @return object + * The label property holds the counters label and the count property + * holds the current count. + **/ + increaseCounter: function CA_increaseCounter(aFrame, aLabel) { + let key = null, label = null; + if (!aLabel) { + key = aFrame.filename + ":" + aFrame.lineNumber.toString(); + } else { + label = aLabel.toString(); + key = label; + } + let counter = {}; + if (!this.counterRegistry.has(key)) { + if (this.counterRegistry.size > MAX_PAGE_COUNTERS - 1) { + return { error: "maxCountersExceeded" }; + } + counter = { label: label, count: 1 }; + } else { + counter = this.counterRegistry.get(key); + counter.count += 1; + } + this.counterRegistry.set(key, counter); + return { label: counter.label, count: counter.count }; } }; this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ConsoleAPI]); diff --git a/dom/tests/browser/browser_ConsoleAPITests.js b/dom/tests/browser/browser_ConsoleAPITests.js --- a/dom/tests/browser/browser_ConsoleAPITests.js +++ b/dom/tests/browser/browser_ConsoleAPITests.js @@ -35,33 +35,41 @@ function test() { function testConsoleData(aMessageObject) { let messageWindow = Services.wm.getOuterWindowWithId(aMessageObject.ID); is(messageWindow, gWindow, "found correct window by window ID"); is(aMessageObject.level, gLevel, "expected level received"); ok(aMessageObject.arguments, "we have arguments"); - if (gLevel == "trace") { - is(aMessageObject.arguments.length, 0, "arguments.length matches"); - is(aMessageObject.stacktrace.toSource(), gArgs.toSource(), - "stack trace is correct"); - } - else { - is(aMessageObject.arguments.length, gArgs.length, "arguments.length matches"); - gArgs.forEach(function (a, i) { - // Waive Xray so that we don't get messed up by Xray ToString. - // - // It'd be nice to just use XPCNativeWrapper.unwrap here, but there are - // a number of dumb reasons we can't. See bug 868675. - var arg = aMessageObject.arguments[i]; - if (Components.utils.isXrayWrapper(arg)) - arg = arg.wrappedJSObject; - is(arg, a, "correct arg " + i); - }); + switch (gLevel) { + case "trace": { + is(aMessageObject.arguments.length, 0, "arguments.length matches"); + is(aMessageObject.stacktrace.toSource(), gArgs.toSource(), + "stack trace is correct"); + break + } + case "count": { + is(aMessageObject.counter.label, gArgs[0].label, "label matches"); + is(aMessageObject.counter.count, gArgs[0].count, "count matches"); + break; + } + default: { + is(aMessageObject.arguments.length, gArgs.length, "arguments.length matches"); + gArgs.forEach(function (a, i) { + // Waive Xray so that we don't get messed up by Xray ToString. + // + // It'd be nice to just use XPCNativeWrapper.unwrap here, but there are + // a number of dumb reasons we can't. See bug 868675. + var arg = aMessageObject.arguments[i]; + if (Components.utils.isXrayWrapper(arg)) + arg = arg.wrappedJSObject; + is(arg, a, "correct arg " + i); + }); + } } gTestDriver.next(); } function testLocationData(aMessageObject) { let messageWindow = Services.wm.getOuterWindowWithId(aMessageObject.ID); is(messageWindow, gWindow, "found correct window by window ID"); @@ -127,17 +135,17 @@ function testConsoleGroup(aMessageObject is(messageWindow, gWindow, "found correct window by window ID"); ok(aMessageObject.level == "group" || aMessageObject.level == "groupCollapsed" || aMessageObject.level == "groupEnd", "expected level received"); is(aMessageObject.functionName, "testGroups", "functionName matches"); - ok(aMessageObject.lineNumber >= 45 && aMessageObject.lineNumber <= 49, + ok(aMessageObject.lineNumber >= 46 && aMessageObject.lineNumber <= 50, "lineNumber matches"); if (aMessageObject.level == "groupCollapsed") { is(aMessageObject.groupName, "a group", "groupCollapsed groupName matches"); is(aMessageObject.arguments[0], "a", "groupCollapsed arguments[0] matches"); is(aMessageObject.arguments[1], "group", "groupCollapsed arguments[0] matches"); } else if (aMessageObject.level == "group") { is(aMessageObject.groupName, "b group", "group groupName matches"); @@ -257,16 +265,32 @@ function observeConsoleTest() { expect("log", "omg ", obj, " foo ", 4, obj2); win.console.log("omg %o foo %o", obj, 4, obj2); yield undefined; expect("assert", "message"); win.console.assert(false, "message"); yield undefined; + expect("count", { label: "label a", count: 1 }) + win.console.count("label a"); + yield undefined; + + expect("count", { label: "label b", count: 1 }) + win.console.count("label b"); + yield undefined; + + expect("count", { label: "label a", count: 2 }) + win.console.count("label a"); + yield undefined; + + expect("count", { label: "label b", count: 2 }) + win.console.count("label b"); + yield undefined; + startTraceTest(); yield undefined; startLocationTest(); yield undefined; } function consoleAPISanityTest() { @@ -282,16 +306,17 @@ function consoleAPISanityTest() { ok(win.console.trace, "console.trace is here"); ok(win.console.dir, "console.dir is here"); ok(win.console.group, "console.group is here"); ok(win.console.groupCollapsed, "console.groupCollapsed is here"); ok(win.console.groupEnd, "console.groupEnd is here"); ok(win.console.time, "console.time is here"); ok(win.console.timeEnd, "console.timeEnd is here"); ok(win.console.assert, "console.assert is here"); + ok(win.console.count, "console.count is here"); } function startTimeTest() { // Reset the observer function to cope with the fabricated test data. ConsoleObserver.observe = function CO_observe(aSubject, aTopic, aData) { try { testConsoleTime(aSubject.wrappedJSObject); } catch (ex) { diff --git a/dom/tests/browser/test-console-api.html b/dom/tests/browser/test-console-api.html --- a/dom/tests/browser/test-console-api.html +++ b/dom/tests/browser/test-console-api.html @@ -36,16 +36,17 @@ var str = "Test Message." console.foobar(str); // if this throws, we don't execute following funcs console.log(str); console.info(str); console.warn(str); console.error(str); console.exception(str); console.assert(false, str); + console.count(str); } function testGroups() { console.groupCollapsed("a", "group"); console.group("b", "group"); console.groupEnd("b", "group"); } diff --git a/dom/tests/mochitest/general/test_consoleAPI.html b/dom/tests/mochitest/general/test_consoleAPI.html --- a/dom/tests/mochitest/general/test_consoleAPI.html +++ b/dom/tests/mochitest/general/test_consoleAPI.html @@ -31,16 +31,17 @@ function doTest() { "group": "function", "groupCollapsed": "function", "groupEnd": "function", "time": "function", "timeEnd": "function", "profile": "function", "profileEnd": "function", "assert": "function", + "count": "function", "__noSuchMethod__": "function" }; var foundProps = 0; for (var prop in console) { foundProps++; is(typeof(console[prop]), expectedProps[prop], "expect console prop " + prop + " exists"); }