Bug 850336: Devtools box model should be editable. r=pbrosset
authorDave Townsend <dtownsend@oxymoronical.com>
Thu, 03 Apr 2014 17:37:26 -0700
changeset 177129 e3564d9cd0155b382fb2b81ae4d424a9bd0872a4
parent 177128 6a285c43e58d721753a0b52261271a2b9788c842
child 177130 6c5b67c8ba3d3adf366af6aa959298a08dd31e15
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewerspbrosset
bugs850336
milestone31.0a1
Bug 850336: Devtools box model should be editable. r=pbrosset
browser/devtools/layoutview/test/browser.ini
browser/devtools/layoutview/test/browser_editablemodel.js
browser/devtools/layoutview/test/browser_editablemodel_allproperties.js
browser/devtools/layoutview/test/browser_editablemodel_border.js
browser/devtools/layoutview/test/browser_editablemodel_stylerules.js
browser/devtools/layoutview/test/browser_layoutview.js
browser/devtools/layoutview/test/head.js
browser/devtools/layoutview/view.css
browser/devtools/layoutview/view.js
browser/devtools/layoutview/view.xhtml
browser/devtools/shared/inplace-editor.js
browser/themes/shared/devtools/layoutview.css
--- a/browser/devtools/layoutview/test/browser.ini
+++ b/browser/devtools/layoutview/test/browser.ini
@@ -1,4 +1,10 @@
 [DEFAULT]
+support-files =
+  head.js
 
 [browser_layoutview.js]
 skip-if = true
+[browser_editablemodel.js]
+[browser_editablemodel_allproperties.js]
+[browser_editablemodel_border.js]
+[browser_editablemodel_stylerules.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/layoutview/test/browser_editablemodel.js
@@ -0,0 +1,145 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function getStyle(node, property) {
+  return node.style.getPropertyValue(property);
+}
+
+let doc;
+let inspector;
+
+let test = asyncTest(function*() {
+  let style = "div { margin: 10px; padding: 3px } #div1 { margin-top: 5px } #div2 { border-bottom: 1em solid black; } #div3 { padding: 2em; }";
+  let html = "<style>" + style + "</style><div id='div1'></div><div id='div2'></div><div id='div3'></div>"
+
+  let content = yield loadTab("data:text/html," + encodeURIComponent(html));
+  doc = content.document;
+
+  let target = TargetFactory.forTab(gBrowser.selectedTab);
+  let toolbox = yield gDevTools.showToolbox(target, "inspector");
+  inspector = toolbox.getCurrentPanel();
+
+  inspector.sidebar.select("layoutview");
+  yield inspector.sidebar.once("layoutview-ready");
+  yield runTests();
+  yield gDevTools.closeToolbox(toolbox);
+});
+
+addTest("Test that editing margin dynamically updates the document, pressing escape cancels the changes",
+function*() {
+  let node = doc.getElementById("div1");
+  is(getStyle(node, "margin-top"), "", "Should be no margin-top on the element.")
+  let view = yield selectNode(node);
+
+  let span = view.document.querySelector(".margin.top > span");
+  is(span.textContent, 5, "Should have the right value in the box model.");
+
+  EventUtils.synthesizeMouseAtCenter(span, {}, view);
+  let editor = view.document.querySelector(".styleinspector-propertyeditor");
+  ok(editor, "Should have opened the editor.");
+  is(editor.value, "5px", "Should have the right value in the editor.");
+
+  EventUtils.synthesizeKey("3", {}, view);
+  yield waitForUpdate();
+
+  is(getStyle(node, "margin-top"), "3px", "Should have updated the margin.");
+
+  EventUtils.synthesizeKey("VK_ESCAPE", {}, view);
+  yield waitForUpdate();
+
+  is(getStyle(node, "margin-top"), "", "Should be no margin-top on the element.")
+  is(span.textContent, 5, "Should have the right value in the box model.");
+});
+
+addTest("Test that arrow keys work correctly and pressing enter commits the changes",
+function*() {
+  let node = doc.getElementById("div1");
+  is(getStyle(node, "margin-left"), "", "Should be no margin-top on the element.")
+  let view = yield selectNode(node);
+
+  let span = view.document.querySelector(".margin.left > span");
+  is(span.textContent, 10, "Should have the right value in the box model.");
+
+  EventUtils.synthesizeMouseAtCenter(span, {}, view);
+  let editor = view.document.querySelector(".styleinspector-propertyeditor");
+  ok(editor, "Should have opened the editor.");
+  is(editor.value, "10px", "Should have the right value in the editor.");
+
+  EventUtils.synthesizeKey("VK_UP", {}, view);
+  yield waitForUpdate();
+
+  is(editor.value, "11px", "Should have the right value in the editor.");
+  is(getStyle(node, "margin-left"), "11px", "Should have updated the margin.");
+
+  EventUtils.synthesizeKey("VK_DOWN", {}, view);
+  yield waitForUpdate();
+
+  is(editor.value, "10px", "Should have the right value in the editor.");
+  is(getStyle(node, "margin-left"), "10px", "Should have updated the margin.");
+
+  EventUtils.synthesizeKey("VK_UP", { shiftKey: true }, view);
+  yield waitForUpdate();
+
+  is(editor.value, "20px", "Should have the right value in the editor.");
+  is(getStyle(node, "margin-left"), "20px", "Should have updated the margin.");
+
+  EventUtils.synthesizeKey("VK_RETURN", {}, view);
+  yield waitForUpdate();
+
+  is(getStyle(node, "margin-left"), "20px", "Should be the right margin-top on the element.")
+  is(span.textContent, 20, "Should have the right value in the box model.");
+});
+
+addTest("Test that deleting the value removes the property but escape undoes that",
+function*() {
+  let node = doc.getElementById("div1");
+  is(getStyle(node, "margin-left"), "20px", "Should be the right margin-top on the element.")
+  let view = yield selectNode(node);
+
+  let span = view.document.querySelector(".margin.left > span");
+  is(span.textContent, 20, "Should have the right value in the box model.");
+
+  EventUtils.synthesizeMouseAtCenter(span, {}, view);
+  let editor = view.document.querySelector(".styleinspector-propertyeditor");
+  ok(editor, "Should have opened the editor.");
+  is(editor.value, "20px", "Should have the right value in the editor.");
+
+  EventUtils.synthesizeKey("VK_DELETE", {}, view);
+  yield waitForUpdate();
+
+  is(editor.value, "", "Should have the right value in the editor.");
+  is(getStyle(node, "margin-left"), "", "Should have updated the margin.");
+
+  EventUtils.synthesizeKey("VK_ESCAPE", {}, view);
+  yield waitForUpdate();
+
+  is(getStyle(node, "margin-left"), "20px", "Should be the right margin-top on the element.")
+  is(span.textContent, 20, "Should have the right value in the box model.");
+});
+
+addTest("Test that deleting the value removes the property",
+function*() {
+  let node = doc.getElementById("div1");
+  node.style.marginRight = "15px";
+  let view = yield selectNode(node);
+
+  let span = view.document.querySelector(".margin.right > span");
+  is(span.textContent, 15, "Should have the right value in the box model.");
+
+  EventUtils.synthesizeMouseAtCenter(span, {}, view);
+  let editor = view.document.querySelector(".styleinspector-propertyeditor");
+  ok(editor, "Should have opened the editor.");
+  is(editor.value, "15px", "Should have the right value in the editor.");
+
+  EventUtils.synthesizeKey("VK_DELETE", {}, view);
+  yield waitForUpdate();
+
+  is(editor.value, "", "Should have the right value in the editor.");
+  is(getStyle(node, "margin-right"), "", "Should have updated the margin.");
+
+  EventUtils.synthesizeKey("VK_RETURN", {}, view);
+  yield waitForUpdate();
+
+  is(getStyle(node, "margin-right"), "", "Should be the right margin-top on the element.")
+  is(span.textContent, 10, "Should have the right value in the box model.");
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/layoutview/test/browser_editablemodel_allproperties.js
@@ -0,0 +1,134 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function getStyle(node, property) {
+  return node.style.getPropertyValue(property);
+}
+
+let doc;
+let inspector;
+
+let test = asyncTest(function*() {
+  let style = "div { margin: 10px; padding: 3px } #div1 { margin-top: 5px } #div2 { border-bottom: 1em solid black; } #div3 { padding: 2em; }";
+  let html = "<style>" + style + "</style><div id='div1'></div><div id='div2'></div><div id='div3'></div>"
+
+  let content = yield loadTab("data:text/html," + encodeURIComponent(html));
+  doc = content.document;
+
+  let target = TargetFactory.forTab(gBrowser.selectedTab);
+  let toolbox = yield gDevTools.showToolbox(target, "inspector");
+  inspector = toolbox.getCurrentPanel();
+
+  inspector.sidebar.select("layoutview");
+  yield inspector.sidebar.once("layoutview-ready");
+  yield runTests();
+  yield gDevTools.closeToolbox(toolbox);
+});
+
+addTest("When all properties are set on the node editing one should work",
+function*() {
+  let node = doc.getElementById("div1");
+  node.style.padding = "5px";
+  let view = yield selectNode(node);
+
+  let span = view.document.querySelector(".padding.bottom > span");
+  is(span.textContent, 5, "Should have the right value in the box model.");
+
+  EventUtils.synthesizeMouseAtCenter(span, {}, view);
+  let editor = view.document.querySelector(".styleinspector-propertyeditor");
+  ok(editor, "Should have opened the editor.");
+  is(editor.value, "5px", "Should have the right value in the editor.");
+
+  EventUtils.synthesizeKey("7", {}, view);
+  yield waitForUpdate();
+
+  is(editor.value, "7", "Should have the right value in the editor.");
+  is(getStyle(node, "padding-bottom"), "7px", "Should have updated the padding");
+
+  EventUtils.synthesizeKey("VK_RETURN", {}, view);
+  yield waitForUpdate();
+
+  is(getStyle(node, "padding-bottom"), "7px", "Should be the right padding.")
+  is(span.textContent, 7, "Should have the right value in the box model.");
+});
+
+addTest("When all properties are set on the node editing one should work",
+function*() {
+  let node = doc.getElementById("div1");
+  node.style.padding = "5px";
+  let view = yield selectNode(node);
+
+  let span = view.document.querySelector(".padding.left > span");
+  is(span.textContent, 5, "Should have the right value in the box model.");
+
+  EventUtils.synthesizeMouseAtCenter(span, {}, view);
+  let editor = view.document.querySelector(".styleinspector-propertyeditor");
+  ok(editor, "Should have opened the editor.");
+  is(editor.value, "5px", "Should have the right value in the editor.");
+
+  EventUtils.synthesizeKey("8", {}, view);
+  yield waitForUpdate();
+
+  is(editor.value, "8", "Should have the right value in the editor.");
+  is(getStyle(node, "padding-left"), "8px", "Should have updated the padding");
+
+  EventUtils.synthesizeKey("VK_ESCAPE", {}, view);
+  yield waitForUpdate();
+
+  is(getStyle(node, "padding-left"), "5px", "Should be the right padding.")
+  is(span.textContent, 5, "Should have the right value in the box model.");
+});
+
+addTest("When all properties are set on the node deleting one should work",
+function*() {
+  let node = doc.getElementById("div1");
+  node.style.padding = "5px";
+  let view = yield selectNode(node);
+
+  let span = view.document.querySelector(".padding.left > span");
+  is(span.textContent, 5, "Should have the right value in the box model.");
+
+  EventUtils.synthesizeMouseAtCenter(span, {}, view);
+  let editor = view.document.querySelector(".styleinspector-propertyeditor");
+  ok(editor, "Should have opened the editor.");
+  is(editor.value, "5px", "Should have the right value in the editor.");
+
+  EventUtils.synthesizeKey("VK_DELETE", {}, view);
+  yield waitForUpdate();
+
+  is(editor.value, "", "Should have the right value in the editor.");
+  is(getStyle(node, "padding-left"), "", "Should have updated the padding");
+
+  EventUtils.synthesizeKey("VK_RETURN", {}, view);
+  yield waitForUpdate();
+
+  is(getStyle(node, "padding-left"), "", "Should be the right padding.")
+  is(span.textContent, 3, "Should have the right value in the box model.");
+});
+
+addTest("When all properties are set on the node deleting one then cancelling should work",
+function*() {
+  let node = doc.getElementById("div1");
+  node.style.padding = "5px";
+  let view = yield selectNode(node);
+
+  let span = view.document.querySelector(".padding.left > span");
+  is(span.textContent, 5, "Should have the right value in the box model.");
+
+  EventUtils.synthesizeMouseAtCenter(span, {}, view);
+  let editor = view.document.querySelector(".styleinspector-propertyeditor");
+  ok(editor, "Should have opened the editor.");
+  is(editor.value, "5px", "Should have the right value in the editor.");
+
+  EventUtils.synthesizeKey("VK_DELETE", {}, view);
+  yield waitForUpdate();
+
+  is(editor.value, "", "Should have the right value in the editor.");
+  is(getStyle(node, "padding-left"), "", "Should have updated the padding");
+
+  EventUtils.synthesizeKey("VK_ESCAPE", {}, view);
+  yield waitForUpdate();
+
+  is(getStyle(node, "padding-left"), "5px", "Should be the right padding.")
+  is(span.textContent, 5, "Should have the right value in the box model.");
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/layoutview/test/browser_editablemodel_border.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function getStyle(node, property) {
+  return node.style.getPropertyValue(property);
+}
+
+let doc;
+let inspector;
+
+let test = asyncTest(function*() {
+  let style = "div { margin: 10px; padding: 3px } #div1 { margin-top: 5px } #div2 { border-bottom: 1em solid black; } #div3 { padding: 2em; }";
+  let html = "<style>" + style + "</style><div id='div1'></div><div id='div2'></div><div id='div3'></div>"
+
+  let content = yield loadTab("data:text/html," + encodeURIComponent(html));
+  doc = content.document;
+
+  let target = TargetFactory.forTab(gBrowser.selectedTab);
+  let toolbox = yield gDevTools.showToolbox(target, "inspector");
+  inspector = toolbox.getCurrentPanel();
+
+  inspector.sidebar.select("layoutview");
+  yield inspector.sidebar.once("layoutview-ready");
+  yield runTests();
+  yield gDevTools.closeToolbox(toolbox);
+});
+
+addTest("Test that adding a border applies a border style when necessary",
+function*() {
+  let node = doc.getElementById("div1");
+  is(getStyle(node, "border-top-width"), "", "Should have the right border");
+  is(getStyle(node, "border-top-style"), "", "Should have the right border");
+  let view = yield selectNode(node);
+
+  let span = view.document.querySelector(".border.top > span");
+  is(span.textContent, 0, "Should have the right value in the box model.");
+
+  EventUtils.synthesizeMouseAtCenter(span, {}, view);
+  let editor = view.document.querySelector(".styleinspector-propertyeditor");
+  ok(editor, "Should have opened the editor.");
+  is(editor.value, "0", "Should have the right value in the editor.");
+
+  EventUtils.synthesizeKey("1", {}, view);
+  yield waitForUpdate();
+
+  is(editor.value, "1", "Should have the right value in the editor.");
+  is(getStyle(node, "border-top-width"), "1px", "Should have the right border");
+  is(getStyle(node, "border-top-style"), "solid", "Should have the right border");
+
+  EventUtils.synthesizeKey("VK_ESCAPE", {}, view);
+  yield waitForUpdate();
+
+  is(getStyle(node, "border-top-width"), "", "Should be the right padding.")
+  is(getStyle(node, "border-top-style"), "", "Should have the right border");
+  is(span.textContent, 0, "Should have the right value in the box model.");
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/layoutview/test/browser_editablemodel_stylerules.js
@@ -0,0 +1,108 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function getStyle(node, property) {
+  return node.style.getPropertyValue(property);
+}
+
+let doc;
+let inspector;
+
+let test = asyncTest(function*() {
+  let style = "div { margin: 10px; padding: 3px } #div1 { margin-top: 5px } #div2 { border-bottom: 1em solid black; } #div3 { padding: 2em; }";
+  let html = "<style>" + style + "</style><div id='div1'></div><div id='div2'></div><div id='div3'></div>"
+
+  let content = yield loadTab("data:text/html," + encodeURIComponent(html));
+  doc = content.document;
+
+  let target = TargetFactory.forTab(gBrowser.selectedTab);
+  let toolbox = yield gDevTools.showToolbox(target, "inspector");
+  inspector = toolbox.getCurrentPanel();
+
+  inspector.sidebar.select("layoutview");
+  yield inspector.sidebar.once("layoutview-ready");
+  yield runTests();
+  yield gDevTools.closeToolbox(toolbox);
+});
+
+addTest("Test that entering units works",
+function*() {
+  let node = doc.getElementById("div1");
+  is(getStyle(node, "padding-top"), "", "Should have the right padding");
+  let view = yield selectNode(node);
+
+  let span = view.document.querySelector(".padding.top > span");
+  is(span.textContent, 3, "Should have the right value in the box model.");
+
+  EventUtils.synthesizeMouseAtCenter(span, {}, view);
+  let editor = view.document.querySelector(".styleinspector-propertyeditor");
+  ok(editor, "Should have opened the editor.");
+  is(editor.value, "3px", "Should have the right value in the editor.");
+
+  EventUtils.synthesizeKey("1", {}, view);
+  yield waitForUpdate();
+  EventUtils.synthesizeKey("e", {}, view);
+  yield waitForUpdate();
+
+  is(getStyle(node, "padding-top"), "", "An invalid value is handled cleanly");
+
+  EventUtils.synthesizeKey("m", {}, view);
+  yield waitForUpdate();
+
+  is(editor.value, "1em", "Should have the right value in the editor.");
+  is(getStyle(node, "padding-top"), "1em", "Should have updated the padding.");
+
+  EventUtils.synthesizeKey("VK_RETURN", {}, view);
+  yield waitForUpdate();
+
+  is(getStyle(node, "padding-top"), "1em", "Should be the right padding.")
+  is(span.textContent, 16, "Should have the right value in the box model.");
+});
+
+addTest("Test that we pick up the value from a higher style rule",
+function*() {
+  let node = doc.getElementById("div2");
+  is(getStyle(node, "border-bottom-width"), "", "Should have the right border-bottom-width");
+  let view = yield selectNode(node);
+
+  let span = view.document.querySelector(".border.bottom > span");
+  is(span.textContent, 16, "Should have the right value in the box model.");
+
+  EventUtils.synthesizeMouseAtCenter(span, {}, view);
+  let editor = view.document.querySelector(".styleinspector-propertyeditor");
+  ok(editor, "Should have opened the editor.");
+  is(editor.value, "1em", "Should have the right value in the editor.");
+
+  EventUtils.synthesizeKey("0", {}, view);
+  yield waitForUpdate();
+
+  is(editor.value, "0", "Should have the right value in the editor.");
+  is(getStyle(node, "border-bottom-width"), "0px", "Should have updated the border.");
+
+  EventUtils.synthesizeKey("VK_RETURN", {}, view);
+  yield waitForUpdate();
+
+  is(getStyle(node, "border-bottom-width"), "0px", "Should be the right border-bottom-width.")
+  is(span.textContent, 0, "Should have the right value in the box model.");
+});
+
+addTest("Test that shorthand properties are parsed correctly",
+function*() {
+  let node = doc.getElementById("div3");
+  is(getStyle(node, "padding-right"), "", "Should have the right padding");
+  let view = yield selectNode(node);
+
+  let span = view.document.querySelector(".padding.right > span");
+  is(span.textContent, 32, "Should have the right value in the box model.");
+
+  EventUtils.synthesizeMouseAtCenter(span, {}, view);
+  let editor = view.document.querySelector(".styleinspector-propertyeditor");
+  ok(editor, "Should have opened the editor.");
+  is(editor.value, "2em", "Should have the right value in the editor.");
+
+  EventUtils.synthesizeKey("VK_RETURN", {}, view);
+  yield waitForUpdate();
+
+  is(getStyle(node, "padding-right"), "", "Should be the right padding.")
+  is(span.textContent, 32, "Should have the right value in the box model.");
+});
--- a/browser/devtools/layoutview/test/browser_layoutview.js
+++ b/browser/devtools/layoutview/test/browser_layoutview.js
@@ -1,134 +1,99 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
-let TargetFactory = devtools.TargetFactory;
-
-function test() {
-  waitForExplicitFinish();
-
-  gDevTools.testing = true;
-  SimpleTest.registerCleanupFunction(() => {
-    gDevTools.testing = false;
-  });
-
-  Services.prefs.setBoolPref("devtools.inspector.sidebarOpen", true);
-
-  let doc;
-  let node;
-  let view;
-  let inspector;
+// Expected values:
+let res1 = [
+      {selector: "#element-size",              value: "160x160"},
+      {selector: ".size > span",               value: "100x100"},
+      {selector: ".margin.top > span",         value: 30},
+      {selector: ".margin.left > span",        value: "auto"},
+      {selector: ".margin.bottom > span",      value: 30},
+      {selector: ".margin.right > span",       value: "auto"},
+      {selector: ".padding.top > span",        value: 20},
+      {selector: ".padding.left > span",       value: 20},
+      {selector: ".padding.bottom > span",     value: 20},
+      {selector: ".padding.right > span",      value: 20},
+      {selector: ".border.top > span",         value: 10},
+      {selector: ".border.left > span",        value: 10},
+      {selector: ".border.bottom > span",      value: 10},
+      {selector: ".border.right > span",       value: 10},
+];
 
-  // Expected values:
-  let res1 = [
-        {selector: "#element-size",              value: "160x160"},
-        {selector: ".size > span",               value: "100x100"},
-        {selector: ".margin.top > span",         value: 30},
-        {selector: ".margin.left > span",        value: "auto"},
-        {selector: ".margin.bottom > span",      value: 30},
-        {selector: ".margin.right > span",       value: "auto"},
-        {selector: ".padding.top > span",        value: 20},
-        {selector: ".padding.left > span",       value: 20},
-        {selector: ".padding.bottom > span",     value: 20},
-        {selector: ".padding.right > span",      value: 20},
-        {selector: ".border.top > span",         value: 10},
-        {selector: ".border.left > span",        value: 10},
-        {selector: ".border.bottom > span",      value: 10},
-        {selector: ".border.right > span",       value: 10},
-  ];
+let res2 = [
+      {selector: "#element-size",              value: "190x210"},
+      {selector: ".size > span",               value: "100x150"},
+      {selector: ".margin.top > span",         value: 30},
+      {selector: ".margin.left > span",        value: "auto"},
+      {selector: ".margin.bottom > span",      value: 30},
+      {selector: ".margin.right > span",       value: "auto"},
+      {selector: ".padding.top > span",        value: 20},
+      {selector: ".padding.left > span",       value: 20},
+      {selector: ".padding.bottom > span",     value: 20},
+      {selector: ".padding.right > span",      value: 50},
+      {selector: ".border.top > span",         value: 10},
+      {selector: ".border.left > span",        value: 10},
+      {selector: ".border.bottom > span",      value: 10},
+      {selector: ".border.right > span",       value: 10},
+];
 
-  let res2 = [
-        {selector: "#element-size",              value: "190x210"},
-        {selector: ".size > span",               value: "100x150"},
-        {selector: ".margin.top > span",         value: 30},
-        {selector: ".margin.left > span",        value: "auto"},
-        {selector: ".margin.bottom > span",      value: 30},
-        {selector: ".margin.right > span",       value: "auto"},
-        {selector: ".padding.top > span",        value: 20},
-        {selector: ".padding.left > span",       value: 20},
-        {selector: ".padding.bottom > span",     value: 20},
-        {selector: ".padding.right > span",      value: 50},
-        {selector: ".border.top > span",         value: 10},
-        {selector: ".border.left > span",        value: 10},
-        {selector: ".border.bottom > span",      value: 10},
-        {selector: ".border.right > span",       value: 10},
-  ];
+let inspector;
+let view;
 
-  gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.selectedBrowser.addEventListener("load", function onload() {
-    gBrowser.selectedBrowser.removeEventListener("load", onload, true);
-    doc = content.document;
-    waitForFocus(setupTest, content);
-  }, true);
-
+let test = asyncTest(function*() {
   let style = "div { position: absolute; top: 42px; left: 42px; height: 100px; width: 100px; border: 10px solid black; padding: 20px; margin: 30px auto;}";
   let html = "<style>" + style + "</style><div></div>"
-  content.location = "data:text/html," + encodeURIComponent(html);
 
-  function setupTest() {
-    node = doc.querySelector("div");
-    ok(node, "node found");
+  let content = yield loadTab("data:text/html," + encodeURIComponent(html));
+  let node = content.document.querySelector("div");
+  ok(node, "node found");
 
-    let target = TargetFactory.forTab(gBrowser.selectedTab);
-    gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
-      openLayoutView(toolbox.getCurrentPanel());
-    });
-  }
+  let target = TargetFactory.forTab(gBrowser.selectedTab);
+  let toolbox = yield gDevTools.showToolbox(target, "inspector");
+  inspector = toolbox.getCurrentPanel();
 
-  function openLayoutView(aInspector) {
-    inspector = aInspector;
+  info("Inspector open");
 
-    info("Inspector open");
+  inspector.sidebar.select("layoutview");
+  yield inspector.sidebar.once("layoutview-ready");
 
-    inspector.sidebar.select("layoutview");
-    inspector.sidebar.once("layoutview-ready", () => {
-      inspector.selection.setNode(node);
-      inspector.once("inspector-updated", viewReady);
-    });
-  }
+  inspector.selection.setNode(node);
+  yield inspector.once("inspector-updated");
 
+  info("Layout view ready");
 
-  function viewReady() {
-    info("Layout view ready");
-
-    view = inspector.sidebar.getWindowForTab("layoutview");
+  view = inspector.sidebar.getWindowForTab("layoutview");
+  ok(!!view.layoutview, "LayoutView document is alive.");
 
-    ok(!!view.layoutview, "LayoutView document is alive.");
+  yield runTests();
 
-    test1();
-  }
+  executeSoon(function() {
+    inspector._toolbox.destroy();
+  });
 
-  function test1() {
-    let viewdoc = view.document;
-
-    for (let i = 0; i < res1.length; i++) {
-      let elt = viewdoc.querySelector(res1[i].selector);
-      is(elt.textContent, res1[i].value, res1[i].selector + " has the right value.");
-    }
+  yield gDevTools.once("toolbox-destroyed");
+});
 
-    inspector.once("layoutview-updated", test2);
+addTest("Test that the initial values of the box model are correct",
+function*() {
+  let viewdoc = view.document;
 
-    inspector.selection.node.style.height = "150px";
-    inspector.selection.node.style.paddingRight = "50px";
+  for (let i = 0; i < res1.length; i++) {
+    let elt = viewdoc.querySelector(res1[i].selector);
+    is(elt.textContent, res1[i].value, res1[i].selector + " has the right value.");
   }
-
-  function test2() {
-    let viewdoc = view.document;
+});
 
-    for (let i = 0; i < res2.length; i++) {
-      let elt = viewdoc.querySelector(res2[i].selector);
-      is(elt.textContent, res2[i].value, res2[i].selector + " has the right value after style update.");
-    }
+addTest("Test that changing the document updates the box model",
+function*() {
+  let viewdoc = view.document;
+
+  inspector.selection.node.style.height = "150px";
+  inspector.selection.node.style.paddingRight = "50px";
 
-    executeSoon(function() {
-      gDevTools.once("toolbox-destroyed", finishUp);
-      inspector._toolbox.destroy();
-    });
-  }
+  yield waitForUpdate();
 
-  function finishUp() {
-    Services.prefs.clearUserPref("devtools.inspector.sidebarOpen");
-    gBrowser.removeCurrentTab();
-    finish();
+  for (let i = 0; i < res2.length; i++) {
+    let elt = viewdoc.querySelector(res2[i].selector);
+    is(elt.textContent, res2[i].value, res2[i].selector + " has the right value after style update.");
   }
-}
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/layoutview/test/head.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://gre/modules/Task.jsm");
+
+let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+const promise = devtools.require("sdk/core/promise");
+let TargetFactory = devtools.TargetFactory;
+
+Services.prefs.setBoolPref("devtools.inspector.sidebarOpen", true);
+Services.prefs.setIntPref("devtools.toolbox.footer.height", 350);
+gDevTools.testing = true;
+SimpleTest.registerCleanupFunction(() => {
+  Services.prefs.clearUserPref("devtools.inspector.sidebarOpen");
+  Services.prefs.clearUserPref("devtools.toolbox.footer.height");
+  gDevTools.testing = false;
+});
+
+// All tests are async in general
+waitForExplicitFinish();
+
+function loadTab(url) {
+  let deferred = promise.defer();
+
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function onload() {
+    gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+    waitForFocus(function() {
+      deferred.resolve(content);
+    }, content);
+  }, true);
+
+  content.location = url;
+
+  return deferred.promise;
+}
+
+function selectNode(aNode) {
+  info("selecting node");
+  let onSelect = inspector.once("layoutview-updated");
+  inspector.selection.setNode(aNode, "test");
+  return onSelect.then(() => {
+    let view = inspector.sidebar.getWindowForTab("layoutview");
+    ok(!!view.layoutview, "LayoutView document is alive.");
+
+    return view;
+  });
+}
+
+function waitForUpdate() {
+  return inspector.once("layoutview-updated");
+}
+
+function asyncTest(testfunc) {
+  return Task.async(function*() {
+    let initialTab = gBrowser.selectedTab;
+
+    yield testfunc();
+
+    // Remove all tabs except for the initial tab. This is basically
+    // gBrowser.removeAllTabsBut without the animation
+    let tabs = gBrowser.visibleTabs;
+    gBrowser.selectedTab = initialTab;
+    for (let i = tabs.length - 1; i >= 0; i--) {
+      if (tabs[i] != initialTab)
+        gBrowser.removeTab(tabs[i]);
+    }
+
+    // Reset the sidebar back to the default
+    Services.prefs.setCharPref("devtools.inspector.activeSidebar", "ruleview");
+
+    finish();
+  });
+}
+
+var TESTS = [];
+
+function addTest(message, func) {
+  TESTS.push([message, Task.async(func)])
+}
+
+var runTests = Task.async(function*() {
+  for (let [message, test] of TESTS) {
+    info(message);
+    yield test();
+  }
+});
--- a/browser/devtools/layoutview/view.css
+++ b/browser/devtools/layoutview/view.css
@@ -81,19 +81,26 @@ body {
 #main > p {
   margin: 0;
   text-align: center;
 }
 
 #main > p > span {
   vertical-align: middle;
   pointer-events: auto;
+}
+
+.size > span {
   cursor: default;
 }
 
+.editable {
+  -moz-user-select: text;
+}
+
 .top,
 .bottom {
   width: calc(100% - 2px);
   text-align: center;
 }
 
 .padding.top {
   top: 55px;
@@ -174,9 +181,8 @@ body {
   pointer-events: none;
 }
 
 body.dim > #header > #element-position,
 body.dim > #main > p,
 body.dim > #main > .tooltip {
   visibility: hidden;
 }
-
--- a/browser/devtools/layoutview/view.js
+++ b/browser/devtools/layoutview/view.js
@@ -6,20 +6,123 @@
 
 "use strict";
 
 const Cu = Components.utils;
 const Ci = Components.interfaces;
 const Cc = Components.classes;
 
 Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/devtools/Loader.jsm");
 Cu.import("resource://gre/modules/devtools/Console.jsm");
 
 const promise = devtools.require("sdk/core/promise");
+const {InplaceEditor, editableItem} = devtools.require("devtools/shared/inplace-editor");
+const {parseDeclarations} = devtools.require("devtools/styleinspector/css-parsing-utils");
+
+const NUMERIC = /^-?[\d\.]+$/;
+
+/**
+ * An instance of EditingSession tracks changes that have been made during the
+ * modification of box model values. All of these changes can be reverted by
+ * calling revert.
+ *
+ * @param doc    A DOM document that can be used to test style rules.
+ * @param rules  An array of the style rules defined for the node being edited.
+ *               These should be in order of priority, least important first.
+ */
+function EditingSession(doc, rules) {
+  this._doc = doc;
+  this._rules = rules;
+  this._modifications = new Map();
+}
+
+EditingSession.prototype = {
+  /**
+   * Gets the value of a single property from the CSS rule.
+   *
+   * @param rule      The CSS rule
+   * @param property  The name of the property
+   */
+  getPropertyFromRule: function(rule, property) {
+    let dummyStyle = this._element.style;
+
+    dummyStyle.cssText = rule.cssText;
+    return dummyStyle.getPropertyValue(property);
+  },
+
+  /**
+   * Returns the current value for a property as a string or the empty string if
+   * no style rules affect the property.
+   *
+   * @param property  The name of the property as a string
+   */
+  getProperty: function(property) {
+    // Create a hidden element for getPropertyFromRule to use
+    let div = this._doc.createElement("div");
+    div.setAttribute("style", "display: none");
+    this._doc.body.appendChild(div);
+    this._element = this._doc.createElement("p");
+    div.appendChild(this._element);
+
+    // As the rules are in order of priority we can just iterate until we find
+    // the first that defines a value for the property and return that.
+    for (let rule of this._rules) {
+      let value = this.getPropertyFromRule(rule, property);
+      if (value !== "") {
+        div.remove();
+        return value;
+      }
+    }
+    div.remove();
+    return "";
+  },
+
+  /**
+   * Sets a number of properties on the node. Returns a promise that will be
+   * resolved when the modifications are complete.
+   *
+   * @param properties  An array of properties, each is an object with name and
+   *                    value properties. If the value is "" then the property
+   *                    is removed.
+   */
+  setProperties: function(properties) {
+    let modifications = this._rules[0].startModifyingProperties();
+
+    for (let property of properties) {
+      if (!this._modifications.has(property.name))
+        this._modifications.set(property.name, this.getPropertyFromRule(this._rules[0], property.name));
+
+      if (property.value == "")
+        modifications.removeProperty(property.name);
+      else
+        modifications.setProperty(property.name, property.value, "");
+    }
+
+    return modifications.apply().then(null, console.error);
+  },
+
+  /**
+   * Reverts all of the property changes made by this instance. Returns a
+   * promise that will be resolved when complete.
+   */
+  revert: function() {
+    let modifications = this._rules[0].startModifyingProperties();
+
+    for (let [property, value] of this._modifications) {
+      if (value != "")
+        modifications.setProperty(property, value, "");
+      else
+        modifications.removeProperty(property);
+    }
+
+    return modifications.apply().then(null, console.error);
+  }
+};
 
 function LayoutView(aInspector, aWindow)
 {
   this.inspector = aInspector;
 
   // <browser> is not always available (for Chrome targets for example)
   if (this.inspector.target.tab) {
     this.browser = aInspector.target.tab.linkedBrowser;
@@ -49,52 +152,111 @@ LayoutView.prototype = {
                  property: "position",
                  value: undefined},
       marginTop: {selector: ".margin.top > span",
                   property: "margin-top",
                   value: undefined},
       marginBottom: {selector: ".margin.bottom > span",
                   property: "margin-bottom",
                   value: undefined},
+      // margin-left is a shorthand for some internal properties,
+      // margin-left-ltr-source and margin-left-rtl-source for example. The
+      // real margin value we want is in margin-left-value
       marginLeft: {selector: ".margin.left > span",
                   property: "margin-left",
+                  realProperty: "margin-left-value",
                   value: undefined},
+      // margin-right behaves the same as margin-left
       marginRight: {selector: ".margin.right > span",
                   property: "margin-right",
+                  realProperty: "margin-right-value",
                   value: undefined},
       paddingTop: {selector: ".padding.top > span",
                   property: "padding-top",
                   value: undefined},
       paddingBottom: {selector: ".padding.bottom > span",
                   property: "padding-bottom",
                   value: undefined},
+      // padding-left behaves the same as margin-left
       paddingLeft: {selector: ".padding.left > span",
                   property: "padding-left",
+                  realProperty: "padding-left-value",
                   value: undefined},
+      // padding-right behaves the same as margin-left
       paddingRight: {selector: ".padding.right > span",
                   property: "padding-right",
+                  realProperty: "padding-right-value",
                   value: undefined},
       borderTop: {selector: ".border.top > span",
                   property: "border-top-width",
                   value: undefined},
       borderBottom: {selector: ".border.bottom > span",
                   property: "border-bottom-width",
                   value: undefined},
       borderLeft: {selector: ".border.left > span",
                   property: "border-left-width",
                   value: undefined},
       borderRight: {selector: ".border.right > span",
                   property: "border-right-width",
                   value: undefined},
     };
 
+    // Make each element the dimensions editable
+    for (let i in this.map) {
+      if (i == "position")
+        continue;
+
+      let dimension = this.map[i];
+      editableItem({ element: this.doc.querySelector(dimension.selector) }, (element, event) => {
+        this.initEditor(element, event, dimension);
+      });
+    }
+
     this.onNewNode();
   },
 
   /**
+   * Called when the user clicks on one of the editable values in the layoutview
+   */
+  initEditor: function LV_initEditor(element, event, dimension) {
+    let { property, realProperty } = dimension;
+    if (!realProperty)
+      realProperty = property;
+    let session = new EditingSession(document, this.elementRules);
+    let initialValue = session.getProperty(realProperty);
+
+    new InplaceEditor({
+      element: element,
+      initial: initialValue,
+
+      change: (value) => {
+        if (NUMERIC.test(value))
+          value += "px";
+        let properties = [
+          { name: property, value: value }
+        ]
+
+        if (property.substring(0, 7) == "border-") {
+          let bprop = property.substring(0, property.length - 5) + "style";
+          let style = session.getProperty(bprop);
+          if (!style || style == "none" || style == "hidden")
+            properties.push({ name: bprop, value: "solid" });
+        }
+
+        session.setProperties(properties);
+      },
+
+      done: (value, commit) => {
+        if (!commit)
+          session.revert();
+      }
+    }, event);
+  },
+
+  /**
    * Is the layoutview visible in the sidebar?
    */
   isActive: function LV_isActive() {
     return this.inspector.sidebar.getCurrentTabID() == "layoutview";
   },
 
   /**
    * Destroy the nodes. Remove listeners.
@@ -153,33 +315,38 @@ LayoutView.prototype = {
       this.trackingPaint = true;
     }
     this.doc.body.classList.remove("dim");
     this.dimmed = false;
   },
 
   /**
    * Compute the dimensions of the node and update the values in
-   * the layoutview/view.xhtml document.
+   * the layoutview/view.xhtml document. Returns a promise that will be resolved
+   * when complete.
    */
   update: function LV_update() {
-    if (!this.isActive() ||
-        !this.inspector.selection.isConnected() ||
-        !this.inspector.selection.isElementNode()) {
-      return promise.resolve(undefined);
-    }
+    let lastRequest = Task.spawn((function*() {
+      if (!this.isActive() ||
+          !this.inspector.selection.isConnected() ||
+          !this.inspector.selection.isElementNode()) {
+        return;
+      }
 
-    let node = this.inspector.selection.nodeFront;
-    let lastRequest = this.inspector.pageStyle.getLayout(node, {
-      autoMargins: !this.dimmed
-    }).then(layout => {
+      let node = this.inspector.selection.nodeFront;
+      let layout = yield this.inspector.pageStyle.getLayout(node, {
+        autoMargins: !this.dimmed
+      });
+      let styleEntries = yield this.inspector.pageStyle.getApplied(node, {});
+
       // If a subsequent request has been made, wait for that one instead.
       if (this._lastRequest != lastRequest) {
         return this._lastRequest;
       }
+
       this._lastRequest = null;
       let width = layout.width;
       let height = layout.height;
       let newLabel = width + "x" + height;
       if (this.sizeHeadingLabel.textContent != newLabel) {
         this.sizeHeadingLabel.textContent = newLabel;
       }
 
@@ -228,22 +395,22 @@ LayoutView.prototype = {
       height -= this.map.borderTop.value + this.map.borderBottom.value +
                 this.map.paddingTop.value + this.map.paddingBottom.value;
 
       let newValue = width + "x" + height;
       if (this.sizeLabel.textContent != newValue) {
         this.sizeLabel.textContent = newValue;
       }
 
+      this.elementRules = [e.rule for (e of styleEntries)];
+
       this.inspector.emit("layoutview-updated");
-      return null;
-    });
+    }).bind(this)).then(null, console.error);
 
-    this._lastRequest = lastRequest;
-    return this._lastRequest;
+    return this._lastRequest = lastRequest;
   },
 
   showBoxModel: function(options={}) {
     let toolbox = this.inspector.toolbox;
     let nodeFront = this.inspector.selection.nodeFront;
 
     toolbox.highlighterUtils.highlightNodeFront(nodeFront, options);
   },
--- a/browser/devtools/layoutview/view.xhtml
+++ b/browser/devtools/layoutview/view.xhtml
@@ -34,31 +34,34 @@
         <div id="borders" data-box="border" tooltip="&border.tooltip;">
           <div id="padding" data-box="padding" tooltip="&padding.tooltip;">
             <div id="content" data-box="content" tooltip="&content.tooltip;">
             </div>
           </div>
         </div>
       </div>
 
-      <p class="border top"><span data-box="border" tooltip="border-top"></span></p>
-      <p class="border right"><span data-box="border" tooltip="border-right"></span></p>
-      <p class="border bottom"><span data-box="border" tooltip="border-bottom"></span></p>
-      <p class="border left"><span data-box="border" tooltip="border-left"></span></p>
+      <p class="border top"><span data-box="border" class="editable" tooltip="border-top"></span></p>
+      <p class="border right"><span data-box="border" class="editable" tooltip="border-right"></span></p>
+      <p class="border bottom"><span data-box="border" class="editable" tooltip="border-bottom"></span></p>
+      <p class="border left"><span data-box="border" class="editable" tooltip="border-left"></span></p>
 
-      <p class="margin top"><span data-box="margin" tooltip="margin-top"></span></p>
-      <p class="margin right"><span data-box="margin" tooltip="margin-right"></span></p>
-      <p class="margin bottom"><span data-box="margin" tooltip="margin-bottom"></span></p>
-      <p class="margin left"><span data-box="margin" tooltip="margin-left"></span></p>
+      <p class="margin top"><span data-box="margin" class="editable" tooltip="margin-top"></span></p>
+      <p class="margin right"><span data-box="margin" class="editable" tooltip="margin-right"></span></p>
+      <p class="margin bottom"><span data-box="margin" class="editable" tooltip="margin-bottom"></span></p>
+      <p class="margin left"><span data-box="margin" class="editable" tooltip="margin-left"></span></p>
 
-      <p class="padding top"><span data-box="padding" tooltip="padding-top"></span></p>
-      <p class="padding right"><span data-box="padding" tooltip="padding-right"></span></p>
-      <p class="padding bottom"><span data-box="padding" tooltip="padding-bottom"></span></p>
-      <p class="padding left"><span data-box="padding" tooltip="padding-left"></span></p>
+      <p class="padding top"><span data-box="padding" class="editable" tooltip="padding-top"></span></p>
+      <p class="padding right"><span data-box="padding" class="editable" tooltip="padding-right"></span></p>
+      <p class="padding bottom"><span data-box="padding" class="editable" tooltip="padding-bottom"></span></p>
+      <p class="padding left"><span data-box="padding" class="editable" tooltip="padding-left"></span></p>
 
       <p class="size"><span data-box="content" tooltip="&content.tooltip;"></span></p>
 
       <span class="tooltip"></span>
 
     </div>
 
+    <div style="display: none">
+      <p id="dummy"></p>
+    </div>
   </body>
 </html>
--- a/browser/devtools/shared/inplace-editor.js
+++ b/browser/devtools/shared/inplace-editor.js
@@ -371,16 +371,21 @@ InplaceEditor.prototype = {
     if (!newValue) {
       return false;
     }
 
     this.input.value = newValue.value;
     this.input.setSelectionRange(newValue.start, newValue.end);
     this._doValidation();
 
+    // Call the user's change handler if available.
+    if (this.change) {
+      this.change(this.input.value.trim());
+    }
+
     return true;
   },
 
   /**
    * Increment the property value based on the property type.
    *
    * @param {string} value
    *        Property value.
--- a/browser/themes/shared/devtools/layoutview.css
+++ b/browser/themes/shared/devtools/layoutview.css
@@ -34,8 +34,21 @@
   border-style: dotted;
   border-color: hsl(210,100%,85%);
 }
 
 #margins {
   background-color: #d89b28;
   opacity: 0.6;
 }
+
+.editable {
+  border-bottom: 1px dashed transparent;
+}
+
+.editable:hover {
+  border-bottom-color: hsl(0,0%,50%);
+}
+
+.styleinspector-propertyeditor {
+  border: 1px solid #CCC;
+  padding: 0;
+}