diff options
Diffstat (limited to 'toolkit/devtools/layoutview')
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> |