# HG changeset patch # Parent 8a56937c985b8525ca5e32c5a08318cf12ad5f6d # 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) { + if (counter.error) { + // TODO: no error is shown in the browser console + Cu.reportError(l10n.getStr(counter.error)); + } else { + Messages.Extended.call(this, + [counter.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,17 @@ 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-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 @@ -227,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,66 @@ +/* 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("button"); + ok(button, "we have the button"); + EventUtils.sendMouseEvent({ type: "click" }, button, content); + + yield waitForMessages({ + webconsole: hud, + messages: [{ + text: "start", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG + }, + { + text: /foo: 1/, + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG + }, + { + text: /bar: 2/, + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG + }, + { + text: /foo: 2/, + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG + }, + { + text: /foo: 3/, + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG + }, + { + text: /bar: 2/, + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG + }, + { + text: "end", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG + }] + }); + + finishTest(); + } +} 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,26 @@ + + + + + + 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. @@ -1179,17 +1180,18 @@ WebConsoleFrame.prototype = { switch (level) { case "log": case "info": case "warn": case "error": case "exception": case "assert": - case "debug": { + case "debug": + case "count": { let msg = new Messages.ConsoleGeneric(aMessage); node = msg.init(this.output).render().element; break; } case "trace": { let msg = new Messages.ConsoleTrace(aMessage); node = msg.init(this.output).render().element; break; 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 @@ -120,16 +120,18 @@ timerStarted=%S: timer started timeEnd=%1$S: %2$Sms # 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(args[0]); + break; default: // unknown console API method! return; } this.notifyObservers(method, consoleEvent); }, @@ -495,12 +507,45 @@ 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 string aLabel + * The label of the counter. + * @return object + * The label property holds the counters label and the count property + * holds the current count. + **/ + increaseCounter: function CA_increaseCounter(aLabel) { + if (!aLabel) { + return false; + } + + let label = aLabel.toString(); + if (!this.counterRegistry.has(label)) { + if (this.counterRegistry.size > MAX_PAGE_COUNTERS - 1) { + return { error: "maxCountersExceeded" }; + } + this.counterRegistry.set(label, 1); + } else { + this.counterRegistry.set(label, this.counterRegistry.get(label) + 1); + } + return { label: label, count: this.counterRegistry.get(label) }; } }; 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 @@ -127,17 +127,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 +257,20 @@ 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"); + win.console.count("label"); + yield undefined; + startTraceTest(); yield undefined; startLocationTest(); yield undefined; } function consoleAPISanityTest() { @@ -282,16 +286,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"); }