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 128120 dfc808a01756195e3ebbc315bf6f87e6cf95e489
parent 128119 f5d6c95a9de90e6b2ea07f3ff1b502e36da4597b
child 128121 26fb3bd67f5f613835504058d6708cb2440d592e
push id24521
push userryanvm@gmail.com
push dateTue, 09 Apr 2013 18:31:04 +0000
treeherdermozilla-central@9d5f05a6d497 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspast, vporof, paul
bugs808370
milestone23.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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();
 }