Bug 808370 - Use the VariablesView in webconsole; r=past,vporof,paul
authorMihai Sucan <mihai.sucan@gmail.com>
Tue, 09 Apr 2013 12:46:30 +0300
changeset 128028 dfc808a01756195e3ebbc315bf6f87e6cf95e489
parent 128027 f5d6c95a9de90e6b2ea07f3ff1b502e36da4597b
child 128029 26fb3bd67f5f613835504058d6708cb2440d592e
push id1482
push usermihai.sucan@gmail.com
push dateTue, 09 Apr 2013 09:55:08 +0000
treeherderfx-team@26fb3bd67f5f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspast, vporof, paul
bugs808370
milestone23.0a1
Bug 808370 - Use the VariablesView in webconsole; r=past,vporof,paul
browser/devtools/debugger/debugger-controller.js
browser/devtools/framework/Sidebar.jsm
browser/devtools/framework/test/browser_toolbox_sidebar.js
browser/devtools/jar.mn
browser/devtools/shared/widgets/VariablesView.jsm
browser/devtools/shared/widgets/VariablesView.xul
browser/devtools/styleinspector/test/browser_ruleview_focus.js
browser/devtools/webconsole/test/Makefile.in
browser/devtools/webconsole/test/browser_bug664688_sandbox_update_after_navigation.js
browser/devtools/webconsole/test/browser_console_log_inspectable_object.js
browser/devtools/webconsole/test/browser_console_variables_view.js
browser/devtools/webconsole/test/browser_console_variables_view_while_debugging.js
browser/devtools/webconsole/test/browser_eval_in_debugger_stackframe.js
browser/devtools/webconsole/test/browser_output_breaks_after_console_dir_uninspectable.js
browser/devtools/webconsole/test/browser_result_format_as_string.js
browser/devtools/webconsole/test/browser_webconsole_bug_594477_clickable_output.js
browser/devtools/webconsole/test/browser_webconsole_bug_595350_multiple_windows_and_tabs.js
browser/devtools/webconsole/test/browser_webconsole_bug_598357_jsterm_output.js
browser/devtools/webconsole/test/browser_webconsole_bug_600183_charset.js
browser/devtools/webconsole/test/browser_webconsole_bug_601177_log_levels.js
browser/devtools/webconsole/test/browser_webconsole_bug_611795.js
browser/devtools/webconsole/test/browser_webconsole_bug_618311_close_panels.js
browser/devtools/webconsole/test/browser_webconsole_bug_632275_getters_document_width.js
browser/devtools/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js
browser/devtools/webconsole/test/browser_webconsole_bug_651501_document_body_autocomplete.js
browser/devtools/webconsole/test/browser_webconsole_bug_659907_console_dir.js
browser/devtools/webconsole/test/browser_webconsole_console_logging_api.js
browser/devtools/webconsole/test/browser_webconsole_execution_scope.js
browser/devtools/webconsole/test/browser_webconsole_jsterm.js
browser/devtools/webconsole/test/browser_webconsole_output_order.js
browser/devtools/webconsole/test/browser_webconsole_property_panel.js
browser/devtools/webconsole/test/head.js
browser/devtools/webconsole/test/test-eval-in-stackframe.html
browser/devtools/webconsole/webconsole.js
browser/devtools/webconsole/webconsole.xul
browser/locales/en-US/chrome/browser/devtools/VariablesView.dtd
browser/locales/en-US/chrome/browser/devtools/webConsole.dtd
browser/locales/en-US/chrome/browser/devtools/webconsole.properties
browser/locales/jar.mn
browser/themes/linux/devtools/webconsole.css
browser/themes/osx/devtools/webconsole.css
browser/themes/windows/devtools/webconsole.css
dom/tests/browser/browser_ConsoleAPITests.js
--- a/browser/devtools/debugger/debugger-controller.js
+++ b/browser/devtools/debugger/debugger-controller.js
@@ -10,27 +10,16 @@ const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 const DBG_STRINGS_URI = "chrome://browser/locale/devtools/debugger.properties";
 const NEW_SOURCE_IGNORED_URLS = ["debugger eval code", "self-hosted"];
 const NEW_SOURCE_DISPLAY_DELAY = 200; // ms
 const FETCH_SOURCE_RESPONSE_DELAY = 50; // ms
 const FRAME_STEP_CLEAR_DELAY = 100; // ms
 const CALL_STACK_PAGE_SIZE = 25; // frames
-const VARIABLES_VIEW_NON_SORTABLE = [
-  "Array",
-  "Int8Array",
-  "Uint8Array",
-  "Int16Array",
-  "Uint16Array",
-  "Int32Array",
-  "Uint32Array",
-  "Float32Array",
-  "Float64Array"
-];
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
 Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
 Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
 Cu.import("resource:///modules/source-editor.jsm");
 Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
@@ -888,17 +877,17 @@ StackFrames.prototype = {
     if (aVar._fetched) {
       return;
     }
     aVar._fetched = true;
     let grip = aVar._sourceGrip;
 
     this.activeThread.pauseGrip(grip).getPrototypeAndProperties(function(aResponse) {
       let { ownProperties, prototype } = aResponse;
-      let sortable = VARIABLES_VIEW_NON_SORTABLE.indexOf(grip.class) == -1;
+      let sortable = VariablesView.NON_SORTABLE_CLASSES.indexOf(grip.class) == -1;
 
       // Add all the variable properties.
       if (ownProperties) {
         aVar.addProperties(ownProperties, {
           // Not all variables need to force sorted properties.
           sorted: sortable,
           // Expansion handlers must be set after the properties are added.
           callback: this._addVarExpander
--- a/browser/devtools/framework/Sidebar.jsm
+++ b/browser/devtools/framework/Sidebar.jsm
@@ -56,24 +56,24 @@ ToolSidebar.prototype = {
     iframe.setAttribute("src", url);
     iframe.tooltip = "aHTMLTooltip";
 
     let tab = this._tabbox.tabs.appendItem();
     tab.setAttribute("label", ""); // Avoid showing "undefined" while the tab is loading
 
     let onIFrameLoaded = function() {
       tab.setAttribute("label", iframe.contentDocument.title);
-      iframe.removeEventListener("DOMContentLoaded", onIFrameLoaded, true);
+      iframe.removeEventListener("load", onIFrameLoaded, true);
       if ("setPanel" in iframe.contentWindow) {
         iframe.contentWindow.setPanel(this._toolPanel, iframe);
       }
       this.emit(id + "-ready");
     }.bind(this);
 
-    iframe.addEventListener("DOMContentLoaded", onIFrameLoaded, true);
+    iframe.addEventListener("load", onIFrameLoaded, true);
 
     let tabpanel = this._panelDoc.createElementNS(XULNS, "tabpanel");
     tabpanel.setAttribute("id", "sidebar-panel-" + id);
     tabpanel.appendChild(iframe);
     this._tabbox.tabpanels.appendChild(tabpanel);
 
     this._tooltip = this._panelDoc.createElementNS(XULNS, "tooltip");
     this._tooltip.id = "aHTMLTooltip";
@@ -86,17 +86,17 @@ ToolSidebar.prototype = {
     this._tabs.set(id, tab);
 
     if (selected) {
       // For some reason I don't understand, if we call this.select in this
       // event loop (after inserting the tab), the tab will never get the
       // the "selected" attribute set to true.
       this._panelDoc.defaultView.setTimeout(function() {
         this.select(id);
-      }.bind(this), 0);
+      }.bind(this), 10);
     }
 
     this.emit("new-tab-registered", id);
   },
 
   /**
    * Select a specific tab.
    */
--- a/browser/devtools/framework/test/browser_toolbox_sidebar.js
+++ b/browser/devtools/framework/test/browser_toolbox_sidebar.js
@@ -17,17 +17,17 @@ function test() {
                   "</hbox>" +
                   "</window>";
 
   const tab1URL = "data:text/html;charset=utf8,<title>1</title><p>1</p>";
   const tab2URL = "data:text/html;charset=utf8,<title>2</title><p>2</p>";
   const tab3URL = "data:text/html;charset=utf8,<title>3</title><p>3</p>";
 
   let panelDoc;
-
+  let tab1Selected = false;
   let registeredTabs = {};
   let readyTabs = {};
 
   let toolDefinition = {
     id: "fakeTool4242",
     killswitch: "devtools.fakeTool4242.enabled",
     url: toolURL,
     label: "FAKE TOOL!!!",
@@ -58,45 +58,52 @@ function test() {
       let tabbox = panel.panelDoc.getElementById("sidebar");
       panel.sidebar = new ToolSidebar(tabbox, panel, true);
 
       panel.sidebar.on("new-tab-registered", function(event, id) {
         registeredTabs[id] = true;
       });
 
       panel.sidebar.once("tab1-ready", function(event) {
+        info(event);
         readyTabs.tab1 = true;
-        if (readyTabs.tab1 && readyTabs.tab2 && readyTabs.tab3) {
-          allTabsReady(panel);
-        }
+        allTabsReady(panel);
       });
 
       panel.sidebar.once("tab2-ready", function(event) {
+        info(event);
         readyTabs.tab2 = true;
-        if (readyTabs.tab1 && readyTabs.tab2 && readyTabs.tab3) {
-          allTabsReady(panel);
-        }
+        allTabsReady(panel);
       });
 
       panel.sidebar.once("tab3-ready", function(event) {
+        info(event);
         readyTabs.tab3 = true;
-        if (readyTabs.tab1 && readyTabs.tab2 && readyTabs.tab3) {
-          allTabsReady(panel);
-        }
+        allTabsReady(panel);
+      });
+
+      panel.sidebar.once("tab1-selected", function(event) {
+        info(event);
+        tab1Selected = true;
+        allTabsReady(panel);
       });
 
       panel.sidebar.addTab("tab1", tab1URL, true);
       panel.sidebar.addTab("tab2", tab2URL);
       panel.sidebar.addTab("tab3", tab3URL);
 
       panel.sidebar.show();
     }).then(null, console.error);
   });
 
   function allTabsReady(panel) {
+    if (!tab1Selected || !readyTabs.tab1 || !readyTabs.tab2 || !readyTabs.tab3) {
+      return;
+    }
+
     ok(registeredTabs.tab1, "tab1 registered");
     ok(registeredTabs.tab2, "tab2 registered");
     ok(registeredTabs.tab3, "tab3 registered");
     ok(readyTabs.tab1, "tab1 ready");
     ok(readyTabs.tab2, "tab2 ready");
     ok(readyTabs.tab3, "tab3 ready");
 
     let tabs = panel.sidebar._tabbox.querySelectorAll("tab");
--- a/browser/devtools/jar.mn
+++ b/browser/devtools/jar.mn
@@ -1,14 +1,15 @@
 # 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/.
 
 browser.jar:
     content/browser/devtools/widgets.css          (shared/widgets/widgets.css)
+    content/browser/devtools/widgets/VariablesView.xul (shared/widgets/VariablesView.xul)
     content/browser/devtools/markup-view.xhtml    (markupview/markup-view.xhtml)
     content/browser/devtools/markup-view.css      (markupview/markup-view.css)
     content/browser/devtools/netmonitor.xul           (netmonitor/netmonitor.xul)
     content/browser/devtools/netmonitor.css           (netmonitor/netmonitor.css)
     content/browser/devtools/netmonitor-controller.js (netmonitor/netmonitor-controller.js)
     content/browser/devtools/netmonitor-view.js       (netmonitor/netmonitor-view.js)
     content/browser/NetworkPanel.xhtml            (webconsole/NetworkPanel.xhtml)
     content/browser/devtools/webconsole.js        (webconsole/webconsole.js)
--- a/browser/devtools/shared/widgets/VariablesView.jsm
+++ b/browser/devtools/shared/widgets/VariablesView.jsm
@@ -913,16 +913,28 @@ VariablesView.prototype = {
   _boxObject: null,
   _searchboxNode: null,
   _searchboxContainer: null,
   _searchboxPlaceholder: "",
   _emptyTextNode: null,
   _emptyTextValue: ""
 };
 
+VariablesView.NON_SORTABLE_CLASSES = [
+  "Array",
+  "Int8Array",
+  "Uint8Array",
+  "Int16Array",
+  "Uint16Array",
+  "Int32Array",
+  "Uint32Array",
+  "Float32Array",
+  "Float64Array"
+];
+
 /**
  * Generates the string evaluated when performing simple value changes.
  *
  * @param Variable | Property aItem
  *        The current variable or property.
  * @param string aCurrentString
  *        The trimmed user inputted string.
  * @return string
@@ -978,17 +990,18 @@ VariablesView.getterOrSetterEvalMacro = 
     case "undefined":
       let mirrorType = type == "get" ? "set" : "get";
       let mirrorLookup = type == "get" ? "__lookupSetter__" : "__lookupGetter__";
 
       // If the parent object will end up without any getter or setter,
       // morph it into a plain value.
       if ((type == "set" && propertyObject.getter.type == "undefined") ||
           (type == "get" && propertyObject.setter.type == "undefined")) {
-        return VariablesView.overrideValueEvalMacro(propertyObject, "undefined");
+        // Make sure the right getter/setter to value override macro is applied to the target object.
+        return propertyObject.evaluationMacro(propertyObject, "undefined");
       }
 
       // Construct and return the getter/setter removal evaluation string.
       // e.g: Object.defineProperty(foo, "bar", {
       //   get: foo.__lookupGetter__("bar"),
       //   set: undefined,
       //   enumerable: true,
       //   configurable: true
@@ -1036,17 +1049,20 @@ VariablesView.getterOrSetterEvalMacro = 
 /**
  * Function invoked when a getter or setter is deleted.
  *
  * @param Property aItem
  *        The current getter or setter property.
  */
 VariablesView.getterOrSetterDeleteCallback = function(aItem) {
   aItem._disable();
-  aItem.ownerView.eval(VariablesView.getterOrSetterEvalMacro(aItem, ""));
+
+  // Make sure the right getter/setter to value override macro is applied to the target object.
+  aItem.ownerView.eval(aItem.evaluationMacro(aItem, ""));
+
   return true; // Don't hide the element.
 };
 
 /**
  * A Scope is an object holding Variable instances.
  * Iterable via "for (let [name, variable] in instance) { }".
  *
  * @param VariablesView aView
@@ -1412,16 +1428,23 @@ Scope.prototype = {
       if (!item._isExpanded) {
         return false;
       }
     }
     return true;
   },
 
   /**
+   * Focus this scope.
+   */
+  focus: function S_focus() {
+    this._variablesView._focusItem(this);
+  },
+
+  /**
    * Adds an event listener for a certain event on this scope's title.
    * @param string aName
    * @param function aCallback
    * @param boolean aCapture
    */
   addEventListener: function S_addEventListener(aName, aCallback, aCapture) {
     this._title.addEventListener(aName, aCallback, aCapture);
   },
@@ -1444,16 +1467,28 @@ Scope.prototype = {
 
   /**
    * Gets the name associated with this item.
    * @return string
    */
   get name() this._nameString,
 
   /**
+   * Gets the displayed value for this item.
+   * @return string
+   */
+  get displayValue() this._valueString,
+
+  /**
+   * Gets the class names used for the displayed value.
+   * @return string
+   */
+  get displayValueClassName() this._valueClassName,
+
+  /**
    * Gets the element associated with this item.
    * @return nsIDOMNode
    */
   get target() this._target,
 
   /**
    * Initializes this scope's id, view and binds event listeners.
    *
@@ -1520,17 +1555,17 @@ Scope.prototype = {
   /**
    * The click listener for this scope's title.
    */
   _onClick: function S__onClick(e) {
     if (e.target == this._inputNode) {
       return;
     }
     this.toggle();
-    this._variablesView._focusItem(this);
+    this.focus();
   },
 
   /**
    * Lazily appends a node to this scope's enumerable or non-enumerable
    * container. Once a certain number of nodes have been batched, they
    * will be appended.
    *
    * @param boolean aImmediateFlag
@@ -1902,17 +1937,17 @@ ViewHelpers.create({ constructor: Variab
    *        it will be inferred from the value.
    *        e.g. - { someProp0: { value: 42 },
    *                 someProp1: { value: true },
    *                 someProp2: { value: "nasu" },
    *                 someProp3: { value: { type: "undefined" } },
    *                 someProp4: { value: { type: "null" } },
    *                 someProp5: { value: { type: "object", class: "Object" } },
    *                 someProp6: { get: { type: "object", class: "Function" },
-   *                              set: { type: "undefined" } }
+   *                              set: { type: "undefined" } } }
    * @param object aOptions [optional]
    *        Additional options for adding the properties. Supported options:
    *        - sorted: true to sort all the properties before adding them
    *        - callback: function invoked after each property is added
    */
   addProperties: function V_addProperties(aProperties, aOptions = {}) {
     let propertyNames = Object.keys(aProperties);
 
@@ -2185,16 +2220,17 @@ ViewHelpers.create({ constructor: Variab
       if (this.ownerView.eval) {
         this.delete = VariablesView.getterOrSetterDeleteCallback;
         this.evaluationMacro = VariablesView.overrideValueEvalMacro;
       }
       // Deleting getters and setters individually is not allowed if no
       // evaluation method is provided.
       else {
         this.delete = null;
+        this.evaluationMacro = null;
       }
 
       let getter = this.addProperty("get", { value: descriptor.get });
       let setter = this.addProperty("set", { value: descriptor.set });
       getter.evaluationMacro = VariablesView.getterOrSetterEvalMacro;
       setter.evaluationMacro = VariablesView.getterOrSetterEvalMacro;
 
       getter.hideArrow();
@@ -2523,40 +2559,40 @@ ViewHelpers.create({ constructor: Variab
    */
   _onNameInputKeyPress: function V__onNameInputKeyPress(e) {
     e.stopPropagation();
 
     switch(e.keyCode) {
       case e.DOM_VK_RETURN:
       case e.DOM_VK_ENTER:
         this._saveNameInput(e);
-        this._variablesView._focusItem(this);
+        this.focus();
         return;
       case e.DOM_VK_ESCAPE:
         this._deactivateNameInput(e);
-        this._variablesView._focusItem(this);
+        this.focus();
         return;
     }
   },
 
   /**
    * The key press listener for this variable's editable value textbox.
    */
   _onValueInputKeyPress: function V__onValueInputKeyPress(e) {
     e.stopPropagation();
 
     switch(e.keyCode) {
       case e.DOM_VK_RETURN:
       case e.DOM_VK_ENTER:
         this._saveValueInput(e);
-        this._variablesView._focusItem(this);
+        this.focus();
         return;
       case e.DOM_VK_ESCAPE:
         this._deactivateValueInput(e);
-        this._variablesView._focusItem(this);
+        this.focus();
         return;
     }
   },
 
   /**
    * The click listener for the edit button.
    */
   _onEdit: function V__onEdit(e) {
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/widgets/VariablesView.xul
@@ -0,0 +1,16 @@
+<?xml version="1.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/. -->
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+<?xml-stylesheet href="chrome://browser/content/devtools/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/common.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/widgets.css" type="text/css"?>
+<!DOCTYPE window [
+  <!ENTITY % viewDTD SYSTEM "chrome://browser/locale/devtools/VariablesView.dtd">
+  %viewDTD;
+]>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        title="&PropertiesViewWindowTitle;">
+  <vbox id="variables" flex="1"/>
+</window>
--- a/browser/devtools/styleinspector/test/browser_ruleview_focus.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_focus.js
@@ -14,19 +14,19 @@ function openRuleView()
   var target = TargetFactory.forTab(gBrowser.selectedTab);
   gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
     inspector = toolbox.getCurrentPanel();
     inspector.sidebar.select("ruleview");
 
     // Highlight a node.
     let node = content.document.getElementsByTagName("h1")[0];
     inspector.selection.once("new-node", testFocus);
-    executeSoon(function() {
-      inspector.selection.setNode(doc.body);
-    });
+
+    inspector.sidebar.once("ruleview-ready",
+                           () => inspector.selection.setNode(doc.body));
   });
 }
 
 function testFocus()
 {
   let win = inspector.sidebar.getWindowForTab("ruleview");
   let brace = win.document.querySelectorAll(".ruleview-ruleclose")[0];
 
--- a/browser/devtools/webconsole/test/Makefile.in
+++ b/browser/devtools/webconsole/test/Makefile.in
@@ -37,17 +37,16 @@ MOCHITEST_BROWSER_FILES = \
 	browser_webconsole_copying_multiple_messages_inserts_newlines_in_between.js \
 	browser_webconsole_bug_586388_select_all.js  \
 	browser_webconsole_bug_588967_input_expansion.js \
 	browser_webconsole_log_node_classes.js \
 	browser_webconsole_network_panel.js \
 	browser_webconsole_jsterm.js \
 	browser_webconsole_null_and_undefined_output.js \
 	browser_webconsole_output_order.js \
-	browser_webconsole_property_panel.js \
 	browser_webconsole_property_provider.js \
 	browser_webconsole_bug_587617_output_copy.js \
 	browser_webconsole_bug_585237_line_limit.js \
 	browser_webconsole_bug_582201_duplicate_errors.js \
 	browser_webconsole_bug_580454_timestamp_l10n.js \
 	browser_webconsole_netlogging.js \
 	browser_webconsole_bug_583816_No_input_and_Tab_key_pressed.js \
 	browser_webconsole_bug_594477_clickable_output.js \
@@ -111,20 +110,21 @@ MOCHITEST_BROWSER_FILES = \
 	browser_result_format_as_string.js \
 	browser_webconsole_bug_737873_mixedcontent.js \
 	browser_output_breaks_after_console_dir_uninspectable.js \
 	browser_console_log_inspectable_object.js \
 	browser_bug_638949_copy_link_location.js \
 	browser_output_longstring_expand.js \
 	browser_netpanel_longstring_expand.js \
 	browser_repeated_messages_accuracy.js \
+	browser_webconsole_bug_821877_csp_errors.js \
+	browser_eval_in_debugger_stackframe.js \
+	browser_console_variables_view.js \
+	browser_console_variables_view_while_debugging.js \
 	head.js \
-	browser_webconsole_bug_821877_csp_errors.js \
-	test-bug-821877-csperrors.html \
-	test-bug-821877-csperrors.html^headers^ \
 	$(NULL)
 
 ifeq ($(OS_ARCH), Darwin)
 MOCHITEST_BROWSER_FILES += \
         browser_webconsole_bug_804845_ctrl_key_nav.js \
         $(NULL)
 endif
 
@@ -212,11 +212,14 @@ MOCHITEST_BROWSER_FILES += \
 	test_bug_770099_bad_policy_uri.html \
 	test_bug_770099_bad_policy_uri.html^headers^ \
 	test-result-format-as-string.html \
 	test-bug-737873-mixedcontent.html \
 	test-repeated-messages.html \
 	test-bug-766001-console-log.js \
 	test-bug-766001-js-console-links.html \
 	test-bug-766001-js-errors.js \
+	test-bug-821877-csperrors.html \
+	test-bug-821877-csperrors.html^headers^ \
+	test-eval-in-stackframe.html \
 	$(NULL)
 
 include $(topsrcdir)/config/rules.mk
--- a/browser/devtools/webconsole/test/browser_bug664688_sandbox_update_after_navigation.js
+++ b/browser/devtools/webconsole/test/browser_bug664688_sandbox_update_after_navigation.js
@@ -47,17 +47,17 @@ function test()
             "jsterm input is also displayed");
 
       is(hud.outputNode.textContent.indexOf("Permission denied"), -1,
          "no permission denied errors");
 
       gBrowser.selectedBrowser.addEventListener("load", onPageLoad2, true);
       content.location = TEST_URI2;
     },
-    failureFn: finishTest,
+    failureFn: finishTestWithError,
   };
 
   function onPageLoad2() {
     gBrowser.selectedBrowser.removeEventListener("load", onPageLoad2, true);
 
     hud.jsterm.clearOutput();
     hud.jsterm.execute("window.location.href");
 
@@ -77,33 +77,33 @@ function test()
       isnot(node.textContent.indexOf("window.location.href"), -1,
             "jsterm input is also displayed");
       is(hud.outputNode.textContent.indexOf("Permission denied"), -1,
          "no permission denied errors");
 
       gBrowser.goBack();
       waitForSuccess(waitForBack);
     },
-    failureFn: finishTest,
+    failureFn: finishTestWithError,
   };
 
   let waitForBack = {
     name: "go back",
     validatorFn: function()
     {
       return content.location.href == TEST_URI1;
     },
     successFn: function()
     {
       hud.jsterm.clearOutput();
       hud.jsterm.execute("window.location.href");
 
       waitForSuccess(waitForLocation3);
     },
-    failureFn: finishTest,
+    failureFn: finishTestWithError,
   };
 
   let waitForLocation3 = {
     name: "window.location.href result is displayed after goBack()",
     validatorFn: function()
     {
       let node = hud.outputNode.getElementsByClassName("webconsole-msg-output")[0];
       return node && node.textContent.indexOf(TEST_URI1) > -1;
@@ -113,11 +113,17 @@ function test()
       let node = hud.outputNode.getElementsByClassName("webconsole-msg-input")[0];
       isnot(node.textContent.indexOf("window.location.href"), -1,
             "jsterm input is also displayed");
       is(hud.outputNode.textContent.indexOf("Permission denied"), -1,
          "no permission denied errors");
 
       executeSoon(finishTest);
     },
-    failureFn: finishTest,
+    failureFn: finishTestWithError,
   };
+
+  function finishTestWithError()
+  {
+    info("output content: " + hud.outputNode.textContent);
+    finishTest();
+  }
 }
--- a/browser/devtools/webconsole/test/browser_console_log_inspectable_object.js
+++ b/browser/devtools/webconsole/test/browser_console_log_inspectable_object.js
@@ -31,27 +31,28 @@ function performTest(hud)
     },
     successFn: function()
     {
       isnot(hud.outputNode.textContent.indexOf("myObj = {"), -1,
             "myObj = ... is shown");
 
       let clickable = hud.outputNode.querySelector(".hud-clickable");
       ok(clickable, "the console.log() object .hud-clickable was found");
-      isnot(clickable.textContent.indexOf("omgBug676722"), -1,
+      isnot(clickable.textContent.indexOf("Object"), -1,
             "clickable node content is correct");
 
-      document.addEventListener("popupshown", function _onPopupShown(aEvent) {
-        document.removeEventListener("popupshown", _onPopupShown);
+      hud.jsterm.once("variablesview-fetched",
+        (aEvent, aVar) => {
+          ok(aVar, "object inspector opened on click");
 
-        isnot(aEvent.target.label.indexOf("omgBug676722"), -1,
-           "object inspector opened on click");
-
-        executeSoon(finishTest);
-      });
+          findVariableViewProperties(aVar, [{
+            name: "abba",
+            value: "omgBug676722",
+          }], { webconsole: hud }).then(finishTest);
+        });
 
       executeSoon(function() {
         EventUtils.synthesizeMouse(clickable, 2, 2, {}, hud.iframeWindow);
       });
     },
     failureFn: finishTest,
   });
 }
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_console_variables_view.js
@@ -0,0 +1,178 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Test that makes sure web console eval happens in the user-selected stackframe
+// from the js debugger.
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-eval-in-stackframe.html";
+
+let gWebConsole, gJSTerm, gVariablesView;
+
+function test()
+{
+  addTab(TEST_URI);
+  browser.addEventListener("load", function onLoad() {
+    browser.removeEventListener("load", onLoad, true);
+    openConsole(null, consoleOpened);
+  }, true);
+}
+
+function consoleOpened(hud)
+{
+  gWebConsole = hud;
+  gJSTerm = hud.jsterm;
+  gJSTerm.execute("fooObj", onExecuteFooObj);
+}
+
+function onExecuteFooObj()
+{
+  let msg = gWebConsole.outputNode.querySelector(".webconsole-msg-output");
+  ok(msg, "output message found");
+  isnot(msg.textContent.indexOf("[object Object]"), -1, "message text check");
+
+  gJSTerm.once("variablesview-fetched", onFooObjFetch);
+
+  executeSoon(() =>
+    EventUtils.synthesizeMouse(msg, 2, 2, {}, gWebConsole.iframeWindow)
+  );
+}
+
+function onFooObjFetch(aEvent, aVar)
+{
+  gVariablesView = aVar._variablesView;
+  ok(gVariablesView, "variables view object");
+
+  findVariableViewProperties(aVar, [
+    { name: "testProp", value: "testValue" },
+  ], { webconsole: gWebConsole }).then(onTestPropFound);
+}
+
+function onTestPropFound(aResults)
+{
+  let prop = aResults[0].matchedProp;
+  ok(prop, "matched the |testProp| property in the variables view");
+
+  is(content.wrappedJSObject.fooObj.testProp, aResults[0].value,
+     "|fooObj.testProp| value is correct");
+
+  // Check that property value updates work and that jsterm functions can be
+  // used.
+  updateVariablesViewProperty({
+    property: prop,
+    field: "value",
+    string: "document.title + window.location + $('p')",
+    webconsole: gWebConsole,
+    callback: onFooObjFetchAfterUpdate,
+  });
+}
+
+function onFooObjFetchAfterUpdate(aEvent, aVar)
+{
+  info("onFooObjFetchAfterUpdate");
+  let para = content.document.querySelector("p");
+  let expectedValue = content.document.title + content.location + para;
+
+  findVariableViewProperties(aVar, [
+    { name: "testProp", value: expectedValue },
+  ], { webconsole: gWebConsole }).then(onUpdatedTestPropFound);
+}
+
+function onUpdatedTestPropFound(aResults)
+{
+  let prop = aResults[0].matchedProp;
+  ok(prop, "matched the updated |testProp| property value");
+
+  is(content.wrappedJSObject.fooObj.testProp, aResults[0].value,
+     "|fooObj.testProp| value has been updated");
+
+  // Check that property name updates work.
+  updateVariablesViewProperty({
+    property: prop,
+    field: "name",
+    string: "testUpdatedProp",
+    webconsole: gWebConsole,
+    callback: onFooObjFetchAfterPropRename,
+  });
+}
+
+function onFooObjFetchAfterPropRename(aEvent, aVar)
+{
+  info("onFooObjFetchAfterPropRename");
+
+  let para = content.document.querySelector("p");
+  let expectedValue = content.document.title + content.location + para;
+
+  // Check that the new value is in the variables view.
+  findVariableViewProperties(aVar, [
+    { name: "testUpdatedProp", value: expectedValue },
+  ], { webconsole: gWebConsole }).then(onRenamedTestPropFound);
+}
+
+function onRenamedTestPropFound(aResults)
+{
+  let prop = aResults[0].matchedProp;
+  ok(prop, "matched the renamed |testProp| property");
+
+  ok(!content.wrappedJSObject.fooObj.testProp,
+     "|fooObj.testProp| has been deleted");
+  is(content.wrappedJSObject.fooObj.testUpdatedProp, aResults[0].value,
+     "|fooObj.testUpdatedProp| is correct");
+
+  // Check that property value updates that cause exceptions are reported in
+  // the web console output.
+  updateVariablesViewProperty({
+    property: prop,
+    field: "value",
+    string: "foobarzFailure()",
+    webconsole: gWebConsole,
+    callback: onPropUpdateError,
+  });
+}
+
+function onPropUpdateError(aEvent, aVar)
+{
+  info("onPropUpdateError");
+
+  let para = content.document.querySelector("p");
+  let expectedValue = content.document.title + content.location + para;
+
+  // Make sure the property did not change.
+  findVariableViewProperties(aVar, [
+    { name: "testUpdatedProp", value: expectedValue },
+  ], { webconsole: gWebConsole }).then(onRenamedTestPropFoundAgain);
+}
+
+function onRenamedTestPropFoundAgain(aResults)
+{
+  let prop = aResults[0].matchedProp;
+  ok(prop, "matched the renamed |testProp| property again");
+
+  let outputNode = gWebConsole.outputNode;
+
+  waitForSuccess({
+    name: "exception in property update reported in the web console output",
+    validatorFn: () => outputNode.textContent.indexOf("foobarzFailure") != -1,
+    successFn: testPropDelete.bind(null, prop),
+    failureFn: testPropDelete.bind(null, prop),
+  });
+}
+
+function testPropDelete(aProp)
+{
+  gVariablesView.window.focus();
+  aProp.focus();
+
+  executeSoon(() => {
+    EventUtils.synthesizeKey("VK_DELETE", {}, gVariablesView.window);
+    gWebConsole = gJSTerm = gVariablesView = null;
+  });
+
+  waitForSuccess({
+    name: "property deleted",
+    validatorFn: () => !("testUpdatedProp" in content.wrappedJSObject.fooObj),
+    successFn: finishTest,
+    failureFn: finishTest,
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_console_variables_view_while_debugging.js
@@ -0,0 +1,132 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Test that makes sure web console eval happens in the user-selected stackframe
+// from the js debugger, when changing the value of a property in the variables
+// view.
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-eval-in-stackframe.html";
+
+let gWebConsole, gJSTerm, gDebuggerWin, gThread, gDebuggerController,
+    gStackframes, gVariablesView;
+
+function test()
+{
+  addTab(TEST_URI);
+  browser.addEventListener("load", function onLoad() {
+    browser.removeEventListener("load", onLoad, true);
+    openConsole(null, consoleOpened);
+  }, true);
+}
+
+function consoleOpened(hud)
+{
+  gWebConsole = hud;
+  gJSTerm = hud.jsterm;
+
+  executeSoon(() => {
+    info("openDebugger");
+    openDebugger().then(debuggerOpened);
+  });
+}
+
+function debuggerOpened(aResult)
+{
+  gDebuggerWin = aResult.panelWin;
+  gDebuggerController = gDebuggerWin.DebuggerController;
+  gThread = gDebuggerController.activeThread;
+  gStackframes = gDebuggerController.StackFrames;
+
+  executeSoon(() => {
+    gThread.addOneTimeListener("framesadded", onFramesAdded);
+
+    info("firstCall()");
+    content.wrappedJSObject.firstCall();
+  });
+}
+
+function onFramesAdded()
+{
+  info("onFramesAdded");
+
+  executeSoon(() =>
+    openConsole(null, () =>
+      gJSTerm.execute("fooObj", onExecuteFooObj)
+    )
+  );
+}
+
+
+function onExecuteFooObj()
+{
+  let msg = gWebConsole.outputNode.querySelector(".webconsole-msg-output");
+  ok(msg, "output message found");
+  isnot(msg.textContent.indexOf("[object Object]"), -1, "message text check");
+
+  gJSTerm.once("variablesview-fetched", onFooObjFetch);
+
+  executeSoon(() => EventUtils.synthesizeMouse(msg, 2, 2, {},
+                                               gWebConsole.iframeWindow));
+}
+
+function onFooObjFetch(aEvent, aVar)
+{
+  gVariablesView = aVar._variablesView;
+  ok(gVariablesView, "variables view object");
+
+  findVariableViewProperties(aVar, [
+    { name: "testProp2", value: "testValue2" },
+    { name: "testProp", value: "testValue", dontMatch: true },
+  ], { webconsole: gWebConsole }).then(onTestPropFound);
+}
+
+function onTestPropFound(aResults)
+{
+  let prop = aResults[0].matchedProp;
+  ok(prop, "matched the |testProp2| property in the variables view");
+
+  // Check that property value updates work and that jsterm functions can be
+  // used.
+  updateVariablesViewProperty({
+    property: prop,
+    field: "value",
+    string: "document.title + foo2 + $('p')",
+    webconsole: gWebConsole,
+    callback: onFooObjFetchAfterUpdate,
+  });
+}
+
+function onFooObjFetchAfterUpdate(aEvent, aVar)
+{
+  info("onFooObjFetchAfterUpdate");
+  let para = content.document.querySelector("p");
+  let expectedValue = content.document.title + "foo2SecondCall" + para;
+
+  findVariableViewProperties(aVar, [
+    { name: "testProp2", value: expectedValue },
+  ], { webconsole: gWebConsole }).then(onUpdatedTestPropFound);
+}
+
+function onUpdatedTestPropFound(aResults)
+{
+  let prop = aResults[0].matchedProp;
+  ok(prop, "matched the updated |testProp2| property value");
+
+  // Check that testProp2 was updated.
+  executeSoon(() => gJSTerm.execute("fooObj.testProp2", onExecuteFooObjTestProp2));
+}
+
+function onExecuteFooObjTestProp2()
+{
+  let para = content.document.querySelector("p");
+  let expected = content.document.title + "foo2SecondCall" + para;
+
+  isnot(gWebConsole.outputNode.textContent.indexOf(expected), -1,
+        "fooObj.testProp2 is correct");
+
+  gWebConsole = gJSTerm = gDebuggerWin = gThread = gDebuggerController =
+    gStackframes = gVariablesView = null;
+  executeSoon(finishTest);
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_eval_in_debugger_stackframe.js
@@ -0,0 +1,150 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Test that makes sure web console eval happens in the user-selected stackframe
+// from the js debugger.
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-eval-in-stackframe.html";
+
+let gWebConsole, gJSTerm, gDebuggerWin, gThread, gDebuggerController, gStackframes;
+
+function test()
+{
+  addTab(TEST_URI);
+  browser.addEventListener("load", function onLoad() {
+    browser.removeEventListener("load", onLoad, true);
+    openConsole(null, consoleOpened);
+  }, true);
+}
+
+function consoleOpened(hud)
+{
+  gWebConsole = hud;
+  gJSTerm = hud.jsterm;
+  gJSTerm.execute("foo", onExecuteFoo);
+}
+
+function onExecuteFoo()
+{
+  isnot(gWebConsole.outputNode.textContent.indexOf("globalFooBug783499"), -1,
+        "|foo| value is correct");
+
+  gJSTerm.clearOutput();
+
+  // Test for Bug 690529 - Web Console and Scratchpad should evaluate
+  // expressions in the scope of the content window, not in a sandbox.
+  executeSoon(() => gJSTerm.execute("foo2 = 'newFoo'; window.foo2", onNewFoo2));
+}
+
+function onNewFoo2()
+{
+  is(gWebConsole.outputNode.textContent.indexOf("undefined"), -1,
+     "|undefined| is not displayed after adding |foo2|");
+
+  let msg = gWebConsole.outputNode.querySelector(".webconsole-msg-output");
+  ok(msg, "output result found");
+
+  isnot(msg.textContent.indexOf("newFoo"), -1,
+        "'newFoo' is displayed after adding |foo2|");
+
+  gJSTerm.clearOutput();
+
+  info("openDebugger");
+  executeSoon(() => openDebugger().then(debuggerOpened));
+}
+
+function debuggerOpened(aResult)
+{
+  gDebuggerWin = aResult.panelWin;
+  gDebuggerController = gDebuggerWin.DebuggerController;
+  gThread = gDebuggerController.activeThread;
+  gStackframes = gDebuggerController.StackFrames;
+
+  info("openConsole");
+  executeSoon(() =>
+    openConsole(null, () =>
+      gJSTerm.execute("foo + foo2", onExecuteFooAndFoo2)
+    )
+  );
+}
+
+function onExecuteFooAndFoo2()
+{
+  let expected = "globalFooBug783499newFoo";
+  isnot(gWebConsole.outputNode.textContent.indexOf(expected), -1,
+        "|foo + foo2| is displayed after starting the debugger");
+
+  executeSoon(() => {
+    gJSTerm.clearOutput();
+
+    info("openDebugger");
+    openDebugger().then(() => {
+      gThread.addOneTimeListener("framesadded", onFramesAdded);
+
+      info("firstCall()");
+      content.wrappedJSObject.firstCall();
+    });
+  });
+}
+
+function onFramesAdded()
+{
+  info("onFramesAdded, openConsole() now");
+  executeSoon(() =>
+    openConsole(null, () =>
+      gJSTerm.execute("foo + foo2", onExecuteFooAndFoo2InSecondCall)
+    )
+  );
+}
+
+function onExecuteFooAndFoo2InSecondCall()
+{
+  let expected = "globalFooBug783499foo2SecondCall";
+  isnot(gWebConsole.outputNode.textContent.indexOf(expected), -1,
+        "|foo + foo2| from |secondCall()|");
+
+  executeSoon(() => {
+    gJSTerm.clearOutput();
+
+    info("openDebugger and selectFrame(1)");
+
+    openDebugger().then(() => {
+      gStackframes.selectFrame(1);
+
+      info("openConsole");
+      executeSoon(() =>
+        openConsole(null, () =>
+          gJSTerm.execute("foo + foo2 + foo3", onExecuteFoo23InFirstCall)
+        )
+      );
+    });
+  });
+}
+
+function onExecuteFoo23InFirstCall()
+{
+  let expected = "fooFirstCallnewFoofoo3FirstCall";
+  isnot(gWebConsole.outputNode.textContent.indexOf(expected), -1,
+        "|foo + foo2 + foo3| from |firstCall()|");
+
+  executeSoon(() =>
+    gJSTerm.execute("foo = 'abba'; foo3 = 'bug783499'; foo + foo3",
+                    onExecuteFooAndFoo3ChangesInFirstCall));
+}
+
+function onExecuteFooAndFoo3ChangesInFirstCall()
+{
+  let expected = "abbabug783499";
+  isnot(gWebConsole.outputNode.textContent.indexOf(expected), -1,
+        "|foo + foo3| updated in |firstCall()|");
+
+  is(content.wrappedJSObject.foo, "globalFooBug783499", "|foo| in content window");
+  is(content.wrappedJSObject.foo2, "newFoo", "|foo2| in content window");
+  ok(!content.wrappedJSObject.foo3, "|foo3| was not added to the content window");
+
+  gWebConsole = gJSTerm = gDebuggerWin = gThread = gDebuggerController =
+    gStackframes = null;
+  executeSoon(finishTest);
+}
--- a/browser/devtools/webconsole/test/browser_output_breaks_after_console_dir_uninspectable.js
+++ b/browser/devtools/webconsole/test/browser_output_breaks_after_console_dir_uninspectable.js
@@ -16,23 +16,24 @@ function test()
     openConsole(null, performTest);
   }, true);
 }
 
 function performTest(hud)
 {
   hud.jsterm.clearOutput(true);
 
-  content.console.log("fooBug773466a");
-  content.console.dir(function funBug773466(){});
+  hud.jsterm.execute("console.log('fooBug773466a')");
+  hud.jsterm.execute("myObj = Object.create(null)");
+  hud.jsterm.execute("console.dir(myObj)");
   waitForSuccess({
     name: "eval results are shown",
     validatorFn: function()
     {
-      return hud.outputNode.textContent.indexOf("funBug773466") > -1;
+      return hud.outputNode.querySelector(".webconsole-msg-inspector");
     },
     successFn: function()
     {
       isnot(hud.outputNode.textContent.indexOf("fooBug773466a"), -1,
             "fooBug773466a shows");
       ok(hud.outputNode.querySelector(".webconsole-msg-inspector"),
          "the console.dir() tree shows");
 
--- a/browser/devtools/webconsole/test/browser_result_format_as_string.js
+++ b/browser/devtools/webconsole/test/browser_result_format_as_string.js
@@ -32,18 +32,18 @@ function performTest(hud)
     successFn: function()
     {
       is(hud.outputNode.textContent.indexOf("bug772506_content"), -1,
             "no content element found");
       ok(!hud.outputNode.querySelector("div"), "no div element found");
 
       let msg = hud.outputNode.querySelector(".webconsole-msg-output");
       ok(msg, "eval output node found");
-      isnot(msg.textContent.indexOf("HTMLDivElement"), -1,
-            "HTMLDivElement string found");
+      is(msg.textContent.indexOf("HTMLDivElement"), -1,
+         "HTMLDivElement string not displayed");
       EventUtils.synthesizeMouseAtCenter(msg, {type: "mousemove"});
       ok(!gBrowser._bug772506, "no content variable");
 
       finishTest();
     },
     failureFn: finishTest,
   });
 }
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_594477_clickable_output.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_594477_clickable_output.js
@@ -44,16 +44,18 @@ function tabLoad2(aEvent) {
     },
     failureFn: finishTest,
   });
 }
 
 function networkPanelShown(aEvent) {
   document.removeEventListener(aEvent.type, networkPanelShown, false);
 
+  info("networkPanelShown");
+
   document.addEventListener("popupshown", networkPanelShowFailure, false);
 
   // The network panel should not open for the second time.
   EventUtils.sendMouseEvent({type: "mousedown"}, outputItem);
   EventUtils.sendMouseEvent({type: "click"}, outputItem);
 
   executeSoon(function() {
     aEvent.target.addEventListener("popuphidden", networkPanelHidden, false);
@@ -65,16 +67,18 @@ function networkPanelShowFailure(aEvent)
   document.removeEventListener(aEvent.type, networkPanelShowFailure, false);
 
   ok(false, "the network panel should not show");
 }
 
 function networkPanelHidden(aEvent) {
   this.removeEventListener(aEvent.type, networkPanelHidden, false);
 
+  info("networkPanelHidden");
+
   // The network panel should not show because this is a mouse event that starts
   // in a position and ends in another.
   EventUtils.sendMouseEvent({type: "mousedown", clientX: 3, clientY: 4},
     outputItem);
   EventUtils.sendMouseEvent({type: "click", clientX: 5, clientY: 6},
     outputItem);
 
   // The network panel should not show because this is a middle-click.
@@ -89,83 +93,35 @@ function networkPanelHidden(aEvent) {
   EventUtils.sendMouseEvent({type: "click", button: 2},
     outputItem);
 
   executeSoon(function() {
     document.removeEventListener("popupshown", networkPanelShowFailure, false);
 
     // Done with the network output. Now test the jsterm output and the property
     // panel.
-    HUD.jsterm.setInputValue("document");
-    HUD.jsterm.execute();
+    HUD.jsterm.execute("document", () => {
+      info("jsterm execute 'document' callback");
 
-    waitForSuccess({
-      name: "jsterm output message",
-      validatorFn: function()
-      {
-        return outputNode.querySelector(".webconsole-msg-output .hud-clickable");
-      },
-      successFn: function()
-      {
-        document.addEventListener("popupshown", propertyPanelShown, false);
+      HUD.jsterm.once("variablesview-open", onVariablesViewOpen);
+      let outputItem = outputNode
+                       .querySelector(".webconsole-msg-output .hud-clickable");
+      ok(outputItem, "jsterm output message found");
 
-        // Send the mousedown and click events such that the property panel opens.
-        EventUtils.sendMouseEvent({type: "mousedown"}, outputItem);
-        EventUtils.sendMouseEvent({type: "click"}, outputItem);
-      },
-      failureFn: finishTest,
+      // Send the mousedown and click events such that the property panel opens.
+      EventUtils.sendMouseEvent({type: "mousedown"}, outputItem);
+      EventUtils.sendMouseEvent({type: "click"}, outputItem);
     });
   });
 }
 
-function propertyPanelShown(aEvent) {
-  document.removeEventListener(aEvent.type, propertyPanelShown, false);
-
-  document.addEventListener("popupshown", propertyPanelShowFailure, false);
-
-  // The property panel should not open for the second time.
-  EventUtils.sendMouseEvent({type: "mousedown"}, outputItem);
-  EventUtils.sendMouseEvent({type: "click"}, outputItem);
+function onVariablesViewOpen() {
+  info("onVariablesViewOpen");
 
   executeSoon(function() {
-    aEvent.target.addEventListener("popuphidden", propertyPanelHidden, false);
-    aEvent.target.hidePopup();
-  });
-}
-
-function propertyPanelShowFailure(aEvent) {
-  document.removeEventListener(aEvent.type, propertyPanelShowFailure, false);
-
-  ok(false, "the property panel should not show");
-}
-
-function propertyPanelHidden(aEvent) {
-  this.removeEventListener(aEvent.type, propertyPanelHidden, false);
-
-  // The property panel should not show because this is a mouse event that
-  // starts in a position and ends in another.
-  EventUtils.sendMouseEvent({type: "mousedown", clientX: 3, clientY: 4},
-    outputItem);
-  EventUtils.sendMouseEvent({type: "click", clientX: 5, clientY: 6},
-    outputItem);
-
-  // The property panel should not show because this is a middle-click.
-  EventUtils.sendMouseEvent({type: "mousedown", button: 1},
-    outputItem);
-  EventUtils.sendMouseEvent({type: "click", button: 1},
-    outputItem);
-
-  // The property panel should not show because this is a right-click.
-  EventUtils.sendMouseEvent({type: "mousedown", button: 2},
-    outputItem);
-  EventUtils.sendMouseEvent({type: "click", button: 2},
-    outputItem);
-
-  executeSoon(function() {
-    document.removeEventListener("popupshown", propertyPanelShowFailure, false);
     HUD = outputItem = null;
     executeSoon(finishTest);
   });
 }
 
 function test() {
   addTab(TEST_URI);
   browser.addEventListener("load", function onLoad() {
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_595350_multiple_windows_and_tabs.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_595350_multiple_windows_and_tabs.js
@@ -14,16 +14,18 @@
 
 const TEST_URI = "data:text/html;charset=utf-8,Web Console test for bug 595350";
 
 let win1 = window, win2;
 let openTabs = [];
 let loadedTabCount = 0;
 
 function test() {
+  requestLongerTimeout(2);
+
   // Add two tabs in the main window.
   addTabs(win1);
 
   // Open a new window.
   win2 = OpenBrowserWindow();
   win2.addEventListener("load", onWindowLoad, true);
 }
 
@@ -59,17 +61,17 @@ function openConsoles() {
       ok(hud, "HUD is open for tab " + index);
       let window = hud.target.tab.linkedBrowser.contentWindow;
       window.console.log("message for tab " + index);
       consolesOpen++;
     }.bind(null, i));
   }
 
   waitForSuccess({
-    timeout: 10000,
+    timeout: 15000,
     name: "4 web consoles opened",
     validatorFn: function()
     {
       return consolesOpen == 4;
     },
     successFn: closeConsoles,
     failureFn: closeConsoles,
   });
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_598357_jsterm_output.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_598357_jsterm_output.js
@@ -19,21 +19,21 @@ let tempScope = {};
 Cu.import("resource://gre/modules/devtools/dbg-server.jsm", tempScope);
 
 let longString = (new Array(tempScope.DebuggerServer.LONG_STRING_LENGTH + 4)).join("a");
 let initialString = longString.substring(0,
   tempScope.DebuggerServer.LONG_STRING_INITIAL_LENGTH);
 
 let inputValues = [
   // [showsPropertyPanel?, input value, expected output format,
-  //    print() output, console output, optional console API test]
+  //    print() output, console API output, optional console API test]
 
   // 0
   [false, "'hello \\nfrom \\rthe \\\"string world!'",
-    '"hello \\nfrom \\rthe \\"string world!"',
+    '"hello \nfrom \rthe "string world!"',
     "hello \nfrom \rthe \"string world!"],
 
   // 1
   [false, "'\xFA\u1E47\u0129\xE7\xF6d\xEA \u021B\u0115\u0219\u0165'",
     "\"\xFA\u1E47\u0129\xE7\xF6d\xEA \u021B\u0115\u0219\u0165\"",
     "\xFA\u1E47\u0129\xE7\xF6d\xEA \u021B\u0115\u0219\u0165"],
 
   // 2
@@ -47,58 +47,52 @@ let inputValues = [
 
   // 5
   [false, "42", "42"],
 
   // 6
   [false, "'42'", '"42"', "42"],
 
   // 7
-  [false, "/foobar/", "/foobar/"],
+  [true, "/foobar/", "[object RegExp]", '"/foobar/"', "[object RegExp]"],
 
   // 8
   [false, "null", "null"],
 
   // 9
   [false, "undefined", "undefined"],
 
   // 10
   [false, "true", "true"],
 
   // 11
-  [false, "document.getElementById", "function getElementById() {\n    [native code]\n}",
+  [true, "document.getElementById", "[object Function]",
     "function getElementById() {\n    [native code]\n}",
-    "function getElementById() {\n    [native code]\n}",
-    "document.wrappedJSObject.getElementById"],
+    "[object Function]"],
 
   // 12
-  [false, "(function() { return 42; })", "function () { return 42; }",
-    "function () { return 42; }",
-    "(function () { return 42; })"],
+  [true, "(function() { return 42; })", "[object Function]",
+    "function () { return 42; }", "[object Function]"],
 
   // 13
-  [false, "new Date(" + dateNow + ")", (new Date(dateNow)).toString()],
+  [true, "new Date(" + dateNow + ")", "[object Date]", (new Date(dateNow)).toString(), "[object Date]"],
 
   // 14
-  [true, "document.body", "[object HTMLBodyElement", "[object HTMLBodyElement",
-    "[object HTMLBodyElement",
-    "document.wrappedJSObject.body"],
+  [true, "document.body", "[object HTMLBodyElement]"],
 
   // 15
-  [true, "window.location", TEST_URI],
+  [true, "window.location", "[object Location]", TEST_URI, "[object Location]"],
 
   // 16
-  [true, "[1,2,3,'a','b','c','4','5']", '[1, 2, 3, "a", "b", "c", "4", "5"]',
+  [true, "[1,2,3,'a','b','c','4','5']", '[object Array]',
     '1,2,3,a,b,c,4,5',
-    '[1, 2, 3, "a", "b", "c", "4", "5"]'],
+    "[object Array]"],
 
   // 17
-  [true, "({a:'b', c:'d', e:1, f:'2'})", '({a:"b", c:"d", e:1, f:"2"})',
-    "[object Object",
-    '({a:"b", c:"d", e:1, f:"2"})'],
+  [true, "({a:'b', c:'d', e:1, f:'2'})", "[object Object]"],
 
   // 18
   [false, "'" + longString + "'",
     '"' + initialString + "\"[\u2026]", initialString],
 ];
 
 longString = null;
 initialString = null;
@@ -149,20 +143,17 @@ function testGen() {
     inputValues[cpos][4] : printOutput;
 
   let consoleTest = inputValues[cpos][5] || inputValue;
 
   HUD.jsterm.clearOutput();
 
   // Test the console.log() output.
 
-  // Ugly but it does the job.
-  with (content) {
-    eval("content.console.log(" + consoleTest + ")");
-  }
+  HUD.jsterm.execute("console.log(" + consoleTest + ")");
 
   waitForSuccess({
     name: "console.log message for test #" + cpos,
     validatorFn: function()
     {
       return HUD.outputNode.querySelector(".hud-log");
     },
     successFn: subtestNext,
@@ -228,38 +219,38 @@ function testGen() {
     "jsterm output is correct for inputValues[" + cpos + "]");
 
   let messageBody = outputItem.querySelector(".webconsole-msg-body");
   ok(messageBody, "we have the message body for inputValues[" + cpos + "]");
 
   // Test click on output.
   let eventHandlerID = eventHandlers.length + 1;
 
-  let propertyPanelShown = function(aEvent) {
-    let label = aEvent.target.getAttribute("label");
-    if (!label || label.indexOf(inputValue) == -1) {
+  let propertyPanelShown = function(aEvent, aView, aOptions) {
+    if (aOptions.label.indexOf(expectedOutput) == -1) {
       return;
     }
 
-    document.removeEventListener(aEvent.type, propertyPanelShown, false);
+    HUD.jsterm.off("variablesview-open", propertyPanelShown);
+
     eventHandlers[eventHandlerID] = null;
 
     ok(showsPropertyPanel,
       "the property panel shown for inputValues[" + cpos + "]");
 
-    aEvent.target.hidePopup();
+    HUD.jsterm._splitter.state = "collapsed";
 
     popupShown[cpos] = true;
 
     if (showsPropertyPanel) {
-      subtestNext();
+      executeSoon(subtestNext);
     }
   };
 
-  document.addEventListener("popupshown", propertyPanelShown, false);
+  HUD.jsterm.on("variablesview-open", propertyPanelShown);
 
   eventHandlers.push(propertyPanelShown);
 
   // Send the mousedown, mouseup and click events to check if the property
   // panel opens.
   EventUtils.sendMouseEvent({ type: "mousedown" }, messageBody, window);
   EventUtils.sendMouseEvent({ type: "click" }, messageBody, window);
 
@@ -276,27 +267,28 @@ function testEnd() {
   if (testEnded) {
     return;
   }
 
   testEnded = true;
 
   for (let i = 0; i < eventHandlers.length; i++) {
     if (eventHandlers[i]) {
-      document.removeEventListener("popupshown", eventHandlers[i], false);
+      HUD.jsterm.off("variablesview-open", eventHandlers[i]);
     }
   }
 
   for (let i = 0; i < inputValues.length; i++) {
     if (inputValues[i][0] && !popupShown[i]) {
       ok(false, "the property panel failed to show for inputValues[" + i + "]");
     }
   }
 
   HUD = inputValues = testDriver = null;
   executeSoon(finishTest);
 }
 
 function test() {
+  requestLongerTimeout(2);
   addTab(TEST_URI);
   browser.addEventListener("load", tabLoad, true);
 }
 
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_600183_charset.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_600183_charset.js
@@ -8,31 +8,34 @@
  *
  * ***** END LICENSE BLOCK ***** */
 
 const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-600183-charset.html";
 
 function performTest(lastFinishedRequest, aConsole)
 {
   ok(lastFinishedRequest, "charset test page was loaded and logged");
+  HUDService.lastFinishedRequestCallback = null;
 
-  aConsole.webConsoleClient.getResponseContent(lastFinishedRequest.actor,
-    function (aResponse) {
-      ok(!aResponse.contentDiscarded, "response body was not discarded");
-
-      let body = aResponse.content.text;
-      ok(body, "we have the response body");
+  executeSoon(() => {
+    aConsole.webConsoleClient.getResponseContent(lastFinishedRequest.actor,
+      (aResponse) => {
+        ok(!aResponse.contentDiscarded, "response body was not discarded");
 
-      let chars = "\u7684\u95ee\u5019!"; // 的问候!
-      isnot(body.indexOf("<p>" + chars + "</p>"), -1,
-        "found the chinese simplified string");
-      executeSoon(finishTest);
-    });
+        let body = aResponse.content.text;
+        ok(body, "we have the response body");
 
-  HUDService.lastFinishedRequestCallback = null;
+        let chars = "\u7684\u95ee\u5019!"; // 的问候!
+        isnot(body.indexOf("<p>" + chars + "</p>"), -1,
+          "found the chinese simplified string");
+
+        HUDService.lastFinishedRequestCallback = null;
+        executeSoon(finishTest);
+      });
+  });
 }
 
 function test()
 {
   addTab("data:text/html;charset=utf-8,Web Console - bug 600183 test");
 
   browser.addEventListener("load", function onLoad() {
     browser.removeEventListener("load", onLoad, true);
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_601177_log_levels.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_601177_log_levels.js
@@ -30,17 +30,17 @@ function performTest()
             "found exception");
 
   findEntry(HUD, "hud-jswarn", "undefinedPropertyBug601177",
             "found strict warning");
 
   findEntry(HUD, "hud-jswarn", "foobarBug601177strictError",
             "found strict error");
 
-  finishTest();
+  executeSoon(finishTest);
 }
 
 function findEntry(aHUD, aClass, aString, aMessage)
 {
   return testLogEntry(aHUD.outputNode, aString, aMessage, false, false,
                       aClass);
 }
 
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_611795.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_611795.js
@@ -54,24 +54,28 @@ function testConsoleLogRepeats()
   let outputNode = HUD.outputNode;
 
   jsterm.clearOutput();
 
   jsterm.setInputValue("for (let i = 0; i < 10; ++i) console.log('this is a line of reasonably long text that I will use to verify that the repeated text node is of an appropriate size.');");
   jsterm.execute();
 
   waitForSuccess({
+    timeout: 10000,
     name: "10 repeated console.log messages",
     validatorFn: function()
     {
       let node = outputNode.querySelector(".webconsole-msg-console");
       return node && node.childNodes[3].firstChild.getAttribute("value") == 10;
     },
     successFn: finishTest,
-    failureFn: finishTest,
+    failureFn: function() {
+      info("output content: " + outputNode.textContent);
+      finishTest();
+    },
   });
 }
 
 /**
  * Unit test for bug 611795:
  * Repeated CSS messages get collapsed into one.
  */
 function test()
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_618311_close_panels.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_618311_close_panels.js
@@ -25,86 +25,66 @@ function test() {
     });
   }, true);
 }
 
 function performTest() {
   let hudId = HUDService.getHudIdByWindow(content);
   let HUD = HUDService.hudReferences[hudId];
 
-  HUD.jsterm.execute("document");
-
   let networkMessage = HUD.outputNode.querySelector(".webconsole-msg-network");
   ok(networkMessage, "found network message");
 
   let networkLink = networkMessage.querySelector(".webconsole-msg-link");
   ok(networkLink, "found network message link");
 
   let popupset = document.getElementById("mainPopupSet");
   ok(popupset, "found #mainPopupSet");
 
   let popupsShown = 0;
   let hiddenPopups = 0;
 
   let onpopupshown = function() {
+    document.removeEventListener("popupshown", onpopupshown, false);
     popupsShown++;
-    if (popupsShown == 2) {
-      document.removeEventListener("popupshown", onpopupshown, false);
 
-      executeSoon(function() {
-        let popups = popupset.querySelectorAll("panel[hudId=" + hudId + "]");
-        is(popups.length, 2, "found two popups");
+    executeSoon(function() {
+      let popups = popupset.querySelectorAll("panel[hudId=" + hudId + "]");
+      is(popups.length, 1, "found one popup");
 
-        document.addEventListener("popuphidden", onpopuphidden, false);
+      document.addEventListener("popuphidden", onpopuphidden, false);
 
-        registerCleanupFunction(function() {
-          is(hiddenPopups, 2, "correct number of popups hidden");
-          if (hiddenPopups != 2) {
-            document.removeEventListener("popuphidden", onpopuphidden, false);
-          }
-        });
+      registerCleanupFunction(function() {
+        is(hiddenPopups, 1, "correct number of popups hidden");
+        if (hiddenPopups != 1) {
+          document.removeEventListener("popuphidden", onpopuphidden, false);
+        }
+      });
 
-        executeSoon(closeConsole);
-      });
-    }
+      executeSoon(closeConsole);
+    });
   };
 
   let onpopuphidden = function() {
+    document.removeEventListener("popuphidden", onpopuphidden, false);
     hiddenPopups++;
-    if (hiddenPopups == 2) {
-      document.removeEventListener("popuphidden", onpopuphidden, false);
 
-      executeSoon(function() {
-        let popups = popupset.querySelectorAll("panel[hudId=" + hudId + "]");
-        is(popups.length, 0, "no popups found");
+    executeSoon(function() {
+      let popups = popupset.querySelectorAll("panel[hudId=" + hudId + "]");
+      is(popups.length, 0, "no popups found");
 
-        executeSoon(finishTest);
-      });
-    }
+      executeSoon(finishTest);
+    });
   };
 
   document.addEventListener("popupshown", onpopupshown, false);
 
   registerCleanupFunction(function() {
-    is(popupsShown, 2, "correct number of popups shown");
-    if (popupsShown != 2) {
+    is(popupsShown, 1, "correct number of popups shown");
+    if (popupsShown != 1) {
       document.removeEventListener("popupshown", onpopupshown, false);
     }
   });
 
-  waitForSuccess({
-    name: "jsterm output message",
-    validatorFn: function()
-    {
-      return HUD.outputNode.querySelector(".webconsole-msg-output");
-    },
-    successFn: function()
-    {
-      let jstermMessage = HUD.outputNode.querySelector(".webconsole-msg-output");
-      EventUtils.sendMouseEvent({ type: "mousedown" }, jstermMessage, HUD.iframeWindow);
-      EventUtils.sendMouseEvent({ type: "click" }, jstermMessage, HUD.iframeWindow);
-      EventUtils.sendMouseEvent({ type: "mousedown" }, networkLink, HUD.iframeWindow);
-      EventUtils.sendMouseEvent({ type: "mouseup" }, networkLink, HUD.iframeWindow);
-      EventUtils.sendMouseEvent({ type: "click" }, networkLink, HUD.iframeWindow);
-    },
-    failureFn: finishTest,
-  });
+  EventUtils.sendMouseEvent({ type: "mousedown" }, networkLink, HUD.iframeWindow);
+  EventUtils.sendMouseEvent({ type: "mouseup" }, networkLink, HUD.iframeWindow);
+  EventUtils.sendMouseEvent({ type: "click" }, networkLink, HUD.iframeWindow);
 }
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_632275_getters_document_width.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_632275_getters_document_width.js
@@ -1,59 +1,44 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-632275-getters.html";
 
+let getterValue = null;
+
 function test() {
   addTab(TEST_URI);
   browser.addEventListener("load", function onLoad() {
     browser.removeEventListener("load", onLoad, true);
     openConsole(null, consoleOpened);
   }, true);
 }
 
-function consoleOpened(HUD) {
-  let jsterm = HUD.jsterm;
+function consoleOpened(hud) {
+  let doc = content.wrappedJSObject.document;
+  getterValue = doc.foobar._val;
+  hud.jsterm.execute("console.dir(document)");
 
+  let onOpen = onViewOpened.bind(null, hud);
+  hud.jsterm.once("variablesview-fetched", onOpen);
+}
+
+function onViewOpened(hud, event, view)
+{
   let doc = content.wrappedJSObject.document;
 
-  let panel = jsterm.openPropertyPanel({ data: { object: doc }});
-
-  let view = panel.treeView;
-  let find = function(regex) {
-    for (let i = 0; i < view.rowCount; i++) {
-      if (regex.test(view.getCellText(i))) {
-        return true;
-      }
-    }
-    return false;
-  };
-
-  ok(!find(/^(width|height):/), "no document.width/height");
-
-  panel.destroy();
-
-  let getterValue = doc.foobar._val;
+  findVariableViewProperties(view, [
+    { name: /^(width|height)$/, dontMatch: 1 },
+    { name: "foobar._val", value: getterValue },
+    { name: "foobar.val", isGetter: true },
+  ], { webconsole: hud }).then(function() {
+    is(doc.foobar._val, getterValue, "getter did not execute");
+    is(doc.foobar.val, getterValue+1, "getter executed");
+    is(doc.foobar._val, getterValue+1, "getter executed (recheck)");
 
-  panel = jsterm.openPropertyPanel({ data: { object: doc.foobar }});
-  view = panel.treeView;
-
-  is(getterValue, doc.foobar._val, "getter did not execute");
-  is(getterValue+1, doc.foobar.val, "getter executed");
-  is(getterValue+1, doc.foobar._val, "getter executed (recheck)");
-
-  ok(find(/^val: Getter$/),
-     "getter is properly displayed");
-
-  ok(find(new RegExp("^_val: " + getterValue + "$")),
-     "getter _val is properly displayed");
-
-  panel.destroy();
-
-  executeSoon(function() {
-    let textContent = HUD.outputNode.textContent;
+    let textContent = hud.outputNode.textContent;
     is(textContent.indexOf("document.body.client"), -1,
        "no document.width/height warning displayed");
 
     finishTest();
   });
 }
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js
@@ -21,104 +21,78 @@ function consoleOpened(HUD) {
   tmp = null;
 
   let jsterm = HUD.jsterm;
   let win = content.wrappedJSObject;
 
   // Make sure autocomplete does not walk through iterators and generators.
   let result = win.gen1.next();
   let completion = JSPropertyProvider(win, "gen1.");
-  is(completion, null, "no matchees for gen1");
+  is(completion, null, "no matches for gen1");
   ok(!WCU.isObjectInspectable(win.gen1),
      "gen1 is not inspectable");
 
   is(result+1, win.gen1.next(), "gen1.next() did not execute");
 
   result = win.gen2.next();
 
   completion = JSPropertyProvider(win, "gen2.");
-  is(completion, null, "no matchees for gen2");
+  is(completion, null, "no matches for gen2");
   ok(!WCU.isObjectInspectable(win.gen2),
      "gen2 is not inspectable");
 
   is((result/2+1)*2, win.gen2.next(),
      "gen2.next() did not execute");
 
   result = win.iter1.next();
   is(result[0], "foo", "iter1.next() [0] is correct");
   is(result[1], "bar", "iter1.next() [1] is correct");
 
   completion = JSPropertyProvider(win, "iter1.");
-  is(completion, null, "no matchees for iter1");
+  is(completion, null, "no matches for iter1");
   ok(!WCU.isObjectInspectable(win.iter1),
      "iter1 is not inspectable");
 
   result = win.iter1.next();
   is(result[0], "baz", "iter1.next() [0] is correct");
   is(result[1], "baaz", "iter1.next() [1] is correct");
 
   completion = JSPropertyProvider(content, "iter2.");
-  is(completion, null, "no matchees for iter2");
+  is(completion, null, "no matches for iter2");
   ok(!WCU.isObjectInspectable(win.iter2),
      "iter2 is not inspectable");
 
   completion = JSPropertyProvider(win, "window.");
   ok(completion, "matches available for window");
   ok(completion.matches.length, "matches available for window (length)");
   ok(WCU.isObjectInspectable(win),
      "window is inspectable");
 
   jsterm.clearOutput();
 
-  jsterm.setInputValue("window");
-  jsterm.execute();
+  jsterm.execute("window");
 
   waitForSuccess({
     name: "jsterm window object output",
     validatorFn: function()
     {
       return HUD.outputNode.querySelector(".webconsole-msg-output");
     },
     successFn: function()
     {
-      document.addEventListener("popupshown", function onShown(aEvent) {
-        document.removeEventListener("popupshown", onShown, false);
-        executeSoon(testPropertyPanel.bind(null, aEvent.target));
-      }, false);
-
+      jsterm.once("variablesview-fetched", testVariablesView.bind(null, HUD));
       let node = HUD.outputNode.querySelector(".webconsole-msg-output");
       EventUtils.synthesizeMouse(node, 2, 2, {}, HUD.iframeWindow);
     },
     failureFn: finishTest,
   });
 }
 
-function testPropertyPanel(aPanel) {
-  let tree = aPanel.querySelector("tree");
-  let view = tree.view;
-  let col = tree.columns[0];
-  ok(view.rowCount, "Property Panel rowCount");
-
-  let find = function(display, children) {
-    for (let i = 0; i < view.rowCount; i++) {
-      if (view.isContainer(i) == children &&
-          view.getCellText(i, col) == display) {
-        return true;
-      }
-    }
-
-    return false;
-  };
-
-  ok(find("gen1: Generator", false),
-     "gen1 is correctly displayed in the Property Panel");
-
-  ok(find("gen2: Generator", false),
-     "gen2 is correctly displayed in the Property Panel");
-
-  ok(find("iter1: Iterator", false),
-     "iter1 is correctly displayed in the Property Panel");
-
-  ok(find("iter2: Object", false),
-     "iter2 is correctly displayed in the Property Panel");
-
-  executeSoon(finishTest);
+function testVariablesView(aWebconsole, aEvent, aView) {
+  findVariableViewProperties(aView, [
+    { name: "gen1", isGenerator: true },
+    { name: "gen2", isGenerator: true },
+    { name: "iter1", isIterator: true },
+    { name: "iter2", isIterator: true },
+  ], { webconsole: aWebconsole }).then(function() {
+    executeSoon(finishTest);
+  });
 }
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_651501_document_body_autocomplete.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_651501_document_body_autocomplete.js
@@ -9,17 +9,17 @@
 function test() {
   addTab("data:text/html;charset=utf-8,Web Console autocompletion bug in document.body");
   browser.addEventListener("load", function onLoad() {
     browser.removeEventListener("load", onLoad, true);
     openConsole(null, consoleOpened);
   }, true);
 }
 
-var gHUD;
+let gHUD;
 
 function consoleOpened(aHud) {
   gHUD = aHud;
   let jsterm = gHUD.jsterm;
   let popup = jsterm.autocompletePopup;
   let completeNode = jsterm.completeNode;
 
   let tmp = {};
@@ -76,62 +76,33 @@ function autocompletePopupHidden()
     failureFn: finishTest,
   });
 }
 
 function testPropertyPanel()
 {
   let jsterm = gHUD.jsterm;
   jsterm.clearOutput();
-  jsterm.setInputValue("document");
-  jsterm.execute();
+  jsterm.execute("document");
 
   waitForSuccess({
     name: "jsterm document object output",
     validatorFn: function()
     {
       return gHUD.outputNode.querySelector(".webconsole-msg-output");
     },
     successFn: function()
     {
-      document.addEventListener("popupshown", function onShown(aEvent) {
-        document.removeEventListener("popupshown", onShown, false);
-        executeSoon(propertyPanelShown.bind(null, aEvent.target));
-      }, false);
-
+      jsterm.once("variablesview-fetched", onVariablesViewReady);
       let node = gHUD.outputNode.querySelector(".webconsole-msg-output");
       EventUtils.synthesizeMouse(node, 2, 2, {}, gHUD.iframeWindow);
     },
     failureFn: finishTest,
   });
 }
 
-function propertyPanelShown(aPanel)
+function onVariablesViewReady(aEvent, aView)
 {
-  let tree = aPanel.querySelector("tree");
-  let view = tree.view;
-  let col = tree.columns[0];
-  ok(view.rowCount, "Property Panel rowCount");
-
-  let foundBody = false;
-  let propPanelProps = [];
-  for (let idx = 0; idx < view.rowCount; ++idx) {
-    let text = view.getCellText(idx, col);
-    if (text == "body: HTMLBodyElement" || text == "body: Object")
-      foundBody = true;
-    propPanelProps.push(text.split(":")[0]);
-  }
-
-  // NB: We pull the properties off the prototype, rather than off object itself,
-  // so that expandos like |constructor|, which the propPanel can't see, are not
-  // included.
-  for (let prop in Object.getPrototypeOf(content.document).wrappedObject) {
-    if (prop == "inputEncoding") {
-      continue;
-    }
-    ok(propPanelProps.indexOf(prop) != -1, "Property |" + prop + "| should be reflected in propertyPanel");
-  }
-
-  ok(foundBody, "found document.body");
-
-  executeSoon(finishTest);
+  findVariableViewProperties(aView, [
+    { name: "__proto__.body", value: "[object HTMLBodyElement]" },
+  ], { webconsole: gHUD }).then(finishTest);
 }
 
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_659907_console_dir.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_659907_console_dir.js
@@ -11,46 +11,19 @@ function test() {
          "object with a dir method");
   browser.addEventListener("load", function onLoad(aEvent) {
     browser.removeEventListener(aEvent.type, onLoad, true);
     openConsole(null, consoleOpened);
   }, true);
 }
 
 function consoleOpened(hud) {
-  outputNode = hud.outputNode;
-  content.console.dir(content.document);
-  waitForSuccess({
-    name: "console.dir displayed",
-    validatorFn: function()
-    {
-      return outputNode.textContent.indexOf("[object HTMLDocument") > -1;
-    },
-    successFn: testConsoleDir.bind(null, outputNode),
-    failureFn: finishTest,
-  });
+  hud.jsterm.execute("console.dir(document)");
+  hud.jsterm.once("variablesview-fetched", testConsoleDir.bind(null, hud));
 }
 
-function testConsoleDir(outputNode) {
-  let msg = outputNode.querySelectorAll(".webconsole-msg-inspector");
-  is(msg.length, 1, "one message node displayed");
-  let view = msg[0].propertyTreeView;
-  let foundQSA = false;
-  let foundLocation = false;
-  let foundWrite = false;
-  for (let i = 0; i < view.rowCount; i++) {
-    let text = view.getCellText(i);
-    if (text == "querySelectorAll: function querySelectorAll()") {
-      foundQSA = true;
-    }
-    else if (text  == "location: Location") {
-      foundLocation = true;
-    }
-    else if (text  == "write: function write()") {
-      foundWrite = true;
-    }
-  }
-  ok(foundQSA, "found document.querySelectorAll");
-  ok(foundLocation, "found document.location");
-  ok(foundWrite, "found document.write");
-  msg = view = outputNode = null;
-  executeSoon(finishTest);
+function testConsoleDir(hud, ev, view) {
+  findVariableViewProperties(view, [
+    { name: "__proto__.querySelectorAll", value: "[object Function]" },
+    { name: "location", value: "[object Location]" },
+    { name: "__proto__.write", value: "[object Function]" },
+  ], { webconsole: hud }).then(finishTest);
 }
--- a/browser/devtools/webconsole/test/browser_webconsole_console_logging_api.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_console_logging_api.js
@@ -123,17 +123,17 @@ function testConsoleLoggingAPI(aMethod) 
   // test for multiple arguments.
   console[aMethod]("foo", "bar");
 
   waitForSuccess({
     name: "show both console arguments for " + aMethod,
     validatorFn: function()
     {
       let node = outputNode.querySelector(".hud-msg-node");
-      return node && /foo bar/.test(node.textContent);
+      return node && /"foo" "bar"/.test(node.textContent);
     },
     successFn: nextTest,
     failureFn: nextTest,
   });
 
   yield;
   testDriver.next();
   yield;
--- a/browser/devtools/webconsole/test/browser_webconsole_execution_scope.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_execution_scope.js
@@ -14,30 +14,30 @@ function test() {
     openConsole(null, testExecutionScope);
   }, true);
 }
 
 function testExecutionScope(hud) {
   let jsterm = hud.jsterm;
 
   jsterm.clearOutput();
-  jsterm.execute("window.location;");
+  jsterm.execute("window.location.href;");
 
   waitForSuccess({
     name: "jsterm execution output (two nodes)",
     validatorFn: function()
     {
       return jsterm.outputNode.querySelectorAll(".hud-msg-node").length == 2;
     },
     successFn: function()
     {
       let nodes = jsterm.outputNode.querySelectorAll(".hud-msg-node");
 
-      is(/window.location;/.test(nodes[0].textContent), true,
-        "'window.location;' written to output");
+      is(/window.location.href;/.test(nodes[0].textContent), true,
+        "'window.location.href;' written to output");
 
       isnot(nodes[1].textContent.indexOf(TEST_URI), -1,
         "command was executed in the window scope");
 
       executeSoon(finishTest);
     },
     failureFn: finishTest,
   });
--- a/browser/devtools/webconsole/test/browser_webconsole_jsterm.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_jsterm.js
@@ -130,17 +130,17 @@ function testJSTerm(hud)
       nextTest();
     },
     failureFn: nextTest,
   });
   yield;
 
   jsterm.clearOutput();
   jsterm.execute("pprint({b:2, a:1})");
-  checkResult("a: 1\n  b: 2", "pprint()", 1);
+  checkResult('"  b: 2\n  a: 1"', "pprint()", 1);
   yield;
 
   // check instanceof correctness, bug 599940
   jsterm.clearOutput();
   jsterm.execute("[] instanceof Array");
   checkResult("true", "[] instanceof Array == true", 1);
   yield;
 
@@ -166,17 +166,17 @@ function testJSTerm(hud)
   jsterm.clearOutput();
   jsterm.execute("keys(window)");
   checkResult(null, "keys(window)", 1);
   yield;
 
   // bug 614561
   jsterm.clearOutput();
   jsterm.execute("pprint('hi')");
-  checkResult('0: "h"\n  1: "i"', "pprint('hi')", 1);
+  checkResult('"  0: "h"\n  1: "i""', "pprint('hi')", 1);
   yield;
 
   // check that pprint(function) shows function source, bug 618344
   jsterm.clearOutput();
   jsterm.execute("pprint(print)");
   checkResult(function(nodes) {
     return nodes[0].textContent.indexOf("aOwner.helperResult") > -1;
   }, "pprint(function) shows source", 1);
--- a/browser/devtools/webconsole/test/browser_webconsole_output_order.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_output_order.js
@@ -29,16 +29,16 @@ function testOutputOrder(hud) {
     {
       return outputNode.querySelectorAll(".hud-msg-node").length == 3;
     },
     successFn: function()
     {
       let nodes = outputNode.querySelectorAll(".hud-msg-node");
       let executedStringFirst =
         /console\.log\('foo', 'bar'\);/.test(nodes[0].textContent);
-      let outputSecond = /foo bar/.test(nodes[2].textContent);
+      let outputSecond = /"foo" "bar"/.test(nodes[2].textContent);
       ok(executedStringFirst && outputSecond, "executed string comes first");
 
       finishTest();
     },
     failureFn: finishTest,
   });
 }
deleted file mode 100644
--- a/browser/devtools/webconsole/test/browser_webconsole_property_panel.js
+++ /dev/null
@@ -1,68 +0,0 @@
-/* vim:set ts=2 sw=2 sts=2 et: */
-/* 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/. */
-
-// Tests the functionality of the "property panel", which allows JavaScript
-// objects and DOM nodes to be inspected.
-
-const TEST_URI = "data:text/html;charset=utf8,<p>property panel test";
-
-function test() {
-  addTab(TEST_URI);
-  browser.addEventListener("load", function onLoad() {
-    browser.removeEventListener("load", onLoad, true);
-    openConsole(null, testPropertyPanel);
-  }, true);
-}
-
-function testPropertyPanel(hud) {
-  let jsterm = hud.jsterm;
-
-  let propPanel = jsterm.openPropertyPanel({
-    data: {
-      object: [
-        1,
-        /abc/,
-        null,
-        undefined,
-        function test() {},
-        {}
-      ]
-    }
-  });
-  is (propPanel.treeView.rowCount, 6, "six elements shown in propertyPanel");
-  propPanel.destroy();
-
-  propPanel = jsterm.openPropertyPanel({
-    data: {
-      object: {
-        "0.02": 0,
-        "0.01": 1,
-        "02":   2,
-        "1":    3,
-        "11":   4,
-        "1.2":  5,
-        "1.1":  6,
-        "foo":  7,
-        "bar":  8
-      }
-    }
-  });
-  is (propPanel.treeView.rowCount, 9, "nine elements shown in propertyPanel");
-
-  let view = propPanel.treeView;
-  is (view.getCellText(0), "0.01: 1", "1. element is okay");
-  is (view.getCellText(1), "0.02: 0", "2. element is okay");
-  is (view.getCellText(2), "1: 3",    "3. element is okay");
-  is (view.getCellText(3), "1.1: 6",  "4. element is okay");
-  is (view.getCellText(4), "1.2: 5",  "5. element is okay");
-  is (view.getCellText(5), "02: 2",   "6. element is okay");
-  is (view.getCellText(6), "11: 4",   "7. element is okay");
-  is (view.getCellText(7), "bar: 8",  "8. element is okay");
-  is (view.getCellText(8), "foo: 7",  "9. element is okay");
-  propPanel.destroy();
-
-  executeSoon(finishTest);
-}
-
--- a/browser/devtools/webconsole/test/head.js
+++ b/browser/devtools/webconsole/test/head.js
@@ -9,16 +9,17 @@ let HUDService = tempScope.HUDService;
 Cu.import("resource://gre/modules/devtools/WebConsoleUtils.jsm", tempScope);
 let WebConsoleUtils = tempScope.WebConsoleUtils;
 Cu.import("resource:///modules/devtools/gDevTools.jsm", tempScope);
 let gDevTools = tempScope.gDevTools;
 Cu.import("resource:///modules/devtools/Target.jsm", tempScope);
 let TargetFactory = tempScope.TargetFactory;
 Components.utils.import("resource://gre/modules/devtools/Console.jsm", tempScope);
 let console = tempScope.console;
+let Promise = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {}).Promise;
 
 const WEBCONSOLE_STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties";
 let WCU_l10n = new WebConsoleUtils.l10n(WEBCONSOLE_STRINGS_URI);
 
 function log(aMsg)
 {
   dump("*** WebConsoleTest: " + aMsg + "\n");
 }
@@ -138,17 +139,19 @@ function findLogEntry(aString)
  * @param function [aCallback]
  *        Optional function to invoke after the Web Console completes
  *        initialization (web-console-created).
  */
 function openConsole(aTab, aCallback = function() { })
 {
   let target = TargetFactory.forTab(aTab || tab);
   gDevTools.showToolbox(target, "webconsole").then(function(toolbox) {
-    aCallback(toolbox.getCurrentPanel().hud);
+    let hud = toolbox.getCurrentPanel().hud;
+    hud.jsterm._lazyVariablesView = false;
+    aCallback(hud);
   });
 }
 
 /**
  * Close the Web Console for the given tab.
  *
  * @param nsIDOMElement [aTab]
  *        Optional tab element for which you want close the Web Console. The
@@ -160,19 +163,17 @@ function openConsole(aTab, aCallback = f
 function closeConsole(aTab, aCallback = function() { })
 {
   let target = TargetFactory.forTab(aTab || tab);
   let toolbox = gDevTools.getToolbox(target);
   if (toolbox) {
     let panel = toolbox.getPanel("webconsole");
     if (panel) {
       let hudId = panel.hud.hudId;
-      toolbox.destroy().then(function() {
-        executeSoon(aCallback.bind(null, hudId));
-      }).then(null, console.error);
+      toolbox.destroy().then(aCallback.bind(null, hudId)).then(null, console.debug);
     }
     else {
       toolbox.destroy().then(aCallback.bind(null));
     }
   }
   else {
     aCallback();
   }
@@ -310,8 +311,450 @@ function waitForSuccess(aOptions)
 
 function openInspector(aCallback, aTab = gBrowser.selectedTab)
 {
   let target = TargetFactory.forTab(aTab);
   gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
     aCallback(toolbox.getCurrentPanel());
   });
 }
+
+/**
+ * Find variables or properties in a VariablesView instance.
+ *
+ * @param object aView
+ *        The VariablesView instance.
+ * @param array aRules
+ *        The array of rules you want to match. Each rule is an object with:
+ *        - name (string|regexp): property name to match.
+ *        - value (string|regexp): property value to match.
+ *        - isIterator (boolean): check if the property is an iterator.
+ *        - isGetter (boolean): check if the property is a getter.
+ *        - isGenerator (boolean): check if the property is a generator.
+ *        - dontMatch (boolean): make sure the rule doesn't match any property.
+ * @param object aOptions
+ *        Options for matching:
+ *        - webconsole: the WebConsole instance we work with.
+ * @return object
+ *         A Promise object that is resolved when all the rules complete
+ *         matching. The resolved callback is given an array of all the rules
+ *         you wanted to check. Each rule has a new property: |matchedProp|
+ *         which holds a reference to the Property object instance from the
+ *         VariablesView. If the rule did not match, then |matchedProp| is
+ *         undefined.
+ */
+function findVariableViewProperties(aView, aRules, aOptions)
+{
+  // Initialize the search.
+  function init()
+  {
+    // Separate out the rules that require expanding properties throughout the
+    // view.
+    let expandRules = [];
+    let rules = aRules.filter((aRule) => {
+      if (typeof aRule.name == "string" && aRule.name.indexOf(".") > -1) {
+        expandRules.push(aRule);
+        return false;
+      }
+      return true;
+    });
+
+    // Search through the view those rules that do not require any properties to
+    // be expanded. Build the array of matchers, outstanding promises to be
+    // resolved.
+    let outstanding = [];
+    finder(rules, aView, outstanding);
+
+    // Process the rules that need to expand properties.
+    let lastStep = processExpandRules.bind(null, expandRules);
+
+    // Return the results - a Promise resolved to hold the updated aRules array.
+    let returnResults = onAllRulesMatched.bind(null, aRules);
+
+    return Promise.all(outstanding).then(lastStep).then(returnResults);
+  }
+
+  function onMatch(aProp, aRule, aMatched)
+  {
+    if (aMatched && !aRule.matchedProp) {
+      aRule.matchedProp = aProp;
+    }
+  }
+
+  function finder(aRules, aVar, aPromises)
+  {
+    for (let [id, prop] in aVar) {
+      for (let rule of aRules) {
+        let matcher = matchVariablesViewProperty(prop, rule, aOptions);
+        aPromises.push(matcher.then(onMatch.bind(null, prop, rule)));
+      }
+    }
+  }
+
+  function processExpandRules(aRules)
+  {
+    let rule = aRules.shift();
+    if (!rule) {
+      return Promise.resolve(null);
+    }
+
+    let deferred = Promise.defer();
+    let expandOptions = {
+      rootVariable: aView,
+      expandTo: rule.name,
+      webconsole: aOptions.webconsole,
+    };
+
+    variablesViewExpandTo(expandOptions).then(function onSuccess(aProp) {
+      let name = rule.name;
+      let lastName = name.split(".").pop();
+      rule.name = lastName;
+
+      let matched = matchVariablesViewProperty(aProp, rule, aOptions);
+      return matched.then(onMatch.bind(null, aProp, rule)).then(function() {
+        rule.name = name;
+      });
+    }, function onFailure() {
+      return Promise.resolve(null);
+    }).then(processExpandRules.bind(null, aRules)).then(function() {
+      deferred.resolve(null);
+    });
+
+    return deferred.promise;
+  }
+
+  function onAllRulesMatched(aRules)
+  {
+    for (let rule of aRules) {
+      let matched = rule.matchedProp;
+      if (matched && !rule.dontMatch) {
+        ok(true, "rule " + rule.name + " matched for property " + matched.name);
+      }
+      else if (matched && rule.dontMatch) {
+        ok(false, "rule " + rule.name + " should not match property " +
+           matched.name);
+      }
+      else {
+        ok(rule.dontMatch, "rule " + rule.name + " did not match any property");
+      }
+    }
+    return aRules;
+  }
+
+  return init();
+}
+
+/**
+ * Check if a given Property object from the variables view matches the given
+ * rule.
+ *
+ * @param object aProp
+ *        The variable's view Property instance.
+ * @param object aRule
+ *        Rules for matching the property. See findVariableViewProperties() for
+ *        details.
+ * @param object aOptions
+ *        Options for matching. See findVariableViewProperties().
+ * @return object
+ *         A Promise that is resolved when all the checks complete. Resolution
+ *         result is a boolean that tells your promise callback the match
+ *         result: true or false.
+ */
+function matchVariablesViewProperty(aProp, aRule, aOptions)
+{
+  function resolve(aResult) {
+    return Promise.resolve(aResult);
+  }
+
+  if (aRule.name) {
+    let match = aRule.name instanceof RegExp ?
+                aRule.name.test(aProp.name) :
+                aProp.name == aRule.name;
+    if (!match) {
+      return resolve(false);
+    }
+  }
+
+  if (aRule.value) {
+    let displayValue = aProp.displayValue;
+    if (aProp.displayValueClassName == "token-string") {
+      displayValue = displayValue.substring(1, displayValue.length - 1);
+    }
+
+    let match = aRule.value instanceof RegExp ?
+                aRule.value.test(displayValue) :
+                displayValue == aRule.value;
+    if (!match) {
+      info("rule " + aRule.name + " did not match value, expected '" +
+           aRule.value + "', found '" + displayValue  + "'");
+      return resolve(false);
+    }
+  }
+
+  if ("isGetter" in aRule) {
+    let isGetter = !!(aProp.getter && aProp.get("get"));
+    if (aRule.isGetter != isGetter) {
+      info("rule " + aRule.name + " getter test failed");
+      return resolve(false);
+    }
+  }
+
+  if ("isGenerator" in aRule) {
+    let isGenerator = aProp.displayValue == "[object Generator]";
+    if (aRule.isGenerator != isGenerator) {
+      info("rule " + aRule.name + " generator test failed");
+      return resolve(false);
+    }
+  }
+
+  let outstanding = [];
+
+  if ("isIterator" in aRule) {
+    let isIterator = isVariableViewPropertyIterator(aProp, aOptions.webconsole);
+    outstanding.push(isIterator.then((aResult) => {
+      if (aResult != aRule.isIterator) {
+        info("rule " + aRule.name + " iterator test failed");
+      }
+      return aResult == aRule.isIterator;
+    }));
+  }
+
+  outstanding.push(Promise.resolve(true));
+
+  return Promise.all(outstanding).then(function _onMatchDone(aResults) {
+    let ruleMatched = aResults.indexOf(false) == -1;
+    return resolve(ruleMatched);
+  });
+}
+
+/**
+ * Check if the given variables view property is an iterator.
+ *
+ * @param object aProp
+ *        The Property instance you want to check.
+ * @param object aWebConsole
+ *        The WebConsole instance to work with.
+ * @return object
+ *         A Promise that is resolved when the check completes. The resolved
+ *         callback is given a boolean: true if the property is an iterator, or
+ *         false otherwise.
+ */
+function isVariableViewPropertyIterator(aProp, aWebConsole)
+{
+  if (aProp.displayValue == "[object Iterator]") {
+    return Promise.resolve(true);
+  }
+
+  let deferred = Promise.defer();
+
+  variablesViewExpandTo({
+    rootVariable: aProp,
+    expandTo: "__proto__.__iterator__",
+    webconsole: aWebConsole,
+  }).then(function onSuccess(aProp) {
+    deferred.resolve(true);
+  }, function onFailure() {
+    deferred.resolve(false);
+  });
+
+  return deferred.promise;
+}
+
+
+/**
+ * Recursively expand the variables view up to a given property.
+ *
+ * @param aOptions
+ *        Options for view expansion:
+ *        - rootVariable: start from the given scope/variable/property.
+ *        - expandTo: string made up of property names you want to expand.
+ *        For example: "body.firstChild.nextSibling" given |rootVariable:
+ *        document|.
+ *        - webconsole: a WebConsole instance. If this is not provided all
+ *        property expand() calls will be considered sync. Things may fail!
+ * @return object
+ *         A Promise that is resolved only when the last property in |expandTo|
+ *         is found, and rejected otherwise. Resolution reason is always the
+ *         last property - |nextSibling| in the example above. Rejection is
+ *         always the last property that was found.
+ */
+function variablesViewExpandTo(aOptions)
+{
+  let root = aOptions.rootVariable;
+  let expandTo = aOptions.expandTo.split(".");
+  let jsterm = (aOptions.webconsole || {}).jsterm;
+  let lastDeferred = Promise.defer();
+
+  function fetch(aProp)
+  {
+    if (!aProp.onexpand) {
+      ok(false, "property " + aProp.name + " cannot be expanded: !onexpand");
+      return Promise.reject(aProp);
+    }
+
+    let deferred = Promise.defer();
+
+    if (aProp._fetched || !jsterm) {
+      executeSoon(function() {
+        deferred.resolve(aProp);
+      });
+    }
+    else {
+      jsterm.once("variablesview-fetched", function _onFetchProp() {
+        executeSoon(() => deferred.resolve(aProp));
+      });
+    }
+
+    aProp.expand();
+
+    return deferred.promise;
+  }
+
+  function getNext(aProp)
+  {
+    let name = expandTo.shift();
+    let newProp = aProp.get(name);
+
+    if (expandTo.length > 0) {
+      ok(newProp, "found property " + name);
+      if (newProp) {
+        fetch(newProp).then(getNext, fetchError);
+      }
+      else {
+        lastDeferred.reject(aProp);
+      }
+    }
+    else {
+      if (newProp) {
+        lastDeferred.resolve(newProp);
+      }
+      else {
+        lastDeferred.reject(aProp);
+      }
+    }
+  }
+
+  function fetchError(aProp)
+  {
+    lastDeferred.reject(aProp);
+  }
+
+  if (!root._fetched) {
+    fetch(root).then(getNext, fetchError);
+  }
+  else {
+    getNext(root);
+  }
+
+  return lastDeferred.promise;
+}
+
+
+/**
+ * Update the content of a property in the variables view.
+ *
+ * @param object aOptions
+ *        Options for the property update:
+ *        - property: the property you want to change.
+ *        - field: string that tells what you want to change:
+ *          - use "name" to change the property name,
+ *          - or "value" to change the property value.
+ *        - string: the new string to write into the field.
+ *        - webconsole: reference to the Web Console instance we work with.
+ *        - callback: function to invoke after the property is updated.
+ */
+function updateVariablesViewProperty(aOptions)
+{
+  let view = aOptions.property._variablesView;
+  view.window.focus();
+  aOptions.property.focus();
+
+  switch (aOptions.field) {
+    case "name":
+      EventUtils.synthesizeKey("VK_ENTER", { shiftKey: true }, view.window);
+      break;
+    case "value":
+      EventUtils.synthesizeKey("VK_ENTER", {}, view.window);
+      break;
+    default:
+      throw new Error("options.field is incorrect");
+      return;
+  }
+
+  executeSoon(() => {
+    EventUtils.synthesizeKey("A", { accelKey: true }, view.window);
+
+    for (let c of aOptions.string) {
+      EventUtils.synthesizeKey(c, {}, gVariablesView.window);
+    }
+
+    if (aOptions.webconsole) {
+      aOptions.webconsole.jsterm.once("variablesview-fetched", aOptions.callback);
+    }
+
+    EventUtils.synthesizeKey("VK_ENTER", {}, view.window);
+
+    if (!aOptions.webconsole) {
+      executeSoon(aOptions.callback);
+    }
+  });
+}
+
+/**
+ * Open the JavaScript debugger.
+ *
+ * @param object aOptions
+ *        Options for opening the debugger:
+ *        - tab: the tab you want to open the debugger for.
+ * @return object
+ *         A Promise that is resolved once the debugger opens, or rejected if
+ *         the open fails. The resolution callback is given one argument, an
+ *         object that holds the following properties:
+ *         - target: the Target object for the Tab.
+ *         - toolbox: the Toolbox instance.
+ *         - panel: the jsdebugger panel instance.
+ *         - panelWin: the window object of the panel iframe.
+ */
+function openDebugger(aOptions = {})
+{
+  if (!aOptions.tab) {
+    aOptions.tab = gBrowser.selectedTab;
+  }
+
+  let deferred = Promise.defer();
+
+  let target = TargetFactory.forTab(aOptions.tab);
+  let toolbox = gDevTools.getToolbox(target);
+  let dbgPanelAlreadyOpen = toolbox.getPanel("jsdebugger");
+
+  gDevTools.showToolbox(target, "jsdebugger").then(function onSuccess(aToolbox) {
+    let panel = aToolbox.getCurrentPanel();
+    let panelWin = panel.panelWin;
+
+    panel._view.Variables.lazyEmpty = false;
+    panel._view.Variables.lazyAppend = false;
+
+    let resolveObject = {
+      target: target,
+      toolbox: aToolbox,
+      panel: panel,
+      panelWin: panelWin,
+    };
+
+    if (dbgPanelAlreadyOpen) {
+      deferred.resolve(resolveObject);
+    }
+    else {
+      panelWin.addEventListener("Debugger:AfterSourcesAdded",
+        function onAfterSourcesAdded() {
+          panelWin.removeEventListener("Debugger:AfterSourcesAdded",
+                                       onAfterSourcesAdded);
+          deferred.resolve(resolveObject);
+        });
+    }
+  }, function onFailure(aReason) {
+    console.debug("failed to open the toolbox for 'jsdebugger'", aReason);
+    deferred.reject(aReason);
+  });
+
+  return deferred.promise;
+}
+
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/test/test-eval-in-stackframe.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html dir="ltr" lang="en">
+  <head>
+    <meta charset="utf8">
+    <!--
+    - Any copyright is dedicated to the Public Domain.
+    - http://creativecommons.org/publicdomain/zero/1.0/
+    -->
+    <title>Test for bug 783499 - use the debugger API in the web console</title>
+    <script>
+      var foo = "globalFooBug783499";
+      var fooObj = {
+        testProp: "testValue",
+      };
+
+      function firstCall()
+      {
+        var foo = "fooFirstCall";
+        var foo3 = "foo3FirstCall";
+        secondCall();
+      }
+
+      function secondCall()
+      {
+        var foo2 = "foo2SecondCall";
+        var fooObj = {
+          testProp2: "testValue2",
+        };
+        var fooObj2 = {
+          testProp22: "testValue22",
+        };
+        debugger;
+      }
+    </script>
+  </head>
+  <body>
+    <p>Hello world!</p>
+  </body>
+</html>
--- a/browser/devtools/webconsole/webconsole.js
+++ b/browser/devtools/webconsole/webconsole.js
@@ -14,45 +14,57 @@ Cu.import("resource://gre/modules/XPCOMU
 
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper",
                                    "@mozilla.org/widget/clipboardhelper;1",
                                    "nsIClipboardHelper");
 
-XPCOMUtils.defineLazyModuleGetter(this, "PropertyPanel",
-                                  "resource:///modules/PropertyPanel.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "PropertyTreeView",
-                                  "resource:///modules/PropertyPanel.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "GripClient",
+                                  "resource://gre/modules/devtools/dbg-client.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "NetworkPanel",
                                   "resource:///modules/NetworkPanel.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "AutocompletePopup",
                                   "resource:///modules/devtools/AutocompletePopup.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "WebConsoleUtils",
                                   "resource://gre/modules/devtools/WebConsoleUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
                                   "resource://gre/modules/commonjs/sdk/core/promise.js");
 
+XPCOMUtils.defineLazyModuleGetter(this, "VariablesView",
+                                  "resource:///modules/devtools/VariablesView.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "ToolSidebar",
+                                  "resource:///modules/devtools/Sidebar.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
+                                  "resource:///modules/devtools/EventEmitter.jsm");
+
 const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties";
 let l10n = new WebConsoleUtils.l10n(STRINGS_URI);
 
 
 // The XUL namespace.
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 const MIXED_CONTENT_LEARN_MORE = "https://developer.mozilla.org/en/Security/MixedContent";
 
 const HELP_URL = "https://developer.mozilla.org/docs/Tools/Web_Console/Helpers";
 
+const VARIABLES_VIEW_URL = "chrome://browser/content/devtools/widgets/VariablesView.xul";
+
+const CONSOLE_DIR_VIEW_HEIGHT = 0.6;
+
+const IGNORED_SOURCE_URLS = ["debugger eval code", "self-hosted"];
+
 // The amount of time in milliseconds that must pass between messages to
 // trigger the display of a new group.
 const NEW_GROUP_DELAY = 5000;
 
 // The amount of time in milliseconds that we wait before performing a live
 // search.
 const SEARCH_DELAY = 200;
 
@@ -949,55 +961,42 @@ WebConsoleFrame.prototype = {
   logConsoleAPIMessage: function WCF_logConsoleAPIMessage(aMessage)
   {
     let body = null;
     let clipboardText = null;
     let sourceURL = aMessage.filename;
     let sourceLine = aMessage.lineNumber;
     let level = aMessage.level;
     let args = aMessage.arguments;
-    let objectActors = [];
+    let objectActors = new Set();
 
     // Gather the actor IDs.
-    args.forEach(function(aValue) {
-      if (aValue && typeof aValue == "object" && aValue.actor) {
-        objectActors.push(aValue.actor);
-        let displayStringIsLong = typeof aValue.displayString == "object" &&
-                                  aValue.displayString.type == "longString";
-        if (displayStringIsLong) {
-          objectActors.push(aValue.displayString.actor);
-        }
+    args.forEach((aValue) => {
+      if (WebConsoleUtils.isActorGrip(aValue)) {
+        objectActors.add(aValue.actor);
       }
-    }, this);
+    });
 
     switch (level) {
       case "log":
       case "info":
       case "warn":
       case "error":
       case "debug":
-      case "dir":
-      case "groupEnd": {
+      case "dir": {
         body = { arguments: args };
         let clipboardArray = [];
-        args.forEach(function(aValue) {
-          clipboardArray.push(WebConsoleUtils.objectActorGripToString(aValue));
-          if (aValue && typeof aValue == "object" && aValue.actor) {
-            let displayStringIsLong = typeof aValue.displayString == "object" &&
-                                      aValue.displayString.type == "longString";
-            if (aValue.type == "longString" || displayStringIsLong) {
-              clipboardArray.push(l10n.getStr("longStringEllipsis"));
-            }
+        args.forEach((aValue) => {
+          clipboardArray.push(VariablesView.getString(aValue));
+          if (aValue && typeof aValue == "object" &&
+              aValue.type == "longString") {
+            clipboardArray.push(l10n.getStr("longStringEllipsis"));
           }
-        }, this);
+        });
         clipboardText = clipboardArray.join(" ");
-
-        if (level == "dir") {
-          body.objectProperties = aMessage.objectProperties;
-        }
         break;
       }
 
       case "trace": {
         let filename = WebConsoleUtils.abbreviateSourceURL(aMessage.filename);
         let functionName = aMessage.functionName ||
                            l10n.getStr("stacktrace.anonymousFunction");
 
@@ -1017,16 +1016,22 @@ WebConsoleFrame.prototype = {
       }
 
       case "group":
       case "groupCollapsed":
         clipboardText = body = aMessage.groupName;
         this.groupDepth++;
         break;
 
+      case "groupEnd":
+        if (this.groupDepth > 0) {
+          this.groupDepth--;
+        }
+        break;
+
       case "time": {
         let timer = aMessage.timer;
         if (!timer) {
           return;
         }
         if (timer.error) {
           Cu.reportError(l10n.getStr(timer.error));
           return;
@@ -1055,62 +1060,41 @@ WebConsoleFrame.prototype = {
     // we ignore their arguments.
     switch (level) {
       case "group":
       case "groupCollapsed":
       case "groupEnd":
       case "trace":
       case "time":
       case "timeEnd":
-        objectActors.forEach(this._releaseObject, this);
-        objectActors = [];
+        for (let actor of objectActors) {
+          this._releaseObject(actor);
+        }
+        objectActors.clear();
     }
 
     if (level == "groupEnd") {
-      if (this.groupDepth > 0) {
-        this.groupDepth--;
-      }
       return; // no need to continue
     }
 
     let node = this.createMessageNode(CATEGORY_WEBDEV, LEVELS[level], body,
                                       sourceURL, sourceLine, clipboardText,
                                       level, aMessage.timeStamp);
 
-    if (objectActors.length) {
+    if (objectActors.size > 0) {
       node._objectActors = objectActors;
     }
 
-    // Make the node bring up the property panel, to allow the user to inspect
+    // Make the node bring up the variables view, to allow the user to inspect
     // the stack trace.
     if (level == "trace") {
       node._stacktrace = aMessage.stacktrace;
 
-      this.makeOutputMessageLink(node, function _traceNodeClickCallback() {
-        if (node._panelOpen) {
-          return;
-        }
-
-        let options = {
-          anchor: node,
-          data: { object: node._stacktrace },
-        };
-
-        let propPanel = this.jsterm.openPropertyPanel(options);
-        propPanel.panel.setAttribute("hudId", this.hudId);
-      }.bind(this));
-    }
-
-    if (level == "dir") {
-      // Initialize the inspector message node, by setting the PropertyTreeView
-      // object on the tree view. This has to be done *after* the node is
-      // shown, because the tree binding must be attached first.
-      node._onOutput = function _onMessageOutput() {
-        node.querySelector("tree").view = node.propertyTreeView;
-      };
+      this.makeOutputMessageLink(node, () =>
+        this.jsterm.openVariablesView({ rawObject: node._stacktrace }));
     }
 
     return node;
   },
 
   /**
    * Handle ConsoleAPICall objects received from the server. This method outputs
    * the window.console API call.
@@ -1123,59 +1107,29 @@ WebConsoleFrame.prototype = {
     this.outputMessage(CATEGORY_WEBDEV, this.logConsoleAPIMessage, [aMessage]);
   },
 
   /**
    * The click event handler for objects shown inline coming from the
    * window.console API.
    *
    * @private
-   * @param nsIDOMNode aMessage
-   *        The message element this handler corresponds to.
    * @param nsIDOMNode aAnchor
    *        The object inspector anchor element. This is the clickable element
    *        in the console.log message we display.
    * @param object aObjectActor
    *        The object actor grip.
    */
-  _consoleLogClick:
-  function WCF__consoleLogClick(aMessage, aAnchor, aObjectActor)
+  _consoleLogClick: function WCF__consoleLogClick(aAnchor, aObjectActor)
   {
-    if (aAnchor._panelOpen) {
-      return;
-    }
-
     let options = {
-      title: aAnchor.textContent,
-      anchor: aAnchor,
-
-      // Data to inspect.
-      data: {
-        objectPropertiesProvider: this.objectPropertiesProvider.bind(this),
-        releaseObject: this._releaseObject.bind(this),
-      },
+      label: aAnchor.textContent,
+      objectActor: aObjectActor,
     };
-
-    let propPanel;
-    let onPopupHide = function _onPopupHide() {
-      propPanel.panel.removeEventListener("popuphiding", onPopupHide, false);
-
-      if (!aMessage.parentNode && aMessage._objectActors) {
-        aMessage._objectActors.forEach(this._releaseObject, this);
-        aMessage._objectActors = null;
-      }
-    }.bind(this);
-
-    this.objectPropertiesProvider(aObjectActor.actor,
-      function _onObjectProperties(aProperties) {
-        options.data.objectProperties = aProperties;
-        propPanel = this.jsterm.openPropertyPanel(options);
-        propPanel.panel.setAttribute("hudId", this.hudId);
-        propPanel.panel.addEventListener("popuphiding", onPopupHide, false);
-      }.bind(this));
+    this.jsterm.openVariablesView(options);
   },
 
   /**
    * Reports an error in the page source, either JavaScript or CSS.
    *
    * @param nsIScriptError aScriptError
    *        The error message to report.
    * @return nsIDOMElement|undefined
@@ -1903,19 +1857,21 @@ WebConsoleFrame.prototype = {
    *
    * @private
    * @param array aItem
    *        The item you want to remove from the output queue.
    */
   _pruneItemFromQueue: function WCF__pruneItemFromQueue(aItem)
   {
     let [category, methodOrNode, args] = aItem;
-    if (typeof methodOrNode != "function" &&
-        methodOrNode._objectActors && !methodOrNode._panelOpen) {
-      methodOrNode._objectActors.forEach(this._releaseObject, this);
+    if (typeof methodOrNode != "function" && methodOrNode._objectActors) {
+      for (let actor of methodOrNode._objectActors) {
+        this._releaseObject(actor);
+      }
+      methodOrNode._objectActors.clear();
     }
 
     if (category == CATEGORY_NETWORK) {
       let connectionId = null;
       if (methodOrNode == this.logNetEvent) {
         connectionId = args[0];
       }
       else if (typeof methodOrNode != "function") {
@@ -1923,40 +1879,21 @@ WebConsoleFrame.prototype = {
       }
       if (connectionId && connectionId in this._networkRequests) {
         delete this._networkRequests[connectionId];
         this._releaseObject(connectionId);
       }
     }
     else if (category == CATEGORY_WEBDEV &&
              methodOrNode == this.logConsoleAPIMessage) {
-      let level = args[0].level;
-      let releaseObject = function _releaseObject(aValue) {
-        if (aValue && typeof aValue == "object" && aValue.actor) {
+      args[0].arguments.forEach((aValue) => {
+        if (WebConsoleUtils.isActorGrip(aValue)) {
           this._releaseObject(aValue.actor);
         }
-      }.bind(this);
-      switch (level) {
-        case "log":
-        case "info":
-        case "warn":
-        case "error":
-        case "debug":
-        case "dir":
-        case "groupEnd": {
-          args[0].arguments.forEach(releaseObject);
-          if (level == "dir") {
-            args[0].objectProperties.forEach(function(aObject) {
-              ["value", "get", "set"].forEach(function(aProp) {
-                releaseObject(aObject[aProp]);
-              });
-            });
-          }
-        }
-      }
+      });
     }
   },
 
   /**
    * Ensures that the number of message nodes of type aCategory don't exceed that
    * category's line limit by removing old messages as needed.
    *
    * @param integer aCategory
@@ -1974,62 +1911,51 @@ WebConsoleFrame.prototype = {
     let n = Math.max(0, messageNodes.length - logLimit);
     let toRemove = Array.prototype.slice.call(messageNodes, 0, n);
     toRemove.forEach(this.removeOutputMessage, this);
 
     return n;
   },
 
   /**
-   * Destroy the property inspector message node. This performs the necessary
-   * cleanup for the tree widget and removes it from the DOM.
-   *
-   * @param nsIDOMNode aMessageNode
-   *        The message node that contains the property inspector from a
-   *        console.dir call.
-   */
-  pruneConsoleDirNode: function WCF_pruneConsoleDirNode(aMessageNode)
-  {
-    if (aMessageNode.parentNode) {
-      aMessageNode.parentNode.removeChild(aMessageNode);
-    }
-
-    let tree = aMessageNode.querySelector("tree");
-    tree.parentNode.removeChild(tree);
-    aMessageNode.propertyTreeView.data = null;
-    aMessageNode.propertyTreeView = null;
-    tree.view = null;
-  },
-
-  /**
    * Remove a given message from the output.
    *
    * @param nsIDOMNode aNode
    *        The message node you want to remove.
    */
   removeOutputMessage: function WCF_removeOutputMessage(aNode)
   {
-    if (aNode._objectActors && !aNode._panelOpen) {
-      aNode._objectActors.forEach(this._releaseObject, this);
+    if (aNode._objectActors) {
+      for (let actor of aNode._objectActors) {
+        this._releaseObject(actor);
+      }
+      aNode._objectActors.clear();
     }
 
     if (aNode.classList.contains("webconsole-msg-cssparser")) {
       let repeatNode = aNode.getElementsByClassName("webconsole-msg-repeat")[0];
       if (repeatNode && repeatNode._uid) {
         delete this._cssNodes[repeatNode._uid];
       }
     }
     else if (aNode._connectionId &&
              aNode.classList.contains("webconsole-msg-network")) {
       delete this._networkRequests[aNode._connectionId];
       this._releaseObject(aNode._connectionId);
     }
     else if (aNode.classList.contains("webconsole-msg-inspector")) {
-      this.pruneConsoleDirNode(aNode);
-      return;
+      let view = aNode._variablesView;
+      let actors = view ?
+                   this.jsterm._objectActorsInVariablesViews.get(view) :
+                   new Set();
+      for (let actor of actors) {
+        this._releaseObject(actor);
+      }
+      actors.clear();
+      aNode._variablesView = null;
     }
 
     if (aNode.parentNode) {
       aNode.parentNode.removeChild(aNode);
     }
   },
 
   /**
@@ -2110,35 +2036,34 @@ WebConsoleFrame.prototype = {
     spacer.flex = 1;
     iconContainer.appendChild(spacer);
 
     // Create the message body, which contains the actual text of the message.
     let bodyNode = this.document.createElementNS(XUL_NS, "description");
     bodyNode.flex = 1;
     bodyNode.classList.add("webconsole-msg-body");
 
-    // Store the body text, since it is needed later for the property tree
-    // case.
+    // Store the body text, since it is needed later for the variables view.
     let body = aBody;
     // If a string was supplied for the body, turn it into a DOM node and an
     // associated clipboard string now.
     aClipboardText = aClipboardText ||
                      (aBody + (aSourceURL ? " @ " + aSourceURL : "") +
                               (aSourceLine ? ":" + aSourceLine : ""));
 
     // Create the containing node and append all its elements to it.
     let node = this.document.createElementNS(XUL_NS, "richlistitem");
 
     if (aBody instanceof Ci.nsIDOMNode) {
       bodyNode.appendChild(aBody);
     }
     else {
       let str = undefined;
       if (aLevel == "dir") {
-        str = WebConsoleUtils.objectActorGripToString(aBody.arguments[0]);
+        str = VariablesView.getString(aBody.arguments[0]);
       }
       else if (["log", "info", "warn", "error", "debug"].indexOf(aLevel) > -1 &&
                typeof aBody == "object") {
         this._makeConsoleLogMessageBody(node, bodyNode, aBody);
       }
       else {
         str = aBody;
       }
@@ -2163,64 +2088,50 @@ WebConsoleFrame.prototype = {
     timestampNode.classList.add("webconsole-timestamp");
     let timestamp = aTimeStamp || Date.now();
     let timestampString = l10n.timestampString(timestamp);
     timestampNode.setAttribute("value", timestampString);
 
     // Create the source location (e.g. www.example.com:6) that sits on the
     // right side of the message, if applicable.
     let locationNode;
-    if (aSourceURL) {
+    if (aSourceURL && IGNORED_SOURCE_URLS.indexOf(aSourceURL) == -1) {
       locationNode = this.createLocationNode(aSourceURL, aSourceLine);
     }
 
     node.clipboardText = aClipboardText;
     node.classList.add("hud-msg-node");
 
     node.timestamp = timestamp;
     this.setMessageType(node, aCategory, aSeverity);
 
     node.appendChild(timestampNode);
     node.appendChild(iconContainer);
-    // Display the object tree after the message node.
+
+    // Display the variables view after the message node.
     if (aLevel == "dir") {
-      // Make the body container, which is a vertical box, for grouping the text
-      // and tree widgets.
+      let viewContainer = this.document.createElement("hbox");
+      viewContainer.flex = 1;
+      viewContainer.height = this.outputNode.clientHeight *
+                             CONSOLE_DIR_VIEW_HEIGHT;
+
+      let options = {
+        objectActor: body.arguments[0],
+        targetElement: viewContainer,
+        hideFilterInput: true,
+      };
+      this.jsterm.openVariablesView(options)
+        .then((aView) => node._variablesView = aView);
+
       let bodyContainer = this.document.createElement("vbox");
       bodyContainer.flex = 1;
       bodyContainer.appendChild(bodyNode);
-      // Create the tree.
-      let tree = this.document.createElement("tree");
-      tree.setAttribute("hidecolumnpicker", "true");
-      tree.flex = 1;
-
-      let treecols = this.document.createElement("treecols");
-      let treecol = this.document.createElement("treecol");
-      treecol.setAttribute("primary", "true");
-      treecol.setAttribute("hideheader", "true");
-      treecol.setAttribute("ignoreincolumnpicker", "true");
-      treecol.flex = 1;
-      treecols.appendChild(treecol);
-      tree.appendChild(treecols);
-
-      tree.appendChild(this.document.createElement("treechildren"));
-
-      bodyContainer.appendChild(tree);
+      bodyContainer.appendChild(viewContainer);
       node.appendChild(bodyContainer);
       node.classList.add("webconsole-msg-inspector");
-      // Create the treeView object.
-      let treeView = node.propertyTreeView = new PropertyTreeView();
-
-      treeView.data = {
-        objectPropertiesProvider: this.objectPropertiesProvider.bind(this),
-        releaseObject: this._releaseObject.bind(this),
-        objectProperties: body.objectProperties,
-      };
-
-      tree.setAttribute("rows", treeView.rowCount);
     }
     else {
       node.appendChild(bodyNode);
     }
     node.appendChild(repeatContainer);
     if (locationNode) {
       node.appendChild(locationNode);
     }
@@ -2258,53 +2169,46 @@ WebConsoleFrame.prototype = {
       configurable: false
     });
 
     aBody.arguments.forEach(function(aItem) {
       if (aContainer.firstChild) {
         aContainer.appendChild(this.document.createTextNode(" "));
       }
 
-      let text = WebConsoleUtils.objectActorGripToString(aItem);
-
-      if (aItem && typeof aItem != "object" || !aItem.inspectable) {
+      let text = VariablesView.getString(aItem);
+      let inspectable = !VariablesView.isPrimitive({ value: aItem });
+
+      if (aItem && typeof aItem != "object" || !inspectable) {
         aContainer.appendChild(this.document.createTextNode(text));
 
-        let longString = null;
         if (aItem.type == "longString") {
-          longString = aItem;
-        }
-        else if (!aItem.inspectable &&
-                 typeof aItem.displayString == "object" &&
-                 aItem.displayString.type == "longString") {
-          longString = aItem.displayString;
-        }
-
-        if (longString) {
           let ellipsis = this.document.createElement("description");
           ellipsis.classList.add("hud-clickable");
           ellipsis.classList.add("longStringEllipsis");
           ellipsis.textContent = l10n.getStr("longStringEllipsis");
 
+          let formatter = function(s) '"' + s + '"';
+
           this._addMessageLinkCallback(ellipsis,
-            this._longStringClick.bind(this, aMessage, longString, null));
+            this._longStringClick.bind(this, aMessage, aItem, formatter));
 
           aContainer.appendChild(ellipsis);
         }
         return;
       }
 
       // For inspectable objects.
       let elem = this.document.createElement("description");
       elem.classList.add("hud-clickable");
       elem.setAttribute("aria-haspopup", "true");
       elem.appendChild(this.document.createTextNode(text));
 
       this._addMessageLinkCallback(elem,
-        this._consoleLogClick.bind(this, aMessage, elem, aItem));
+        this._consoleLogClick.bind(this, elem, aItem));
 
       aContainer.appendChild(elem);
     }, this);
   },
 
   /**
    * Click event handler for the ellipsis shown immediately after a long string.
    * This method retrieves the full string and updates the console output to
@@ -2677,28 +2581,76 @@ function JSTerm(aWebConsoleFrame)
 {
   this.hud = aWebConsoleFrame;
   this.hudId = this.hud.hudId;
 
   this.lastCompletion = { value: null };
   this.history = [];
   this.historyIndex = 0;
   this.historyPlaceHolder = 0;  // this.history.length;
+  this._objectActorsInVariablesViews = new Map();
+
   this._keyPress = this.keyPress.bind(this);
   this._inputEventHandler = this.inputEventHandler.bind(this);
+  this._fetchVarProperties = this._fetchVarProperties.bind(this);
+  this._fetchVarLongString = this._fetchVarLongString.bind(this);
+
+  EventEmitter.decorate(this);
 }
 
 JSTerm.prototype = {
+  SELECTED_FRAME: -1,
+
   /**
    * Stores the data for the last completion.
    * @type object
    */
   lastCompletion: null,
 
   /**
+   * The Web Console sidebar.
+   * @see this._createSidebar()
+   * @see Sidebar.jsm
+   */
+  sidebar: null,
+
+  /**
+   * The Web Console splitter between output and the sidebar.
+   * @private
+   * @type nsIDOMElement
+   */
+  _splitter: null,
+
+  /**
+   * The Variables View instance shown in the sidebar.
+   * @private
+   * @type object
+   */
+  _variablesView: null,
+
+  /**
+   * Tells if you want the variables view UI updates to be lazy or not. Tests
+   * disable lazy updates.
+   *
+   * @private
+   * @type boolean
+   */
+  _lazyVariablesView: true,
+
+  /**
+   * Holds a map between VariablesView instances and sets of ObjectActor IDs
+   * that have been retrieved from the server. This allows us to release the
+   * objects when needed.
+   *
+   * @private
+   * @type Map
+   */
+  _objectActorsInVariablesViews: null,
+
+  /**
    * Last input value.
    * @type string
    */
   lastInputValue: "",
 
   /**
    * History of code that was executed.
    * @type array
@@ -2746,16 +2698,18 @@ JSTerm.prototype = {
 
     let doc = this.hud.document;
     this.completeNode = doc.querySelector(".jsterm-complete-node");
     this.inputNode = doc.querySelector(".jsterm-input-node");
     this.inputNode.addEventListener("keypress", this._keyPress, false);
     this.inputNode.addEventListener("input", this._inputEventHandler, false);
     this.inputNode.addEventListener("keyup", this._inputEventHandler, false);
 
+    this._splitter = doc.querySelector(".devtools-side-splitter");
+
     this.lastInputValue && this.setInputValue(this.lastInputValue);
   },
 
   /**
    * The JavaScript evaluation response handler.
    *
    * @private
    * @param nsIDOMElement [aAfterNode]
@@ -2768,33 +2722,47 @@ JSTerm.prototype = {
    *        The message received from the server.
    */
   _executeResultCallback:
   function JST__executeResultCallback(aAfterNode, aCallback, aResponse)
   {
     if (!this.hud) {
       return;
     }
-
-    let errorMessage = aResponse.errorMessage;
+    if (aResponse.error) {
+      Cu.reportError("Evaluation error " + aResponse.error + ": " +
+                     aResponse.message);
+      return;
+    }
+    let errorMessage = aResponse.exceptionMessage;
     let result = aResponse.result;
-    let inspectable = result && typeof result == "object" && result.inspectable;
+    let inspectable = false;
+    if (result && !VariablesView.isPrimitive({ value: result })) {
+      inspectable = true;
+    }
     let helperResult = aResponse.helperResult;
     let helperHasRawOutput = !!(helperResult || {}).rawOutput;
-    let resultString =
-      WebConsoleUtils.objectActorGripToString(result,
-                                              !helperHasRawOutput);
+    let resultString = VariablesView.getString(result);
 
     if (helperResult && helperResult.type) {
       switch (helperResult.type) {
         case "clearOutput":
           this.clearOutput();
           break;
         case "inspectObject":
-          this.handleInspectObject(helperResult.input, helperResult.object);
+          if (aAfterNode) {
+            if (!aAfterNode._objectActors) {
+              aAfterNode._objectActors = new Set();
+            }
+            aAfterNode._objectActors.add(helperResult.object.actor);
+          }
+          this.openVariablesView({
+            label: VariablesView.getString(helperResult.object),
+            objectActor: helperResult.object,
+          });
           break;
         case "error":
           try {
             errorMessage = l10n.getStr(helperResult.message);
           }
           catch (ex) {
             errorMessage = helperResult.message;
           }
@@ -2833,48 +2801,40 @@ JSTerm.prototype = {
                                 this._evalOutputClick.bind(this, aResponse),
                                 aAfterNode, aResponse.timestamp);
     }
     else {
       node = this.writeOutput(resultString, CATEGORY_OUTPUT, SEVERITY_LOG,
                               aAfterNode, aResponse.timestamp);
     }
 
-    if (result && typeof result == "object" && result.actor) {
-      node._objectActors = [result.actor];
-      if (typeof result.displayString == "object" &&
-          result.displayString.type == "longString") {
-        node._objectActors.push(result.displayString.actor);
-      }
-
-      // Add an ellipsis to expand the short string if the object is not
-      // inspectable.
-      let longString = null;
-      let formatter = null;
+    node._objectActors = new Set();
+
+    let error = aResponse.exception;
+    if (WebConsoleUtils.isActorGrip(error)) {
+      node._objectActors.add(error.actor);
+    }
+
+    if (WebConsoleUtils.isActorGrip(result)) {
+      node._objectActors.add(result.actor);
+
       if (result.type == "longString") {
-        longString = result;
-        if (!helperHasRawOutput) {
-          formatter = WebConsoleUtils.formatResultString.bind(WebConsoleUtils);
-        }
-      }
-      else if (!inspectable && !errorMessage &&
-               typeof result.displayString == "object" &&
-               result.displayString.type == "longString") {
-        longString = result.displayString;
-      }
-
-      if (longString) {
+        // Add an ellipsis to expand the short string if the object is not
+        // inspectable.
+
         let body = node.querySelector(".webconsole-msg-body");
         let ellipsis = this.hud.document.createElement("description");
         ellipsis.classList.add("hud-clickable");
         ellipsis.classList.add("longStringEllipsis");
         ellipsis.textContent = l10n.getStr("longStringEllipsis");
 
-        this.hud._addMessageLinkCallback(ellipsis,
-          this.hud._longStringClick.bind(this.hud, node, longString, formatter));
+        let formatter = function(s) '"' + s + '"';
+        let onclick = this.hud._longStringClick.bind(this.hud, node, result,
+                                                    formatter);
+        this.hud._addMessageLinkCallback(ellipsis, onclick);
 
         body.appendChild(ellipsis);
 
         node.clipboardText += " " + ellipsis.textContent;
       }
     }
   },
 
@@ -2895,81 +2855,627 @@ JSTerm.prototype = {
       this.writeOutput(l10n.getStr("executeEmptyInput"), CATEGORY_OUTPUT,
                        SEVERITY_LOG);
       return;
     }
 
     let node = this.writeOutput(aExecuteString, CATEGORY_INPUT, SEVERITY_LOG);
     let onResult = this._executeResultCallback.bind(this, node, aCallback);
 
-    this.webConsoleClient.evaluateJS(aExecuteString, onResult);
+    let options = { frame: this.SELECTED_FRAME };
+    this.requestEvaluation(aExecuteString, options).then(onResult, onResult);
 
     this.history.push(aExecuteString);
     this.historyIndex++;
     this.historyPlaceHolder = this.history.length;
     this.setInputValue("");
     this.clearCompletion();
   },
 
   /**
-   * Opens a new property panel that allows the inspection of the given object.
-   * The object information can be retrieved both async and sync, depending on
-   * the given options.
+   * Request a JavaScript string evaluation from the server.
+   *
+   * @param string aString
+   *        String to execute.
+   * @param object [aOptions]
+   *        Options for evaluation:
+   *        - bindObjectActor: tells the ObjectActor ID for which you want to do
+   *        the evaluation. The Debugger.Object of the OA will be bound to
+   *        |_self| during evaluation, such that it's usable in the string you
+   *        execute.
+   *        - frame: tells the stackframe depth to evaluate the string in. If
+   *        the jsdebugger is paused, you can pick the stackframe to be used for
+   *        evaluation. Use |this.SELECTED_FRAME| to always pick the
+   *        user-selected stackframe.
+   *        If you do not provide a |frame| the string will be evaluated in the
+   *        global content window.
+   * @return object
+   *         A Promise object that is resolved when the server response is
+   *         received.
+   */
+  requestEvaluation: function JST_requestEvaluation(aString, aOptions = {})
+  {
+    let deferred = Promise.defer();
+
+    function onResult(aResponse) {
+      if (!aResponse.error) {
+        deferred.resolve(aResponse);
+      }
+      else {
+        deferred.reject(aResponse);
+      }
+    }
+
+    let frameActor = null;
+    if ("frame" in aOptions) {
+      frameActor = this.getFrameActor(aOptions.frame);
+    }
+
+    let evalOptions = {
+      bindObjectActor: aOptions.bindObjectActor,
+      frameActor: frameActor,
+    };
+
+    this.webConsoleClient.evaluateJS(aString, onResult, evalOptions);
+    return deferred.promise;
+  },
+
+  /**
+   * Retrieve the FrameActor ID given a frame depth.
+   *
+   * @param number aFrame
+   *        Frame depth.
+   * @return string|null
+   *         The FrameActor ID for the given frame depth.
+   */
+  getFrameActor: function JST_getFrameActor(aFrame)
+  {
+    let state = this.hud.owner.getDebuggerFrames();
+    if (!state) {
+      return null;
+    }
+
+    let grip;
+    if (aFrame == this.SELECTED_FRAME) {
+      grip = state.frames[state.selected];
+    }
+    else {
+      grip = state.frames[aFrame];
+    }
+
+    return grip ? grip.actor : null;
+  },
+
+  /**
+   * Opens a new variables view that allows the inspection of the given object.
    *
    * @param object aOptions
-   *        Property panel options:
-   *        - title:
-   *        Panel title.
-   *        - anchor:
-   *        The DOM element you want the panel to be anchored to.
-   *        - updateButtonCallback:
-   *        An optional function you want invoked when the user clicks the
-   *        Update button. If this function is not provided the Update button is
-   *        not shown.
-   *        - data:
-   *        An object that represents the object you want to inspect. Please see
-   *        the PropertyPanel documentation - this object is passed to the
-   *        PropertyPanel constructor
+   *        Options for the variables view:
+   *        - objectActor: grip of the ObjectActor you want to show in the
+   *        variables view.
+   *        - rawObject: the raw object you want to show in the variables view.
+   *        - label: label to display in the variables view for inspected
+   *        object.
+   *        - hideFilterInput: optional boolean, |true| if you want to hide the
+   *        variables view filter input.
+   *        - targetElement: optional nsIDOMElement to append the variables view
+   *        to. An iframe element is used as a container for the view. If this
+   *        option is not used, then the variables view opens in the sidebar.
+   * @return object
+   *         A Promise object that is resolved when the variables view has
+   *         opened. The new variables view instance is given to the callbacks.
+   */
+  openVariablesView: function JST_openVariablesView(aOptions)
+  {
+    let onContainerReady = (aWindow) => {
+      let container = aWindow.document.querySelector("#variables");
+      let view = this._variablesView;
+      if (!view || aOptions.targetElement) {
+        let viewOptions = {
+          container: container,
+          hideFilterInput: aOptions.hideFilterInput,
+        };
+        view = this._createVariablesView(viewOptions);
+        if (!aOptions.targetElement) {
+          this._variablesView = view;
+        }
+      }
+      aOptions.view = view;
+      this._updateVariablesView(aOptions);
+      this.emit("variablesview-open", view, aOptions);
+      return view;
+    };
+
+    let promise;
+    if (aOptions.targetElement) {
+      let deferred = Promise.defer();
+      promise = deferred.promise;
+      let document = aOptions.targetElement.ownerDocument;
+      let iframe = document.createElement("iframe");
+
+      iframe.addEventListener("load", function onIframeLoad(aEvent) {
+        iframe.removeEventListener("load", onIframeLoad, true);
+        deferred.resolve(iframe.contentWindow);
+      }, true);
+
+      iframe.flex = 1;
+      iframe.setAttribute("src", VARIABLES_VIEW_URL);
+      aOptions.targetElement.appendChild(iframe);
+    }
+    else {
+      this._createSidebar();
+      promise = this._addVariablesViewSidebarTab();
+    }
+
+    return promise.then(onContainerReady);
+  },
+
+  /**
+   * Create the Web Console sidebar.
+   *
+   * @see Sidebar.jsm
+   * @private
+   */
+  _createSidebar: function JST__createSidebar()
+  {
+    if (!this.sidebar) {
+      let tabbox = this.hud.document.querySelector("#webconsole-sidebar");
+      this.sidebar = new ToolSidebar(tabbox, this);
+    }
+    this.sidebar.show();
+    this._splitter.setAttribute("state", "open");
+  },
+
+  /**
+   * Add the variables view tab to the sidebar.
+   *
+   * @private
+   * @return object
+   *         A Promise object for the adding of the new tab.
+   */
+  _addVariablesViewSidebarTab: function JST__addVariablesViewSidebarTab()
+  {
+    let deferred = Promise.defer();
+
+    let onTabReady = () => {
+      let window = this.sidebar.getWindowForTab("variablesview");
+      deferred.resolve(window);
+    };
+
+    let tab = this.sidebar.getTab("variablesview");
+    if (tab) {
+      if (this.sidebar.getCurrentTabID() == "variablesview") {
+        onTabReady();
+      }
+      else {
+        this.sidebar.once("variablesview-selected", onTabReady);
+        this.sidebar.select("variablesview");
+      }
+    }
+    else {
+      this.sidebar.once("variablesview-ready", onTabReady);
+      this.sidebar.addTab("variablesview", VARIABLES_VIEW_URL, true);
+    }
+
+    return deferred.promise;
+  },
+
+  /**
+   * Create a variables view instance.
+   *
+   * @private
+   * @param object aOptions
+   *        Options for the new Variables View instance:
+   *        - container: the DOM element where the variables view is inserted.
+   *        - hideFilterInput: boolean, if true the variables filter input is
+   *        hidden.
    * @return object
-   *         The new instance of PropertyPanel.
+   *         The new Variables View instance.
+   */
+  _createVariablesView: function JST__createVariablesView(aOptions)
+  {
+    let view = new VariablesView(aOptions.container);
+    view.searchPlaceholder = l10n.getStr("propertiesFilterPlaceholder");
+    view.emptyText = l10n.getStr("emptyPropertiesList");
+    view.searchEnabled = !aOptions.hideFilterInput;
+    view.lazyEmpty = this._lazyVariablesView;
+    view.lazyAppend = this._lazyVariablesView;
+    this._objectActorsInVariablesViews.set(view, new Set());
+    return view;
+  },
+
+  /**
+   * Update the variables view.
+   *
+   * @private
+   * @param object aOptions
+   *        Options for updating the variables view:
+   *        - view: the view you want to update.
+   *        - objectActor: the grip of the new ObjectActor you want to show in
+   *        the view.
+   *        - rawObject: the new raw object you want to show.
+   *        - label: the new label for the inspected object.
    */
-  openPropertyPanel: function JST_openPropertyPanel(aOptions)
+  _updateVariablesView: function JST__updateVariablesView(aOptions)
+  {
+    let view = aOptions.view;
+    view.createHierarchy();
+    view.empty();
+
+    let actors = this._objectActorsInVariablesViews.get(view);
+    for (let actor of actors) {
+      // We need to avoid pruning the object inspection starting point.
+      // That one is pruned when the console message is removed.
+      if (view._consoleLastObjectActor != actor) {
+        this.hud._releaseObject(actor);
+      }
+    }
+
+    actors.clear();
+
+    if (aOptions.objectActor) {
+      // Make sure eval works in the correct context.
+      view.eval = this._variablesViewEvaluate.bind(this, aOptions);
+      view.switch = this._variablesViewSwitch.bind(this, aOptions);
+      view.delete = this._variablesViewDelete.bind(this, aOptions);
+    }
+    else {
+      view.eval = null;
+      view.switch = null;
+      view.delete = null;
+    }
+
+    let scope = view.addScope(aOptions.label);
+    scope.expanded = true;
+    scope.locked = true;
+
+    let container = scope.addVar();
+    container.evaluationMacro = this._variablesViewSimpleValueEvalMacro;
+
+    if (aOptions.objectActor) {
+      this._fetchVarProperties(container, aOptions.objectActor);
+      view._consoleLastObjectActor = aOptions.objectActor.actor;
+    }
+    else if (aOptions.rawObject) {
+      container.populate(aOptions.rawObject);
+      view.commitHierarchy();
+      view._consoleLastObjectActor = null;
+    }
+    else {
+      throw new Error("Variables View cannot open without giving it an object " +
+                      "display.");
+    }
+
+    this.emit("variablesview-updated", view, aOptions);
+  },
+
+  /**
+   * The evaluation function used by the variables view when editing a property
+   * value.
+   *
+   * @private
+   * @param object aOptions
+   *        The options used for |this._updateVariablesView()|.
+   * @param string aString
+   *        The string that the variables view wants to evaluate.
+   */
+  _variablesViewEvaluate: function JST__variablesViewEvaluate(aOptions, aString)
+  {
+    let updater = this._updateVariablesView.bind(this, aOptions);
+    let onEval = this._silentEvalCallback.bind(this, updater);
+
+    let evalOptions = {
+      frame: this.SELECTED_FRAME,
+      bindObjectActor: aOptions.objectActor.actor,
+    };
+
+    this.requestEvaluation(aString, evalOptions).then(onEval, onEval);
+  },
+
+  /**
+   * Generates the string evaluated when performing simple value changes in the
+   * variables view.
+   *
+   * @private
+   * @param Variable | Property aItem
+   *        The current variable or property.
+   * @param string aCurrentString
+   *        The trimmed user inputted string.
+   * @return string
+   *         The string to be evaluated.
+   */
+  _variablesViewSimpleValueEvalMacro:
+  function JST__variablesViewSimpleValueEvalMacro(aItem, aCurrentString)
+  {
+    return "_self" + aItem.symbolicName + "=" + aCurrentString;
+  },
+
+
+  /**
+   * Generates the string evaluated when overriding getters and setters with
+   * plain values in the variables view.
+   *
+   * @private
+   * @param Property aItem
+   *        The current getter or setter property.
+   * @param string aCurrentString
+   *        The trimmed user inputted string.
+   * @return string
+   *         The string to be evaluated.
+   */
+  _variablesViewOverrideValueEvalMacro:
+  function JST__variablesViewOverrideValueEvalMacro(aItem, aCurrentString)
   {
-    // The property panel has one button:
-    //    `Update`: reexecutes the string executed on the command line. The
-    //    result will be inspected by this panel.
-    let buttons = [];
-
-    if (aOptions.updateButtonCallback) {
-      buttons.push({
-        label: l10n.getStr("update.button"),
-        accesskey: l10n.getStr("update.accesskey"),
-        oncommand: aOptions.updateButtonCallback,
-      });
+    let parent = aItem.ownerView;
+    let symbolicName = parent.symbolicName;
+    if (symbolicName.indexOf("_self") != 0) {
+      parent._symbolicName = "_self" + symbolicName;
+    }
+
+    let result = VariablesView.overrideValueEvalMacro.apply(this, arguments);
+
+    parent._symbolicName = symbolicName;
+
+    return result;
+  },
+
+  /**
+   * Generates the string evaluated when performing getters and setters changes
+   * in the variables view.
+   *
+   * @private
+   * @param Property aItem
+   *        The current getter or setter property.
+   * @param string aCurrentString
+   *        The trimmed user inputted string.
+   * @return string
+   *         The string to be evaluated.
+   */
+  _variablesViewGetterOrSetterEvalMacro:
+  function JST__variablesViewGetterOrSetterEvalMacro(aItem, aCurrentString)
+  {
+    let propertyObject = aItem.ownerView;
+    let parentObject = propertyObject.ownerView;
+    let parent = parentObject.symbolicName;
+    parentObject._symbolicName = "_self" + parent;
+
+    let result = VariablesView.getterOrSetterEvalMacro.apply(this, arguments);
+
+    parentObject._symbolicName = parent;
+
+    return result;
+  },
+
+  /**
+   * The property deletion function used by the variables view when a property
+   * is deleted.
+   *
+   * @private
+   * @param object aOptions
+   *        The options used for |this._updateVariablesView()|.
+   * @param object aVar
+   *        The Variable object instance for the deleted property.
+   */
+  _variablesViewDelete: function JST__variablesViewDelete(aOptions, aVar)
+  {
+    let onEval = this._silentEvalCallback.bind(this, null);
+
+    let evalOptions = {
+      frame: this.SELECTED_FRAME,
+      bindObjectActor: aOptions.objectActor.actor,
+    };
+
+    this.requestEvaluation("delete _self" + aVar.symbolicName, evalOptions)
+        .then(onEval, onEval);
+  },
+
+  /**
+   * The property rename function used by the variables view when a property
+   * is renamed.
+   *
+   * @private
+   * @param object aOptions
+   *        The options used for |this._updateVariablesView()|.
+   * @param object aVar
+   *        The Variable object instance for the renamed property.
+   * @param string aNewName
+   *        The new name for the property.
+   */
+  _variablesViewSwitch:
+  function JST__variablesViewSwitch(aOptions, aVar, aNewName)
+  {
+    let updater = this._updateVariablesView.bind(this, aOptions);
+    let onEval = this._silentEvalCallback.bind(this, updater);
+
+    let evalOptions = {
+      frame: this.SELECTED_FRAME,
+      bindObjectActor: aOptions.objectActor.actor,
+    };
+
+    let newSymbolicName = aVar.ownerView.symbolicName + '["' + aNewName + '"]';
+    if (newSymbolicName == aVar.symbolicName) {
+      return;
+    }
+
+    let code = "_self" + newSymbolicName + " = _self" + aVar.symbolicName + ";" +
+               "delete _self" + aVar.symbolicName;
+
+    this.requestEvaluation(code, evalOptions).then(onEval, onEval);
+  },
+
+  /**
+   * A noop callback for JavaScript evaluation. This method releases any
+   * result ObjectActors that come from the server for evaluation requests. This
+   * is used for editing, renaming and deleting properties in the variables
+   * view.
+   *
+   * Exceptions are displayed in the output.
+   *
+   * @private
+   * @param function aCallback
+   *        Function to invoke once the response is received.
+   * @param object aResponse
+   *        The response packet received from the server.
+   */
+  _silentEvalCallback: function JST__silentEvalCallback(aCallback, aResponse)
+  {
+    if (aResponse.error) {
+      Cu.reportError("Web Console evaluation failed. " + aResponse.error + ":" +
+                     aResponse.message);
+
+      aCallback && aCallback(aResponse);
+      return;
+    }
+
+    let exception = aResponse.exception;
+    if (exception) {
+      let node = this.writeOutput(aResponse.exceptionMessage,
+                                  CATEGORY_OUTPUT, SEVERITY_ERROR,
+                                  null, aResponse.timestamp);
+      node._objectActors = new Set();
+      if (WebConsoleUtils.isActorGrip(exception)) {
+        node._objectActors.add(exception.actor);
+      }
     }
 
-    let parent = this.hud.popupset;
-    let title = aOptions.title ?
-                l10n.getFormatStr("jsPropertyInspectTitle", [aOptions.title]) :
-                l10n.getStr("jsPropertyTitle");
-
-    let propPanel = new PropertyPanel(parent, title, aOptions.data, buttons);
-
-    propPanel.panel.openPopup(aOptions.anchor, "after_pointer", 0, 0, false, false);
-    propPanel.panel.sizeTo(350, 450);
-
-    if (aOptions.anchor) {
-      propPanel.panel.addEventListener("popuphiding", function onPopupHide() {
-        propPanel.panel.removeEventListener("popuphiding", onPopupHide, false);
-        aOptions.anchor._panelOpen = false;
-      }, false);
-      aOptions.anchor._panelOpen = true;
+    let helper = aResponse.helperResult || { type: null };
+    let helperGrip = null;
+    if (helper.type == "inspectObject") {
+      helperGrip = helper.object;
+    }
+
+    let grips = [aResponse.result, helperGrip];
+    for (let grip of grips) {
+      if (WebConsoleUtils.isActorGrip(grip)) {
+        this.hud._releaseObject(grip.actor);
+      }
+    }
+
+    aCallback && aCallback(aResponse);
+  },
+
+  /**
+   * Adds properties to a variable in the view. Triggered when a variable is
+   * expanded. It does not expand the variable.
+   *
+   * @param object aVar
+   *        The VariablseView Variable instance where the properties get added.
+   * @param object [aGrip]
+   *        Optional, the object actor grip of the variable. If the grip is not
+   *        provided, then the aVar.value is used as the object actor grip.
+   */
+  _fetchVarProperties: function JST__fetchVarProperties(aVar, aGrip)
+  {
+    // Retrieve the properties only once.
+    if (aVar._fetched) {
+      return;
+    }
+    aVar._fetched = true;
+
+    let grip = aGrip || aVar.value;
+    if (!grip) {
+      throw new Error("No object actor grip was given for the variable.");
+    }
+
+    let view = aVar._variablesView;
+    let actors = this._objectActorsInVariablesViews.get(view);
+
+    function addActorForDescriptor(aGrip) {
+      if (WebConsoleUtils.isActorGrip(aGrip)) {
+        actors.add(aGrip.actor);
+      }
     }
 
-    return propPanel;
+    let onNewProperty = (aProperty) => {
+      if (aProperty.getter || aProperty.setter) {
+        aProperty.evaluationMacro = this._variablesViewOverrideValueEvalMacro;
+        let getter = aProperty.get("get");
+        let setter = aProperty.get("set");
+        if (getter) {
+          getter.evaluationMacro = this._variablesViewGetterOrSetterEvalMacro;
+        }
+        if (setter) {
+          setter.evaluationMacro = this._variablesViewGetterOrSetterEvalMacro;
+        }
+      }
+      else {
+        aProperty.evaluationMacro = this._variablesViewSimpleValueEvalMacro;
+      }
+
+      let grips = [aProperty.value, aProperty.gettter, aProperty.settter];
+      grips.forEach(addActorForDescriptor);
+
+      let inspectable = !VariablesView.isPrimitive({ value: aProperty.value });
+      let longString = WebConsoleUtils.isActorGrip(aProperty.value) &&
+                       aProperty.value.type == "longString";
+      if (inspectable) {
+        aProperty.onexpand = this._fetchVarProperties;
+      }
+      else if (longString) {
+        aProperty.onexpand = this._fetchVarLongString;
+        aProperty.showArrow();
+      }
+    };
+
+    let client = new GripClient(this.hud.proxy.client, grip);
+    client.getPrototypeAndProperties((aResponse) => {
+      let { ownProperties, prototype } = aResponse;
+      let sortable = VariablesView.NON_SORTABLE_CLASSES.indexOf(grip.class) == -1;
+
+      // Add all the variable properties.
+      if (ownProperties) {
+        aVar.addProperties(ownProperties, {
+          sorted: sortable,
+          callback: onNewProperty,
+        });
+      }
+
+      // Add the variable's __proto__.
+      if (prototype && prototype.type != "null") {
+        let proto = aVar.addProperty("__proto__", { value: prototype });
+        onNewProperty(proto);
+      }
+
+      aVar._retrieved = true;
+      view.commitHierarchy();
+      this.emit("variablesview-fetched", aVar);
+    });
+  },
+
+  /**
+   * Fetch the full string for a given variable that displays a long string.
+   *
+   * @param object aVar
+   *        The VariablesView Variable instance where the properties get added.
+   */
+  _fetchVarLongString: function JST__fetchVarLongString(aVar)
+  {
+    if (aVar._fetched) {
+      return;
+    }
+    aVar._fetched = true;
+
+    let grip = aVar.value;
+    if (!grip) {
+      throw new Error("No long string actor grip was given for the variable.");
+    }
+
+    let client = this.webConsoleClient.longString(grip);
+    client.substring(grip.initial.length, grip.length, (aResponse) => {
+      if (aResponse.error) {
+        Cu.reportError("JST__fetchVarLongString substring failure: " +
+                       aResponse.error + ": " + aResponse.message);
+        return;
+      }
+
+      aVar.onexpand = null;
+      aVar.setGrip(grip.initial + aResponse.substring);
+      aVar.hideArrow();
+      aVar._retrieved = true;
+    });
   },
 
   /**
    * Writes a JS object to the JSTerm outputNode.
    *
    * @param string aOutputMessage
    *        The message to display.
    * @param function [aCallback]
@@ -3593,171 +4099,49 @@ JSTerm.prototype = {
   updateCompleteNode: function JSTF_updateCompleteNode(aSuffix)
   {
     // completion prefix = input, with non-control chars replaced by spaces
     let prefix = aSuffix ? this.inputNode.value.replace(/[\S]/g, " ") : "";
     this.completeNode.value = prefix + aSuffix;
   },
 
   /**
-   * The JSTerm InspectObject remote message handler. This allows the remote
-   * process to open the Property Panel for a given object.
-   *
-   * @param object aRequest
-   *        The request message from the content process. This message includes
-   *        the user input string that was evaluated to inspect an object and
-   *        the result object which is to be inspected.
-   */
-  handleInspectObject: function JST_handleInspectObject(aInput, aActor)
-  {
-    let options = {
-      title: aInput,
-
-      data: {
-        objectPropertiesProvider: this.hud.objectPropertiesProvider.bind(this.hud),
-        releaseObject: this.hud._releaseObject.bind(this.hud),
-      },
-    };
-
-    let propPanel;
-
-    let onPopupHide = function JST__onPopupHide() {
-      propPanel.panel.removeEventListener("popuphiding", onPopupHide, false);
-      this.hud._releaseObject(aActor.actor);
-    }.bind(this);
-
-    this.hud.objectPropertiesProvider(aActor.actor,
-      function _onObjectProperties(aProperties) {
-        options.data.objectProperties = aProperties;
-        propPanel = this.openPropertyPanel(options);
-        propPanel.panel.setAttribute("hudId", this.hudId);
-        propPanel.panel.addEventListener("popuphiding", onPopupHide, false);
-      }.bind(this));
-  },
-
-  /**
    * The click event handler for evaluation results in the output.
    *
    * @private
    * @param object aResponse
    *        The JavaScript evaluation response received from the server.
-   * @param nsIDOMNode aLink
-   *        The message node for which we are handling events.
    */
-  _evalOutputClick: function JST__evalOutputClick(aResponse, aLinkNode)
+  _evalOutputClick: function JST__evalOutputClick(aResponse)
   {
-    if (aLinkNode._panelOpen) {
-      return;
-    }
-
-    let options = {
-      title: aResponse.input,
-      anchor: aLinkNode,
-
-      // Data to inspect.
-      data: {
-        objectPropertiesProvider: this.hud.objectPropertiesProvider.bind(this.hud),
-        releaseObject: this.hud._releaseObject.bind(this.hud),
-      },
-    };
-
-    let propPanel;
-
-    options.updateButtonCallback = function JST__evalUpdateButton() {
-      let onResult =
-        this._evalOutputUpdatePanelCallback.bind(this, options, propPanel,
-                                                 aResponse);
-      this.webConsoleClient.evaluateJS(aResponse.input, onResult);
-    }.bind(this);
-
-    let onPopupHide = function JST__evalInspectPopupHide() {
-      propPanel.panel.removeEventListener("popuphiding", onPopupHide, false);
-
-      if (!aLinkNode.parentNode && aLinkNode._objectActors) {
-        aLinkNode._objectActors.forEach(this.hud._releaseObject, this.hud);
-        aLinkNode._objectActors = null;
-      }
-    }.bind(this);
-
-    this.hud.objectPropertiesProvider(aResponse.result.actor,
-      function _onObjectProperties(aProperties) {
-        options.data.objectProperties = aProperties;
-        propPanel = this.openPropertyPanel(options);
-        propPanel.panel.setAttribute("hudId", this.hudId);
-        propPanel.panel.addEventListener("popuphiding", onPopupHide, false);
-      }.bind(this));
-  },
-
-  /**
-   * The callback used for updating the Property Panel when the user clicks the
-   * Update button.
-   *
-   * @private
-   * @param object aOptions
-   *        The options object used for opening the initial Property Panel.
-   * @param object aPropPanel
-   *        The Property Panel instance.
-   * @param object aOldResponse
-   *        The previous JSTerm:EvalResult message received from the content
-   *        process.
-   * @param object aNewResponse
-   *        The new JSTerm:EvalResult message received after the user clicked
-   *        the Update button.
-   */
-  _evalOutputUpdatePanelCallback:
-  function JST__updatePanelCallback(aOptions, aPropPanel, aOldResponse,
-                                    aNewResponse)
-  {
-    if (aNewResponse.errorMessage) {
-      this.writeOutput(aNewResponse.errorMessage, CATEGORY_OUTPUT,
-                       SEVERITY_ERROR);
-      return;
-    }
-
-    let result = aNewResponse.result;
-    let inspectable = result && typeof result == "object" && result.inspectable;
-    let newActor = result && typeof result == "object" ? result.actor : null;
-
-    let anchor = aOptions.anchor;
-    if (anchor && newActor) {
-      if (!anchor._objectActors) {
-        anchor._objectActors = [];
-      }
-      if (anchor._objectActors.indexOf(newActor) == -1) {
-        anchor._objectActors.push(newActor);
-      }
-    }
-
-    if (!inspectable) {
-      this.writeOutput(l10n.getStr("JSTerm.updateNotInspectable"), CATEGORY_OUTPUT, SEVERITY_ERROR);
-      return;
-    }
-
-    // Update the old response object such that when the panel is reopen, the
-    // user sees the new response.
-    aOldResponse.result = aNewResponse.result;
-    aOldResponse.error = aNewResponse.error;
-    aOldResponse.errorMessage = aNewResponse.errorMessage;
-    aOldResponse.timestamp = aNewResponse.timestamp;
-
-    this.hud.objectPropertiesProvider(newActor,
-      function _onObjectProperties(aProperties) {
-        aOptions.data.objectProperties = aProperties;
-        // TODO: This updates the value of the tree.
-        // However, the states of open nodes is not saved.
-        // See bug 586246.
-        aPropPanel.treeView.data = aOptions.data;
-      }.bind(this));
+    this.openVariablesView({
+      label: VariablesView.getString(aResponse.result),
+      objectActor: aResponse.result,
+    });
   },
 
   /**
    * Destroy the JSTerm object. Call this method to avoid memory leaks.
    */
   destroy: function JST_destroy()
   {
+    if (this._variablesView) {
+      let actors = this._objectActorsInVariablesViews.get(this._variablesView);
+      for (let actor of actors) {
+        this.hud._releaseObject(actor);
+      }
+      actors.clear();
+      this._variablesView = null;
+    }
+
+    if (this.sidebar) {
+      this.sidebar.destroy();
+      this.sidebar = null;
+    }
+
     this.clearCompletion();
     this.clearOutput();
 
     this.autocompletePopup.destroy();
     this.autocompletePopup = null;
 
     let popup = this.hud.owner.chromeDocument
                 .getElementById("webConsole_autocompletePopup");
--- a/browser/devtools/webconsole/webconsole.xul
+++ b/browser/devtools/webconsole/webconsole.xul
@@ -55,17 +55,17 @@
       <menuitem id="menu_copyURL" label="&copyURLCmd.label;"
                 accesskey="&copyURLCmd.accesskey;" command="consoleCmd_copyURL"
                 selection="network" selectionType="single"/>
       <menuitem id="menu_copy"/>
       <menuitem id="menu_selectAll"/>
     </menupopup>
   </popupset>
 
-  <vbox class="hud-outer-wrapper" flex="1">
+  <hbox class="hud-outer-wrapper" flex="1">
     <vbox class="hud-console-wrapper" flex="1">
       <toolbar class="hud-console-filter-toolbar devtools-toolbar" mode="full">
         <toolbarbutton label="&btnPageNet.label;" type="menu-button"
                        category="net" class="devtools-toolbarbutton webconsole-filter-button"
                        tooltiptext="&btnPageNet.tooltip;">
           <menupopup>
             <menuitem label="&btnConsoleErrors;" type="checkbox" autocheck="false"
                       prefKey="network"/>
@@ -109,25 +109,34 @@
             <menuitem label="&btnConsoleLog;" type="checkbox" autocheck="false"
                       prefKey="log"/>
           </menupopup>
         </toolbarbutton>
 
         <spacer flex="1"/>
 
         <textbox class="compact hud-filter-box devtools-searchinput" type="search"
-                 placeholder="&filterBox.placeholder;"/>
+                 placeholder="&filterOutput.placeholder;"/>
         <toolbarbutton class="webconsole-clear-console-button devtools-toolbarbutton"
                        label="&btnClear.label;" tooltiptext="&btnClear.tooltip;"/>
       </toolbar>
+
       <richlistbox class="hud-output-node" orient="vertical" flex="1"
                    seltype="multiple" context="output-contextmenu"
                    style="direction:ltr;"/>
+
       <hbox class="jsterm-input-container" style="direction:ltr">
         <stack class="jsterm-stack-node" flex="1">
           <textbox class="jsterm-complete-node" multiline="true" rows="1"
                    tabindex="-1"/>
           <textbox class="jsterm-input-node" multiline="true" rows="1"/>
         </stack>
       </hbox>
     </vbox>
-  </vbox>
+
+    <splitter class="devtools-side-splitter" collapse="after" state="collapsed" />
+
+    <tabbox id="webconsole-sidebar" class="devtools-sidebar-tabs" hidden="true" width="300">
+      <tabs/>
+      <tabpanels flex="1"/>
+    </tabbox>
+  </hbox>
 </window>
new file mode 100644
--- /dev/null
+++ b/browser/locales/en-US/chrome/browser/devtools/VariablesView.dtd
@@ -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/. -->
+
+<!-- LOCALIZATION NOTE : FILE 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. -->
+
+<!ENTITY PropertiesViewWindowTitle "Properties">
+
--- a/browser/locales/en-US/chrome/browser/devtools/webConsole.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/webConsole.dtd
@@ -55,17 +55,17 @@
   -  console.error(). -->
 <!ENTITY btnPageLogging.label   "Logging">
 <!ENTITY btnPageLogging.tooltip "Log messages sent to the window.console object">
 <!ENTITY btnConsoleErrors       "Errors">
 <!ENTITY btnConsoleInfo         "Info">
 <!ENTITY btnConsoleWarnings     "Warnings">
 <!ENTITY btnConsoleLog          "Log">
 
-<!ENTITY filterBox.placeholder "Filter">
+<!ENTITY filterOutput.placeholder "Filter output">
 <!ENTITY btnClear.label        "Clear">
 <!ENTITY btnClear.tooltip      "Clear the Web Console output">
 
 <!ENTITY fullZoomEnlargeCmd.commandkey  "+">
 <!ENTITY fullZoomEnlargeCmd.commandkey2 "="> <!-- + is above this key on many keyboards -->
 <!ENTITY fullZoomEnlargeCmd.commandkey3 "">
 
 <!ENTITY fullZoomReduceCmd.commandkey   "-">
--- a/browser/locales/en-US/chrome/browser/devtools/webconsole.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/webconsole.properties
@@ -192,8 +192,17 @@ NetworkPanel.fetchRemainingResponseConte
 # LOCALIZATION NOTE (NetworkPanel.fetchRemainingRequestContentLink): This is
 # displayed in the network panel when the request body is only partially
 # available.
 NetworkPanel.fetchRemainingRequestContentLink=Fetch the request body (%1$S bytes)
 
 # LOCALIZATION NOTE (connectionTimeout): Message displayed when the Remote Web
 # Console fails to connect to the server due to a timeout.
 connectionTimeout=Connection timeout. Check the Error Console on both ends for potential error messages. Reopen the Web Console to try again.
+
+# LOCALIZATION NOTE (propertiesFilterPlaceholder): This is the text that
+# appears in the filter text box for the properties view container.
+propertiesFilterPlaceholder=Filter properties
+
+# LOCALIZATION NOTE (emptyPropertiesList): The text that is displayed in the
+# properties pane when there are no properties to display.
+emptyPropertiesList=No properties to display
+
--- a/browser/locales/jar.mn
+++ b/browser/locales/jar.mn
@@ -33,16 +33,17 @@
     locale/browser/devtools/tilt.properties           (%chrome/browser/devtools/tilt.properties)
     locale/browser/devtools/scratchpad.properties     (%chrome/browser/devtools/scratchpad.properties)
     locale/browser/devtools/scratchpad.dtd            (%chrome/browser/devtools/scratchpad.dtd)
     locale/browser/devtools/styleeditor.properties    (%chrome/browser/devtools/styleeditor.properties)
     locale/browser/devtools/styleeditor.dtd           (%chrome/browser/devtools/styleeditor.dtd)
     locale/browser/devtools/styleinspector.properties (%chrome/browser/devtools/styleinspector.properties)
     locale/browser/devtools/styleinspector.dtd        (%chrome/browser/devtools/styleinspector.dtd)
     locale/browser/devtools/webConsole.dtd            (%chrome/browser/devtools/webConsole.dtd)
+    locale/browser/devtools/VariablesView.dtd         (%chrome/browser/devtools/VariablesView.dtd)
     locale/browser/devtools/sourceeditor.properties   (%chrome/browser/devtools/sourceeditor.properties)
     locale/browser/devtools/sourceeditor.dtd          (%chrome/browser/devtools/sourceeditor.dtd)
     locale/browser/devtools/profiler.dtd              (%chrome/browser/devtools/profiler.dtd)
     locale/browser/devtools/profiler.properties       (%chrome/browser/devtools/profiler.properties)
     locale/browser/devtools/layoutview.dtd            (%chrome/browser/devtools/layoutview.dtd)
     locale/browser/devtools/responsiveUI.properties   (%chrome/browser/devtools/responsiveUI.properties)
     locale/browser/devtools/toolbox.dtd            (%chrome/browser/devtools/toolbox.dtd)
     locale/browser/devtools/toolbox.properties     (%chrome/browser/devtools/toolbox.properties)
--- a/browser/themes/linux/devtools/webconsole.css
+++ b/browser/themes/linux/devtools/webconsole.css
@@ -234,8 +234,23 @@
 :-moz-any(.jsterm-input-node,
           .jsterm-complete-node) > .textbox-input-box > .textbox-textarea {
   overflow-x: hidden;
 }
 
 .jsterm-complete-node > .textbox-input-box > .textbox-textarea {
   color: GrayText;
 }
+
+.webconsole-msg-inspector iframe {
+  height: 7em;
+  margin-bottom: 15px;
+}
+
+.devtools-side-splitter {
+  background: #666;
+  width: 2px;
+}
+
+#webconsole-sidebar > tabs {
+  height: 0;
+  overflow: hidden;
+}
--- a/browser/themes/osx/devtools/webconsole.css
+++ b/browser/themes/osx/devtools/webconsole.css
@@ -238,8 +238,23 @@
 :-moz-any(.jsterm-input-node,
           .jsterm-complete-node) > .textbox-input-box > .textbox-textarea {
   overflow-x: hidden;
 }
 
 .jsterm-complete-node > .textbox-input-box > .textbox-textarea {
   color: GrayText;
 }
+
+.webconsole-msg-inspector iframe {
+  height: 7em;
+  margin-bottom: 15px;
+}
+
+.devtools-side-splitter {
+  background: #666;
+  width: 2px;
+}
+
+#webconsole-sidebar > tabs {
+  height: 0;
+  overflow: hidden;
+}
--- a/browser/themes/windows/devtools/webconsole.css
+++ b/browser/themes/windows/devtools/webconsole.css
@@ -242,8 +242,23 @@
 /*
  * This hardcoded width likely due to a toolkit Windows specific bug.
  * See http://hg.mozilla.org/mozilla-central/annotate/f38d6df93cad/toolkit/themes/winstripe/global/textbox-aero.css#l7
  */
 
 .hud-filter-box {
   width: 200px;
 }
+
+.webconsole-msg-inspector iframe {
+  height: 7em;
+  margin-bottom: 15px;
+}
+
+.devtools-side-splitter {
+  background: #666;
+  width: 2px;
+}
+
+#webconsole-sidebar > tabs {
+  height: 0;
+  overflow: hidden;
+}
--- a/dom/tests/browser/browser_ConsoleAPITests.js
+++ b/dom/tests/browser/browser_ConsoleAPITests.js
@@ -321,17 +321,18 @@ function testConsoleTimeEnd(aMessageObje
   ok(aMessageObject.arguments, "we have arguments");
 
   is(aMessageObject.filename, gArgs[0].filename, "filename matches");
   is(aMessageObject.lineNumber, gArgs[0].lineNumber, "lineNumber matches");
   is(aMessageObject.functionName, gArgs[0].functionName, "functionName matches");
   is(aMessageObject.arguments.length, gArgs[0].arguments.length, "arguments.length matches");
   is(aMessageObject.timer.name, gArgs[0].timer.name, "timer name matches");
   is(typeof aMessageObject.timer.duration, "number", "timer duration is a number");
-  ok(aMessageObject.timer.duration > 0, "timer duration is positive");
+  info("timer duration: " + aMessageObject.timer.duration);
+  ok(aMessageObject.timer.duration >= 0, "timer duration is positive");
 
   gArgs[0].arguments.forEach(function (a, i) {
     is(aMessageObject.arguments[i], a, "correct arg " + i);
   });
 
   startEmptyTimerTest();
 }