Bug 831794 - Variables View: allow users to override getter properties to plain value properties, r=past
authorVictor Porof <vporof@mozilla.com>
Thu, 31 Jan 2013 18:07:24 +0200
changeset 120275 af82229821f8
parent 120274 d3156a1adca7
child 120276 03c38d3c4182
push id1322
push uservporof@mozilla.com
push dateThu, 31 Jan 2013 16:08:04 +0000
treeherderfx-team@73ccb94a810d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspast
bugs831794
milestone21.0a1
Bug 831794 - Variables View: allow users to override getter properties to plain value properties, r=past
browser/devtools/debugger/test/Makefile.in
browser/devtools/debugger/test/browser_dbg_frame-parameters.html
browser/devtools/debugger/test/browser_dbg_propertyview-07.js
browser/devtools/debugger/test/browser_dbg_propertyview-08.js
browser/devtools/debugger/test/browser_dbg_propertyview-09.js
browser/devtools/debugger/test/browser_dbg_propertyview-10.js
browser/devtools/debugger/test/browser_dbg_propertyview-big-data.js
browser/devtools/debugger/test/browser_dbg_propertyview-data-big.js
browser/devtools/debugger/test/browser_dbg_propertyview-data-getset-01.js
browser/devtools/debugger/test/browser_dbg_propertyview-data-getset-02.js
browser/devtools/debugger/test/browser_dbg_propertyview-edit-value.js
browser/devtools/debugger/test/browser_dbg_propertyview-edit.js
browser/devtools/shared/VariablesView.jsm
browser/locales/en-US/chrome/browser/devtools/debugger.properties
browser/themes/gnomestripe/devtools/debugger.css
browser/themes/pinstripe/devtools/debugger.css
browser/themes/winstripe/devtools/debugger.css
--- a/browser/devtools/debugger/test/Makefile.in
+++ b/browser/devtools/debugger/test/Makefile.in
@@ -27,19 +27,21 @@ MOCHITEST_BROWSER_TESTS = \
 	browser_dbg_propertyview-03.js \
 	browser_dbg_propertyview-04.js \
 	browser_dbg_propertyview-05.js \
 	browser_dbg_propertyview-06.js \
 	browser_dbg_propertyview-07.js \
 	browser_dbg_propertyview-08.js \
 	browser_dbg_propertyview-09.js \
 	browser_dbg_propertyview-10.js \
-	browser_dbg_propertyview-edit.js \
+	browser_dbg_propertyview-edit-value.js \
 	browser_dbg_propertyview-edit-watch.js \
-	browser_dbg_propertyview-big-data.js \
+	browser_dbg_propertyview-data-big.js \
+	browser_dbg_propertyview-data-getset-01.js \
+	browser_dbg_propertyview-data-getset-02.js \
 	browser_dbg_propertyview-data.js \
 	browser_dbg_propertyview-filter-01.js \
 	browser_dbg_propertyview-filter-02.js \
 	browser_dbg_propertyview-filter-03.js \
 	browser_dbg_propertyview-filter-04.js \
 	browser_dbg_propertyview-filter-05.js \
 	browser_dbg_propertyview-filter-06.js \
 	browser_dbg_propertyview-filter-07.js \
--- a/browser/devtools/debugger/test/browser_dbg_frame-parameters.html
+++ b/browser/devtools/debugger/test/browser_dbg_frame-parameters.html
@@ -6,16 +6,21 @@
     <!-- Any copyright is dedicated to the Public Domain.
          http://creativecommons.org/publicdomain/zero/1.0/ -->
     <script type="text/javascript">
       window.addEventListener("load", function() {
         function test(aArg, bArg, cArg, dArg, eArg, fArg) {
           var a = 1;
           var b = { a: a };
           var c = { a: 1, b: "beta", c: true, d: b };
+          var myVar = {
+            _prop: 42,
+            get prop() { return this._prop; },
+            set prop(val) { this._prop = val; }
+          };
 
           debugger;
         }
         function load() {
           var a = { a: 1, b: "beta", c: true };
           var e = eval("test(a, 'beta', 3, false, null)");
         }
         var button = document.querySelector("button");
--- a/browser/devtools/debugger/test/browser_dbg_propertyview-07.js
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-07.js
@@ -35,17 +35,17 @@ function testFrameParameters()
           localNodes = localScope.querySelector(".details").childNodes;
 
       is(gDebugger.DebuggerController.activeThread.state, "paused",
         "Should only be getting stack frames while paused.");
 
       is(frames.querySelectorAll(".dbg-stackframe").length, 3,
         "Should have three frames.");
 
-      is(localNodes.length, 11,
+      is(localNodes.length, 12,
         "The localScope should contain all the created variable elements.");
 
       is(localNodes[0].querySelector(".value").getAttribute("value"), "[object Proxy]",
         "Should have the right property value for 'this'.");
 
       is(localNodes[1].querySelector(".value").getAttribute("value"), "[object Object]",
         "Should have the right property value for 'aArg'.");
 
--- a/browser/devtools/debugger/test/browser_dbg_propertyview-08.js
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-08.js
@@ -35,17 +35,17 @@ function testFrameParameters()
           localNonEnums = localScope.querySelector(".nonenum").childNodes;
 
       is(gDebugger.DebuggerController.activeThread.state, "paused",
         "Should only be getting stack frames while paused.");
 
       is(frames.querySelectorAll(".dbg-stackframe").length, 3,
         "Should have three frames.");
 
-      is(localNodes.length + localNonEnums.length, 11,
+      is(localNodes.length + localNonEnums.length, 12,
         "The localScope and localNonEnums should contain all the created variable elements.");
 
       is(localNodes[0].querySelector(".value").getAttribute("value"), "[object Proxy]",
         "Should have the right property value for 'this'.");
       is(localNodes[8].querySelector(".value").getAttribute("value"), "[object Arguments]",
         "Should have the right property value for 'arguments'.");
       is(localNodes[10].querySelector(".value").getAttribute("value"), "[object Object]",
         "Should have the right property value for 'c'.");
--- a/browser/devtools/debugger/test/browser_dbg_propertyview-09.js
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-09.js
@@ -48,17 +48,17 @@ function testFrameParameters()
         "Should only be getting stack frames while paused.");
 
       is(frames.querySelectorAll(".dbg-stackframe").length, 3,
         "Should have three frames.");
 
       is(globalNodes[0].querySelector(".name").getAttribute("value"), "InstallTrigger",
         "Should have the right property name for |InstallTrigger|.");
 
-      is(globalNodes[0].querySelector(".value").getAttribute("value"), "undefined",
+      is(globalNodes[0].querySelector(".value").getAttribute("value"), "",
         "Should have the right property value for |InstallTrigger|.");
 
       is(globalNodes[1].querySelector(".name").getAttribute("value"), "SpecialPowers",
         "Should have the right property name for |SpecialPowers|.");
 
       is(globalNodes[1].querySelector(".value").getAttribute("value"), "[object Proxy]",
         "Should have the right property value for |SpecialPowers|.");
 
--- a/browser/devtools/debugger/test/browser_dbg_propertyview-10.js
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-10.js
@@ -59,17 +59,17 @@ function testWithFrame()
         "Should have the right property name for |one|.");
 
       is(innerNodes[1].querySelector(".value").getAttribute("value"), "1",
         "Should have the right property value for |one|.");
 
       is(globalNodes[0].querySelector(".name").getAttribute("value"), "InstallTrigger",
         "Should have the right property name for |InstallTrigger|.");
 
-      is(globalNodes[0].querySelector(".value").getAttribute("value"), "undefined",
+      is(globalNodes[0].querySelector(".value").getAttribute("value"), "",
         "Should have the right property value for |InstallTrigger|.");
 
       let len = globalNodes.length - 1;
       is(globalNodes[len].querySelector(".name").getAttribute("value"), "window",
         "Should have the right property name for |window|.");
 
       is(globalNodes[len].querySelector(".value").getAttribute("value"), "[object Proxy]",
         "Should have the right property value for |window|.");
rename from browser/devtools/debugger/test/browser_dbg_propertyview-big-data.js
rename to browser/devtools/debugger/test/browser_dbg_propertyview-data-big.js
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-data-getset-01.js
@@ -0,0 +1,343 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Make sure that the property view knows how to edit getters and setters.
+ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_frame-parameters.html";
+
+var gPane = null;
+var gTab = null;
+var gDebugger = null;
+var gVars = null;
+var gWatch = null;
+
+requestLongerTimeout(2);
+
+function test()
+{
+  debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+    gTab = aTab;
+    gPane = aPane;
+    gDebugger = gPane.panelWin;
+    gVars = gDebugger.DebuggerView.Variables;
+    gWatch = gDebugger.DebuggerView.WatchExpressions;
+
+    gVars.switch = function() {};
+    gVars.delete = function() {};
+
+    prepareVariablesView();
+  });
+}
+
+function prepareVariablesView() {
+  gDebugger.addEventListener("Debugger:FetchedVariables", function test() {
+    gDebugger.removeEventListener("Debugger:FetchedVariables", test, false);
+    Services.tm.currentThread.dispatch({ run: function() {
+
+      testVariablesView();
+
+    }}, 0);
+  }, false);
+
+  EventUtils.sendMouseEvent({ type: "click" },
+    content.document.querySelector("button"),
+    content.window);
+}
+
+function testVariablesView()
+{
+  executeSoon(function() {
+    addWatchExpressions(function() {
+      testEdit("set", "this._prop = value + ' BEER CAN'", function() {
+        testEdit("set", "{ this._prop = value + ' BEACON' }", function() {
+          testEdit("set", "{ this._prop = value + ' BEACON;'; }", function() {
+            testEdit("set", "{ return this._prop = value + ' BEACON;;'; }", function() {
+              testEdit("set", "function(value) { this._prop = value + ' BACON' }", function() {
+                testEdit("get", "'brelx BEER CAN'", function() {
+                  testEdit("get", "{ 'brelx BEACON' }", function() {
+                    testEdit("get", "{ 'brelx BEACON;'; }", function() {
+                      testEdit("get", "{ return 'brelx BEACON;;'; }", function() {
+                        testEdit("get", "function() { return 'brelx BACON'; }", function() {
+                          testEdit("get", "bogus", function() {
+                            testEdit("set", "sugob", function() {
+                              testEdit("get", "", function() {
+                                testEdit("set", "", function() {
+                                  waitForWatchExpressions(function() {
+                                    testEdit("self", "2507", function() {
+                                      closeDebuggerAndFinish();
+                                    }, {
+                                      "myVar.prop": 2507,
+                                      "myVar.prop + 42": "250742"
+                                    });
+                                  })
+                                  gWatch.deleteExpression({ name: "myVar.prop = 'xlerb'" });
+                                }, {
+                                  "myVar.prop": "xlerb",
+                                  "myVar.prop + 42": NaN,
+                                  "myVar.prop = 'xlerb'": "xlerb"
+                                });
+                              }, {
+                                "myVar.prop": undefined,
+                                "myVar.prop + 42": NaN,
+                                "myVar.prop = 'xlerb'": "ReferenceError: sugob is not defined"
+                              });
+                            }, {
+                              "myVar.prop": "ReferenceError: bogus is not defined",
+                              "myVar.prop + 42": "ReferenceError: bogus is not defined",
+                              "myVar.prop = 'xlerb'": "ReferenceError: sugob is not defined"
+                            });
+                          }, {
+                            "myVar.prop": "ReferenceError: bogus is not defined",
+                            "myVar.prop + 42": "ReferenceError: bogus is not defined",
+                            "myVar.prop = 'xlerb'": "xlerb"
+                          });
+                        }, {
+                          "myVar.prop": "brelx BACON",
+                          "myVar.prop + 42": "brelx BACON42",
+                          "myVar.prop = 'xlerb'": "xlerb"
+                        });
+                      }, {
+                        "myVar.prop": "brelx BEACON;;",
+                        "myVar.prop + 42": "brelx BEACON;;42",
+                        "myVar.prop = 'xlerb'": "xlerb"
+                      });
+                    }, {
+                      "myVar.prop": undefined,
+                      "myVar.prop + 42": NaN,
+                      "myVar.prop = 'xlerb'": "xlerb"
+                    });
+                  }, {
+                    "myVar.prop": undefined,
+                    "myVar.prop + 42": NaN,
+                    "myVar.prop = 'xlerb'": "xlerb"
+                  });
+                }, {
+                  "myVar.prop": "brelx BEER CAN",
+                  "myVar.prop + 42": "brelx BEER CAN42",
+                  "myVar.prop = 'xlerb'": "xlerb"
+                });
+              }, {
+                "myVar.prop": "xlerb BACON",
+                "myVar.prop + 42": "xlerb BACON42",
+                "myVar.prop = 'xlerb'": "xlerb"
+              });
+            }, {
+              "myVar.prop": "xlerb BEACON;;",
+              "myVar.prop + 42": "xlerb BEACON;;42",
+              "myVar.prop = 'xlerb'": "xlerb"
+            });
+          }, {
+            "myVar.prop": "xlerb BEACON;",
+            "myVar.prop + 42": "xlerb BEACON;42",
+            "myVar.prop = 'xlerb'": "xlerb"
+          });
+        }, {
+          "myVar.prop": "xlerb BEACON",
+          "myVar.prop + 42": "xlerb BEACON42",
+          "myVar.prop = 'xlerb'": "xlerb"
+        });
+      }, {
+        "myVar.prop": "xlerb BEER CAN",
+        "myVar.prop + 42": "xlerb BEER CAN42",
+        "myVar.prop = 'xlerb'": "xlerb"
+      });
+    });
+  });
+}
+
+function addWatchExpressions(callback)
+{
+  waitForWatchExpressions(function() {
+    let label = gDebugger.L10N.getStr("watchExpressionsScopeLabel");
+    let scope = gVars._currHierarchy.get(label);
+
+    ok(scope, "There should be a wach expressions scope in the variables view");
+    is(scope._store.size, 1, "There should be 1 evaluation availalble");
+
+    let w1 = scope.get("myVar.prop");
+    let w2 = scope.get("myVar.prop + 42");
+    let w3 = scope.get("myVar.prop = 'xlerb'");
+
+    ok(w1, "The first watch expression should be present in the scope");
+    ok(!w2, "The second watch expression should not be present in the scope");
+    ok(!w3, "The third watch expression should not be present in the scope");
+
+    is(w1.value, 42, "The first value is correct.");
+
+
+    waitForWatchExpressions(function() {
+      let label = gDebugger.L10N.getStr("watchExpressionsScopeLabel");
+      let scope = gVars._currHierarchy.get(label);
+
+      ok(scope, "There should be a wach expressions scope in the variables view");
+      is(scope._store.size, 2, "There should be 2 evaluations availalble");
+
+      let w1 = scope.get("myVar.prop");
+      let w2 = scope.get("myVar.prop + 42");
+      let w3 = scope.get("myVar.prop = 'xlerb'");
+
+      ok(w1, "The first watch expression should be present in the scope");
+      ok(w2, "The second watch expression should be present in the scope");
+      ok(!w3, "The third watch expression should not be present in the scope");
+
+      is(w1.value, "42", "The first expression value is correct.");
+      is(w2.value, "84", "The second expression value is correct.");
+
+
+      waitForWatchExpressions(function() {
+        let label = gDebugger.L10N.getStr("watchExpressionsScopeLabel");
+        let scope = gVars._currHierarchy.get(label);
+
+        ok(scope, "There should be a wach expressions scope in the variables view");
+        is(scope._store.size, 3, "There should be 3 evaluations availalble");
+
+        let w1 = scope.get("myVar.prop");
+        let w2 = scope.get("myVar.prop + 42");
+        let w3 = scope.get("myVar.prop = 'xlerb'");
+
+        ok(w1, "The first watch expression should be present in the scope");
+        ok(w2, "The second watch expression should be present in the scope");
+        ok(w3, "The third watch expression should be present in the scope");
+
+        is(w1.value, "xlerb", "The first expression value is correct.");
+        is(w2.value, "xlerb42", "The second expression value is correct.");
+        is(w3.value, "xlerb", "The third expression value is correct.");
+
+        callback();
+      });
+
+      gWatch.addExpression("myVar.prop = 'xlerb'");
+      gDebugger.editor.focus();
+    });
+
+    gWatch.addExpression("myVar.prop + 42");
+    gDebugger.editor.focus();
+  });
+
+  gWatch.addExpression("myVar.prop");
+  gDebugger.editor.focus();
+}
+
+function testEdit(what, string, callback, expected)
+{
+  let localScope = gDebugger.DebuggerView.Variables._list.querySelectorAll(".scope")[1],
+      localNodes = localScope.querySelector(".details").childNodes,
+      myVar = gVars.getItemForNode(localNodes[11]);
+
+  waitForProperties(function() {
+    let prop = myVar.get("prop");
+    let getterOrSetter = (what != "self" ? prop.get(what) : prop);
+
+    EventUtils.sendMouseEvent({ type: "mousedown" },
+      getterOrSetter._target.querySelector(".title > .value"),
+      gDebugger);
+
+    waitForElement(".element-value-input", true, function() {
+      waitForWatchExpressions(function() {
+        let label = gDebugger.L10N.getStr("watchExpressionsScopeLabel");
+        let scope = gVars._currHierarchy.get(label);
+
+        let w1 = scope.get(Object.keys(expected)[0]);
+        let w2 = scope.get(Object.keys(expected)[1]);
+        let w3 = scope.get(Object.keys(expected)[2]);
+
+        if (w1) {
+          if (isNaN(expected[w1.name]) && typeof expected[w1.name] == "number") {
+            ok(isNaN(w1.value),
+              "The first expression value is correct after the edit (NaN).");
+          } else {
+            is(w1.value, expected[w1.name],
+              "The first expression value is correct after the edit.");
+          }
+          info(w1.value + " is equal to " + expected[w1.name]);
+        }
+        if (w2) {
+          if (isNaN(expected[w2.name]) && typeof expected[w2.name] == "number") {
+            ok(isNaN(w2.value),
+              "The second expression value is correct after the edit (NaN).");
+          } else {
+            is(w2.value, expected[w2.name],
+              "The second expression value is correct after the edit.");
+          }
+          info(w2.value + " is equal to " + expected[w2.name]);
+        }
+        if (w3) {
+          if (isNaN(expected[w3.name]) && typeof expected[w3.name] == "number") {
+            ok(isNaN(w3.value),
+              "The third expression value is correct after the edit (NaN).");
+          } else {
+            is(w3.value, expected[w3.name],
+              "The third expression value is correct after the edit.");
+          }
+          info(w3.value + " is equal to " + expected[w3.name]);
+        }
+
+        callback();
+      });
+
+      info("Changing the " + what + "ter with '" + string + "'.");
+
+      write(string);
+      EventUtils.sendKey("RETURN", gDebugger);
+    });
+  });
+
+  myVar.expand();
+  gVars.clearHierarchy();
+}
+
+function waitForWatchExpressions(callback) {
+  gDebugger.addEventListener("Debugger:FetchedWatchExpressions", function onFetch() {
+    gDebugger.removeEventListener("Debugger:FetchedWatchExpressions", onFetch, false);
+    executeSoon(callback);
+  }, false);
+}
+
+function waitForProperties(callback) {
+  gDebugger.addEventListener("Debugger:FetchedProperties", function onFetch() {
+    gDebugger.removeEventListener("Debugger:FetchedProperties", onFetch, false);
+    executeSoon(callback);
+  }, false);
+}
+
+function waitForElement(selector, exists, callback)
+{
+  // Poll every few milliseconds until the element are retrieved.
+  let count = 0;
+  let intervalID = window.setInterval(function() {
+    info("count: " + count + " ");
+    if (++count > 50) {
+      ok(false, "Timed out while polling for the element.");
+      window.clearInterval(intervalID);
+      return closeDebuggerAndFinish();
+    }
+    if (!!gVars._list.querySelector(selector) != exists) {
+      return;
+    }
+    // We got the element, it's safe to callback.
+    window.clearInterval(intervalID);
+    callback();
+  }, 100);
+}
+
+function write(text) {
+  if (!text) {
+    EventUtils.sendKey("BACK_SPACE", gDebugger);
+    return;
+  }
+  for (let i = 0; i < text.length; i++) {
+    EventUtils.sendChar(text[i], gDebugger);
+  }
+}
+
+registerCleanupFunction(function() {
+  removeTab(gTab);
+  gPane = null;
+  gTab = null;
+  gDebugger = null;
+  gVars = null;
+  gWatch = null;
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-data-getset-02.js
@@ -0,0 +1,185 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Make sure that the property view is able to override getter properties
+ * to plain value properties.
+ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_frame-parameters.html";
+
+var gPane = null;
+var gTab = null;
+var gDebugger = null;
+var gVars = null;
+var gWatch = null;
+
+requestLongerTimeout(2);
+
+function test()
+{
+  debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+    gTab = aTab;
+    gPane = aPane;
+    gDebugger = gPane.panelWin;
+    gVars = gDebugger.DebuggerView.Variables;
+    gWatch = gDebugger.DebuggerView.WatchExpressions;
+
+    gVars.switch = function() {};
+    gVars.delete = function() {};
+
+    prepareVariablesView();
+  });
+}
+
+function prepareVariablesView() {
+  gDebugger.addEventListener("Debugger:FetchedVariables", function test() {
+    gDebugger.removeEventListener("Debugger:FetchedVariables", test, false);
+    Services.tm.currentThread.dispatch({ run: function() {
+
+      testVariablesView();
+
+    }}, 0);
+  }, false);
+
+  EventUtils.sendMouseEvent({ type: "click" },
+    content.document.querySelector("button"),
+    content.window);
+}
+
+function testVariablesView()
+{
+  executeSoon(function() {
+    addWatchExpressions(function() {
+      testEdit("\"xlerb\"", "xlerb", function() {
+        closeDebuggerAndFinish();
+      });
+    });
+  });
+}
+
+function addWatchExpressions(callback)
+{
+  waitForWatchExpressions(function() {
+    let label = gDebugger.L10N.getStr("watchExpressionsScopeLabel");
+    let scope = gVars._currHierarchy.get(label);
+
+    ok(scope, "There should be a wach expressions scope in the variables view");
+    is(scope._store.size, 1, "There should be 1 evaluation availalble");
+
+    let expr = scope.get("myVar.prop");
+    ok(expr, "The watch expression should be present in the scope");
+    is(expr.value, 42, "The value is correct.");
+
+    callback();
+  });
+
+  gWatch.addExpression("myVar.prop");
+  gDebugger.editor.focus();
+}
+
+function testEdit(string, expected, callback)
+{
+  let localScope = gDebugger.DebuggerView.Variables._list.querySelectorAll(".scope")[1],
+      localNodes = localScope.querySelector(".details").childNodes,
+      myVar = gVars.getItemForNode(localNodes[11]);
+
+  waitForProperties(function() {
+    let prop = myVar.get("prop");
+
+    is(prop.ownerView.name, "myVar",
+      "The right owner property name wasn't found.");
+    is(prop.name, "prop",
+      "The right property name wasn't found.");
+
+    is(prop.ownerView.value.type, "object",
+      "The right owner property value type wasn't found.");
+    is(prop.ownerView.value.class, "Object",
+      "The right owner property value class wasn't found.");
+
+    is(prop.name, "prop",
+      "The right property name wasn't found.");
+    is(prop.value, undefined,
+      "The right property value wasn't found.");
+    ok(prop.getter,
+      "The right property getter wasn't found.");
+    ok(prop.setter,
+      "The right property setter wasn't found.");
+
+    EventUtils.sendMouseEvent({ type: "mousedown" },
+      prop._target.querySelector(".dbg-variable-edit"),
+      gDebugger);
+
+    waitForElement(".element-value-input", true, function() {
+      waitForWatchExpressions(function() {
+        let label = gDebugger.L10N.getStr("watchExpressionsScopeLabel");
+        let scope = gVars._currHierarchy.get(label);
+
+        let expr = scope.get("myVar.prop");
+        is(expr.value, expected, "The value is correct.");
+
+        callback();
+      });
+
+      write(string);
+      EventUtils.sendKey("RETURN", gDebugger);
+    });
+  });
+
+  myVar.expand();
+  gVars.clearHierarchy();
+}
+
+function waitForWatchExpressions(callback) {
+  gDebugger.addEventListener("Debugger:FetchedWatchExpressions", function onFetch() {
+    gDebugger.removeEventListener("Debugger:FetchedWatchExpressions", onFetch, false);
+    executeSoon(callback);
+  }, false);
+}
+
+function waitForProperties(callback) {
+  gDebugger.addEventListener("Debugger:FetchedProperties", function onFetch() {
+    gDebugger.removeEventListener("Debugger:FetchedProperties", onFetch, false);
+    executeSoon(callback);
+  }, false);
+}
+
+function waitForElement(selector, exists, callback)
+{
+  // Poll every few milliseconds until the element are retrieved.
+  let count = 0;
+  let intervalID = window.setInterval(function() {
+    info("count: " + count + " ");
+    if (++count > 50) {
+      ok(false, "Timed out while polling for the element.");
+      window.clearInterval(intervalID);
+      return closeDebuggerAndFinish();
+    }
+    if (!!gVars._list.querySelector(selector) != exists) {
+      return;
+    }
+    // We got the element, it's safe to callback.
+    window.clearInterval(intervalID);
+    callback();
+  }, 100);
+}
+
+function write(text) {
+  if (!text) {
+    EventUtils.sendKey("BACK_SPACE", gDebugger);
+    return;
+  }
+  for (let i = 0; i < text.length; i++) {
+    EventUtils.sendChar(text[i], gDebugger);
+  }
+}
+
+registerCleanupFunction(function() {
+  removeTab(gTab);
+  gPane = null;
+  gTab = null;
+  gDebugger = null;
+  gVars = null;
+  gWatch = null;
+});
rename from browser/devtools/debugger/test/browser_dbg_propertyview-edit.js
rename to browser/devtools/debugger/test/browser_dbg_propertyview-edit-value.js
--- a/browser/devtools/shared/VariablesView.jsm
+++ b/browser/devtools/shared/VariablesView.jsm
@@ -220,16 +220,26 @@ VariablesView.prototype = {
    * function is provided, in order to change the variable or property's name.
    *
    * This flag is applied recursively onto each scope in this view and
    * affects only the child nodes when they're created.
    */
   editableNameTooltip: STR.GetStringFromName("variablesEditableNameTooltip"),
 
   /**
+   * The tooltip text shown on a variable or property's edit button if an
+   * |eval| function is provided and a getter/setter descriptor is present,
+   * in order to change the variable or property to a plain value.
+   *
+   * This flag is applied recursively onto each scope in this view and
+   * affects only the child nodes when they're created.
+   */
+  editButtonTooltip: STR.GetStringFromName("variablesEditButtonTooltip"),
+
+  /**
    * The tooltip text shown on a variable or property's delete button if a
    * |delete| function is provided, in order to delete the variable or property.
    *
    * This flag is applied recursively onto each scope in this view and
    * affects only the child nodes when they're created.
    */
   deleteButtonTooltip: STR.GetStringFromName("variablesCloseButtonTooltip"),
 
@@ -880,16 +890,143 @@ VariablesView.prototype = {
   _searchboxNode: null,
   _searchboxContainer: null,
   _searchboxPlaceholder: "",
   _emptyTextNode: null,
   _emptyTextValue: ""
 };
 
 /**
+ * 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
+ *         The string to be evaluated.
+ */
+VariablesView.simpleValueEvalMacro = function(aItem, aCurrentString) {
+  return aItem._symbolicName + "=" + aCurrentString;
+};
+
+/**
+ * Generates the string evaluated when overriding getters and setters with
+ * plain values.
+ *
+ * @param Property aItem
+ *        The current getter or setter property.
+ * @param string aCurrentString
+ *        The trimmed user inputted string.
+ * @return string
+ *         The string to be evaluated.
+ */
+VariablesView.overrideValueEvalMacro = function(aItem, aCurrentString) {
+  let property = "\"" + aItem._nameString + "\"";
+  let parent = aItem.ownerView._symbolicName || "this";
+
+  return "Object.defineProperty(" + parent + "," + property + "," +
+    "{ value: " + aCurrentString +
+    ", enumerable: " + parent + ".propertyIsEnumerable(" + property + ")" +
+    ", configurable: true" +
+    ", writable: true" +
+    "})";
+};
+
+/**
+ * Generates the string evaluated when performing getters and setters changes.
+ *
+ * @param Property aItem
+ *        The current getter or setter property.
+ * @param string aCurrentString
+ *        The trimmed user inputted string.
+ * @return string
+ *         The string to be evaluated.
+ */
+VariablesView.getterOrSetterEvalMacro = function(aItem, aCurrentString) {
+  let type = aItem._nameString;
+  let propertyObject = aItem.ownerView;
+  let parentObject = propertyObject.ownerView;
+  let property = "\"" + propertyObject._nameString + "\"";
+  let parent = parentObject._symbolicName || "this";
+
+  switch (aCurrentString) {
+    case "":
+    case "null":
+    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");
+      }
+
+      // 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
+      // })
+      return "Object.defineProperty(" + parent + "," + property + "," +
+        "{" + mirrorType + ":" + parent + "." + mirrorLookup + "(" + property + ")" +
+        "," + type + ":" + undefined +
+        ", enumerable: " + parent + ".propertyIsEnumerable(" + property + ")" +
+        ", configurable: true" +
+        "})";
+
+    default:
+      // Wrap statements inside a function declaration if not already wrapped.
+      if (aCurrentString.indexOf("function") != 0) {
+        let header = "function(" + (type == "set" ? "value" : "") + ")";
+        let body = "";
+        // If there's a return statement explicitly written, always use the
+        // standard function definition syntax
+        if (aCurrentString.indexOf("return ") != -1) {
+          body = "{" + aCurrentString + "}";
+        }
+        // If block syntax is used, use the whole string as the function body.
+        else if (aCurrentString.indexOf("{") == 0) {
+          body = aCurrentString;
+        }
+        // Prefer an expression closure.
+        else {
+          body = "(" + aCurrentString + ")";
+        }
+        aCurrentString = header + body;
+      }
+
+      // Determine if a new getter or setter should be defined.
+      let defineType = type == "get" ? "__defineGetter__" : "__defineSetter__";
+
+      // Make sure all quotes are escaped in the expression's syntax,
+      let defineFunc = "eval(\"(" + aCurrentString.replace(/"/g, "\\$&") + ")\")";
+
+      // Construct and return the getter/setter evaluation string.
+      // e.g: foo.__defineGetter__("bar", eval("(function() { return 42; })"))
+      return parent + "." + defineType + "(" + property + "," + defineFunc + ")";
+  }
+};
+
+/**
+ * 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, ""));
+  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
  *        The view to contain this scope.
  * @param string aName
  *        The scope's name.
  * @param object aFlags [optional]
@@ -906,16 +1043,17 @@ function Scope(aView, aName, aFlags = {}
 
   // Inherit properties and flags from the parent view. You can override
   // each of these directly onto any scope, variable or property instance.
   this.eval = aView.eval;
   this.switch = aView.switch;
   this.delete = aView.delete;
   this.editableValueTooltip = aView.editableValueTooltip;
   this.editableNameTooltip = aView.editableNameTooltip;
+  this.editButtonTooltip = aView.editButtonTooltip;
   this.deleteButtonTooltip = aView.deleteButtonTooltip;
   this.descriptorTooltip = aView.descriptorTooltip;
   this.contextMenuId = aView.contextMenuId;
   this.separatorStr = aView.separatorStr;
 
   this._store = new Map();
   this._init(aName.trim(), aFlags);
 }
@@ -1631,16 +1769,17 @@ Scope.prototype = {
   _window: null,
 
   ownerView: null,
   eval: null,
   switch: null,
   delete: null,
   editableValueTooltip: "",
   editableNameTooltip: "",
+  editButtonTooltip: "",
   deleteButtonTooltip: "",
   descriptorTooltip: true,
   contextMenuId: "",
   separatorStr: "",
 
   _store: null,
   _fetched: false,
   _retrieved: false,
@@ -1676,20 +1815,16 @@ Scope.prototype = {
  *        The variable's name.
  * @param object aDescriptor
  *        The variable's descriptor.
  */
 function Variable(aScope, aName, aDescriptor) {
   this._displayTooltip = this._displayTooltip.bind(this);
   this._activateNameInput = this._activateNameInput.bind(this);
   this._activateValueInput = this._activateValueInput.bind(this);
-  this._deactivateNameInput = this._deactivateNameInput.bind(this);
-  this._deactivateValueInput = this._deactivateValueInput.bind(this);
-  this._onNameInputKeyPress = this._onNameInputKeyPress.bind(this);
-  this._onValueInputKeyPress = this._onValueInputKeyPress.bind(this);
 
   Scope.call(this, aScope, aName, this._initialDescriptor = aDescriptor);
   this.setGrip(aDescriptor.value);
   this._symbolicName = aName;
   this._absoluteName = aScope.name + "[\"" + aName + "\"]";
 }
 
 create({ constructor: Variable, proto: Scope.prototype }, {
@@ -1881,16 +2016,21 @@ create({ constructor: Variable, proto: S
    *             - { type: "null" }
    *             - { type: "object", class: "Object" }
    */
   setGrip: function V_setGrip(aGrip) {
     // Don't allow displaying grip information if there's no name available.
     if (!this._nameString) {
       return;
     }
+    // Getters and setters should display grip information in sub-properties.
+    if (!this._isUndefined && (this.getter || this.setter)) {
+      this._valueLabel.setAttribute("value", "");
+      return;
+    }
 
     if (aGrip === undefined) {
       aGrip = { type: "undefined" };
     }
     if (aGrip === null) {
       aGrip = { type: "null" };
     }
 
@@ -1960,42 +2100,59 @@ create({ constructor: Variable, proto: S
 
     let valueLabel = this._valueLabel = document.createElement("label");
     valueLabel.className = "plain value";
     valueLabel.setAttribute("crop", "center");
 
     this._title.appendChild(separatorLabel);
     this._title.appendChild(valueLabel);
 
-    let isPrimitive = VariablesView.isPrimitive(descriptor);
-    let isUndefined = VariablesView.isUndefined(descriptor);
+    let isPrimitive = this._isPrimitive = VariablesView.isPrimitive(descriptor);
+    let isUndefined = this._isUndefined = VariablesView.isUndefined(descriptor);
 
     if (isPrimitive || isUndefined) {
       this.hideArrow();
     }
     if (!isUndefined && (descriptor.get || descriptor.set)) {
-      // FIXME: editing getters and setters is not allowed yet. Bug 831794.
-      this.eval = null;
-      this.addProperty("get", { value: descriptor.get });
-      this.addProperty("set", { value: descriptor.set });
-      this.expand();
       separatorLabel.hidden = true;
       valueLabel.hidden = true;
+
+      this.delete = VariablesView.getterOrSetterDeleteCallback;
+      this.evaluationMacro = VariablesView.overrideValueEvalMacro;
+
+      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();
+      setter.hideArrow();
+      this.expand();
     }
   },
 
   /**
    * Adds specific nodes for this variable based on custom flags.
    */
   _customizeVariable: function V__customizeVariable() {
+    if (this.ownerView.eval) {
+      if (!this._isUndefined && (this.getter || this.setter)) {
+        let editNode = this._editNode = this.document.createElement("toolbarbutton");
+        editNode.className = "plain dbg-variable-edit";
+        editNode.addEventListener("mousedown", this._onEdit.bind(this), false);
+        this._title.appendChild(editNode);
+      }
+    }
     if (this.ownerView.delete) {
-      let deleteNode = this._deleteNode = this.document.createElement("toolbarbutton");
-      deleteNode.className = "plain dbg-variable-delete devtools-closebutton";
-      deleteNode.addEventListener("click", this._onDelete.bind(this), false);
-      this._title.appendChild(deleteNode);
+      if (!this._isUndefined || !(this.ownerView.getter && this.ownerView.setter)) {
+        let deleteNode = this._deleteNode = this.document.createElement("toolbarbutton");
+        deleteNode.className = "plain dbg-variable-delete devtools-closebutton";
+        deleteNode.addEventListener("click", this._onDelete.bind(this), false);
+        this._title.appendChild(deleteNode);
+      }
     }
     if (this.ownerView.contextMenuId) {
       this._title.setAttribute("context", this.ownerView.contextMenuId);
     }
   },
 
   /**
    * Prepares a tooltip for this variable.
@@ -2026,16 +2183,19 @@ create({ constructor: Variable, proto: S
       tooltip.setAttribute("orient", "horizontal");
       tooltip.appendChild(configurableLabel);
       tooltip.appendChild(enumerableLabel);
       tooltip.appendChild(writableLabel);
 
       this._target.appendChild(tooltip);
       this._target.setAttribute("tooltip", tooltip.id);
     }
+    if (this.ownerView.eval && !this._isUndefined && (this.getter || this.setter)) {
+      this._editNode.setAttribute("tooltiptext", this.ownerView.editButtonTooltip);
+    }
     if (this.ownerView.eval) {
       this._valueLabel.setAttribute("tooltiptext", this.ownerView.editableValueTooltip);
     }
     if (this.ownerView.switch) {
       this._name.setAttribute("tooltiptext", this.ownerView.editableNameTooltip);
     }
     if (this.ownerView.delete) {
       this._deleteNode.setAttribute("tooltiptext", this.ownerView.deleteButtonTooltip);
@@ -2051,17 +2211,17 @@ create({ constructor: Variable, proto: S
     let name = this._nameString;
 
     if (!descriptor.configurable) {
       this._target.setAttribute("non-configurable", "");
     }
     if (!descriptor.enumerable) {
       this._target.setAttribute("non-enumerable", "");
     }
-    if (!descriptor.writable) {
+    if (!descriptor.writable && !this.ownerView.get && !this.ownerView.set) {
       this._target.setAttribute("non-writable", "");
     }
     if (name == "this") {
       this._target.setAttribute("self", "");
     }
     else if (name == "<exception>") {
       this._target.setAttribute("exception", "");
     }
@@ -2153,16 +2313,19 @@ create({ constructor: Variable, proto: S
     if (!this.ownerView.switch) {
       return;
     }
     if (e) {
       e.preventDefault();
       e.stopPropagation();
     }
 
+    this._onNameInputKeyPress = this._onNameInputKeyPress.bind(this);
+    this._deactivateNameInput = this._deactivateNameInput.bind(this);
+
     this._activateInput(this._name, "element-name-input", {
       onKeypress: this._onNameInputKeyPress,
       onBlur: this._deactivateNameInput
     });
     this._separatorLabel.hidden = true;
     this._valueLabel.hidden = true;
   },
 
@@ -2189,16 +2352,19 @@ create({ constructor: Variable, proto: S
     if (!this.ownerView.eval) {
       return;
     }
     if (e) {
       e.preventDefault();
       e.stopPropagation();
     }
 
+    this._onValueInputKeyPress = this._onValueInputKeyPress.bind(this);
+    this._deactivateValueInput = this._deactivateValueInput.bind(this);
+
     this._activateInput(this._valueLabel, "element-value-input", {
       onKeypress: this._onValueInputKeyPress,
       onBlur: this._deactivateValueInput
     });
   },
 
   /**
    * Deactivates this variable's editable value mode.
@@ -2209,21 +2375,28 @@ create({ constructor: Variable, proto: S
       onBlur: this._deactivateValueInput
     });
   },
 
   /**
    * Disables this variable prior to a new name switch or value evaluation.
    */
   _disable: function V__disable() {
-    this.twisty = false;
+    this.hideArrow();
     this._separatorLabel.hidden = true;
     this._valueLabel.hidden = true;
     this._enum.hidden = true;
     this._nonenum.hidden = true;
+
+    if (this._editNode) {
+      this._editNode.hidden = true;
+    }
+    if (this._deleteNode) {
+      this._deleteNode.hidden = true;
+    }
   },
 
   /**
    * Deactivates this variable's editable mode and callbacks the new name.
    */
   _saveNameInput: function V__saveNameInput(e) {
     let input = e.target;
     let initialString = this._name.getAttribute("value");
@@ -2243,21 +2416,27 @@ create({ constructor: Variable, proto: S
   _saveValueInput: function V__saveValueInput(e) {
     let input = e.target;
     let initialString = this._valueLabel.getAttribute("value");
     let currentString = input.value.trim();
     this._deactivateValueInput(e);
 
     if (initialString != currentString) {
       this._disable();
-      this.ownerView.eval(this._symbolicName + "=" + currentString);
+      this.ownerView.eval(this.evaluationMacro(this, currentString.trim()));
     }
   },
 
   /**
+   * The current macro used to generate the string evaluated when performing
+   * a variable or property value change.
+   */
+  evaluationMacro: VariablesView.simpleValueEvalMacro,
+
+  /**
    * The key press listener for this variable's editable name textbox.
    */
   _onNameInputKeyPress: function V__onNameInputKeyPress(e) {
     e.stopPropagation();
 
     switch(e.keyCode) {
       case e.DOM_VK_RETURN:
       case e.DOM_VK_ENTER:
@@ -2286,33 +2465,46 @@ create({ constructor: Variable, proto: S
       case e.DOM_VK_ESCAPE:
         this._deactivateValueInput(e);
         this._variablesView._focusItem(this);
         return;
     }
   },
 
   /**
+   * The click listener for the edit button.
+   */
+  _onEdit: function V__onEdit(e) {
+    e.preventDefault();
+    e.stopPropagation();
+    this._activateValueInput();
+  },
+
+  /**
    * The click listener for the delete button.
    */
   _onDelete: function V__onDelete(e) {
     e.preventDefault();
     e.stopPropagation();
 
     if (this.ownerView.delete) {
-      this.ownerView.delete(this);
-      this.hide();
+      if (!this.ownerView.delete(this)) {
+        this.hide();
+      }
     }
   },
 
   _symbolicName: "",
   _absoluteName: "",
   _initialDescriptor: null,
+  _isPrimitive: false,
+  _isUndefined: false,
   _separatorLabel: null,
   _valueLabel: null,
+  _editNode: null,
   _deleteNode: null,
   _tooltip: null,
   _valueGrip: null,
   _valueString: "",
   _valueClassName: "",
   _prevExpandable: false,
   _prevExpanded: false
 });
--- a/browser/locales/en-US/chrome/browser/devtools/debugger.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/debugger.properties
@@ -191,22 +191,26 @@ ToolboxDebugger.label=Debugger
 # displayed inside the developer tools window..
 ToolboxDebugger.tooltip=JavaScript Debugger
 
 # LOCALIZATION NOTE (variablesEditableNameTooltip): The text that is displayed
 # in the variables list on an item with an editable name.
 variablesEditableNameTooltip=Double click to edit
 
 # LOCALIZATION NOTE (variablesEditableValueTooltip): The text that is displayed
-# in the variables list on an item with an editable name.
+# in the variables list on an item with an editable value.
 variablesEditableValueTooltip=Click to change value
 
 # LOCALIZATION NOTE (variablesCloseButtonTooltip): The text that is displayed
-# in the variables list on an item with which can be removed.
+# in the variables list on an item which can be removed.
 variablesCloseButtonTooltip=Click to remove
 
+# LOCALIZATION NOTE (variablesEditButtonTooltip): The text that is displayed
+# in the variables list on a getter or setter which can be edited.
+variablesEditButtonTooltip=Click to set value
+
 # LOCALIZATION NOTE (variablesSeparatorLabel): The text that is displayed
 # in the variables list as a separator between the name and value.
 variablesSeparatorLabel=:
 
 # LOCALIZATION NOTE (watchExpressionsSeparatorLabel): The text that is displayed
 # in the watch expressions list as a separator between the code and evaluation.
 watchExpressionsSeparatorLabel=\ →
--- a/browser/themes/gnomestripe/devtools/debugger.css
+++ b/browser/themes/gnomestripe/devtools/debugger.css
@@ -289,18 +289,25 @@
   background-color: white;
   min-height: 10px;
 }
 
 .dbg-variable-delete:not(:hover) {
   opacity: 0.5;
 }
 
+.dbg-variable-edit {
+  background: url("chrome://browser/skin/tabview/edit-light.png") center no-repeat;
+  width: 20px;
+  height: 16px;
+  cursor: pointer;
+}
+
 .dbg-variable-throbber {
-  background: url("chrome://global/skin/icons/loading_16.png");
+  background: url("chrome://global/skin/icons/loading_16.png") center no-repeat;
   width: 16px;
   height: 16px;
 }
 
 /**
  * Scope element
  */
 
@@ -311,17 +318,17 @@
 
 .scope > .title {
   text-shadow: 0 1px #222;
   color: #fff;
 }
 
 .scope > .details {
   -moz-margin-start: 2px;
-  -moz-margin-end: 2px;
+  -moz-margin-end: 1px;
 }
 
 .scope > .details.nonenum:not(:empty) {
   border-top: 1px solid #ddd;
 }
 
 /**
  * Variable element
--- a/browser/themes/pinstripe/devtools/debugger.css
+++ b/browser/themes/pinstripe/devtools/debugger.css
@@ -296,18 +296,25 @@
   min-height: 10px;
 }
 
 .dbg-variable-delete:not(:hover) {
   -moz-image-region: rect(0, 32px, 16px, 16px);
   opacity: 0.5;
 }
 
+.dbg-variable-edit {
+  background: url("chrome://browser/skin/tabview/edit-light.png") center no-repeat;
+  width: 20px;
+  height: 16px;
+  cursor: pointer;
+}
+
 .dbg-variable-throbber {
-  background: url("chrome://global/skin/icons/loading_16.png");
+  background: url("chrome://global/skin/icons/loading_16.png") center no-repeat;
   width: 16px;
   height: 16px;
 }
 
 /**
  * Scope element
  */
 
@@ -318,17 +325,17 @@
 
 .scope > .title {
   text-shadow: 0 1px #222;
   color: #fff;
 }
 
 .scope > .details {
   -moz-margin-start: 2px;
-  -moz-margin-end: 2px;
+  -moz-margin-end: 1px;
 }
 
 .scope > .details.nonenum:not(:empty) {
   border-top: 1px solid #ddd;
 }
 
 /**
  * Variable element
--- a/browser/themes/winstripe/devtools/debugger.css
+++ b/browser/themes/winstripe/devtools/debugger.css
@@ -302,18 +302,25 @@
   min-height: 10px;
 }
 
 .dbg-variable-delete:not(:hover) {
   -moz-image-region: rect(0, 32px, 16px, 16px);
   opacity: 0.5;
 }
 
+.dbg-variable-edit {
+  background: url("chrome://browser/skin/tabview/edit-light.png") center no-repeat;
+  width: 20px;
+  height: 16px;
+  cursor: pointer;
+}
+
 .dbg-variable-throbber {
-  background: url("chrome://global/skin/icons/loading_16.png");
+  background: url("chrome://global/skin/icons/loading_16.png") center no-repeat;
   width: 16px;
   height: 16px;
 }
 
 /**
  * Scope element
  */
 
@@ -324,17 +331,17 @@
 
 .scope > .title {
   text-shadow: 0 1px #222;
   color: #fff;
 }
 
 .scope > .details {
   -moz-margin-start: 2px;
-  -moz-margin-end: 2px;
+  -moz-margin-end: 1px;
 }
 
 .scope > .details.nonenum:not(:empty) {
   border-top: 1px solid #ddd;
 }
 
 /**
  * Variable element