Merge f-t to m-c
authorPhil Ringnalda <philringnalda@gmail.com>
Mon, 17 Feb 2014 20:56:06 -0800
changeset 169586 6e3ec93efe1d4154cb3e12d999b4730bac0b93bf
parent 169579 463618fae4cc8e07217f9b080b56cc9617a5b9bd (current diff)
parent 169585 22892079a77598076ed1bc0296ecad73f0fa8e39 (diff)
child 169587 dfa1bf6d4c6094d08f23fbaf929b6872d12c1231
child 169622 c97246bac7b726bfe9826dbf4e32f899b07f24be
child 169685 de286a964490a0599f1ad083abcb86481e4c4074
push id270
push userpvanderbeken@mozilla.com
push dateThu, 06 Mar 2014 09:24:21 +0000
milestone30.0a1
Merge f-t to m-c
--- 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,30 @@ 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, 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(" ");
--- 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]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_count.js
@@ -0,0 +1,77 @@
+/* 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() {
+  Task.spawn(runner).then(finishTest);
+
+  function* runner() {
+    const {tab} = yield loadTab(TEST_URI);
+    const hud = yield openConsole(tab);
+
+    let button = content.document.querySelector("#local");
+    ok(button, "we have the local-tests button");
+    EventUtils.sendMouseEvent({ type: "click" }, button, content);
+    let messages = [];
+    [
+      "start",
+      "<no label>: 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: "<no label>: 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: "<no label>: 1",
+      category: CATEGORY_WEBDEV,
+      severity: SEVERITY_LOG,
+      count: 2
+    });
+    yield waitForMessages({
+      webconsole: hud,
+      messages: messages
+    });
+  }
+}
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();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/test/test-console-count.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+  <head>
+    <!--
+      Any copyright is dedicated to the Public Domain.
+      http://creativecommons.org/publicdomain/zero/1.0/
+    -->
+    <meta charset="utf-8">
+    <title>console.count() test</title>
+    <script src="test-console-count-external-file.js"></script>
+    <script tyoe="text/javascript">
+      function counterSeperateScriptTag() {
+        console.count("console.count() testcounter");
+      }
+    </script>
+    <script type="text/javascript">
+      function counterNoLabel() {
+        console.count();
+      }
+      function countersWithoutLabel() {
+        console.count();
+        console.count();
+      }
+      function counterWithLabel() {
+        console.count("console.count() testcounter");
+      }
+      function testLocal() {
+        console.log("start");
+        counterNoLabel();
+        counterNoLabel();
+        countersWithoutLabel();
+        counterWithLabel();
+        counterWithLabel();
+        counterSeperateScriptTag();
+        counterSeperateScriptTag();
+        console.log("end");
+      }
+      function testExternal() {
+        console.log("start");
+        counterExternalFile();
+        counterExternalFile();
+        externalCountersWithoutLabel();
+        console.log("end");
+      }
+    </script>
+  </head>
+  <body>
+    <p>test console.count()</p>
+    <button id="local" onclick="testLocal();">
+      test local console.count() calls
+    </button>
+    <button id="external" onclick="testExternal();">
+      test external console.count() calls
+    </button>
+  </body>
+</html>
--- a/browser/devtools/webconsole/test/test-console-extras.html
+++ b/browser/devtools/webconsole/test/test-console-extras.html
@@ -4,17 +4,16 @@
     <title>Console extended API test</title>
     <script type="text/javascript">
       function test() {
         console.log("start");
         console.clear()
         console.dirxml()
         console.profile()
         console.profileEnd()
-        console.count()
         console.table()
         console.log("end");
       }
     </script>
   </head>
   <body>
     <h1 id="header">Heads Up Display Demo</h1>
     <button onclick="test();">Test Extended API</button>
--- 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,29 +1240,44 @@ 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) {
       case "group":
       case "groupCollapsed":
       case "groupEnd":
       case "time":
       case "timeEnd":
+      case "count":
         for (let actor of objectActors) {
           this._releaseObject(actor);
         }
         objectActors.clear();
     }
 
     if (level == "groupEnd") {
       return null; // no need to continue
--- a/browser/locales/en-US/chrome/browser/devtools/webconsole.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/webconsole.properties
@@ -114,22 +114,30 @@ unknownLocation=<unknown>
 # 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=<no label>
+
 # 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.
 
+# LOCALIZATION NOTE (maxCountersExceeded): Error message shown when the maximum
+# number of console.count()-counters 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.
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -1480,17 +1480,17 @@ toolbarbutton[type="socialmark"] > .tool
 }
 
 /* Implements editBookmarkPanel resizing on folderTree un-collapse. */
 #editBMPanel_folderTree {
   min-width: 27em;
 }
 
 .panel-promo-box {
-  margin: 10px -10px -10px;
+  margin: 10px -4px -4px;
   padding: 8px 10px;
   border-top: 1px solid ThreeDShadow;
   background-image: linear-gradient(hsla(0,0%,0%,.15), hsla(0,0%,0%,.08) 6px);
   border-bottom-left-radius: 3px;
   border-bottom-right-radius: 3px;
 }
 
 @media (-moz-windows-default-theme) {
--- a/content/base/src/nsFrameLoader.cpp
+++ b/content/base/src/nsFrameLoader.cpp
@@ -48,16 +48,17 @@
 #include "nsEventDispatcher.h"
 #include "nsISHistory.h"
 #include "nsISHistoryInternal.h"
 #include "nsIDOMHTMLDocument.h"
 #include "nsIXULWindow.h"
 #include "nsIEditor.h"
 #include "nsIMozBrowserFrame.h"
 #include "nsIPermissionManager.h"
+#include "nsISHistory.h"
 
 #include "nsLayoutUtils.h"
 #include "nsView.h"
 #include "nsAsyncDOMEvent.h"
 
 #include "nsIURI.h"
 #include "nsIURL.h"
 #include "nsNetUtil.h"
@@ -1668,16 +1669,28 @@ nsFrameLoader::MaybeCreateDocShell()
   // but it must be called to make sure things are properly
   // initialized.
   if (NS_FAILED(base_win->Create()) || !win_private) {
     // Do not call Destroy() here. See bug 472312.
     NS_WARNING("Something wrong when creating the docshell for a frameloader!");
     return NS_ERROR_FAILURE;
   }
 
+  if (mIsTopLevelContent &&
+      mOwnerContent->IsXUL(nsGkAtoms::browser) &&
+      !mOwnerContent->HasAttr(kNameSpaceID_None, nsGkAtoms::disablehistory)) {
+    nsresult rv;
+    nsCOMPtr<nsISHistory> sessionHistory =
+      do_CreateInstance(NS_SHISTORY_CONTRACTID, &rv);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    nsCOMPtr<nsIWebNavigation> webNav(do_QueryInterface(mDocShell));
+    webNav->SetSessionHistory(sessionHistory);
+  }
+
   EnsureMessageManager();
 
   if (OwnerIsAppFrame()) {
     // You can't be both an app and a browser frame.
     MOZ_ASSERT(!OwnerIsBrowserFrame());
 
     nsCOMPtr<mozIApplication> ownApp = GetOwnApp();
     MOZ_ASSERT(ownApp);
--- a/content/base/src/nsGkAtomList.h
+++ b/content/base/src/nsGkAtomList.h
@@ -284,16 +284,17 @@ GK_ATOM(dfn, "dfn")
 GK_ATOM(dialog, "dialog")
 GK_ATOM(difference, "difference")
 GK_ATOM(digit, "digit")
 GK_ATOM(dir, "dir")
 GK_ATOM(dirAutoSetBy, "dirAutoSetBy")
 GK_ATOM(directionality, "directionality")
 GK_ATOM(disableOutputEscaping, "disable-output-escaping")
 GK_ATOM(disabled, "disabled")
+GK_ATOM(disablehistory, "disablehistory")
 GK_ATOM(display, "display")
 GK_ATOM(distinct, "distinct")
 GK_ATOM(div, "div")
 GK_ATOM(dl, "dl")
 GK_ATOM(doctypePublic, "doctype-public")
 GK_ATOM(doctypeSystem, "doctype-system")
 GK_ATOM(document, "document")
 GK_ATOM(download, "download")
--- 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;
+    try {
+      label = key = aLabel ? aLabel + "" : "";
+    } catch (ex) { }
+    if (!key) {
+      key = aFrame.filename + ":" + aFrame.lineNumber;
+    }
+    let counter = null;
+    if (!this.counterRegistry.has(key)) {
+      if (this.counterRegistry.size > MAX_PAGE_COUNTERS - 1) {
+        return { error: "maxCountersExceeded" };
+      }
+      counter = { label: label, count: 1 };
+      this.counterRegistry.set(key, counter);
+    } else {
+      counter = this.counterRegistry.get(key);
+      counter.count += 1;
+    }
+    return { label: counter.label, count: counter.count };
   }
 };
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ConsoleAPI]);
--- 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) {
--- 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");
       }
 
--- 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");
   }
--- a/toolkit/content/widgets/browser.xml
+++ b/toolkit/content/widgets/browser.xml
@@ -743,24 +743,24 @@
       <!-- This is managed by the tabbrowser -->
       <field name="lastURI">null</field>
 
       <field name="mDestroyed">false</field>
 
       <constructor>
         <![CDATA[
           try {
-            if (this.docShell && !this.hasAttribute("disablehistory")) {
+            // |webNavigation.sessionHistory| will have been set by the frame
+            // loader when creating the docShell as long as this xul:browser
+            // doesn't have the 'disablehistory' attribute set.
+            if (this.docShell && this.webNavigation.sessionHistory) {
               var os = Components.classes["@mozilla.org/observer-service;1"]
                                  .getService(Components.interfaces.nsIObserverService);
               os.addObserver(this, "browser:purge-session-history", false);
-              // wire up session history
-              this.webNavigation.sessionHistory =
-                      Components.classes["@mozilla.org/browser/shistory;1"]
-                                .createInstance(Components.interfaces.nsISHistory);
+
               // enable global history if we weren't told otherwise
               if (!this.hasAttribute("disableglobalhistory") && !this.isRemoteBrowser) {
                 try {
                   this.docShell.useGlobalHistory = true;
                 } catch(ex) {
                   // This can occur if the Places database is locked
                   Components.utils.reportError("Error enabling browser global history: " + ex);
                 }
@@ -793,17 +793,17 @@
            we are removed from a tabbrowser. This will be explicitly called by tabbrowser -->
       <method name="destroy">
         <body>
           <![CDATA[
           if (this.mDestroyed)
             return;
           this.mDestroyed = true;
 
-          if (!this.hasAttribute("disablehistory")) {
+          if (this.docShell && this.webNavigation.sessionHistory) {
             var os = Components.classes["@mozilla.org/observer-service;1"]
                                .getService(Components.interfaces.nsIObserverService);
             try {
               os.removeObserver(this, "browser:purge-session-history");
             } catch (ex) {
               // It's not clear why this sometimes throws an exception.
             }
           }