Merge m-c to inbound, a=merge
authorWes Kocher <wkocher@mozilla.com>
Thu, 20 Apr 2017 15:55:31 -0700
changeset 566347 4002d5c4ca3e6f92c3fef5df4dd7a0ec139618c8
parent 566346 a2c9cf2db1e41f369fe7e1edf854317a9bb4ed23 (current diff)
parent 566156 8b854986038cf3f3f240697e27ef48ea65914c13 (diff)
child 566348 7e27cbd5c6990eb363ca5277e01b05c70fe3d705
push id55180
push userjjong@mozilla.com
push dateFri, 21 Apr 2017 09:36:13 +0000
reviewersmerge
milestone55.0a1
Merge m-c to inbound, a=merge MozReview-Commit-ID: HfJqvXF2Ux7
browser/extensions/formautofill/content/editProfile.css
browser/themes/shared/toolbarbuttons.inc.css
mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
mobile/android/chrome/content/browser.js
tools/lint/eslint/eslint-plugin-mozilla/tests/test-run-all.js
--- a/browser/components/customizableui/CustomizableUI.jsm
+++ b/browser/components/customizableui/CustomizableUI.jsm
@@ -1487,17 +1487,18 @@ var CustomizableUIInternal = {
                                      aWidget.id);
       }
     } else if (aWidget.type == "view") {
       let ownerWindow = aNode.ownerGlobal;
       let area = this.getPlacementOfWidget(aNode.id).area;
       let anchor = aNode;
       if (area != CustomizableUI.AREA_PANEL) {
         let wrapper = this.wrapWidget(aWidget.id).forWindow(ownerWindow);
-        if (wrapper && wrapper.anchor) {
+
+        if (wrapper && !wrapper.overflowed && wrapper.anchor) {
           this.hidePanelForNode(aNode);
           anchor = wrapper.anchor;
         }
       }
       ownerWindow.PanelUI.showSubView(aWidget.viewId, anchor, area);
     }
   },
 
@@ -4113,17 +4114,19 @@ OverflowableToolbar.prototype = {
 
   show() {
     if (this._panel.state == "open") {
       return Promise.resolve();
     }
     return new Promise(resolve => {
       let doc = this._panel.ownerDocument;
       this._panel.hidden = false;
-      let contextMenu = doc.getElementById(this._panel.getAttribute("context"));
+      let mainViewId = this._panel.querySelector("panelmultiview").getAttribute("mainViewId");
+      let mainView = doc.getElementById(mainViewId);
+      let contextMenu = doc.getElementById(mainView.getAttribute("context"));
       gELS.addSystemEventListener(contextMenu, "command", this, true);
       let anchor = doc.getAnonymousElementByAttribute(this._chevron, "class", "toolbarbutton-icon");
       this._panel.openPopup(anchor || this._chevron);
       this._chevron.open = true;
 
       let overflowableToolbarInstance = this;
       this._panel.addEventListener("popupshown", function(aEvent) {
         this.addEventListener("dragover", overflowableToolbarInstance);
--- a/browser/components/customizableui/content/panelUI.inc.xul
+++ b/browser/components/customizableui/content/panelUI.inc.xul
@@ -372,23 +372,27 @@
               label="&customizeMenu.addMoreItems.label;"/>
   </menupopup>
 </panel>
 
 <panel id="widget-overflow"
        role="group"
        type="arrow"
        noautofocus="true"
-       context="toolbar-context-menu"
        position="bottomcenter topright"
        hidden="true">
-  <vbox id="widget-overflow-scroller">
-    <vbox id="widget-overflow-list" class="widget-overflow-list"
-          overflowfortoolbar="nav-bar"/>
-  </vbox>
+  <panelmultiview mainViewId="widget-overflow-mainView">
+    <panelview id="widget-overflow-mainView"
+               context="toolbar-context-menu">
+      <vbox id="widget-overflow-scroller">
+        <vbox id="widget-overflow-list" class="widget-overflow-list"
+              overflowfortoolbar="nav-bar"/>
+      </vbox>
+    </panelview>
+  </panelmultiview>
 </panel>
 
 <panel id="customization-tipPanel"
        type="arrow"
        flip="none"
        side="left"
        position="leftcenter topright"
        noautohide="true"
--- a/browser/components/customizableui/content/panelUI.js
+++ b/browser/components/customizableui/content/panelUI.js
@@ -426,18 +426,19 @@ const PanelUI = {
       return;
     }
 
     if (!aAnchor) {
       Cu.reportError("Expected an anchor when opening subview with id: " + aViewId);
       return;
     }
 
-    if (aPlacementArea == CustomizableUI.AREA_PANEL) {
-      this.multiView.showSubView(aViewId, aAnchor);
+    let container = aAnchor.closest("panelmultiview");
+    if (container) {
+      container.showSubView(aViewId, aAnchor);
     } else if (!aAnchor.open) {
       aAnchor.open = true;
 
       let tempPanel = document.createElement("panel");
       tempPanel.setAttribute("type", "arrow");
       tempPanel.setAttribute("id", "customizationui-widget-panel");
       tempPanel.setAttribute("class", "cui-widget-panel");
       tempPanel.setAttribute("viewId", aViewId);
--- a/browser/components/customizableui/content/panelUI.xml
+++ b/browser/components/customizableui/content/panelUI.xml
@@ -173,16 +173,24 @@
       </method>
 
       <method name="showSubView">
         <parameter name="aViewId"/>
         <parameter name="aAnchor"/>
         <body><![CDATA[
           Task.spawn(function*() {
             let viewNode = this.querySelector("#" + aViewId);
+            if (!viewNode) {
+              viewNode = document.getElementById(aViewId);
+              if (viewNode) {
+                this._subViews.appendChild(viewNode);
+              } else {
+                throw new Error(`Subview ${aViewId} doesn't exist!`);
+              }
+            }
             viewNode.setAttribute("current", true);
             // Emit the ViewShowing event so that the widget definition has a chance
             // to lazily populate the subview with things.
             let detail = {
               blockers: new Set(),
               addBlocker(aPromise) {
                 this.blockers.add(aPromise);
               },
--- a/browser/components/customizableui/test/browser.ini
+++ b/browser/components/customizableui/test/browser.ini
@@ -144,13 +144,14 @@ tags = fullscreen
 skip-if = os == "mac"
 [browser_1087303_button_preferences.js]
 [browser_1089591_still_customizable_after_reset.js]
 [browser_1096763_seen_widgets_post_reset.js]
 [browser_1161838_inserted_new_default_buttons.js]
 [browser_bootstrapped_custom_toolbar.js]
 [browser_customizemode_contextmenu_menubuttonstate.js]
 [browser_exit_background_customize_mode.js]
+[browser_overflow_use_subviews.js]
 [browser_panel_toggle.js]
 [browser_panelUINotifications.js]
 [browser_switch_to_customize_mode.js]
 [browser_synced_tabs_menu.js]
 [browser_check_tooltips_in_navbar.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/customizableui/test/browser_overflow_use_subviews.js
@@ -0,0 +1,69 @@
+"use strict";
+
+const kOverflowPanel = document.getElementById("widget-overflow");
+
+var gOriginalWidth;
+registerCleanupFunction(function*() {
+  kOverflowPanel.removeAttribute("animate");
+  window.resizeTo(gOriginalWidth, window.outerHeight);
+  CustomizableUI.reset();
+});
+
+/**
+ * This checks that subview-compatible items show up as subviews rather than
+ * re-anchored panels. If we ever remove the character encoding widget, please
+ * replace this test with another subview - don't remove it.
+ */
+add_task(async function check_character_encoding_subview_in_overflow() {
+  kOverflowPanel.setAttribute("animate", "false");
+  gOriginalWidth = window.outerWidth;
+
+  CustomizableUI.addWidgetToArea("developer-button", CustomizableUI.AREA_NAVBAR);
+
+  let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR);
+  ok(!navbar.hasAttribute("overflowing"), "Should start with a non-overflowing toolbar.");
+  window.resizeTo(400, window.outerHeight);
+
+  await waitForCondition(() => navbar.hasAttribute("overflowing"));
+
+  let chevron = document.getElementById("nav-bar-overflow-button");
+  let shownPanelPromise = promisePanelElementShown(window, kOverflowPanel);
+  chevron.click();
+  await shownPanelPromise;
+
+  let developerView = document.getElementById("PanelUI-developer");
+  let button = document.getElementById("developer-button");
+  let subviewShownPromise = subviewShown(developerView);
+  button.click();
+  await subviewShownPromise;
+  is(developerView.closest("panel"), kOverflowPanel, "Should be inside the panel");
+  kOverflowPanel.hidePopup();
+  await Promise.resolve(); // wait for popup to hide fully.
+
+  CustomizableUI.reset();
+});
+
+/**
+ * This checks that non-subview-compatible items still work correctly.
+ * Ideally we should make the downloads panel and bookmarks/library item
+ * proper subview items, then this test can go away, and potentially we can
+ * simplify some of the subview anchoring code.
+ */
+add_task(async function check_downloads_panel_in_overflow() {
+  let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR);
+  ok(navbar.hasAttribute("overflowing"), "Should still be overflowing");
+  let chevron = document.getElementById("nav-bar-overflow-button");
+  let shownPanelPromise = promisePanelElementShown(window, kOverflowPanel);
+  chevron.click();
+  await shownPanelPromise;
+
+  let button = document.getElementById("downloads-button");
+  button.click();
+  await waitForCondition(() => {
+    let panel = document.getElementById("downloadsPanel");
+    return panel && panel.state != "closed";
+  });
+  let downloadsPanel = document.getElementById("downloadsPanel");
+  isnot(downloadsPanel.state, "closed", "Should be attempting to show the downloads panel.");
+  downloadsPanel.hidePopup();
+});
deleted file mode 100644
--- a/browser/extensions/formautofill/content/editProfile.css
+++ /dev/null
@@ -1,95 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-body {
-  font-size: 1rem;
-  /* body needs padding until the edit profile dialog could be loaded as a
-     stacked subdialog */
-  padding: 2em;
-}
-
-form,
-label,
-div {
-  display: flex;
-}
-
-form {
-  flex-wrap: wrap;
-}
-
-label {
-  margin: 0 0 0.5em;
-}
-
-label > span {
-  flex: 0 0 8em;
-  padding-inline-end: 0.5em;
-  align-self: center;
-  text-align: end;
-}
-
-input,
-select {
-  flex: 1 0 auto;
-  width: 9em;
-}
-
-option {
-  padding: 6px;
-}
-
-textarea {
-  resize: none;
-}
-
-button {
-  padding: 3px 2em;
-}
-
-#country-container {
-  width: 15em;
-}
-
-#first-name-container,
-#middle-name-container,
-#address-level1-container,
-#postal-code-container,
-#country-container {
-  flex: 0 1 50%;
-}
-
-#last-name-container,
-#organization-container,
-#street-address-container,
-#address-level2-container,
-#email-container,
-#tel-container,
-#controls-container {
-  flex: 0 1 100%;
-}
-
-#controls-container {
-  justify-content: end;
-}
-
-#last-name,
-#organization,
-#address-level2,
-#tel{
-  flex: 0 0 auto;
-  width: calc(50% - 10em);
-}
-
-#street-address,
-#email {
-  flex: 1 0 auto;
-}
-
-#first-name-container,
-#middle-name-container,
-#last-name-container {
-  /* Hide until we support names */
-  display: none;
-}
--- a/browser/extensions/formautofill/content/editProfile.xhtml
+++ b/browser/extensions/formautofill/content/editProfile.xhtml
@@ -5,17 +5,18 @@
 <!DOCTYPE html>
 <html xmlns="http://www.w3.org/1999/xhtml">
 <head>
   <title>Profile Autofill - Edit Profile</title>
   <!-- common.css and dialog.css need to be included until this file can be
      - loaded as a stacked subdialog. -->
   <link rel="stylesheet" href="chrome://global/skin/in-content/common.css" />
   <link rel="stylesheet" href="chrome://browser/skin/preferences/in-content/dialog.css" />
-  <link rel="stylesheet" href="chrome://formautofill/content/editProfile.css" />
+  <link rel="stylesheet" href="chrome://formautofill-shared/skin/editProfile.css" />
+  <link rel="stylesheet" href="chrome://formautofill/skin/editProfile.css" />
   <script src="chrome://formautofill/content/editProfile.js"></script>
 </head>
 <body>
   <form>
     <label id="first-name-container">
       <span>First Name</span>
       <input id="first-name" type="text"/>
     </label>
new file mode 100644
--- /dev/null
+++ b/browser/extensions/formautofill/skin/linux/editProfile.css
@@ -0,0 +1,5 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* Linux specific rules */
new file mode 100644
--- /dev/null
+++ b/browser/extensions/formautofill/skin/osx/editProfile.css
@@ -0,0 +1,5 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* OSX specific rules */
new file mode 100644
--- /dev/null
+++ b/browser/extensions/formautofill/skin/shared/editProfile.css
@@ -0,0 +1,95 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+body {
+  font-size: 1rem;
+  /* body needs padding until the edit profile dialog could be loaded as a
+     stacked subdialog */
+  padding: 2em;
+}
+
+form,
+label,
+div {
+  display: flex;
+}
+
+form {
+  flex-wrap: wrap;
+}
+
+label {
+  margin: 0 0 0.5em;
+}
+
+label > span {
+  flex: 0 0 8em;
+  padding-inline-end: 0.5em;
+  align-self: center;
+  text-align: end;
+}
+
+input,
+select {
+  flex: 1 0 auto;
+  width: 9em;
+}
+
+option {
+  padding: 6px;
+}
+
+textarea {
+  resize: none;
+}
+
+button {
+  padding: 3px 2em;
+}
+
+#country-container {
+  width: 15em;
+}
+
+#first-name-container,
+#middle-name-container,
+#address-level1-container,
+#postal-code-container,
+#country-container {
+  flex: 0 1 50%;
+}
+
+#last-name-container,
+#organization-container,
+#street-address-container,
+#address-level2-container,
+#email-container,
+#tel-container,
+#controls-container {
+  flex: 0 1 100%;
+}
+
+#controls-container {
+  justify-content: end;
+}
+
+#last-name,
+#organization,
+#address-level2,
+#tel{
+  flex: 0 0 auto;
+  width: calc(50% - 10em);
+}
+
+#street-address,
+#email {
+  flex: 1 0 auto;
+}
+
+#first-name-container,
+#middle-name-container,
+#last-name-container {
+  /* Hide until we support names */
+  display: none;
+}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/formautofill/skin/windows/editProfile.css
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* The save button should be on the left and cancel on the right for Windows */
+#save {
+  order: 0;
+}
+
+#cancel {
+  order: 1;
+}
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -548,17 +548,17 @@ menuitem.bookmark-item {
   list-style-image: url("moz-icon://stock/gtk-go-forward-ltr?size=menu") !important;
 }
 .unified-nav-forward[_moz-menuactive]:-moz-locale-dir(rtl) {
   list-style-image: url("moz-icon://stock/gtk-go-forward-rtl?size=menu") !important;
 }
 
 /* Menu panel buttons */
 
-%include ../shared/toolbarbuttons.inc.css
+%include ../shared/toolbarbutton-icons.inc.css
 %include ../shared/menupanel.inc.css
 
 #main-window:not([customizing]) .toolbarbutton-1[disabled=true] > .toolbarbutton-icon,
 #main-window:not([customizing]) .toolbarbutton-1[disabled=true] > .toolbarbutton-menu-dropmarker,
 #main-window:not([customizing]) .toolbarbutton-1[disabled=true] > .toolbarbutton-menubutton-dropmarker,
 #main-window:not([customizing]) .toolbarbutton-1[disabled=true] > .toolbarbutton-menubutton-button > .toolbarbutton-icon,
 #main-window:not([customizing]) .toolbarbutton-1 > .toolbarbutton-menubutton-button[disabled=true] > .toolbarbutton-icon {
   opacity: 0.4;
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -721,17 +721,17 @@ toolbar .toolbarbutton-1 > .toolbarbutto
   margin-top: -4px;
   margin-bottom: -4px;
   margin-inline-start: 5px;
   margin-inline-end: 1px;
   padding-inline-start: 5px;
   -moz-box-align: center;
 }
 
-%include ../shared/toolbarbuttons.inc.css
+%include ../shared/toolbarbutton-icons.inc.css
 %include ../shared/menupanel.inc.css
 
 @media not all and (min-resolution: 1.1dppx) {
   #back-button:hover:active:not([disabled="true"]) {
     -moz-image-region: rect(18px, 36px, 36px, 18px);
   }
 
   #forward-button:hover:active:not([disabled="true"]) {
rename from browser/themes/shared/toolbarbuttons.inc.css
rename to browser/themes/shared/toolbarbutton-icons.inc.css
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -573,17 +573,17 @@ menuitem.bookmark-item {
 
 .bookmark-item[cutting] > .toolbarbutton-text,
 .bookmark-item[cutting] > .menu-iconic-left > .menu-iconic-text {
   opacity: 0.7;
 }
 
 /* ::::: primary toolbar buttons ::::: */
 
-%include ../shared/toolbarbuttons.inc.css
+%include ../shared/toolbarbutton-icons.inc.css
 
 #main-window:not([customizing]) .toolbarbutton-1[disabled=true] > .toolbarbutton-icon,
 #main-window:not([customizing]) .toolbarbutton-1[disabled=true] > .toolbarbutton-menu-dropmarker,
 #main-window:not([customizing]) .toolbarbutton-1[disabled=true] > .toolbarbutton-menubutton-dropmarker,
 #main-window:not([customizing]) .toolbarbutton-1[disabled=true] > .toolbarbutton-menubutton-button > .toolbarbutton-icon,
 #main-window:not([customizing]) .toolbarbutton-1 > .toolbarbutton-menubutton-button[disabled=true] > .toolbarbutton-icon {
   opacity: .4;
 }
--- a/devtools/client/commandline/test/browser_gcli_telemetry.js
+++ b/devtools/client/commandline/test/browser_gcli_telemetry.js
@@ -74,49 +74,52 @@ function* spawnTest() {
  * it.
  * Store all recordings in Telemetry.telemetryInfo.
  * @return {Telemetry}
  */
 function loadTelemetryAndRecordLogs() {
   info("Mock the Telemetry log function to record logged information");
 
   let Telemetry = require("devtools/client/shared/telemetry");
-
   Telemetry.prototype.telemetryInfo = {};
   Telemetry.prototype._oldlog = Telemetry.prototype.log;
   Telemetry.prototype.log = function (histogramId, value) {
     if (!this.telemetryInfo) {
       // Telemetry instance still in use after stopRecordingTelemetryLogs
       return;
     }
     if (histogramId) {
       if (!this.telemetryInfo[histogramId]) {
         this.telemetryInfo[histogramId] = [];
       }
       this.telemetryInfo[histogramId].push(value);
     }
   };
+  Telemetry.prototype._oldlogScalar = Telemetry.prototype.logScalar;
+  Telemetry.prototype.logScalar = Telemetry.prototype.log;
   Telemetry.prototype._oldlogKeyed = Telemetry.prototype.logKeyed;
   Telemetry.prototype.logKeyed = function (histogramId, key, value) {
     this.log(`${histogramId}|${key}`, value);
   };
 
   return Telemetry;
 }
 
 /**
  * Stop recording the Telemetry logs and put back the utils as it was before.
  * @param {Telemetry} Required Telemetry
  *        Telemetry object that needs to be stopped.
  */
 function stopRecordingTelemetryLogs(Telemetry) {
   info("Stopping Telemetry");
   Telemetry.prototype.log = Telemetry.prototype._oldlog;
+  Telemetry.prototype.logScalar = Telemetry.prototype._oldlogScalar;
   Telemetry.prototype.logKeyed = Telemetry.prototype._oldlogKeyed;
   delete Telemetry.prototype._oldlog;
+  delete Telemetry.prototype._oldlogScalar;
   delete Telemetry.prototype._oldlogKeyed;
   delete Telemetry.prototype.telemetryInfo;
 }
 
 function checkTelemetryResults(results) {
   let prefix = COMMAND_HISTOGRAM_ID + "|";
   let keys = Object.keys(results).filter(result => {
     return result.startsWith(prefix);
--- a/devtools/client/framework/test/shared-head.js
+++ b/devtools/client/framework/test/shared-head.js
@@ -579,34 +579,38 @@ function loadTelemetryAndRecordLogs() {
     }
     if (histogramId) {
       if (!this.telemetryInfo[histogramId]) {
         this.telemetryInfo[histogramId] = [];
       }
       this.telemetryInfo[histogramId].push(value);
     }
   };
+  Telemetry.prototype._oldlogScalar = Telemetry.prototype.logScalar;
+  Telemetry.prototype.logScalar = Telemetry.prototype.log;
   Telemetry.prototype._oldlogKeyed = Telemetry.prototype.logKeyed;
   Telemetry.prototype.logKeyed = function (histogramId, key, value) {
     this.log(`${histogramId}|${key}`, value);
   };
 
   return Telemetry;
 }
 
 /**
  * Stop recording the Telemetry logs and put back the utils as it was before.
  * @param {Telemetry} Required Telemetry
  *        Telemetry object that needs to be stopped.
  */
 function stopRecordingTelemetryLogs(Telemetry) {
   info("Stopping Telemetry");
   Telemetry.prototype.log = Telemetry.prototype._oldlog;
+  Telemetry.prototype.logScalar = Telemetry.prototype._oldlogScalar;
   Telemetry.prototype.logKeyed = Telemetry.prototype._oldlogKeyed;
   delete Telemetry.prototype._oldlog;
+  delete Telemetry.prototype._oldlogScalar;
   delete Telemetry.prototype._oldlogKeyed;
   delete Telemetry.prototype.telemetryInfo;
 }
 
 /**
  * Clean the logical clipboard content. This method only clears the OS clipboard on
  * Windows (see Bug 666254).
  */
--- a/devtools/client/inspector/test/browser_inspector_menu-02-copy-items.js
+++ b/devtools/client/inspector/test/browser_inspector_menu-02-copy-items.js
@@ -1,16 +1,18 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
 http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
 // Test that the various copy items in the context menu works correctly.
 
 const TEST_URL = URL_ROOT + "doc_inspector_menu.html";
+const SELECTOR_UNIQUE = "devtools.copy.unique.css.selector.opened";
+const SELECTOR_FULL = "devtools.copy.full.css.selector.opened";
 const COPY_ITEMS_TEST_DATA = [
   {
     desc: "copy inner html",
     id: "node-menu-copyinner",
     selector: "[data-id=\"copy\"]",
     text: "Paragraph for testing copy",
   },
   {
@@ -21,35 +23,65 @@ const COPY_ITEMS_TEST_DATA = [
   },
   {
     desc: "copy unique selector",
     id: "node-menu-copyuniqueselector",
     selector: "[data-id=\"copy\"]",
     text: "body > div:nth-child(1) > p:nth-child(2)",
   },
   {
-    desc: "copy css path",
+    desc: "copy CSS path",
     id: "node-menu-copycsspath",
     selector: "[data-id=\"copy\"]",
     text: "html body div p",
   },
   {
     desc: "copy image data uri",
     id: "node-menu-copyimagedatauri",
     selector: "#copyimage",
     text: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABC" +
       "AAAAAA6fptVAAAACklEQVQYV2P4DwABAQEAWk1v8QAAAABJRU5ErkJggg==",
   },
 ];
 
 add_task(function* () {
+  let Telemetry = loadTelemetryAndRecordLogs();
   let { inspector } = yield openInspectorForURL(TEST_URL);
+
   for (let {desc, id, selector, text} of COPY_ITEMS_TEST_DATA) {
     info("Testing " + desc);
     yield selectNode(selector, inspector);
 
     let allMenuItems = openContextMenuAndGetAllItems(inspector);
     let item = allMenuItems.find(i => i.id === id);
     ok(item, "The popup has a " + desc + " menu item.");
 
     yield waitForClipboardPromise(() => item.click(), text);
   }
+
+  checkTelemetryResults(Telemetry);
+  stopRecordingTelemetryLogs(Telemetry);
 });
+
+function checkTelemetryResults(Telemetry) {
+  let data = Telemetry.prototype.telemetryInfo;
+  let results = new Map();
+
+  for (let key in data) {
+    if (key.toLowerCase() === key) {
+      let pings = data[key].length;
+
+      results.set(key, pings);
+    }
+  }
+
+  is(results.size, 2, "The correct number of scalars were logged");
+
+  let pings = checkPings(SELECTOR_UNIQUE, results);
+  is(pings, 1, `${SELECTOR_UNIQUE} has just 1 ping`);
+
+  pings = checkPings(SELECTOR_FULL, results);
+  is(pings, 1, `${SELECTOR_FULL} has just 1 ping`);
+}
+
+function checkPings(scalarId, results) {
+  return results.get(scalarId);
+}
--- a/devtools/client/locales/en-US/responsiveUI.properties
+++ b/devtools/client/locales/en-US/responsiveUI.properties
@@ -6,17 +6,16 @@
 # which is available from the Web Developer sub-menu -> 'Responsive Mode'.
 #
 # The correct localization of this file might be to keep it in
 # English, or another language commonly spoken among web developers.
 # You want to make that choice consistent across the developer tools.
 # A good criteria is the language in which you'd find the best
 # documentation on web development on the web.
 
-
 # LOCALIZATION NOTE  (responsiveUI.rotate2): tooltip of the rotate button.
 responsiveUI.rotate2=Rotate
 
 # LOCALIZATION NOTE  (responsiveUI.screenshot): tooltip of the screenshot button.
 responsiveUI.screenshot=Screenshot
 
 # LOCALIZATION NOTE  (responsiveUI.userAgentPlaceholder): placeholder for the user agent input.
 responsiveUI.userAgentPlaceholder=Custom User Agent
@@ -62,8 +61,18 @@ responsiveUI.resizerTooltip=Use the Cont
 
 # LOCALIZATION NOTE (responsiveUI.needReload): notification that appears
 # when touch events are enabled
 responsiveUI.needReload=If touch event listeners have been added earlier, the page needs to be reloaded.
 responsiveUI.notificationReload=Reload
 responsiveUI.notificationReload_accesskey=R
 responsiveUI.dontShowReloadNotification=Never show again
 responsiveUI.dontShowReloadNotification_accesskey=N
+
+# LOCALIZATION NOTE (responsiveUI.newVersionUserDisabled): notification that appears
+# when old RDM is displayed because the user has disabled new RDM.
+responsiveUI.newVersionUserDisabled=A new version of Responsive Design Mode is available, but it appears to be disabled. Please enable it and provide feedback, as this version will be removed.
+# LOCALIZATION NOTE (responsiveUI.newVersionE10sDisabled): notification that appears
+# when old RDM is displayed because e10s is disabled.
+responsiveUI.newVersionE10sDisabled=A new version of Responsive Design Mode is available, but it requires multi-process mode, which is currently disabled. Please enable it and provide feedback, as this version will be removed.
+# LOCALIZATION NOTE (responsiveUI.newVersionEnableAndRestart): button text in notification
+# to enable new RDM itself or e10s as a prerequisite for new RDM.
+responsiveUI.newVersionEnableAndRestart=Enable and Restart
\ No newline at end of file
--- a/devtools/client/responsivedesign/responsivedesign.jsm
+++ b/devtools/client/responsivedesign/responsivedesign.jsm
@@ -9,32 +9,37 @@ const Cu = Components.utils;
 const { loader, require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
 const { LocalizationHelper } = require("devtools/shared/l10n");
 const { Task } = require("devtools/shared/task");
 const Services = require("Services");
 const EventEmitter = require("devtools/shared/event-emitter");
 
 loader.lazyImporter(this, "SystemAppProxy",
                     "resource://gre/modules/SystemAppProxy.jsm");
+loader.lazyImporter(this, "BrowserUtils",
+                    "resource://gre/modules/BrowserUtils.jsm");
 loader.lazyRequireGetter(this, "Telemetry", "devtools/client/shared/telemetry");
 loader.lazyRequireGetter(this, "showDoorhanger",
                          "devtools/client/shared/doorhanger", true);
 loader.lazyRequireGetter(this, "TouchEventSimulator",
                          "devtools/shared/touch/simulator", true);
 loader.lazyRequireGetter(this, "flags",
                          "devtools/shared/flags");
 loader.lazyRequireGetter(this, "EmulationFront",
                          "devtools/shared/fronts/emulation", true);
 loader.lazyRequireGetter(this, "DebuggerClient",
                          "devtools/shared/client/main", true);
 loader.lazyRequireGetter(this, "DebuggerServer",
                          "devtools/server/main", true);
+loader.lazyRequireGetter(this, "system", "devtools/shared/system");
 
 this.EXPORTED_SYMBOLS = ["ResponsiveUIManager"];
 
+const NEW_RDM_ENABLED = "devtools.responsive.html.enabled";
+
 const MIN_WIDTH = 50;
 const MIN_HEIGHT = 50;
 
 const MAX_WIDTH = 10000;
 const MAX_HEIGHT = 10000;
 
 const SLOW_RATIO = 6;
 const ROUND_RATIO = 10;
@@ -131,17 +136,17 @@ var Manager = {
 };
 
 EventEmitter.decorate(Manager);
 
 // If the new HTML RDM UI is enabled and e10s is enabled by default (e10s is required for
 // the new HTML RDM UI to function), delegate the ResponsiveUIManager API over to that
 // tool instead.  Performing this delegation here allows us to contain the pref check to a
 // single place.
-if (Services.prefs.getBoolPref("devtools.responsive.html.enabled") &&
+if (Services.prefs.getBoolPref(NEW_RDM_ENABLED) &&
     Services.appinfo.browserTabsRemoteAutostart) {
   let { ResponsiveUIManager } =
     require("devtools/client/responsive.html/manager");
   this.ResponsiveUIManager = ResponsiveUIManager;
 } else {
   this.ResponsiveUIManager = Manager;
 }
 
@@ -261,16 +266,18 @@ ResponsiveUI.prototype = {
     // Hook to display promotional Developer Edition doorhanger.
     // Only displayed once.
     showDoorhanger({
       window: this.mainWindow,
       type: "deveditionpromo",
       anchor: this.chromeDoc.querySelector("#content")
     });
 
+    this.showNewUINotification();
+
     // Notify that responsive mode is on.
     this._telemetry.toolOpened("responsive");
     ResponsiveUIManager.emit("on", { tab: this.tab });
   }),
 
   connectToServer: Task.async(function* () {
     if (!DebuggerServer.initialized) {
       DebuggerServer.init();
@@ -394,22 +401,24 @@ ResponsiveUI.prototype = {
       this.touchEventSimulator.stop();
     }
 
     yield this.client.close();
     this.client = this.emulationFront = null;
 
     this._telemetry.toolClosed("responsive");
 
-    if (this.tab.linkedBrowser.messageManager) {
+    if (this.tab.linkedBrowser && this.tab.linkedBrowser.messageManager) {
       let stopped = this.waitForMessage("ResponsiveMode:Stop:Done");
       this.tab.linkedBrowser.messageManager.sendAsyncMessage("ResponsiveMode:Stop");
       yield stopped;
     }
 
+    this.hideNewUINotification();
+
     this.inited = null;
     ResponsiveUIManager.emit("off", { tab: this.tab });
   }),
 
   waitForMessage(message) {
     return new Promise(resolve => {
       let listener = () => {
         this.mm.removeMessageListener(message, listener);
@@ -678,16 +687,79 @@ ResponsiveUI.prototype = {
     homeButton.addEventListener("mouseup", () => {
       SystemAppProxy.dispatchKeyboardEvent("keyup", { key: "Home" });
     });
     bottomToolbar.appendChild(homeButton);
     this.bottomToolbar = bottomToolbar;
     this.container.appendChild(bottomToolbar);
   },
 
+  showNewUINotification() {
+    let nbox = this.mainWindow.gBrowser.getNotificationBox(this.browser);
+
+    // One reason we might be using old RDM is that the user explcitly disabled new RDM.
+    // We should encourage them to use the new one, since the old one will be removed.
+    if (Services.prefs.prefHasUserValue(NEW_RDM_ENABLED) &&
+        !Services.prefs.getBoolPref(NEW_RDM_ENABLED)) {
+      let buttons = [{
+        label: this.strings.GetStringFromName("responsiveUI.newVersionEnableAndRestart"),
+        callback: () => {
+          Services.prefs.setBoolPref(NEW_RDM_ENABLED, true);
+          BrowserUtils.restartApplication();
+        },
+      }];
+      nbox.appendNotification(
+        this.strings.GetStringFromName("responsiveUI.newVersionUserDisabled"),
+        "responsive-ui-new-version-user-disabled",
+        null,
+        nbox.PRIORITY_INFO_LOW,
+        buttons
+      );
+      return;
+    }
+
+    // Only show a notification about the new RDM UI on channels where there is an e10s
+    // switch in the preferences UI (Dev. Ed, Nightly).  On other channels, it is less
+    // clear how a user would proceed here, so don't show a message.
+    if (!system.constants.E10S_TESTING_ONLY) {
+      return;
+    }
+
+    let buttons = [{
+      label: this.strings.GetStringFromName("responsiveUI.newVersionEnableAndRestart"),
+      callback: () => {
+        Services.prefs.setBoolPref("browser.tabs.remote.autostart", true);
+        Services.prefs.setBoolPref("browser.tabs.remote.autostart.2", true);
+        BrowserUtils.restartApplication();
+      },
+    }];
+    nbox.appendNotification(
+      this.strings.GetStringFromName("responsiveUI.newVersionE10sDisabled"),
+      "responsive-ui-new-version-e10s-disabled",
+      null,
+      nbox.PRIORITY_INFO_LOW,
+      buttons
+    );
+  },
+
+  hideNewUINotification() {
+    if (!this.mainWindow.gBrowser || !this.mainWindow.gBrowser.getNotificationBox) {
+      return;
+    }
+    let nbox = this.mainWindow.gBrowser.getNotificationBox(this.browser);
+    let n = nbox.getNotificationWithValue("responsive-ui-new-version-user-disabled");
+    if (n) {
+      n.close();
+    }
+    n = nbox.getNotificationWithValue("responsive-ui-new-version-e10s-disabled");
+    if (n) {
+      n.close();
+    }
+  },
+
   /**
    * Validate and apply any user input on the editable menulist
    */
   handleManualInput: function () {
     let userInput = this.menulist.inputField.value;
     let value = INPUT_PARSER.exec(userInput);
     let selectedPreset = this.menuitems.get(this.selectedItem);
 
--- a/devtools/client/shared/components/reps/reps.js
+++ b/devtools/client/shared/components/reps/reps.js
@@ -2,17 +2,17 @@
 	if(typeof exports === 'object' && typeof module === 'object')
 		module.exports = factory(require("devtools/client/shared/vendor/react"));
 	else if(typeof define === 'function' && define.amd)
 		define(["devtools/client/shared/vendor/react"], factory);
 	else {
 		var a = typeof exports === 'object' ? factory(require("devtools/client/shared/vendor/react")) : factory(root["devtools/client/shared/vendor/react"]);
 		for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i];
 	}
-})(this, function(__WEBPACK_EXTERNAL_MODULE_3__) {
+})(this, function(__WEBPACK_EXTERNAL_MODULE_4__) {
 return /******/ (function(modules) { // webpackBootstrap
 /******/ 	// The module cache
 /******/ 	var installedModules = {};
 /******/
 /******/ 	// The require function
 /******/ 	function __webpack_require__(moduleId) {
 /******/
 /******/ 		// Check if module is in cache
@@ -50,27 +50,28 @@ return /******/ (function(modules) { // 
 /******/ 	return __webpack_require__(0);
 /******/ })
 /************************************************************************/
 /******/ ([
 /* 0 */
 /***/ function(module, exports, __webpack_require__) {
 
 	const { MODE } = __webpack_require__(1);
-	const { REPS } = __webpack_require__(2);
+	const { REPS, getRep } = __webpack_require__(2);
 	const {
 	  createFactories,
 	  parseURLEncodedText,
 	  parseURLParams,
 	  getSelectableInInspectorGrips,
 	  maybeEscapePropertyName
-	} = __webpack_require__(4);
+	} = __webpack_require__(3);
 	
 	module.exports = {
 	  REPS,
+	  getRep,
 	  MODE,
 	  createFactories,
 	  maybeEscapePropertyName,
 	  parseURLEncodedText,
 	  parseURLParams,
 	  getSelectableInInspectorGrips
 	};
 
@@ -85,20 +86,17 @@ return /******/ (function(modules) { // 
 	    LONG: Symbol("LONG")
 	  }
 	};
 
 /***/ },
 /* 2 */
 /***/ function(module, exports, __webpack_require__) {
 
-	const React = __webpack_require__(3);
-	
-	const { isGrip } = __webpack_require__(4);
-	const { MODE } = __webpack_require__(1);
+	const { isGrip } = __webpack_require__(3);
 	
 	// Load all existing rep templates
 	const Undefined = __webpack_require__(6);
 	const Null = __webpack_require__(7);
 	const StringRep = __webpack_require__(8);
 	const LongStringRep = __webpack_require__(9);
 	const Number = __webpack_require__(10);
 	const ArrayRep = __webpack_require__(11);
@@ -133,31 +131,24 @@ return /******/ (function(modules) { // 
 	let reps = [RegExp, StyleSheet, Event, DateTime, CommentNode, ElementNode, TextNode, Attribute, LongStringRep, Func, PromiseRep, ArrayRep, Document, Window, ObjectWithText, ObjectWithURL, ErrorRep, GripArray, GripMap, Grip, Undefined, Null, StringRep, Number, SymbolRep, InfinityRep, NaNRep];
 	
 	/**
 	 * Generic rep that is using for rendering native JS types or an object.
 	 * The right template used for rendering is picked automatically according
 	 * to the current value type. The value must be passed is as 'object'
 	 * property.
 	 */
-	const Rep = React.createClass({
-	  displayName: "Rep",
-	
-	  propTypes: {
-	    object: React.PropTypes.any,
-	    defaultRep: React.PropTypes.object,
-	    // @TODO Change this to Object.values once it's supported in Node's version of V8
-	    mode: React.PropTypes.oneOf(Object.keys(MODE).map(key => MODE[key]))
-	  },
-	
-	  render: function () {
-	    let rep = getRep(this.props.object, this.props.defaultRep);
-	    return rep(this.props);
-	  }
-	});
+	const Rep = function (props) {
+	  let {
+	    object,
+	    defaultRep
+	  } = props;
+	  let rep = getRep(object, defaultRep);
+	  return rep(props);
+	};
 	
 	// Helpers
 	
 	/**
 	 * Return a rep object that is responsible for rendering given
 	 * object.
 	 *
 	 * @param object {Object} Object to be rendered in the UI. This
@@ -181,24 +172,24 @@ return /******/ (function(modules) { // 
 	
 	  for (let i = 0; i < reps.length; i++) {
 	    let rep = reps[i];
 	    try {
 	      // supportsObject could return weight (not only true/false
 	      // but a number), which would allow to priorities templates and
 	      // support better extensibility.
 	      if (rep.supportsObject(object, type)) {
-	        return React.createFactory(rep.rep);
+	        return rep.rep;
 	      }
 	    } catch (err) {
 	      console.error(err);
 	    }
 	  }
 	
-	  return React.createFactory(defaultRep.rep);
+	  return defaultRep.rep;
 	}
 	
 	module.exports = {
 	  Rep,
 	  REPS: {
 	    ArrayRep,
 	    Attribute,
 	    CommentNode,
@@ -223,31 +214,27 @@ return /******/ (function(modules) { // 
 	    RegExp,
 	    Rep,
 	    StringRep,
 	    StyleSheet,
 	    SymbolRep,
 	    TextNode,
 	    Undefined,
 	    Window
-	  }
+	  },
+	  // Exporting for tests
+	  getRep
 	};
 
 /***/ },
 /* 3 */
-/***/ function(module, exports) {
-
-	module.exports = __WEBPACK_EXTERNAL_MODULE_3__;
-
-/***/ },
-/* 4 */
 /***/ function(module, exports, __webpack_require__) {
 
 	// Dependencies
-	const React = __webpack_require__(3);
+	const React = __webpack_require__(4);
 	
 	// Utils
 	const nodeConstants = __webpack_require__(5);
 	
 	/**
 	 * Create React factories for given arguments.
 	 * Example:
 	 *   const { Rep } = createFactories(require("./rep"));
@@ -316,22 +303,27 @@ return /******/ (function(modules) { // 
 	/**
 	 * Escape a string so that the result is viewable and valid JS.
 	 * Control characters, other invisibles, invalid characters,
 	 * backslash, and double quotes are escaped.  The resulting string is
 	 * surrounded by double quotes.
 	 *
 	 * @param {String} str
 	 *        the input
+	 * @param {Boolean} escapeWhitespace
+	 *        if true, TAB, CR, and NL characters will be escaped
 	 * @return {String} the escaped string
 	 */
-	function escapeString(str) {
+	function escapeString(str, escapeWhitespace) {
 	  return "\"" + str.replace(escapeRegexp, (match, offset) => {
 	    let c = match.charCodeAt(0);
 	    if (c in escapeMap) {
+	      if (!escapeWhitespace && (c === 9 || c === 0xa || c === 0xd)) {
+	        return match[0];
+	      }
 	      return escapeMap[c];
 	    }
 	    if (c >= 0xd800 && c <= 0xdfff) {
 	      // Find the full code point containing the surrogate, with a
 	      // special case for a trailing surrogate at the start of the
 	      // string.
 	      if (c >= 0xdc00 && offset > 0) {
 	        --offset;
@@ -490,28 +482,31 @@ return /******/ (function(modules) { // 
 	  };
 	}
 	
 	/**
 	 * Wrap the provided render() method of a rep in a try/catch block that will render a
 	 * fallback rep if the render fails.
 	 */
 	function wrapRender(renderMethod) {
-	  return function () {
+	  const wrappedFunction = function (props) {
 	    try {
-	      return renderMethod.call(this);
+	      return renderMethod.call(this, props);
 	    } catch (e) {
+	      console.error(e);
 	      return React.DOM.span({
 	        className: "objectBox objectBox-failure",
 	        title: "This object could not be rendered, " + "please file a bug on bugzilla.mozilla.org"
 	      },
 	      /* Labels have to be hardcoded for reps, see Bug 1317038. */
 	      "Invalid object");
 	    }
 	  };
+	  wrappedFunction.propTypes = renderMethod.propTypes;
+	  return wrappedFunction;
 	}
 	
 	/**
 	 * Get an array of all the items from the grip in parameter (including the grip itself)
 	 * which can be selected in the inspector.
 	 *
 	 * @param {Object} Grip
 	 * @return {Array} Flat array of the grips which can be selected in the inspector
@@ -594,34 +589,71 @@ return /******/ (function(modules) { // 
 	    }
 	
 	    return propertiesValues;
 	  }
 	
 	  return [];
 	}
 	
+	/**
+	 * Returns a new element wrapped with a component, props.objectLink if it exists,
+	 * or a span if there are multiple childs, or directly the child if only one is passed.
+	 *
+	 * @param {Object} props A Rep "props" object that may contain `objectLink`
+	 *                 and `object` properties.
+	 * @param {Object} config Object to pass as props to the `objectLink` component.
+	 * @param {...Element} children Elements to be wrapped with the `objectLink` component.
+	 * @return {Element} Element, wrapped or not, depending if `objectLink`
+	 *                   was supplied in props.
+	 */
+	function safeObjectLink(props, config, ...children) {
+	  const {
+	    objectLink,
+	    object
+	  } = props;
+	
+	  if (objectLink) {
+	    return objectLink(Object.assign({
+	      object
+	    }, config), ...children);
+	  }
+	
+	  if ((!config || Object.keys(config).length === 0) && children.length === 1) {
+	    return children[0];
+	  }
+	
+	  return React.DOM.span(config, ...children);
+	}
+	
 	module.exports = {
 	  createFactories,
 	  isGrip,
 	  cropString,
 	  rawCropString,
 	  sanitizeString,
 	  escapeString,
 	  wrapRender,
 	  cropMultipleLines,
 	  parseURLParams,
 	  parseURLEncodedText,
 	  getFileName,
 	  getURLDisplayString,
 	  getSelectableInInspectorGrips,
-	  maybeEscapePropertyName
+	  maybeEscapePropertyName,
+	  safeObjectLink
 	};
 
 /***/ },
+/* 4 */
+/***/ function(module, exports) {
+
+	module.exports = __WEBPACK_EXTERNAL_MODULE_4__;
+
+/***/ },
 /* 5 */
 /***/ function(module, exports) {
 
 	module.exports = {
 	  ELEMENT_NODE: 1,
 	  ATTRIBUTE_NODE: 2,
 	  TEXT_NODE: 3,
 	  CDATA_SECTION_NODE: 4,
@@ -643,1932 +675,1692 @@ return /******/ (function(modules) { // 
 	  DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC: 0x20
 	};
 
 /***/ },
 /* 6 */
 /***/ function(module, exports, __webpack_require__) {
 
 	// Dependencies
-	const React = __webpack_require__(3);
-	
-	const { wrapRender } = __webpack_require__(4);
+	const React = __webpack_require__(4);
+	
+	const { wrapRender } = __webpack_require__(3);
 	
 	// Shortcuts
 	const { span } = React.DOM;
 	
 	/**
 	 * Renders undefined value
 	 */
-	const Undefined = React.createClass({
-	  displayName: "UndefinedRep",
-	
-	  render: wrapRender(function () {
-	    return span({ className: "objectBox objectBox-undefined" }, "undefined");
-	  })
-	});
+	const Undefined = function () {
+	  return span({ className: "objectBox objectBox-undefined" }, "undefined");
+	};
 	
 	function supportsObject(object, type) {
 	  if (object && object.type && object.type == "undefined") {
 	    return true;
 	  }
 	
 	  return type == "undefined";
 	}
 	
 	// Exports from this module
 	
 	module.exports = {
-	  rep: Undefined,
-	  supportsObject: supportsObject
+	  rep: wrapRender(Undefined),
+	  supportsObject
 	};
 
 /***/ },
 /* 7 */
 /***/ function(module, exports, __webpack_require__) {
 
 	// Dependencies
-	const React = __webpack_require__(3);
-	
-	const { wrapRender } = __webpack_require__(4);
+	const React = __webpack_require__(4);
+	
+	const { wrapRender } = __webpack_require__(3);
 	
 	// Shortcuts
 	const { span } = React.DOM;
 	
 	/**
 	 * Renders null value
 	 */
-	const Null = React.createClass({
-	  displayName: "NullRep",
-	
-	  render: wrapRender(function () {
-	    return span({ className: "objectBox objectBox-null" }, "null");
-	  })
-	});
+	function Null(props) {
+	  return span({ className: "objectBox objectBox-null" }, "null");
+	}
 	
 	function supportsObject(object, type) {
 	  if (object && object.type && object.type == "null") {
 	    return true;
 	  }
 	
 	  return object == null;
 	}
 	
 	// Exports from this module
 	
 	module.exports = {
-	  rep: Null,
-	  supportsObject: supportsObject
+	  rep: wrapRender(Null),
+	  supportsObject
 	};
 
 /***/ },
 /* 8 */
 /***/ function(module, exports, __webpack_require__) {
 
 	// Dependencies
-	const React = __webpack_require__(3);
+	const React = __webpack_require__(4);
 	
 	const {
 	  escapeString,
 	  rawCropString,
 	  sanitizeString,
 	  wrapRender
-	} = __webpack_require__(4);
+	} = __webpack_require__(3);
 	
 	// Shortcuts
 	const { span } = React.DOM;
 	
 	/**
 	 * Renders a string. String value is enclosed within quotes.
 	 */
-	const StringRep = React.createClass({
-	  displayName: "StringRep",
-	
-	  propTypes: {
-	    useQuotes: React.PropTypes.bool,
-	    style: React.PropTypes.object,
-	    object: React.PropTypes.string.isRequired,
-	    member: React.PropTypes.any,
-	    cropLimit: React.PropTypes.number
-	  },
-	
-	  getDefaultProps: function () {
-	    return {
-	      useQuotes: true
-	    };
-	  },
-	
-	  render: wrapRender(function () {
-	    let text = this.props.object;
-	    let member = this.props.member;
-	    let style = this.props.style;
-	
-	    let config = { className: "objectBox objectBox-string" };
-	    if (style) {
-	      config.style = style;
-	    }
-	
-	    if (this.props.useQuotes) {
-	      text = escapeString(text);
-	    } else {
-	      text = sanitizeString(text);
-	    }
-	
-	    if ((!member || !member.open) && this.props.cropLimit) {
-	      text = rawCropString(text, this.props.cropLimit);
-	    }
-	
-	    return span(config, text);
-	  })
-	});
+	StringRep.propTypes = {
+	  useQuotes: React.PropTypes.bool,
+	  escapeWhitespace: React.PropTypes.bool,
+	  style: React.PropTypes.object,
+	  object: React.PropTypes.string.isRequired,
+	  member: React.PropTypes.any,
+	  cropLimit: React.PropTypes.number
+	};
+	
+	function StringRep(props) {
+	  let {
+	    cropLimit,
+	    object: text,
+	    member,
+	    style,
+	    useQuotes = true,
+	    escapeWhitespace = true
+	  } = props;
+	
+	  let config = { className: "objectBox objectBox-string" };
+	  if (style) {
+	    config.style = style;
+	  }
+	
+	  if (useQuotes) {
+	    text = escapeString(text, escapeWhitespace);
+	  } else {
+	    text = sanitizeString(text);
+	  }
+	
+	  if ((!member || !member.open) && cropLimit) {
+	    text = rawCropString(text, cropLimit);
+	  }
+	
+	  return span(config, text);
+	}
 	
 	function supportsObject(object, type) {
 	  return type == "string";
 	}
 	
 	// Exports from this module
 	
 	module.exports = {
-	  rep: StringRep,
-	  supportsObject: supportsObject
+	  rep: wrapRender(StringRep),
+	  supportsObject
 	};
 
 /***/ },
 /* 9 */
 /***/ function(module, exports, __webpack_require__) {
 
 	// Dependencies
-	const React = __webpack_require__(3);
+	const React = __webpack_require__(4);
 	const {
 	  escapeString,
 	  sanitizeString,
 	  isGrip,
 	  wrapRender
-	} = __webpack_require__(4);
+	} = __webpack_require__(3);
 	// Shortcuts
 	const { span } = React.DOM;
 	
 	/**
 	 * Renders a long string grip.
 	 */
-	const LongStringRep = React.createClass({
-	  displayName: "LongStringRep",
-	
-	  propTypes: {
-	    useQuotes: React.PropTypes.bool,
-	    style: React.PropTypes.object,
-	    cropLimit: React.PropTypes.number.isRequired,
-	    member: React.PropTypes.string,
-	    object: React.PropTypes.object.isRequired
-	  },
-	
-	  getDefaultProps: function () {
-	    return {
-	      useQuotes: true
-	    };
-	  },
-	
-	  render: wrapRender(function () {
-	    let {
-	      cropLimit,
-	      member,
-	      object,
-	      style,
-	      useQuotes
-	    } = this.props;
-	    let { fullText, initial, length } = object;
-	
-	    let config = { className: "objectBox objectBox-string" };
-	    if (style) {
-	      config.style = style;
-	    }
-	
-	    let string = member && member.open ? fullText || initial : initial.substring(0, cropLimit);
-	
-	    if (string.length < length) {
-	      string += "\u2026";
-	    }
-	    let formattedString = useQuotes ? escapeString(string) : sanitizeString(string);
-	    return span(config, formattedString);
-	  })
-	});
+	LongStringRep.propTypes = {
+	  useQuotes: React.PropTypes.bool,
+	  escapeWhitespace: React.PropTypes.bool,
+	  style: React.PropTypes.object,
+	  cropLimit: React.PropTypes.number.isRequired,
+	  member: React.PropTypes.string,
+	  object: React.PropTypes.object.isRequired
+	};
+	
+	function LongStringRep(props) {
+	  let {
+	    cropLimit,
+	    member,
+	    object,
+	    style,
+	    useQuotes = true,
+	    escapeWhitespace = true
+	  } = props;
+	  let { fullText, initial, length } = object;
+	
+	  let config = { className: "objectBox objectBox-string" };
+	  if (style) {
+	    config.style = style;
+	  }
+	
+	  let string = member && member.open ? fullText || initial : initial.substring(0, cropLimit);
+	
+	  if (string.length < length) {
+	    string += "\u2026";
+	  }
+	  let formattedString = useQuotes ? escapeString(string, escapeWhitespace) : sanitizeString(string);
+	  return span(config, formattedString);
+	}
 	
 	function supportsObject(object, type) {
 	  if (!isGrip(object)) {
 	    return false;
 	  }
 	  return object.type === "longString";
 	}
 	
 	// Exports from this module
 	module.exports = {
-	  rep: LongStringRep,
-	  supportsObject: supportsObject
+	  rep: wrapRender(LongStringRep),
+	  supportsObject
 	};
 
 /***/ },
 /* 10 */
 /***/ function(module, exports, __webpack_require__) {
 
 	// Dependencies
-	const React = __webpack_require__(3);
-	
-	const { wrapRender } = __webpack_require__(4);
+	const React = __webpack_require__(4);
+	
+	const { wrapRender } = __webpack_require__(3);
 	
 	// Shortcuts
 	const { span } = React.DOM;
 	
 	/**
 	 * Renders a number
 	 */
-	const Number = React.createClass({
-	  displayName: "Number",
-	
-	  propTypes: {
-	    object: React.PropTypes.oneOfType([React.PropTypes.object, React.PropTypes.number, React.PropTypes.bool]).isRequired
-	  },
-	
-	  stringify: function (object) {
-	    let isNegativeZero = Object.is(object, -0) || object.type && object.type == "-0";
-	
-	    return isNegativeZero ? "-0" : String(object);
-	  },
-	
-	  render: wrapRender(function () {
-	    let value = this.props.object;
-	
-	    return span({ className: "objectBox objectBox-number" }, this.stringify(value));
-	  })
-	});
+	Number.propTypes = {
+	  object: React.PropTypes.oneOfType([React.PropTypes.object, React.PropTypes.number, React.PropTypes.bool]).isRequired
+	};
+	
+	function Number(props) {
+	  let value = props.object;
+	
+	  return span({ className: "objectBox objectBox-number" }, stringify(value));
+	}
+	
+	function stringify(object) {
+	  let isNegativeZero = Object.is(object, -0) || object.type && object.type == "-0";
+	
+	  return isNegativeZero ? "-0" : String(object);
+	}
 	
 	function supportsObject(object, type) {
 	  return ["boolean", "number", "-0"].includes(type);
 	}
 	
 	// Exports from this module
 	
 	module.exports = {
-	  rep: Number,
-	  supportsObject: supportsObject
+	  rep: wrapRender(Number),
+	  supportsObject
 	};
 
 /***/ },
 /* 11 */
 /***/ function(module, exports, __webpack_require__) {
 
 	// Dependencies
-	const React = __webpack_require__(3);
+	const React = __webpack_require__(4);
 	const {
-	  createFactories,
+	  safeObjectLink,
 	  wrapRender
-	} = __webpack_require__(4);
-	const Caption = React.createFactory(__webpack_require__(12));
+	} = __webpack_require__(3);
+	const Caption = __webpack_require__(12);
 	const { MODE } = __webpack_require__(1);
 	
 	const ModePropType = React.PropTypes.oneOf(
 	// @TODO Change this to Object.values once it's supported in Node's version of V8
 	Object.keys(MODE).map(key => MODE[key]));
 	
 	// Shortcuts
 	const DOM = React.DOM;
 	
 	/**
 	 * Renders an array. The array is enclosed by left and right bracket
 	 * and the max number of rendered items depends on the current mode.
 	 */
-	let ArrayRep = React.createClass({
-	  displayName: "ArrayRep",
-	
-	  propTypes: {
-	    mode: ModePropType,
-	    objectLink: React.PropTypes.func,
-	    object: React.PropTypes.array.isRequired
-	  },
-	
-	  getTitle: function (object, context) {
-	    return "[" + object.length + "]";
-	  },
-	
-	  arrayIterator: function (array, max) {
-	    let items = [];
-	    let delim;
-	
-	    for (let i = 0; i < array.length && i < max; i++) {
-	      try {
-	        let value = array[i];
-	
-	        delim = i == array.length - 1 ? "" : ", ";
-	
-	        items.push(ItemRep({
-	          object: value,
-	          // Hardcode tiny mode to avoid recursive handling.
-	          mode: MODE.TINY,
-	          delim: delim
-	        }));
-	      } catch (exc) {
-	        items.push(ItemRep({
-	          object: exc,
-	          mode: MODE.TINY,
-	          delim: delim
-	        }));
-	      }
-	    }
-	
-	    if (array.length > max) {
-	      items.push(Caption({
-	        object: this.safeObjectLink({
-	          object: this.props.object
-	        }, array.length - max + " more…")
+	ArrayRep.propTypes = {
+	  mode: ModePropType,
+	  objectLink: React.PropTypes.func,
+	  object: React.PropTypes.array.isRequired
+	};
+	
+	function ArrayRep(props) {
+	  let {
+	    object,
+	    mode = MODE.SHORT
+	  } = props;
+	
+	  let items;
+	  let brackets;
+	  let needSpace = function (space) {
+	    return space ? { left: "[ ", right: " ]" } : { left: "[", right: "]" };
+	  };
+	
+	  if (mode === MODE.TINY) {
+	    let isEmpty = object.length === 0;
+	    items = [DOM.span({ className: "length" }, isEmpty ? "" : object.length)];
+	    brackets = needSpace(false);
+	  } else {
+	    let max = mode === MODE.SHORT ? 3 : 10;
+	    items = arrayIterator(props, object, max);
+	    brackets = needSpace(items.length > 0);
+	  }
+	
+	  return DOM.span({
+	    className: "objectBox objectBox-array" }, safeObjectLink(props, {
+	    className: "arrayLeftBracket",
+	    object: object
+	  }, brackets.left), ...items, safeObjectLink(props, {
+	    className: "arrayRightBracket",
+	    object: object
+	  }, brackets.right), DOM.span({
+	    className: "arrayProperties",
+	    role: "group" }));
+	}
+	
+	function arrayIterator(props, array, max) {
+	  let items = [];
+	  let delim;
+	
+	  for (let i = 0; i < array.length && i < max; i++) {
+	    try {
+	      let value = array[i];
+	
+	      delim = i == array.length - 1 ? "" : ", ";
+	
+	      items.push(ItemRep({
+	        object: value,
+	        // Hardcode tiny mode to avoid recursive handling.
+	        mode: MODE.TINY,
+	        delim: delim
+	      }));
+	    } catch (exc) {
+	      items.push(ItemRep({
+	        object: exc,
+	        mode: MODE.TINY,
+	        delim: delim
 	      }));
 	    }
-	
-	    return items;
-	  },
-	
-	  /**
-	   * Returns true if the passed object is an array with additional (custom)
-	   * properties, otherwise returns false. Custom properties should be
-	   * displayed in extra expandable section.
-	   *
-	   * Example array with a custom property.
-	   * let arr = [0, 1];
-	   * arr.myProp = "Hello";
-	   *
-	   * @param {Array} array The array object.
-	   */
-	  hasSpecialProperties: function (array) {
-	    function isInteger(x) {
-	      let y = parseInt(x, 10);
-	      if (isNaN(y)) {
-	        return false;
-	      }
-	      return x === y.toString();
-	    }
-	
-	    let propsArray = Object.getOwnPropertyNames(array);
-	    for (let i = 0; i < propsArray.length; i++) {
-	      let p = propsArray[i];
-	
-	      // Valid indexes are skipped
-	      if (isInteger(p)) {
-	        continue;
-	      }
-	
-	      // Ignore standard 'length' property, anything else is custom.
-	      if (p != "length") {
-	        return true;
-	      }
-	    }
-	
-	    return false;
-	  },
-	
-	  // Event Handlers
-	
-	  onToggleProperties: function (event) {},
-	
-	  onClickBracket: function (event) {},
-	
-	  safeObjectLink: function (config, ...children) {
-	    if (this.props.objectLink) {
-	      return this.props.objectLink(Object.assign({
-	        object: this.props.object
-	      }, config), ...children);
-	    }
-	
-	    if (Object.keys(config).length === 0 && children.length === 1) {
-	      return children[0];
-	    }
-	
-	    return DOM.span(config, ...children);
-	  },
-	
-	  render: wrapRender(function () {
-	    let {
-	      object,
-	      mode = MODE.SHORT
-	    } = this.props;
-	
-	    let items;
-	    let brackets;
-	    let needSpace = function (space) {
-	      return space ? { left: "[ ", right: " ]" } : { left: "[", right: "]" };
-	    };
-	
-	    if (mode === MODE.TINY) {
-	      let isEmpty = object.length === 0;
-	      items = [DOM.span({ className: "length" }, isEmpty ? "" : object.length)];
-	      brackets = needSpace(false);
-	    } else {
-	      let max = mode === MODE.SHORT ? 3 : 10;
-	      items = this.arrayIterator(object, max);
-	      brackets = needSpace(items.length > 0);
-	    }
-	
-	    return DOM.span({
-	      className: "objectBox objectBox-array" }, this.safeObjectLink({
-	      className: "arrayLeftBracket",
-	      object: object
-	    }, brackets.left), ...items, this.safeObjectLink({
-	      className: "arrayRightBracket",
-	      object: object
-	    }, brackets.right), DOM.span({
-	      className: "arrayProperties",
-	      role: "group" }));
-	  })
-	});
+	  }
+	
+	  if (array.length > max) {
+	    items.push(Caption({
+	      object: safeObjectLink(props, {
+	        object: props.object
+	      }, array.length - max + " more…")
+	    }));
+	  }
+	
+	  return items;
+	}
 	
 	/**
 	 * Renders array item. Individual values are separated by a comma.
 	 */
-	let ItemRep = React.createFactory(React.createClass({
-	  displayName: "ItemRep",
-	
-	  propTypes: {
-	    object: React.PropTypes.any.isRequired,
-	    delim: React.PropTypes.string.isRequired,
-	    mode: ModePropType
-	  },
-	
-	  render: wrapRender(function () {
-	    const { Rep } = createFactories(__webpack_require__(2));
-	
-	    let object = this.props.object;
-	    let delim = this.props.delim;
-	    let mode = this.props.mode;
-	    return DOM.span({}, Rep({ object: object, mode: mode }), delim);
-	  })
-	}));
+	ItemRep.propTypes = {
+	  object: React.PropTypes.any.isRequired,
+	  delim: React.PropTypes.string.isRequired,
+	  mode: ModePropType
+	};
+	
+	function ItemRep(props) {
+	  const { Rep } = __webpack_require__(2);
+	
+	  let {
+	    object,
+	    delim,
+	    mode
+	  } = props;
+	  return DOM.span({}, Rep({ object: object, mode: mode }), delim);
+	}
 	
 	function supportsObject(object, type) {
 	  return Array.isArray(object) || Object.prototype.toString.call(object) === "[object Arguments]";
 	}
 	
 	// Exports from this module
 	module.exports = {
-	  rep: ArrayRep,
-	  supportsObject: supportsObject
+	  rep: wrapRender(ArrayRep),
+	  supportsObject
 	};
 
 /***/ },
 /* 12 */
 /***/ function(module, exports, __webpack_require__) {
 
 	// Dependencies
-	const React = __webpack_require__(3);
+	const React = __webpack_require__(4);
 	const DOM = React.DOM;
 	
-	const { wrapRender } = __webpack_require__(4);
+	const { wrapRender } = __webpack_require__(3);
 	
 	/**
 	 * Renders a caption. This template is used by other components
 	 * that needs to distinguish between a simple text/value and a label.
 	 */
-	const Caption = React.createClass({
-	  displayName: "Caption",
-	
-	  propTypes: {
-	    object: React.PropTypes.oneOfType([React.PropTypes.number, React.PropTypes.string]).isRequired
-	  },
-	
-	  render: wrapRender(function () {
-	    return DOM.span({ "className": "caption" }, this.props.object);
-	  })
-	});
+	Caption.propTypes = {
+	  object: React.PropTypes.oneOfType([React.PropTypes.number, React.PropTypes.string]).isRequired
+	};
+	
+	function Caption(props) {
+	  return DOM.span({ "className": "caption" }, props.object);
+	}
 	
 	// Exports from this module
-	module.exports = Caption;
+	module.exports = wrapRender(Caption);
 
 /***/ },
 /* 13 */
 /***/ function(module, exports, __webpack_require__) {
 
 	// Dependencies
-	const React = __webpack_require__(3);
+	const React = __webpack_require__(4);
 	const {
+	  safeObjectLink,
 	  wrapRender
-	} = __webpack_require__(4);
-	const Caption = React.createFactory(__webpack_require__(12));
-	const PropRep = React.createFactory(__webpack_require__(14));
+	} = __webpack_require__(3);
+	const Caption = __webpack_require__(12);
+	const PropRep = __webpack_require__(14);
 	const { MODE } = __webpack_require__(1);
 	// Shortcuts
 	const { span } = React.DOM;
 	/**
 	 * Renders an object. An object is represented by a list of its
 	 * properties enclosed in curly brackets.
 	 */
-	const Obj = React.createClass({
-	  displayName: "Obj",
-	
-	  propTypes: {
-	    object: React.PropTypes.object.isRequired,
-	    // @TODO Change this to Object.values once it's supported in Node's version of V8
-	    mode: React.PropTypes.oneOf(Object.keys(MODE).map(key => MODE[key])),
-	    objectLink: React.PropTypes.func,
-	    title: React.PropTypes.string
-	  },
-	
-	  getTitle: function (object) {
-	    let title = this.props.title || object.class || "Object";
-	    return this.safeObjectLink({ className: "objectTitle" }, title);
-	  },
-	
-	  safePropIterator: function (object, max) {
-	    max = typeof max === "undefined" ? 3 : max;
-	    try {
-	      return this.propIterator(object, max);
-	    } catch (err) {
-	      console.error(err);
-	    }
-	    return [];
-	  },
-	
-	  propIterator: function (object, max) {
-	    let isInterestingProp = (t, value) => {
-	      // Do not pick objects, it could cause recursion.
-	      return t == "boolean" || t == "number" || t == "string" && value;
-	    };
-	
-	    // Work around https://bugzilla.mozilla.org/show_bug.cgi?id=945377
-	    if (Object.prototype.toString.call(object) === "[object Generator]") {
-	      object = Object.getPrototypeOf(object);
-	    }
-	
-	    // Object members with non-empty values are preferred since it gives the
-	    // user a better overview of the object.
-	    let propsArray = this.getPropsArray(object, max, isInterestingProp);
-	
-	    if (propsArray.length <= max) {
-	      // There are not enough props yet (or at least, not enough props to
-	      // be able to know whether we should print "more…" or not).
-	      // Let's display also empty members and functions.
-	      propsArray = propsArray.concat(this.getPropsArray(object, max, (t, value) => {
-	        return !isInterestingProp(t, value);
-	      }));
-	    }
-	
-	    if (propsArray.length > max) {
-	      propsArray.pop();
-	      let objectLink = this.props.objectLink || span;
-	
-	      propsArray.push(Caption({
-	        object: objectLink({
-	          object: object
-	        }, Object.keys(object).length - max + " more…")
-	      }));
-	    } else if (propsArray.length > 0) {
-	      // Remove the last comma.
-	      propsArray[propsArray.length - 1] = React.cloneElement(propsArray[propsArray.length - 1], { delim: "" });
-	    }
-	
+	ObjectRep.propTypes = {
+	  object: React.PropTypes.object.isRequired,
+	  // @TODO Change this to Object.values once it's supported in Node's version of V8
+	  mode: React.PropTypes.oneOf(Object.keys(MODE).map(key => MODE[key])),
+	  objectLink: React.PropTypes.func,
+	  title: React.PropTypes.string
+	};
+	
+	function ObjectRep(props) {
+	  let object = props.object;
+	  let propsArray = safePropIterator(props, object);
+	
+	  if (props.mode === MODE.TINY || !propsArray.length) {
+	    return span({ className: "objectBox objectBox-object" }, getTitle(props, object));
+	  }
+	
+	  return span({ className: "objectBox objectBox-object" }, getTitle(props, object), safeObjectLink(props, {
+	    className: "objectLeftBrace"
+	  }, " { "), ...propsArray, safeObjectLink(props, {
+	    className: "objectRightBrace"
+	  }, " }"));
+	}
+	
+	function getTitle(props, object) {
+	  let title = props.title || object.class || "Object";
+	  return safeObjectLink(props, { className: "objectTitle" }, title);
+	}
+	
+	function safePropIterator(props, object, max) {
+	  max = typeof max === "undefined" ? 3 : max;
+	  try {
+	    return propIterator(props, object, max);
+	  } catch (err) {
+	    console.error(err);
+	  }
+	  return [];
+	}
+	
+	function propIterator(props, object, max) {
+	  let isInterestingProp = (type, value) => {
+	    // Do not pick objects, it could cause recursion.
+	    return type == "boolean" || type == "number" || type == "string" && value;
+	  };
+	
+	  // Work around https://bugzilla.mozilla.org/show_bug.cgi?id=945377
+	  if (Object.prototype.toString.call(object) === "[object Generator]") {
+	    object = Object.getPrototypeOf(object);
+	  }
+	
+	  // Object members with non-empty values are preferred since it gives the
+	  // user a better overview of the object.
+	  let interestingObject = getFilteredObject(object, max, isInterestingProp);
+	
+	  if (Object.keys(interestingObject).length < max) {
+	    // There are not enough props yet (or at least, not enough props to
+	    // be able to know whether we should print "more…" or not).
+	    // Let's display also empty members and functions.
+	    interestingObject = Object.assign({}, interestingObject, getFilteredObject(object, max - Object.keys(interestingObject).length, (type, value) => !isInterestingProp(type, value)));
+	  }
+	
+	  const truncated = Object.keys(object).length > max;
+	  let propsArray = getPropsArray(interestingObject, truncated);
+	  if (truncated) {
+	    propsArray.push(Caption({
+	      object: safeObjectLink(props, {}, Object.keys(object).length - max + " more…")
+	    }));
+	  }
+	
+	  return propsArray;
+	}
+	
+	/**
+	 * Get an array of components representing the properties of the object
+	 *
+	 * @param {Object} object
+	 * @param {Boolean} truncated true if the object is truncated.
+	 * @return {Array} Array of PropRep.
+	 */
+	function getPropsArray(object, truncated) {
+	  let propsArray = [];
+	
+	  if (!object) {
 	    return propsArray;
-	  },
-	
-	  getPropsArray: function (object, max, filter) {
-	    let propsArray = [];
-	
-	    max = max || 3;
-	    if (!object) {
-	      return propsArray;
-	    }
-	
-	    // Hardcode tiny mode to avoid recursive handling.
-	    let mode = MODE.TINY;
-	
-	    try {
-	      for (let name in object) {
-	        if (propsArray.length > max) {
-	          return propsArray;
-	        }
-	
-	        let value;
-	        try {
-	          value = object[name];
-	        } catch (exc) {
-	          continue;
-	        }
-	
-	        let t = typeof value;
-	        if (filter(t, value)) {
-	          propsArray.push(PropRep({
-	            mode: mode,
-	            name: name,
-	            object: value,
-	            equal: ": ",
-	            delim: ", "
-	          }));
-	        }
+	  }
+	
+	  // Hardcode tiny mode to avoid recursive handling.
+	  let mode = MODE.TINY;
+	  const objectKeys = Object.keys(object);
+	  return objectKeys.map((name, i) => PropRep({
+	    mode,
+	    name,
+	    object: object[name],
+	    equal: ": ",
+	    delim: i !== objectKeys.length - 1 || truncated ? ", " : null
+	  }));
+	}
+	
+	/**
+	 * Get a copy of the object filtered by a given predicate.
+	 *
+	 * @param {Object} object.
+	 * @param {Number} max The maximum length of keys array.
+	 * @param {Function} filter Filter the props you want.
+	 * @return {Object} the filtered object.
+	 */
+	function getFilteredObject(object, max, filter) {
+	  let filteredObject = {};
+	
+	  try {
+	    for (let name in object) {
+	      if (Object.keys(filteredObject).length >= max) {
+	        return filteredObject;
 	      }
-	    } catch (err) {
-	      console.error(err);
-	    }
-	
-	    return propsArray;
-	  },
-	
-	  safeObjectLink: function (config, ...children) {
-	    if (this.props.objectLink) {
-	      return this.props.objectLink(Object.assign({
-	        object: this.props.object
-	      }, config), ...children);
-	    }
-	
-	    if (Object.keys(config).length === 0 && children.length === 1) {
-	      return children[0];
+	
+	      let value;
+	      try {
+	        value = object[name];
+	      } catch (exc) {
+	        continue;
+	      }
+	
+	      let t = typeof value;
+	      if (filter(t, value)) {
+	        filteredObject[name] = value;
+	      }
 	    }
-	
-	    return span(config, ...children);
-	  },
-	
-	  render: wrapRender(function () {
-	    let object = this.props.object;
-	    let propsArray = this.safePropIterator(object);
-	
-	    if (this.props.mode === MODE.TINY || !propsArray.length) {
-	      return span({ className: "objectBox objectBox-object" }, this.getTitle(object));
-	    }
-	
-	    return span({ className: "objectBox objectBox-object" }, this.getTitle(object), this.safeObjectLink({
-	      className: "objectLeftBrace"
-	    }, " { "), ...propsArray, this.safeObjectLink({
-	      className: "objectRightBrace"
-	    }, " }"));
-	  })
-	});
+	  } catch (err) {
+	    console.error(err);
+	  }
+	  return filteredObject;
+	}
+	
 	function supportsObject(object, type) {
 	  return true;
 	}
 	
 	// Exports from this module
 	module.exports = {
-	  rep: Obj,
-	  supportsObject: supportsObject
+	  rep: wrapRender(ObjectRep),
+	  supportsObject
 	};
 
 /***/ },
 /* 14 */
 /***/ function(module, exports, __webpack_require__) {
 
 	// Dependencies
-	const React = __webpack_require__(3);
+	const React = __webpack_require__(4);
 	const {
-	  createFactories,
 	  maybeEscapePropertyName,
 	  wrapRender
-	} = __webpack_require__(4);
+	} = __webpack_require__(3);
 	const { MODE } = __webpack_require__(1);
 	// Shortcuts
 	const { span } = React.DOM;
 	
 	/**
 	 * Property for Obj (local JS objects), Grip (remote JS objects)
 	 * and GripMap (remote JS maps and weakmaps) reps.
 	 * It's used to render object properties.
 	 */
-	let PropRep = React.createClass({
-	  displayName: "PropRep",
-	
-	  propTypes: {
-	    // Property name.
-	    name: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.object]).isRequired,
-	    // Equal character rendered between property name and value.
-	    equal: React.PropTypes.string,
-	    // Delimiter character used to separate individual properties.
-	    delim: React.PropTypes.string,
-	    // @TODO Change this to Object.values once it's supported in Node's version of V8
-	    mode: React.PropTypes.oneOf(Object.keys(MODE).map(key => MODE[key])),
-	    objectLink: React.PropTypes.func,
-	    attachedActorIds: React.PropTypes.array,
-	    onDOMNodeMouseOver: React.PropTypes.func,
-	    onDOMNodeMouseOut: React.PropTypes.func,
-	    onInspectIconClick: React.PropTypes.func,
-	    // Normally a PropRep will quote a property name that isn't valid
-	    // when unquoted; but this flag can be used to suppress the
-	    // quoting.
-	    suppressQuotes: React.PropTypes.bool
-	  },
-	
-	  render: wrapRender(function () {
-	    const Grip = __webpack_require__(15);
-	    let { Rep } = createFactories(__webpack_require__(2));
-	    let {
-	      name,
-	      mode,
-	      equal,
-	      delim,
-	      suppressQuotes
-	    } = this.props;
-	
-	    let key;
-	    // The key can be a simple string, for plain objects,
-	    // or another object for maps and weakmaps.
-	    if (typeof name === "string") {
-	      if (!suppressQuotes) {
-	        name = maybeEscapePropertyName(name);
-	      }
-	      key = span({ "className": "nodeName" }, name);
-	    } else {
-	      key = Rep(Object.assign({}, this.props, {
-	        object: name,
-	        mode: mode || MODE.TINY,
-	        defaultRep: Grip
-	      }));
+	PropRep.propTypes = {
+	  // Property name.
+	  name: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.object]).isRequired,
+	  // Equal character rendered between property name and value.
+	  equal: React.PropTypes.string,
+	  // Delimiter character used to separate individual properties.
+	  delim: React.PropTypes.string,
+	  // @TODO Change this to Object.values once it's supported in Node's version of V8
+	  mode: React.PropTypes.oneOf(Object.keys(MODE).map(key => MODE[key])),
+	  objectLink: React.PropTypes.func,
+	  onDOMNodeMouseOver: React.PropTypes.func,
+	  onDOMNodeMouseOut: React.PropTypes.func,
+	  onInspectIconClick: React.PropTypes.func,
+	  // Normally a PropRep will quote a property name that isn't valid
+	  // when unquoted; but this flag can be used to suppress the
+	  // quoting.
+	  suppressQuotes: React.PropTypes.bool
+	};
+	
+	function PropRep(props) {
+	  const Grip = __webpack_require__(15);
+	  const { Rep } = __webpack_require__(2);
+	
+	  let {
+	    name,
+	    mode,
+	    equal,
+	    delim,
+	    suppressQuotes
+	  } = props;
+	
+	  let key;
+	  // The key can be a simple string, for plain objects,
+	  // or another object for maps and weakmaps.
+	  if (typeof name === "string") {
+	    if (!suppressQuotes) {
+	      name = maybeEscapePropertyName(name);
 	    }
-	
-	    return span({}, key, span({
-	      "className": "objectEqual"
-	    }, equal), Rep(Object.assign({}, this.props)), span({
+	    key = span({ "className": "nodeName" }, name);
+	  } else {
+	    key = Rep(Object.assign({}, props, {
+	      object: name,
+	      mode: mode || MODE.TINY,
+	      defaultRep: Grip
+	    }));
+	  }
+	
+	  let delimElement;
+	  if (delim) {
+	    delimElement = span({
 	      "className": "objectComma"
-	    }, delim));
-	  })
-	});
+	    }, delim);
+	  }
+	
+	  return span({}, key, span({
+	    "className": "objectEqual"
+	  }, equal), Rep(Object.assign({}, props)), delimElement);
+	}
 	
 	// Exports from this module
-	module.exports = PropRep;
+	module.exports = wrapRender(PropRep);
 
 /***/ },
 /* 15 */
 /***/ function(module, exports, __webpack_require__) {
 
 	// ReactJS
-	const React = __webpack_require__(3);
+	const React = __webpack_require__(4);
 	// Dependencies
 	const {
-	  createFactories,
 	  isGrip,
+	  safeObjectLink,
 	  wrapRender
-	} = __webpack_require__(4);
-	const Caption = React.createFactory(__webpack_require__(12));
-	const PropRep = React.createFactory(__webpack_require__(14));
+	} = __webpack_require__(3);
+	const Caption = __webpack_require__(12);
+	const PropRep = __webpack_require__(14);
 	const { MODE } = __webpack_require__(1);
 	// Shortcuts
 	const { span } = React.DOM;
 	
 	/**
 	 * Renders generic grip. Grip is client representation
 	 * of remote JS object and is used as an input object
 	 * for this rep component.
 	 */
-	const GripRep = React.createClass({
-	  displayName: "Grip",
-	
-	  propTypes: {
-	    object: React.PropTypes.object.isRequired,
-	    // @TODO Change this to Object.values once it's supported in Node's version of V8
-	    mode: React.PropTypes.oneOf(Object.keys(MODE).map(key => MODE[key])),
-	    isInterestingProp: React.PropTypes.func,
-	    title: React.PropTypes.string,
-	    objectLink: React.PropTypes.func,
-	    attachedActorIds: React.PropTypes.array,
-	    onDOMNodeMouseOver: React.PropTypes.func,
-	    onDOMNodeMouseOut: React.PropTypes.func,
-	    onInspectIconClick: React.PropTypes.func
-	  },
-	
-	  getTitle: function (object) {
-	    let title = this.props.title || object.class || "Object";
-	    return this.safeObjectLink({}, title);
-	  },
-	
-	  safePropIterator: function (object, max) {
-	    max = typeof max === "undefined" ? 3 : max;
-	    try {
-	      return this.propIterator(object, max);
-	    } catch (err) {
-	      console.error(err);
-	    }
-	    return [];
-	  },
-	
-	  propIterator: function (object, max) {
-	    if (object.preview && Object.keys(object.preview).includes("wrappedValue")) {
-	      const { Rep } = createFactories(__webpack_require__(2));
-	
-	      return [Rep({
-	        object: object.preview.wrappedValue,
-	        mode: this.props.mode || MODE.TINY,
-	        defaultRep: Grip
-	      })];
-	    }
-	
-	    // Property filter. Show only interesting properties to the user.
-	    let isInterestingProp = this.props.isInterestingProp || ((type, value) => {
-	      return type == "boolean" || type == "number" || type == "string" && value.length != 0;
-	    });
-	
-	    let properties = object.preview ? object.preview.ownProperties : {};
-	    let propertiesLength = object.preview && object.preview.ownPropertiesLength ? object.preview.ownPropertiesLength : object.ownPropertyLength;
-	
-	    if (object.preview && object.preview.safeGetterValues) {
-	      properties = Object.assign({}, properties, object.preview.safeGetterValues);
-	      propertiesLength += Object.keys(object.preview.safeGetterValues).length;
-	    }
-	
-	    let indexes = this.getPropIndexes(properties, max, isInterestingProp);
-	    if (indexes.length < max && indexes.length < propertiesLength) {
-	      // There are not enough props yet. Then add uninteresting props to display them.
-	      indexes = indexes.concat(this.getPropIndexes(properties, max - indexes.length, (t, value, name) => {
-	        return !isInterestingProp(t, value, name);
-	      }));
-	    }
-	
-	    const truncate = Object.keys(properties).length > max;
-	    // The server synthesizes some property names for a Proxy, like
-	    // <target> and <handler>; we don't want to quote these because,
-	    // as synthetic properties, they appear more natural when
-	    // unquoted.
-	    const suppressQuotes = object.class === "Proxy";
-	    let propsArray = this.getProps(properties, indexes, truncate, suppressQuotes);
-	    if (truncate) {
-	      // There are some undisplayed props. Then display "more...".
-	      propsArray.push(Caption({
-	        object: this.safeObjectLink({}, `${propertiesLength - max} more…`)
-	      }));
+	GripRep.propTypes = {
+	  object: React.PropTypes.object.isRequired,
+	  // @TODO Change this to Object.values once it's supported in Node's version of V8
+	  mode: React.PropTypes.oneOf(Object.keys(MODE).map(key => MODE[key])),
+	  isInterestingProp: React.PropTypes.func,
+	  title: React.PropTypes.string,
+	  objectLink: React.PropTypes.func,
+	  onDOMNodeMouseOver: React.PropTypes.func,
+	  onDOMNodeMouseOut: React.PropTypes.func,
+	  onInspectIconClick: React.PropTypes.func
+	};
+	
+	function GripRep(props) {
+	  let object = props.object;
+	  let propsArray = safePropIterator(props, object, props.mode === MODE.LONG ? 10 : 3);
+	
+	  if (props.mode === MODE.TINY) {
+	    return span({ className: "objectBox objectBox-object" }, getTitle(props, object));
+	  }
+	
+	  return span({ className: "objectBox objectBox-object" }, getTitle(props, object), safeObjectLink(props, {
+	    className: "objectLeftBrace"
+	  }, " { "), ...propsArray, safeObjectLink(props, {
+	    className: "objectRightBrace"
+	  }, " }"));
+	}
+	
+	function getTitle(props, object) {
+	  let title = props.title || object.class || "Object";
+	  return safeObjectLink(props, {}, title);
+	}
+	
+	function safePropIterator(props, object, max) {
+	  max = typeof max === "undefined" ? 3 : max;
+	  try {
+	    return propIterator(props, object, max);
+	  } catch (err) {
+	    console.error(err);
+	  }
+	  return [];
+	}
+	
+	function propIterator(props, object, max) {
+	  if (object.preview && Object.keys(object.preview).includes("wrappedValue")) {
+	    const { Rep } = __webpack_require__(2);
+	
+	    return [Rep({
+	      object: object.preview.wrappedValue,
+	      mode: props.mode || MODE.TINY,
+	      defaultRep: Grip
+	    })];
+	  }
+	
+	  // Property filter. Show only interesting properties to the user.
+	  let isInterestingProp = props.isInterestingProp || ((type, value) => {
+	    return type == "boolean" || type == "number" || type == "string" && value.length != 0;
+	  });
+	
+	  let properties = object.preview ? object.preview.ownProperties : {};
+	  let propertiesLength = object.preview && object.preview.ownPropertiesLength ? object.preview.ownPropertiesLength : object.ownPropertyLength;
+	
+	  if (object.preview && object.preview.safeGetterValues) {
+	    properties = Object.assign({}, properties, object.preview.safeGetterValues);
+	    propertiesLength += Object.keys(object.preview.safeGetterValues).length;
+	  }
+	
+	  let indexes = getPropIndexes(properties, max, isInterestingProp);
+	  if (indexes.length < max && indexes.length < propertiesLength) {
+	    // There are not enough props yet. Then add uninteresting props to display them.
+	    indexes = indexes.concat(getPropIndexes(properties, max - indexes.length, (t, value, name) => {
+	      return !isInterestingProp(t, value, name);
+	    }));
+	  }
+	
+	  const truncate = Object.keys(properties).length > max;
+	  // The server synthesizes some property names for a Proxy, like
+	  // <target> and <handler>; we don't want to quote these because,
+	  // as synthetic properties, they appear more natural when
+	  // unquoted.
+	  const suppressQuotes = object.class === "Proxy";
+	  let propsArray = getProps(props, properties, indexes, truncate, suppressQuotes);
+	  if (truncate) {
+	    // There are some undisplayed props. Then display "more...".
+	    propsArray.push(Caption({
+	      object: safeObjectLink(props, {}, `${propertiesLength - max} more…`)
+	    }));
+	  }
+	
+	  return propsArray;
+	}
+	
+	/**
+	 * Get props ordered by index.
+	 *
+	 * @param {Object} componentProps Grip Component props.
+	 * @param {Object} properties Properties of the object the Grip describes.
+	 * @param {Array} indexes Indexes of properties.
+	 * @param {Boolean} truncate true if the grip will be truncated.
+	 * @param {Boolean} suppressQuotes true if we should suppress quotes
+	 *                  on property names.
+	 * @return {Array} Props.
+	 */
+	function getProps(componentProps, properties, indexes, truncate, suppressQuotes) {
+	  // Make indexes ordered by ascending.
+	  indexes.sort(function (a, b) {
+	    return a - b;
+	  });
+	
+	  const propertiesKeys = Object.keys(properties);
+	  return indexes.map(i => {
+	    let name = propertiesKeys[i];
+	    let value = getPropValue(properties[name]);
+	
+	    return PropRep(Object.assign({}, componentProps, {
+	      mode: MODE.TINY,
+	      name,
+	      object: value,
+	      equal: ": ",
+	      delim: i !== indexes.length - 1 || truncate ? ", " : null,
+	      defaultRep: Grip,
+	      // Do not propagate title and objectLink to properties reps
+	      title: null,
+	      objectLink: null,
+	      suppressQuotes
+	    }));
+	  });
+	}
+	
+	/**
+	 * Get the indexes of props in the object.
+	 *
+	 * @param {Object} properties Props object.
+	 * @param {Number} max The maximum length of indexes array.
+	 * @param {Function} filter Filter the props you want.
+	 * @return {Array} Indexes of interesting props in the object.
+	 */
+	function getPropIndexes(properties, max, filter) {
+	  let indexes = [];
+	
+	  try {
+	    let i = 0;
+	    for (let name in properties) {
+	      if (indexes.length >= max) {
+	        return indexes;
+	      }
+	
+	      // Type is specified in grip's "class" field and for primitive
+	      // values use typeof.
+	      let value = getPropValue(properties[name]);
+	      let type = value.class || typeof value;
+	      type = type.toLowerCase();
+	
+	      if (filter(type, value, name)) {
+	        indexes.push(i);
+	      }
+	      i++;
 	    }
-	
-	    return propsArray;
-	  },
-	
-	  /**
-	   * Get props ordered by index.
-	   *
-	   * @param {Object} properties Props object.
-	   * @param {Array} indexes Indexes of props.
-	   * @param {Boolean} truncate true if the grip will be truncated.
-	   * @param {Boolean} suppressQuotes true if we should suppress quotes
-	   *                  on property names.
-	   * @return {Array} Props.
-	   */
-	  getProps: function (properties, indexes, truncate, suppressQuotes) {
-	    let propsArray = [];
-	
-	    // Make indexes ordered by ascending.
-	    indexes.sort(function (a, b) {
-	      return a - b;
-	    });
-	
-	    indexes.forEach(i => {
-	      let name = Object.keys(properties)[i];
-	      let value = this.getPropValue(properties[name]);
-	
-	      let propRepProps = Object.assign({}, this.props, {
-	        mode: MODE.TINY,
-	        name: name,
-	        object: value,
-	        equal: ": ",
-	        delim: i !== indexes.length - 1 || truncate ? ", " : "",
-	        defaultRep: Grip,
-	        // Do not propagate title to properties reps
-	        title: undefined,
-	        suppressQuotes
-	      });
-	      delete propRepProps.objectLink;
-	      propsArray.push(PropRep(propRepProps));
-	    });
-	
-	    return propsArray;
-	  },
-	
-	  /**
-	   * Get the indexes of props in the object.
-	   *
-	   * @param {Object} properties Props object.
-	   * @param {Number} max The maximum length of indexes array.
-	   * @param {Function} filter Filter the props you want.
-	   * @return {Array} Indexes of interesting props in the object.
-	   */
-	  getPropIndexes: function (properties, max, filter) {
-	    let indexes = [];
-	
-	    try {
-	      let i = 0;
-	      for (let name in properties) {
-	        if (indexes.length >= max) {
-	          return indexes;
-	        }
-	
-	        // Type is specified in grip's "class" field and for primitive
-	        // values use typeof.
-	        let value = this.getPropValue(properties[name]);
-	        let type = value.class || typeof value;
-	        type = type.toLowerCase();
-	
-	        if (filter(type, value, name)) {
-	          indexes.push(i);
-	        }
-	        i++;
-	      }
-	    } catch (err) {
-	      console.error(err);
+	  } catch (err) {
+	    console.error(err);
+	  }
+	  return indexes;
+	}
+	
+	/**
+	 * Get the actual value of a property.
+	 *
+	 * @param {Object} property
+	 * @return {Object} Value of the property.
+	 */
+	function getPropValue(property) {
+	  let value = property;
+	  if (typeof property === "object") {
+	    let keys = Object.keys(property);
+	    if (keys.includes("value")) {
+	      value = property.value;
+	    } else if (keys.includes("getterValue")) {
+	      value = property.getterValue;
 	    }
-	    return indexes;
-	  },
-	
-	  /**
-	   * Get the actual value of a property.
-	   *
-	   * @param {Object} property
-	   * @return {Object} Value of the property.
-	   */
-	  getPropValue: function (property) {
-	    let value = property;
-	    if (typeof property === "object") {
-	      let keys = Object.keys(property);
-	      if (keys.includes("value")) {
-	        value = property.value;
-	      } else if (keys.includes("getterValue")) {
-	        value = property.getterValue;
-	      }
-	    }
-	    return value;
-	  },
-	
-	  safeObjectLink: function (config, ...children) {
-	    if (this.props.objectLink) {
-	      return this.props.objectLink(Object.assign({
-	        object: this.props.object
-	      }, config), ...children);
-	    }
-	
-	    if (Object.keys(config).length === 0 && children.length === 1) {
-	      return children[0];
-	    }
-	
-	    return span(config, ...children);
-	  },
-	
-	  render: wrapRender(function () {
-	    let object = this.props.object;
-	    let propsArray = this.safePropIterator(object, this.props.mode === MODE.LONG ? 10 : 3);
-	
-	    if (this.props.mode === MODE.TINY) {
-	      return span({ className: "objectBox objectBox-object" }, this.getTitle(object));
-	    }
-	
-	    return span({ className: "objectBox objectBox-object" }, this.getTitle(object), this.safeObjectLink({
-	      className: "objectLeftBrace"
-	    }, " { "), ...propsArray, this.safeObjectLink({
-	      className: "objectRightBrace"
-	    }, " }"));
-	  })
-	});
+	  }
+	  return value;
+	}
 	
 	// Registration
 	function supportsObject(object, type) {
 	  if (!isGrip(object)) {
 	    return false;
 	  }
 	  return object.preview && object.preview.ownProperties;
 	}
 	
 	// Grip is used in propIterator and has to be defined here.
 	let Grip = {
-	  rep: GripRep,
-	  supportsObject: supportsObject
+	  rep: wrapRender(GripRep),
+	  supportsObject
 	};
 	
 	// Exports from this module
 	module.exports = Grip;
 
 /***/ },
 /* 16 */
 /***/ function(module, exports, __webpack_require__) {
 
 	// Dependencies
-	const React = __webpack_require__(3);
-	
-	const { wrapRender } = __webpack_require__(4);
+	const React = __webpack_require__(4);
+	
+	const { wrapRender } = __webpack_require__(3);
 	
 	// Shortcuts
 	const { span } = React.DOM;
 	
 	/**
 	 * Renders a symbol.
 	 */
-	const SymbolRep = React.createClass({
-	  displayName: "SymbolRep",
-	
-	  propTypes: {
-	    object: React.PropTypes.object.isRequired
-	  },
-	
-	  render: wrapRender(function () {
-	    let { object } = this.props;
-	    let { name } = object;
-	
-	    return span({ className: "objectBox objectBox-symbol" }, `Symbol(${name || ""})`);
-	  })
-	});
+	SymbolRep.propTypes = {
+	  object: React.PropTypes.object.isRequired
+	};
+	
+	function SymbolRep(props) {
+	  let { object } = props;
+	  let { name } = object;
+	
+	  return span({ className: "objectBox objectBox-symbol" }, `Symbol(${name || ""})`);
+	}
 	
 	function supportsObject(object, type) {
 	  return type == "symbol";
 	}
 	
 	// Exports from this module
 	module.exports = {
-	  rep: SymbolRep,
-	  supportsObject: supportsObject
+	  rep: wrapRender(SymbolRep),
+	  supportsObject
 	};
 
 /***/ },
 /* 17 */
 /***/ function(module, exports, __webpack_require__) {
 
 	// Dependencies
-	const React = __webpack_require__(3);
-	
-	const { wrapRender } = __webpack_require__(4);
+	const React = __webpack_require__(4);
+	
+	const { wrapRender } = __webpack_require__(3);
 	
 	// Shortcuts
 	const { span } = React.DOM;
 	
 	/**
 	 * Renders a Infinity object
 	 */
-	const InfinityRep = React.createClass({
-	  displayName: "Infinity",
-	
-	  propTypes: {
-	    object: React.PropTypes.object.isRequired
-	  },
-	
-	  render: wrapRender(function () {
-	    return span({ className: "objectBox objectBox-number" }, this.props.object.type);
-	  })
-	});
+	InfinityRep.propTypes = {
+	  object: React.PropTypes.object.isRequired
+	};
+	
+	function InfinityRep(props) {
+	  return span({ className: "objectBox objectBox-number" }, props.object.type);
+	}
 	
 	function supportsObject(object, type) {
 	  return type == "Infinity" || type == "-Infinity";
 	}
 	
 	// Exports from this module
 	module.exports = {
-	  rep: InfinityRep,
-	  supportsObject: supportsObject
+	  rep: wrapRender(InfinityRep),
+	  supportsObject
 	};
 
 /***/ },
 /* 18 */
 /***/ function(module, exports, __webpack_require__) {
 
 	// Dependencies
-	const React = __webpack_require__(3);
-	
-	const { wrapRender } = __webpack_require__(4);
+	const React = __webpack_require__(4);
+	
+	const { wrapRender } = __webpack_require__(3);
 	
 	// Shortcuts
 	const { span } = React.DOM;
 	
 	/**
 	 * Renders a NaN object
 	 */
-	const NaNRep = React.createClass({
-	  displayName: "NaN",
-	
-	  render: wrapRender(function () {
-	    return span({ className: "objectBox objectBox-nan" }, "NaN");
-	  })
-	});
+	function NaNRep(props) {
+	  return span({ className: "objectBox objectBox-nan" }, "NaN");
+	}
 	
 	function supportsObject(object, type) {
 	  return type == "NaN";
 	}
 	
 	// Exports from this module
 	module.exports = {
-	  rep: NaNRep,
-	  supportsObject: supportsObject
+	  rep: wrapRender(NaNRep),
+	  supportsObject
 	};
 
 /***/ },
 /* 19 */
 /***/ function(module, exports, __webpack_require__) {
 
 	// ReactJS
-	const React = __webpack_require__(3);
+	const React = __webpack_require__(4);
 	
 	// Reps
 	const {
-	  createFactories,
 	  isGrip,
+	  safeObjectLink,
 	  wrapRender
-	} = __webpack_require__(4);
-	const StringRep = __webpack_require__(8);
+	} = __webpack_require__(3);
+	const { rep: StringRep } = __webpack_require__(8);
 	
 	// Shortcuts
 	const { span } = React.DOM;
-	const { rep: StringRepFactory } = createFactories(StringRep);
 	
 	/**
 	 * Renders DOM attribute
 	 */
-	let Attribute = React.createClass({
-	  displayName: "Attr",
-	
-	  propTypes: {
-	    object: React.PropTypes.object.isRequired,
-	    objectLink: React.PropTypes.func
-	  },
-	
-	  getTitle: function (grip) {
-	    return grip.preview.nodeName;
-	  },
-	
-	  render: wrapRender(function () {
-	    let object = this.props.object;
-	    let value = object.preview.value;
-	    let objectLink = (config, ...children) => {
-	      if (this.props.objectLink) {
-	        return this.props.objectLink(Object.assign({ object }, config), ...children);
-	      }
-	      return span(config, ...children);
-	    };
-	
-	    return objectLink({ className: "objectLink-Attr" }, span({ className: "attrTitle" }, this.getTitle(object)), span({ className: "attrEqual" }, "="), StringRepFactory({ object: value }));
-	  })
-	});
+	Attribute.propTypes = {
+	  object: React.PropTypes.object.isRequired,
+	  objectLink: React.PropTypes.func
+	};
+	
+	function Attribute(props) {
+	  let {
+	    object
+	  } = props;
+	  let value = object.preview.value;
+	
+	  return safeObjectLink(props, { className: "objectLink-Attr" }, span({ className: "attrTitle" }, getTitle(object)), span({ className: "attrEqual" }, "="), StringRep({ object: value }));
+	}
+	
+	function getTitle(grip) {
+	  return grip.preview.nodeName;
+	}
 	
 	// Registration
-	
 	function supportsObject(grip, type) {
 	  if (!isGrip(grip)) {
 	    return false;
 	  }
 	
 	  return type == "Attr" && grip.preview;
 	}
 	
 	module.exports = {
-	  rep: Attribute,
-	  supportsObject: supportsObject
+	  rep: wrapRender(Attribute),
+	  supportsObject
 	};
 
 /***/ },
 /* 20 */
 /***/ function(module, exports, __webpack_require__) {
 
 	// ReactJS
-	const React = __webpack_require__(3);
+	const React = __webpack_require__(4);
 	
 	// Reps
 	const {
 	  isGrip,
+	  safeObjectLink,
 	  wrapRender
-	} = __webpack_require__(4);
+	} = __webpack_require__(3);
 	
 	// Shortcuts
 	const { span } = React.DOM;
 	
 	/**
 	 * Used to render JS built-in Date() object.
 	 */
-	let DateTime = React.createClass({
-	  displayName: "Date",
-	
-	  propTypes: {
-	    object: React.PropTypes.object.isRequired,
-	    objectLink: React.PropTypes.func
-	  },
-	
-	  getTitle: function (grip) {
-	    if (this.props.objectLink) {
-	      return this.props.objectLink({
-	        object: grip
-	      }, grip.class + " ");
-	    }
-	    return "";
-	  },
-	
-	  render: wrapRender(function () {
-	    let grip = this.props.object;
-	    let date;
-	    try {
-	      date = span({ className: "objectBox" }, this.getTitle(grip), span({ className: "Date" }, new Date(grip.preview.timestamp).toISOString()));
-	    } catch (e) {
-	      date = span({ className: "objectBox" }, "Invalid Date");
-	    }
-	
-	    return date;
-	  })
-	});
+	DateTime.propTypes = {
+	  object: React.PropTypes.object.isRequired,
+	  objectLink: React.PropTypes.func
+	};
+	
+	function DateTime(props) {
+	  let grip = props.object;
+	  let date;
+	  try {
+	    date = span({ className: "objectBox" }, getTitle(props, grip), span({ className: "Date" }, new Date(grip.preview.timestamp).toISOString()));
+	  } catch (e) {
+	    date = span({ className: "objectBox" }, "Invalid Date");
+	  }
+	
+	  return date;
+	}
+	
+	function getTitle(props, grip) {
+	  return safeObjectLink(props, {}, grip.class + " ");
+	}
 	
 	// Registration
-	
 	function supportsObject(grip, type) {
 	  if (!isGrip(grip)) {
 	    return false;
 	  }
 	
 	  return type == "Date" && grip.preview;
 	}
 	
 	// Exports from this module
 	module.exports = {
-	  rep: DateTime,
-	  supportsObject: supportsObject
+	  rep: wrapRender(DateTime),
+	  supportsObject
 	};
 
 /***/ },
 /* 21 */
 /***/ function(module, exports, __webpack_require__) {
 
 	// ReactJS
-	const React = __webpack_require__(3);
+	const React = __webpack_require__(4);
 	
 	// Reps
 	const {
 	  isGrip,
 	  getURLDisplayString,
+	  safeObjectLink,
 	  wrapRender
-	} = __webpack_require__(4);
+	} = __webpack_require__(3);
 	
 	// Shortcuts
 	const { span } = React.DOM;
 	
 	/**
 	 * Renders DOM document object.
 	 */
-	let Document = React.createClass({
-	  displayName: "Document",
-	
-	  propTypes: {
-	    object: React.PropTypes.object.isRequired,
-	    objectLink: React.PropTypes.func
-	  },
-	
-	  getLocation: function (grip) {
-	    let location = grip.preview.location;
-	    return location ? getURLDisplayString(location) : "";
-	  },
-	
-	  getTitle: function (grip) {
-	    if (this.props.objectLink) {
-	      return span({ className: "objectBox" }, this.props.objectLink({
-	        object: grip
-	      }, grip.class + " "));
-	    }
-	    return "";
-	  },
-	
-	  getTooltip: function (doc) {
-	    return doc.location.href;
-	  },
-	
-	  render: wrapRender(function () {
-	    let grip = this.props.object;
-	
-	    return span({ className: "objectBox objectBox-object" }, this.getTitle(grip), span({ className: "objectPropValue" }, this.getLocation(grip)));
-	  })
-	});
+	Document.propTypes = {
+	  object: React.PropTypes.object.isRequired,
+	  objectLink: React.PropTypes.func
+	};
+	
+	function Document(props) {
+	  let grip = props.object;
+	
+	  return span({ className: "objectBox objectBox-object" }, getTitle(props, grip), span({ className: "objectPropValue" }, getLocation(grip)));
+	}
+	
+	function getLocation(grip) {
+	  let location = grip.preview.location;
+	  return location ? getURLDisplayString(location) : "";
+	}
+	
+	function getTitle(props, grip) {
+	  return safeObjectLink(props, {}, grip.class + " ");
+	}
 	
 	// Registration
-	
 	function supportsObject(object, type) {
 	  if (!isGrip(object)) {
 	    return false;
 	  }
 	
 	  return object.preview && type == "HTMLDocument";
 	}
 	
 	// Exports from this module
 	module.exports = {
-	  rep: Document,
-	  supportsObject: supportsObject
+	  rep: wrapRender(Document),
+	  supportsObject
 	};
 
 /***/ },
 /* 22 */
 /***/ function(module, exports, __webpack_require__) {
 
 	// ReactJS
-	const React = __webpack_require__(3);
+	const React = __webpack_require__(4);
 	
 	// Reps
 	const {
-	  createFactories,
 	  isGrip,
 	  wrapRender
-	} = __webpack_require__(4);
-	
-	const { rep } = createFactories(__webpack_require__(15));
+	} = __webpack_require__(3);
+	
 	const { MODE } = __webpack_require__(1);
+	const { rep } = __webpack_require__(15);
 	
 	/**
 	 * Renders DOM event objects.
 	 */
-	let Event = React.createClass({
-	  displayName: "event",
-	
-	  propTypes: {
-	    object: React.PropTypes.object.isRequired,
-	    objectLink: React.PropTypes.func,
-	    // @TODO Change this to Object.values once it's supported in Node's version of V8
-	    mode: React.PropTypes.oneOf(Object.keys(MODE).map(key => MODE[key])),
-	    attachedActorIds: React.PropTypes.array,
-	    onDOMNodeMouseOver: React.PropTypes.func,
-	    onDOMNodeMouseOut: React.PropTypes.func,
-	    onInspectIconClick: React.PropTypes.func
-	  },
-	
-	  getTitle: function (props) {
-	    let preview = props.object.preview;
-	    let title = preview.type;
-	
-	    if (preview.eventKind == "key" && preview.modifiers && preview.modifiers.length) {
-	      title = `${title} ${preview.modifiers.join("-")}`;
-	    }
-	    return title;
-	  },
-	
-	  render: wrapRender(function () {
-	    // Use `Object.assign` to keep `this.props` without changes because:
-	    // 1. JSON.stringify/JSON.parse is slow.
-	    // 2. Immutable.js is planned for the future.
-	    let gripProps = Object.assign({}, this.props, {
-	      title: this.getTitle(this.props)
+	Event.propTypes = {
+	  object: React.PropTypes.object.isRequired,
+	  objectLink: React.PropTypes.func,
+	  // @TODO Change this to Object.values once it's supported in Node's version of V8
+	  mode: React.PropTypes.oneOf(Object.keys(MODE).map(key => MODE[key])),
+	  onDOMNodeMouseOver: React.PropTypes.func,
+	  onDOMNodeMouseOut: React.PropTypes.func,
+	  onInspectIconClick: React.PropTypes.func
+	};
+	
+	function Event(props) {
+	  // Use `Object.assign` to keep `props` without changes because:
+	  // 1. JSON.stringify/JSON.parse is slow.
+	  // 2. Immutable.js is planned for the future.
+	  let gripProps = Object.assign({}, props, {
+	    title: getTitle(props)
+	  });
+	  gripProps.object = Object.assign({}, props.object);
+	  gripProps.object.preview = Object.assign({}, props.object.preview);
+	
+	  gripProps.object.preview.ownProperties = {};
+	  if (gripProps.object.preview.target) {
+	    Object.assign(gripProps.object.preview.ownProperties, {
+	      target: gripProps.object.preview.target
 	    });
-	    gripProps.object = Object.assign({}, this.props.object);
-	    gripProps.object.preview = Object.assign({}, this.props.object.preview);
-	
-	    gripProps.object.preview.ownProperties = {};
-	    if (gripProps.object.preview.target) {
-	      Object.assign(gripProps.object.preview.ownProperties, {
-	        target: gripProps.object.preview.target
-	      });
-	    }
-	    Object.assign(gripProps.object.preview.ownProperties, gripProps.object.preview.properties);
-	
-	    delete gripProps.object.preview.properties;
-	    gripProps.object.ownPropertyLength = Object.keys(gripProps.object.preview.ownProperties).length;
-	
-	    switch (gripProps.object.class) {
-	      case "MouseEvent":
-	        gripProps.isInterestingProp = (type, value, name) => {
-	          return ["target", "clientX", "clientY", "layerX", "layerY"].includes(name);
-	        };
-	        break;
-	      case "KeyboardEvent":
-	        gripProps.isInterestingProp = (type, value, name) => {
-	          return ["target", "key", "charCode", "keyCode"].includes(name);
-	        };
-	        break;
-	      case "MessageEvent":
-	        gripProps.isInterestingProp = (type, value, name) => {
-	          return ["target", "isTrusted", "data"].includes(name);
-	        };
-	        break;
-	      default:
-	        gripProps.isInterestingProp = (type, value, name) => {
-	          // We want to show the properties in the order they are declared.
-	          return Object.keys(gripProps.object.preview.ownProperties).includes(name);
-	        };
-	    }
-	
-	    return rep(gripProps);
-	  })
-	});
+	  }
+	  Object.assign(gripProps.object.preview.ownProperties, gripProps.object.preview.properties);
+	
+	  delete gripProps.object.preview.properties;
+	  gripProps.object.ownPropertyLength = Object.keys(gripProps.object.preview.ownProperties).length;
+	
+	  switch (gripProps.object.class) {
+	    case "MouseEvent":
+	      gripProps.isInterestingProp = (type, value, name) => {
+	        return ["target", "clientX", "clientY", "layerX", "layerY"].includes(name);
+	      };
+	      break;
+	    case "KeyboardEvent":
+	      gripProps.isInterestingProp = (type, value, name) => {
+	        return ["target", "key", "charCode", "keyCode"].includes(name);
+	      };
+	      break;
+	    case "MessageEvent":
+	      gripProps.isInterestingProp = (type, value, name) => {
+	        return ["target", "isTrusted", "data"].includes(name);
+	      };
+	      break;
+	    default:
+	      gripProps.isInterestingProp = (type, value, name) => {
+	        // We want to show the properties in the order they are declared.
+	        return Object.keys(gripProps.object.preview.ownProperties).includes(name);
+	      };
+	  }
+	
+	  return rep(gripProps);
+	}
+	
+	function getTitle(props) {
+	  let preview = props.object.preview;
+	  let title = preview.type;
+	
+	  if (preview.eventKind == "key" && preview.modifiers && preview.modifiers.length) {
+	    title = `${title} ${preview.modifiers.join("-")}`;
+	  }
+	  return title;
+	}
 	
 	// Registration
-	
 	function supportsObject(grip, type) {
 	  if (!isGrip(grip)) {
 	    return false;
 	  }
 	
 	  return grip.preview && grip.preview.kind == "DOMEvent";
 	}
 	
 	// Exports from this module
 	module.exports = {
-	  rep: Event,
-	  supportsObject: supportsObject
+	  rep: wrapRender(Event),
+	  supportsObject
 	};
 
 /***/ },
 /* 23 */
 /***/ function(module, exports, __webpack_require__) {
 
 	// ReactJS
-	const React = __webpack_require__(3);
+	const React = __webpack_require__(4);
 	
 	// Reps
 	const {
 	  isGrip,
 	  cropString,
+	  safeObjectLink,
 	  wrapRender
-	} = __webpack_require__(4);
+	} = __webpack_require__(3);
 	
 	// Shortcuts
 	const { span } = React.DOM;
 	
 	/**
 	 * This component represents a template for Function objects.
 	 */
-	let Func = React.createClass({
-	  displayName: "Func",
-	
-	  propTypes: {
-	    object: React.PropTypes.object.isRequired,
-	    objectLink: React.PropTypes.func
-	  },
-	
-	  getTitle: function (grip) {
-	    let title = "function ";
-	    if (grip.isGenerator) {
-	      title = "function* ";
-	    }
-	    if (grip.isAsync) {
-	      title = "async " + title;
-	    }
-	
-	    if (this.props.objectLink) {
-	      return this.props.objectLink({
-	        object: grip
-	      }, title);
-	    }
-	
-	    return title;
-	  },
-	
-	  summarizeFunction: function (grip) {
-	    let name = grip.userDisplayName || grip.displayName || grip.name || "";
-	    return cropString(name + "()", 100);
-	  },
-	
-	  render: wrapRender(function () {
-	    let grip = this.props.object;
-	
-	    return (
-	      // Set dir="ltr" to prevent function parentheses from
-	      // appearing in the wrong direction
-	      span({ dir: "ltr", className: "objectBox objectBox-function" }, this.getTitle(grip), this.summarizeFunction(grip))
-	    );
-	  })
-	});
+	FunctionRep.propTypes = {
+	  object: React.PropTypes.object.isRequired,
+	  objectLink: React.PropTypes.func
+	};
+	
+	function FunctionRep(props) {
+	  let grip = props.object;
+	
+	  return (
+	    // Set dir="ltr" to prevent function parentheses from
+	    // appearing in the wrong direction
+	    span({ dir: "ltr", className: "objectBox objectBox-function" }, getTitle(props, grip), summarizeFunction(grip))
+	  );
+	}
+	
+	function getTitle(props, grip) {
+	  let title = "function ";
+	  if (grip.isGenerator) {
+	    title = "function* ";
+	  }
+	  if (grip.isAsync) {
+	    title = "async " + title;
+	  }
+	
+	  return safeObjectLink(props, {}, title);
+	}
+	
+	function summarizeFunction(grip) {
+	  let name = grip.userDisplayName || grip.displayName || grip.name || "";
+	  return cropString(name + "()", 100);
+	}
 	
 	// Registration
-	
 	function supportsObject(grip, type) {
 	  if (!isGrip(grip)) {
 	    return type == "function";
 	  }
 	
 	  return type == "Function";
 	}
 	
 	// Exports from this module
 	
 	module.exports = {
-	  rep: Func,
-	  supportsObject: supportsObject
+	  rep: wrapRender(FunctionRep),
+	  supportsObject
 	};
 
 /***/ },
 /* 24 */
 /***/ function(module, exports, __webpack_require__) {
 
 	// ReactJS
-	const React = __webpack_require__(3);
+	const React = __webpack_require__(4);
 	// Dependencies
 	const {
-	  createFactories,
 	  isGrip,
+	  safeObjectLink,
 	  wrapRender
-	} = __webpack_require__(4);
-	
-	const PropRep = React.createFactory(__webpack_require__(14));
+	} = __webpack_require__(3);
+	
+	const PropRep = __webpack_require__(14);
 	const { MODE } = __webpack_require__(1);
 	// Shortcuts
 	const { span } = React.DOM;
 	
 	/**
 	 * Renders a DOM Promise object.
 	 */
-	const PromiseRep = React.createClass({
-	  displayName: "Promise",
-	
-	  propTypes: {
-	    object: React.PropTypes.object.isRequired,
-	    // @TODO Change this to Object.values once it's supported in Node's version of V8
-	    mode: React.PropTypes.oneOf(Object.keys(MODE).map(key => MODE[key])),
-	    objectLink: React.PropTypes.func,
-	    attachedActorIds: React.PropTypes.array,
-	    onDOMNodeMouseOver: React.PropTypes.func,
-	    onDOMNodeMouseOut: React.PropTypes.func,
-	    onInspectIconClick: React.PropTypes.func
-	  },
-	
-	  getTitle: function (object) {
-	    const title = object.class;
-	    return this.safeObjectLink({}, title);
-	  },
-	
-	  getProps: function (promiseState) {
-	    const keys = ["state"];
-	    if (Object.keys(promiseState).includes("value")) {
-	      keys.push("value");
-	    }
-	
-	    return keys.map((key, i) => {
-	      let object = promiseState[key];
-	      return PropRep(Object.assign({}, this.props, {
-	        mode: MODE.TINY,
-	        name: `<${key}>`,
-	        object,
-	        equal: ": ",
-	        delim: i < keys.length - 1 ? ", " : "",
-	        suppressQuotes: true
-	      }));
-	    });
-	  },
-	
-	  safeObjectLink: function (config, ...children) {
-	    if (this.props.objectLink) {
-	      return this.props.objectLink(Object.assign({
-	        object: this.props.object
-	      }, config), ...children);
-	    }
-	
-	    if (Object.keys(config).length === 0 && children.length === 1) {
-	      return children[0];
-	    }
-	
-	    return span(config, ...children);
-	  },
-	
-	  render: wrapRender(function () {
-	    const object = this.props.object;
-	    const { promiseState } = object;
-	
-	    if (this.props.mode === MODE.TINY) {
-	      let { Rep } = createFactories(__webpack_require__(2));
-	
-	      return span({ className: "objectBox objectBox-object" }, this.getTitle(object), this.safeObjectLink({
-	        className: "objectLeftBrace"
-	      }, " { "), Rep({ object: promiseState.state }), this.safeObjectLink({
-	        className: "objectRightBrace"
-	      }, " }"));
-	    }
-	
-	    const propsArray = this.getProps(promiseState);
-	    return span({ className: "objectBox objectBox-object" }, this.getTitle(object), this.safeObjectLink({
+	PromiseRep.propTypes = {
+	  object: React.PropTypes.object.isRequired,
+	  // @TODO Change this to Object.values once it's supported in Node's version of V8
+	  mode: React.PropTypes.oneOf(Object.keys(MODE).map(key => MODE[key])),
+	  objectLink: React.PropTypes.func,
+	  onDOMNodeMouseOver: React.PropTypes.func,
+	  onDOMNodeMouseOut: React.PropTypes.func,
+	  onInspectIconClick: React.PropTypes.func
+	};
+	
+	function PromiseRep(props) {
+	  const object = props.object;
+	  const { promiseState } = object;
+	
+	  if (props.mode === MODE.TINY) {
+	    let { Rep } = __webpack_require__(2);
+	
+	    return span({ className: "objectBox objectBox-object" }, getTitle(props, object), safeObjectLink(props, {
 	      className: "objectLeftBrace"
-	    }, " { "), ...propsArray, this.safeObjectLink({
+	    }, " { "), Rep({ object: promiseState.state }), safeObjectLink(props, {
 	      className: "objectRightBrace"
 	    }, " }"));
-	  })
-	});
+	  }
+	
+	  const propsArray = getProps(props, promiseState);
+	  return span({ className: "objectBox objectBox-object" }, getTitle(props, object), safeObjectLink(props, {
+	    className: "objectLeftBrace"
+	  }, " { "), ...propsArray, safeObjectLink(props, {
+	    className: "objectRightBrace"
+	  }, " }"));
+	}
+	
+	function getTitle(props, object) {
+	  const title = object.class;
+	  return safeObjectLink(props, {}, title);
+	}
+	
+	function getProps(props, promiseState) {
+	  const keys = ["state"];
+	  if (Object.keys(promiseState).includes("value")) {
+	    keys.push("value");
+	  }
+	
+	  return keys.map((key, i) => {
+	    let object = promiseState[key];
+	    return PropRep(Object.assign({}, props, {
+	      mode: MODE.TINY,
+	      name: `<${key}>`,
+	      object,
+	      equal: ": ",
+	      delim: i < keys.length - 1 ? ", " : null,
+	      suppressQuotes: true
+	    }));
+	  });
+	}
 	
 	// Registration
 	function supportsObject(object, type) {
 	  if (!isGrip(object)) {
 	    return false;
 	  }
 	  return type === "Promise";
 	}
 	
 	// Exports from this module
 	module.exports = {
-	  rep: PromiseRep,
-	  supportsObject: supportsObject
+	  rep: wrapRender(PromiseRep),
+	  supportsObject
 	};
 
 /***/ },
 /* 25 */
 /***/ function(module, exports, __webpack_require__) {
 
 	// ReactJS
-	const React = __webpack_require__(3);
+	const React = __webpack_require__(4);
 	
 	// Reps
 	const {
 	  isGrip,
+	  safeObjectLink,
 	  wrapRender
-	} = __webpack_require__(4);
-	
-	// Shortcuts
-	const { span } = React.DOM;
+	} = __webpack_require__(3);
 	
 	/**
 	 * Renders a grip object with regular expression.
 	 */
-	let RegExp = React.createClass({
-	  displayName: "regexp",
-	
-	  propTypes: {
-	    object: React.PropTypes.object.isRequired,
-	    objectLink: React.PropTypes.func
-	  },
-	
-	  getSource: function (grip) {
-	    return grip.displayString;
-	  },
-	
-	  render: wrapRender(function () {
-	    let { object } = this.props;
-	    let objectLink = (config, ...children) => {
-	      if (this.props.objectLink) {
-	        return this.props.objectLink(Object.assign({ object }, config), ...children);
-	      }
-	      return span(config, ...children);
-	    };
-	
-	    return objectLink({
-	      className: "objectBox objectBox-regexp regexpSource"
-	    }, this.getSource(object));
-	  })
-	});
+	RegExp.propTypes = {
+	  object: React.PropTypes.object.isRequired,
+	  objectLink: React.PropTypes.func
+	};
+	
+	function RegExp(props) {
+	  let { object } = props;
+	
+	  return safeObjectLink(props, {
+	    className: "objectBox objectBox-regexp regexpSource"
+	  }, getSource(object));
+	}
+	
+	function getSource(grip) {
+	  return grip.displayString;
+	}
 	
 	// Registration
-	
 	function supportsObject(object, type) {
 	  if (!isGrip(object)) {
 	    return false;
 	  }
 	
 	  return type == "RegExp";
 	}
 	
 	// Exports from this module
 	module.exports = {
-	  rep: RegExp,
-	  supportsObject: supportsObject
+	  rep: wrapRender(RegExp),
+	  supportsObject
 	};
 
 /***/ },
 /* 26 */
 /***/ function(module, exports, __webpack_require__) {
 
 	// ReactJS
-	const React = __webpack_require__(3);
+	const React = __webpack_require__(4);
 	
 	// Reps
 	const {
 	  isGrip,
 	  getURLDisplayString,
+	  safeObjectLink,
 	  wrapRender
-	} = __webpack_require__(4);
+	} = __webpack_require__(3);
 	
 	// Shortcuts
-	const DOM = React.DOM;
+	const { span } = React.DOM;
 	
 	/**
 	 * Renders a grip representing CSSStyleSheet
 	 */
-	let StyleSheet = React.createClass({
-	  displayName: "object",
-	
-	  propTypes: {
-	    object: React.PropTypes.object.isRequired,
-	    objectLink: React.PropTypes.func
-	  },
-	
-	  getTitle: function (grip) {
-	    let title = "StyleSheet ";
-	    if (this.props.objectLink) {
-	      return DOM.span({ className: "objectBox" }, this.props.objectLink({
-	        object: grip
-	      }, title));
-	    }
-	    return title;
-	  },
-	
-	  getLocation: function (grip) {
-	    // Embedded stylesheets don't have URL and so, no preview.
-	    let url = grip.preview ? grip.preview.url : "";
-	    return url ? getURLDisplayString(url) : "";
-	  },
-	
-	  render: wrapRender(function () {
-	    let grip = this.props.object;
-	
-	    return DOM.span({ className: "objectBox objectBox-object" }, this.getTitle(grip), DOM.span({ className: "objectPropValue" }, this.getLocation(grip)));
-	  })
-	});
+	StyleSheet.propTypes = {
+	  object: React.PropTypes.object.isRequired,
+	  objectLink: React.PropTypes.func
+	};
+	
+	function StyleSheet(props) {
+	  let grip = props.object;
+	
+	  return span({ className: "objectBox objectBox-object" }, getTitle(props, grip), span({ className: "objectPropValue" }, getLocation(grip)));
+	}
+	
+	function getTitle(props, grip) {
+	  let title = "StyleSheet ";
+	  return safeObjectLink(props, { className: "objectBox" }, title);
+	}
+	
+	function getLocation(grip) {
+	  // Embedded stylesheets don't have URL and so, no preview.
+	  let url = grip.preview ? grip.preview.url : "";
+	  return url ? getURLDisplayString(url) : "";
+	}
 	
 	// Registration
-	
 	function supportsObject(object, type) {
 	  if (!isGrip(object)) {
 	    return false;
 	  }
 	
 	  return type == "CSSStyleSheet";
 	}
 	
 	// Exports from this module
 	
 	module.exports = {
-	  rep: StyleSheet,
-	  supportsObject: supportsObject
+	  rep: wrapRender(StyleSheet),
+	  supportsObject
 	};
 
 /***/ },
 /* 27 */
 /***/ function(module, exports, __webpack_require__) {
 
 	// Dependencies
-	const React = __webpack_require__(3);
+	const React = __webpack_require__(4);
 	const {
 	  isGrip,
 	  cropString,
 	  cropMultipleLines,
 	  wrapRender
-	} = __webpack_require__(4);
+	} = __webpack_require__(3);
 	const { MODE } = __webpack_require__(1);
 	const nodeConstants = __webpack_require__(5);
 	
 	// Shortcuts
 	const { span } = React.DOM;
 	
 	/**
 	 * Renders DOM comment node.
 	 */
-	const CommentNode = React.createClass({
-	  displayName: "CommentNode",
-	
-	  propTypes: {
-	    object: React.PropTypes.object.isRequired,
-	    // @TODO Change this to Object.values once it's supported in Node's version of V8
-	    mode: React.PropTypes.oneOf(Object.keys(MODE).map(key => MODE[key]))
-	  },
-	
-	  render: wrapRender(function () {
-	    let {
-	      object,
-	      mode = MODE.SHORT
-	    } = this.props;
-	
-	    let { textContent } = object.preview;
-	    if (mode === MODE.TINY) {
-	      textContent = cropMultipleLines(textContent, 30);
-	    } else if (mode === MODE.SHORT) {
-	      textContent = cropString(textContent, 50);
-	    }
-	
-	    return span({ className: "objectBox theme-comment" }, `<!-- ${textContent} -->`);
-	  })
-	});
+	CommentNode.propTypes = {
+	  object: React.PropTypes.object.isRequired,
+	  // @TODO Change this to Object.values once it's supported in Node's version of V8
+	  mode: React.PropTypes.oneOf(Object.keys(MODE).map(key => MODE[key]))
+	};
+	
+	function CommentNode(props) {
+	  let {
+	    object,
+	    mode = MODE.SHORT
+	  } = props;
+	
+	  let { textContent } = object.preview;
+	  if (mode === MODE.TINY) {
+	    textContent = cropMultipleLines(textContent, 30);
+	  } else if (mode === MODE.SHORT) {
+	    textContent = cropString(textContent, 50);
+	  }
+	
+	  return span({ className: "objectBox theme-comment" }, `<!-- ${textContent} -->`);
+	}
 	
 	// Registration
 	function supportsObject(object, type) {
 	  if (!isGrip(object)) {
 	    return false;
 	  }
 	  return object.preview && object.preview.nodeType === nodeConstants.COMMENT_NODE;
 	}
 	
 	// Exports from this module
 	module.exports = {
-	  rep: CommentNode,
-	  supportsObject: supportsObject
+	  rep: wrapRender(CommentNode),
+	  supportsObject
 	};
 
 /***/ },
 /* 28 */
 /***/ function(module, exports, __webpack_require__) {
 
 	// ReactJS
-	const React = __webpack_require__(3);
+	const React = __webpack_require__(4);
 	
 	// Utils
 	const {
 	  isGrip,
+	  safeObjectLink,
 	  wrapRender
-	} = __webpack_require__(4);
+	} = __webpack_require__(3);
 	const { MODE } = __webpack_require__(1);
 	const nodeConstants = __webpack_require__(5);
 	const Svg = __webpack_require__(29);
 	
 	// Shortcuts
 	const { span } = React.DOM;
 	
 	/**
 	 * Renders DOM element node.
 	 */
-	const ElementNode = React.createClass({
-	  displayName: "ElementNode",
-	
-	  propTypes: {
-	    object: React.PropTypes.object.isRequired,
-	    // @TODO Change this to Object.values once it's supported in Node's version of V8
-	    mode: React.PropTypes.oneOf(Object.keys(MODE).map(key => MODE[key])),
-	    attachedActorIds: React.PropTypes.array,
-	    onDOMNodeMouseOver: React.PropTypes.func,
-	    onDOMNodeMouseOut: React.PropTypes.func,
-	    onInspectIconClick: React.PropTypes.func,
-	    objectLink: React.PropTypes.func
-	  },
-	
-	  getElements: function (grip, mode) {
-	    let { attributes, nodeName } = grip.preview;
-	    const nodeNameElement = span({
-	      className: "tag-name theme-fg-color3"
-	    }, nodeName);
-	
-	    if (mode === MODE.TINY) {
-	      let elements = [nodeNameElement];
-	      if (attributes.id) {
-	        elements.push(span({ className: "attr-name theme-fg-color2" }, `#${attributes.id}`));
-	      }
-	      if (attributes.class) {
-	        elements.push(span({ className: "attr-name theme-fg-color2" }, attributes.class.replace(/(^\s+)|(\s+$)/g, "").split(" ").map(cls => `.${cls}`).join("")));
-	      }
-	      return elements;
+	ElementNode.propTypes = {
+	  object: React.PropTypes.object.isRequired,
+	  // @TODO Change this to Object.values once it's supported in Node's version of V8
+	  mode: React.PropTypes.oneOf(Object.keys(MODE).map(key => MODE[key])),
+	  onDOMNodeMouseOver: React.PropTypes.func,
+	  onDOMNodeMouseOut: React.PropTypes.func,
+	  onInspectIconClick: React.PropTypes.func,
+	  objectLink: React.PropTypes.func
+	};
+	
+	function ElementNode(props) {
+	  let {
+	    object,
+	    mode,
+	    onDOMNodeMouseOver,
+	    onDOMNodeMouseOut,
+	    onInspectIconClick
+	  } = props;
+	  let elements = getElements(object, mode);
+	
+	  let isInTree = object.preview && object.preview.isConnected === true;
+	
+	  let baseConfig = { className: "objectBox objectBox-node" };
+	  let inspectIcon;
+	  if (isInTree) {
+	    if (onDOMNodeMouseOver) {
+	      Object.assign(baseConfig, {
+	        onMouseOver: _ => onDOMNodeMouseOver(object)
+	      });
+	    }
+	
+	    if (onDOMNodeMouseOut) {
+	      Object.assign(baseConfig, {
+	        onMouseOut: onDOMNodeMouseOut
+	      });
 	    }
-	    let attributeElements = Object.keys(attributes).sort(function getIdAndClassFirst(a1, a2) {
-	      if ([a1, a2].includes("id")) {
-	        return 3 * (a1 === "id" ? -1 : 1);
-	      }
-	      if ([a1, a2].includes("class")) {
-	        return 2 * (a1 === "class" ? -1 : 1);
-	      }
-	
-	      // `id` and `class` excepted,
-	      // we want to keep the same order that in `attributes`.
-	      return 0;
-	    }).reduce((arr, name, i, keys) => {
-	      let value = attributes[name];
-	      let attribute = span({}, span({ className: "attr-name theme-fg-color2" }, `${name}`), `="`, span({ className: "attr-value theme-fg-color6" }, `${value}`), `"`);
-	
-	      return arr.concat([" ", attribute]);
-	    }, []);
-	
-	    return ["<", nodeNameElement, ...attributeElements, ">"];
-	  },
-	
-	  render: wrapRender(function () {
-	    let {
-	      object,
-	      mode,
-	      attachedActorIds,
-	      onDOMNodeMouseOver,
-	      onDOMNodeMouseOut,
-	      onInspectIconClick
-	    } = this.props;
-	    let elements = this.getElements(object, mode);
-	    let objectLink = (config, ...children) => {
-	      if (this.props.objectLink) {
-	        return this.props.objectLink(Object.assign({ object }, config), ...children);
-	      }
-	      return span(config, ...children);
-	    };
-	
-	    let isInTree = attachedActorIds ? attachedActorIds.includes(object.actor) : true;
-	
-	    let baseConfig = { className: "objectBox objectBox-node" };
-	    let inspectIcon;
-	    if (isInTree) {
-	      if (onDOMNodeMouseOver) {
-	        Object.assign(baseConfig, {
-	          onMouseOver: _ => onDOMNodeMouseOver(object)
-	        });
-	      }
-	
-	      if (onDOMNodeMouseOut) {
-	        Object.assign(baseConfig, {
-	          onMouseOut: onDOMNodeMouseOut
-	        });
-	      }
-	
-	      if (onInspectIconClick) {
-	        inspectIcon = Svg("open-inspector", {
-	          element: "a",
-	          draggable: false,
-	          // TODO: Localize this with "openNodeInInspector" when Bug 1317038 lands
-	          title: "Click to select the node in the inspector",
-	          onClick: e => onInspectIconClick(object, e)
-	        });
-	      }
+	
+	    if (onInspectIconClick) {
+	      inspectIcon = Svg("open-inspector", {
+	        element: "a",
+	        draggable: false,
+	        // TODO: Localize this with "openNodeInInspector" when Bug 1317038 lands
+	        title: "Click to select the node in the inspector",
+	        onClick: e => onInspectIconClick(object, e)
+	      });
+	    }
+	  }
+	
+	  return span(baseConfig, safeObjectLink(props, {}, ...elements), inspectIcon);
+	}
+	
+	function getElements(grip, mode) {
+	  let { attributes, nodeName } = grip.preview;
+	  const nodeNameElement = span({
+	    className: "tag-name theme-fg-color3"
+	  }, nodeName);
+	
+	  if (mode === MODE.TINY) {
+	    let elements = [nodeNameElement];
+	    if (attributes.id) {
+	      elements.push(span({ className: "attr-name theme-fg-color2" }, `#${attributes.id}`));
 	    }
-	
-	    return span(baseConfig, objectLink({}, ...elements), inspectIcon);
-	  })
-	});
+	    if (attributes.class) {
+	      elements.push(span({ className: "attr-name theme-fg-color2" }, attributes.class.replace(/(^\s+)|(\s+$)/g, "").split(" ").map(cls => `.${cls}`).join("")));
+	    }
+	    return elements;
+	  }
+	  let attributeElements = Object.keys(attributes).sort(function getIdAndClassFirst(a1, a2) {
+	    if ([a1, a2].includes("id")) {
+	      return 3 * (a1 === "id" ? -1 : 1);
+	    }
+	    if ([a1, a2].includes("class")) {
+	      return 2 * (a1 === "class" ? -1 : 1);
+	    }
+	
+	    // `id` and `class` excepted,
+	    // we want to keep the same order that in `attributes`.
+	    return 0;
+	  }).reduce((arr, name, i, keys) => {
+	    let value = attributes[name];
+	    let attribute = span({}, span({ className: "attr-name theme-fg-color2" }, `${name}`), `="`, span({ className: "attr-value theme-fg-color6" }, `${value}`), `"`);
+	
+	    return arr.concat([" ", attribute]);
+	  }, []);
+	
+	  return ["<", nodeNameElement, ...attributeElements, ">"];
+	}
 	
 	// Registration
 	function supportsObject(object, type) {
 	  if (!isGrip(object)) {
 	    return false;
 	  }
 	  return object.preview && object.preview.nodeType === nodeConstants.ELEMENT_NODE;
 	}
 	
 	// Exports from this module
 	module.exports = {
-	  rep: ElementNode,
-	  supportsObject: supportsObject
+	  rep: wrapRender(ElementNode),
+	  supportsObject
 	};
 
 /***/ },
 /* 29 */
 /***/ function(module, exports, __webpack_require__) {
 
-	const React = __webpack_require__(3);
+	const React = __webpack_require__(4);
 	const InlineSVG = __webpack_require__(30);
 	
 	const svg = {
 	  "open-inspector": __webpack_require__(31)
 	};
 	
 	module.exports = function (name, props) {
 	  // eslint-disable-line
@@ -2605,17 +2397,17 @@ return /******/ (function(modules) { // 
 	function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
 	
 	function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; }
 	
 	function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
 	
 	function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
 	
-	var _react = __webpack_require__(3);
+	var _react = __webpack_require__(4);
 	
 	var _react2 = _interopRequireDefault(_react);
 	
 	var DOMParser = typeof window !== 'undefined' && window.DOMParser;
 	var process = process || {};
 	process.env = process.env || {};
 	var parserAvailable = typeof DOMParser !== 'undefined' && DOMParser.prototype != null && DOMParser.prototype.parseFromString != null;
 	
@@ -2744,792 +2536,707 @@ return /******/ (function(modules) { // 
 
 	module.exports = "<!-- This Source Code Form is subject to the terms of the Mozilla Public - License, v. 2.0. If a copy of the MPL was not distributed with this - file, You can obtain one at http://mozilla.org/MPL/2.0/. --><svg viewBox=\"0 0 16 16\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M8,3L12,3L12,7L14,7L14,8L12,8L12,12L8,12L8,14L7,14L7,12L3,12L3,8L1,8L1,7L3,7L3,3L7,3L7,1L8,1L8,3ZM10,10L10,5L5,5L5,10L10,10Z\"></path></svg>"
 
 /***/ },
 /* 32 */
 /***/ function(module, exports, __webpack_require__) {
 
 	// ReactJS
-	const React = __webpack_require__(3);
+	const React = __webpack_require__(4);
 	
 	// Reps
 	const {
 	  isGrip,
 	  cropString,
+	  safeObjectLink,
 	  wrapRender
-	} = __webpack_require__(4);
+	} = __webpack_require__(3);
 	const { MODE } = __webpack_require__(1);
 	const Svg = __webpack_require__(29);
 	
 	// Shortcuts
 	const DOM = React.DOM;
 	
 	/**
 	 * Renders DOM #text node.
 	 */
-	let TextNode = React.createClass({
-	  displayName: "TextNode",
-	
-	  propTypes: {
-	    object: React.PropTypes.object.isRequired,
-	    // @TODO Change this to Object.values once it's supported in Node's version of V8
-	    mode: React.PropTypes.oneOf(Object.keys(MODE).map(key => MODE[key])),
-	    objectLink: React.PropTypes.func,
-	    attachedActorIds: React.PropTypes.array,
-	    onDOMNodeMouseOver: React.PropTypes.func,
-	    onDOMNodeMouseOut: React.PropTypes.func,
-	    onInspectIconClick: React.PropTypes.func
-	  },
-	
-	  getTextContent: function (grip) {
-	    return cropString(grip.preview.textContent);
-	  },
-	
-	  getTitle: function (grip) {
-	    const title = "#text";
-	    if (this.props.objectLink) {
-	      return this.props.objectLink({
-	        object: grip
-	      }, title);
+	TextNode.propTypes = {
+	  object: React.PropTypes.object.isRequired,
+	  // @TODO Change this to Object.values once it's supported in Node's version of V8
+	  mode: React.PropTypes.oneOf(Object.keys(MODE).map(key => MODE[key])),
+	  objectLink: React.PropTypes.func,
+	  onDOMNodeMouseOver: React.PropTypes.func,
+	  onDOMNodeMouseOut: React.PropTypes.func,
+	  onInspectIconClick: React.PropTypes.func
+	};
+	
+	function TextNode(props) {
+	  let {
+	    object: grip,
+	    mode = MODE.SHORT,
+	    onDOMNodeMouseOver,
+	    onDOMNodeMouseOut,
+	    onInspectIconClick
+	  } = props;
+	
+	  let baseConfig = { className: "objectBox objectBox-textNode" };
+	  let inspectIcon;
+	  let isInTree = grip.preview && grip.preview.isConnected === true;
+	
+	  if (isInTree) {
+	    if (onDOMNodeMouseOver) {
+	      Object.assign(baseConfig, {
+	        onMouseOver: _ => onDOMNodeMouseOver(grip)
+	      });
 	    }
-	    return title;
-	  },
-	
-	  render: wrapRender(function () {
-	    let {
-	      object: grip,
-	      mode = MODE.SHORT,
-	      attachedActorIds,
-	      onDOMNodeMouseOver,
-	      onDOMNodeMouseOut,
-	      onInspectIconClick
-	    } = this.props;
-	
-	    let baseConfig = { className: "objectBox objectBox-textNode" };
-	    let inspectIcon;
-	    let isInTree = attachedActorIds ? attachedActorIds.includes(grip.actor) : true;
-	
-	    if (isInTree) {
-	      if (onDOMNodeMouseOver) {
-	        Object.assign(baseConfig, {
-	          onMouseOver: _ => onDOMNodeMouseOver(grip)
-	        });
-	      }
-	
-	      if (onDOMNodeMouseOut) {
-	        Object.assign(baseConfig, {
-	          onMouseOut: onDOMNodeMouseOut
-	        });
-	      }
-	
-	      if (onInspectIconClick) {
-	        inspectIcon = Svg("open-inspector", {
-	          element: "a",
-	          draggable: false,
-	          // TODO: Localize this with "openNodeInInspector" when Bug 1317038 lands
-	          title: "Click to select the node in the inspector",
-	          onClick: e => onInspectIconClick(grip, e)
-	        });
-	      }
+	
+	    if (onDOMNodeMouseOut) {
+	      Object.assign(baseConfig, {
+	        onMouseOut: onDOMNodeMouseOut
+	      });
+	    }
+	
+	    if (onInspectIconClick) {
+	      inspectIcon = Svg("open-inspector", {
+	        element: "a",
+	        draggable: false,
+	        // TODO: Localize this with "openNodeInInspector" when Bug 1317038 lands
+	        title: "Click to select the node in the inspector",
+	        onClick: e => onInspectIconClick(grip, e)
+	      });
 	    }
-	
-	    if (mode === MODE.TINY) {
-	      return DOM.span(baseConfig, this.getTitle(grip), inspectIcon);
-	    }
-	
-	    return DOM.span(baseConfig, this.getTitle(grip), DOM.span({ className: "nodeValue" }, " ", `"${this.getTextContent(grip)}"`), inspectIcon);
-	  })
-	});
+	  }
+	
+	  if (mode === MODE.TINY) {
+	    return DOM.span(baseConfig, getTitle(props, grip), inspectIcon);
+	  }
+	
+	  return DOM.span(baseConfig, getTitle(props, grip), DOM.span({ className: "nodeValue" }, " ", `"${getTextContent(grip)}"`), inspectIcon);
+	}
+	
+	function getTextContent(grip) {
+	  return cropString(grip.preview.textContent);
+	}
+	
+	function getTitle(props, grip) {
+	  const title = "#text";
+	  return safeObjectLink(props, {}, title);
+	}
 	
 	// Registration
-	
 	function supportsObject(grip, type) {
 	  if (!isGrip(grip)) {
 	    return false;
 	  }
 	
 	  return grip.preview && grip.class == "Text";
 	}
 	
 	// Exports from this module
 	module.exports = {
-	  rep: TextNode,
-	  supportsObject: supportsObject
+	  rep: wrapRender(TextNode),
+	  supportsObject
 	};
 
 /***/ },
 /* 33 */
 /***/ function(module, exports, __webpack_require__) {
 
 	// ReactJS
-	const React = __webpack_require__(3);
+	const React = __webpack_require__(4);
 	// Utils
 	const {
 	  isGrip,
+	  safeObjectLink,
 	  wrapRender
-	} = __webpack_require__(4);
+	} = __webpack_require__(3);
 	const { MODE } = __webpack_require__(1);
-	// Shortcuts
-	const { span } = React.DOM;
 	
 	/**
 	 * Renders Error objects.
 	 */
-	const ErrorRep = React.createClass({
-	  displayName: "Error",
-	
-	  propTypes: {
-	    object: React.PropTypes.object.isRequired,
-	    // @TODO Change this to Object.values once it's supported in Node's version of V8
-	    mode: React.PropTypes.oneOf(Object.keys(MODE).map(key => MODE[key])),
-	    objectLink: React.PropTypes.func
-	  },
-	
-	  render: wrapRender(function () {
-	    let object = this.props.object;
-	    let preview = object.preview;
-	    let name = preview && preview.name ? preview.name : "Error";
-	
-	    let content = this.props.mode === MODE.TINY ? name : `${name}: ${preview.message}`;
-	
-	    if (preview.stack && this.props.mode !== MODE.TINY) {
-	      /*
-	       * Since Reps are used in the JSON Viewer, we can't localize
-	       * the "Stack trace" label (defined in debugger.properties as
-	       * "variablesViewErrorStacktrace" property), until Bug 1317038 lands.
-	       */
-	      content = `${content}\nStack trace:\n${preview.stack}`;
-	    }
-	
-	    let objectLink = (config, ...children) => {
-	      if (this.props.objectLink) {
-	        return this.props.objectLink(Object.assign({ object }, config), ...children);
-	      }
-	      return span(config, ...children);
-	    };
-	
-	    return objectLink({ className: "objectBox-stackTrace" }, content);
-	  })
-	});
+	ErrorRep.propTypes = {
+	  object: React.PropTypes.object.isRequired,
+	  // @TODO Change this to Object.values once it's supported in Node's version of V8
+	  mode: React.PropTypes.oneOf(Object.keys(MODE).map(key => MODE[key])),
+	  objectLink: React.PropTypes.func
+	};
+	
+	function ErrorRep(props) {
+	  let object = props.object;
+	  let preview = object.preview;
+	  let name = preview && preview.name ? preview.name : "Error";
+	
+	  let content = props.mode === MODE.TINY ? name : `${name}: ${preview.message}`;
+	
+	  if (preview.stack && props.mode !== MODE.TINY) {
+	    /*
+	      * Since Reps are used in the JSON Viewer, we can't localize
+	      * the "Stack trace" label (defined in debugger.properties as
+	      * "variablesViewErrorStacktrace" property), until Bug 1317038 lands.
+	      */
+	    content = `${content}\nStack trace:\n${preview.stack}`;
+	  }
+	
+	  return safeObjectLink(props, { className: "objectBox-stackTrace" }, content);
+	}
 	
 	// Registration
 	function supportsObject(object, type) {
 	  if (!isGrip(object)) {
 	    return false;
 	  }
 	  return object.preview && type === "Error";
 	}
 	
 	// Exports from this module
 	module.exports = {
-	  rep: ErrorRep,
-	  supportsObject: supportsObject
+	  rep: wrapRender(ErrorRep),
+	  supportsObject
 	};
 
 /***/ },
 /* 34 */
 /***/ function(module, exports, __webpack_require__) {
 
 	// ReactJS
-	const React = __webpack_require__(3);
+	const React = __webpack_require__(4);
 	
 	// Reps
 	const {
 	  isGrip,
 	  getURLDisplayString,
+	  safeObjectLink,
 	  wrapRender
-	} = __webpack_require__(4);
+	} = __webpack_require__(3);
 	
 	const { MODE } = __webpack_require__(1);
 	
 	// Shortcuts
-	const DOM = React.DOM;
+	const { span } = React.DOM;
 	
 	/**
 	 * Renders a grip representing a window.
 	 */
-	let Window = React.createClass({
-	  displayName: "Window",
-	
-	  propTypes: {
-	    // @TODO Change this to Object.values once it's supported in Node's version of V8
-	    mode: React.PropTypes.oneOf(Object.keys(MODE).map(key => MODE[key])),
-	    object: React.PropTypes.object.isRequired,
-	    objectLink: React.PropTypes.func
-	  },
-	
-	  getTitle: function (object) {
-	    let title = object.displayClass || object.class || "Window";
-	    if (this.props.objectLink) {
-	      return DOM.span({ className: "objectBox" }, this.props.objectLink({
-	        object
-	      }, title));
-	    }
-	    return title;
-	  },
-	
-	  getLocation: function (object) {
-	    return getURLDisplayString(object.preview.url);
-	  },
-	
-	  render: wrapRender(function () {
-	    let {
-	      mode,
-	      object
-	    } = this.props;
-	
-	    if (mode === MODE.TINY) {
-	      return DOM.span({ className: "objectBox objectBox-Window" }, this.getTitle(object));
-	    }
-	
-	    return DOM.span({ className: "objectBox objectBox-Window" }, this.getTitle(object), " ", DOM.span({ className: "objectPropValue" }, this.getLocation(object)));
-	  })
-	});
+	WindowRep.propTypes = {
+	  // @TODO Change this to Object.values once it's supported in Node's version of V8
+	  mode: React.PropTypes.oneOf(Object.keys(MODE).map(key => MODE[key])),
+	  object: React.PropTypes.object.isRequired,
+	  objectLink: React.PropTypes.func
+	};
+	
+	function WindowRep(props) {
+	  let {
+	    mode,
+	    object
+	  } = props;
+	
+	  if (mode === MODE.TINY) {
+	    return span({ className: "objectBox objectBox-Window" }, getTitle(props, object));
+	  }
+	
+	  return span({ className: "objectBox objectBox-Window" }, getTitle(props, object), " ", span({ className: "objectPropValue" }, getLocation(object)));
+	}
+	
+	function getTitle(props, object) {
+	  let title = object.displayClass || object.class || "Window";
+	  return safeObjectLink(props, { className: "objectBox" }, title);
+	}
+	
+	function getLocation(object) {
+	  return getURLDisplayString(object.preview.url);
+	}
 	
 	// Registration
-	
 	function supportsObject(object, type) {
 	  if (!isGrip(object)) {
 	    return false;
 	  }
 	
 	  return object.preview && type == "Window";
 	}
 	
 	// Exports from this module
 	module.exports = {
-	  rep: Window,
-	  supportsObject: supportsObject
+	  rep: wrapRender(WindowRep),
+	  supportsObject
 	};
 
 /***/ },
 /* 35 */
 /***/ function(module, exports, __webpack_require__) {
 
 	// ReactJS
-	const React = __webpack_require__(3);
+	const React = __webpack_require__(4);
 	
 	// Reps
 	const {
 	  isGrip,
 	  wrapRender
-	} = __webpack_require__(4);
+	} = __webpack_require__(3);
 	
 	// Shortcuts
 	const { span } = React.DOM;
 	
 	/**
 	 * Renders a grip object with textual data.
 	 */
-	let ObjectWithText = React.createClass({
-	  displayName: "ObjectWithText",
-	
-	  propTypes: {
-	    object: React.PropTypes.object.isRequired,
-	    objectLink: React.PropTypes.func
-	  },
-	
-	  getTitle: function (grip) {
-	    if (this.props.objectLink) {
-	      return span({ className: "objectBox" }, this.props.objectLink({
-	        object: grip
-	      }, this.getType(grip) + " "));
-	    }
-	    return "";
-	  },
-	
-	  getType: function (grip) {
-	    return grip.class;
-	  },
-	
-	  getDescription: function (grip) {
-	    return "\"" + grip.preview.text + "\"";
-	  },
-	
-	  render: wrapRender(function () {
-	    let grip = this.props.object;
-	    return span({ className: "objectBox objectBox-" + this.getType(grip) }, this.getTitle(grip), span({ className: "objectPropValue" }, this.getDescription(grip)));
-	  })
-	});
+	ObjectWithText.propTypes = {
+	  object: React.PropTypes.object.isRequired,
+	  objectLink: React.PropTypes.func
+	};
+	
+	function ObjectWithText(props) {
+	  let grip = props.object;
+	  return span({ className: "objectBox objectBox-" + getType(grip) }, getTitle(props, grip), span({ className: "objectPropValue" }, getDescription(grip)));
+	}
+	
+	function getTitle(props, grip) {
+	  if (props.objectLink) {
+	    return span({ className: "objectBox" }, props.objectLink({
+	      object: grip
+	    }, getType(grip) + " "));
+	  }
+	  return "";
+	}
+	
+	function getType(grip) {
+	  return grip.class;
+	}
+	
+	function getDescription(grip) {
+	  return "\"" + grip.preview.text + "\"";
+	}
 	
 	// Registration
-	
 	function supportsObject(grip, type) {
 	  if (!isGrip(grip)) {
 	    return false;
 	  }
 	
 	  return grip.preview && grip.preview.kind == "ObjectWithText";
 	}
 	
 	// Exports from this module
 	module.exports = {
-	  rep: ObjectWithText,
-	  supportsObject: supportsObject
+	  rep: wrapRender(ObjectWithText),
+	  supportsObject
 	};
 
 /***/ },
 /* 36 */
 /***/ function(module, exports, __webpack_require__) {
 
 	// ReactJS
-	const React = __webpack_require__(3);
+	const React = __webpack_require__(4);
 	
 	// Reps
 	const {
 	  isGrip,
 	  getURLDisplayString,
+	  safeObjectLink,
 	  wrapRender
-	} = __webpack_require__(4);
+	} = __webpack_require__(3);
 	
 	// Shortcuts
 	const { span } = React.DOM;
 	
 	/**
 	 * Renders a grip object with URL data.
 	 */
-	let ObjectWithURL = React.createClass({
-	  displayName: "ObjectWithURL",
-	
-	  propTypes: {
-	    object: React.PropTypes.object.isRequired,
-	    objectLink: React.PropTypes.func
-	  },
-	
-	  getTitle: function (grip) {
-	    if (this.props.objectLink) {
-	      return span({ className: "objectBox" }, this.props.objectLink({
-	        object: grip
-	      }, this.getType(grip) + " "));
-	    }
-	    return "";
-	  },
-	
-	  getType: function (grip) {
-	    return grip.class;
-	  },
-	
-	  getDescription: function (grip) {
-	    return getURLDisplayString(grip.preview.url);
-	  },
-	
-	  render: wrapRender(function () {
-	    let grip = this.props.object;
-	    return span({ className: "objectBox objectBox-" + this.getType(grip) }, this.getTitle(grip), span({ className: "objectPropValue" }, this.getDescription(grip)));
-	  })
-	});
+	ObjectWithURL.propTypes = {
+	  object: React.PropTypes.object.isRequired,
+	  objectLink: React.PropTypes.func
+	};
+	
+	function ObjectWithURL(props) {
+	  let grip = props.object;
+	  return span({ className: "objectBox objectBox-" + getType(grip) }, getTitle(props, grip), span({ className: "objectPropValue" }, getDescription(grip)));
+	}
+	
+	function getTitle(props, grip) {
+	  return safeObjectLink(props, { className: "objectBox" }, getType(grip) + " ");
+	}
+	
+	function getType(grip) {
+	  return grip.class;
+	}
+	
+	function getDescription(grip) {
+	  return getURLDisplayString(grip.preview.url);
+	}
 	
 	// Registration
-	
 	function supportsObject(grip, type) {
 	  if (!isGrip(grip)) {
 	    return false;
 	  }
 	
 	  return grip.preview && grip.preview.kind == "ObjectWithURL";
 	}
 	
 	// Exports from this module
 	module.exports = {
-	  rep: ObjectWithURL,
-	  supportsObject: supportsObject
+	  rep: wrapRender(ObjectWithURL),
+	  supportsObject
 	};
 
 /***/ },
 /* 37 */
 /***/ function(module, exports, __webpack_require__) {
 
 	// Dependencies
-	const React = __webpack_require__(3);
+	const React = __webpack_require__(4);
 	const {
-	  createFactories,
 	  isGrip,
+	  safeObjectLink,
 	  wrapRender
-	} = __webpack_require__(4);
-	const Caption = React.createFactory(__webpack_require__(12));
+	} = __webpack_require__(3);
+	const Caption = __webpack_require__(12);
 	const { MODE } = __webpack_require__(1);
 	
 	// Shortcuts
 	const { span } = React.DOM;
 	
 	/**
 	 * Renders an array. The array is enclosed by left and right bracket
 	 * and the max number of rendered items depends on the current mode.
 	 */
-	let GripArray = React.createClass({
-	  displayName: "GripArray",
-	
-	  propTypes: {
-	    object: React.PropTypes.object.isRequired,
-	    // @TODO Change this to Object.values once it's supported in Node's version of V8
-	    mode: React.PropTypes.oneOf(Object.keys(MODE).map(key => MODE[key])),
-	    provider: React.PropTypes.object,
-	    objectLink: React.PropTypes.func,
-	    attachedActorIds: React.PropTypes.array,
-	    onDOMNodeMouseOver: React.PropTypes.func,
-	    onDOMNodeMouseOut: React.PropTypes.func,
-	    onInspectIconClick: React.PropTypes.func
-	  },
-	
-	  getLength: function (grip) {
-	    if (!grip.preview) {
-	      return 0;
-	    }
-	
-	    return grip.preview.length || grip.preview.childNodesLength || 0;
-	  },
-	
-	  getTitle: function (object, context) {
-	    if (this.props.mode === MODE.TINY) {
-	      return "";
-	    }
-	
-	    let title = this.props.title || object.class || "Array";
-	    return this.safeObjectLink({}, title + " ");
-	  },
-	
-	  getPreviewItems: function (grip) {
-	    if (!grip.preview) {
-	      return null;
-	    }
-	
-	    return grip.preview.items || grip.preview.childNodes || null;
-	  },
-	
-	  arrayIterator: function (grip, max) {
-	    let items = [];
-	    const gripLength = this.getLength(grip);
-	
-	    if (!gripLength) {
-	      return items;
-	    }
-	
-	    const previewItems = this.getPreviewItems(grip);
-	    if (!previewItems) {
-	      return items;
-	    }
-	
-	    let delim;
-	    // number of grip preview items is limited to 10, but we may have more
-	    // items in grip-array.
-	    let delimMax = gripLength > previewItems.length ? previewItems.length : previewItems.length - 1;
-	    let provider = this.props.provider;
-	
-	    for (let i = 0; i < previewItems.length && i < max; i++) {
-	      try {
-	        let itemGrip = previewItems[i];
-	        let value = provider ? provider.getValue(itemGrip) : itemGrip;
-	
-	        delim = i == delimMax ? "" : ", ";
-	
-	        items.push(GripArrayItem(Object.assign({}, this.props, {
-	          object: value,
-	          delim: delim,
-	          // Do not propagate title to array items reps
-	          title: undefined
-	        })));
-	      } catch (exc) {
-	        items.push(GripArrayItem(Object.assign({}, this.props, {
-	          object: exc,
-	          delim: delim,
-	          // Do not propagate title to array items reps
-	          title: undefined
-	        })));
-	      }
-	    }
-	    if (previewItems.length > max || gripLength > previewItems.length) {
-	      let leftItemNum = gripLength - max > 0 ? gripLength - max : gripLength - previewItems.length;
-	      items.push(Caption({
-	        object: this.safeObjectLink({}, leftItemNum + " more…")
-	      }));
-	    }
-	
-	    return items;
-	  },
-	
-	  safeObjectLink: function (config, ...children) {
-	    if (this.props.objectLink) {
-	      return this.props.objectLink(Object.assign({
-	        object: this.props.object
-	      }, config), ...children);
-	    }
-	
-	    if (Object.keys(config).length === 0 && children.length === 1) {
-	      return children[0];
-	    }
-	
-	    return span(config, ...children);
-	  },
-	
-	  render: wrapRender(function () {
-	    let {
-	      object,
-	      mode = MODE.SHORT
-	    } = this.props;
-	
-	    let items;
-	    let brackets;
-	    let needSpace = function (space) {
-	      return space ? { left: "[ ", right: " ]" } : { left: "[", right: "]" };
-	    };
-	
-	    if (mode === MODE.TINY) {
-	      let objectLength = this.getLength(object);
-	      let isEmpty = objectLength === 0;
-	      items = [span({ className: "length" }, isEmpty ? "" : objectLength)];
-	      brackets = needSpace(false);
-	    } else {
-	      let max = mode === MODE.SHORT ? 3 : 10;
-	      items = this.arrayIterator(object, max);
-	      brackets = needSpace(items.length > 0);
-	    }
-	
-	    let title = this.getTitle(object);
-	
-	    return span({
-	      className: "objectBox objectBox-array" }, title, this.safeObjectLink({
-	      className: "arrayLeftBracket"
-	    }, brackets.left), ...items, this.safeObjectLink({
-	      className: "arrayRightBracket"
-	    }, brackets.right), span({
-	      className: "arrayProperties",
-	      role: "group" }));
-	  })
-	});
+	GripArray.propTypes = {
+	  object: React.PropTypes.object.isRequired,
+	  // @TODO Change this to Object.values once it's supported in Node's version of V8
+	  mode: React.PropTypes.oneOf(Object.keys(MODE).map(key => MODE[key])),
+	  provider: React.PropTypes.object,
+	  objectLink: React.PropTypes.func,
+	  onDOMNodeMouseOver: React.PropTypes.func,
+	  onDOMNodeMouseOut: React.PropTypes.func,
+	  onInspectIconClick: React.PropTypes.func
+	};
+	
+	function GripArray(props) {
+	  let {
+	    object,
+	    mode = MODE.SHORT
+	  } = props;
+	
+	  let items;
+	  let brackets;
+	  let needSpace = function (space) {
+	    return space ? { left: "[ ", right: " ]" } : { left: "[", right: "]" };
+	  };
+	
+	  if (mode === MODE.TINY) {
+	    let objectLength = getLength(object);
+	    let isEmpty = objectLength === 0;
+	    items = [span({ className: "length" }, isEmpty ? "" : objectLength)];
+	    brackets = needSpace(false);
+	  } else {
+	    let max = mode === MODE.SHORT ? 3 : 10;
+	    items = arrayIterator(props, object, max);
+	    brackets = needSpace(items.length > 0);
+	  }
+	
+	  let title = getTitle(props, object);
+	
+	  return span({
+	    className: "objectBox objectBox-array" }, title, safeObjectLink(props, {
+	    className: "arrayLeftBracket"
+	  }, brackets.left), ...items, safeObjectLink(props, {
+	    className: "arrayRightBracket"
+	  }, brackets.right), span({
+	    className: "arrayProperties",
+	    role: "group" }));
+	}
 	
 	/**
 	 * Renders array item. Individual values are separated by
 	 * a delimiter (a comma by default).
 	 */
-	let GripArrayItem = React.createFactory(React.createClass({
-	  displayName: "GripArrayItem",
-	
-	  propTypes: {
-	    delim: React.PropTypes.string,
-	    object: React.PropTypes.oneOfType([React.PropTypes.object, React.PropTypes.number, React.PropTypes.string]).isRequired,
-	    objectLink: React.PropTypes.func,
-	    // @TODO Change this to Object.values once it's supported in Node's version of V8
-	    mode: React.PropTypes.oneOf(Object.keys(MODE).map(key => MODE[key])),
-	    provider: React.PropTypes.object,
-	    attachedActorIds: React.PropTypes.array,
-	    onDOMNodeMouseOver: React.PropTypes.func,
-	    onDOMNodeMouseOut: React.PropTypes.func,
-	    onInspectIconClick: React.PropTypes.func
-	  },
-	
-	  render: function () {
-	    let { Rep } = createFactories(__webpack_require__(2));
-	
-	    return span({}, Rep(Object.assign({}, this.props, {
-	      mode: MODE.TINY
-	    })), this.props.delim);
+	GripArrayItem.propTypes = {
+	  delim: React.PropTypes.string,
+	  object: React.PropTypes.oneOfType([React.PropTypes.object, React.PropTypes.number, React.PropTypes.string]).isRequired,
+	  objectLink: React.PropTypes.func,
+	  // @TODO Change this to Object.values once it's supported in Node's version of V8
+	  mode: React.PropTypes.oneOf(Object.keys(MODE).map(key => MODE[key])),
+	  provider: React.PropTypes.object,
+	  onDOMNodeMouseOver: React.PropTypes.func,
+	  onDOMNodeMouseOut: React.PropTypes.func,
+	  onInspectIconClick: React.PropTypes.func
+	};
+	
+	function GripArrayItem(props) {
+	  let { Rep } = __webpack_require__(2);
+	  let {
+	    delim
+	  } = props;
+	
+	  return span({}, Rep(Object.assign({}, props, {
+	    mode: MODE.TINY
+	  })), delim);
+	}
+	
+	function getLength(grip) {
+	  if (!grip.preview) {
+	    return 0;
+	  }
+	
+	  return grip.preview.length || grip.preview.childNodesLength || 0;
+	}
+	
+	function getTitle(props, object, context) {
+	  if (props.mode === MODE.TINY) {
+	    return "";
+	  }
+	
+	  let title = props.title || object.class || "Array";
+	  return safeObjectLink(props, {}, title + " ");
+	}
+	
+	function getPreviewItems(grip) {
+	  if (!grip.preview) {
+	    return null;
 	  }
-	}));
+	
+	  return grip.preview.items || grip.preview.childNodes || null;
+	}
+	
+	function arrayIterator(props, grip, max) {
+	  let items = [];
+	  const gripLength = getLength(grip);
+	
+	  if (!gripLength) {
+	    return items;
+	  }
+	
+	  const previewItems = getPreviewItems(grip);
+	  if (!previewItems) {
+	    return items;
+	  }
+	
+	  let delim;
+	  // number of grip preview items is limited to 10, but we may have more
+	  // items in grip-array.
+	  let delimMax = gripLength > previewItems.length ? previewItems.length : previewItems.length - 1;
+	  let provider = props.provider;
+	
+	  for (let i = 0; i < previewItems.length && i < max; i++) {
+	    try {
+	      let itemGrip = previewItems[i];
+	      let value = provider ? provider.getValue(itemGrip) : itemGrip;
+	
+	      delim = i == delimMax ? "" : ", ";
+	
+	      items.push(GripArrayItem(Object.assign({}, props, {
+	        object: value,
+	        delim: delim,
+	        // Do not propagate title to array items reps
+	        title: undefined
+	      })));
+	    } catch (exc) {
+	      items.push(GripArrayItem(Object.assign({}, props, {
+	        object: exc,
+	        delim: delim,
+	        // Do not propagate title to array items reps
+	        title: undefined
+	      })));
+	    }
+	  }
+	  if (previewItems.length > max || gripLength > previewItems.length) {
+	    let leftItemNum = gripLength - max > 0 ? gripLength - max : gripLength - previewItems.length;
+	    items.push(Caption({
+	      object: safeObjectLink(props, {}, leftItemNum + " more…")
+	    }));
+	  }
+	
+	  return items;
+	}
 	
 	function supportsObject(grip, type) {
 	  if (!isGrip(grip)) {
 	    return false;
 	  }
 	
 	  return grip.preview && (grip.preview.kind == "ArrayLike" || type === "DocumentFragment");
 	}
 	
 	// Exports from this module
 	module.exports = {
-	  rep: GripArray,
-	  supportsObject: supportsObject
+	  rep: wrapRender(GripArray),
+	  supportsObject
 	};
 
 /***/ },
 /* 38 */
 /***/ function(module, exports, __webpack_require__) {
 
 	// Dependencies
-	const React = __webpack_require__(3);
+	const React = __webpack_require__(4);
 	const {
 	  isGrip,
+	  safeObjectLink,
 	  wrapRender
-	} = __webpack_require__(4);
-	const Caption = React.createFactory(__webpack_require__(12));
-	const PropRep = React.createFactory(__webpack_require__(14));
+	} = __webpack_require__(3);
+	const Caption = __webpack_require__(12);
+	const PropRep = __webpack_require__(14);
 	const { MODE } = __webpack_require__(1);
 	// Shortcuts
 	const { span } = React.DOM;
+	
 	/**
 	 * Renders an map. A map is represented by a list of its
 	 * entries enclosed in curly brackets.
 	 */
-	const GripMap = React.createClass({
-	  displayName: "GripMap",
-	
-	  propTypes: {
-	    object: React.PropTypes.object,
-	    // @TODO Change this to Object.values once it's supported in Node's version of V8
-	    mode: React.PropTypes.oneOf(Object.keys(MODE).map(key => MODE[key])),
-	    objectLink: React.PropTypes.func,
-	    isInterestingEntry: React.PropTypes.func,
-	    attachedActorIds: React.PropTypes.array,
-	    onDOMNodeMouseOver: React.PropTypes.func,
-	    onDOMNodeMouseOut: React.PropTypes.func,
-	    onInspectIconClick: React.PropTypes.func,
-	    title: React.PropTypes.string
-	  },
-	
-	  getTitle: function (object) {
-	    let title = this.props.title || (object && object.class ? object.class : "Map");
-	    return this.safeObjectLink({}, title);
-	  },
-	
-	  safeEntriesIterator: function (object, max) {
-	    max = typeof max === "undefined" ? 3 : max;
-	    try {
-	      return this.entriesIterator(object, max);
-	    } catch (err) {
-	      console.error(err);
-	    }
-	    return [];
-	  },
-	
-	  entriesIterator: function (object, max) {
-	    // Entry filter. Show only interesting entries to the user.
-	    let isInterestingEntry = this.props.isInterestingEntry || ((type, value) => {
-	      return type == "boolean" || type == "number" || type == "string" && value.length != 0;
-	    });
-	
-	    let mapEntries = object.preview && object.preview.entries ? object.preview.entries : [];
-	
-	    let indexes = this.getEntriesIndexes(mapEntries, max, isInterestingEntry);
-	    if (indexes.length < max && indexes.length < mapEntries.length) {
-	      // There are not enough entries yet, so we add uninteresting entries.
-	      indexes = indexes.concat(this.getEntriesIndexes(mapEntries, max - indexes.length, (t, value, name) => {
-	        return !isInterestingEntry(t, value, name);
-	      }));
-	    }
-	
-	    let entries = this.getEntries(mapEntries, indexes);
-	    if (entries.length < mapEntries.length) {
-	      // There are some undisplayed entries. Then display "more…".
-	      entries.push(Caption({
-	        key: "more",
-	        object: this.safeObjectLink({}, `${mapEntries.length - max} more…`)
-	      }));
-	    }
-	
-	    return entries;
-	  },
-	
-	  /**
-	   * Get entries ordered by index.
-	   *
-	   * @param {Array} entries Entries array.
-	   * @param {Array} indexes Indexes of entries.
-	   * @return {Array} Array of PropRep.
-	   */
-	  getEntries: function (entries, indexes) {
-	    let {
+	GripMap.propTypes = {
+	  object: React.PropTypes.object,
+	  // @TODO Change this to Object.values once it's supported in Node's version of V8
+	  mode: React.PropTypes.oneOf(Object.keys(MODE).map(key => MODE[key])),
+	  objectLink: React.PropTypes.func,
+	  isInterestingEntry: React.PropTypes.func,
+	  onDOMNodeMouseOver: React.PropTypes.func,
+	  onDOMNodeMouseOut: React.PropTypes.func,
+	  onInspectIconClick: React.PropTypes.func,
+	  title: React.PropTypes.string
+	};
+	
+	function GripMap(props) {
+	  let object = props.object;
+	  let propsArray = safeEntriesIterator(props, object, props.mode === MODE.LONG ? 10 : 3);
+	
+	  if (props.mode === MODE.TINY) {
+	    return span({ className: "objectBox objectBox-object" }, getTitle(props, object));
+	  }
+	
+	  return span({ className: "objectBox objectBox-object" }, getTitle(props, object), safeObjectLink(props, {
+	    className: "objectLeftBrace"
+	  }, " { "), propsArray, safeObjectLink(props, {
+	    className: "objectRightBrace"
+	  }, " }"));
+	}
+	
+	function getTitle(props, object) {
+	  let title = props.title || (object && object.class ? object.class : "Map");
+	  return safeObjectLink(props, {}, title);
+	}
+	
+	function safeEntriesIterator(props, object, max) {
+	  max = typeof max === "undefined" ? 3 : max;
+	  try {
+	    return entriesIterator(props, object, max);
+	  } catch (err) {
+	    console.error(err);
+	  }
+	  return [];
+	}
+	
+	function entriesIterator(props, object, max) {
+	  // Entry filter. Show only interesting entries to the user.
+	  let isInterestingEntry = props.isInterestingEntry || ((type, value) => {
+	    return type == "boolean" || type == "number" || type == "string" && value.length != 0;
+	  });
+	
+	  let mapEntries = object.preview && object.preview.entries ? object.preview.entries : [];
+	
+	  let indexes = getEntriesIndexes(mapEntries, max, isInterestingEntry);
+	  if (indexes.length < max && indexes.length < mapEntries.length) {
+	    // There are not enough entries yet, so we add uninteresting entries.
+	    indexes = indexes.concat(getEntriesIndexes(mapEntries, max - indexes.length, (t, value, name) => {
+	      return !isInterestingEntry(t, value, name);
+	    }));
+	  }
+	
+	  let entries = getEntries(props, mapEntries, indexes);
+	  if (entries.length < mapEntries.length) {
+	    // There are some undisplayed entries. Then display "more…".
+	    entries.push(Caption({
+	      key: "more",
+	      object: safeObjectLink(props, {}, `${mapEntries.length - max} more…`)
+	    }));
+	  }
+	
+	  return entries;
+	}
+	
+	/**
+	 * Get entries ordered by index.
+	 *
+	 * @param {Object} props Component props.
+	 * @param {Array} entries Entries array.
+	 * @param {Array} indexes Indexes of entries.
+	 * @return {Array} Array of PropRep.
+	 */
+	function getEntries(props, entries, indexes) {
+	  let {
+	    objectLink,
+	    onDOMNodeMouseOver,
+	    onDOMNodeMouseOut,
+	    onInspectIconClick
+	  } = props;
+	
+	  // Make indexes ordered by ascending.
+	  indexes.sort(function (a, b) {
+	    return a - b;
+	  });
+	
+	  return indexes.map((index, i) => {
+	    let [key, entryValue] = entries[index];
+	    let value = entryValue.value !== undefined ? entryValue.value : entryValue;
+	
+	    return PropRep({
+	      // key,
+	      name: key,
+	      equal: ": ",
+	      object: value,
+	      // Do not add a trailing comma on the last entry
+	      // if there won't be a "more..." item.
+	      delim: i < indexes.length - 1 || indexes.length < entries.length ? ", " : null,
+	      mode: MODE.TINY,
 	      objectLink,
-	      attachedActorIds,
 	      onDOMNodeMouseOver,
 	      onDOMNodeMouseOut,
 	      onInspectIconClick
-	    } = this.props;
-	
-	    // Make indexes ordered by ascending.
-	    indexes.sort(function (a, b) {
-	      return a - b;
 	    });
-	
-	    return indexes.map((index, i) => {
-	      let [key, entryValue] = entries[index];
-	      let value = entryValue.value !== undefined ? entryValue.value : entryValue;
-	
-	      return PropRep({
-	        // key,
-	        name: key,
-	        equal: ": ",
-	        object: value,
-	        // Do not add a trailing comma on the last entry
-	        // if there won't be a "more..." item.
-	        delim: i < indexes.length - 1 || indexes.length < entries.length ? ", " : "",
-	        mode: MODE.TINY,
-	        objectLink,
-	        attachedActorIds,
-	        onDOMNodeMouseOver,
-	        onDOMNodeMouseOut,
-	        onInspectIconClick
-	      });
-	    });
-	  },
-	
-	  /**
-	   * Get the indexes of entries in the map.
-	   *
-	   * @param {Array} entries Entries array.
-	   * @param {Number} max The maximum length of indexes array.
-	   * @param {Function} filter Filter the entry you want.
-	   * @return {Array} Indexes of filtered entries in the map.
-	   */
-	  getEntriesIndexes: function (entries, max, filter) {
-	    return entries.reduce((indexes, [key, entry], i) => {
-	      if (indexes.length < max) {
-	        let value = entry && entry.value !== undefined ? entry.value : entry;
-	        // Type is specified in grip's "class" field and for primitive
-	        // values use typeof.
-	        let type = (value && value.class ? value.class : typeof value).toLowerCase();
-	
-	        if (filter(type, value, key)) {
-	          indexes.push(i);
-	        }
+	  });
+	}
+	
+	/**
+	 * Get the indexes of entries in the map.
+	 *
+	 * @param {Array} entries Entries array.
+	 * @param {Number} max The maximum length of indexes array.
+	 * @param {Function} filter Filter the entry you want.
+	 * @return {Array} Indexes of filtered entries in the map.
+	 */
+	function getEntriesIndexes(entries, max, filter) {
+	  return entries.reduce((indexes, [key, entry], i) => {
+	    if (indexes.length < max) {
+	      let value = entry && entry.value !== undefined ? entry.value : entry;
+	      // Type is specified in grip's "class" field and for primitive
+	      // values use typeof.
+	      let type = (value && value.class ? value.class : typeof value).toLowerCase();
+	
+	      if (filter(type, value, key)) {
+	        indexes.push(i);
 	      }
-	
-	      return indexes;
-	    }, []);
-	  },
-	
-	  safeObjectLink: function (config, ...children) {
-	    if (this.props.objectLink) {
-	      return this.props.objectLink(Object.assign({
-	        object: this.props.object
-	      }, config), ...children);
 	    }
 	
-	    if (Object.keys(config).length === 0 && children.length === 1) {
-	      return children[0];
-	    }
-	
-	    return span(config, ...children);
-	  },
-	
-	  render: wrapRender(function () {
-	    let object = this.props.object;
-	    let propsArray = this.safeEntriesIterator(object, this.props.mode === MODE.LONG ? 10 : 3);
-	
-	    if (this.props.mode === MODE.TINY) {
-	      return span({ className: "objectBox objectBox-object" }, this.getTitle(object));
-	    }
-	
-	    return span({ className: "objectBox objectBox-object" }, this.getTitle(object), this.safeObjectLink({
-	      className: "objectLeftBrace"
-	    }, " { "), propsArray, this.safeObjectLink({
-	      className: "objectRightBrace"
-	    }, " }"));
-	  })
-	});
+	    return indexes;
+	  }, []);
+	}
 	
 	function supportsObject(grip, type) {
 	  if (!isGrip(grip)) {
 	    return false;
 	  }
 	  return grip.preview && grip.preview.kind == "MapLike";
 	}
 	
 	// Exports from this module
 	module.exports = {
-	  rep: GripMap,
-	  supportsObject: supportsObject
+	  rep: wrapRender(GripMap),
+	  supportsObject
 	};
 
 /***/ }
 /******/ ])
 });
 ;
 //# sourceMappingURL=reps.js.map
\ No newline at end of file
--- a/devtools/client/shared/components/reps/test/mochitest/head.js
+++ b/devtools/client/shared/components/reps/test/mochitest/head.js
@@ -64,12 +64,8 @@ function testRepRenderModes(modeTests, t
 
     const rendered = renderComponent(
       componentUnderTest.rep,
       Object.assign({}, { object: gripStub, mode, title }, props)
     );
     is(rendered.textContent, expectedOutput, message);
   });
 }
-
-function getStubAttachedActorIds(gripStubs) {
-  return gripStubs.map(gripStub => gripStub.actor);
-}
--- a/devtools/client/shared/components/reps/test/mochitest/test_reps_array.html
+++ b/devtools/client/shared/components/reps/test/mochitest/test_reps_array.html
@@ -15,18 +15,22 @@ Test ArrayRep rep
 <body>
 <pre id="test">
 <script src="head.js" type="application/javascript"></script>
 <script type="application/javascript">
 "use strict";
 /* import-globals-from head.js */
 
 window.onload = Task.async(function* () {
-  const { REPS, MODE } = browserRequire("devtools/client/shared/components/reps/reps");
-  let { Rep, ArrayRep } = REPS;
+  const {
+    REPS,
+    MODE,
+    getRep,
+  } = browserRequire("devtools/client/shared/components/reps/reps");
+  let { ArrayRep } = REPS;
 
   let componentUnderTest = ArrayRep;
   const maxLength = {
     short: 3,
     long: 10
   };
 
   try {
@@ -47,19 +51,17 @@ window.onload = Task.async(function* () 
     ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
   } finally {
     SimpleTest.finish();
   }
 
   function testBasic() {
     // Test that correct rep is chosen
     const stub = [];
-    const renderedRep = shallowRenderComponent(Rep, { object: stub });
-    is(renderedRep.type, ArrayRep.rep,
-       `Rep correctly selects ${ArrayRep.rep.displayName}`);
+    is(getRep(stub), ArrayRep.rep, "Rep correctly selects Array Rep");
 
 
     // Test rendering
     const defaultOutput = `[]`;
 
     const modeTests = [
       {
         mode: undefined,
--- a/devtools/client/shared/components/reps/test/mochitest/test_reps_attribute.html
+++ b/devtools/client/shared/components/reps/test/mochitest/test_reps_attribute.html
@@ -12,32 +12,34 @@ Test Attribute rep
   <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
 </head>
 <body>
 <pre id="test">
 <script src="head.js" type="application/javascript"></script>
 <script type="application/javascript">
 window.onload = Task.async(function* () {
-  const { REPS } = browserRequire("devtools/client/shared/components/reps/reps");
-  let { Rep, Attribute } = REPS;
+  const {
+    REPS,
+    getRep,
+  } = browserRequire("devtools/client/shared/components/reps/reps");
+  let { Attribute } = REPS;
 
   try {
     testBasic();
     testObjectLink();
   } catch (e) {
     ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
   } finally {
     SimpleTest.finish();
   }
 
   function testBasic() {
     // Test that correct rep is chosen
-    const renderedRep = shallowRenderComponent(Rep, { object: getStub() });
-    is(renderedRep.type, Attribute.rep, `Rep correctly selects ${Attribute.rep.displayName}`);
+    is(getRep(getStub()), Attribute.rep, "Rep correctly selects Attribute Rep");
 
     // Test rendering
     const renderedComponent = renderComponent(Attribute.rep, { object: getStub() });
     is(renderedComponent.textContent, "class=\"autocomplete-suggestions\"", "Attribute rep has expected text content");
   }
 
   function testObjectLink() {
     const renderedComponent = renderComponent(Attribute.rep, {
--- a/devtools/client/shared/components/reps/test/mochitest/test_reps_comment-node.html
+++ b/devtools/client/shared/components/reps/test/mochitest/test_reps_comment-node.html
@@ -15,18 +15,22 @@ Test comment-node rep
 <body>
 <pre id="test">
 <script src="head.js" type="application/javascript"></script>
 <script type="application/javascript">
 "use strict";
 
 window.onload = Task.async(function* () {
   try {
-    const { REPS, MODE } = browserRequire("devtools/client/shared/components/reps/reps");
-    let { Rep, CommentNode } = REPS;
+    const {
+      REPS,
+      MODE,
+      getRep,
+    } = browserRequire("devtools/client/shared/components/reps/reps");
+    let { CommentNode } = REPS;
 
     let gripStub = {
       "type": "object",
       "actor": "server1.conn1.child1/obj47",
       "class": "Comment",
       "extensible": true,
       "frozen": false,
       "sealed": false,
@@ -35,19 +39,17 @@ window.onload = Task.async(function* () 
         "kind": "DOMNode",
         "nodeType": 8,
         "nodeName": "#comment",
         "textContent": "test\nand test\nand test\nand test\nand test\nand test\nand test"
       }
     };
 
     // Test that correct rep is chosen.
-    const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
-    is(renderedRep.type, CommentNode.rep,
-      `Rep correctly selects ${CommentNode.rep.displayName}`);
+    is(getRep(gripStub), CommentNode.rep, "Rep correctly selects CommentNode Rep");
 
     // Test rendering.
     const renderedComponent = renderComponent(CommentNode.rep, { object: gripStub });
     is(renderedComponent.className, "objectBox theme-comment",
       "CommentNode rep has expected class names");
     is(renderedComponent.textContent,
       `<!-- test\nand test\nand test\nan…d test\nand test\nand test -->`,
       "CommentNode rep has expected text content");
--- a/devtools/client/shared/components/reps/test/mochitest/test_reps_date-time.html
+++ b/devtools/client/shared/components/reps/test/mochitest/test_reps_date-time.html
@@ -12,18 +12,21 @@ Test DateTime rep
   <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
 </head>
 <body>
 <pre id="test">
 <script src="head.js" type="application/javascript"></script>
 <script type="application/javascript">
 window.onload = Task.async(function* () {
-  const { REPS } = browserRequire("devtools/client/shared/components/reps/reps");
-  let { Rep, DateTime } = REPS;
+  const {
+    REPS,
+    getRep,
+  } = browserRequire("devtools/client/shared/components/reps/reps");
+  let { DateTime } = REPS;
 
   try {
     testValid();
     testInvalid();
     testObjectLink();
   } catch(e) {
     ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
   } finally {
@@ -40,22 +43,21 @@ window.onload = Task.async(function* () 
       "sealed": false,
       "ownPropertyLength": 0,
       "preview": {
         "timestamp": 1459372644859
       }
     };
 
     // Test that correct rep is chosen
-    const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
-    is(renderedRep.type, DateTime.rep, `Rep correctly selects ${DateTime.rep.displayName}`);
+    is(getRep(gripStub), DateTime.rep, "Rep correctly selects DateTime Rep");
 
     // Test rendering
     const renderedComponent = renderComponent(DateTime.rep, { object: gripStub });
-    is(renderedComponent.textContent, "2016-03-30T21:17:24.859Z", "DateTime rep has expected text content for valid date");
+    is(renderedComponent.textContent, "Date 2016-03-30T21:17:24.859Z", "DateTime rep has expected text content for valid date");
   }
 
   function testInvalid() {
     let gripStub = {
       "type": "object",
       "actor": "server1.conn0.child1/obj32",
       "class": "Date",
       "extensible": true,
--- a/devtools/client/shared/components/reps/test/mochitest/test_reps_document.html
+++ b/devtools/client/shared/components/reps/test/mochitest/test_reps_document.html
@@ -12,36 +12,38 @@ Test Document rep
   <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
 </head>
 <body>
 <pre id="test">
 <script src="head.js" type="application/javascript"></script>
 <script type="application/javascript">
 window.onload = Task.async(function* () {
-  const { REPS } = browserRequire("devtools/client/shared/components/reps/reps");
-  let { Rep, Document } = REPS;
+  const {
+    REPS,
+    getRep,
+  } = browserRequire("devtools/client/shared/components/reps/reps");
+  let { Document } = REPS;
 
   try {
     testBasic();
     testObjectLink();
   } catch (e) {
     ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
   } finally {
     SimpleTest.finish();
   }
 
   function testBasic() {
     // Test that correct rep is chosen
-    const renderedRep = shallowRenderComponent(Rep, { object: getStub() });
-    is(renderedRep.type, Document.rep, `Rep correctly selects ${Document.rep.displayName}`);
+    is(getRep(getStub()), Document.rep, "Rep correctly selects Document Rep");
 
     // Test rendering
     const renderedComponent = renderComponent(Document.rep, { object: getStub() });
-    is(renderedComponent.textContent, "https://www.mozilla.org/en-US/firefox/new/",
+    is(renderedComponent.textContent, "HTMLDocument https://www.mozilla.org/en-US/firefox/new/",
       "Document rep has expected text content");
   }
 
   function testObjectLink() {
     // Test rendering
     const renderedComponent = renderComponent(Document.rep, {
       object: getStub(),
       objectLink: (props, ...children) => React.DOM.span({},
--- a/devtools/client/shared/components/reps/test/mochitest/test_reps_element-node.html
+++ b/devtools/client/shared/components/reps/test/mochitest/test_reps_element-node.html
@@ -17,19 +17,20 @@ Test Element node rep
 <script src="head.js" type="application/javascript"></script>
 <script type="application/javascript">
 "use strict";
 
 window.onload = Task.async(function* () {
   const {
     REPS,
     MODE,
+    getRep,
     getSelectableInInspectorGrips,
   } = browserRequire("devtools/client/shared/components/reps/reps");
-  let { Rep, ElementNode } = REPS;
+  let { ElementNode } = REPS;
 
   try {
     yield testBodyNode();
     yield testDocumentElement();
     yield testNode();
     yield testNodeWithLeadingAndTrailingSpacesClassName();
     yield testNodeWithoutAttributes();
     yield testLotsOfAttributes();
@@ -43,70 +44,66 @@ window.onload = Task.async(function* () 
   } catch (e) {
     ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
   } finally {
     SimpleTest.finish();
   }
 
   function testBodyNode() {
     const stub = getGripStub("testBodyNode");
-    const renderedRep = shallowRenderComponent(Rep, { object: stub });
-    is(renderedRep.type, ElementNode.rep,
-      `Rep correctly selects ${ElementNode.rep.displayName} for body node`);
+    is(getRep(stub), ElementNode.rep,
+      "Rep correctly selects ElementNode Rep for body node");
 
     const renderedComponent = renderComponent(ElementNode.rep, { object: stub });
     is(renderedComponent.textContent, `<body id="body-id" class="body-class">`,
       "Element node rep has expected text content for body node");
 
     const tinyRenderedComponent = renderComponent(
       ElementNode.rep, { object: stub, mode: MODE.TINY });
     is(tinyRenderedComponent.textContent, `body#body-id.body-class`,
       "Element node rep has expected text content for body node in tiny mode");
   }
 
   function testDocumentElement() {
     const stub = getGripStub("testDocumentElement");
-    const renderedRep = shallowRenderComponent(Rep, { object: stub });
-    is(renderedRep.type, ElementNode.rep,
-      `Rep correctly selects ${ElementNode.rep.displayName} for document element node`);
+    is(getRep(stub), ElementNode.rep,
+      "Rep correctly selects ElementNode Rep for document element node");
 
     const renderedComponent = renderComponent(ElementNode.rep, { object: stub });
     is(renderedComponent.textContent, `<html dir="ltr" lang="en-US">`,
       "Element node rep has expected text content for document element node");
 
     const tinyRenderedComponent = renderComponent(
       ElementNode.rep, { object: stub, mode: MODE.TINY });
     is(tinyRenderedComponent.textContent, `html`,
       "Element node rep has expected text content for document element in tiny mode");
   }
 
   function testNode() {
     const stub = getGripStub("testNode");
-    const renderedRep = shallowRenderComponent(Rep, { object: stub });
-    is(renderedRep.type, ElementNode.rep,
-      `Rep correctly selects ${ElementNode.rep.displayName} for element node`);
+    is(getRep(stub), ElementNode.rep,
+      "Rep correctly selects ElementNode Rep for element node");
 
     const renderedComponent = renderComponent(ElementNode.rep, { object: stub });
     is(renderedComponent.textContent,
       `<input id="newtab-customize-button" class="bar baz" dir="ltr" ` +
       `title="Customize your New Tab page" value="foo" type="button">`,
       "Element node rep has expected text content for element node");
 
     const tinyRenderedComponent = renderComponent(
       ElementNode.rep, { object: stub, mode: MODE.TINY });
     is(tinyRenderedComponent.textContent,
       `input#newtab-customize-button.bar.baz`,
       "Element node rep has expected text content for element node in tiny mode");
   }
 
   function testNodeWithLeadingAndTrailingSpacesClassName() {
     const stub = getGripStub("testNodeWithLeadingAndTrailingSpacesClassName");
-    const renderedRep = shallowRenderComponent(Rep, { object: stub });
-    is(renderedRep.type, ElementNode.rep,
-      `Rep correctly selects ${ElementNode.rep.displayName} for element node`);
+    is(getRep(stub), ElementNode.rep,
+      "Rep correctly selects ElementNode Rep for element node");
 
     const renderedComponent = renderComponent(ElementNode.rep, { object: stub });
     is(renderedComponent.textContent,
       `<body id="nightly-whatsnew" class="  html-ltr    ">`,
       "Element node rep output element node with the class trailing spaces");
 
     const tinyRenderedComponent = renderComponent(
       ElementNode.rep, { object: stub, mode: MODE.TINY });
@@ -114,18 +111,17 @@ window.onload = Task.async(function* () 
       `body#nightly-whatsnew.html-ltr`,
       "Element node rep does not show leading nor trailing spaces " +
       "on class attribute in tiny mode");
   }
 
   function testNodeWithoutAttributes() {
     const stub = getGripStub("testNodeWithoutAttributes");
 
-    const renderedComponent = renderComponent(ElementNode.rep, { object: stub });
-    is(renderedComponent.textContent, "<p>",
+    is(getRep(stub), ElementNode.rep,
       "Element node rep has expected text content for element node without attributes");
 
     const tinyRenderedComponent = renderComponent(
       ElementNode.rep, { object: stub, mode: MODE.TINY });
     is(tinyRenderedComponent.textContent, `p`,
       "Element node rep has expected text content for element node without attributes");
   }
 
@@ -142,133 +138,112 @@ window.onload = Task.async(function* () 
       ElementNode.rep, { object: stub, mode: MODE.TINY });
     is(tinyRenderedComponent.textContent, `p#lots-of-attributes`,
       "Element node rep has expected text content for node in tiny mode");
   }
 
   function testSvgNode() {
     const stub = getGripStub("testSvgNode");
 
-    const renderedRep = shallowRenderComponent(Rep, { object: stub });
-    is(renderedRep.type, ElementNode.rep,
+    is(getRep(stub), ElementNode.rep,
       `Rep correctly selects ${ElementNode.rep.displayName} for SVG element node`);
 
     const renderedComponent = renderComponent(ElementNode.rep, { object: stub });
     is(renderedComponent.textContent,
       '<clipPath id="clip" class="svg-element">',
       "Element node rep has expected text content for SVG element node");
 
     const tinyRenderedComponent = renderComponent(
       ElementNode.rep, { object: stub, mode: MODE.TINY });
     is(tinyRenderedComponent.textContent, `clipPath#clip.svg-element`,
       "Element node rep has expected text content for SVG element node in tiny mode");
   }
 
   function testSvgNodeInXHTML() {
     const stub = getGripStub("testSvgNodeInXHTML");
 
-    const renderedRep = shallowRenderComponent(Rep, { object: stub });
-    is(renderedRep.type, ElementNode.rep,
+    is(getRep(stub), ElementNode.rep,
       `Rep correctly selects ${ElementNode.rep.displayName} for XHTML SVG element node`);
 
     const renderedComponent = renderComponent(ElementNode.rep, { object: stub });
     is(renderedComponent.textContent,
       '<svg:circle class="svg-element" cx="0" cy="0" r="5">',
       "Element node rep has expected text content for XHTML SVG element node");
 
     const tinyRenderedComponent = renderComponent(
       ElementNode.rep, { object: stub, mode: MODE.TINY });
     is(tinyRenderedComponent.textContent, `svg:circle.svg-element`,
       "Element node rep has expected text content for XHTML SVG element in tiny mode");
   }
 
   function testOnMouseOver() {
     const stub = getGripStub("testNode");
-  debugger;
     const grips = getSelectableInInspectorGrips(stub);
     is(grips.length, 1, "the stub has one node grip");
 
-    const attachedActorIds = getStubAttachedActorIds(grips);
-
     let mouseOverValue;
     let onDOMNodeMouseOver = (object) => {
       mouseOverValue = object;
     };
     const renderedComponent = renderComponent(ElementNode.rep, {
       object: stub,
       onDOMNodeMouseOver,
-      attachedActorIds,
     });
 
     TestUtils.Simulate.mouseOver(renderedComponent);
 
     is(mouseOverValue, grips[0], "onDOMNodeMouseOver is called " +
       "with the expected argument when mouseover is fired on the Rep");
   }
 
   function testOnMouseOut() {
     const stub = getGripStub("testNode");
     const grips = getSelectableInInspectorGrips(stub);
     is(grips.length, 1, "the stub has one node grip");
 
-    const attachedActorIds = getStubAttachedActorIds(grips);
-
     let called = false;
     let onDOMNodeMouseOut = (object) => {
       called = true;
     };
     const renderedComponent = renderComponent(ElementNode.rep, {
       object: stub,
       onDOMNodeMouseOut,
-      attachedActorIds,
     });
 
     TestUtils.Simulate.mouseOut(renderedComponent);
 
     is(called, true, "onDOMNodeMouseOut is called when mouseout is fired on the Rep");
   }
 
   function testOnInspectIconClick() {
     const stub = getGripStub("testNode");
     const grips = getSelectableInInspectorGrips(stub);
     is(grips.length, 1, "the stub has one node grip");
 
-    const attachedActorIds = getStubAttachedActorIds(grips);
-
     let inspectIconClickedValue = null;
     let inspectIconClickedEvent = null;
 
     let onInspectIconClick = (object, event) => {
       inspectIconClickedValue = object;
       inspectIconClickedEvent = event;
     };
 
     const renderedComponentWithoutInspectIcon = renderComponent(ElementNode.rep, {
-      object: stub,
+      object: getGripStub("testDisconnectedNode"),
       onInspectIconClick,
-      attachedActorIds: ["someOtherId"]
     });
     is(renderedComponentWithoutInspectIcon.querySelector(".open-inspector"), null,
-      "There isn't an inspect icon when actor is not in attachedActorIds");
+      "There isn't an inspect icon when the node is not in the DOM tree");
 
-    let renderedComponent = renderComponent(ElementNode.rep, {
+    const renderedComponent = renderComponent(ElementNode.rep, {
       object: stub,
       onInspectIconClick,
     });
-    let inspectIconNode = renderedComponent.querySelector(".open-inspector");
-    ok(inspectIconNode !== null,
-      "There is an inspect icon when attachedActorIds is not specified");
 
-    renderedComponent = renderComponent(ElementNode.rep, {
-      object: stub,
-      onInspectIconClick,
-      attachedActorIds,
-    });
-
-    inspectIconNode = renderedComponent.querySelector(".open-inspector");
+    const inspectIconNode = renderedComponent.querySelector(".open-inspector");
     ok(inspectIconNode !== null, "There is an inspect icon as expected");
     TestUtils.Simulate.click(inspectIconNode);
 
     is(inspectIconClickedValue, grips[0],
       "onInspectIconClick is called with expected value when inspect icon is clicked");
     ok(inspectIconClickedEvent !== null && inspectIconClickedEvent.type === "click",
       "onInspectIconClick forwarded the original event to the callback");
   }
@@ -340,16 +315,42 @@ window.onload = Task.async(function* () 
           "extensible": true,
           "frozen": false,
           "sealed": false,
           "ownPropertyLength": 0,
           "preview": {
             "kind": "DOMNode",
             "nodeType": 1,
             "nodeName": "input",
+            "isConnected": true,
+            "attributes": {
+              "id": "newtab-customize-button",
+              "dir": "ltr",
+              "title": "Customize your New Tab page",
+              "class": "bar baz",
+              "value": "foo",
+              "type": "button"
+            },
+            "attributesLength": 6
+          }
+        };
+      case "testDisconnectedNode":
+        return {
+          "type": "object",
+          "actor": "server1.conn2.child1/obj116",
+          "class": "HTMLInputElement",
+          "extensible": true,
+          "frozen": false,
+          "sealed": false,
+          "ownPropertyLength": 0,
+          "preview": {
+            "kind": "DOMNode",
+            "nodeType": 1,
+            "nodeName": "input",
+            "isConnected": false,
             "attributes": {
               "id": "newtab-customize-button",
               "dir": "ltr",
               "title": "Customize your New Tab page",
               "class": "bar baz",
               "value": "foo",
               "type": "button"
             },
--- a/devtools/client/shared/components/reps/test/mochitest/test_reps_error.html
+++ b/devtools/client/shared/components/reps/test/mochitest/test_reps_error.html
@@ -14,18 +14,22 @@ Test Error rep
 </head>
 <body>
 <pre id="test">
 <script src="head.js" type="application/javascript"></script>
 <script type="application/javascript">
 "use strict";
 
 window.onload = Task.async(function* () {
-  const { REPS, MODE } = browserRequire("devtools/client/shared/components/reps/reps");
-  let { Rep, ErrorRep } = REPS;
+  const {
+    REPS,
+    MODE,
+    getRep,
+  } = browserRequire("devtools/client/shared/components/reps/reps");
+  let { ErrorRep } = REPS;
 
   try {
     // Test errors with different properties
     yield testSimpleError();
     yield testMultilineStackError();
     yield testErrorWithoutStacktrace();
 
     // Test different kind of errors
@@ -42,19 +46,17 @@ window.onload = Task.async(function* () 
     ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
   } finally {
     SimpleTest.finish();
   }
 
   function testSimpleError() {
     // Test object = `new Error("Error message")`
     const stub = getGripStub("testSimpleError");
-    const renderedRep = shallowRenderComponent(Rep, {object: stub});
-    is(renderedRep.type, ErrorRep.rep,
-      `Rep correctly selects ${ErrorRep.rep.displayName} for Error object`);
+    is(getRep(stub), ErrorRep.rep, "Rep correctly selects Error Rep for Error object");
 
     const renderedComponent = renderComponent(ErrorRep.rep, {object: stub});
     is(renderedComponent.textContent,
       "Error: Error message\n" +
       "Stack trace:\n" +
       "@debugger eval code:1:13\n",
       "Error Rep has expected text content for a simple error");
 
@@ -72,19 +74,18 @@ window.onload = Task.async(function* () 
      *     errorBar();
      *   }
      *   function errorBar() {
      *     console.log(new Error("bar"));
      *   }
      *   errorFoo();`
      */
     const stub = getGripStub("testMultilineStackError");
-    const renderedRep = shallowRenderComponent(Rep, {object: stub});
-    is(renderedRep.type, ErrorRep.rep,
-      `Rep correctly selects ${ErrorRep.rep.displayName} for Error object`);
+    is(getRep(stub), ErrorRep.rep,
+      `Rep correctly selects Error Rep for Error object`);
 
     const renderedComponent = renderComponent(ErrorRep.rep, {object: stub});
     is(renderedComponent.textContent,
       "Error: bar\n" +
       "Stack trace:\n" +
       "errorBar@debugger eval code:6:15\n" +
       "errorFoo@debugger eval code:3:3\n" +
       "@debugger eval code:8:1\n",
@@ -94,38 +95,36 @@ window.onload = Task.async(function* () 
       ErrorRep.rep, {object: stub, mode: MODE.TINY});
     is(tinyRenderedComponent.textContent,
       "Error",
       "Error Rep has expected text content for an error with a multiple line in tiny mode");
   }
 
   function testErrorWithoutStacktrace() {
     const stub = getGripStub("testErrorWithoutStacktrace");
-    const renderedRep = shallowRenderComponent(Rep, {object: stub});
-    is(renderedRep.type, ErrorRep.rep,
-      `Rep correctly selects ${ErrorRep.rep.displayName} for Error object`);
+    is(getRep(stub), ErrorRep.rep,
+      `Rep correctly selects Error Rep for Error object`);
 
     const renderedComponent = renderComponent(ErrorRep.rep, {object: stub});
     is(renderedComponent.textContent,
       "Error: Error message",
       "Error Rep has expected text content for an error without stacktrace");
 
     const tinyRenderedComponent = renderComponent(
       ErrorRep.rep, {object: stub, mode: MODE.TINY});
     is(tinyRenderedComponent.textContent,
       "Error",
       "Error Rep has expected text content for an error without stacktrace in tiny mode");
   }
 
   function testEvalError() {
     // Test object = `new EvalError("EvalError message")`
     const stub = getGripStub("testEvalError");
-    const renderedRep = shallowRenderComponent(Rep, {object: stub});
-    is(renderedRep.type, ErrorRep.rep,
-      `Rep correctly selects ${ErrorRep.rep.displayName} for EvalError object`);
+    is(getRep(stub), ErrorRep.rep,
+      `Rep correctly selects Error Rep for EvalError object`);
 
     const renderedComponent = renderComponent(ErrorRep.rep, {object: stub});
     is(renderedComponent.textContent,
       "EvalError: EvalError message\n" +
       "Stack trace:\n" +
       "@debugger eval code:10:13\n",
       "Error Rep has expected text content for an EvalError");
 
@@ -134,19 +133,18 @@ window.onload = Task.async(function* () 
     is(tinyRenderedComponent.textContent,
       "EvalError",
       "Error Rep has expected text content for an EvalError in tiny mode");
   }
 
   function testInternalError() {
     // Test object = `new InternalError("InternalError message")`
     const stub = getGripStub("testInternalError");
-    const renderedRep = shallowRenderComponent(Rep, {object: stub});
-    is(renderedRep.type, ErrorRep.rep,
-      `Rep correctly selects ${ErrorRep.rep.displayName} for InternalError object`);
+    is(getRep(stub), ErrorRep.rep,
+      `Rep correctly selects Error Rep for InternalError object`);
 
     const renderedComponent = renderComponent(ErrorRep.rep, {object: stub});
     is(renderedComponent.textContent,
       "InternalError: InternalError message\n" +
       "Stack trace:\n" +
       "@debugger eval code:11:13\n",
       "Error Rep has expected text content for an InternalError");
 
@@ -155,19 +153,18 @@ window.onload = Task.async(function* () 
     is(tinyRenderedComponent.textContent,
       "InternalError",
       "Error Rep has expected text content for an InternalError in tiny mode");
   }
 
   function testRangeError() {
     // Test object = `new RangeError("RangeError message")`
     const stub = getGripStub("testRangeError");
-    const renderedRep = shallowRenderComponent(Rep, {object: stub});
-    is(renderedRep.type, ErrorRep.rep,
-      `Rep correctly selects ${ErrorRep.rep.displayName} for RangeError object`);
+    is(getRep(stub), ErrorRep.rep,
+      `Rep correctly selects Error Rep for RangeError object`);
 
     const renderedComponent = renderComponent(ErrorRep.rep, {object: stub});
     is(renderedComponent.textContent,
       "RangeError: RangeError message\n" +
       "Stack trace:\n" +
       "@debugger eval code:12:13\n",
       "Error Rep has expected text content for RangeError");
 
@@ -176,19 +173,18 @@ window.onload = Task.async(function* () 
     is(tinyRenderedComponent.textContent,
       "RangeError",
       "Error Rep has expected text content for RangeError in tiny mode");
   }
 
   function testReferenceError() {
     // Test object = `new ReferenceError("ReferenceError message"`
     const stub = getGripStub("testReferenceError");
-    const renderedRep = shallowRenderComponent(Rep, {object: stub});
-    is(renderedRep.type, ErrorRep.rep,
-      `Rep correctly selects ${ErrorRep.rep.displayName} for ReferenceError object`);
+    is(getRep(stub), ErrorRep.rep,
+      `Rep correctly selects Error Rep for ReferenceError object`);
 
     const renderedComponent = renderComponent(ErrorRep.rep, {object: stub});
     is(renderedComponent.textContent,
       "ReferenceError: ReferenceError message\n" +
       "Stack trace:\n" +
       "@debugger eval code:13:13\n",
       "Error Rep has expected text content for ReferenceError");
 
@@ -197,19 +193,18 @@ window.onload = Task.async(function* () 
     is(tinyRenderedComponent.textContent,
       "ReferenceError",
       "Error Rep has expected text content for ReferenceError in tiny mode");
   }
 
   function testSyntaxError() {
     // Test object = `new SyntaxError("SyntaxError message"`
     const stub = getGripStub("testSyntaxError");
-    const renderedRep = shallowRenderComponent(Rep, {object: stub});
-    is(renderedRep.type, ErrorRep.rep,
-      `Rep correctly selects ${ErrorRep.rep.displayName} for SyntaxError object`);
+    is(getRep(stub), ErrorRep.rep,
+      `Rep correctly selects Error Rep for SyntaxError object`);
 
     const renderedComponent = renderComponent(ErrorRep.rep, {object: stub});
     is(renderedComponent.textContent,
       "SyntaxError: SyntaxError message\n" +
       "Stack trace:\n" +
       "@debugger eval code:14:13\n",
       "Error Rep has expected text content for SyntaxError");
 
@@ -218,19 +213,18 @@ window.onload = Task.async(function* () 
     is(tinyRenderedComponent.textContent,
       "SyntaxError",
       "SyntaxError Rep has expected text content for SyntaxError in tiny mode");
   }
 
   function testTypeError() {
     // Test object = `new TypeError("TypeError message"`
     const stub = getGripStub("testTypeError");
-    const renderedRep = shallowRenderComponent(Rep, {object: stub});
-    is(renderedRep.type, ErrorRep.rep,
-      `Rep correctly selects ${ErrorRep.rep.displayName} for TypeError`);
+    is(getRep(stub), ErrorRep.rep,
+      `Rep correctly selects Error Rep for TypeError`);
 
     const renderedComponent = renderComponent(ErrorRep.rep, {object: stub});
     is(renderedComponent.textContent,
       "TypeError: TypeError message\n" +
       "Stack trace:\n" +
       "@debugger eval code:15:13\n",
       "Error Rep has expected text content for TypeError");
 
@@ -239,19 +233,18 @@ window.onload = Task.async(function* () 
     is(tinyRenderedComponent.textContent,
       "TypeError",
       "Error Rep has expected text content for a TypeError in tiny mode");
   }
 
   function testURIError() {
     // Test object = `new URIError("URIError message")`
     const stub = getGripStub("testURIError");
-    const renderedRep = shallowRenderComponent(Rep, {object: stub});
-    is(renderedRep.type, ErrorRep.rep,
-      `Rep correctly selects ${ErrorRep.rep.displayName} for URIError object`);
+    is(getRep(stub), ErrorRep.rep,
+      `Rep correctly selects Error Rep for URIError object`);
 
     const renderedComponent = renderComponent(ErrorRep.rep, {object: stub});
     is(renderedComponent.textContent,
       "URIError: URIError message\n" +
       "Stack trace:\n" +
       "@debugger eval code:16:13\n",
       "Error Rep has expected text content for URIError");
 
--- a/devtools/client/shared/components/reps/test/mochitest/test_reps_event.html
+++ b/devtools/client/shared/components/reps/test/mochitest/test_reps_event.html
@@ -15,24 +15,24 @@ Test Event rep
 <body>
 <pre id="test">
 <script src="head.js" type="application/javascript"></script>
 <script type="application/javascript">
 window.onload = Task.async(function* () {
   const {
     REPS,
     MODE,
+    getRep,
     getSelectableInInspectorGrips,
   } = browserRequire("devtools/client/shared/components/reps/reps");
-  let { Rep, Event } = REPS;
+  let { Event } = REPS;
 
   try {
     // Test that correct rep is chosen
-    const renderedRep = shallowRenderComponent(Rep, { object: getGripStub("testEvent") });
-    is(renderedRep.type, Event.rep, `Rep correctly selects ${Event.rep.displayName}`);
+    is(getRep(getGripStub("testEvent")), Event.rep, "Rep correctly selects Event Rep");
 
     yield testEvent();
     yield testMouseEvent();
     yield testKeyboardEvent();
     yield testKeyboardEventWithModifiers();
     yield testMessageEvent();
 
     yield testOnDomNodeMouseOver();
@@ -124,91 +124,70 @@ window.onload = Task.async(function* () 
   }
 
   function testOnDomNodeMouseOver() {
     const stub = getGripStub("testMouseEvent");
     const grips = getSelectableInInspectorGrips(stub);
 
     is(grips.length, 1, "the stub has one node grip");
 
-    const attachedActorIds = getStubAttachedActorIds(grips);
-
     let mouseOverValue;
     let onDOMNodeMouseOver = (object) => {
       mouseOverValue = object;
     };
     const renderedComponent = renderComponent(Event.rep, {
       object: stub,
       onDOMNodeMouseOver,
-      attachedActorIds,
     });
 
     const node = renderedComponent.querySelector(".objectBox-node");
     TestUtils.Simulate.mouseOver(node);
 
     is(mouseOverValue, grips[0], "onDOMNodeMouseOver is called with " +
       "the expected argument when mouseover is fired on the Rep");
   }
 
   function testOnDomNodeMouseOut() {
     const stub = getGripStub("testMouseEvent");
     const grips = getSelectableInInspectorGrips(stub);
     is(grips.length, 1, "the stub has one node grip");
 
-    const attachedActorIds = getStubAttachedActorIds(grips);
-
     let called = false;
     let onDOMNodeMouseOut = (object) => {
       called = true;
     };
     const renderedComponent = renderComponent(Event.rep, {
       object: stub,
       onDOMNodeMouseOut,
-      attachedActorIds
     });
 
     const node = renderedComponent.querySelector(".objectBox-node");
     TestUtils.Simulate.mouseOut(node);
 
     is(called, true, "onDOMNodeMouseOut is called when mouseout is fired on the Rep");
   }
 
   function testOnDomNodeInspectIconClick() {
     const stub = getGripStub("testMouseEvent");
     const grips = getSelectableInInspectorGrips(stub);
     is(grips.length, 1, "the stub has one node grip");
 
-    const attachedActorIds = getStubAttachedActorIds(grips);
-
     let inspectIconClickedValue = null;
     let onInspectIconClick = (object) => {
       inspectIconClickedValue = object;
     };
 
-    let renderedComponentWithoutInspectIcon = renderComponent(Event.rep, {
-      object: stub,
-      onInspectIconClick,
-      attachedActorIds: ["someOtherId"]
-    });
-    is(renderedComponentWithoutInspectIcon.querySelector(".open-inspector"), null,
-      "There isn't an inspect icon when the actor is not in attachedActorIds");
-
-    is(renderedComponentWithoutInspectIcon.querySelector(".open-inspector"), null,
-      "There isn't an inspect icon when attachedActorIds does not have keys " +
-      "matching grip event's target item");
-
     const renderedComponent = renderComponent(Event.rep, {
       object: stub,
       onInspectIconClick,
-      attachedActorIds
     });
 
     const icon = renderedComponent.querySelector(".open-inspector");
-    ok(icon !== null, "There is an icon as expected when passing a matching " +
-      "attachedActorIds item");
+    ok(icon !== null,
+      "There is an inspect icon when the node is connected to the DOM tree");
 
     TestUtils.Simulate.click(icon);
 
     is(inspectIconClickedValue, grips[0],
       "onInspectIconClick is called with the expected argument " +
       "when the inspect icon is clicked");
   }
 
@@ -339,16 +318,17 @@ window.onload = Task.async(function* () 
               "extensible": true,
               "frozen": false,
               "sealed": false,
               "ownPropertyLength": 0,
               "preview": {
                 "kind": "DOMNode",
                 "nodeType": 1,
                 "nodeName": "div",
+                "isConnected": true,
                 "attributes": {
                   "id": "test"
                 },
                 "attributesLength": 1
               }
             }
           }
         };
--- a/devtools/client/shared/components/reps/test/mochitest/test_reps_failure.html
+++ b/devtools/client/shared/components/reps/test/mochitest/test_reps_failure.html
@@ -13,18 +13,21 @@ Test fallback for rep rendering when a r
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
 </head>
 <body>
 <pre id="test">
 <script src="head.js" type="application/javascript"></script>
 <script type="application/javascript">
 window.onload = Task.async(function* () {
   try {
-    const { REPS } = browserRequire("devtools/client/shared/components/reps/reps");
-    let { Rep, ArrayRep, RegExp } = REPS;
+    const {
+      REPS,
+      getRep,
+    } = browserRequire("devtools/client/shared/components/reps/reps");
+    let { ArrayRep, RegExp } = REPS;
 
     // Force the RegExp rep to crash by creating RegExp grip that throws when accessing
     // the displayString property
     let gripStub = {
       "type": "object",
       "class": "RegExp",
       "actor": "server1.conn22.obj39",
       "extensible": true,
@@ -32,18 +35,17 @@ window.onload = Task.async(function* () 
       "sealed": false,
       "ownPropertyLength": 1,
       get displayString() {
         throw new Error("failure");
       }
     };
 
     // Test that correct rep is chosen.
-    const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
-    is(renderedRep.type, RegExp.rep, `Rep correctly selects ${RegExp.rep.displayName}`);
+    is(getRep(gripStub), RegExp.rep, "Rep correctly selects RegExp Rep");
 
     // Test fallback message is displayed when rendering bad rep directly.
     let renderedComponent = renderComponent(RegExp.rep, { object: gripStub });
     is(renderedComponent.textContent, "Invalid object", "Fallback rendering has expected text content");
 
     // Test fallback message is displayed when bad rep is nested in another rep.
     renderedComponent = renderComponent(ArrayRep.rep, { object: [1, gripStub, 2] });
     is(renderedComponent.textContent, "[ 1, Invalid object, 2 ]", "Fallback rendering has expected text content");
--- a/devtools/client/shared/components/reps/test/mochitest/test_reps_function.html
+++ b/devtools/client/shared/components/reps/test/mochitest/test_reps_function.html
@@ -12,26 +12,29 @@ Test Func rep
   <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
 </head>
 <body>
 <pre id="test">
 <script src="head.js" type="application/javascript"></script>
 <script type="application/javascript">
 window.onload = Task.async(function* () {
-  const { REPS, MODE } = browserRequire("devtools/client/shared/components/reps/reps");
-  let { Rep, Func } = REPS;
+  const {
+    REPS,
+    MODE,
+    getRep,
+   } = browserRequire("devtools/client/shared/components/reps/reps");
+  let { Func } = REPS;
 
   const componentUnderTest = Func;
 
   try {
     // Test that correct rep is chosen
     const gripStub = getGripStub("testNamed");
-    const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
-    is(renderedRep.type, Func.rep, `Rep correctly selects ${Func.rep.displayName}`);
+    is(getRep(gripStub), Func.rep, "Rep correctly selects Func Rep");
 
     yield testNamed();
     yield testVarNamed();
     yield testAnon();
     yield testLongName();
     yield testAsyncFunction();
     yield testAnonAsyncFunction();
     yield testGeneratorFunction();
--- a/devtools/client/shared/components/reps/test/mochitest/test_reps_grip-array.html
+++ b/devtools/client/shared/components/reps/test/mochitest/test_reps_grip-array.html
@@ -15,19 +15,20 @@ Test GripArray rep
 <body>
 <pre id="test">
 <script src="head.js" type="application/javascript"></script>
 <script type="application/javascript">
 window.onload = Task.async(function* () {
   const {
     REPS,
     MODE,
+    getRep,
     getSelectableInInspectorGrips,
   } = browserRequire("devtools/client/shared/components/reps/reps");
-  let { Rep, GripArray } = REPS;
+  let { GripArray } = REPS;
 
   let componentUnderTest = GripArray;
   const maxLength = {
     short: 3,
     long: 10
   };
 
   try {
@@ -55,18 +56,17 @@ window.onload = Task.async(function* () 
   }
 
   function testBasic() {
     // Test array: `[]`
     const testName = "testBasic";
 
     // Test that correct rep is chosen
     const gripStub = getGripStub("testBasic");
-    const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
-    is(renderedRep.type, GripArray.rep, `Rep correctly selects ${GripArray.rep.displayName}`);
+    is(getRep(gripStub), GripArray.rep, "Rep correctly selects GripArray Rep");
 
     // Test rendering
     const defaultOutput = `Array []`;
 
     const modeTests = [
       {
         mode: undefined,
         expectedOutput: defaultOutput,
@@ -323,26 +323,23 @@ window.onload = Task.async(function* () 
   }
 
   function testOnDomNodeMouseOver() {
     const stub = getGripStub("testNodeList");
     const grips = getSelectableInInspectorGrips(stub);
 
     is(grips.length, 3, "the stub has three node grips");
 
-    const attachedActorIds = getStubAttachedActorIds(grips);
-
     let mouseOverValue;
     let onDOMNodeMouseOver = (object) => {
       mouseOverValue = object;
     };
     const renderedComponent = renderComponent(GripArray.rep, {
       object: stub,
       onDOMNodeMouseOver,
-      attachedActorIds,
     });
 
     const nodes = renderedComponent.querySelectorAll(".objectBox-node");
     is(nodes.length, 3, "There are three node elements");
     nodes.forEach((node, index) => {
       TestUtils.Simulate.mouseOver(node);
 
       is(mouseOverValue, grips[index],
@@ -352,65 +349,57 @@ window.onload = Task.async(function* () 
   }
 
   function testOnDomNodeMouseOut() {
     const stub = getGripStub("testNodeList");
     const grips = getSelectableInInspectorGrips(stub);
 
     is(grips.length, 3, "the stub has three node grips");
 
-    const attachedActorIds = getStubAttachedActorIds(grips);
-
     let called = 0;
     let onDOMNodeMouseOut = (object) => {
       called++;
     };
     const renderedComponent = renderComponent(GripArray.rep, {
       object: stub,
       onDOMNodeMouseOut,
-      attachedActorIds,
     });
 
     const nodes = renderedComponent.querySelectorAll(".objectBox-node");
     info("Simulating mouseout on each node");
     Array.from(nodes).forEach(node => TestUtils.Simulate.mouseOut(node));
 
     is(called, 3, "onDOMNodeMouseOut is called when mouseout is fired on each NodeRep");
   }
 
   function testOnDomNodeInspectIconClick() {
-    const stub = getGripStub("testNodeList");
-    const grips = getSelectableInInspectorGrips(stub);
-
-    is(grips.length, 3, "the stub has three node grips");
-
-    const attachedActorIds = getStubAttachedActorIds(grips);
-
     let inspectIconClickedValue = null;
     let onInspectIconClick = (object) => {
       inspectIconClickedValue = object;
     };
 
     let renderedComponentWithoutInspectIcon = renderComponent(GripArray.rep, {
-      object: stub,
+      object: getGripStub("testDisconnectedNodeList"),
       onInspectIconClick,
-      attachedActorIds: ["someOtherId"],
     });
     is(renderedComponentWithoutInspectIcon.querySelector(".open-inspector"), null,
-      "There isn't an inspect icon when the actor is not in attachedActorIds");
+      "There isn't an inspect icon when the nodes are not connected to the DOM tree");
 
+    const stub = getGripStub("testNodeList");
+    const grips = getSelectableInInspectorGrips(stub);
+
+    is(grips.length, 3, "the stub has three node grips");
     const renderedComponent = renderComponent(GripArray.rep, {
       object: stub,
       onInspectIconClick,
-      attachedActorIds,
     });
 
     const icons = renderedComponent.querySelectorAll(".open-inspector");
     is(icons.length, grips.length,
-      "There is an icon for each grip array item with a matching attachedNodeFront");
+      "There is an icon for each node connected to the DOM tree");
 
     icons.forEach((icon, index) => {
       TestUtils.Simulate.click(icon);
 
       is(inspectIconClickedValue, grips[index],
         "onInspectIconClick is called with the expected argument " +
         "when the inspect icon is clicked");
     });
@@ -673,16 +662,17 @@ window.onload = Task.async(function* () 
                 "extensible": true,
                 "frozen": false,
                 "sealed": false,
                 "ownPropertyLength": 0,
                 "preview": {
                   "kind": "DOMNode",
                   "nodeType": 1,
                   "nodeName": "button",
+                  "isConnected": true,
                   "attributes": {
                     "id": "btn-1",
                     "class": "btn btn-log",
                     "type": "button"
                   },
                   "attributesLength": 3
                 }
               },
@@ -693,16 +683,17 @@ window.onload = Task.async(function* () 
                 "extensible": true,
                 "frozen": false,
                 "sealed": false,
                 "ownPropertyLength": 0,
                 "preview": {
                   "kind": "DOMNode",
                   "nodeType": 1,
                   "nodeName": "button",
+                  "isConnected": true,
                   "attributes": {
                     "id": "btn-2",
                     "class": "btn btn-err",
                     "type": "button"
                   },
                   "attributesLength": 3
                 }
               },
@@ -713,16 +704,97 @@ window.onload = Task.async(function* () 
                 "extensible": true,
                 "frozen": false,
                 "sealed": false,
                 "ownPropertyLength": 0,
                 "preview": {
                   "kind": "DOMNode",
                   "nodeType": 1,
                   "nodeName": "button",
+                  "isConnected": true,
+                  "attributes": {
+                    "id": "btn-3",
+                    "class": "btn btn-count",
+                    "type": "button"
+                  },
+                  "attributesLength": 3
+                }
+              }
+            ]
+          }
+        };
+
+      case "testDisconnectedNodeList":
+        return {
+          "type": "object",
+          "actor": "server1.conn1.child1/obj51",
+          "class": "NodeList",
+          "extensible": true,
+          "frozen": false,
+          "sealed": false,
+          "ownPropertyLength": 3,
+          "preview": {
+            "kind": "ArrayLike",
+            "length": 3,
+            "items": [
+              {
+                "type": "object",
+                "actor": "server1.conn1.child1/obj52",
+                "class": "HTMLButtonElement",
+                "extensible": true,
+                "frozen": false,
+                "sealed": false,
+                "ownPropertyLength": 0,
+                "preview": {
+                  "kind": "DOMNode",
+                  "nodeType": 1,
+                  "nodeName": "button",
+                  "isConnected": false,
+                  "attributes": {
+                    "id": "btn-1",
+                    "class": "btn btn-log",
+                    "type": "button"
+                  },
+                  "attributesLength": 3
+                }
+              },
+              {
+                "type": "object",
+                "actor": "server1.conn1.child1/obj53",
+                "class": "HTMLButtonElement",
+                "extensible": true,
+                "frozen": false,
+                "sealed": false,
+                "ownPropertyLength": 0,
+                "preview": {
+                  "kind": "DOMNode",
+                  "nodeType": 1,
+                  "nodeName": "button",
+                  "isConnected": false,
+                  "attributes": {
+                    "id": "btn-2",
+                    "class": "btn btn-err",
+                    "type": "button"
+                  },
+                  "attributesLength": 3
+                }
+              },
+              {
+                "type": "object",
+                "actor": "server1.conn1.child1/obj54",
+                "class": "HTMLButtonElement",
+                "extensible": true,
+                "frozen": false,
+                "sealed": false,
+                "ownPropertyLength": 0,
+                "preview": {
+                  "kind": "DOMNode",
+                  "nodeType": 1,
+                  "nodeName": "button",
+                  "isConnected": false,
                   "attributes": {
                     "id": "btn-3",
                     "class": "btn btn-count",
                     "type": "button"
                   },
                   "attributesLength": 3
                 }
               }
--- a/devtools/client/shared/components/reps/test/mochitest/test_reps_grip-map.html
+++ b/devtools/client/shared/components/reps/test/mochitest/test_reps_grip-map.html
@@ -17,19 +17,20 @@ Test GripMap rep
 <script src="head.js" type="application/javascript"></script>
 <script type="application/javascript">
 "use strict";
 
 window.onload = Task.async(function* () {
   const {
     REPS,
     MODE,
+    getRep,
     getSelectableInInspectorGrips,
   } = browserRequire("devtools/client/shared/components/reps/reps");
-  let { Rep, GripMap } = REPS;
+  let { GripMap } = REPS;
 
   const componentUnderTest = GripMap;
 
   try {
     yield testEmptyMap();
     yield testSymbolKeyedMap();
     yield testWeakMap();
 
@@ -50,18 +51,17 @@ window.onload = Task.async(function* () 
   }
 
   function testEmptyMap() {
     // Test object: `new Map()`
     const testName = "testEmptyMap";
 
     // Test that correct rep is chosen
     const gripStub = getGripStub("testEmptyMap");
-    const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
-    is(renderedRep.type, GripMap.rep, `Rep correctly selects ${GripMap.rep.displayName}`);
+    is(getRep(gripStub), GripMap.rep, "Rep correctly selects GripMap Rep");
 
     // Test rendering
     const defaultOutput = `Map {  }`;
 
     const modeTests = [
       {
         mode: undefined,
         expectedOutput: defaultOutput,
@@ -113,18 +113,17 @@ window.onload = Task.async(function* () 
   }
 
   function testWeakMap() {
     // Test object: `new WeakMap([[{a: "key-a"}, "value-a"]])`
     const testName = "testWeakMap";
 
     // Test that correct rep is chosen
     const gripStub = getGripStub("testWeakMap");
-    const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
-    is(renderedRep.type, GripMap.rep, `Rep correctly selects ${GripMap.rep.displayName}`);
+    is(getRep(gripStub), GripMap.rep, "Rep correctly selects GripMap Rep");
 
     // Test rendering
     const defaultOutput = `WeakMap { Object: "value-a" }`;
 
     const modeTests = [
       {
         mode: undefined,
         expectedOutput: defaultOutput,
@@ -249,47 +248,43 @@ window.onload = Task.async(function* () 
   }
 
   function testOnDomNodeMouseOver() {
     const nodeValuedStub = getGripStub("testNodeValuedMap");
     const nodeKeyedStub = getGripStub("testNodeKeyedMap");
 
     const valuesGrips = getSelectableInInspectorGrips(nodeValuedStub);
     is(valuesGrips.length, 3, "the stub has three node grips");
-    const valuesattachedActorIds = getStubAttachedActorIds(valuesGrips);
 
     const keysGrips = getSelectableInInspectorGrips(nodeKeyedStub);
     is(keysGrips.length, 3, "the stub has three node grips");
-    const keysAttachedActorIds = getStubAttachedActorIds(keysGrips);
 
     let mouseOverValue;
     let onDOMNodeMouseOver = (object) => {
       mouseOverValue = object;
     };
 
     info("Testing onDOMNodeMouseOver on node valued Map");
     const nodeValuedRenderedComponent = renderComponent(GripMap.rep, {
       object: nodeValuedStub,
       onDOMNodeMouseOver,
-      attachedActorIds: valuesattachedActorIds,
     });
 
     let nodes = nodeValuedRenderedComponent.querySelectorAll(".objectBox-node");
     nodes.forEach((node, index) => {
       TestUtils.Simulate.mouseOver(node);
       is(mouseOverValue, valuesGrips[index],
         "onDOMNodeMouseOver is called with the expected argument " +
         "when mouseover is fired on the Rep");
     });
 
     info("Testing onDOMNodeMouseOver on node keyed Map");
     const nodeKeyedRenderedComponent = renderComponent(GripMap.rep, {
       object: nodeKeyedStub,
       onDOMNodeMouseOver,
-      attachedActorIds: keysAttachedActorIds,
     });
 
     nodes = nodeKeyedRenderedComponent.querySelectorAll(".objectBox-node");
     nodes.forEach((node, index) => {
       TestUtils.Simulate.mouseOver(node);
       is(mouseOverValue, keysGrips[index],
         "onDOMNodeMouseOver is called with the expected argument " +
         "when mouseover is fired on the Rep");
@@ -297,45 +292,41 @@ window.onload = Task.async(function* () 
   }
 
   function testOnDomNodeMouseOut() {
     const nodeValuedStub = getGripStub("testNodeValuedMap");
     const nodeKeyedStub = getGripStub("testNodeKeyedMap");
 
     const valuesGrips = getSelectableInInspectorGrips(nodeValuedStub);
     is(valuesGrips.length, 3, "the stub has three node grips");
-    const valuesattachedActorIds = getStubAttachedActorIds(valuesGrips);
 
     const keysGrips = getSelectableInInspectorGrips(nodeKeyedStub);
     is(keysGrips.length, 3, "the stub has three node grips");
-    const keysAttachedActorIds = getStubAttachedActorIds(keysGrips);
 
     let called = 0;
     let onDOMNodeMouseOut = (object) => {
       called++;
     };
 
     info("Testing onDOMNodeMouseOut on node valued Map");
     const nodeValuedRenderedComponent = renderComponent(GripMap.rep, {
       object: nodeValuedStub,
       onDOMNodeMouseOut,
-      attachedActorIds: valuesattachedActorIds,
     });
 
     let nodes = nodeValuedRenderedComponent.querySelectorAll(".objectBox-node");
     info("Simulating mouseout on each value node");
     nodes.forEach((node, index) => TestUtils.Simulate.mouseOut(node));
     is(called, 3,
       "onDOMNodeMouseOut is called when mouseout is fired on each value NodeRep");
 
     info("Testing onDOMNodeMouseOut on node keyed Map");
     const nodeKeyedRenderedComponent = renderComponent(GripMap.rep, {
       object: nodeKeyedStub,
       onDOMNodeMouseOut,
-      attachedActorIds: keysAttachedActorIds,
     });
 
     nodes = nodeKeyedRenderedComponent.querySelectorAll(".objectBox-node");
     // Resets counter
     called = 0;
     info("Simulating mouseout on each key node");
     nodes.forEach((node, index) => TestUtils.Simulate.mouseOut(node));
     is(called, 3,
@@ -343,62 +334,57 @@ window.onload = Task.async(function* () 
   }
 
   function testOnDomNodeInspectIconClick() {
     const nodeValuedStub = getGripStub("testNodeValuedMap");
     const nodeKeyedStub = getGripStub("testNodeKeyedMap");
 
     const valuesGrips = getSelectableInInspectorGrips(nodeValuedStub);
     is(valuesGrips.length, 3, "the stub has three node grips");
-    const valuesattachedActorIds = getStubAttachedActorIds(valuesGrips);
 
     const keysGrips = getSelectableInInspectorGrips(nodeKeyedStub);
     is(keysGrips.length, 3, "the stub has three node grips");
-    const keysAttachedActorIds = getStubAttachedActorIds(keysGrips);
 
     let inspectIconClickedValue = null;
     let onInspectIconClick = (object) => {
       inspectIconClickedValue = object;
     };
 
     const renderedComponentWithoutInspectIcon = renderComponent(GripMap.rep, {
-      object: nodeValuedStub,
+      object: getGripStub("testDisconnectedNodeValuedMap"),
       onInspectIconClick,
-      attachedActorIds: [],
     });
     is(renderedComponentWithoutInspectIcon.querySelector(".open-inspector"), null,
-      "There isn't an inspect icon when the actor is not in attachedActorIds");
+      "There isn't an inspect icon when nodes are not connected to the DOM tree");
 
     info("Testing onInspectIconClick on node valued Map");
     const nodeValuedRenderedComponent = renderComponent(GripMap.rep, {
       object: nodeValuedStub,
       onInspectIconClick,
-      attachedActorIds: valuesattachedActorIds,
     });
 
     let icons = nodeValuedRenderedComponent.querySelectorAll(".open-inspector");
     is(icons.length, valuesGrips.length,
-      "There is an icon for each map value with a matching attachedNodeFront");
+      "There is an icon for each node connected to the DOM tree");
 
     icons.forEach((icon, index) => {
       TestUtils.Simulate.click(icon);
       is(inspectIconClickedValue, valuesGrips[index], "onInspectIconClick is called " +
         "with the expected argument when the inspect icon is clicked");
     });
 
     info("Testing onInspectIconClick on node keyed Map");
     const nodeKeyedRenderedComponent = renderComponent(GripMap.rep, {
       object: nodeKeyedStub,
       onInspectIconClick,
-      attachedActorIds: keysAttachedActorIds,
     });
 
     icons = nodeKeyedRenderedComponent.querySelectorAll(".open-inspector");
     is(icons.length, keysGrips.length,
-      "There is an icon for each map key with a matching attachedNodeFront");
+      "There is an icon for each node connected to the DOM tree");
 
     icons.forEach((icon, index) => {
       TestUtils.Simulate.click(icon);
       is(inspectIconClickedValue, keysGrips[index], "onInspectIconClick is called " +
         "with the expected argument when the inspect icon is clicked");
     });
   }
 
@@ -602,16 +588,102 @@ window.onload = Task.async(function* () 
               [
                 "key-d",
                 4
               ]
             ]
           }
         };
 
+      case "testDisconnectedNodeValuedMap":
+        return {
+          "type": "object",
+          "actor": "server1.conn1.child1/obj213",
+          "class": "Map",
+          "ownPropertyLength": 0,
+          "preview": {
+            "kind": "MapLike",
+            "size": 3,
+            "entries": [
+              [
+                "item-0",
+                {
+                  "type": "object",
+                  "actor": "server1.conn1.child1/obj214",
+                  "class": "HTMLButtonElement",
+                  "extensible": true,
+                  "frozen": false,
+                  "sealed": false,
+                  "ownPropertyLength": 0,
+                  "preview": {
+                    "kind": "DOMNode",
+                    "nodeType": 1,
+                    "nodeName": "button",
+                    "isConnected": false,
+                    "attributes": {
+                      "id": "btn-1",
+                      "class": "btn btn-log",
+                      "type": "button"
+                    },
+                    "attributesLength": 3
+                  }
+                }
+              ],
+              [
+                "item-1",
+                {
+                  "type": "object",
+                  "actor": "server1.conn1.child1/obj215",
+                  "class": "HTMLButtonElement",
+                  "extensible": true,
+                  "frozen": false,
+                  "sealed": false,
+                  "ownPropertyLength": 0,
+                  "preview": {
+                    "kind": "DOMNode",
+                    "nodeType": 1,
+                    "nodeName": "button",
+                    "isConnected": false,
+                    "attributes": {
+                      "id": "btn-2",
+                      "class": "btn btn-err",
+                      "type": "button"
+                    },
+                    "attributesLength": 3
+                  }
+                }
+              ],
+              [
+                "item-2",
+                {
+                  "type": "object",
+                  "actor": "server1.conn1.child1/obj216",
+                  "class": "HTMLButtonElement",
+                  "extensible": true,
+                  "frozen": false,
+                  "sealed": false,
+                  "ownPropertyLength": 0,
+                  "preview": {
+                    "kind": "DOMNode",
+                    "nodeType": 1,
+                    "nodeName": "button",
+                    "isConnected": false,
+                    "attributes": {
+                      "id": "btn-3",
+                      "class": "btn btn-count",
+                      "type": "button"
+                    },
+                    "attributesLength": 3
+                  }
+                }
+              ]
+            ]
+          }
+        };
+
       case "testNodeValuedMap":
         return {
           "type": "object",
           "actor": "server1.conn1.child1/obj213",
           "class": "Map",
           "ownPropertyLength": 0,
           "preview": {
             "kind": "MapLike",
@@ -626,16 +698,17 @@ window.onload = Task.async(function* () 
                   "extensible": true,
                   "frozen": false,
                   "sealed": false,
                   "ownPropertyLength": 0,
                   "preview": {
                     "kind": "DOMNode",
                     "nodeType": 1,
                     "nodeName": "button",
+                    "isConnected": true,
                     "attributes": {
                       "id": "btn-1",
                       "class": "btn btn-log",
                       "type": "button"
                     },
                     "attributesLength": 3
                   }
                 }
@@ -649,16 +722,17 @@ window.onload = Task.async(function* () 
                   "extensible": true,
                   "frozen": false,
                   "sealed": false,
                   "ownPropertyLength": 0,
                   "preview": {
                     "kind": "DOMNode",
                     "nodeType": 1,
                     "nodeName": "button",
+                    "isConnected": true,
                     "attributes": {
                       "id": "btn-2",
                       "class": "btn btn-err",
                       "type": "button"
                     },
                     "attributesLength": 3
                   }
                 }
@@ -672,16 +746,17 @@ window.onload = Task.async(function* () 
                   "extensible": true,
                   "frozen": false,
                   "sealed": false,
                   "ownPropertyLength": 0,
                   "preview": {
                     "kind": "DOMNode",
                     "nodeType": 1,
                     "nodeName": "button",
+                    "isConnected": true,
                     "attributes": {
                       "id": "btn-3",
                       "class": "btn btn-count",
                       "type": "button"
                     },
                     "attributesLength": 3
                   }
                 }
@@ -708,16 +783,17 @@ window.onload = Task.async(function* () 
                   "extensible": true,
                   "frozen": false,
                   "sealed": false,
                   "ownPropertyLength": 0,
                   "preview": {
                     "kind": "DOMNode",
                     "nodeType": 1,
                     "nodeName": "button",
+                    "isConnected": true,
                     "attributes": {
                       "id": "btn-1",
                       "class": "btn btn-log",
                       "type": "button"
                     },
                     "attributesLength": 3
                   }
                 },
@@ -731,16 +807,17 @@ window.onload = Task.async(function* () 
                   "extensible": true,
                   "frozen": false,
                   "sealed": false,
                   "ownPropertyLength": 0,
                   "preview": {
                     "kind": "DOMNode",
                     "nodeType": 1,
                     "nodeName": "button",
+                    "isConnected": true,
                     "attributes": {
                       "id": "btn-3",
                       "class": "btn btn-count",
                       "type": "button"
                     },
                     "attributesLength": 3
                   }
                 },
@@ -754,16 +831,17 @@ window.onload = Task.async(function* () 
                   "extensible": true,
                   "frozen": false,
                   "sealed": false,
                   "ownPropertyLength": 0,
                   "preview": {
                     "kind": "DOMNode",
                     "nodeType": 1,
                     "nodeName": "button",
+                    "isConnected": true,
                     "attributes": {
                       "id": "btn-2",
                       "class": "btn btn-err",
                       "type": "button"
                     },
                     "attributesLength": 3
                   }
                 },
--- a/devtools/client/shared/components/reps/test/mochitest/test_reps_grip.html
+++ b/devtools/client/shared/components/reps/test/mochitest/test_reps_grip.html
@@ -15,19 +15,20 @@ Test grip rep
 <body>
 <pre id="test">
 <script src="head.js" type="application/javascript"></script>
 <script type="application/javascript">
 window.onload = Task.async(function* () {
   const {
     REPS,
     MODE,
+    getRep,
     getSelectableInInspectorGrips,
   } = browserRequire("devtools/client/shared/components/reps/reps");
-  let { Rep, Grip } = REPS;
+  let { Grip } = REPS;
 
   const componentUnderTest = Grip;
 
   try {
     yield testBasic();
     yield testBooleanObject();
     yield testNumberObject();
     yield testStringObject();
@@ -61,18 +62,17 @@ window.onload = Task.async(function* () 
   }
 
   function testBasic() {
     // Test object: `{}`
     const testName = "testBasic";
 
     // Test that correct rep is chosen
     const gripStub = getGripStub("testBasic");
-    const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
-    is(renderedRep.type, Grip.rep, `Rep correctly selects ${Grip.rep.displayName}`);
+    is(getRep(gripStub), Grip.rep, "Rep correctly selects Grip Rep");
 
     // Test rendering
     const defaultOutput = `Object {  }`;
 
     const modeTests = [
       {
         mode: undefined,
         expectedOutput: defaultOutput,
@@ -95,18 +95,17 @@ window.onload = Task.async(function* () 
   }
 
   function testBooleanObject() {
     // Test object: `new Boolean(true)`
     const testName = "testBooleanObject";
 
     // Test that correct rep is chosen
     const gripStub = getGripStub(testName);
-    const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
-    is(renderedRep.type, Grip.rep, `Rep correctly selects ${Grip.rep.displayName}`);
+    is(getRep(gripStub), Grip.rep, "Rep correctly selects Grip Rep");
 
     // Test rendering
     const defaultOutput = `Boolean { true }`;
 
     const modeTests = [
       {
         mode: undefined,
         expectedOutput: defaultOutput,
@@ -129,18 +128,17 @@ window.onload = Task.async(function* () 
   }
 
   function testNumberObject() {
     // Test object: `new Number(42)`
     const testName = "testNumberObject";
 
     // Test that correct rep is chosen
     const gripStub = getGripStub(testName);
-    const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
-    is(renderedRep.type, Grip.rep, `Rep correctly selects ${Grip.rep.displayName}`);
+    is(getRep(gripStub), Grip.rep, "Rep correctly selects Grip Rep");
 
     // Test rendering
     const defaultOutput = `Number { 42 }`;
 
     const modeTests = [
       {
         mode: undefined,
         expectedOutput: defaultOutput,
@@ -163,18 +161,17 @@ window.onload = Task.async(function* () 
   }
 
   function testStringObject() {
     // Test object: `new String("foo")`
     const testName = "testStringObject";
 
     // Test that correct rep is chosen
     const gripStub = getGripStub(testName);
-    const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
-    is(renderedRep.type, Grip.rep, `Rep correctly selects ${Grip.rep.displayName}`);
+    is(getRep(gripStub), Grip.rep, "Rep correctly selects Grip Rep");
 
     // Test rendering
     const defaultOutput = `String { "foo" }`;
 
     const modeTests = [
       {
         mode: undefined,
         expectedOutput: defaultOutput,
@@ -197,18 +194,17 @@ window.onload = Task.async(function* () 
   }
 
   function testProxy() {
     // Test object: `new Proxy({a:1},[1,2,3])`
     const testName = "testProxy";
 
     // Test that correct rep is chosen
     const gripStub = getGripStub(testName);
-    const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
-    is(renderedRep.type, Grip.rep, `Rep correctly selects ${Grip.rep.displayName}`);
+    is(getRep(gripStub), Grip.rep, "Rep correctly selects Grip Rep");
 
     // Test rendering
     const defaultOutput = `Proxy { <target>: Object, <handler>: [3] }`;
 
     const modeTests = [
       {
         mode: undefined,
         expectedOutput: defaultOutput,
@@ -231,18 +227,17 @@ window.onload = Task.async(function* () 
   }
 
   function testArrayBuffer() {
     // Test object: `new ArrayBuffer(10)`
     const testName = "testArrayBuffer";
 
     // Test that correct rep is chosen
     const gripStub = getGripStub(testName);
-    const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
-    is(renderedRep.type, Grip.rep, `Rep correctly selects ${Grip.rep.displayName}`);
+    is(getRep(gripStub), Grip.rep, "Rep correctly selects Grip Rep");
 
     // Test rendering
     const defaultOutput = `ArrayBuffer { byteLength: 10 }`;
 
     const modeTests = [
       {
         mode: undefined,
         expectedOutput: defaultOutput,
@@ -265,18 +260,17 @@ window.onload = Task.async(function* () 
   }
 
   function testSharedArrayBuffer() {
     // Test object: `new SharedArrayBuffer(5)`
     const testName = "testSharedArrayBuffer";
 
     // Test that correct rep is chosen
     const gripStub = getGripStub(testName);
-    const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
-    is(renderedRep.type, Grip.rep, `Rep correctly selects ${Grip.rep.displayName}`);
+    is(getRep(gripStub), Grip.rep, "Rep correctly selects Grip Rep");
 
     // Test rendering
     const defaultOutput = `SharedArrayBuffer { byteLength: 5 }`;
 
     const modeTests = [
       {
         mode: undefined,
         expectedOutput: defaultOutput,
@@ -299,18 +293,17 @@ window.onload = Task.async(function* () 
   }
 
   function testApplicationCache() {
     // Test object: `window.applicationCache`
     const testName = "testApplicationCache";
 
     // Test that correct rep is chosen
     const gripStub = getGripStub(testName);
-    const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
-    is(renderedRep.type, Grip.rep, `Rep correctly selects ${Grip.rep.displayName}`);
+    is(getRep(gripStub), Grip.rep, "Rep correctly selects Grip Rep");
 
     // Test rendering
     const defaultOutput =
       "OfflineResourceList { status: 0, onchecking: null, onerror: null, 7 more… }";
 
     const modeTests = [
       {
         mode: undefined,
@@ -407,18 +400,17 @@ window.onload = Task.async(function* () 
   }
 
   function testNonEnumerableProps() {
     // Test object: `Object.defineProperty({}, "foo", {enumerable : false});`
     const testName = "testNonEnumerableProps";
 
     // Test that correct rep is chosen
     const gripStub = getGripStub("testNonEnumerableProps");
-    const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
-    is(renderedRep.type, Grip.rep, `Rep correctly selects ${Grip.rep.displayName}`);
+    is(getRep(gripStub), Grip.rep, "Rep correctly selects Grip Rep");
 
     // Test rendering
     const defaultOutput = `Object {  }`;
 
     const modeTests = [
       {
         mode: undefined,
         expectedOutput: defaultOutput,
@@ -532,29 +524,27 @@ window.onload = Task.async(function* () 
     testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
   }
 
   function testOnDomNodeMouseOver() {
     const stub = getGripStub("testObjectWithNodes");
 
     const grips = getSelectableInInspectorGrips(stub);
     is(grips.length, 2, "the stub has two node grips");
-    const attachedActorIds = getStubAttachedActorIds(grips);
 
     let mouseOverValue;
     let called = 0;
     let onDOMNodeMouseOver = (object) => {
       mouseOverValue = object;
       called++;
     };
 
     const renderedComponent = renderComponent(Grip.rep, {
       object: stub,
       onDOMNodeMouseOver,
-      attachedActorIds,
     });
 
     const nodes = renderedComponent.querySelectorAll(".objectBox-node");
     nodes.forEach((node, index) => {
       TestUtils.Simulate.mouseOver(node);
       is(mouseOverValue, grips[index],
         "onDOMNodeMouseOver is called with the expected argument " +
         "when mouseover is fired on the Rep");
@@ -563,69 +553,60 @@ window.onload = Task.async(function* () 
       "onDOMNodeMouseOver is called when mouseOverValue is fired on each NodeRep");
   }
 
   function testOnDomNodeMouseOut() {
     const stub = getGripStub("testObjectWithNodes");
 
     const grips = getSelectableInInspectorGrips(stub);
     is(grips.length, 2, "the stub has two node grips");
-    const attachedActorIds = getStubAttachedActorIds(grips);
 
     let called = 0;
     let onDOMNodeMouseOut = (object) => {
       called++;
     };
 
     const renderedComponent = renderComponent(Grip.rep, {
       object: stub,
       onDOMNodeMouseOut,
-      attachedActorIds,
     });
 
     const nodes = renderedComponent.querySelectorAll(".objectBox-node");
     info("Simulating mouseout on each node");
     Array.from(nodes).forEach(node => TestUtils.Simulate.mouseOut(node));
 
     is(called, 2, "onDOMNodeMouseOut is called when mouseout is fired on each NodeRep");
   }
 
   function testOnDomNodeInspectIconClick() {
     const stub = getGripStub("testObjectWithNodes");
 
     const grips = getSelectableInInspectorGrips(stub);
     is(grips.length, 2, "the stub has two node grips");
-    const attachedActorIds = getStubAttachedActorIds(grips);
 
     let inspectIconClickedValue = null;
     let onInspectIconClick = (object) => {
       inspectIconClickedValue = object;
     };
 
     let renderedComponentWithoutInspectIcon = renderComponent(Grip.rep, {
-      object: stub,
+      object: getGripStub("testObjectWithDisconnectedNodes"),
       onInspectIconClick,
-      attachedActorIds: ["someOtherId"],
     });
     is(renderedComponentWithoutInspectIcon.querySelector(".open-inspector"), null,
-      "There isn't an inspect icon when the actor is not in attachedActorIds");
-
-    is(renderedComponentWithoutInspectIcon.querySelector(".open-inspector"), null,
-      "There isn't an inspect icon when attachedActorIds does not have keys " +
-      "matching grip properties");
+      "There isn't an inspect icon when the node is not connected to the DOM tree");
 
     const renderedComponent = renderComponent(Grip.rep, {
       object: stub,
       onInspectIconClick,
-      attachedActorIds,
     });
 
     const icons = renderedComponent.querySelectorAll(".open-inspector");
     is(icons.length, 2,
-      "There is an icon for each grip property matching an attachedNodeFront");
+      "There is an icon for each node connected to the DOM tree");
 
     icons.forEach((icon, index) => {
       TestUtils.Simulate.click(icon);
       is(inspectIconClickedValue, grips[index],
         "onInspectIconClick is called with the expected argument " +
         "when the inspect icon is clicked");
     });
   }
@@ -1188,16 +1169,82 @@ window.onload = Task.async(function* () 
                   "extensible": true,
                   "frozen": false,
                   "sealed": false,
                   "ownPropertyLength": 0,
                   "preview": {
                     "kind": "DOMNode",
                     "nodeType": 1,
                     "nodeName": "button",
+                    "isConnected": true,
+                    "attributes": {
+                      "id": "btn-1",
+                      "class": "btn btn-log",
+                      "type": "button"
+                    },
+                    "attributesLength": 3
+                  }
+                }
+              },
+              "bar": {
+                "configurable": true,
+                "enumerable": true,
+                "writable": true,
+                "value": {
+                  "type": "object",
+                  "actor": "server1.conn1.child1/obj216",
+                  "class": "HTMLButtonElement",
+                  "extensible": true,
+                  "frozen": false,
+                  "sealed": false,
+                  "ownPropertyLength": 0,
+                  "preview": {
+                    "kind": "DOMNode",
+                    "nodeType": 1,
+                    "nodeName": "button",
+                    "isConnected": true,
+                    "attributes": {
+                      "id": "btn-2",
+                      "class": "btn btn-err",
+                      "type": "button"
+                    },
+                    "attributesLength": 3
+                  }
+                }
+              }
+            },
+            "ownPropertiesLength": 2,
+            "safeGetterValues": {}
+          }
+        };
+      case "testObjectWithDisconnectedNodes":
+        return {
+          "type": "object",
+          "actor": "server1.conn1.child1/obj214",
+          "class": "Object",
+          "ownPropertyLength": 2,
+          "preview": {
+            "kind": "Object",
+            "ownProperties": {
+              "foo": {
+                "configurable": true,
+                "enumerable": true,
+                "writable": true,
+                "value": {
+                  "type": "object",
+                  "actor": "server1.conn1.child1/obj215",
+                  "class": "HTMLButtonElement",
+                  "extensible": true,
+                  "frozen": false,
+                  "sealed": false,
+                  "ownPropertyLength": 0,
+                  "preview": {
+                    "kind": "DOMNode",
+                    "nodeType": 1,
+                    "nodeName": "button",
                     "attributes": {
                       "id": "btn-1",
                       "class": "btn btn-log",
                       "type": "button"
                     },
                     "attributesLength": 3
                   }
                 }
--- a/devtools/client/shared/components/reps/test/mochitest/test_reps_infinity.html
+++ b/devtools/client/shared/components/reps/test/mochitest/test_reps_infinity.html
@@ -14,44 +14,43 @@ Test Infinity rep
 </head>
 <body>
 <pre id="test">
 <script src="head.js" type="application/javascript"></script>
 <script type="application/javascript">
 "use strict";
 
 window.onload = Task.async(function* () {
-  const { REPS } = browserRequire("devtools/client/shared/components/reps/reps");
-  let { Rep, InfinityRep } = REPS;
+  const {
+    REPS,
+    getRep,
+  } = browserRequire("devtools/client/shared/components/reps/reps");
+  let { InfinityRep } = REPS;
 
   try {
     yield testInfinity();
     yield testNegativeInfinity();
   } catch (e) {
     ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
   } finally {
     SimpleTest.finish();
   }
 
   function testInfinity() {
     const stub = getGripStub("testInfinity");
-    const renderedRep = shallowRenderComponent(Rep, { object: stub });
-    is(renderedRep.type, InfinityRep.rep,
-      `Rep correctly selects ${InfinityRep.rep.displayName} for Infinity value`);
+    is(getRep(stub), InfinityRep.rep, "Rep correctly selects Infinity Rep");
 
     const renderedComponent = renderComponent(InfinityRep.rep, { object: stub });
     is(renderedComponent.textContent, "Infinity",
       "Infinity rep has expected text content for Infinity");
   }
 
   function testNegativeInfinity() {
     const stub = getGripStub("testNegativeInfinity");
-    const renderedRep = shallowRenderComponent(Rep, { object: stub });
-    is(renderedRep.type, InfinityRep.rep,
-      `Rep correctly selects ${InfinityRep.rep.displayName} for negative Infinity value`);
+    is(getRep(stub), InfinityRep.rep, "Rep correctly selects Infinity Rep");
 
     const renderedComponent = renderComponent(InfinityRep.rep, { object: stub });
     is(renderedComponent.textContent, "-Infinity",
       "Infinity rep has expected text content for negative Infinity");
   }
 
   function getGripStub(name) {
     switch (name) {
--- a/devtools/client/shared/components/reps/test/mochitest/test_reps_long-string.html
+++ b/devtools/client/shared/components/reps/test/mochitest/test_reps_long-string.html
@@ -12,24 +12,26 @@ Test LongString rep
   <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
 </head>
 <body>
 <pre id="test">
 <script src="head.js" type="application/javascript"></script>
 <script type="application/javascript">
 window.onload = Task.async(function* () {
-  const { REPS } = browserRequire("devtools/client/shared/components/reps/reps");
-  let { Rep, LongStringRep } = REPS;
+  const {
+    REPS,
+    getRep,
+  } = browserRequire("devtools/client/shared/components/reps/reps");
+  let { LongStringRep } = REPS;
 
   try {
     // Test that correct rep is chosen
-    const renderedRep = shallowRenderComponent(Rep, { object: getGripStub("testMultiline") });
-    is(renderedRep.type, LongStringRep.rep,
-      `Rep correctly selects ${LongStringRep.rep.displayName}`);
+    is(getRep(getGripStub("testMultiline")), LongStringRep.rep,
+      "Rep correctly selects LongString Rep");
 
     // Test rendering
     yield testMultiline();
     yield testMultilineOpen();
     yield testFullText();
     yield testMultilineLimit();
     yield testUseQuotes();
   } catch (e) {
--- a/devtools/client/shared/components/reps/test/mochitest/test_reps_nan.html
+++ b/devtools/client/shared/components/reps/test/mochitest/test_reps_nan.html
@@ -14,34 +14,35 @@ Test NaN rep
 </head>
 <body>
 <pre id="test">
 <script src="head.js" type="application/javascript"></script>
 <script type="application/javascript">
 "use strict";
 
 window.onload = Task.async(function* () {
-  const { REPS } = browserRequire("devtools/client/shared/components/reps/reps");
-  let { Rep, NaNRep } = REPS;
+  const {
+    REPS,
+    getRep,
+  } = browserRequire("devtools/client/shared/components/reps/reps");
+  let { NaNRep } = REPS;
 
   try {
     yield testNaN();
   } catch (e) {
     ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
   } finally {
     SimpleTest.finish();
   }
 
   function testNaN() {
     const stub = {
       type: "NaN"
     };
-    const renderedRep = shallowRenderComponent(Rep, {object: stub});
-    is(renderedRep.type, NaNRep.rep,
-      `Rep correctly selects ${NaNRep.rep.displayName} for NaN value`);
+    is(getRep(stub), NaNRep.rep, "Rep correctly selects NaN Rep");
 
     const renderedComponent = renderComponent(NaNRep.rep, {object: stub});
     is(renderedComponent.textContent, "NaN", "NaN rep has expected text content");
   }
 });
 </script>
 </pre>
 </body>
--- a/devtools/client/shared/components/reps/test/mochitest/test_reps_null.html
+++ b/devtools/client/shared/components/reps/test/mochitest/test_reps_null.html
@@ -13,26 +13,28 @@ Test Null rep
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
 </head>
 <body>
 <pre id="test">
 <script src="head.js" type="application/javascript"></script>
 <script type="application/javascript">
 window.onload = Task.async(function* () {
   try {
-    const { REPS } = browserRequire("devtools/client/shared/components/reps/reps");
-    let { Rep, Null } = REPS;
+    const {
+      REPS,
+      getRep,
+    } = browserRequire("devtools/client/shared/components/reps/reps");
+    let { Null } = REPS;
 
     let gripStub = {
       "type": "null"
     };
 
     // Test that correct rep is chosen
-    const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
-    is(renderedRep.type, Null.rep, `Rep correctly selects ${Null.rep.displayName}`);
+    is(getRep(gripStub), Null.rep, "Rep correctly selects Null Rep");
 
     // Test rendering
     const renderedComponent = renderComponent(Null.rep, { object: gripStub });
     is(renderedComponent.textContent, "null", "Null rep has expected text content");
   } catch(e) {
     ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
   } finally {
     SimpleTest.finish();
--- a/devtools/client/shared/components/reps/test/mochitest/test_reps_number.html
+++ b/devtools/client/shared/components/reps/test/mochitest/test_reps_number.html
@@ -12,53 +12,53 @@ Test Number rep
   <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
 </head>
 <body>
 <pre id="test">
 <script src="head.js" type="application/javascript"></script>
 <script type="application/javascript">
 window.onload = Task.async(function* () {
-  const { REPS } = browserRequire("devtools/client/shared/components/reps/reps");
-  let { Rep, Number } = REPS;
+  const {
+    REPS,
+    getRep,
+  } = browserRequire("devtools/client/shared/components/reps/reps");
+  let { Number } = REPS;
 
   try {
     yield testInt();
     yield testBoolean();
     yield testNegativeZero();
     yield testUnsafeInt();
   } catch(e) {
     ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
   } finally {
     SimpleTest.finish();
   }
 
 
   function testInt() {
-    const renderedRep = shallowRenderComponent(Rep, { object: getGripStub("testInt") });
-    is(renderedRep.type, Number.rep, `Rep correctly selects ${Number.rep.displayName} for integer value`);
+    is(getRep(getGripStub("testInt")), Number.rep, "Rep correctly selects Number Rep");
 
     const renderedComponent = renderComponent(Number.rep, { object: getGripStub("testInt") });
     is(renderedComponent.textContent, "5", "Number rep has expected text content for integer");
   }
 
   function testBoolean() {
-    const renderedRep = shallowRenderComponent(Rep, { object: getGripStub("testTrue") });
-    is(renderedRep.type, Number.rep, `Rep correctly selects ${Number.rep.displayName} for boolean value`);
+    is(getRep(getGripStub("testTrue")), Number.rep, "Rep correctly selects Number Rep for boolean value");
 
     let renderedComponent = renderComponent(Number.rep, { object: getGripStub("testTrue") });
     is(renderedComponent.textContent, "true", "Number rep has expected text content for boolean true");
 
     renderedComponent = renderComponent(Number.rep, { object: getGripStub("testFalse") });
     is(renderedComponent.textContent, "false", "Number rep has expected text content for boolean false");
   }
 
   function testNegativeZero() {
-    const renderedRep = shallowRenderComponent(Rep, { object: getGripStub("testNegZeroGrip") });
-    is(renderedRep.type, Number.rep, `Rep correctly selects ${Number.rep.displayName} for negative zero value`);
+    is(getRep(getGripStub("testNegZeroGrip")), Number.rep, "Rep correctly selects Number Rep for negative zero value");
 
     let renderedComponent = renderComponent(Number.rep, { object: getGripStub("testNegZeroGrip") });
     is(renderedComponent.textContent, "-0", "Number rep has expected text content for negative zero grip");
 
     renderedComponent = renderComponent(Number.rep, { object: getGripStub("testNegZeroValue") });
     is(renderedComponent.textContent, "-0", "Number rep has expected text content for negative zero value");
   }
 
--- a/devtools/client/shared/components/reps/test/mochitest/test_reps_object-with-text.html
+++ b/devtools/client/shared/components/reps/test/mochitest/test_reps_object-with-text.html
@@ -13,36 +13,38 @@ Test ObjectWithText rep
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
 </head>
 <body>
 <pre id="test">
 <script src="head.js" type="application/javascript"></script>
 <script type="application/javascript">
 window.onload = Task.async(function* () {
   try {
-    const { REPS } = browserRequire("devtools/client/shared/components/reps/reps");
-    let { Rep, ObjectWithText } = REPS;
+    const {
+      REPS,
+      getRep,
+    } = browserRequire("devtools/client/shared/components/reps/reps");
+    let { ObjectWithText } = REPS;
 
     let gripStub = {
       "type": "object",
       "class": "CSSStyleRule",
       "actor": "server1.conn3.obj273",
       "extensible": true,
       "frozen": false,
       "sealed": false,
       "ownPropertyLength": 0,
       "preview": {
         "kind": "ObjectWithText",
         "text": ".Shadow"
       }
     };
 
     // Test that correct rep is chosen
-    const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
-    is(renderedRep.type, ObjectWithText.rep, `Rep correctly selects ${ObjectWithText.rep.displayName}`);
+    is(getRep(gripStub), ObjectWithText.rep, "Rep correctly selects ObjectWithText Rep");
 
     // Test rendering
     const renderedComponent = renderComponent(ObjectWithText.rep, { object: gripStub });
     is(renderedComponent.textContent, "\".Shadow\"", "ObjectWithText rep has expected text content");
 
     // Test rendering with objectLink
     const objectLinkRenderedComponent = renderComponent(ObjectWithText.rep, {
       object: gripStub,
--- a/devtools/client/shared/components/reps/test/mochitest/test_reps_object-with-url.html
+++ b/devtools/client/shared/components/reps/test/mochitest/test_reps_object-with-url.html
@@ -16,36 +16,38 @@ Test ObjectWithURL rep
 <pre id="test">
 <script src="head.js" type="application/javascript"></script>
 <script type="application/javascript">
 window.onload = Task.async(function* () {
   try {
     let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
     let React = browserRequire("devtools/client/shared/vendor/react");
 
-    const { REPS } = browserRequire("devtools/client/shared/components/reps/reps");
-    let { Rep, ObjectWithURL } = REPS;
+    const {
+      REPS,
+      getRep,
+    } = browserRequire("devtools/client/shared/components/reps/reps");
+    let { ObjectWithURL } = REPS;
 
     let gripStub = {
       "type": "object",
       "class": "Location",
       "actor": "server1.conn2.obj272",
       "extensible": true,
       "frozen": false,
       "sealed": false,
       "ownPropertyLength": 15,
       "preview": {
         "kind": "ObjectWithURL",
         "url": "https://www.mozilla.org/en-US/"
       }
     };
 
     // Test that correct rep is chosen
-    const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
-    is(renderedRep.type, ObjectWithURL.rep, `Rep correctly selects ${ObjectWithURL.rep.displayName}`);
+    is(getRep(gripStub), ObjectWithURL.rep, "Rep correctly selects ObjectWithURL Rep");
 
     // Test rendering
     const renderedComponent = renderComponent(ObjectWithURL.rep, { object: gripStub });
     ok(renderedComponent.className.includes("objectBox-Location"), "ObjectWithURL rep has expected class name");
     const innerNode = renderedComponent.querySelector(".objectPropValue");
     is(innerNode.textContent, "https://www.mozilla.org/en-US/", "ObjectWithURL rep has expected inner HTML structure and text content");
 
     // Test rendering with objectLink
--- a/devtools/client/shared/components/reps/test/mochitest/test_reps_object.html
+++ b/devtools/client/shared/components/reps/test/mochitest/test_reps_object.html
@@ -12,18 +12,22 @@ Test Obj rep
   <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
 </head>
 <body>
 <pre id="test">
 <script src="head.js" type="application/javascript"></script>
 <script type="application/javascript">
 window.onload = Task.async(function* () {
-  const { REPS, MODE } = browserRequire("devtools/client/shared/components/reps/reps");
-  let { Rep, Obj } = REPS;
+  const {
+    REPS,
+    MODE,
+    getRep,
+  } = browserRequire("devtools/client/shared/components/reps/reps");
+  let { Obj } = REPS;
 
   const componentUnderTest = Obj;
 
   try {
     yield testBasic();
 
     // Test property iterator
     yield testMaxProps();
@@ -49,18 +53,17 @@ window.onload = Task.async(function* () 
   } finally {
     SimpleTest.finish();
   }
 
   function testBasic() {
     const stub = {};
 
     // Test that correct rep is chosen
-    const renderedRep = shallowRenderComponent(Rep, { object: stub });
-    is(renderedRep.type, Obj.rep, `Rep correctly selects ${Obj.rep.displayName}`);
+    is(getRep(stub), Obj.rep, "Rep correctly selects Obj Rep");
 
     // Test rendering
     const defaultOutput = `Object`;
 
     const modeTests = [
       {
         mode: undefined,
         expectedOutput: defaultOutput,
--- a/devtools/client/shared/components/reps/test/mochitest/test_reps_promise.html
+++ b/devtools/client/shared/components/reps/test/mochitest/test_reps_promise.html
@@ -17,19 +17,20 @@ Test Promise rep
 <script src="head.js" type="application/javascript"></script>
 <script type="application/javascript">
 "use strict";
 
 window.onload = Task.async(function* () {
   const {
     REPS,
     MODE,
+    getRep,
     getSelectableInInspectorGrips,
   } = browserRequire("devtools/client/shared/components/reps/reps");
-  let { Rep, PromiseRep } = REPS;
+  let { PromiseRep } = REPS;
 
   const componentUnderTest = PromiseRep;
 
   try {
     yield testPending();
     yield testFulfilledWithNumber();
     yield testFulfilledWithString();
     yield testFulfilledWithObject();
@@ -46,19 +47,18 @@ window.onload = Task.async(function* () 
     SimpleTest.finish();
   }
 
   function testPending() {
     // Test object = `new Promise((resolve, reject) => true)`
     const stub = getGripStub("testPending");
 
     // Test that correct rep is chosen.
-    const renderedRep = shallowRenderComponent(Rep, { object: stub });
-    is(renderedRep.type, PromiseRep.rep,
-      `Rep correctly selects ${PromiseRep.rep.displayName} for pending Promise`);
+    is(getRep(stub), PromiseRep.rep,
+      "Rep correctly selects PromiseRep Rep for pending Promise");
 
     // Test rendering
     const defaultOutput = `Promise { <state>: "pending" }`;
 
     const modeTests = [
       {
         mode: undefined,
         expectedOutput: defaultOutput,
@@ -79,20 +79,18 @@ window.onload = Task.async(function* () 
 
     testRepRenderModes(modeTests, "testPending", componentUnderTest, stub);
   }
   function testFulfilledWithNumber() {
     // Test object = `Promise.resolve(42)`
     const stub = getGripStub("testFulfilledWithNumber");
 
     // Test that correct rep is chosen.
-    const renderedRep = shallowRenderComponent(Rep, { object: stub });
-    const {displayName} = PromiseRep.rep;
-    is(renderedRep.type, PromiseRep.rep,
-      `Rep correctly selects ${displayName} for Promise fulfilled with a number`);
+    is(getRep(stub), PromiseRep.rep,
+      "Rep correctly selects PromiseRep Rep for Promise fulfilled with a number");
 
     // Test rendering
     const defaultOutput = `Promise { <state>: "fulfilled", <value>: 42 }`;
 
     const modeTests = [
       {
         mode: undefined,
         expectedOutput: defaultOutput,
@@ -113,20 +111,18 @@ window.onload = Task.async(function* () 
 
     testRepRenderModes(modeTests, "testFulfilledWithNumber", componentUnderTest, stub);
   }
   function testFulfilledWithString() {
     // Test object = `Promise.resolve("foo")`
     const stub = getGripStub("testFulfilledWithString");
 
     // Test that correct rep is chosen.
-    const renderedRep = shallowRenderComponent(Rep, { object: stub });
-    const {displayName} = PromiseRep.rep;
-    is(renderedRep.type, PromiseRep.rep,
-      `Rep correctly selects ${displayName} for Promise fulfilled with a string`);
+    is(getRep(stub), PromiseRep.rep,
+      "Rep correctly selects PromiseRep Rep for Promise fulfilled with a string");
 
     // Test rendering
     const defaultOutput = `Promise { <state>: "fulfilled", <value>: "foo" }`;
 
     const modeTests = [
       {
         mode: undefined,
         expectedOutput: defaultOutput,
@@ -148,20 +144,18 @@ window.onload = Task.async(function* () 
     testRepRenderModes(modeTests, "testFulfilledWithString", componentUnderTest, stub);
   }
 
   function testFulfilledWithObject() {
     // Test object = `Promise.resolve({foo: "bar", baz: "boo"})`
     const stub = getGripStub("testFulfilledWithObject");
 
     // Test that correct rep is chosen.
-    const renderedRep = shallowRenderComponent(Rep, { object: stub });
-    const {displayName} = PromiseRep.rep;
-    is(renderedRep.type, PromiseRep.rep,
-      `Rep correctly selects ${displayName} for Promise fulfilled with an object`);
+    is(getRep(stub), PromiseRep.rep,
+      "Rep correctly selects PromiseRep Rep for Promise fulfilled with an object");
 
     // Test rendering
     const defaultOutput = `Promise { <state>: "fulfilled", <value>: Object }`;
 
     const modeTests = [
       {
         mode: undefined,
         expectedOutput: defaultOutput,
@@ -183,20 +177,18 @@ window.onload = Task.async(function* () 
     testRepRenderModes(modeTests, "testFulfilledWithObject", componentUnderTest, stub);
   }
 
   function testFulfilledWithArray() {
     // Test object = `Promise.resolve([1,2,3])`
     const stub = getGripStub("testFulfilledWithArray");
 
     // Test that correct rep is chosen.
-    const renderedRep = shallowRenderComponent(Rep, { object: stub });
-    const {displayName} = PromiseRep.rep;
-    is(renderedRep.type, PromiseRep.rep,
-      `Rep correctly selects ${displayName} for Promise fulfilled with an array`);
+    is(getRep(stub), PromiseRep.rep,
+      "Rep correctly selects PromiseRep Rep for Promise fulfilled with an array");
 
     // Test rendering
     const defaultOutput = `Promise { <state>: "fulfilled", <value>: [3] }`;
 
     const modeTests = [
       {
         mode: undefined,
         expectedOutput: defaultOutput,
@@ -218,84 +210,76 @@ window.onload = Task.async(function* () 
     testRepRenderModes(modeTests, "testFulfilledWithArray", componentUnderTest, stub);
   }
 
   function testOnDomNodeMouseOver() {
     const stub = getGripStub("testFulfilledWithNode");
 
     const grips = getSelectableInInspectorGrips(stub);
     is(grips.length, 1, "the stub has one node grip");
-    const attachedActorIds = getStubAttachedActorIds(grips);
 
     let mouseOverValue;
     let onDOMNodeMouseOver = (object) => {
       mouseOverValue = object;
     };
 
     const renderedComponent = renderComponent(PromiseRep.rep, {
       object: stub,
       onDOMNodeMouseOver,
-      attachedActorIds,
     });
 
     const node = renderedComponent.querySelector(".objectBox-node");
     TestUtils.Simulate.mouseOver(node);
 
     is(mouseOverValue, grips[0], "onDOMNodeMouseOver is called with " +
       "the expected argument when mouseover is fired on the node element");
   }
 
   function testOnDomNodeMouseOut() {
     const stub = getGripStub("testFulfilledWithNode");
 
     const grips = getSelectableInInspectorGrips(stub);
     is(grips.length, 1, "the stub has one node grip");
-    const attachedActorIds = getStubAttachedActorIds(grips);
 
     let called = false;
     let onDOMNodeMouseOut = (object) => {
       called = true;
     };
     const renderedComponent = renderComponent(PromiseRep.rep, {
       object: stub,
       onDOMNodeMouseOut,
-      attachedActorIds,
     });
 
     const node = renderedComponent.querySelector(".objectBox-node");
     TestUtils.Simulate.mouseOut(node);
 
     is(called, true,
       "onDOMNodeMouseOut is called when mouseout is fired on the node element");
   }
 
   function testOnDomNodeInspectIconClick() {
     const stub = getGripStub("testFulfilledWithNode");
-
     const grips = getSelectableInInspectorGrips(stub);
     is(grips.length, 1, "the stub has one node grip");
-    const attachedActorIds = getStubAttachedActorIds(grips);
 
     let inspectIconClickedValues = null;
     let onInspectIconClick = (object) => {
       inspectIconClickedValues = object;
     };
 
     let renderedComponentWithoutInspectIcon = renderComponent(PromiseRep.rep, {
-      object: stub,
+      object: getGripStub("testFulfilledWithDisconnectedNode"),
       onInspectIconClick,
-      attachedActorIds: ["someOtherId"],
     });
     is(renderedComponentWithoutInspectIcon.querySelector(".open-inspector"), null,
-      "There isn't an inspect icon when the actor is not in attachedActorIds");
+      "There isn't an inspect icon when the node is not connected to the DOM tree");
 
     const renderedComponent = renderComponent(PromiseRep.rep, {
       object: stub,
       onInspectIconClick,
-      attachedActorIds,
     });
 
     const icon = renderedComponent.querySelector(".open-inspector");
     ok(icon !== null, "There is an inspect icon as expected");
 
     TestUtils.Simulate.click(icon);
     is(inspectIconClickedValues, grips[0],
       "onInspectIconClick is called with the expected argument " +
@@ -469,16 +453,56 @@ window.onload = Task.async(function* () 
               "extensible": true,
               "frozen": false,
               "sealed": false,
               "ownPropertyLength": 0,
               "preview": {
                 "kind": "DOMNode",
                 "nodeType": 1,
                 "nodeName": "button",
+                "isConnected": true,
+                "attributes": {
+                  "id": "btn-1",
+                  "class": "btn btn-log",
+                  "type": "button"
+                },
+                "attributesLength": 3
+              }
+            },
+            "creationTimestamp": 1480423091620.3716,
+            "timeToSettle": 0.02842400000372436
+          },
+          "ownPropertyLength": 0,
+          "preview": {
+            "kind": "Object",
+            "ownProperties": {},
+            "ownPropertiesLength": 0,
+            "safeGetterValues": {}
+          }
+        };
+      case "testFulfilledWithDisconnectedNode":
+        return {
+          "type": "object",
+          "actor": "server1.conn1.child1/obj217",
+          "class": "Promise",
+          "promiseState": {
+            "state": "fulfilled",
+            "value": {
+              "type": "object",
+              "actor": "server1.conn1.child1/obj218",
+              "class": "HTMLButtonElement",
+              "extensible": true,
+              "frozen": false,
+              "sealed": false,
+              "ownPropertyLength": 0,
+              "preview": {
+                "kind": "DOMNode",
+                "nodeType": 1,
+                "nodeName": "button",
+                "isConnected": false,
                 "attributes": {
                   "id": "btn-1",
                   "class": "btn btn-log",
                   "type": "button"
                 },
                 "attributesLength": 3
               }
             },
--- a/devtools/client/shared/components/reps/test/mochitest/test_reps_regexp.html
+++ b/devtools/client/shared/components/reps/test/mochitest/test_reps_regexp.html
@@ -13,33 +13,35 @@ Test RegExp rep
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
 </head>
 <body>
 <pre id="test">
 <script src="head.js" type="application/javascript"></script>
 <script type="application/javascript">
 window.onload = Task.async(function* () {
   try {
-    const { REPS } = browserRequire("devtools/client/shared/components/reps/reps");
-    let { Rep, RegExp } = REPS;
+    const {
+      REPS,
+      getRep,
+    } = browserRequire("devtools/client/shared/components/reps/reps");
+    let { RegExp } = REPS;
 
     let gripStub = {
       "type": "object",
       "class": "RegExp",
       "actor": "server1.conn22.obj39",
       "extensible": true,
       "frozen": false,
       "sealed": false,
       "ownPropertyLength": 1,
       "displayString": "/ab+c/i"
     };
 
     // Test that correct rep is chosen
-    const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
-    is(renderedRep.type, RegExp.rep, `Rep correctly selects ${RegExp.rep.displayName}`);
+    is(getRep(gripStub), RegExp.rep, "Rep correctly selects RegExp Rep");
 
     // Test rendering
     const renderedComponent = renderComponent(RegExp.rep, { object: gripStub });
     is(renderedComponent.textContent, "/ab+c/i", "RegExp rep has expected text content");
 
     // Test rendering with objectLink
     const objectLinkRenderedComponent = renderComponent(RegExp.rep, {
       object: gripStub,
--- a/devtools/client/shared/components/reps/test/mochitest/test_reps_string.html
+++ b/devtools/client/shared/components/reps/test/mochitest/test_reps_string.html
@@ -12,18 +12,21 @@ Test String rep
   <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
 </head>
 <body>
 <pre id="test">
 <script src="head.js" type="application/javascript"></script>
 <script type="application/javascript">
 window.onload = Task.async(function* () {
-  const { REPS } = browserRequire("devtools/client/shared/components/reps/reps");
-  let { Rep, StringRep } = REPS;
+  const {
+    REPS,
+    getRep,
+   } = browserRequire("devtools/client/shared/components/reps/reps");
+  let { StringRep } = REPS;
 
   const test_cases = [{
     name: "testMultiline",
     props: {
       object: "aaaaaaaaaaaaaaaaaaaaa\nbbbbbbbbbbbbbbbbbbb\ncccccccccccccccc\n",
     },
     result: "\"aaaaaaaaaaaaaaaaaaaaa\\nbbbbbbbbbbbbbbbbbbb\\ncccccccccccccccc\\n\""
   }, {
@@ -70,22 +73,30 @@ window.onload = Task.async(function* () 
     result: "\"\\udc23\""
   }, {
     name: "testValidSurrogate",
     props: {
       object: "\ud83d\udeec",
       useQuotes: true
     },
     result: "\"\ud83d\udeec\""
+  }, {
+    name: "testNoEscapeWhitespace",
+    props: {
+      object: "line 1\r\nline 2\n\tline 3",
+      useQuotes: true,
+      escapeWhitespace: false,
+    },
+    result: "\"line 1\r\nline 2\n\tline 3\""
   }];
 
   try {
     // Test that correct rep is chosen
-    const renderedRep = shallowRenderComponent(Rep, test_cases[0].props);
-    is(renderedRep.type, StringRep.rep, `Rep correctly selects ${StringRep.rep.displayName}`);
+    is(getRep(test_cases[0].props.object), StringRep.rep,
+      "Rep correctly selects String Rep");
 
     // Test rendering
     for (let test of test_cases) {
       const renderedComponent = renderComponent(StringRep.rep, test.props);
       is(renderedComponent.textContent, test.result, "String rep " + test.name);
     }
   } catch(e) {
     ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
--- a/devtools/client/shared/components/reps/test/mochitest/test_reps_stylesheet.html
+++ b/devtools/client/shared/components/reps/test/mochitest/test_reps_stylesheet.html
@@ -13,36 +13,38 @@ Test Stylesheet rep
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
 </head>
 <body>
 <pre id="test">
 <script src="head.js" type="application/javascript"></script>
 <script type="application/javascript">
 window.onload = Task.async(function* () {
   try {
-    const { REPS } = browserRequire("devtools/client/shared/components/reps/reps");
-    let { Rep, StyleSheet } = REPS;
+    const {
+      REPS,
+      getRep,
+    } = browserRequire("devtools/client/shared/components/reps/reps");
+    let { StyleSheet } = REPS;
 
     let gripStub = {
       "type": "object",
       "class": "CSSStyleSheet",
       "actor": "server1.conn2.obj1067",
       "extensible": true,
       "frozen": false,
       "sealed": false,
       "ownPropertyLength": 0,
       "preview": {
         "kind": "ObjectWithURL",
         "url": "https://example.com/styles.css"
       }
     };
 
     // Test that correct rep is chosen
-    const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
-    is(renderedRep.type, StyleSheet.rep, `Rep correctly selects ${StyleSheet.rep.displayName}`);
+    is(getRep(gripStub), StyleSheet.rep, "Rep correctly selects StyleSheet Rep");
 
     // Test rendering
     const renderedComponent = renderComponent(StyleSheet.rep, { object: gripStub });
     is(renderedComponent.textContent, "StyleSheet https://example.com/styles.css", "StyleSheet rep has expected text content");
 
     // Test rendering with objectLink
     const objectLinkRenderedComponent = renderComponent(StyleSheet.rep, {
       object: gripStub,
--- a/devtools/client/shared/components/reps/test/mochitest/test_reps_symbol.html
+++ b/devtools/client/shared/components/reps/test/mochitest/test_reps_symbol.html
@@ -15,37 +15,35 @@ Test Symbol rep
 <body>
 <pre id="test">
 <script src="head.js" type="application/javascript"></script>
 <script type="application/javascript">
 "use strict";
 /* import-globals-from head.js */
 
 window.onload = Task.async(function* () {
-  const { REPS } = browserRequire("devtools/client/shared/components/reps/reps");
-  let { Rep, SymbolRep } = REPS;
+  const {
+    REPS,
+    getRep,
+  } = browserRequire("devtools/client/shared/components/reps/reps");
+  let { SymbolRep } = REPS;
 
   let gripStubs = new Map();
   gripStubs.set("testSymbolFoo", {
     type: "symbol",
     name: "foo"
   });
   gripStubs.set("testSymbolWithoutIdentifier", {
     type: "symbol"
   });
 
   try {
     // Test that correct rep is chosen
-    const renderedRep = shallowRenderComponent(
-      Rep,
-      { object: gripStubs.get("testSymbolFoo")}
-    );
-
-    is(renderedRep.type, SymbolRep.rep,
-      `Rep correctly selects ${SymbolRep.rep.displayName}`);
+    is(getRep(gripStubs.get("testSymbolFoo")), SymbolRep.rep,
+      "Rep correctly selects SymbolRep Rep");
 
     // Test rendering
     yield testSymbol();
     yield testSymbolWithoutIdentifier();
   } catch (e) {
     ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
   } finally {
     SimpleTest.finish();
--- a/devtools/client/shared/components/reps/test/mochitest/test_reps_text-node.html
+++ b/devtools/client/shared/components/reps/test/mochitest/test_reps_text-node.html
@@ -17,50 +17,59 @@ Test text-node rep
 <script src="head.js" type="application/javascript"></script>
 <script type="application/javascript">
 "use strict";
 
 window.onload = Task.async(function* () {
   const {
     REPS,
     MODE,
+    getRep,
     getSelectableInInspectorGrips,
   } = browserRequire("devtools/client/shared/components/reps/reps");
-  let { Rep, TextNode } = REPS;
+  let { TextNode } = REPS;
 
   let gripStubs = new Map();
   gripStubs.set("testRendering", {
     "class": "Text",
     "actor": "server1.conn1.child1/obj50",
     "preview": {
       "kind": "DOMNode",
       "nodeType": 3,
       "nodeName": "#text",
-      "textContent": "hello world"
+      "textContent": "hello world",
+      "isConnected": true,
+    }
+  });
+  gripStubs.set("testRenderingDisconnected", {
+    "class": "Text",
+    "actor": "server1.conn1.child1/obj50",
+    "preview": {
+      "kind": "DOMNode",
+      "nodeType": 3,
+      "nodeName": "#text",
+      "textContent": "hello world",
+      "isConnected": false,
     }
   });
   gripStubs.set("testRenderingWithEOL", {
     "class": "Text",
     "actor": "server1.conn1.child1/obj50",
     "preview": {
       "kind": "DOMNode",
       "nodeType": 3,
       "nodeName": "#text",
       "textContent": "hello\nworld"
     }
   });
 
   try {
     // Test that correct rep is chosen
-    const renderedRep = shallowRenderComponent(Rep, {
-      object: gripStubs.get("testRendering")
-    });
-
-    is(renderedRep.type, TextNode.rep,
-      `Rep correctly selects ${TextNode.rep.displayName}`);
+    is(getRep(gripStubs.get("testRendering")), TextNode.rep,
+      "Rep correctly selects TextNode Rep");
 
     yield testRendering();
     yield testRenderingWithEOL();
 
     yield testOnMouseOver();
     yield testOnMouseOut();
     yield testOnInspectIconClick();
 
@@ -123,81 +132,74 @@ window.onload = Task.async(function* () 
     testRepRenderModes(modeTests, "testRenderingWithEOL", TextNode, stub);
   }
 
   function testOnMouseOver() {
     const stub = gripStubs.get("testRendering");
 
     const grips = getSelectableInInspectorGrips(stub);
     is(grips.length, 1, "the stub has one text node grip");
-    const attachedActorIds = getStubAttachedActorIds(grips);
 
     let mouseOverValue;
     let onDOMNodeMouseOver = (object) => {
       mouseOverValue = object;
     };
     const renderedComponent = renderComponent(TextNode.rep, {
       object: stub,
       onDOMNodeMouseOver,
-      attachedActorIds,
     });
 
     TestUtils.Simulate.mouseOver(renderedComponent);
     is(mouseOverValue, grips[0], "onDOMNodeMouseOver is called " +
       "with the expected argument when mouseover is fired on the Rep");
   }
 
   function testOnMouseOut() {
     const stub = gripStubs.get("testRendering");
 
     const grips = getSelectableInInspectorGrips(stub);
     is(grips.length, 1, "the stub has one text node grip");
-    const attachedActorIds = getStubAttachedActorIds(grips);
 
     let called = false;
     let onDOMNodeMouseOut = (object) => {
       called = true;
     };
     const renderedComponent = renderComponent(TextNode.rep, {
       object: stub,
       onDOMNodeMouseOut,
-      attachedActorIds,
     });
 
     TestUtils.Simulate.mouseOut(renderedComponent);
     is(called, true, "onDOMNodeMouseOut is called when mouseout is fired on the Rep");
   }
 
   function testOnInspectIconClick() {
     const stub = gripStubs.get("testRendering");
 
     const grips = getSelectableInInspectorGrips(stub);
     is(grips.length, 1, "the stub has one text node grip");
-    const attachedActorIds = getStubAttachedActorIds(grips);
 
     let inspectIconClickedValue = null;
     let inspectIconClickedEvent = null;
 
     let onInspectIconClick = (object, event) => {
       inspectIconClickedValue = object;
       inspectIconClickedEvent = event;
     };
 
     const renderedComponentWithoutInspectIcon = renderComponent(TextNode.rep, {
-      object: stub,
+      object: gripStubs.get("testRenderingDisconnected"),
       onInspectIconClick,
-      attachedActorIds: ["someOtherId"]
     });
     is(renderedComponentWithoutInspectIcon.querySelector(".open-inspector"), null,
-      "There isn't an inspect icon when the actor is not in attachedActorIds");
+      "There isn't an inspect icon when the node is not connected to the DOM tree");
 
     const renderedComponent = renderComponent(TextNode.rep, {
       object: stub,
       onInspectIconClick,
-      attachedActorIds,
     });
 
     const inspectIconNode = renderedComponent.querySelector(".open-inspector");
     ok(inspectIconNode !== null, "There is an inspect icon as expected");
     TestUtils.Simulate.click(inspectIconNode);
 
     is(inspectIconClickedValue, grips[0],
       "onInspectIconClick is called with expected value when inspect icon is clicked");
--- a/devtools/client/shared/components/reps/test/mochitest/test_reps_undefined.html
+++ b/devtools/client/shared/components/reps/test/mochitest/test_reps_undefined.html
@@ -15,26 +15,28 @@ Test undefined rep
 <body>
 <pre id="test">
 <script src="head.js" type="application/javascript"></script>
 <script type="application/javascript">
 window.onload = Task.async(function* () {
   try {
     let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
     let React = browserRequire("devtools/client/shared/vendor/react");
-    const { REPS } = browserRequire("devtools/client/shared/components/reps/reps");
-    let { Rep, Undefined } = REPS;
+    const {
+      REPS,
+      getRep,
+    } = browserRequire("devtools/client/shared/components/reps/reps");
+    let { Undefined } = REPS;
 
     let gripStub = {
       "type": "undefined"
     };
 
     // Test that correct rep is chosen
-    const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
-    is(renderedRep.type, Undefined.rep, `Rep correctly selects ${Undefined.rep.displayName}`);
+    is(getRep(gripStub), Undefined.rep, "Rep correctly selects Undefined Rep");
 
     // Test rendering
     const renderedComponent = renderComponent(Undefined.rep, {});
     is(renderedComponent.className, "objectBox objectBox-undefined", "Undefined rep has expected class names");
     is(renderedComponent.textContent, "undefined", "Undefined rep has expected text content");
   } catch(e) {
     ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
   } finally {
--- a/devtools/client/shared/components/reps/test/mochitest/test_reps_window.html
+++ b/devtools/client/shared/components/reps/test/mochitest/test_reps_window.html
@@ -16,17 +16,21 @@ Test window rep
 <pre id="test">
 <script src="head.js" type="application/javascript"></script>
 <script type="application/javascript">
 window.onload = Task.async(function* () {
   try {
     let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
     let React = browserRequire("devtools/client/shared/vendor/react");
 
-    const { REPS, MODE } = browserRequire("devtools/client/shared/components/reps/reps");
+    const {
+      REPS,
+      MODE,
+      getRep,
+    } = browserRequire("devtools/client/shared/components/reps/reps");
     let { Rep, Window } = REPS;
 
     let gripStub = {
       "type": "object",
       "class": "Window",
       "actor": "server1.conn3.obj198",
       "extensible": true,
       "frozen": false,
@@ -34,21 +38,20 @@ window.onload = Task.async(function* () 
       "ownPropertyLength": 887,
       "preview": {
         "kind": "ObjectWithURL",
         "url": "about:newtab"
       }
     };
 
     // Test that correct rep is chosen
-    const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
-    is(renderedRep.type, Window.rep, `Rep correctly selects ${Window.rep.displayName}`);
+    is(getRep(gripStub), Window.rep, "Rep correctly selects Window Rep");
 
     // Test rendering
-    const renderedComponent = renderComponent(Window.rep, { object: gripStub });
+    const renderedComponent = renderComponent(Rep, { object: gripStub });
     ok(renderedComponent.className.includes("objectBox-Window"), "Window rep has expected class name");
     is(renderedComponent.textContent, "Window about:newtab", "Window rep has expected text content");
     const innerNode = renderedComponent.querySelector(".objectPropValue");
     is(innerNode.textContent, "about:newtab", "Window rep has expected inner HTML structure and text content");
 
     const tinyRenderedComponent = renderComponent(Window.rep, {
       object: gripStub,
       mode: MODE.TINY,
--- a/devtools/client/shared/telemetry.js
+++ b/devtools/client/shared/telemetry.js
@@ -133,23 +133,23 @@ Telemetry.prototype = {
     },
     menueyedropper: {
       histogram: "DEVTOOLS_MENU_EYEDROPPER_OPENED_COUNT",
     },
     pickereyedropper: {
       histogram: "DEVTOOLS_PICKER_EYEDROPPER_OPENED_COUNT",
     },
     toolbareyedropper: {
-      histogram: "DEVTOOLS_TOOLBAR_EYEDROPPER_OPENED_COUNT",
+      scalar: "devtools.toolbar.eyedropper.opened",
     },
     copyuniquecssselector: {
-      histogram: "DEVTOOLS_COPY_UNIQUE_CSS_SELECTOR_OPENED_COUNT",
+      scalar: "devtools.copy.unique.css.selector.opened",
     },
     copyfullcssselector: {
-      histogram: "DEVTOOLS_COPY_FULL_CSS_SELECTOR_OPENED_COUNT",
+      scalar: "devtools.copy.full.css.selector.opened",
     },
     developertoolbar: {
       histogram: "DEVTOOLS_DEVELOPERTOOLBAR_OPENED_COUNT",
       timerHistogram: "DEVTOOLS_DEVELOPERTOOLBAR_TIME_ACTIVE_SECONDS"
     },
     aboutdebugging: {
       histogram: "DEVTOOLS_ABOUTDEBUGGING_OPENED_COUNT",
       timerHistogram: "DEVTOOLS_ABOUTDEBUGGING_TIME_ACTIVE_SECONDS"
@@ -194,16 +194,19 @@ Telemetry.prototype = {
     let charts = this._histograms[id] || this._histograms.custom;
 
     if (charts.histogram) {
       this.log(charts.histogram, true);
     }
     if (charts.timerHistogram) {
       this.startTimer(charts.timerHistogram);
     }
+    if (charts.scalar) {
+      this.logScalar(charts.scalar, 1);
+    }
   },
 
   /**
    * Record that an action occurred.  Aliases to `toolOpened`, so it's just for
    * readability at the call site for cases where we aren't actually opening
    * tools.
    */
   actionOccurred(id) {
@@ -255,24 +258,54 @@ Telemetry.prototype = {
    * Log a value to a histogram.
    *
    * @param  {String} histogramId
    *         Histogram in which the data is to be stored.
    * @param  value
    *         Value to store.
    */
   log: function (histogramId, value) {
-    if (histogramId) {
-      try {
-        let histogram = Services.telemetry.getHistogramById(histogramId);
-        histogram.add(value);
-      } catch (e) {
-        dump("Warning: An attempt was made to write to the " + histogramId +
-             " histogram, which is not defined in Histograms.json\n");
+    if (!histogramId) {
+      return;
+    }
+
+    try {
+      let histogram = Services.telemetry.getHistogramById(histogramId);
+      histogram.add(value);
+    } catch (e) {
+      dump(`Warning: An attempt was made to write to the ${histogramId} ` +
+           `histogram, which is not defined in Histograms.json\n`);
+    }
+  },
+
+  /**
+   * Log a value to a scalar.
+   *
+   * @param  {String} scalarId
+   *         Scalar in which the data is to be stored.
+   * @param  value
+   *         Value to store.
+   */
+  logScalar: function (scalarId, value) {
+    if (!scalarId) {
+      return;
+    }
+
+    try {
+      if (isNaN(value)) {
+        dump(`Warning: An attempt was made to write a non-numeric value ` +
+             `${value} to the ${scalarId} scalar. Only numeric values are ` +
+             `allowed.`);
+
+        return;
       }
+      Services.telemetry.scalarSet(scalarId, value);
+    } catch (e) {
+      dump(`Warning: An attempt was made to write to the ${scalarId} ` +
+           `scalar, which is not defined in Scalars.yaml\n`);
     }
   },
 
   /**
    * Log a value to a keyed histogram.
    *
    * @param  {String} histogramId
    *         Histogram in which the data is to be stored.
@@ -287,18 +320,18 @@ Telemetry.prototype = {
         let histogram = Services.telemetry.getKeyedHistogramById(histogramId);
 
         if (typeof value === "undefined") {
           histogram.add(key);
         } else {
           histogram.add(key, value);
         }
       } catch (e) {
-        dump("Warning: An attempt was made to write to the " + histogramId +
-             " histogram, which is not defined in Histograms.json\n");
+        dump(`Warning: An attempt was made to write to the ${histogramId} ` +
+             `histogram, which is not defined in Histograms.json\n`);
       }
     }
   },
 
   /**
    * Log info about usage once per browser version. This allows us to discover
    * how many individual users are using our tools for each browser version.
    *
--- a/devtools/client/shared/test/browser_telemetry_button_eyedropper.js
+++ b/devtools/client/shared/test/browser_telemetry_button_eyedropper.js
@@ -1,14 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
 const TEST_URI = "data:text/html;charset=utf-8," +
   "<p>browser_telemetry_button_eyedropper.js</p><div>test</div>";
+const EYEDROPPER_OPENED = "devtools.toolbar.eyedropper.opened";
 
 add_task(function* () {
   yield addTab(TEST_URI);
   let Telemetry = loadTelemetryAndRecordLogs();
 
   let target = TargetFactory.forTab(gBrowser.selectedTab);
   let toolbox = yield gDevTools.showToolbox(target, "inspector");
   info("inspector opened");
@@ -22,31 +23,32 @@ add_task(function* () {
 });
 
 function* testButton(toolbox, Telemetry) {
   info("Calling the eyedropper button's callback");
   // We call the button callback directly because we don't need to test the UI here, we're
   // only concerned about testing the telemetry probe.
   yield toolbox.getPanel("inspector").showEyeDropper();
 
-  checkResults("_EYEDROPPER_", Telemetry);
+  checkTelemetryResults(Telemetry);
 }
 
-function checkResults(histIdFocus, Telemetry) {
-  let result = Telemetry.prototype.telemetryInfo;
+function checkTelemetryResults(Telemetry) {
+  let data = Telemetry.prototype.telemetryInfo;
+  let results = new Map();
 
-  for (let [histId, value] of Object.entries(result)) {
-    if (histId.startsWith("DEVTOOLS_INSPECTOR_") ||
-        !histId.includes(histIdFocus)) {
-      // Inspector stats are tested in
-      // browser_telemetry_toolboxtabs_{toolname}.js so we skip them here
-      // because we only open the inspector once for this test.
-      continue;
-    }
+  for (let key in data) {
+    if (key.toLowerCase() === key) {
+      let pings = data[key].length;
 
-    if (histId.endsWith("OPENED_COUNT")) {
-      is(value.length, 1, histId + " has one entry");
-
-      let okay = value.every(element => element === true);
-      ok(okay, "All " + histId + " entries are === true");
+      results.set(key, pings);
     }
   }
+
+  is(results.size, 1, "The correct number of scalars were logged");
+
+  let pings = checkPings(EYEDROPPER_OPENED, results);
+  is(pings, 1, `${EYEDROPPER_OPENED} has just 1 ping`);
 }
+
+function checkPings(scalarId, results) {
+  return results.get(scalarId);
+}
--- a/devtools/client/webconsole/new-console-output/components/variables-view-link.js
+++ b/devtools/client/webconsole/new-console-output/components/variables-view-link.js
@@ -16,21 +16,24 @@ const {openVariablesView} = require("dev
 VariablesViewLink.displayName = "VariablesViewLink";
 
 VariablesViewLink.propTypes = {
   object: PropTypes.object.isRequired
 };
 
 function VariablesViewLink(props) {
   const { className, object, children } = props;
-
+  const classes = ["cm-variable"];
+  if (className) {
+    classes.push(className);
+  }
   return (
     dom.a({
       onClick: openVariablesView.bind(null, object),
       // Context menu can use this actor id information to enable additional menu items.
       "data-link-actor-id": object.actor,
-      className: className || "cm-variable",
+      className: classes.join(" "),
       draggable: false,
     }, children)
   );
 }
 
 module.exports = VariablesViewLink;
--- a/dom/base/nsAttrAndChildArray.cpp
+++ b/dom/base/nsAttrAndChildArray.cpp
@@ -875,18 +875,17 @@ nsAttrAndChildArray::AddAttrSlot()
   void** offset = mImpl->mBuffer + slotCount * ATTRSIZE;
 
   if (childCount > 0) {
     memmove(&ATTRS(mImpl)[slotCount + 1], &ATTRS(mImpl)[slotCount],
             childCount * sizeof(nsIContent*));
   }
 
   SetAttrSlotCount(slotCount + 1);
-  offset[0] = nullptr;
-  offset[1] = nullptr;
+  memset(static_cast<void*>(offset), 0, sizeof(InternalAttr));
 
   return true;
 }
 
 inline void
 nsAttrAndChildArray::SetChildAtPos(void** aPos, nsIContent* aChild,
                                    uint32_t aIndex, uint32_t aChildCount)
 {
--- a/dom/base/nsContentUtils.cpp
+++ b/dom/base/nsContentUtils.cpp
@@ -10221,17 +10221,17 @@ nsContentUtils::HtmlObjectContentTypeFor
     // ShouldPlay will handle checking for disabled plugins
     return nsIObjectLoadingContent::TYPE_PLUGIN;
   }
 
   return nsIObjectLoadingContent::TYPE_NULL;
 }
 
 /* static */ already_AddRefed<nsIEventTarget>
-  nsContentUtils::GetEventTargetByLoadInfo(nsILoadInfo* aLoadInfo, TaskCategory aCategory)
+nsContentUtils::GetEventTargetByLoadInfo(nsILoadInfo* aLoadInfo, TaskCategory aCategory)
 {
   if (NS_WARN_IF(!aLoadInfo)) {
     return nullptr;
   }
 
   nsCOMPtr<nsIDOMDocument> domDoc;
   aLoadInfo->GetLoadingDocument(getter_AddRefs(domDoc));
   nsCOMPtr<nsIDocument> doc = do_QueryInterface(domDoc);
@@ -10254,8 +10254,24 @@ nsContentUtils::HtmlObjectContentTypeFor
       return nullptr;
     }
 
     target = window->TabGroup()->EventTargetFor(aCategory);
   }
 
   return target.forget();
 }
+
+/* static */ bool
+nsContentUtils::IsLocalRefURL(const nsString& aString)
+{
+  // Find the first non-"C0 controls + space" character.
+  const char16_t* current = aString.get();
+  for (; *current != '\0'; current++) {
+    if (*current > 0x20) {
+      // if the first non-"C0 controls + space" character is '#', this is a
+      // local-ref URL.
+      return *current == '#';
+    }
+  }
+
+  return false;
+}
\ No newline at end of file
--- a/dom/base/nsContentUtils.h
+++ b/dom/base/nsContentUtils.h
@@ -2858,16 +2858,23 @@ public:
    */
   static uint32_t
   HtmlObjectContentTypeForMIMEType(const nsCString& aMIMEType,
                                    nsIContent* aContent);
 
   static already_AddRefed<nsIEventTarget>
   GetEventTargetByLoadInfo(nsILoadInfo* aLoadInfo, mozilla::TaskCategory aCategory);
 
+  /**
+   * Detect whether a string is a local-url.
+   * https://drafts.csswg.org/css-values/#local-urls
+   */
+  static bool
+  IsLocalRefURL(const nsString& aString);
+
   static bool
   IsCustomElementsEnabled() { return sIsCustomElementsEnabled; }
 
 private:
   static bool InitializeEventTable();
 
   static nsresult EnsureStringBundle(PropertiesFile aFile);
 
--- a/dom/gamepad/Gamepad.cpp
+++ b/dom/gamepad/Gamepad.cpp
@@ -102,16 +102,22 @@ Gamepad::SetAxis(uint32_t aAxis, double 
 void
 Gamepad::SetPose(const GamepadPoseState& aPose)
 {
   mPose->SetPoseState(aPose);
   UpdateTimestamp();
 }
 
 void
+Gamepad::SetHand(GamepadHand aHand)
+{
+  mHand = aHand;
+}
+
+void
 Gamepad::SyncState(Gamepad* aOther)
 {
   const char* kGamepadExtEnabledPref = "dom.gamepad.extensions.enabled";
 
   if (mButtons.Length() != aOther->mButtons.Length() ||
       mAxes.Length() != aOther->mAxes.Length()) {
     return;
   }
--- a/dom/gamepad/Gamepad.h
+++ b/dom/gamepad/Gamepad.h
@@ -51,16 +51,17 @@ public:
   NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(Gamepad)
 
   void SetConnected(bool aConnected);
   void SetButton(uint32_t aButton, bool aPressed,
                  bool aTouched, double aValue);
   void SetAxis(uint32_t aAxis, double aValue);
   void SetIndex(uint32_t aIndex);
   void SetPose(const GamepadPoseState& aPose);
+  void SetHand(GamepadHand aHand);
 
   // Make the state of this gamepad equivalent to other.
   void SyncState(Gamepad* aOther);
 
   // Return a new Gamepad containing the same data as this object,
   // parented to aParent.
   already_AddRefed<Gamepad> Clone(nsISupports* aParent);
 
--- a/dom/gamepad/GamepadManager.cpp
+++ b/dom/gamepad/GamepadManager.cpp
@@ -447,16 +447,59 @@ GamepadManager::NewPoseEvent(uint32_t aI
       if (firstTime) {
         FireConnectionEvent(listeners[i], listenerGamepad, true);
       }
     }
   }
 }
 
 void
+GamepadManager::NewHandChangeEvent(uint32_t aIndex, GamepadServiceType aServiceType,
+                                   GamepadHand aHand)
+{
+  if (mShuttingDown) {
+    return;
+  }
+
+  uint32_t newIndex = GetGamepadIndexWithServiceType(aIndex, aServiceType);
+
+  RefPtr<Gamepad> gamepad = GetGamepad(newIndex);
+  if (!gamepad) {
+    return;
+  }
+  gamepad->SetHand(aHand);
+
+  // Hold on to listeners in a separate array because firing events
+  // can mutate the mListeners array.
+  nsTArray<RefPtr<nsGlobalWindow>> listeners(mListeners);
+  MOZ_ASSERT(!listeners.IsEmpty());
+
+  for (uint32_t i = 0; i < listeners.Length(); i++) {
+
+    MOZ_ASSERT(listeners[i]->IsInnerWindow());
+
+    // Only send events to non-background windows
+    if (!listeners[i]->AsInner()->IsCurrentInnerWindow() ||
+        listeners[i]->GetOuterWindow()->IsBackground()) {
+      continue;
+    }
+
+    bool firstTime = MaybeWindowHasSeenGamepad(listeners[i], newIndex);
+
+    RefPtr<Gamepad> listenerGamepad = listeners[i]->GetGamepad(newIndex);
+    if (listenerGamepad) {
+      listenerGamepad->SetHand(aHand);
+      if (firstTime) {
+        FireConnectionEvent(listeners[i], listenerGamepad, true);
+      }
+    }
+  }
+}
+
+void
 GamepadManager::NewConnectionEvent(uint32_t aIndex, bool aConnected)
 {
   if (mShuttingDown) {
     return;
   }
 
   RefPtr<Gamepad> gamepad = GetGamepad(aIndex);
   if (!gamepad) {
@@ -659,16 +702,21 @@ GamepadManager::Update(const GamepadChan
     NewAxisMoveEvent(a.index(), a.service_type(), a.axis(), a.value());
     return;
   }
   if (aEvent.type() == GamepadChangeEvent::TGamepadPoseInformation) {
     const GamepadPoseInformation& a = aEvent.get_GamepadPoseInformation();
     NewPoseEvent(a.index(), a.service_type(), a.pose_state());
     return;
   }
+   if (aEvent.type() == GamepadChangeEvent::TGamepadHandInformation) {
+    const GamepadHandInformation& a = aEvent.get_GamepadHandInformation();
+    NewHandChangeEvent(a.index(), a.service_type(), a.hand());
+    return;
+  }
 
   MOZ_CRASH("We shouldn't be here!");
 
 }
 
 already_AddRefed<Promise>
 GamepadManager::VibrateHaptic(uint32_t aControllerIdx, uint32_t aHapticIndex,
                               double aIntensity, double aDuration,
--- a/dom/gamepad/GamepadManager.h
+++ b/dom/gamepad/GamepadManager.h
@@ -70,16 +70,21 @@ class GamepadManager final : public nsIO
   void NewAxisMoveEvent(uint32_t aIndex, GamepadServiceType aServiceType,
                         uint32_t aAxis, double aValue);
 
   // Update the state of |aState| for the gamepad at |aIndex| for all
   // windows that are listening and visible.
   void NewPoseEvent(uint32_t aIndex, GamepadServiceType aServiceType,
                     const GamepadPoseState& aState);
 
+  // Update the hand of |aHand| for the gamepad at |aIndex| for all
+  // windows that are listening and visible.
+  void NewHandChangeEvent(uint32_t aIndex, GamepadServiceType aServiceType,
+                          GamepadHand aHand);
+
   // Synchronize the state of |aGamepad| to match the gamepad stored at |aIndex|
   void SyncGamepadState(uint32_t aIndex, Gamepad* aGamepad);
 
   // Returns gamepad object if index exists, null otherwise
   already_AddRefed<Gamepad> GetGamepad(uint32_t aIndex) const;
 
   // Receive GamepadChangeEvent messages from parent process to fire DOM events
   void Update(const GamepadChangeEvent& aGamepadEvent);
--- a/dom/gamepad/ipc/GamepadEventTypes.ipdlh
+++ b/dom/gamepad/ipc/GamepadEventTypes.ipdlh
@@ -44,18 +44,25 @@ struct GamepadButtonInformation {
 };
 
 struct GamepadPoseInformation {
   uint32_t index;
   GamepadServiceType service_type;
   GamepadPoseState pose_state;
 };
 
+struct GamepadHandInformation {
+  uint32_t index;
+  GamepadServiceType service_type;
+  GamepadHand hand;
+};
+
 union GamepadChangeEvent {
   GamepadAdded;
   GamepadRemoved;
   GamepadAxisInformation;
   GamepadButtonInformation;
   GamepadPoseInformation;
+  GamepadHandInformation;
 };
 
 } // namespace dom
 } // namespace mozilla
\ No newline at end of file
--- a/dom/html/HTMLFormSubmission.cpp
+++ b/dom/html/HTMLFormSubmission.cpp
@@ -30,16 +30,17 @@
 #include "nsIMIMEInputStream.h"
 #include "nsIMIMEService.h"
 #include "nsIConsoleService.h"
 #include "nsIScriptError.h"
 #include "nsIStringBundle.h"
 #include "nsCExternalHandlerService.h"
 #include "nsIFileStreams.h"
 #include "nsContentUtils.h"
+#include "mozilla/Telemetry.h"
 
 #include "mozilla/dom/Directory.h"
 #include "mozilla/dom/EncodingUtils.h"
 #include "mozilla/dom/File.h"
 
 namespace mozilla {
 namespace dom {
 
@@ -183,16 +184,17 @@ FSURLEncoded::AddIsindex(const nsAString
 {
   // Encode value
   nsCString convValue;
   nsresult rv = URLEncode(aValue, convValue);
   NS_ENSURE_SUCCESS(rv, rv);
 
   // Append data to string
   if (mQueryString.IsEmpty()) {
+    Telemetry::Accumulate(Telemetry::FORM_ISINDEX_USED, true);
     mQueryString.Assign(convValue);
   } else {
     mQueryString += NS_LITERAL_CSTRING("&isindex=") + convValue;
   }
 
   return NS_OK;
 }
 
--- a/dom/ipc/ContentParent.cpp
+++ b/dom/ipc/ContentParent.cpp
@@ -5019,37 +5019,20 @@ ContentParent::ForceTabPaint(TabParent* 
   ProcessHangMonitor::ForcePaint(mHangMonitorActor, aTabParent, aLayerObserverEpoch);
 }
 
 nsresult
 ContentParent::TransmitPermissionsFor(nsIChannel* aChannel)
 {
   MOZ_ASSERT(aChannel);
 #ifdef MOZ_PERMISSIONS
-  // Check if this channel is going to be used to create a document. If it has
-  // LOAD_DOCUMENT_URI set it is trivially creating a document. If
-  // LOAD_HTML_OBJECT_DATA is set it may or may not be used to create a
-  // document, depending on its MIME type.
-  nsLoadFlags loadFlags;
-  nsresult rv = aChannel->GetLoadFlags(&loadFlags);
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  if (!(loadFlags & nsIChannel::LOAD_DOCUMENT_URI)) {
-    if (loadFlags & nsIRequest::LOAD_HTML_OBJECT_DATA) {
-      nsAutoCString mimeType;
-      aChannel->GetContentType(mimeType);
-      if (nsContentUtils::HtmlObjectContentTypeForMIMEType(mimeType, nullptr) !=
-          nsIObjectLoadingContent::TYPE_DOCUMENT) {
-        // The MIME type would not cause the creation of a document
-        return NS_OK;
-      }
-    } else {
-      // neither flag was set
-      return NS_OK;
-    }
+
+  nsresult rv;
+  if (!aChannel->IsDocument()) {
+    return NS_OK;
   }
 
   // Get the principal for the channel result, so that we can get the permission
   // key for the document which will be created from this response.
   nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager();
   if (NS_WARN_IF(!ssm)) {
     return NS_ERROR_FAILURE;
   }
--- a/dom/ipc/TabParent.cpp
+++ b/dom/ipc/TabParent.cpp
@@ -2939,16 +2939,17 @@ public:
   NS_IMETHOD GetStatus(nsresult*) NO_IMPL
   NS_IMETHOD Cancel(nsresult) NO_IMPL
   NS_IMETHOD Suspend() NO_IMPL
   NS_IMETHOD Resume() NO_IMPL
   NS_IMETHOD GetLoadGroup(nsILoadGroup**) NO_IMPL
   NS_IMETHOD SetLoadGroup(nsILoadGroup*) NO_IMPL
   NS_IMETHOD SetLoadFlags(nsLoadFlags) NO_IMPL
   NS_IMETHOD GetLoadFlags(nsLoadFlags*) NO_IMPL
+  NS_IMETHOD GetIsDocument(bool *) NO_IMPL
   NS_IMETHOD GetOriginalURI(nsIURI**) NO_IMPL
   NS_IMETHOD SetOriginalURI(nsIURI*) NO_IMPL
   NS_IMETHOD GetURI(nsIURI** aUri) override
   {
     nsCOMPtr<nsIURI> copy = mUri;
     copy.forget(aUri);
     return NS_OK;
   }
--- a/dom/jsurl/nsJSProtocolHandler.cpp
+++ b/dom/jsurl/nsJSProtocolHandler.cpp
@@ -437,16 +437,22 @@ nsresult nsJSChannel::Init(nsIURI* aURI,
             writableBag->SetPropertyAsInterface(NS_LITERAL_STRING("baseURI"),
                                                 jsURI->GetBaseURI());
         }
     }
 
     return rv;
 }
 
+NS_IMETHODIMP
+nsJSChannel::GetIsDocument(bool *aIsDocument)
+{
+  return NS_GetIsDocumentChannel(this, aIsDocument);
+}
+
 //
 // nsISupports implementation...
 //
 
 NS_IMPL_ISUPPORTS(nsJSChannel, nsIChannel, nsIRequest, nsIRequestObserver,
                   nsIStreamListener, nsIScriptChannel, nsIPropertyBag,
                   nsIPropertyBag2)
 
--- a/dom/media/ADTSDemuxer.cpp
+++ b/dom/media/ADTSDemuxer.cpp
@@ -747,22 +747,22 @@ ADTSTrackDemuxer::GetNextFrame(const adt
   const uint32_t read = Read(frameWriter->Data(), offset, length);
   if (read != length) {
     ADTSLOG("GetNext() Exit read=%u frame->Size()=%" PRIuSIZE, read, frame->Size());
     return nullptr;
   }
 
   UpdateState(aFrame);
 
-  frame->mTime = Duration(mFrameIndex - 1).ToMicroseconds();
+  frame->mTime = Duration(mFrameIndex - 1);
   frame->mDuration = Duration(1);
   frame->mTimecode = frame->mTime;
   frame->mKeyframe = true;
 
-  MOZ_ASSERT(frame->mTime >= 0);
+  MOZ_ASSERT(!frame->mTime.IsNegative());
   MOZ_ASSERT(frame->mDuration.IsPositive());
 
   ADTSLOGV("GetNext() End mOffset=%" PRIu64 " mNumParsedFrames=%" PRIu64
            " mFrameIndex=%" PRId64 " mTotalFrameLen=%" PRIu64
            " mSamplesPerFrame=%d mSamplesPerSecond=%d mChannels=%d",
            mOffset, mNumParsedFrames, mFrameIndex, mTotalFrameLen,
            mSamplesPerFrame, mSamplesPerSecond, mChannels);
 
--- a/dom/media/MP3Demuxer.cpp
+++ b/dom/media/MP3Demuxer.cpp
@@ -599,22 +599,22 @@ MP3TrackDemuxer::GetNextFrame(const Medi
 
   if (read != aRange.Length()) {
     MP3LOG("GetNext() Exit read=%u frame->Size()=%" PRIuSIZE, read, frame->Size());
     return nullptr;
   }
 
   UpdateState(aRange);
 
-  frame->mTime = Duration(mFrameIndex - 1).ToMicroseconds();
+  frame->mTime = Duration(mFrameIndex - 1);
   frame->mDuration = Duration(1);
   frame->mTimecode = frame->mTime;
   frame->mKeyframe = true;
 
-  MOZ_ASSERT(frame->mTime >= 0);
+  MOZ_ASSERT(!frame->mTime.IsNegative());
   MOZ_ASSERT(frame->mDuration.IsPositive());
 
   if (mNumParsedFrames == 1) {
     // First frame parsed, let's read VBR info if available.
     ByteReader reader(frame->Data(), frame->Size());
     mParser.ParseVBRHeader(&reader);
     mFirstFrameOffset = frame->mOffset;
   }
--- a/dom/media/MediaData.cpp
+++ b/dom/media/MediaData.cpp
@@ -171,17 +171,17 @@ VideoData::VideoData(int64_t aOffset,
                      layers::ImageContainer::FrameID aFrameID)
   : MediaData(VIDEO_DATA, aOffset, aTime, aDuration, 1)
   , mDisplay(aDisplay)
   , mFrameID(aFrameID)
   , mSentToCompositor(false)
 {
   MOZ_ASSERT(!mDuration.IsNegative(), "Frame must have non-negative duration.");
   mKeyframe = aKeyframe;
-  mTimecode = aTimecode;
+  mTimecode = TimeUnit::FromMicroseconds(aTimecode);
 }
 
 VideoData::~VideoData()
 {
 }
 
 void
 VideoData::SetListener(UniquePtr<Listener> aListener)
@@ -232,17 +232,17 @@ VideoData::UpdateDuration(const TimeUnit
 void
 VideoData::UpdateTimestamp(const TimeUnit& aTimestamp)
 {
   MOZ_ASSERT(!aTimestamp.IsNegative());
 
   auto updatedDuration = GetEndTime() - aTimestamp;
   MOZ_ASSERT(!updatedDuration.IsNegative());
 
-  mTime = aTimestamp.ToMicroseconds();
+  mTime = aTimestamp;
   mDuration = updatedDuration;
 }
 
 /* static */
 bool VideoData::SetVideoDataToImage(PlanarYCbCrImage* aVideoImage,
                                     const VideoInfo& aInfo,
                                     const YCbCrBuffer &aBuffer,
                                     const IntRect& aPicture,
--- a/dom/media/MediaData.h
+++ b/dom/media/MediaData.h
@@ -289,54 +289,54 @@ public:
 
   MediaData(Type aType,
             int64_t aOffset,
             int64_t aTimestamp,
             int64_t aDuration,
             uint32_t aFrames)
     : mType(aType)
     , mOffset(aOffset)
-    , mTime(aTimestamp)
-    , mTimecode(aTimestamp)
+    , mTime(media::TimeUnit::FromMicroseconds(aTimestamp))
+    , mTimecode(media::TimeUnit::FromMicroseconds(aTimestamp))
     , mDuration(media::TimeUnit::FromMicroseconds(aDuration))
     , mFrames(aFrames)
     , mKeyframe(false)
   {
   }
 
   // Type of contained data.
   const Type mType;
 
   // Approximate byte offset where this data was demuxed from its media.
   int64_t mOffset;
 
-  // Start time of sample, in microseconds.
-  int64_t mTime;
+  // Start time of sample.
+  media::TimeUnit mTime;
 
   // Codec specific internal time code. For Ogg based codecs this is the
   // granulepos.
-  int64_t mTimecode;
+  media::TimeUnit mTimecode;
 
   // Duration of sample, in microseconds.
   media::TimeUnit mDuration;
 
   // Amount of frames for contained data.
   const uint32_t mFrames;
 
   bool mKeyframe;
 
   media::TimeUnit GetEndTime() const
   {
-    return media::TimeUnit::FromMicroseconds(mTime) + mDuration;
+    return mTime + mDuration;
   }
 
   bool AdjustForStartTime(int64_t aStartTime)
   {
-    mTime = mTime - aStartTime;
-    return mTime >= 0;
+    mTime = mTime - media::TimeUnit::FromMicroseconds(aStartTime);
+    return !mTime.IsNegative();
   }
 
   template <typename ReturnType>
   const ReturnType* As() const
   {
     MOZ_ASSERT(this->mType == ReturnType::sType);
     return static_cast<const ReturnType*>(this);
   }
@@ -347,18 +347,16 @@ public:
     MOZ_ASSERT(this->mType == ReturnType::sType);
     return static_cast<ReturnType*>(this);
   }
 
 protected:
   MediaData(Type aType, uint32_t aFrames)
     : mType(aType)
     , mOffset(0)
-    , mTime(0)
-    , mTimecode(0)
     , mFrames(aFrames)
     , mKeyframe(false)
   {
   }
 
   virtual ~MediaData() { }
 
 };
--- a/dom/media/MediaDecoderStateMachine.cpp
+++ b/dom/media/MediaDecoderStateMachine.cpp
@@ -1232,18 +1232,18 @@ private:
       RefPtr<AudioData> audio = AudioQueue().PeekFront();
       RefPtr<VideoData> video = VideoQueue().PeekFront();
 
       // A situation that both audio and video approaches the end.
       if (!audio && !video) {
         return seekTime;
       }
 
-      const int64_t audioStart = audio ? audio->mTime : INT64_MAX;
-      const int64_t videoStart = video ? video->mTime : INT64_MAX;
+      const int64_t audioStart = audio ? audio->mTime.ToMicroseconds() : INT64_MAX;
+      const int64_t videoStart = video ? video->mTime.ToMicroseconds() : INT64_MAX;
       const int64_t audioGap = std::abs(audioStart - seekTime.ToMicroseconds());
       const int64_t videoGap = std::abs(videoStart - seekTime.ToMicroseconds());
       return TimeUnit::FromMicroseconds(
         audioGap <= videoGap ? audioStart : videoStart);
     }
 
     MOZ_ASSERT(false, "AccurateSeekTask doesn't handle other seek types.");
     return TimeUnit::Zero();
@@ -1309,17 +1309,17 @@ private:
     MOZ_ASSERT(!mDoneVideoSeeking);
     mMaster->RequestVideoData(false, media::TimeUnit());
   }
 
   void AdjustFastSeekIfNeeded(MediaData* aSample)
   {
     if (mSeekJob.mTarget->IsFast()
         && mSeekJob.mTarget->GetTime() > mCurrentTimeBeforeSeek
-        && aSample->mTime < mCurrentTimeBeforeSeek.ToMicroseconds()) {
+        && aSample->mTime < mCurrentTimeBeforeSeek) {
       // We are doing a fastSeek, but we ended up *before* the previous
       // playback position. This is surprising UX, so switch to an accurate
       // seek and decode to the seek target. This is not conformant to the
       // spec, fastSeek should always be fast, but until we get the time to
       // change all Readers to seek to the keyframe after the currentTime
       // in this case, we'll just decode forward. Bug 1026330.
       mSeekJob.mTarget->SetType(SeekTarget::Accurate);
     }
@@ -1330,17 +1330,17 @@ private:
     MOZ_ASSERT(aAudio && mSeekJob.mTarget->IsAccurate());
 
     auto sampleDuration = FramesToTimeUnit(
       aAudio->mFrames, Info().mAudio.mRate);
     if (!sampleDuration.IsValid()) {
       return NS_ERROR_DOM_MEDIA_OVERFLOW_ERR;
     }
 
-    auto audioTime = TimeUnit::FromMicroseconds(aAudio->mTime);
+    auto audioTime = aAudio->mTime;
     if (audioTime + sampleDuration <= mSeekJob.mTarget->GetTime()) {
       // Our seek target lies after the frames in this AudioData. Don't
       // push it onto the audio queue, and keep decoding forwards.
       return NS_OK;
     }
 
     if (audioTime > mSeekJob.mTarget->GetTime()) {
       // The seek target doesn't lie in the audio block just after the last
@@ -1400,38 +1400,38 @@ private:
 
     return NS_OK;
   }
 
   nsresult DropVideoUpToSeekTarget(VideoData* aVideo)
   {
     MOZ_ASSERT(aVideo);
     SLOG("DropVideoUpToSeekTarget() frame [%" PRId64 ", %" PRId64 "]",
-         aVideo->mTime, aVideo->GetEndTime().ToMicroseconds());
+         aVideo->mTime.ToMicroseconds(), aVideo->GetEndTime().ToMicroseconds());
     const auto target = mSeekJob.mTarget->GetTime();
 
     // If the frame end time is less than the seek target, we won't want
     // to display this frame after the seek, so discard it.
     if (target >= aVideo->GetEndTime()) {
       SLOG("DropVideoUpToSeekTarget() pop video frame [%" PRId64 ", %" PRId64 "] target=%" PRId64,
-           aVideo->mTime, aVideo->GetEndTime().ToMicroseconds(),
+           aVideo->mTime.ToMicroseconds(), aVideo->GetEndTime().ToMicroseconds(),
            target.ToMicroseconds());
       mFirstVideoFrameAfterSeek = aVideo;
     } else {
-      if (target.ToMicroseconds() >= aVideo->mTime &&
+      if (target >= aVideo->mTime &&
           aVideo->GetEndTime() >= target) {
         // The seek target lies inside this frame's time slice. Adjust the
         // frame's start time to match the seek target.
         aVideo->UpdateTimestamp(target);
       }
       mFirstVideoFrameAfterSeek = nullptr;
 
       SLOG("DropVideoUpToSeekTarget() found video frame [%" PRId64 ", %" PRId64 "] "
            "containing target=%" PRId64,
-           aVideo->mTime, aVideo->GetEndTime().ToMicroseconds(),
+           aVideo->mTime.ToMicroseconds(), aVideo->GetEndTime().ToMicroseconds(),
            target.ToMicroseconds());
 
       MOZ_ASSERT(VideoQueue().GetSize() == 0,
                  "Should be the 1st sample after seeking");
       mMaster->PushVideo(aVideo);
       mDoneVideoSeeking = true;
     }
 
@@ -1470,17 +1470,17 @@ private:
  * aCompare A function object with the signature bool(int64_t) which returns
  *          true for samples that should be removed.
  */
 template <typename Type, typename Function>
 static void
 DiscardFrames(MediaQueue<Type>& aQueue, const Function& aCompare)
 {
   while(aQueue.GetSize() > 0) {
-    if (aCompare(aQueue.PeekFront()->mTime)) {
+    if (aCompare(aQueue.PeekFront()->mTime.ToMicroseconds())) {
       RefPtr<Type> releaseMe = aQueue.PopFront();
       continue;
     }
     break;
   }
 }
 
 class MediaDecoderStateMachine::NextFrameSeekingState
@@ -1570,17 +1570,17 @@ private:
   }
 
   void HandleVideoDecoded(VideoData* aVideo, TimeStamp aDecodeStart) override
   {
     MOZ_ASSERT(aVideo);
     MOZ_ASSERT(!mSeekJob.mPromise.IsEmpty(), "Seek shouldn't be finished");
     MOZ_ASSERT(NeedMoreVideo());
 
-    if (aVideo->mTime > mCurrentTime.ToMicroseconds()) {
+    if (aVideo->mTime > mCurrentTime) {
       mMaster->PushVideo(aVideo);
       FinishSeek();
     } else {
       RequestVideoData();
     }
   }
 
   void HandleWaitingForAudio() override
@@ -1662,17 +1662,17 @@ private:
 
   // Update the seek target's time before resolving this seek task, the updated
   // time will be used in the MDSM::SeekCompleted() to update the MDSM's
   // position.
   void UpdateSeekTargetTime()
   {
     RefPtr<VideoData> data = VideoQueue().PeekFront();
     if (data) {
-      mSeekJob.mTarget->SetTime(TimeUnit::FromMicroseconds(data->mTime));
+      mSeekJob.mTarget->SetTime(data->mTime);
     } else {
       MOZ_ASSERT(VideoQueue().AtEndOfStream());
       mSeekJob.mTarget->SetTime(mDuration);
     }
   }
 
   void FinishSeek()
   {
@@ -3172,17 +3172,18 @@ MediaDecoderStateMachine::RequestAudioDa
   mReader->RequestAudioData()->Then(
     OwnerThread(), __func__,
     [this, self] (AudioData* aAudio) {
       MOZ_ASSERT(aAudio);
       mAudioDataRequest.Complete();
       // audio->GetEndTime() is not always mono-increasing in chained ogg.
       mDecodedAudioEndTime = std::max(
         aAudio->GetEndTime(), mDecodedAudioEndTime);
-      LOGV("OnAudioDecoded [%" PRId64 ",%" PRId64 "]", aAudio->mTime,
+      LOGV("OnAudioDecoded [%" PRId64 ",%" PRId64 "]",
+           aAudio->mTime.ToMicroseconds(),
            aAudio->GetEndTime().ToMicroseconds());
       mStateObj->HandleAudioDecoded(aAudio);
     },
     [this, self] (const MediaResult& aError) {
       LOGV("OnAudioNotDecoded aError=%" PRIu32, static_cast<uint32_t>(aError.Code()));
       mAudioDataRequest.Complete();
       switch (aError.Code()) {
         case NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA:
@@ -3218,17 +3219,18 @@ MediaDecoderStateMachine::RequestVideoDa
   mReader->RequestVideoData(aSkipToNextKeyframe, aCurrentTime)->Then(
     OwnerThread(), __func__,
     [this, self, videoDecodeStartTime] (VideoData* aVideo) {
       MOZ_ASSERT(aVideo);
       mVideoDataRequest.Complete();
       // Handle abnormal or negative timestamps.
       mDecodedVideoEndTime = std::max(
         mDecodedVideoEndTime, aVideo->GetEndTime());
-      LOGV("OnVideoDecoded [%" PRId64 ",%" PRId64 "]", aVideo->mTime,
+      LOGV("OnVideoDecoded [%" PRId64 ",%" PRId64 "]",
+           aVideo->mTime.ToMicroseconds(),
            aVideo->GetEndTime().ToMicroseconds());
       mStateObj->HandleVideoDecoded(aVideo, videoDecodeStartTime);
     },
     [this, self] (const MediaResult& aError) {
       LOGV("OnVideoNotDecoded aError=%" PRIu32 , static_cast<uint32_t>(aError.Code()));
       mVideoDataRequest.Complete();
       switch (aError.Code()) {
         case NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA:
--- a/dom/media/MediaFormatReader.cpp
+++ b/dom/media/MediaFormatReader.cpp
@@ -1719,17 +1719,17 @@ MediaFormatReader::OnAudioDemuxCompleted
 void
 MediaFormatReader::NotifyNewOutput(
   TrackType aTrack, const MediaDataDecoder::DecodedData& aResults)
 {
   MOZ_ASSERT(OnTaskQueue());
   auto& decoder = GetDecoderData(aTrack);
   for (auto& sample : aResults) {
     LOGV("Received new %s sample time:%" PRId64 " duration:%" PRId64,
-         TrackTypeToStr(aTrack), sample->mTime,
+         TrackTypeToStr(aTrack), sample->mTime.ToMicroseconds(),
          sample->mDuration.ToMicroseconds());
     decoder.mOutput.AppendElement(sample);
     decoder.mNumSamplesOutput++;
     decoder.mNumOfConsecutiveError = 0;
   }
   LOG("Done processing new %s samples", TrackTypeToStr(aTrack));
   ScheduleUpdate(aTrack);
 }
@@ -2006,29 +2006,29 @@ MediaFormatReader::HandleDemuxedSamples(
         }
       }
 
       decoder.mInfo = info;
 
       if (sample->mKeyframe) {
         ScheduleUpdate(aTrack);
       } else {
-        auto time = TimeInterval(
-          TimeUnit::FromMicroseconds(sample->mTime), sample->GetEndTime());
+        auto time = TimeInterval(sample->mTime, sample->GetEndTime());
         InternalSeekTarget seekTarget =
           decoder.mTimeThreshold.refOr(InternalSeekTarget(time, false));
         LOG("Stream change occurred on a non-keyframe. Seeking to:%" PRId64,
-            sample->mTime);
+            sample->mTime.ToMicroseconds());
         InternalSeek(aTrack, seekTarget);
       }
       return;
     }
 
     LOGV("Input:%" PRId64 " (dts:%" PRId64 " kf:%d)",
-         sample->mTime, sample->mTimecode, sample->mKeyframe);
+         sample->mTime.ToMicroseconds(), sample->mTimecode.ToMicroseconds(),
+         sample->mKeyframe);
     decoder.mNumSamplesInput++;
     decoder.mSizeOfQueue++;
     if (aTrack == TrackInfo::kVideoTrack) {
       aA.mStats.mParsedFrames++;
     }
 
     DecodeDemuxedSamples(aTrack, sample);
 
@@ -2181,70 +2181,70 @@ MediaFormatReader::Update(TrackType aTra
   // Record number of frames decoded and parsed. Automatically update the
   // stats counters using the AutoNotifyDecoded stack-based class.
   AbstractMediaDecoder::AutoNotifyDecoded a(mDecoder);
 
   // Drop any frames found prior our internal seek target.
   while (decoder.mTimeThreshold && decoder.mOutput.Length()) {
     RefPtr<MediaData>& output = decoder.mOutput[0];
     InternalSeekTarget target = decoder.mTimeThreshold.ref();
-    media::TimeUnit time = media::TimeUnit::FromMicroseconds(output->mTime);
+    media::TimeUnit time = output->mTime;
     if (time >= target.Time()) {
       // We have reached our internal seek target.
       decoder.mTimeThreshold.reset();
       // We might have dropped some keyframes.
       mPreviousDecodedKeyframeTime_us = sNoPreviousDecodedKeyframe;
     }
     if (time < target.Time() || (target.mDropTarget && target.Contains(time))) {
       LOGV("Internal Seeking: Dropping %s frame time:%f wanted:%f (kf:%d)",
            TrackTypeToStr(aTrack),
-           media::TimeUnit::FromMicroseconds(output->mTime).ToSeconds(),
+           output->mTime.ToSeconds(),
            target.Time().ToSeconds(),
            output->mKeyframe);
       decoder.mOutput.RemoveElementAt(0);
       decoder.mSizeOfQueue -= 1;
     }
   }
 
   while (decoder.mOutput.Length()
          && decoder.mOutput[0]->mType == MediaData::NULL_DATA) {
-    LOGV("Dropping null data. Time: %" PRId64, decoder.mOutput[0]->mTime);
+    LOGV("Dropping null data. Time: %" PRId64,
+         decoder.mOutput[0]->mTime.ToMicroseconds());
     decoder.mOutput.RemoveElementAt(0);
     decoder.mSizeOfQueue -= 1;
   }
 
   if (decoder.HasPromise()) {
     needOutput = true;
     if (decoder.mOutput.Length()) {
       RefPtr<MediaData> output = decoder.mOutput[0];
       decoder.mOutput.RemoveElementAt(0);
       decoder.mSizeOfQueue -= 1;
       decoder.mLastSampleTime =
-        Some(TimeInterval(TimeUnit::FromMicroseconds(output->mTime),
-                          output->GetEndTime()));
+        Some(TimeInterval(output->mTime, output->GetEndTime()));
       decoder.mNumSamplesOutputTotal++;
       ReturnOutput(output, aTrack);
       // We have a decoded sample ready to be returned.
       if (aTrack == TrackType::kVideoTrack) {
         uint64_t delta =
           decoder.mNumSamplesOutputTotal - mLastReportedNumDecodedFrames;
         a.mStats.mDecodedFrames = static_cast<uint32_t>(delta);
         mLastReportedNumDecodedFrames = decoder.mNumSamplesOutputTotal;
         if (output->mKeyframe) {
-          if (mPreviousDecodedKeyframeTime_us < output->mTime) {
+          if (mPreviousDecodedKeyframeTime_us < output->mTime.ToMicroseconds()) {
             // There is a previous keyframe -> Record inter-keyframe stats.
             uint64_t segment_us =
-              output->mTime - mPreviousDecodedKeyframeTime_us;
+              output->mTime.ToMicroseconds() - mPreviousDecodedKeyframeTime_us;
             a.mStats.mInterKeyframeSum_us += segment_us;
             a.mStats.mInterKeyframeCount += 1;
             if (a.mStats.mInterKeyFrameMax_us < segment_us) {
               a.mStats.mInterKeyFrameMax_us = segment_us;
             }
           }
-          mPreviousDecodedKeyframeTime_us = output->mTime;
+          mPreviousDecodedKeyframeTime_us = output->mTime.ToMicroseconds();
         }
         nsCString error;
         mVideo.mIsHardwareAccelerated =
           mVideo.mDecoder && mVideo.mDecoder->IsHardwareAccelerated(error);
       }
     } else if (decoder.HasFatalError()) {
       LOG("Rejecting %s promise: DECODE_ERROR", TrackTypeToStr(aTrack));
       decoder.RejectPromise(decoder.mError.ref(), __func__);
@@ -2374,17 +2374,17 @@ MediaFormatReader::Update(TrackType aTra
 }
 
 void
 MediaFormatReader::ReturnOutput(MediaData* aData, TrackType aTrack)
 {
   MOZ_ASSERT(GetDecoderData(aTrack).HasPromise());
   MOZ_DIAGNOSTIC_ASSERT(aData->mType != MediaData::NULL_DATA);
   LOG("Resolved data promise for %s [%" PRId64 ", %" PRId64 "]", TrackTypeToStr(aTrack),
-      aData->mTime, aData->GetEndTime().ToMicroseconds());
+      aData->mTime.ToMicroseconds(), aData->GetEndTime().ToMicroseconds());
 
   if (aTrack == TrackInfo::kAudioTrack) {
     AudioData* audioData = static_cast<AudioData*>(aData);
 
     if (audioData->mChannels != mInfo.mAudio.mChannels
         || audioData->mRate != mInfo.mAudio.mRate) {
       LOG("change of audio format (rate:%d->%d). "
           "This is an unsupported configuration",
@@ -2501,18 +2501,17 @@ MediaFormatReader::Reset(TrackType aTrac
 
 void
 MediaFormatReader::DropDecodedSamples(TrackType aTrack)
 {
   MOZ_ASSERT(OnTaskQueue());
   auto& decoder = GetDecoderData(aTrack);
   size_t lengthDecodedQueue = decoder.mOutput.Length();
   if (lengthDecodedQueue && decoder.mTimeThreshold.isSome()) {
-    TimeUnit time =
-      TimeUnit::FromMicroseconds(decoder.mOutput.LastElement()->mTime);
+    TimeUnit time = decoder.mOutput.LastElement()->mTime;
     if (time >= decoder.mTimeThreshold.ref().Time()) {
       // We would have reached our internal seek target.
       decoder.mTimeThreshold.reset();
     }
   }
   decoder.mOutput.Clear();
   decoder.mSizeOfQueue -= lengthDecodedQueue;
   if (aTrack == TrackInfo::kVideoTrack && mDecoder) {
@@ -3097,18 +3096,17 @@ MediaFormatReader::OnFirstDemuxCompleted
   MOZ_ASSERT(OnTaskQueue());
 
   if (mShutdown) {
     return;
   }
 
   auto& decoder = GetDecoderData(aType);
   MOZ_ASSERT(decoder.mFirstDemuxedSampleTime.isNothing());
-  decoder.mFirstDemuxedSampleTime.emplace(
-    TimeUnit::FromMicroseconds(aSamples->mSamples[0]->mTime));
+  decoder.mFirstDemuxedSampleTime.emplace(aSamples->mSamples[0]->mTime);
   MaybeResolveMetadataPromise();
 }
 
 void
 MediaFormatReader::OnFirstDemuxFailed(TrackInfo::TrackType aType,
                                       const MediaResult& aError)
 {
   MOZ_ASSERT(OnTaskQueue());
--- a/dom/media/MediaPrefs.h
+++ b/dom/media/MediaPrefs.h
@@ -170,17 +170,17 @@ private:
   // Flac
   DECL_MEDIA_PREF("media.ogg.flac.enabled",                   FlacInOgg, bool, false);
   DECL_MEDIA_PREF("media.flac.enabled",                       FlacEnabled, bool, true);
 
 #if !defined(RELEASE_OR_BETA)
   DECL_MEDIA_PREF("media.rust.test_mode",                     RustTestMode, bool, false);
 #endif
 
-#if defined(OS_LINUX)
+#if defined(MOZ_WIDGET_GTK)
   DECL_MEDIA_PREF("media.rust.mp4parser",                     EnableRustMP4Parser, bool, true);
 #else
   DECL_MEDIA_PREF("media.rust.mp4parser",                     EnableRustMP4Parser, bool, false);
 #endif
 
   // Error/warning handling, Decoder Doctor
   DECL_MEDIA_PREF("media.playback.warnings-as-errors",        MediaWarningsAsErrors, bool, false);
   DECL_MEDIA_PREF("media.playback.warnings-as-errors.stagefright-vs-rust",
--- a/dom/media/MediaQueue.h
+++ b/dom/media/MediaQueue.h
@@ -42,17 +42,17 @@ public:
     return nsDeque::GetSize();
   }
 
   inline void Push(T* aItem) {
     ReentrantMonitorAutoEnter mon(mReentrantMonitor);
     MOZ_ASSERT(!mEndOfStream);
     MOZ_ASSERT(aItem);
     NS_ADDREF(aItem);
-    MOZ_ASSERT(aItem->GetEndTime().ToMicroseconds() >= aItem->mTime);
+    MOZ_ASSERT(aItem->GetEndTime() >= aItem->mTime);
     nsDeque::Push(aItem);
     mPushEvent.Notify(RefPtr<T>(aItem));
   }
 
   inline already_AddRefed<T> PopFront() {
     ReentrantMonitorAutoEnter mon(mReentrantMonitor);
     RefPtr<T> rv = dont_AddRef(static_cast<T*>(nsDeque::PopFront()));
     if (rv) {
@@ -99,17 +99,17 @@ public:
   // Returns the approximate number of microseconds of items in the queue.
   int64_t Duration() {
     ReentrantMonitorAutoEnter mon(mReentrantMonitor);
     if (GetSize() == 0) {
       return 0;
     }
     T* last = static_cast<T*>(nsDeque::Peek());
     T* first = static_cast<T*>(nsDeque::PeekFront());
-    return last->GetEndTime().ToMicroseconds() - first->mTime;
+    return (last->GetEndTime() - first->mTime).ToMicroseconds();
   }
 
   void LockedForEach(nsDequeFunctor& aFunctor) const {
     ReentrantMonitorAutoEnter mon(mReentrantMonitor);
     ForEach(aFunctor);
   }
 
   // Extracts elements from the queue into aResult, in order.
--- a/dom/media/android/AndroidMediaReader.cpp
+++ b/dom/media/android/AndroidMediaReader.cpp
@@ -136,17 +136,18 @@ bool AndroidMediaReader::DecodeVideoFram
     MPAPI::VideoFrame frame;
     if (!mPlugin->ReadVideo(mPlugin, &frame, mVideoSeekTimeUs, &bufferCallback)) {
       // We reached the end of the video stream. If we have a buffered
       // video frame, push it the video queue using the total duration
       // of the video as the end time.
       if (mLastVideoFrame) {
         int64_t durationUs;
         mPlugin->GetDuration(mPlugin, &durationUs);
-        durationUs = std::max<int64_t>(durationUs - mLastVideoFrame->mTime, 0);
+        durationUs = std::max<int64_t>(
+          durationUs - mLastVideoFrame->mTime.ToMicroseconds(), 0);
         mLastVideoFrame->UpdateDuration(TimeUnit::FromMicroseconds(durationUs));
         mVideoQueue.Push(mLastVideoFrame);
         mLastVideoFrame = nullptr;
       }
       return false;
     }
     mVideoSeekTimeUs = -1;
 
@@ -242,18 +243,18 @@ bool AndroidMediaReader::DecodeVideoFram
     if (!mLastVideoFrame) {
       mLastVideoFrame = v;
       continue;
     }
 
     // Calculate the duration as the timestamp of the current frame minus the
     // timestamp of the previous frame. We can then return the previously
     // decoded frame, and it will have a valid timestamp.
-    int64_t duration = v->mTime - mLastVideoFrame->mTime;
-    mLastVideoFrame->UpdateDuration(TimeUnit::FromMicroseconds(duration));
+    auto duration = v->mTime - mLastVideoFrame->mTime;
+    mLastVideoFrame->UpdateDuration(duration);
 
     // We have the start time of the next frame, so we can push the previous
     // frame into the queue, except if the end time is below the threshold,
     // in which case it wouldn't be displayed anyway.
     if (mLastVideoFrame->GetEndTime() < aTimeThreshold) {
       mLastVideoFrame = nullptr;
       continue;
     }
@@ -315,17 +316,17 @@ AndroidMediaReader::Seek(const SeekTarge
     // stream to the preceeding keyframe first, get the stream time, and then
     // seek the audio stream to match the video stream's time. Otherwise, the
     // audio and video streams won't be in sync after the seek.
     mVideoSeekTimeUs = aTarget.GetTime().ToMicroseconds();
 
     RefPtr<AndroidMediaReader> self = this;
     DecodeToFirstVideoData()->Then(OwnerThread(), __func__, [self] (MediaData* v) {
       self->mSeekRequest.Complete();
-      self->mAudioSeekTimeUs = v->mTime;
+      self->mAudioSeekTimeUs = v->mTime.ToMicroseconds();
       self->mSeekPromise.Resolve(media::TimeUnit::FromMicroseconds(self->mAudioSeekTimeUs), __func__);
     }, [self, aTarget] () {
       self->mSeekRequest.Complete();
       self->mAudioSeekTimeUs = aTarget.GetTime().ToMicroseconds();
       self->mSeekPromise.Resolve(aTarget.GetTime(), __func__);
     })->Track(mSeekRequest);
   } else {
     mAudioSeekTimeUs = mVideoSeekTimeUs = aTarget.GetTime().ToMicroseconds();
--- a/dom/media/flac/FlacDemuxer.cpp
+++ b/dom/media/flac/FlacDemuxer.cpp
@@ -975,23 +975,23 @@ FlacTrackDemuxer::GetNextFrame(const fla
   }
 
   const uint32_t read = Read(frameWriter->Data(), offset, size);
   if (read != size) {
     LOG("GetNextFrame() Exit read=%u frame->Size=%" PRIuSIZE, read, frame->Size());
     return nullptr;
   }
 
-  frame->mTime = aFrame.Time().ToMicroseconds();
+  frame->mTime = aFrame.Time();
   frame->mDuration = aFrame.Duration();
   frame->mTimecode = frame->mTime;
   frame->mOffset = aFrame.Offset();
   frame->mKeyframe = true;
 
-  MOZ_ASSERT(frame->mTime >= 0);
+  MOZ_ASSERT(!frame->mTime.IsNegative());
   MOZ_ASSERT(!frame->mDuration.IsNegative());
 
   return frame.forget();
 }
 
 int32_t
 FlacTrackDemuxer::Read(uint8_t* aBuffer, int64_t aOffset, int32_t aSize)
 {
--- a/dom/media/fmp4/MP4Demuxer.cpp
+++ b/dom/media/fmp4/MP4Demuxer.cpp
@@ -406,20 +406,20 @@ MP4TrackDemuxer::EnsureUpToDateIndex()
   }
   mIndex->UpdateMoofIndex(byteRanges);
   mNeedReIndex = false;
 }
 
 RefPtr<MP4TrackDemuxer::SeekPromise>
 MP4TrackDemuxer::Seek(const media::TimeUnit& aTime)
 {
-  int64_t seekTime = aTime.ToMicroseconds();
+  auto seekTime = aTime;
   mQueuedSample = nullptr;
 
-  mIterator->Seek(seekTime);
+  mIterator->Seek(seekTime.ToMicroseconds());
 
   // Check what time we actually seeked to.
   RefPtr<MediaRawData> sample;
   do {
     sample = GetNextSample();
     if (!sample) {
       return SeekPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_END_OF_STREAM,
                                           __func__);
@@ -431,18 +431,17 @@ MP4TrackDemuxer::Seek(const media::TimeU
     if (sample->mKeyframe) {
       mQueuedSample = sample;
       seekTime = mQueuedSample->mTime;
     }
   } while (!mQueuedSample);
 
   SetNextKeyFrameTime();
 
-  return SeekPromise::CreateAndResolve(
-    media::TimeUnit::FromMicroseconds(seekTime), __func__);
+  return SeekPromise::CreateAndResolve(seekTime, __func__);
 }
 
 already_AddRefed<MediaRawData>
 MP4TrackDemuxer::GetNextSample()
 {
   RefPtr<MediaRawData> sample = mIterator->GetNext();
   if (!sample) {
     return nullptr;
@@ -456,30 +455,32 @@ MP4TrackDemuxer::GetNextSample()
         case mp4_demuxer::H264::FrameType::I_FRAME: MOZ_FALLTHROUGH;
         case mp4_demuxer::H264::FrameType::OTHER:
         {
           bool keyframe = type == mp4_demuxer::H264::FrameType::I_FRAME;
           if (sample->mKeyframe != keyframe) {
             NS_WARNING(nsPrintfCString("Frame incorrectly marked as %skeyframe "
                                        "@ pts:%" PRId64 " dur:%" PRId64
                                        " dts:%" PRId64,
-                                       keyframe ? "" : "non-", sample->mTime,
+                                       keyframe ? "" : "non-",
+                                       sample->mTime.ToMicroseconds(),
                                        sample->mDuration.ToMicroseconds(),
-                                       sample->mTimecode)
+                                       sample->mTimecode.ToMicroseconds())
                          .get());
             sample->mKeyframe = keyframe;
           }
           break;
         }
         case mp4_demuxer::H264::FrameType::INVALID:
           NS_WARNING(
             nsPrintfCString("Invalid H264 frame @ pts:%" PRId64 " dur:%" PRId64
                             " dts:%" PRId64,
-                            sample->mTime, sample->mDuration.ToMicroseconds(),
-                            sample->mTimecode)
+                            sample->mTime.ToMicroseconds(),
+                            sample->mDuration.ToMicroseconds(),
+                            sample->mTimecode.ToMicroseconds())
               .get());
           // We could reject the sample now, however demuxer errors are fatal.
           // So we keep the invalid frame, relying on the H264 decoder to
           // handle the error later.
           // TODO: make demuxer errors non-fatal.
           break;
       }
     }
@@ -535,17 +536,17 @@ MP4TrackDemuxer::GetSamples(int32_t aNum
       RefPtr<MediaByteBuffer> extradata =
         mp4_demuxer::AnnexB::ExtractExtraData(sample);
       mNeedSPSForTelemetry = AccumulateSPSTelemetry(extradata);
     }
   }
 
   if (mNextKeyframeTime.isNothing()
       || samples->mSamples.LastElement()->mTime
-      >= mNextKeyframeTime.value().ToMicroseconds()) {
+      >= mNextKeyframeTime.value()) {
     SetNextKeyFrameTime();
   }
   return SamplesPromise::CreateAndResolve(samples, __func__);
 }
 
 void
 MP4TrackDemuxer::SetNextKeyFrameTime()
 {
@@ -585,17 +586,17 @@ MP4TrackDemuxer::SkipToNextRandomAccessP
 {
   mQueuedSample = nullptr;
   // Loop until we reach the next keyframe after the threshold.
   uint32_t parsed = 0;
   bool found = false;
   RefPtr<MediaRawData> sample;
   while (!found && (sample = GetNextSample())) {
     parsed++;
-    if (sample->mKeyframe && sample->mTime >= aTimeThreshold.ToMicroseconds()) {
+    if (sample->mKeyframe && sample->mTime >= aTimeThreshold) {
       found = true;
       mQueuedSample = sample;
     }
   }
   SetNextKeyFrameTime();
   if (found) {
     return SkipAccessPointPromise::CreateAndResolve(parsed, __func__);
   }
--- a/dom/media/gmp/ChromiumCDMParent.cpp
+++ b/dom/media/gmp/ChromiumCDMParent.cpp
@@ -185,17 +185,17 @@ ChromiumCDMParent::InitCDMInputBuffer(gm
   if (!AllocShmem(aSample->Size(), Shmem::SharedMemory::TYPE_BASIC, &shmem)) {
     return false;
   }
   memcpy(shmem.get<uint8_t>(), aSample->Data(), aSample->Size());
 
   aBuffer = gmp::CDMInputBuffer(shmem,
                                 crypto.mKeyId,
                                 crypto.mIV,
-                                aSample->mTime,
+                                aSample->mTime.ToMicroseconds(),
                                 aSample->mDuration.ToMicroseconds(),
                                 crypto.mPlainSizes,
                                 crypto.mEncryptedSizes,
                                 crypto.mValid);
   return true;
 }
 
 bool
@@ -830,17 +830,17 @@ ChromiumCDMParent::DecryptAndDecodeFrame
   if (mIsShutdown) {
     return MediaDataDecoder::DecodePromise::CreateAndReject(
       MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
                   RESULT_DETAIL("ChromiumCDMParent is shutdown")),
       __func__);
   }
 
   GMP_LOG("ChromiumCDMParent::DecryptAndDecodeFrame t=%" PRId64,
-          aSample->mTime);
+          aSample->mTime.ToMicroseconds());
 
   CDMInputBuffer buffer;
 
   if (!InitCDMInputBuffer(buffer, aSample)) {
     return MediaDataDecoder::DecodePromise::CreateAndReject(
       MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, "Failed to init CDM buffer."),
       __func__);
   }
--- a/dom/media/gtest/TestMP4Demuxer.cpp
+++ b/dom/media/gtest/TestMP4Demuxer.cpp
@@ -58,36 +58,36 @@ public:
   RefPtr<GenericPromise>
   CheckTrackKeyFrame(MediaTrackDemuxer* aTrackDemuxer)
   {
     MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn());
 
     RefPtr<MediaTrackDemuxer> track = aTrackDemuxer;
     RefPtr<MP4DemuxerBinding> binding = this;
 
-    int64_t time = -1;
+    auto time = media::TimeUnit::Invalid();
     while (mIndex < mSamples.Length()) {
       uint32_t i = mIndex++;
       if (mSamples[i]->mKeyframe) {
         time = mSamples[i]->mTime;
         break;
       }
     }
 
     RefPtr<GenericPromise> p = mCheckTrackKeyFramePromise.Ensure(__func__);
 
-    if (time == -1) {
+    if (!time.IsValid()) {
       mCheckTrackKeyFramePromise.Resolve(true, __func__);
       return p;
     }
 
 
     DispatchTask(
       [track, time, binding] () {
-        track->Seek(media::TimeUnit::FromMicroseconds(time))->Then(binding->mTaskQueue, __func__,
+        track->Seek(time)->Then(binding->mTaskQueue, __func__,
           [track, time, binding] () {
             track->GetSamples()->Then(binding->mTaskQueue, __func__,
               [track, time, binding] (RefPtr<MediaTrackDemuxer::SamplesHolder> aSamples) {
                 EXPECT_EQ(time, aSamples->mSamples[0]->mTime);
                 binding->CheckTrackKeyFrame(track);
               },
               DO_FAIL
             );
@@ -120,17 +120,17 @@ public:
             }
           },
           [binding] (const MediaResult& aError) {
             if (aError == NS_ERROR_DOM_MEDIA_END_OF_STREAM) {
               EXPECT_TRUE(binding->mSamples.Length() > 1);
               for (uint32_t i = 0; i < (binding->mSamples.Length() - 1); i++) {
                 EXPECT_LT(binding->mSamples[i]->mTimecode, binding->mSamples[i + 1]->mTimecode);
                 if (binding->mSamples[i]->mKeyframe) {
-                  binding->mKeyFrameTimecodes.AppendElement(binding->mSamples[i]->mTimecode);
+                  binding->mKeyFrameTimecodes.AppendElement(binding->mSamples[i]->mTimecode.ToMicroseconds());
                 }
               }
               binding->mCheckTrackSamples.Resolve(true, __func__);
             } else {
               EXPECT_TRUE(false);
               binding->mCheckTrackSamples.Reject(aError, __func__);
             }
           }
--- a/dom/media/ipc/VideoDecoderChild.cpp
+++ b/dom/media/ipc/VideoDecoderChild.cpp
@@ -225,18 +225,18 @@ VideoDecoderChild::Decode(MediaRawData* 
   if (!AllocShmem(aSample->Size(), Shmem::SharedMemory::TYPE_BASIC, &buffer)) {
     return MediaDataDecoder::DecodePromise::CreateAndReject(
       NS_ERROR_DOM_MEDIA_DECODE_ERR, __func__);
   }
 
   memcpy(buffer.get<uint8_t>(), aSample->Data(), aSample->Size());
 
   MediaRawDataIPDL sample(MediaDataIPDL(aSample->mOffset,
-                                        aSample->mTime,
-                                        aSample->mTimecode,
+                                        aSample->mTime.ToMicroseconds(),
+                                        aSample->mTimecode.ToMicroseconds(),
                                         aSample->mDuration.ToMicroseconds(),
                                         aSample->mFrames,
                                         aSample->mKeyframe),
                           buffer);
   SendInput(sample);
   return mDecodePromise.Ensure(__func__);
 }
 
--- a/dom/media/ipc/VideoDecoderParent.cpp
+++ b/dom/media/ipc/VideoDecoderParent.cpp
@@ -132,18 +132,18 @@ VideoDecoderParent::RecvInput(const Medi
   RefPtr<MediaRawData> data = new MediaRawData(aData.buffer().get<uint8_t>(),
                                                aData.buffer().Size<uint8_t>());
   if (aData.buffer().Size<uint8_t>() && !data->Data()) {
     // OOM
     Error(NS_ERROR_OUT_OF_MEMORY);
     return IPC_OK();
   }
   data->mOffset = aData.base().offset();
-  data->mTime = aData.base().time();
-  data->mTimecode = aData.base().timecode();
+  data->mTime = media::TimeUnit::FromMicroseconds(aData.base().time());
+  data->mTimecode = media::TimeUnit::FromMicroseconds(aData.base().timecode());
   data->mDuration = media::TimeUnit::FromMicroseconds(aData.base().duration());
   data->mKeyframe = aData.base().keyframe();
 
   DeallocShmem(aData.buffer());
 
   RefPtr<VideoDecoderParent> self = this;
   mDecoder->Decode(data)->Then(
     mManagerTaskQueue, __func__,
@@ -186,17 +186,18 @@ VideoDecoderParent::ProcessDecodedData(
     }
 
     if (texture && !texture->IsAddedToCompositableClient()) {
       texture->InitIPDLActor(mKnowsCompositor);
       texture->SetAddedToCompositableClient();
     }
 
     VideoDataIPDL output(
-      MediaDataIPDL(data->mOffset, data->mTime, data->mTimecode,
+      MediaDataIPDL(data->mOffset, data->mTime.ToMicroseconds(),
+                    data->mTimecode.ToMicroseconds(),
                     data->mDuration.ToMicroseconds(),
                     data->mFrames, data->mKeyframe),
       video->mDisplay,
       texture ? texture->GetSize() : IntSize(),
       texture ? mParent->StoreImage(video->mImage, texture)
               : SurfaceDescriptorGPUVideo(0),
       video->mFrameID);
     Unused << SendOutput(output);
--- a/dom/media/mediasink/AudioSink.cpp
+++ b/dom/media/mediasink/AudioSink.cpp
@@ -280,17 +280,18 @@ AudioSink::PopFrames(uint32_t aFrames)
     MOZ_ASSERT(mCurrentData->mFrames > 0);
     mProcessedQueueLength -=
       FramesToUsecs(mCurrentData->mFrames, mOutputRate).value();
   }
 
   auto framesToPop = std::min(aFrames, mCursor->Available());
 
   SINK_LOG_V("playing audio at time=%" PRId64 " offset=%u length=%u",
-             mCurrentData->mTime, mCurrentData->mFrames - mCursor->Available(), framesToPop);
+             mCurrentData->mTime.ToMicroseconds(),
+             mCurrentData->mFrames - mCursor->Available(), framesToPop);
 
   UniquePtr<AudioStream::Chunk> chunk =
     MakeUnique<Chunk>(mCurrentData, framesToPop, mCursor->Ptr());
 
   {
     MonitorAutoLock mon(mMonitor);
     mWritten += framesToPop;
     mCursor->Advance(framesToPop);
@@ -401,18 +402,18 @@ AudioSink::NotifyAudioNeeded()
           AudioConfig(data->mChannels, data->mRate),
           AudioConfig(mOutputChannels, mOutputRate));
     }
 
     // See if there's a gap in the audio. If there is, push silence into the
     // audio hardware, so we can play across the gap.
     // Calculate the timestamp of the next chunk of audio in numbers of
     // samples.
-    CheckedInt64 sampleTime = TimeUnitToFrames(
-      TimeUnit::FromMicroseconds(data->mTime) - mStartTime, data->mRate);
+    CheckedInt64 sampleTime =
+      TimeUnitToFrames(data->mTime - mStartTime, data->mRate);
     // Calculate the number of frames that have been pushed onto the audio hardware.
     CheckedInt64 missingFrames = sampleTime - mFramesParsed;
 
     if (!missingFrames.isValid()) {
       NS_WARNING("Int overflow in AudioSink");
       mErrored = true;
       return;
     }
@@ -496,17 +497,17 @@ AudioSink::CreateAudioFromBuffer(Aligned
   CheckedInt64 duration = FramesToUsecs(frames, mOutputRate);
   if (!duration.isValid()) {
     NS_WARNING("Int overflow in AudioSink");
     mErrored = true;
     return nullptr;
   }
   RefPtr<AudioData> data =
     new AudioData(aReference->mOffset,
-                  aReference->mTime,
+                  aReference->mTime.ToMicroseconds(),
                   duration.value(),
                   frames,
                   Move(aBuffer),
                   mOutputChannels,
                   mOutputRate);
   return data.forget();
 }
 
--- a/dom/media/mediasink/DecodedStream.cpp
+++ b/dom/media/mediasink/DecodedStream.cpp
@@ -456,17 +456,17 @@ SendStreamAudio(DecodedStreamData* aStre
   static const int64_t AUDIO_FUZZ_FRAMES = 1;
 
   MOZ_ASSERT(aData);
   AudioData* audio = aData;
   // This logic has to mimic AudioSink closely to make sure we write
   // the exact same silences
   CheckedInt64 audioWrittenOffset = aStream->mAudioFramesWritten
     + TimeUnitToFrames(aStartTime, aRate);
-  CheckedInt64 frameOffset = UsecsToFrames(audio->mTime, aRate);
+  CheckedInt64 frameOffset = TimeUnitToFrames(audio->mTime, aRate);
 
   if (!audioWrittenOffset.isValid() ||
       !frameOffset.isValid() ||
       // ignore packet that we've already processed
       audio->GetEndTime() <= aStream->mNextAudioTime) {
     return;
   }
 
@@ -590,32 +590,31 @@ DecodedStream::SendVideo(bool aIsSameOri
   TimeStamp tracksStartTimeStamp = sourceStream->GetStreamTracksStrartTimeStamp();
   if (tracksStartTimeStamp.IsNull()) {
     tracksStartTimeStamp = TimeStamp::Now();
   }
 
   for (uint32_t i = 0; i < video.Length(); ++i) {
     VideoData* v = video[i];
 
-    if (mData->mNextVideoTime.ToMicroseconds() < v->mTime) {
+    if (mData->mNextVideoTime < v->mTime) {
       // Write last video frame to catch up. mLastVideoImage can be null here
       // which is fine, it just means there's no video.
 
       // TODO: |mLastVideoImage| should come from the last image rendered
       // by the state machine. This will avoid the black frame when capture
       // happens in the middle of playback (especially in th middle of a
       // video frame). E.g. if we have a video frame that is 30 sec long
       // and capture happens at 15 sec, we'll have to append a black frame
       // that is 15 sec long.
-      WriteVideoToMediaStream(sourceStream, mData->mLastVideoImage,
-        FromMicroseconds(v->mTime),
+      WriteVideoToMediaStream(sourceStream, mData->mLastVideoImage, v->mTime,
         mData->mNextVideoTime, mData->mLastVideoImageDisplaySize,
-        tracksStartTimeStamp + TimeDuration::FromMicroseconds(v->mTime),
+        tracksStartTimeStamp + v->mTime.ToTimeDuration(),
         &output, aPrincipalHandle);
-      mData->mNextVideoTime = FromMicroseconds(v->mTime);
+      mData->mNextVideoTime = v->mTime;
     }
 
     if (mData->mNextVideoTime < v->GetEndTime()) {
       WriteVideoToMediaStream(sourceStream, v->mImage, v->GetEndTime(),
         mData->mNextVideoTime, v->mDisplay,
         tracksStartTimeStamp + v->GetEndTime().ToTimeDuration(),
         &output, aPrincipalHandle);
       mData->mNextVideoTime = v->GetEndTime();
@@ -741,17 +740,17 @@ DecodedStream::GetPosition(TimeStamp* aT
   return mStartTime.ref() + mLastOutputTime;
 }
 
 void
 DecodedStream::NotifyOutput(int64_t aTime)
 {
   AssertOwnerThread();
   mLastOutputTime = FromMicroseconds(aTime);
-  int64_t currentTime = GetPosition().ToMicroseconds();
+  auto currentTime = GetPosition();
 
   // Remove audio samples that have been played by MSG from the queue.
   RefPtr<AudioData> a = mAudioQueue.PeekFront();
   for (; a && a->mTime < currentTime;) {
     RefPtr<AudioData> releaseMe = mAudioQueue.PopFront();
     a = mAudioQueue.PeekFront();
   }
 }
--- a/dom/media/mediasink/VideoSink.cpp
+++ b/dom/media/mediasink/VideoSink.cpp
@@ -360,26 +360,25 @@ VideoSink::RenderVideoFrames(int32_t aMa
 
     frame->MarkSentToCompositor();
 
     if (!frame->mImage || !frame->mImage->IsValid() ||
         !frame->mImage->GetSize().width || !frame->mImage->GetSize().height) {
       continue;
     }
 
-    int64_t frameTime = frame->mTime;
-    if (frameTime < 0) {
+    if (frame->mTime.IsNegative()) {
       // Frame times before the start time are invalid; drop such frames
       continue;
     }
 
     TimeStamp t;
     if (aMaxFrames > 1) {
       MOZ_ASSERT(!aClockTimeStamp.IsNull());
-      int64_t delta = frame->mTime - aClockTime;
+      int64_t delta = frame->mTime.ToMicroseconds() - aClockTime;
       t = aClockTimeStamp +
           TimeDuration::FromMicroseconds(delta / params.mPlaybackRate);
       if (!lastFrameTime.IsNull() && t <= lastFrameTime) {
         // Timestamps out of order; drop the new frame. In theory we should
         // probably replace the previous frame with the new frame if the
         // timestamps are equal, but this is a corrupt video file already so
         // never mind.
         continue;
@@ -389,17 +388,18 @@ VideoSink::RenderVideoFrames(int32_t aMa
 
     ImageContainer::NonOwningImage* img = images.AppendElement();
     img->mTimeStamp = t;
     img->mImage = frame->mImage;
     img->mFrameID = frame->mFrameID;
     img->mProducerID = mProducerID;
 
     VSINK_LOG_V("playing video frame %" PRId64 " (id=%x) (vq-queued=%" PRIuSIZE ")",
-                frame->mTime, frame->mFrameID, VideoQueue().GetSize());
+                frame->mTime.ToMicroseconds(), frame->mFrameID,
+                VideoQueue().GetSize());
   }
 
   if (images.Length() > 0) {
     mContainer->SetCurrentFrames(frames[0]->mDisplay, images);
   }
 }
 
 void
@@ -419,17 +419,17 @@ VideoSink::UpdateRenderedVideoFrames()
          clockTime >= VideoQueue().PeekFront()->GetEndTime()) {
     RefPtr<VideoData> frame = VideoQueue().PopFront();
     lastFrameEndTime = frame->GetEndTime();
     if (frame->IsSentToCompositor()) {
       mFrameStats.NotifyPresentedFrame();
     } else {
       mFrameStats.NotifyDecodedFrames({ 0, 0, 1 });
       VSINK_LOG_V("discarding video frame mTime=%" PRId64 " clock_time=%" PRId64,
-                  frame->mTime, clockTime.ToMicroseconds());
+                  frame->mTime.ToMicroseconds(), clockTime.ToMicroseconds());
     }
   }
 
   // The presentation end time of the last video frame displayed is either
   // the end time of the current frame, or if we dropped all frames in the
   // queue, the end time of the last frame we removed from the queue.
   RefPtr<VideoData> currentFrame = VideoQueue().PeekFront();
   mVideoFrameEndTime = std::max(mVideoFrameEndTime,
@@ -445,17 +445,17 @@ VideoSink::UpdateRenderedVideoFrames()
   // the start time of the next frame. If we don't have a next frame,
   // we will run render loops again upon incoming frames.
   nsTArray<RefPtr<VideoData>> frames;
   VideoQueue().GetFirstElements(2, &frames);
   if (frames.Length() < 2) {
     return;
   }
 
-  int64_t nextFrameTime = frames[1]->mTime;
+  int64_t nextFrameTime = frames[1]->mTime.ToMicroseconds();
   int64_t delta = std::max(
     nextFrameTime - clockTime.ToMicroseconds(), MIN_UPDATE_INTERVAL_US);
   TimeStamp target = nowTime + TimeDuration::FromMicroseconds(
      delta / mAudioSink->GetPlaybackParams().mPlaybackRate);
 
   RefPtr<VideoSink> self = this;
   mUpdateScheduler.Ensure(target, [self] () {
     self->UpdateRenderedVideoFramesByTimer();
--- a/dom/media/mediasource/MediaSourceDemuxer.cpp
+++ b/dom/media/mediasource/MediaSourceDemuxer.cpp
@@ -474,17 +474,17 @@ MediaSourceTrackDemuxer::DoGetSamples(in
           ? NS_ERROR_DOM_MEDIA_END_OF_STREAM
           : NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA, __func__);
       }
       return SamplesPromise::CreateAndReject(result, __func__);
     }
   }
   RefPtr<SamplesHolder> samples = new SamplesHolder;
   samples->mSamples.AppendElement(sample);
-  if (mNextRandomAccessPoint.ToMicroseconds() <= sample->mTime) {
+  if (mNextRandomAccessPoint <= sample->mTime) {
     MonitorAutoLock mon(mMonitor);
     mNextRandomAccessPoint =
       mManager->GetNextRandomAccessPoint(mType, MediaSourceDemuxer::EOS_FUZZ);
   }
   return SamplesPromise::CreateAndResolve(samples, __func__);
 }
 
 RefPtr<MediaSourceTrackDemuxer::SkipAccessPointPromise>
--- a/dom/media/mediasource/TrackBuffersManager.cpp
+++ b/dom/media/mediasource/TrackBuffersManager.cpp
@@ -454,18 +454,18 @@ TrackBuffersManager::DoEvictData(const T
   }
 
   const int64_t finalSize = mSizeSourceBuffer - aSizeToEvict;
 
   if (lastKeyFrameIndex > 0) {
     MSE_DEBUG("Step1. Evicting %" PRId64 " bytes prior currentTime",
               aSizeToEvict - toEvict);
     CodedFrameRemoval(
-      TimeInterval(TimeUnit::FromMicroseconds(0),
-                   TimeUnit::FromMicroseconds(buffer[lastKeyFrameIndex]->mTime - 1)));
+      TimeInterval(TimeUnit::Zero(),
+                   buffer[lastKeyFrameIndex]->mTime - TimeUnit::FromMicroseconds(1)));
   }
 
   if (mSizeSourceBuffer <= finalSize) {
     return;
   }
 
   toEvict = mSizeSourceBuffer - finalSize;
 
@@ -482,29 +482,29 @@ TrackBuffersManager::DoEvictData(const T
     return;
   }
 
   // Don't evict before the end of the current segment
   TimeUnit upperLimit = futureBuffered[0].mEnd;
   uint32_t evictedFramesStartIndex = buffer.Length();
   for (int32_t i = buffer.Length() - 1; i >= 0; i--) {
     const auto& frame = buffer[i];
-    if (frame->mTime <= upperLimit.ToMicroseconds() || toEvict < 0) {
+    if (frame->mTime <= upperLimit || toEvict < 0) {
       // We've reached a frame that shouldn't be evicted -> Evict after it -> i+1.
       // Or the previous loop reached the eviction threshold -> Evict from it -> i+1.
       evictedFramesStartIndex = i + 1;
       break;
     }
     toEvict -= frame->ComputedSizeOfIncludingThis();
   }
   if (evictedFramesStartIndex < buffer.Length()) {
     MSE_DEBUG("Step2. Evicting %" PRId64 " bytes from trailing data",
               mSizeSourceBuffer - finalSize - toEvict);
     CodedFrameRemoval(
-      TimeInterval(TimeUnit::FromMicroseconds(buffer[evictedFramesStartIndex]->mTime),
+      TimeInterval(buffer[evictedFramesStartIndex]->mTime,
                    TimeUnit::FromInfinity()));
   }
 }
 
 RefPtr<TrackBuffersManager::RangeRemovalPromise>
 TrackBuffersManager::CodedFrameRemovalWithPromise(TimeInterval aInterval)
 {
   MOZ_ASSERT(OnTaskQueue());
@@ -554,18 +554,18 @@ TrackBuffersManager::CodedFrameRemoval(T
       // Nothing to remove.
       continue;
     }
 
     // 2. If this track buffer has a random access point timestamp that is greater than or equal to end,
     // then update remove end timestamp to that random access point timestamp.
     if (end < track->mBufferedRanges.GetEnd()) {
       for (auto& frame : track->GetTrackBuffer()) {
-        if (frame->mKeyframe && frame->mTime >= end.ToMicroseconds()) {
-          removeEndTimestamp = TimeUnit::FromMicroseconds(frame->mTime);
+        if (frame->mKeyframe && frame->mTime >= end) {
+          removeEndTimestamp = frame->mTime;
           break;
         }
       }
     }
 
     // 3. Remove all media data, from this track buffer, that contain starting
     // timestamps greater than or equal to start and less than the remove end timestamp.
     // 4. Remove decoding dependencies of the coded frames removed in the previous step:
@@ -1415,24 +1415,22 @@ TrackBuffersManager::CheckSequenceDiscon
     mSourceBufferAttributes->ResetGroupStartTimestamp();
   }
 }
 
 TimeInterval
 TrackBuffersManager::PresentationInterval(const TrackBuffer& aSamples) const
 {
   TimeInterval presentationInterval =
-    TimeInterval(TimeUnit::FromMicroseconds(aSamples[0]->mTime),
-                 aSamples[0]->GetEndTime());
+    TimeInterval(aSamples[0]->mTime, aSamples[0]->GetEndTime());
 
   for (uint32_t i = 1; i < aSamples.Length(); i++) {
     auto& sample = aSamples[i];
     presentationInterval = presentationInterval.Span(
-      TimeInterval(TimeUnit::FromMicroseconds(sample->mTime),
-                   sample->GetEndTime()));
+      TimeInterval(sample->mTime, sample->GetEndTime()));
   }
   return presentationInterval;
 }
 
 void
 TrackBuffersManager::ProcessFrames(TrackBuffer& aSamples, TrackData& aTrackData)
 {
   if (!aSamples.Length()) {
@@ -1440,18 +1438,18 @@ TrackBuffersManager::ProcessFrames(Track
   }
 
   // 1. If generate timestamps flag equals true
   // Let presentation timestamp equal 0.
   // Otherwise
   // Let presentation timestamp be a double precision floating point representation of the coded frame's presentation timestamp in seconds.
   TimeUnit presentationTimestamp =
     mSourceBufferAttributes->mGenerateTimestamps
-    ? TimeUnit()
-    : TimeUnit::FromMicroseconds(aSamples[0]->mTime);
+    ? TimeUnit::Zero()
+    : aSamples[0]->mTime;
 
   // 3. If mode equals "sequence" and group start timestamp is set, then run the following steps:
   CheckSequenceDiscontinuity(presentationTimestamp);
 
   // 5. Let track buffer equal the track buffer that the coded frame will be added to.
   auto& trackBuffer = aTrackData;
 
   // Some videos do not exactly start at 0, but instead a small negative value.
@@ -1483,19 +1481,19 @@ TrackBuffersManager::ProcessFrames(Track
   if (aSamples.Length()) {
     aTrackData.mLastParsedEndTime = TimeUnit();
   }
 
   for (auto& sample : aSamples) {
     SAMPLE_DEBUG("Processing %s frame(pts:%" PRId64 " end:%" PRId64 ", dts:%" PRId64 ", duration:%" PRId64 ", "
                "kf:%d)",
                aTrackData.mInfo->mMimeType.get(),
-               sample->mTime,
+               sample->mTime.ToMicroseconds(),
                sample->GetEndTime().ToMicroseconds(),
-               sample->mTimecode,
+               sample->mTimecode.ToMicroseconds(),
                sample->mDuration.ToMicroseconds(),
                sample->mKeyframe);
 
     const TimeUnit sampleEndTime = sample->GetEndTime();
     if (sampleEndTime > aTrackData.mLastParsedEndTime) {
       aTrackData.mLastParsedEndTime = sampleEndTime;
     }
 
@@ -1519,18 +1517,18 @@ TrackBuffersManager::ProcessFrames(Track
     // Otherwise:
     //   Let presentation timestamp be a double precision floating point representation of the coded frame's presentation timestamp in seconds.
     //   Let decode timestamp be a double precision floating point representation of the coded frame's decode timestamp in seconds.
 
     // 2. Let frame duration be a double precision floating point representation of the coded frame's duration in seconds.
     // Step 3 is performed earlier or when a discontinuity has been detected.
     // 4. If timestampOffset is not 0, then run the following steps:
 
-    TimeUnit sampleTime = TimeUnit::FromMicroseconds(sample->mTime);
-    TimeUnit sampleTimecode = TimeUnit::FromMicroseconds(sample->mTimecode);
+    TimeUnit sampleTime = sample->mTime;
+    TimeUnit sampleTimecode = sample->mTimecode;
     TimeUnit sampleDuration = sample->mDuration;
     TimeUnit timestampOffset = mSourceBufferAttributes->GetTimestampOffset();
 
     TimeInterval sampleInterval =
       mSourceBufferAttributes->mGenerateTimestamps
       ? TimeInterval(timestampOffset, timestampOffset + sampleDuration)
       : TimeInterval(timestampOffset + sampleTime,
                      timestampOffset + sampleTime + sampleDuration);
@@ -1613,18 +1611,18 @@ TrackBuffersManager::ProcessFrames(Track
       }
       trackBuffer.mNeedRandomAccessPoint = true;
       needDiscontinuityCheck = true;
       continue;
     }
 
     samplesRange += sampleInterval;
     sizeNewSamples += sample->ComputedSizeOfIncludingThis();
-    sample->mTime = sampleInterval.mStart.ToMicroseconds();
-    sample->mTimecode = decodeTimestamp.ToMicroseconds();
+    sample->mTime = sampleInterval.mStart;
+    sample->mTimecode = decodeTimestamp;
     sample->mTrackInfo = trackBuffer.mLastInfo;
     samples.AppendElement(sample);
 
     // Steps 11,12,13,14, 15 and 16 will be done in one block in InsertFrames.
 
     trackBuffer.mLongestFrameDuration =
       trackBuffer.mLastFrameDuration.isSome()
       ? sample->mKeyframe
@@ -1689,17 +1687,17 @@ TrackBuffersManager::CheckNextInsertionI
     // No target found, it will be added at the end of the track buffer.
     aTrackData.mNextInsertionIndex = Some(uint32_t(data.Length()));
     return true;
   }
   // We now need to find the first fr