summaryrefslogtreecommitdiff
path: root/toolkit/devtools/layoutview
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/devtools/layoutview')
-rw-r--r--toolkit/devtools/layoutview/moz.build8
-rw-r--r--toolkit/devtools/layoutview/test/browser.ini20
-rw-r--r--toolkit/devtools/layoutview/test/browser_layoutview.js79
-rw-r--r--toolkit/devtools/layoutview/test/browser_layoutview_editablemodel.js146
-rw-r--r--toolkit/devtools/layoutview/test/browser_layoutview_editablemodel_allproperties.js143
-rw-r--r--toolkit/devtools/layoutview/test/browser_layoutview_editablemodel_border.js51
-rw-r--r--toolkit/devtools/layoutview/test/browser_layoutview_editablemodel_stylerules.js106
-rw-r--r--toolkit/devtools/layoutview/test/browser_layoutview_guides.js55
-rw-r--r--toolkit/devtools/layoutview/test/browser_layoutview_rotate-labels-on-sides.js46
-rw-r--r--toolkit/devtools/layoutview/test/browser_layoutview_update-after-navigation.js98
-rw-r--r--toolkit/devtools/layoutview/test/browser_layoutview_update-after-reload.js40
-rw-r--r--toolkit/devtools/layoutview/test/browser_layoutview_update-in-iframes.js60
-rw-r--r--toolkit/devtools/layoutview/test/doc_layoutview_iframe1.html3
-rw-r--r--toolkit/devtools/layoutview/test/doc_layoutview_iframe2.html3
-rw-r--r--toolkit/devtools/layoutview/test/head.js262
-rw-r--r--toolkit/devtools/layoutview/view.css263
-rw-r--r--toolkit/devtools/layoutview/view.js552
-rw-r--r--toolkit/devtools/layoutview/view.xhtml67
18 files changed, 2002 insertions, 0 deletions
diff --git a/toolkit/devtools/layoutview/moz.build b/toolkit/devtools/layoutview/moz.build
new file mode 100644
index 000000000..413e62508
--- /dev/null
+++ b/toolkit/devtools/layoutview/moz.build
@@ -0,0 +1,8 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
+
diff --git a/toolkit/devtools/layoutview/test/browser.ini b/toolkit/devtools/layoutview/test/browser.ini
new file mode 100644
index 000000000..de9ae08dd
--- /dev/null
+++ b/toolkit/devtools/layoutview/test/browser.ini
@@ -0,0 +1,20 @@
+[DEFAULT]
+skip-if = e10s # Bug ?????? - devtools tests disabled with e10s
+subsuite = devtools
+support-files =
+ doc_layoutview_iframe1.html
+ doc_layoutview_iframe2.html
+ head.js
+
+[browser_layoutview.js]
+[browser_layoutview_editablemodel.js]
+# [browser_layoutview_editablemodel_allproperties.js]
+# Disabled for too many intermittent failures (bug 1009322)
+[browser_layoutview_editablemodel_border.js]
+[browser_layoutview_editablemodel_stylerules.js]
+[browser_layoutview_guides.js]
+[browser_layoutview_rotate-labels-on-sides.js]
+[browser_layoutview_update-after-navigation.js]
+[browser_layoutview_update-after-reload.js]
+# [browser_layoutview_update-in-iframes.js]
+# Bug 1020038 layout-view updates for iframe elements changes
diff --git a/toolkit/devtools/layoutview/test/browser_layoutview.js b/toolkit/devtools/layoutview/test/browser_layoutview.js
new file mode 100644
index 000000000..e9dad7f4a
--- /dev/null
+++ b/toolkit/devtools/layoutview/test/browser_layoutview.js
@@ -0,0 +1,79 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the layout-view displays the right values and that it updates when
+// the node's style is changed
+
+// Expected values:
+let res1 = [
+ {selector: "#element-size", value: "160" + "\u00D7" + "160"},
+ {selector: ".size > span", value: "100" + "\u00D7" + "100"},
+ {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: "190" + "\u00D7" + "210"},
+ {selector: ".size > span", value: "100" + "\u00D7" + "150"},
+ {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},
+];
+
+add_task(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>"
+
+ yield addTab("data:text/html," + encodeURIComponent(html));
+ let {toolbox, inspector, view} = yield openLayoutView();
+ yield selectNode("div", inspector);
+
+ yield runTests(inspector, view);
+});
+
+addTest("Test that the initial values of the box model are correct",
+function*(inspector, view) {
+ let viewdoc = view.doc;
+
+ 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.");
+ }
+});
+
+addTest("Test that changing the document updates the box model",
+function*(inspector, view) {
+ let viewdoc = view.doc;
+
+ let onUpdated = waitForUpdate(inspector);
+ inspector.selection.node.style.height = "150px";
+ inspector.selection.node.style.paddingRight = "50px";
+ yield onUpdated;
+
+ 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.");
+ }
+});
diff --git a/toolkit/devtools/layoutview/test/browser_layoutview_editablemodel.js b/toolkit/devtools/layoutview/test/browser_layoutview_editablemodel.js
new file mode 100644
index 000000000..cea92081a
--- /dev/null
+++ b/toolkit/devtools/layoutview/test/browser_layoutview_editablemodel.js
@@ -0,0 +1,146 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that editing the box-model values works as expected and test various
+// key bindings
+
+const TEST_URI = "<style>" +
+ "div { margin: 10px; padding: 3px }" +
+ "#div1 { margin-top: 5px }" +
+ "#div2 { border-bottom: 1em solid black; }" +
+ "#div3 { padding: 2em; }" +
+ "</style>" +
+ "<div id='div1'></div><div id='div2'></div><div id='div3'></div>";
+
+function getStyle(node, property) {
+ return node.style.getPropertyValue(property);
+}
+
+add_task(function*() {
+ yield addTab("data:text/html," + encodeURIComponent(TEST_URI));
+ let {toolbox, inspector, view} = yield openLayoutView();
+
+ yield runTests(inspector, view);
+});
+
+addTest("Test that editing margin dynamically updates the document, pressing escape cancels the changes",
+function*(inspector, view) {
+ let node = content.document.getElementById("div1");
+ is(getStyle(node, "margin-top"), "", "Should be no margin-top on the element.")
+ yield selectNode(node, inspector);
+
+ let span = view.doc.querySelector(".margin.top > span");
+ is(span.textContent, 5, "Should have the right value in the box model.");
+
+ EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
+ let editor = view.doc.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.doc.defaultView);
+ yield waitForUpdate(inspector);
+
+ is(getStyle(node, "margin-top"), "3px", "Should have updated the margin.");
+
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, view.doc.defaultView);
+ yield waitForUpdate(inspector);
+
+ 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*(inspector, view) {
+ let node = content.document.getElementById("div1");
+ is(getStyle(node, "margin-left"), "", "Should be no margin-top on the element.")
+ yield selectNode(node, inspector);
+
+ let span = view.doc.querySelector(".margin.left > span");
+ is(span.textContent, 10, "Should have the right value in the box model.");
+
+ EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
+ let editor = view.doc.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.doc.defaultView);
+ yield waitForUpdate(inspector);
+
+ 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.doc.defaultView);
+ yield waitForUpdate(inspector);
+
+ 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.doc.defaultView);
+ yield waitForUpdate(inspector);
+
+ 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.doc.defaultView);
+
+ 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*(inspector, view) {
+ let node = content.document.getElementById("div1");
+ is(getStyle(node, "margin-left"), "20px", "Should be the right margin-top on the element.")
+ yield selectNode(node, inspector);
+
+ let span = view.doc.querySelector(".margin.left > span");
+ is(span.textContent, 20, "Should have the right value in the box model.");
+
+ EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
+ let editor = view.doc.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.doc.defaultView);
+ yield waitForUpdate(inspector);
+
+ 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.doc.defaultView);
+ yield waitForUpdate(inspector);
+
+ 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*(inspector, view) {
+ let node = content.document.getElementById("div1");
+
+ node.style.marginRight = "15px";
+ yield waitForUpdate(inspector);
+
+ yield selectNode(node, inspector);
+
+ let span = view.doc.querySelector(".margin.right > span");
+ is(span.textContent, 15, "Should have the right value in the box model.");
+
+ EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
+ let editor = view.doc.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.doc.defaultView);
+ yield waitForUpdate(inspector);
+
+ 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.doc.defaultView);
+
+ 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.");
+});
diff --git a/toolkit/devtools/layoutview/test/browser_layoutview_editablemodel_allproperties.js b/toolkit/devtools/layoutview/test/browser_layoutview_editablemodel_allproperties.js
new file mode 100644
index 000000000..3df11773b
--- /dev/null
+++ b/toolkit/devtools/layoutview/test/browser_layoutview_editablemodel_allproperties.js
@@ -0,0 +1,143 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test editing box model values when all values are set
+
+const TEST_URI = "<style>" +
+ "div { margin: 10px; padding: 3px }" +
+ "#div1 { margin-top: 5px }" +
+ "#div2 { border-bottom: 1em solid black; }" +
+ "#div3 { padding: 2em; }" +
+ "</style>" +
+ "<div id='div1'></div><div id='div2'></div><div id='div3'></div>";
+
+function getStyle(node, property) {
+ return node.style.getPropertyValue(property);
+}
+
+add_task(function*() {
+ yield addTab("data:text/html," + encodeURIComponent(TEST_URI));
+ let {toolbox, inspector, view} = yield openLayoutView();
+
+ yield runTests(inspector, view);
+});
+
+addTest("When all properties are set on the node editing one should work",
+function*(inspector, view) {
+ let node = content.document.getElementById("div1");
+
+ node.style.padding = "5px";
+ yield waitForUpdate(inspector);
+
+ yield selectNode(node, inspector);
+
+ let span = view.doc.querySelector(".padding.bottom > span");
+ is(span.textContent, 5, "Should have the right value in the box model.");
+
+ EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
+ let editor = view.doc.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.doc.defaultView);
+ yield waitForUpdate(inspector);
+
+ 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.doc.defaultView);
+
+ 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*(inspector, view) {
+ let node = content.document.getElementById("div1");
+
+ node.style.padding = "5px";
+ yield waitForUpdate(inspector);
+
+ yield selectNode(node, inspector);
+
+ let span = view.doc.querySelector(".padding.left > span");
+ is(span.textContent, 5, "Should have the right value in the box model.");
+
+ EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
+ let editor = view.doc.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.doc.defaultView);
+ yield waitForUpdate(inspector);
+
+ 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.doc.defaultView);
+ yield waitForUpdate(inspector);
+
+ 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*(inspector, view) {
+ let node = content.document.getElementById("div1");
+
+ node.style.padding = "5px";
+
+ yield selectNode(node, inspector);
+
+ let span = view.doc.querySelector(".padding.left > span");
+ is(span.textContent, 5, "Should have the right value in the box model.");
+
+ EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
+ let editor = view.doc.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.doc.defaultView);
+ yield waitForUpdate(inspector);
+
+ 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.doc.defaultView);
+
+ 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*(inspector, view) {
+ let node = content.document.getElementById("div1");
+
+ node.style.padding = "5px";
+ yield waitForUpdate(inspector);
+
+ yield selectNode(node, inspector);
+
+ let span = view.doc.querySelector(".padding.left > span");
+ is(span.textContent, 5, "Should have the right value in the box model.");
+
+ EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
+ let editor = view.doc.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.doc.defaultView);
+ yield waitForUpdate(inspector);
+
+ 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.doc.defaultView);
+ yield waitForUpdate(inspector);
+
+ is(getStyle(node, "padding-left"), "5px", "Should be the right padding.")
+ is(span.textContent, 5, "Should have the right value in the box model.");
+});
diff --git a/toolkit/devtools/layoutview/test/browser_layoutview_editablemodel_border.js b/toolkit/devtools/layoutview/test/browser_layoutview_editablemodel_border.js
new file mode 100644
index 000000000..3d92aa5f0
--- /dev/null
+++ b/toolkit/devtools/layoutview/test/browser_layoutview_editablemodel_border.js
@@ -0,0 +1,51 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that editing the border value in the box model applies the border style
+
+const TEST_URI = "<style>" +
+ "div { margin: 10px; padding: 3px }" +
+ "#div1 { margin-top: 5px }" +
+ "#div2 { border-bottom: 1em solid black; }" +
+ "#div3 { padding: 2em; }" +
+ "</style>" +
+ "<div id='div1'></div><div id='div2'></div><div id='div3'></div>";
+
+function getStyle(node, property) {
+ return node.style.getPropertyValue(property);
+}
+
+add_task(function*() {
+ yield addTab("data:text/html," + encodeURIComponent(TEST_URI));
+ let {toolbox, inspector, view} = yield openLayoutView();
+
+ let node = content.document.getElementById("div1");
+ is(getStyle(node, "border-top-width"), "", "Should have the right border");
+ is(getStyle(node, "border-top-style"), "", "Should have the right border");
+ yield selectNode(node, inspector);
+
+ let span = view.doc.querySelector(".border.top > span");
+ is(span.textContent, 0, "Should have the right value in the box model.");
+
+ EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
+ let editor = view.doc.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.doc.defaultView);
+ yield waitForUpdate(inspector);
+
+ 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.doc.defaultView);
+ yield waitForUpdate(inspector);
+
+ 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.");
+});
diff --git a/toolkit/devtools/layoutview/test/browser_layoutview_editablemodel_stylerules.js b/toolkit/devtools/layoutview/test/browser_layoutview_editablemodel_stylerules.js
new file mode 100644
index 000000000..44698116d
--- /dev/null
+++ b/toolkit/devtools/layoutview/test/browser_layoutview_editablemodel_stylerules.js
@@ -0,0 +1,106 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that units are displayed correctly when editing values in the box model
+// and that values are retrieved and parsed correctly from the back-end
+
+const TEST_URI = "<style>" +
+ "div { margin: 10px; padding: 3px }" +
+ "#div1 { margin-top: 5px }" +
+ "#div2 { border-bottom: 1em solid black; }" +
+ "#div3 { padding: 2em; }" +
+ "</style>" +
+ "<div id='div1'></div><div id='div2'></div><div id='div3'></div>";
+
+function getStyle(node, property) {
+ return node.style.getPropertyValue(property);
+}
+
+add_task(function*() {
+ yield addTab("data:text/html," + encodeURIComponent(TEST_URI));
+ let {toolbox, inspector, view} = yield openLayoutView();
+
+ yield runTests(inspector, view);
+});
+
+addTest("Test that entering units works",
+function*(inspector, view) {
+ let node = content.document.getElementById("div1");
+ is(getStyle(node, "padding-top"), "", "Should have the right padding");
+ yield selectNode(node, inspector);
+
+ let span = view.doc.querySelector(".padding.top > span");
+ is(span.textContent, 3, "Should have the right value in the box model.");
+
+ EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
+ let editor = view.doc.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.doc.defaultView);
+ yield waitForUpdate(inspector);
+ EventUtils.synthesizeKey("e", {}, view.doc.defaultView);
+ yield waitForUpdate(inspector);
+
+ is(getStyle(node, "padding-top"), "", "An invalid value is handled cleanly");
+
+ EventUtils.synthesizeKey("m", {}, view.doc.defaultView);
+ yield waitForUpdate(inspector);
+
+ 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.doc.defaultView);
+
+ 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*(inspector, view) {
+ let node = content.document.getElementById("div2");
+ is(getStyle(node, "border-bottom-width"), "", "Should have the right border-bottom-width");
+ yield selectNode(node, inspector);
+
+ let span = view.doc.querySelector(".border.bottom > span");
+ is(span.textContent, 16, "Should have the right value in the box model.");
+
+ EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
+ let editor = view.doc.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.doc.defaultView);
+ yield waitForUpdate(inspector);
+
+ 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.doc.defaultView);
+
+ 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*(inspector, view) {
+ let node = content.document.getElementById("div3");
+ is(getStyle(node, "padding-right"), "", "Should have the right padding");
+ yield selectNode(node, inspector);
+
+ let span = view.doc.querySelector(".padding.right > span");
+ is(span.textContent, 32, "Should have the right value in the box model.");
+
+ EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
+ let editor = view.doc.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.doc.defaultView);
+
+ is(getStyle(node, "padding-right"), "", "Should be the right padding.")
+ is(span.textContent, 32, "Should have the right value in the box model.");
+});
diff --git a/toolkit/devtools/layoutview/test/browser_layoutview_guides.js b/toolkit/devtools/layoutview/test/browser_layoutview_guides.js
new file mode 100644
index 000000000..a4ba45fd2
--- /dev/null
+++ b/toolkit/devtools/layoutview/test/browser_layoutview_guides.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that hovering over regions in the box-model shows the highlighter with
+// the right options.
+// Tests that actually check the highlighter is displayed and correct are in the
+// devtools/inspector/test folder. This test only cares about checking that the
+// layout-view does call the highlighter, and it does so by mocking it.
+
+const STYLE = "div { position: absolute; top: 50px; left: 50px; height: 10px; " +
+ "width: 10px; border: 10px solid black; padding: 10px; margin: 10px;}";
+const HTML = "<style>" + STYLE + "</style><div></div>";
+const TEST_URL = "data:text/html;charset=utf-8," + encodeURIComponent(HTML);
+
+let highlightedNodeFront, highlighterOptions;
+
+add_task(function*() {
+ yield addTab(TEST_URL);
+ let {toolbox, inspector, view} = yield openLayoutView();
+ yield selectNode("div", inspector);
+
+ // Mock the highlighter by replacing the showBoxModel method.
+ toolbox.highlighter.showBoxModel = function(nodeFront, options) {
+ highlightedNodeFront = nodeFront;
+ highlighterOptions = options;
+ }
+
+ let elt = view.doc.getElementById("margins");
+ yield testGuideOnLayoutHover(elt, "margin", inspector, view);
+
+ elt = view.doc.getElementById("borders");
+ yield testGuideOnLayoutHover(elt, "border", inspector, view);
+
+ elt = view.doc.getElementById("padding");
+ yield testGuideOnLayoutHover(elt, "padding", inspector, view);
+
+ elt = view.doc.getElementById("content");
+ yield testGuideOnLayoutHover(elt, "content", inspector, view);
+});
+
+function* testGuideOnLayoutHover(elt, expectedRegion, inspector, view) {
+ info("Synthesizing mouseover on the layout-view");
+ EventUtils.synthesizeMouse(elt, 2, 2, {type:'mouseover'},
+ elt.ownerDocument.defaultView);
+
+ info("Waiting for the node-highlight event from the toolbox");
+ yield inspector.toolbox.once("node-highlight");
+
+ is(highlightedNodeFront, inspector.selection.nodeFront,
+ "The right nodeFront was highlighted");
+ is(highlighterOptions.region, expectedRegion,
+ "Region " + expectedRegion + " was highlighted");
+}
diff --git a/toolkit/devtools/layoutview/test/browser_layoutview_rotate-labels-on-sides.js b/toolkit/devtools/layoutview/test/browser_layoutview_rotate-labels-on-sides.js
new file mode 100644
index 000000000..6d73b4f26
--- /dev/null
+++ b/toolkit/devtools/layoutview/test/browser_layoutview_rotate-labels-on-sides.js
@@ -0,0 +1,46 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that longer values are rotated on the side
+
+const res1 = [
+ {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: 2000000},
+ {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},
+];
+
+const TEST_URI = encodeURIComponent([
+ "<style>",
+ "div{border:10px solid black; padding: 20px 20px 20px 2000000px; margin: 30px auto;}",
+ "</style>",
+ "<div></div>"
+].join(""));
+const LONG_TEXT_ROTATE_LIMIT = 3;
+
+add_task(function*() {
+ yield addTab("data:text/html," + TEST_URI);
+ let {toolbox, inspector, view} = yield openLayoutView();
+ yield selectNode("div", inspector);
+
+ for (let i = 0; i < res1.length; i++) {
+ let elt = view.doc.querySelector(res1[i].selector);
+ let isLong = elt.textContent.length > LONG_TEXT_ROTATE_LIMIT;
+ let classList = elt.parentNode.classList
+ let canBeRotated = classList.contains("left") || classList.contains("right");
+ let isRotated = classList.contains("rotate");
+
+ is(canBeRotated && isLong, isRotated, res1[i].selector + " correctly rotated.");
+ }
+});
diff --git a/toolkit/devtools/layoutview/test/browser_layoutview_update-after-navigation.js b/toolkit/devtools/layoutview/test/browser_layoutview_update-after-navigation.js
new file mode 100644
index 000000000..3eebb2481
--- /dev/null
+++ b/toolkit/devtools/layoutview/test/browser_layoutview_update-after-navigation.js
@@ -0,0 +1,98 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the layout-view continues to work after a page navigation and that
+// it also works after going back
+
+add_task(function*() {
+ yield addTab(TEST_URL_ROOT + "doc_layoutview_iframe1.html");
+ let {toolbox, inspector, view} = yield openLayoutView();
+ yield runTests(inspector, view);
+});
+
+addTest("Test that the layout-view works on the first page",
+function*(inspector, view) {
+ info("Selecting the test node");
+ yield selectNode("p", inspector);
+
+ info("Checking that the layout-view shows the right value");
+ let paddingElt = view.doc.querySelector(".padding.top > span");
+ is(paddingElt.textContent, "50");
+
+ info("Listening for layout-view changes and modifying the padding");
+ let onUpdated = waitForUpdate(inspector);
+ getNode("p").style.padding = "20px";
+ yield onUpdated;
+ ok(true, "Layout-view got updated");
+
+ info("Checking that the layout-view shows the right value after update");
+ is(paddingElt.textContent, "20");
+});
+
+addTest("Navigate to the second page",
+function*(inspector, view) {
+ yield navigateTo(TEST_URL_ROOT + "doc_layoutview_iframe2.html");
+ yield inspector.once("markuploaded");
+});
+
+addTest("Test that the layout-view works on the second page",
+function*(inspector, view) {
+ info("Selecting the test node");
+ yield selectNode("p", inspector);
+
+ info("Checking that the layout-view shows the right value");
+ let sizeElt = view.doc.querySelector(".size > span");
+ is(sizeElt.textContent, "100" + "\u00D7" + "100");
+
+ info("Listening for layout-view changes and modifying the size");
+ let onUpdated = waitForUpdate(inspector);
+ getNode("p").style.width = "200px";
+ yield onUpdated;
+ ok(true, "Layout-view got updated");
+
+ info("Checking that the layout-view shows the right value after update");
+ is(sizeElt.textContent, "200" + "\u00D7" + "100");
+});
+
+addTest("Go back to the first page",
+function*(inspector, view) {
+ content.history.back();
+ yield inspector.once("markuploaded");
+});
+
+addTest("Test that the layout-view works on the first page after going back",
+function*(inspector, view) {
+ info("Selecting the test node");
+ yield selectNode("p", inspector);
+
+ info("Checking that the layout-view shows the right value, which is the" +
+ "modified value from step one because of the bfcache");
+ let paddingElt = view.doc.querySelector(".padding.top > span");
+ is(paddingElt.textContent, "20");
+
+ info("Listening for layout-view changes and modifying the padding");
+ let onUpdated = waitForUpdate(inspector);
+ getNode("p").style.padding = "100px";
+ yield onUpdated;
+ ok(true, "Layout-view got updated");
+
+ info("Checking that the layout-view shows the right value after update");
+ is(paddingElt.textContent, "100");
+});
+
+function navigateTo(url) {
+ info("Navigating to " + url);
+
+ let def = promise.defer();
+ gBrowser.selectedBrowser.addEventListener("load", function onload() {
+ gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+ info("URL " + url + " loading complete");
+ waitForFocus(def.resolve, content);
+ }, true);
+ content.location = url;
+
+ return def.promise;
+}
diff --git a/toolkit/devtools/layoutview/test/browser_layoutview_update-after-reload.js b/toolkit/devtools/layoutview/test/browser_layoutview_update-after-reload.js
new file mode 100644
index 000000000..6dc5a5ab5
--- /dev/null
+++ b/toolkit/devtools/layoutview/test/browser_layoutview_update-after-reload.js
@@ -0,0 +1,40 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the layout-view continues to work after the page is reloaded
+
+add_task(function*() {
+ yield addTab(TEST_URL_ROOT + "doc_layoutview_iframe1.html");
+ let {toolbox, inspector, view} = yield openLayoutView();
+
+ info("Test that the layout-view works on the first page");
+ yield assertLayoutView(inspector, view);
+
+ info("Reload the page");
+ content.location.reload();
+ yield inspector.once("markuploaded");
+
+ info("Test that the layout-view works on the reloaded page");
+ yield assertLayoutView(inspector, view);
+});
+
+function* assertLayoutView(inspector, view) {
+ info("Selecting the test node");
+ yield selectNode("p", inspector);
+
+ info("Checking that the layout-view shows the right value");
+ let paddingElt = view.doc.querySelector(".padding.top > span");
+ is(paddingElt.textContent, "50");
+
+ info("Listening for layout-view changes and modifying the padding");
+ let onUpdated = waitForUpdate(inspector);
+ getNode("p").style.padding = "20px";
+ yield onUpdated;
+ ok(true, "Layout-view got updated");
+
+ info("Checking that the layout-view shows the right value after update");
+ is(paddingElt.textContent, "20");
+}
diff --git a/toolkit/devtools/layoutview/test/browser_layoutview_update-in-iframes.js b/toolkit/devtools/layoutview/test/browser_layoutview_update-in-iframes.js
new file mode 100644
index 000000000..3d2ccd003
--- /dev/null
+++ b/toolkit/devtools/layoutview/test/browser_layoutview_update-in-iframes.js
@@ -0,0 +1,60 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the layout-view for elements within iframes also updates when they
+// change
+
+add_task(function*() {
+ yield addTab(TEST_URL_ROOT + "doc_layoutview_iframe1.html");
+ let iframe2 = getNode("iframe").contentDocument.querySelector("iframe");
+
+ let {toolbox, inspector, view} = yield openLayoutView();
+ yield runTests(inspector, view, iframe2);
+});
+
+addTest("Test that resizing an element in an iframe updates its box model",
+function*(inspector, view, iframe2) {
+ info("Selecting the nested test node");
+ let node = iframe2.contentDocument.querySelector("div");
+ yield selectNode(node, inspector);
+
+ info("Checking that the layout-view shows the right value");
+ let sizeElt = view.doc.querySelector(".size > span");
+ is(sizeElt.textContent, "400x200");
+
+ info("Listening for layout-view changes and modifying its size");
+ let onUpdated = waitForUpdate(inspector);
+ node.style.width = "200px";
+ yield onUpdated;
+ ok(true, "Layout-view got updated");
+
+ info("Checking that the layout-view shows the right value after update");
+ is(sizeElt.textContent, "200x200");
+});
+
+addTest("Test reflows are still sent to the layout-view after deleting an iframe",
+function*(inspector, view, iframe2) {
+ info("Deleting the iframe2");
+ iframe2.remove();
+ yield inspector.once("inspector-updated");
+
+ info("Selecting the test node in iframe1");
+ let node = getNode("iframe").contentDocument.querySelector("p");
+ yield selectNode(node, inspector);
+
+ info("Checking that the layout-view shows the right value");
+ let sizeElt = view.doc.querySelector(".size > span");
+ is(sizeElt.textContent, "100x100");
+
+ info("Listening for layout-view changes and modifying its size");
+ let onUpdated = waitForUpdate(inspector);
+ node.style.width = "200px";
+ yield onUpdated;
+ ok(true, "Layout-view got updated");
+
+ info("Checking that the layout-view shows the right value after update");
+ is(sizeElt.textContent, "200x100");
+});
diff --git a/toolkit/devtools/layoutview/test/doc_layoutview_iframe1.html b/toolkit/devtools/layoutview/test/doc_layoutview_iframe1.html
new file mode 100644
index 000000000..5d1bbc3df
--- /dev/null
+++ b/toolkit/devtools/layoutview/test/doc_layoutview_iframe1.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<p style="padding:50px;color:#f06;">Root page</p>
+<iframe src="doc_layoutview_iframe2.html"></iframe> \ No newline at end of file
diff --git a/toolkit/devtools/layoutview/test/doc_layoutview_iframe2.html b/toolkit/devtools/layoutview/test/doc_layoutview_iframe2.html
new file mode 100644
index 000000000..b651f6f1e
--- /dev/null
+++ b/toolkit/devtools/layoutview/test/doc_layoutview_iframe2.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<p style="width:100px;height:100px;background:red;">iframe 1</p>
+<iframe src="data:text/html,<div style='width:400px;height:200px;background:yellow;'>iframe 2</div>"></iframe> \ No newline at end of file
diff --git a/toolkit/devtools/layoutview/test/head.js b/toolkit/devtools/layoutview/test/head.js
new file mode 100644
index 000000000..5e3aa0ce2
--- /dev/null
+++ b/toolkit/devtools/layoutview/test/head.js
@@ -0,0 +1,262 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const Cu = Components.utils;
+let {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
+let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+let TargetFactory = devtools.TargetFactory;
+let {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
+let {console} = Components.utils.import("resource://gre/modules/devtools/Console.jsm", {});
+
+// All test are asynchronous
+waitForExplicitFinish();
+
+const TEST_URL_ROOT = "http://example.com/browser/browser/devtools/layoutview/test/";
+
+// Uncomment this pref to dump all devtools emitted events to the console.
+// Services.prefs.setBoolPref("devtools.dump.emit", true);
+
+// Services.prefs.setBoolPref("devtools.debugger.log", true);
+
+// Set the testing flag on gDevTools and reset it when the test ends
+gDevTools.testing = true;
+registerCleanupFunction(() => gDevTools.testing = false);
+
+// Clean-up all prefs that might have been changed during a test run
+// (safer here because if the test fails, then the pref is never reverted)
+Services.prefs.setIntPref("devtools.toolbox.footer.height", 350);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.dump.emit");
+ Services.prefs.clearUserPref("devtools.debugger.log");
+ Services.prefs.clearUserPref("devtools.toolbox.footer.height");
+ Services.prefs.setCharPref("devtools.inspector.activeSidebar", "ruleview");
+});
+
+registerCleanupFunction(function*() {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ yield gDevTools.closeToolbox(target);
+
+ // Move the mouse outside inspector. If the test happened fake a mouse event
+ // somewhere over inspector the pointer is considered to be there when the
+ // next test begins. This might cause unexpected events to be emitted when
+ // another test moves the mouse.
+ EventUtils.synthesizeMouseAtPoint(1, 1, {type: "mousemove"}, window);
+
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+});
+
+/**
+ * Add a new test tab in the browser and load the given url.
+ * @param {String} url The url to be loaded in the new tab
+ * @return a promise that resolves to the tab object when the url is loaded
+ */
+function addTab(url) {
+ let def = promise.defer();
+
+ let tab = gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onload() {
+ gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+ info("URL " + url + " loading complete into new test tab");
+ waitForFocus(() => {
+ def.resolve(tab);
+ }, content);
+ }, true);
+ content.location = url;
+
+ return def.promise;
+}
+
+/**
+ * Simple DOM node accesor function that takes either a node or a string css
+ * selector as argument and returns the corresponding node
+ * @param {String|DOMNode} nodeOrSelector
+ * @return {DOMNode}
+ */
+function getNode(nodeOrSelector) {
+ return typeof nodeOrSelector === "string" ?
+ content.document.querySelector(nodeOrSelector) :
+ nodeOrSelector;
+}
+
+/**
+ * Highlight a node and set the inspector's current selection to the node or
+ * the first match of the given css selector.
+ * @param {String|DOMNode} nodeOrSelector
+ * @param {InspectorPanel} inspector
+ * The instance of InspectorPanel currently loaded in the toolbox
+ * @return a promise that resolves when the inspector is updated with the new
+ * node
+ */
+function selectAndHighlightNode(nodeOrSelector, inspector) {
+ info("Highlighting and selecting the node " + nodeOrSelector);
+
+ let node = getNode(nodeOrSelector);
+ let updated = inspector.toolbox.once("highlighter-ready");
+ inspector.selection.setNode(node, "test-highlight");
+ return updated;
+
+}
+
+/**
+ * Set the inspector's current selection to a node or to the first match of the
+ * given css selector.
+ * @param {String|DOMNode} nodeOrSelector
+ * @param {InspectorPanel} inspector
+ * The instance of InspectorPanel currently loaded in the toolbox
+ * @param {String} reason
+ * Defaults to "test" which instructs the inspector not to highlight the
+ * node upon selection
+ * @return a promise that resolves when the inspector is updated with the new
+ * node
+ */
+function selectNode(nodeOrSelector, inspector, reason="test") {
+ info("Selecting the node " + nodeOrSelector);
+
+ let node = getNode(nodeOrSelector);
+ let updated = inspector.once("inspector-updated");
+ inspector.selection.setNode(node, reason);
+ return updated;
+}
+
+/**
+ * Open the toolbox, with the inspector tool visible.
+ * @return a promise that resolves when the inspector is ready
+ */
+let openInspector = Task.async(function*() {
+ info("Opening the inspector");
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+
+ let inspector, toolbox;
+
+ // The actual highligher show/hide methods are mocked in layoutview tests.
+ // The highlighter is tested in devtools/inspector/test.
+ function mockHighlighter({highlighter}) {
+ highlighter.showBoxModel = function(nodeFront, options) {
+ return promise.resolve();
+ }
+ highlighter.hideBoxModel = function() {
+ return promise.resolve();
+ }
+ }
+
+ // Checking if the toolbox and the inspector are already loaded
+ // The inspector-updated event should only be waited for if the inspector
+ // isn't loaded yet
+ toolbox = gDevTools.getToolbox(target);
+ if (toolbox) {
+ inspector = toolbox.getPanel("inspector");
+ if (inspector) {
+ info("Toolbox and inspector already open");
+ mockHighlighter(toolbox);
+ return {
+ toolbox: toolbox,
+ inspector: inspector
+ };
+ }
+ }
+
+ info("Opening the toolbox");
+ toolbox = yield gDevTools.showToolbox(target, "inspector");
+ yield waitForToolboxFrameFocus(toolbox);
+ inspector = toolbox.getPanel("inspector");
+
+ info("Waiting for the inspector to update");
+ yield inspector.once("inspector-updated");
+
+ mockHighlighter(toolbox);
+ return {
+ toolbox: toolbox,
+ inspector: inspector
+ };
+});
+
+/**
+ * Wait for the toolbox frame to receive focus after it loads
+ * @param {Toolbox} toolbox
+ * @return a promise that resolves when focus has been received
+ */
+function waitForToolboxFrameFocus(toolbox) {
+ info("Making sure that the toolbox's frame is focused");
+ let def = promise.defer();
+ let win = toolbox.frame.contentWindow;
+ waitForFocus(def.resolve, win);
+ return def.promise;
+}
+
+/**
+ * Checks whether the inspector's sidebar corresponding to the given id already
+ * exists
+ * @param {InspectorPanel}
+ * @param {String}
+ * @return {Boolean}
+ */
+function hasSideBarTab(inspector, id) {
+ return !!inspector.sidebar.getWindowForTab(id);
+}
+
+/**
+ * Open the toolbox, with the inspector tool visible, and the layout-view
+ * sidebar tab selected.
+ * @return a promise that resolves when the inspector is ready and the layout
+ * view is visible and ready
+ */
+let openLayoutView = Task.async(function*() {
+ let {toolbox, inspector} = yield openInspector();
+
+ if (!hasSideBarTab(inspector, "layoutview")) {
+ info("Waiting for the layoutview sidebar to be ready");
+ yield inspector.sidebar.once("layoutview-ready");
+ }
+
+ info("Selecting the layoutview sidebar");
+ inspector.sidebar.select("layoutview");
+
+ return {
+ toolbox: toolbox,
+ inspector: inspector,
+ view: inspector.sidebar.getWindowForTab("layoutview")["layoutview"]
+ };
+});
+
+/**
+ * Wait for the layoutview-updated event and for all of the inspector's panels
+ * to update too.
+ * Use this to make sure the inspector is updated and ready after a change was
+ * made in one of the layout-view editable fields.
+ * @return a promise
+ */
+function waitForUpdate(inspector) {
+ let onLayoutView = inspector.once("layoutview-updated");
+ let onInspector = inspector.once("inspector-updated");
+ return promise.all([onLayoutView, onInspector]);
+}
+
+/**
+ * The addTest/runTests function couple provides a simple way to define
+ * subsequent test cases in a test file. Example:
+ *
+ * addTest("what this test does", function*() {
+ * ... actual code for the test ...
+ * });
+ * addTest("what this second test does", function*() {
+ * ... actual code for the second test ...
+ * });
+ * runTests().then(...);
+ */
+var TESTS = [];
+
+function addTest(message, func) {
+ TESTS.push([message, Task.async(func)])
+}
+
+let runTests = Task.async(function*(...args) {
+ for (let [message, test] of TESTS) {
+ info("Running new test case: " + message);
+ yield test.apply(null, args);
+ }
+});
diff --git a/toolkit/devtools/layoutview/view.css b/toolkit/devtools/layoutview/view.css
new file mode 100644
index 000000000..f68ab5c22
--- /dev/null
+++ b/toolkit/devtools/layoutview/view.css
@@ -0,0 +1,263 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+body {
+ max-width: 320px;
+ position: relative;
+ margin: 0px auto;
+ padding: 0;
+}
+
+#header {
+ box-sizing: border-box;
+ width: 100%;
+ padding: 4px 13px;
+ display: -moz-box;
+ vertical-align: top;
+}
+
+#header:-moz-dir(rtl) {
+ -moz-box-direction: reverse;
+}
+
+#header > span {
+ display: -moz-box;
+}
+
+#element-size {
+ -moz-box-flex: 1;
+}
+
+#element-size:-moz-dir(rtl) {
+ -moz-box-pack: end;
+}
+
+#main {
+ margin: 0 14px 10px 14px;
+ box-sizing: border-box;
+ width: calc(100% - 2 * 14px);
+ position: absolute;
+ border-width: 1px;
+}
+
+#content,
+#borders {
+ border-width: 1px;
+}
+
+#content {
+ height: 25px;
+}
+
+#margins,
+#padding {
+ border-style: solid;
+ border-width: 25px;
+}
+
+#borders {
+ padding: 25px;
+}
+
+#main > p {
+ position: absolute;
+ pointer-events: none;
+}
+
+#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;
+}
+
+.padding.bottom {
+ bottom: 57px;
+}
+
+.border.top {
+ top: 30px;
+}
+
+.border.bottom {
+ bottom: 31px;
+}
+
+.margin.top {
+ top: 5px;
+}
+
+.margin.bottom {
+ bottom: 6px;
+}
+
+.size,
+.margin.left,
+.margin.right,
+.border.left,
+.border.right,
+.padding.left,
+.padding.right {
+ top: 22px;
+ line-height: 132px;
+}
+
+.size {
+ width: calc(100% - 2px);
+}
+
+.margin.right,
+.margin.left,
+.border.left,
+.border.right,
+.padding.right,
+.padding.left {
+ width: 25px;
+}
+
+.padding.left {
+ left: 52px;
+}
+
+.padding.right {
+ right: 51px;
+}
+
+.border.left {
+ left: 26px;
+}
+
+.border.right {
+ right: 26px;
+}
+
+.margin.right {
+ right: 0;
+}
+
+.margin.left {
+ left: 0;
+}
+
+.rotate.left:not(.editing) {
+ transform: rotate(-90deg);
+}
+
+.rotate.right:not(.editing) {
+ transform: rotate(90deg);
+}
+
+.tooltip {
+ position: absolute;
+ bottom: 0;
+ right: 2px;
+ pointer-events: none;
+}
+
+body.dim > #header > #element-position,
+body.dim > #main > p,
+body.dim > #main > .tooltip {
+ visibility: hidden;
+}
+
+@media (max-height: 228px) {
+ #header {
+ padding-top: 0;
+ padding-bottom: 0;
+ margin-top: 10px;
+ margin-bottom: 8px;
+ }
+
+ #margins,
+ #padding {
+ border-width: 21px;
+ }
+ #borders {
+ padding: 21px;
+ }
+
+ #content {
+ height: 21px;
+ }
+
+ .padding.top {
+ top: 46px;
+ }
+
+ .padding.bottom {
+ bottom: 46px;
+ }
+
+ .border.top {
+ top: 25px;
+ }
+
+ .border.bottom {
+ bottom: 25px;
+ }
+
+ .margin.top {
+ top: 4px;
+ }
+
+ .margin.bottom {
+ bottom: 4px;
+ }
+
+ .size,
+ .margin.left,
+ .margin.right,
+ .border.left,
+ .border.right,
+ .padding.left,
+ .padding.right {
+ line-height: 106px;
+ }
+
+ .margin.right,
+ .margin.left,
+ .border.left,
+ .border.right,
+ .padding.right,
+ .padding.left {
+ width: 21px;
+ }
+
+ .padding.left {
+ left: 43px;
+ }
+
+ .padding.right {
+ right: 43px;
+ }
+
+ .border.left {
+ left: 22px;
+ }
+
+ .border.right {
+ right: 22px;
+ }
+}
diff --git a/toolkit/devtools/layoutview/view.js b/toolkit/devtools/layoutview/view.js
new file mode 100644
index 000000000..0548e5edc
--- /dev/null
+++ b/toolkit/devtools/layoutview/view.js
@@ -0,0 +1,552 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"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");
+Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
+
+const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
+const {InplaceEditor, editableItem} = devtools.require("devtools/shared/inplace-editor");
+const {parseDeclarations} = devtools.require("devtools/styleinspector/css-parsing-utils");
+const {ReflowFront} = devtools.require("devtools/server/actors/layout");
+
+const SHARED_L10N = new ViewHelpers.L10N("chrome://browser/locale/devtools/shared.properties");
+const NUMERIC = /^-?[\d\.]+$/;
+const LONG_TEXT_ROTATE_LIMIT = 3;
+
+/**
+ * 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);
+ },
+
+ destroy: function() {
+ this._doc = null;
+ this._rules = null;
+ this._modifications.clear();
+ }
+};
+
+/**
+ * The layout-view panel
+ * @param {InspectorPanel} inspector An instance of the inspector-panel
+ * currently loaded in the toolbox
+ * @param {Window} win The window containing the panel
+ */
+function LayoutView(inspector, win) {
+ this.inspector = inspector;
+
+ this.doc = win.document;
+ this.sizeLabel = this.doc.querySelector(".size > span");
+ this.sizeHeadingLabel = this.doc.getElementById("element-size");
+
+ this.init();
+}
+
+LayoutView.prototype = {
+ init: function() {
+ this.update = this.update.bind(this);
+
+ this.onNewSelection = this.onNewSelection.bind(this);
+ this.inspector.selection.on("new-node-front", this.onNewSelection);
+
+ this.onNewNode = this.onNewNode.bind(this);
+ this.inspector.sidebar.on("layoutview-selected", this.onNewNode);
+
+ this.onSidebarSelect = this.onSidebarSelect.bind(this);
+ this.inspector.sidebar.on("select", this.onSidebarSelect);
+
+ // Store for the different dimensions of the node.
+ // 'selector' refers to the element that holds the value in view.xhtml;
+ // 'property' is what we are measuring;
+ // 'value' is the computed dimension, computed in update().
+ this.map = {
+ position: {selector: "#element-position",
+ property: "position",
+ value: undefined},
+ marginTop: {selector: ".margin.top > span",
+ property: "margin-top",
+ value: undefined},
+ marginBottom: {selector: ".margin.bottom > span",
+ property: "margin-bottom",
+ value: undefined},
+ marginLeft: {selector: ".margin.left > span",
+ property: "margin-left",
+ value: undefined},
+ marginRight: {selector: ".margin.right > span",
+ property: "margin-right",
+ value: undefined},
+ paddingTop: {selector: ".padding.top > span",
+ property: "padding-top",
+ value: undefined},
+ paddingBottom: {selector: ".padding.bottom > span",
+ property: "padding-bottom",
+ value: undefined},
+ paddingLeft: {selector: ".padding.left > span",
+ property: "padding-left",
+ value: undefined},
+ paddingRight: {selector: ".padding.right > span",
+ property: "padding-right",
+ 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();
+ },
+
+ /**
+ * Start listening to reflows in the current tab.
+ */
+ trackReflows: function() {
+ if (!this.reflowFront) {
+ let toolbox = this.inspector.toolbox;
+ if (toolbox.target.form.reflowActor) {
+ this.reflowFront = ReflowFront(toolbox.target.client, toolbox.target.form);
+ } else {
+ return;
+ }
+ }
+
+ this.reflowFront.on("reflows", this.update);
+ this.reflowFront.start();
+ },
+
+ /**
+ * Stop listening to reflows in the current tab.
+ */
+ untrackReflows: function() {
+ if (!this.reflowFront) {
+ return;
+ }
+
+ this.reflowFront.off("reflows", this.update);
+ this.reflowFront.stop();
+ },
+
+ /**
+ * Called when the user clicks on one of the editable values in the layoutview
+ */
+ initEditor: function(element, event, dimension) {
+ let { property } = dimension;
+ let session = new EditingSession(document, this.elementRules);
+ let initialValue = session.getProperty(property);
+
+ let editor = new InplaceEditor({
+ element: element,
+ initial: initialValue,
+
+ start: (editor) => {
+ editor.elt.parentNode.classList.add("editing");
+ },
+
+ 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) => {
+ editor.elt.parentNode.classList.remove("editing");
+ if (!commit) {
+ session.revert();
+ session.destroy();
+ }
+ }
+ }, event);
+ },
+
+ /**
+ * Is the layoutview visible in the sidebar?
+ */
+ isActive: function() {
+ return this.inspector &&
+ this.inspector.sidebar.getCurrentTabID() == "layoutview";
+ },
+
+ /**
+ * Destroy the nodes. Remove listeners.
+ */
+ destroy: function() {
+ this.inspector.sidebar.off("layoutview-selected", this.onNewNode);
+ this.inspector.selection.off("new-node-front", this.onNewSelection);
+ this.inspector.sidebar.off("select", this.onSidebarSelect);
+
+ this.sizeHeadingLabel = null;
+ this.sizeLabel = null;
+ this.inspector = null;
+ this.doc = null;
+
+ if (this.reflowFront) {
+ this.untrackReflows();
+ this.reflowFront.destroy();
+ this.reflowFront = null;
+ }
+ },
+
+ onSidebarSelect: function(e, sidebar) {
+ if (sidebar !== "layoutview") {
+ this.dim();
+ }
+ },
+
+ /**
+ * Selection 'new-node-front' event handler.
+ */
+ onNewSelection: function() {
+ let done = this.inspector.updating("layoutview");
+ this.onNewNode().then(done, (err) => { console.error(err); done() });
+ },
+
+ /**
+ * @return a promise that resolves when the view has been updated
+ */
+ onNewNode: function() {
+ if (this.isActive() &&
+ this.inspector.selection.isConnected() &&
+ this.inspector.selection.isElementNode()) {
+ this.undim();
+ } else {
+ this.dim();
+ }
+
+ return this.update();
+ },
+
+ /**
+ * Hide the layout boxes and stop refreshing on reflows. No node is selected
+ * or the layout-view sidebar is inactive.
+ */
+ dim: function() {
+ this.untrackReflows();
+ this.doc.body.classList.add("dim");
+ this.dimmed = true;
+ },
+
+ /**
+ * Show the layout boxes and start refreshing on reflows. A node is selected
+ * and the layout-view side is active.
+ */
+ undim: function() {
+ this.trackReflows();
+ 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.
+ * @return a promise that will be resolved when complete.
+ */
+ update: function() {
+ let lastRequest = Task.spawn((function*() {
+ if (!this.isActive() ||
+ !this.inspector.selection.isConnected() ||
+ !this.inspector.selection.isElementNode()) {
+ return;
+ }
+
+ 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 = SHARED_L10N.getFormatStr("dimensions", width, height);
+
+ if (this.sizeHeadingLabel.textContent != newLabel) {
+ this.sizeHeadingLabel.textContent = newLabel;
+ }
+
+ // If the view is dimmed, no need to do anything more.
+ if (this.dimmed) {
+ this.inspector.emit("layoutview-updated");
+ return null;
+ }
+
+ for (let i in this.map) {
+ let property = this.map[i].property;
+ if (!(property in layout)) {
+ // Depending on the actor version, some properties
+ // might be missing.
+ continue;
+ }
+ let parsedValue = parseInt(layout[property]);
+ if (Number.isNaN(parsedValue)) {
+ // Not a number. We use the raw string.
+ // Useful for "position" for example.
+ this.map[i].value = layout[property];
+ } else {
+ this.map[i].value = parsedValue;
+ }
+ }
+
+ let margins = layout.autoMargins;
+ if ("top" in margins) this.map.marginTop.value = "auto";
+ if ("right" in margins) this.map.marginRight.value = "auto";
+ if ("bottom" in margins) this.map.marginBottom.value = "auto";
+ if ("left" in margins) this.map.marginLeft.value = "auto";
+
+ for (let i in this.map) {
+ let selector = this.map[i].selector;
+ let span = this.doc.querySelector(selector);
+ if (span.textContent.length > 0 &&
+ span.textContent == this.map[i].value) {
+ continue;
+ }
+ span.textContent = this.map[i].value;
+ this.manageOverflowingText(span);
+ }
+
+ width -= this.map.borderLeft.value + this.map.borderRight.value +
+ this.map.paddingLeft.value + this.map.paddingRight.value;
+
+ height -= this.map.borderTop.value + this.map.borderBottom.value +
+ this.map.paddingTop.value + this.map.paddingBottom.value;
+
+ let newValue = width + "\u00D7" + height;
+ if (this.sizeLabel.textContent != newValue) {
+ this.sizeLabel.textContent = newValue;
+ }
+
+ this.elementRules = [e.rule for (e of styleEntries)];
+
+ this.inspector.emit("layoutview-updated");
+ }).bind(this)).then(null, console.error);
+
+ return this._lastRequest = lastRequest;
+ },
+
+ /**
+ * Show the box-model highlighter on the currently selected element
+ * @param {Object} options Options passed to the highlighter actor
+ */
+ showBoxModel: function(options={}) {
+ let toolbox = this.inspector.toolbox;
+ let nodeFront = this.inspector.selection.nodeFront;
+
+ toolbox.highlighterUtils.highlightNodeFront(nodeFront, options);
+ },
+
+ /**
+ * Hide the box-model highlighter on the currently selected element
+ */
+ hideBoxModel: function() {
+ let toolbox = this.inspector.toolbox;
+
+ toolbox.highlighterUtils.unhighlight();
+ },
+
+ manageOverflowingText: function(span) {
+ let classList = span.parentNode.classList;
+
+ if (classList.contains("left") || classList.contains("right")) {
+ let force = span.textContent.length > LONG_TEXT_ROTATE_LIMIT;
+ classList.toggle("rotate", force);
+ }
+ }
+};
+
+let elts;
+let tooltip;
+
+let onmouseover = function(e) {
+ let region = e.target.getAttribute("data-box");
+
+ tooltip.textContent = e.target.getAttribute("tooltip");
+ this.layoutview.showBoxModel({region});
+
+ return false;
+}.bind(window);
+
+let onmouseout = function(e) {
+ tooltip.textContent = "";
+ this.layoutview.hideBoxModel();
+
+ return false;
+}.bind(window);
+
+window.setPanel = function(panel) {
+ this.layoutview = new LayoutView(panel, window);
+
+ // Tooltip mechanism
+ elts = document.querySelectorAll("*[tooltip]");
+ tooltip = document.querySelector(".tooltip");
+ for (let i = 0; i < elts.length; i++) {
+ let elt = elts[i];
+ elt.addEventListener("mouseover", onmouseover, true);
+ elt.addEventListener("mouseout", onmouseout, true);
+ }
+
+ // Mark document as RTL or LTR:
+ let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].
+ getService(Ci.nsIXULChromeRegistry);
+ let dir = chromeReg.isLocaleRTL("global");
+ document.body.setAttribute("dir", dir ? "rtl" : "ltr");
+
+ window.parent.postMessage("layoutview-ready", "*");
+};
+
+window.onunload = function() {
+ if (this.layoutview) {
+ this.layoutview.destroy();
+ }
+ if (elts) {
+ for (let i = 0; i < elts.length; i++) {
+ let elt = elts[i];
+ elt.removeEventListener("mouseover", onmouseover, true);
+ elt.removeEventListener("mouseout", onmouseout, true);
+ }
+ }
+};
diff --git a/toolkit/devtools/layoutview/view.xhtml b/toolkit/devtools/layoutview/view.xhtml
new file mode 100644
index 000000000..261d1a42a
--- /dev/null
+++ b/toolkit/devtools/layoutview/view.xhtml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE html [
+<!ENTITY % layoutviewDTD SYSTEM "chrome://browser/locale/devtools/layoutview.dtd" >
+ %layoutviewDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <head>
+ <title>&title;</title>
+
+ <script type="application/javascript;version=1.8"
+ src="chrome://browser/content/devtools/theme-switching.js"/>
+
+ <script type="application/javascript;version=1.8" src="view.js"></script>
+
+ <link rel="stylesheet" href="chrome://browser/skin/devtools/common.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://browser/skin/devtools/layoutview.css" type="text/css"/>
+ <link rel="stylesheet" href="view.css" type="text/css"/>
+
+ </head>
+ <body class="theme-sidebar devtools-monospace">
+
+ <p id="header">
+ <span id="element-size"></span><span id="element-position"></span>
+ </p>
+
+ <div id="main">
+
+ <div id="margins" data-box="margin" tooltip="&margin.tooltip;">
+ <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" 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" 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" 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>