summaryrefslogtreecommitdiffstats
path: root/devtools/client/inspector/components
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/inspector/components')
-rw-r--r--devtools/client/inspector/components/box-model.js841
-rw-r--r--devtools/client/inspector/components/inspector-tab-panel.css15
-rw-r--r--devtools/client/inspector/components/inspector-tab-panel.js67
-rw-r--r--devtools/client/inspector/components/moz.build13
-rw-r--r--devtools/client/inspector/components/test/.eslintrc.js6
-rw-r--r--devtools/client/inspector/components/test/browser.ini29
-rw-r--r--devtools/client/inspector/components/test/browser_boxmodel.js168
-rw-r--r--devtools/client/inspector/components/test/browser_boxmodel_editablemodel.js194
-rw-r--r--devtools/client/inspector/components/test/browser_boxmodel_editablemodel_allproperties.js146
-rw-r--r--devtools/client/inspector/components/test/browser_boxmodel_editablemodel_bluronclick.js74
-rw-r--r--devtools/client/inspector/components/test/browser_boxmodel_editablemodel_border.js52
-rw-r--r--devtools/client/inspector/components/test/browser_boxmodel_editablemodel_stylerules.js113
-rw-r--r--devtools/client/inspector/components/test/browser_boxmodel_guides.js56
-rw-r--r--devtools/client/inspector/components/test/browser_boxmodel_rotate-labels-on-sides.js49
-rw-r--r--devtools/client/inspector/components/test/browser_boxmodel_sync.js44
-rw-r--r--devtools/client/inspector/components/test/browser_boxmodel_tooltips.js126
-rw-r--r--devtools/client/inspector/components/test/browser_boxmodel_update-after-navigation.js91
-rw-r--r--devtools/client/inspector/components/test/browser_boxmodel_update-after-reload.js40
-rw-r--r--devtools/client/inspector/components/test/browser_boxmodel_update-in-iframes.js101
-rw-r--r--devtools/client/inspector/components/test/doc_boxmodel_iframe1.html3
-rw-r--r--devtools/client/inspector/components/test/doc_boxmodel_iframe2.html3
-rw-r--r--devtools/client/inspector/components/test/head.js87
22 files changed, 2318 insertions, 0 deletions
diff --git a/devtools/client/inspector/components/box-model.js b/devtools/client/inspector/components/box-model.js
new file mode 100644
index 000000000..fc36fac71
--- /dev/null
+++ b/devtools/client/inspector/components/box-model.js
@@ -0,0 +1,841 @@
+/* -*- 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 {Task} = require("devtools/shared/task");
+const {InplaceEditor, editableItem} =
+ require("devtools/client/shared/inplace-editor");
+const {ReflowFront} = require("devtools/shared/fronts/reflow");
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const {getCssProperties} = require("devtools/shared/fronts/css-properties");
+
+const STRINGS_URI = "devtools/client/locales/shared.properties";
+const STRINGS_INSPECTOR = "devtools/shared/locales/styleinspector.properties";
+const SHARED_L10N = new LocalizationHelper(STRINGS_URI);
+const INSPECTOR_L10N = new LocalizationHelper(STRINGS_INSPECTOR);
+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. The main parameter is the BoxModelView that created it.
+ *
+ * @param inspector The inspector panel.
+ * @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({inspector, doc, elementRules}) {
+ this._doc = doc;
+ this._rules = elementRules;
+ this._modifications = new Map();
+ this._cssProperties = getCssProperties(inspector.toolbox);
+}
+
+EditingSession.prototype = {
+ /**
+ * Gets the value of a single property from the CSS rule.
+ *
+ * @param {StyleRuleFront} rule The CSS rule.
+ * @param {String} property The name of the property.
+ * @return {String} The value.
+ */
+ getPropertyFromRule: function (rule, property) {
+ // Use the parsed declarations in the StyleRuleFront object if available.
+ let index = this.getPropertyIndex(property, rule);
+ if (index !== -1) {
+ return rule.declarations[index].value;
+ }
+
+ // Fallback to parsing the cssText locally otherwise.
+ 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.getElementById("sidebar-panel-computedview").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 "";
+ },
+
+ /**
+ * Get the index of a given css property name in a CSS rule.
+ * Or -1, if there are no properties in the rule yet.
+ * @param {String} name The property name.
+ * @param {StyleRuleFront} rule Optional, defaults to the element style rule.
+ * @return {Number} The property index in the rule.
+ */
+ getPropertyIndex: function (name, rule = this._rules[0]) {
+ let elementStyleRule = this._rules[0];
+ if (!elementStyleRule.declarations.length) {
+ return -1;
+ }
+
+ return elementStyleRule.declarations.findIndex(p => p.name === name);
+ },
+
+ /**
+ * Sets a number of properties on the node.
+ * @param properties An array of properties, each is an object with name and
+ * value properties. If the value is "" then the property
+ * is removed.
+ * @return {Promise} Resolves when the modifications are complete.
+ */
+ setProperties: Task.async(function* (properties) {
+ for (let property of properties) {
+ // Get a RuleModificationList or RuleRewriter helper object from the
+ // StyleRuleActor to make changes to CSS properties.
+ // Note that RuleRewriter doesn't support modifying several properties at
+ // once, so we do this in a sequence here.
+ let modifications = this._rules[0].startModifyingProperties(
+ this._cssProperties);
+
+ // Remember the property so it can be reverted.
+ if (!this._modifications.has(property.name)) {
+ this._modifications.set(property.name,
+ this.getPropertyFromRule(this._rules[0], property.name));
+ }
+
+ // Find the index of the property to be changed, or get the next index to
+ // insert the new property at.
+ let index = this.getPropertyIndex(property.name);
+ if (index === -1) {
+ index = this._rules[0].declarations.length;
+ }
+
+ if (property.value == "") {
+ modifications.removeProperty(index, property.name);
+ } else {
+ modifications.setProperty(index, property.name, property.value, "");
+ }
+
+ yield modifications.apply();
+ }
+ }),
+
+ /**
+ * Reverts all of the property changes made by this instance.
+ * @return {Promise} Resolves when all properties have been reverted.
+ */
+ revert: Task.async(function* () {
+ // Revert each property that we modified previously, one by one. See
+ // setProperties for information about why.
+ for (let [property, value] of this._modifications) {
+ let modifications = this._rules[0].startModifyingProperties(
+ this._cssProperties);
+
+ // Find the index of the property to be reverted.
+ let index = this.getPropertyIndex(property);
+
+ if (value != "") {
+ // If the property doesn't exist anymore, insert at the beginning of the
+ // rule.
+ if (index === -1) {
+ index = 0;
+ }
+ modifications.setProperty(index, property, value, "");
+ } else {
+ // If the property doesn't exist anymore, no need to remove it. It had
+ // not been added after all.
+ if (index === -1) {
+ continue;
+ }
+ modifications.removeProperty(index, property);
+ }
+
+ yield modifications.apply();
+ }
+ }),
+
+ destroy: function () {
+ this._doc = null;
+ this._rules = null;
+ this._modifications.clear();
+ }
+};
+
+/**
+ * The box model view
+ * @param {InspectorPanel} inspector
+ * An instance of the inspector-panel currently loaded in the toolbox
+ * @param {Document} document
+ * The document that will contain the box model view.
+ */
+function BoxModelView(inspector, document) {
+ this.inspector = inspector;
+ this.doc = document;
+ this.wrapper = this.doc.getElementById("boxmodel-wrapper");
+ this.container = this.doc.getElementById("boxmodel-container");
+ this.expander = this.doc.getElementById("boxmodel-expander");
+ this.sizeLabel = this.doc.querySelector(".boxmodel-size > span");
+ this.sizeHeadingLabel = this.doc.getElementById("boxmodel-element-size");
+ this._geometryEditorHighlighter = null;
+ this._cssProperties = getCssProperties(inspector.toolbox);
+
+ this.init();
+}
+
+BoxModelView.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("computedview-selected", this.onNewNode);
+
+ this.onSidebarSelect = this.onSidebarSelect.bind(this);
+ this.inspector.sidebar.on("select", this.onSidebarSelect);
+
+ this.onToggleExpander = this.onToggleExpander.bind(this);
+ this.expander.addEventListener("click", this.onToggleExpander);
+ let header = this.doc.getElementById("boxmodel-header");
+ header.addEventListener("dblclick", this.onToggleExpander);
+
+ this.onFilterComputedView = this.onFilterComputedView.bind(this);
+ this.inspector.on("computed-view-filtered",
+ this.onFilterComputedView);
+
+ this.onPickerStarted = this.onPickerStarted.bind(this);
+ this.onMarkupViewLeave = this.onMarkupViewLeave.bind(this);
+ this.onMarkupViewNodeHover = this.onMarkupViewNodeHover.bind(this);
+ this.onWillNavigate = this.onWillNavigate.bind(this);
+
+ this.initBoxModelHighlighter();
+
+ // Store for the different dimensions of the node.
+ // 'selector' refers to the element that holds the value;
+ // 'property' is what we are measuring;
+ // 'value' is the computed dimension, computed in update().
+ this.map = {
+ position: {
+ selector: "#boxmodel-element-position",
+ property: "position",
+ value: undefined
+ },
+ marginTop: {
+ selector: ".boxmodel-margin.boxmodel-top > span",
+ property: "margin-top",
+ value: undefined
+ },
+ marginBottom: {
+ selector: ".boxmodel-margin.boxmodel-bottom > span",
+ property: "margin-bottom",
+ value: undefined
+ },
+ marginLeft: {
+ selector: ".boxmodel-margin.boxmodel-left > span",
+ property: "margin-left",
+ value: undefined
+ },
+ marginRight: {
+ selector: ".boxmodel-margin.boxmodel-right > span",
+ property: "margin-right",
+ value: undefined
+ },
+ paddingTop: {
+ selector: ".boxmodel-padding.boxmodel-top > span",
+ property: "padding-top",
+ value: undefined
+ },
+ paddingBottom: {
+ selector: ".boxmodel-padding.boxmodel-bottom > span",
+ property: "padding-bottom",
+ value: undefined
+ },
+ paddingLeft: {
+ selector: ".boxmodel-padding.boxmodel-left > span",
+ property: "padding-left",
+ value: undefined
+ },
+ paddingRight: {
+ selector: ".boxmodel-padding.boxmodel-right > span",
+ property: "padding-right",
+ value: undefined
+ },
+ borderTop: {
+ selector: ".boxmodel-border.boxmodel-top > span",
+ property: "border-top-width",
+ value: undefined
+ },
+ borderBottom: {
+ selector: ".boxmodel-border.boxmodel-bottom > span",
+ property: "border-bottom-width",
+ value: undefined
+ },
+ borderLeft: {
+ selector: ".boxmodel-border.boxmodel-left > span",
+ property: "border-left-width",
+ value: undefined
+ },
+ borderRight: {
+ selector: ".boxmodel-border.boxmodel-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();
+
+ let nodeGeometry = this.doc.getElementById("layout-geometry-editor");
+ this.onGeometryButtonClick = this.onGeometryButtonClick.bind(this);
+ nodeGeometry.addEventListener("click", this.onGeometryButtonClick);
+ },
+
+ initBoxModelHighlighter: function () {
+ let highlightElts = this.doc.querySelectorAll("#boxmodel-container *[title]");
+ this.onHighlightMouseOver = this.onHighlightMouseOver.bind(this);
+ this.onHighlightMouseOut = this.onHighlightMouseOut.bind(this);
+
+ for (let element of highlightElts) {
+ element.addEventListener("mouseover", this.onHighlightMouseOver, true);
+ element.addEventListener("mouseout", this.onHighlightMouseOut, true);
+ }
+ },
+
+ /**
+ * Start listening to reflows in the current tab.
+ */
+ trackReflows: function () {
+ if (!this.reflowFront) {
+ let { target } = this.inspector;
+ if (target.form.reflowActor) {
+ this.reflowFront = ReflowFront(target.client,
+ 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 box model view
+ */
+ initEditor: function (element, event, dimension) {
+ let { property } = dimension;
+ let session = new EditingSession(this);
+ let initialValue = session.getProperty(property);
+
+ let editor = new InplaceEditor({
+ element: element,
+ initial: initialValue,
+ contentType: InplaceEditor.CONTENT_TYPES.CSS_VALUE,
+ property: {
+ name: dimension.property
+ },
+ start: self => {
+ self.elt.parentNode.classList.add("boxmodel-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).catch(e => console.error(e));
+ },
+ done: (value, commit) => {
+ editor.elt.parentNode.classList.remove("boxmodel-editing");
+ if (!commit) {
+ session.revert().then(() => {
+ session.destroy();
+ }, e => console.error(e));
+ }
+ },
+ contextMenu: this.inspector.onTextBoxContextMenu,
+ cssProperties: this._cssProperties
+ }, event);
+ },
+
+ /**
+ * Is the BoxModelView visible in the sidebar.
+ * @return {Boolean}
+ */
+ isViewVisible: function () {
+ return this.inspector &&
+ this.inspector.sidebar.getCurrentTabID() == "computedview";
+ },
+
+ /**
+ * Is the BoxModelView visible in the sidebar and is the current node valid to
+ * be displayed in the view.
+ * @return {Boolean}
+ */
+ isViewVisibleAndNodeValid: function () {
+ return this.isViewVisible() &&
+ this.inspector.selection.isConnected() &&
+ this.inspector.selection.isElementNode();
+ },
+
+ /**
+ * Destroy the nodes. Remove listeners.
+ */
+ destroy: function () {
+ let highlightElts = this.doc.querySelectorAll("#boxmodel-container *[title]");
+
+ for (let element of highlightElts) {
+ element.removeEventListener("mouseover", this.onHighlightMouseOver, true);
+ element.removeEventListener("mouseout", this.onHighlightMouseOut, true);
+ }
+
+ this.expander.removeEventListener("click", this.onToggleExpander);
+ let header = this.doc.getElementById("boxmodel-header");
+ header.removeEventListener("dblclick", this.onToggleExpander);
+
+ let nodeGeometry = this.doc.getElementById("layout-geometry-editor");
+ nodeGeometry.removeEventListener("click", this.onGeometryButtonClick);
+
+ this.inspector.off("picker-started", this.onPickerStarted);
+
+ // Inspector Panel will destroy `markup` object on "will-navigate" event,
+ // therefore we have to check if it's still available in case BoxModelView
+ // is destroyed immediately after.
+ if (this.inspector.markup) {
+ this.inspector.markup.off("leave", this.onMarkupViewLeave);
+ this.inspector.markup.off("node-hover", this.onMarkupViewNodeHover);
+ }
+
+ this.inspector.sidebar.off("computedview-selected", this.onNewNode);
+ this.inspector.selection.off("new-node-front", this.onNewSelection);
+ this.inspector.sidebar.off("select", this.onSidebarSelect);
+ this.inspector.target.off("will-navigate", this.onWillNavigate);
+ this.inspector.off("computed-view-filtered", this.onFilterComputedView);
+
+ this.inspector = null;
+ this.doc = null;
+ this.wrapper = null;
+ this.container = null;
+ this.expander = null;
+ this.sizeLabel = null;
+ this.sizeHeadingLabel = null;
+
+ if (this.reflowFront) {
+ this.untrackReflows();
+ this.reflowFront.destroy();
+ this.reflowFront = null;
+ }
+ },
+
+ onSidebarSelect: function (e, sidebar) {
+ this.setActive(sidebar === "computedview");
+ },
+
+ /**
+ * Selection 'new-node-front' event handler.
+ */
+ onNewSelection: function () {
+ let done = this.inspector.updating("computed-view");
+ this.onNewNode()
+ .then(() => this.hideGeometryEditor())
+ .then(done, (err) => {
+ console.error(err);
+ done();
+ }).catch(console.error);
+ },
+
+ /**
+ * @return a promise that resolves when the view has been updated
+ */
+ onNewNode: function () {
+ this.setActive(this.isViewVisibleAndNodeValid());
+ return this.update();
+ },
+
+ onHighlightMouseOver: function (e) {
+ let region = e.target.getAttribute("data-box");
+ if (!region) {
+ return;
+ }
+
+ this.showBoxModel({
+ region,
+ showOnly: region,
+ onlyRegionArea: true
+ });
+ },
+
+ onHighlightMouseOut: function () {
+ this.hideBoxModel();
+ },
+
+ onGeometryButtonClick: function ({target}) {
+ if (target.hasAttribute("checked")) {
+ target.removeAttribute("checked");
+ this.hideGeometryEditor();
+ } else {
+ target.setAttribute("checked", "true");
+ this.showGeometryEditor();
+ }
+ },
+
+ onPickerStarted: function () {
+ this.hideGeometryEditor();
+ },
+
+ onToggleExpander: function () {
+ let isOpen = this.expander.hasAttribute("open");
+
+ if (isOpen) {
+ this.container.hidden = true;
+ this.expander.removeAttribute("open");
+ } else {
+ this.container.hidden = false;
+ this.expander.setAttribute("open", "");
+ }
+ },
+
+ onMarkupViewLeave: function () {
+ this.showGeometryEditor(true);
+ },
+
+ onMarkupViewNodeHover: function () {
+ this.hideGeometryEditor(false);
+ },
+
+ onWillNavigate: function () {
+ this._geometryEditorHighlighter.release().catch(console.error);
+ this._geometryEditorHighlighter = null;
+ },
+
+ /**
+ * Event handler that responds to the computed view being filtered
+ * @param {String} reason
+ * @param {Boolean} hidden
+ * Whether or not to hide the box model wrapper
+ */
+ onFilterComputedView: function (reason, hidden) {
+ this.wrapper.hidden = hidden;
+ },
+
+ /**
+ * Stop tracking reflows and hide all values when no node is selected or the
+ * box model view is hidden, otherwise track reflows and show values.
+ * @param {Boolean} isActive
+ */
+ setActive: function (isActive) {
+ if (isActive === this.isActive) {
+ return;
+ }
+ this.isActive = isActive;
+
+ if (isActive) {
+ this.trackReflows();
+ } else {
+ this.untrackReflows();
+ }
+ },
+
+ /**
+ * Compute the dimensions of the node and update the values in
+ * the inspector.xul document.
+ * @return a promise that will be resolved when complete.
+ */
+ update: function () {
+ let lastRequest = Task.spawn((function* () {
+ if (!this.isViewVisibleAndNodeValid()) {
+ this.wrapper.hidden = true;
+ this.inspector.emit("boxmodel-view-updated");
+ return null;
+ }
+
+ let node = this.inspector.selection.nodeFront;
+ let layout = yield this.inspector.pageStyle.getLayout(node, {
+ autoMargins: this.isActive
+ });
+ let styleEntries = yield this.inspector.pageStyle.getApplied(node, {});
+
+ yield this.updateGeometryButton();
+
+ // 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;
+ }
+
+ 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 = parseFloat(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);
+ this.updateSourceRuleTooltip(span, this.map[i].property, styleEntries);
+ 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;
+ width = parseFloat(width.toPrecision(6));
+ height -= this.map.borderTop.value + this.map.borderBottom.value +
+ this.map.paddingTop.value + this.map.paddingBottom.value;
+ height = parseFloat(height.toPrecision(6));
+
+ let newValue = width + "\u00D7" + height;
+ if (this.sizeLabel.textContent != newValue) {
+ this.sizeLabel.textContent = newValue;
+ }
+
+ this.elementRules = styleEntries.map(e => e.rule);
+
+ this.wrapper.hidden = false;
+
+ this.inspector.emit("boxmodel-view-updated");
+ return null;
+ }).bind(this)).catch(console.error);
+
+ this._lastRequest = lastRequest;
+ return this._lastRequest;
+ },
+
+ /**
+ * Update the text in the tooltip shown when hovering over a value to provide
+ * information about the source CSS rule that sets this value.
+ * @param {DOMNode} el The element that will receive the tooltip.
+ * @param {String} property The name of the CSS property for the tooltip.
+ * @param {Array} rules An array of applied rules retrieved by
+ * styleActor.getApplied.
+ */
+ updateSourceRuleTooltip: function (el, property, rules) {
+ // Dummy element used to parse the cssText of applied rules.
+ let dummyEl = this.doc.createElement("div");
+
+ // Rules are in order of priority so iterate until we find the first that
+ // defines a value for the property.
+ let sourceRule, value;
+ for (let {rule} of rules) {
+ dummyEl.style.cssText = rule.cssText;
+ value = dummyEl.style.getPropertyValue(property);
+ if (value !== "") {
+ sourceRule = rule;
+ break;
+ }
+ }
+
+ let title = property;
+ if (sourceRule && sourceRule.selectors) {
+ title += "\n" + sourceRule.selectors.join(", ");
+ }
+ if (sourceRule && sourceRule.parentStyleSheet) {
+ if (sourceRule.parentStyleSheet.href) {
+ title += "\n" + sourceRule.parentStyleSheet.href + ":" + sourceRule.line;
+ } else {
+ title += "\n" + INSPECTOR_L10N.getStr("rule.sourceInline") +
+ ":" + sourceRule.line;
+ }
+ }
+
+ el.setAttribute("title", title);
+ },
+
+ /**
+ * 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();
+ },
+
+ /**
+ * Show the geometry editor highlighter on the currently selected element
+ * @param {Boolean} [showOnlyIfActive=false]
+ * Indicates if the Geometry Editor should be shown only if it's active but
+ * hidden.
+ */
+ showGeometryEditor: function (showOnlyIfActive = false) {
+ let toolbox = this.inspector.toolbox;
+ let nodeFront = this.inspector.selection.nodeFront;
+ let nodeGeometry = this.doc.getElementById("layout-geometry-editor");
+ let isActive = nodeGeometry.hasAttribute("checked");
+
+ if (showOnlyIfActive && !isActive) {
+ return;
+ }
+
+ if (this._geometryEditorHighlighter) {
+ this._geometryEditorHighlighter.show(nodeFront).catch(console.error);
+ return;
+ }
+
+ // instantiate Geometry Editor highlighter
+ toolbox.highlighterUtils
+ .getHighlighterByType("GeometryEditorHighlighter").then(highlighter => {
+ highlighter.show(nodeFront).catch(console.error);
+ this._geometryEditorHighlighter = highlighter;
+
+ // Hide completely the geometry editor if the picker is clicked
+ toolbox.on("picker-started", this.onPickerStarted);
+
+ // Temporary hide the geometry editor
+ this.inspector.markup.on("leave", this.onMarkupViewLeave);
+ this.inspector.markup.on("node-hover", this.onMarkupViewNodeHover);
+
+ // Release the actor on will-navigate event
+ this.inspector.target.once("will-navigate", this.onWillNavigate);
+ });
+ },
+
+ /**
+ * Hide the geometry editor highlighter on the currently selected element
+ * @param {Boolean} [updateButton=true]
+ * Indicates if the Geometry Editor's button needs to be unchecked too
+ */
+ hideGeometryEditor: function (updateButton = true) {
+ if (this._geometryEditorHighlighter) {
+ this._geometryEditorHighlighter.hide().catch(console.error);
+ }
+
+ if (updateButton) {
+ let nodeGeometry = this.doc.getElementById("layout-geometry-editor");
+ nodeGeometry.removeAttribute("checked");
+ }
+ },
+
+ /**
+ * Update the visibility and the state of the geometry editor button,
+ * based on the selected node.
+ */
+ updateGeometryButton: Task.async(function* () {
+ let node = this.inspector.selection.nodeFront;
+ let isEditable = false;
+
+ if (node) {
+ isEditable = yield this.inspector.pageStyle.isPositionEditable(node);
+ }
+
+ let nodeGeometry = this.doc.getElementById("layout-geometry-editor");
+ nodeGeometry.style.visibility = isEditable ? "visible" : "hidden";
+ }),
+
+ manageOverflowingText: function (span) {
+ let classList = span.parentNode.classList;
+
+ if (classList.contains("boxmodel-left") ||
+ classList.contains("boxmodel-right")) {
+ let force = span.textContent.length > LONG_TEXT_ROTATE_LIMIT;
+ classList.toggle("boxmodel-rotate", force);
+ }
+ }
+};
+
+exports.BoxModelView = BoxModelView;
diff --git a/devtools/client/inspector/components/inspector-tab-panel.css b/devtools/client/inspector/components/inspector-tab-panel.css
new file mode 100644
index 000000000..e85e5daed
--- /dev/null
+++ b/devtools/client/inspector/components/inspector-tab-panel.css
@@ -0,0 +1,15 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.devtools-inspector-tab-frame {
+ border: none;
+ height: 100%;
+ width: 100%;
+}
+
+.devtools-inspector-tab-panel {
+ width: 100%;
+ height: 100%;
+}
diff --git a/devtools/client/inspector/components/inspector-tab-panel.js b/devtools/client/inspector/components/inspector-tab-panel.js
new file mode 100644
index 000000000..68db7781e
--- /dev/null
+++ b/devtools/client/inspector/components/inspector-tab-panel.js
@@ -0,0 +1,67 @@
+/* -*- 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 { DOM, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
+
+// Shortcuts
+const { div } = DOM;
+
+/**
+ * Helper panel component that is using an existing DOM node
+ * as the content. It's used by Sidebar as well as SplitBox
+ * components.
+ */
+var InspectorTabPanel = createClass({
+ displayName: "InspectorTabPanel",
+
+ propTypes: {
+ // ID of the node that should be rendered as the content.
+ id: PropTypes.string.isRequired,
+ // Optional prefix for panel IDs.
+ idPrefix: PropTypes.string,
+ // Optional mount callback
+ onMount: PropTypes.func,
+ },
+
+ getDefaultProps: function () {
+ return {
+ idPrefix: "",
+ };
+ },
+
+ componentDidMount: function () {
+ let doc = this.refs.content.ownerDocument;
+ let panel = doc.getElementById(this.props.idPrefix + this.props.id);
+
+ // Append existing DOM node into panel's content.
+ this.refs.content.appendChild(panel);
+
+ if (this.props.onMount) {
+ this.props.onMount(this.refs.content, this.props);
+ }
+ },
+
+ componentWillUnmount: function () {
+ let doc = this.refs.content.ownerDocument;
+ let panels = doc.getElementById("tabpanels");
+
+ // Move panel's content node back into list of tab panels.
+ panels.appendChild(this.refs.content.firstChild);
+ },
+
+ render: function () {
+ return (
+ div({
+ ref: "content",
+ className: "devtools-inspector-tab-panel",
+ })
+ );
+ }
+});
+
+module.exports = InspectorTabPanel;
diff --git a/devtools/client/inspector/components/moz.build b/devtools/client/inspector/components/moz.build
new file mode 100644
index 000000000..5e4dd40ed
--- /dev/null
+++ b/devtools/client/inspector/components/moz.build
@@ -0,0 +1,13 @@
+# -*- Mode: python; 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/.
+
+DevToolsModules(
+ 'box-model.js',
+ 'inspector-tab-panel.css',
+ 'inspector-tab-panel.js',
+)
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
diff --git a/devtools/client/inspector/components/test/.eslintrc.js b/devtools/client/inspector/components/test/.eslintrc.js
new file mode 100644
index 000000000..698ae9181
--- /dev/null
+++ b/devtools/client/inspector/components/test/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../../.eslintrc.mochitests.js"
+};
diff --git a/devtools/client/inspector/components/test/browser.ini b/devtools/client/inspector/components/test/browser.ini
new file mode 100644
index 000000000..42eb352d6
--- /dev/null
+++ b/devtools/client/inspector/components/test/browser.ini
@@ -0,0 +1,29 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ doc_boxmodel_iframe1.html
+ doc_boxmodel_iframe2.html
+ head.js
+ !/devtools/client/commandline/test/helpers.js
+ !/devtools/client/framework/test/shared-head.js
+ !/devtools/client/inspector/test/head.js
+ !/devtools/client/inspector/test/shared-head.js
+ !/devtools/client/shared/test/test-actor.js
+ !/devtools/client/shared/test/test-actor-registry.js
+
+[browser_boxmodel.js]
+[browser_boxmodel_editablemodel.js]
+# [browser_boxmodel_editablemodel_allproperties.js]
+# Disabled for too many intermittent failures (bug 1009322)
+[browser_boxmodel_editablemodel_bluronclick.js]
+[browser_boxmodel_editablemodel_border.js]
+[browser_boxmodel_editablemodel_stylerules.js]
+[browser_boxmodel_guides.js]
+[browser_boxmodel_rotate-labels-on-sides.js]
+[browser_boxmodel_sync.js]
+[browser_boxmodel_tooltips.js]
+[browser_boxmodel_update-after-navigation.js]
+[browser_boxmodel_update-after-reload.js]
+# [browser_boxmodel_update-in-iframes.js]
+# Bug 1020038 boxmodel-view updates for iframe elements changes
diff --git a/devtools/client/inspector/components/test/browser_boxmodel.js b/devtools/client/inspector/components/test/browser_boxmodel.js
new file mode 100644
index 000000000..f8b87f421
--- /dev/null
+++ b/devtools/client/inspector/components/test/browser_boxmodel.js
@@ -0,0 +1,168 @@
+/* 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 box model displays the right values and that it updates when
+// the node's style is changed
+
+// Expected values:
+var res1 = [
+ {
+ selector: "#boxmodel-element-size",
+ value: "160" + "\u00D7" + "160.117"
+ },
+ {
+ selector: ".boxmodel-size > span",
+ value: "100" + "\u00D7" + "100.117"
+ },
+ {
+ selector: ".boxmodel-margin.boxmodel-top > span",
+ value: 30
+ },
+ {
+ selector: ".boxmodel-margin.boxmodel-left > span",
+ value: "auto"
+ },
+ {
+ selector: ".boxmodel-margin.boxmodel-bottom > span",
+ value: 30
+ },
+ {
+ selector: ".boxmodel-margin.boxmodel-right > span",
+ value: "auto"
+ },
+ {
+ selector: ".boxmodel-padding.boxmodel-top > span",
+ value: 20
+ },
+ {
+ selector: ".boxmodel-padding.boxmodel-left > span",
+ value: 20
+ },
+ {
+ selector: ".boxmodel-padding.boxmodel-bottom > span",
+ value: 20
+ },
+ {
+ selector: ".boxmodel-padding.boxmodel-right > span",
+ value: 20
+ },
+ {
+ selector: ".boxmodel-border.boxmodel-top > span",
+ value: 10
+ },
+ {
+ selector: ".boxmodel-border.boxmodel-left > span",
+ value: 10
+ },
+ {
+ selector: ".boxmodel-border.boxmodel-bottom > span",
+ value: 10
+ },
+ {
+ selector: ".boxmodel-border.boxmodel-right > span",
+ value: 10
+ },
+];
+
+var res2 = [
+ {
+ selector: "#boxmodel-element-size",
+ value: "190" + "\u00D7" + "210"
+ },
+ {
+ selector: ".boxmodel-size > span",
+ value: "100" + "\u00D7" + "150"
+ },
+ {
+ selector: ".boxmodel-margin.boxmodel-top > span",
+ value: 30
+ },
+ {
+ selector: ".boxmodel-margin.boxmodel-left > span",
+ value: "auto"
+ },
+ {
+ selector: ".boxmodel-margin.boxmodel-bottom > span",
+ value: 30
+ },
+ {
+ selector: ".boxmodel-margin.boxmodel-right > span",
+ value: "auto"
+ },
+ {
+ selector: ".boxmodel-padding.boxmodel-top > span",
+ value: 20
+ },
+ {
+ selector: ".boxmodel-padding.boxmodel-left > span",
+ value: 20
+ },
+ {
+ selector: ".boxmodel-padding.boxmodel-bottom > span",
+ value: 20
+ },
+ {
+ selector: ".boxmodel-padding.boxmodel-right > span",
+ value: 50
+ },
+ {
+ selector: ".boxmodel-border.boxmodel-top > span",
+ value: 10
+ },
+ {
+ selector: ".boxmodel-border.boxmodel-left > span",
+ value: 10
+ },
+ {
+ selector: ".boxmodel-border.boxmodel-bottom > span",
+ value: 10
+ },
+ {
+ selector: ".boxmodel-border.boxmodel-right > span",
+ value: 10
+ },
+];
+
+add_task(function* () {
+ let style = "div { position: absolute; top: 42px; left: 42px; " +
+ "height: 100.111px; 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 {inspector, view, testActor} = yield openBoxModelView();
+ yield selectNode("div", inspector);
+
+ yield testInitialValues(inspector, view);
+ yield testChangingValues(inspector, view, testActor);
+});
+
+function* testInitialValues(inspector, view) {
+ info("Test that the initial values of the box model are correct");
+ 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.");
+ }
+}
+
+function* testChangingValues(inspector, view, testActor) {
+ info("Test that changing the document updates the box model");
+ let viewdoc = view.doc;
+
+ let onUpdated = waitForUpdate(inspector);
+ yield testActor.setAttribute("div", "style",
+ "height:150px;padding-right: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/devtools/client/inspector/components/test/browser_boxmodel_editablemodel.js b/devtools/client/inspector/components/test/browser_boxmodel_editablemodel.js
new file mode 100644
index 000000000..5c32c2029
--- /dev/null
+++ b/devtools/client/inspector/components/test/browser_boxmodel_editablemodel.js
@@ -0,0 +1,194 @@
+/* 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; }" +
+ "#div4 { margin: 1px; }" +
+ "</style>" +
+ "<div id='div1'></div><div id='div2'></div>" +
+ "<div id='div3'></div><div id='div4'></div>";
+
+add_task(function* () {
+ yield addTab("data:text/html," + encodeURIComponent(TEST_URI));
+ let {inspector, view, testActor} = yield openBoxModelView();
+
+ yield testEditingMargins(inspector, view, testActor);
+ yield testKeyBindings(inspector, view, testActor);
+ yield testEscapeToUndo(inspector, view, testActor);
+ yield testDeletingValue(inspector, view, testActor);
+ yield testRefocusingOnClick(inspector, view, testActor);
+});
+
+function* testEditingMargins(inspector, view, testActor) {
+ info("Test that editing margin dynamically updates the document, pressing " +
+ "escape cancels the changes");
+
+ is((yield getStyle(testActor, "#div1", "margin-top")), "",
+ "Should be no margin-top on the element.");
+ yield selectNode("#div1", inspector);
+
+ let span = view.doc.querySelector(".boxmodel-margin.boxmodel-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((yield getStyle(testActor, "#div1", "margin-top")), "3px",
+ "Should have updated the margin.");
+
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, view.doc.defaultView);
+ yield waitForUpdate(inspector);
+
+ is((yield getStyle(testActor, "#div1", "margin-top")), "",
+ "Should be no margin-top on the element.");
+ is(span.textContent, 5, "Should have the right value in the box model.");
+}
+
+function* testKeyBindings(inspector, view, testActor) {
+ info("Test that arrow keys work correctly and pressing enter commits the " +
+ "changes");
+
+ is((yield getStyle(testActor, "#div1", "margin-left")), "",
+ "Should be no margin-top on the element.");
+ yield selectNode("#div1", inspector);
+
+ let span = view.doc.querySelector(".boxmodel-margin.boxmodel-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((yield getStyle(testActor, "#div1", "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((yield getStyle(testActor, "#div1", "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((yield getStyle(testActor, "#div1", "margin-left")), "20px",
+ "Should have updated the margin.");
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView);
+
+ is((yield getStyle(testActor, "#div1", "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.");
+}
+
+function* testEscapeToUndo(inspector, view, testActor) {
+ info("Test that deleting the value removes the property but escape undoes " +
+ "that");
+
+ is((yield getStyle(testActor, "#div1", "margin-left")), "20px",
+ "Should be the right margin-top on the element.");
+ yield selectNode("#div1", inspector);
+
+ let span = view.doc.querySelector(".boxmodel-margin.boxmodel-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((yield getStyle(testActor, "#div1", "margin-left")), "",
+ "Should have updated the margin.");
+
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, view.doc.defaultView);
+ yield waitForUpdate(inspector);
+
+ is((yield getStyle(testActor, "#div1", "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.");
+}
+
+function* testDeletingValue(inspector, view, testActor) {
+ info("Test that deleting the value removes the property");
+
+ yield setStyle(testActor, "#div1", "marginRight", "15px");
+ yield waitForUpdate(inspector);
+
+ yield selectNode("#div1", inspector);
+
+ let span = view.doc.querySelector(".boxmodel-margin.boxmodel-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((yield getStyle(testActor, "#div1", "margin-right")), "",
+ "Should have updated the margin.");
+
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView);
+
+ is((yield getStyle(testActor, "#div1", "margin-right")), "",
+ "Should be the right margin-top on the element.");
+ is(span.textContent, 10, "Should have the right value in the box model.");
+}
+
+function* testRefocusingOnClick(inspector, view, testActor) {
+ info("Test that clicking in the editor input does not remove focus");
+
+ yield selectNode("#div4", inspector);
+
+ let span = view.doc.querySelector(".boxmodel-margin.boxmodel-top > span");
+ is(span.textContent, 1, "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.");
+
+ info("Click in the already opened editor input");
+ EventUtils.synthesizeMouseAtCenter(editor, {}, view.doc.defaultView);
+ is(editor, view.doc.activeElement,
+ "Inplace editor input should still have focus.");
+
+ info("Check the input can still be used as expected");
+ EventUtils.synthesizeKey("VK_UP", {}, view.doc.defaultView);
+ yield waitForUpdate(inspector);
+
+ is(editor.value, "2px", "Should have the right value in the editor.");
+ is((yield getStyle(testActor, "#div4", "margin-top")), "2px",
+ "Should have updated the margin.");
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView);
+
+ is((yield getStyle(testActor, "#div4", "margin-top")), "2px",
+ "Should be the right margin-top on the element.");
+ is(span.textContent, 2, "Should have the right value in the box model.");
+}
diff --git a/devtools/client/inspector/components/test/browser_boxmodel_editablemodel_allproperties.js b/devtools/client/inspector/components/test/browser_boxmodel_editablemodel_allproperties.js
new file mode 100644
index 000000000..464a7b6c5
--- /dev/null
+++ b/devtools/client/inspector/components/test/browser_boxmodel_editablemodel_allproperties.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 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>";
+
+add_task(function* () {
+ yield addTab("data:text/html," + encodeURIComponent(TEST_URI));
+ let {inspector, view, testActor} = yield openBoxModelView();
+
+ yield testEditing(inspector, view, testActor);
+ yield testEditingAndCanceling(inspector, view, testActor);
+ yield testDeleting(inspector, view, testActor);
+ yield testDeletingAndCanceling(inspector, view, testActor);
+});
+
+function* testEditing(inspector, view, testActor) {
+ info("When all properties are set on the node editing one should work");
+
+ yield setStyle(testActor, "#div1", "padding", "5px");
+ yield waitForUpdate(inspector);
+
+ yield selectNode("#div1", inspector);
+
+ let span = view.doc.querySelector(".boxmodel-padding.boxmodel-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((yield getStyle(testActor, "#div1", "padding-bottom")), "7px",
+ "Should have updated the padding");
+
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView);
+
+ is((yield getStyle(testActor, "#div1", "padding-bottom")), "7px",
+ "Should be the right padding.");
+ is(span.textContent, 7, "Should have the right value in the box model.");
+}
+
+function* testEditingAndCanceling(inspector, view, testActor) {
+ info("When all properties are set on the node editing one and then " +
+ "cancelling with ESCAPE should work");
+
+ yield setStyle(testActor, "#div1", "padding", "5px");
+ yield waitForUpdate(inspector);
+
+ yield selectNode("#div1", inspector);
+
+ let span = view.doc.querySelector(".boxmodel-padding.boxmodel-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((yield getStyle(testActor, "#div1", "padding-left")), "8px",
+ "Should have updated the padding");
+
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, view.doc.defaultView);
+ yield waitForUpdate(inspector);
+
+ is((yield getStyle(testActor, "#div1", "padding-left")), "5px",
+ "Should be the right padding.");
+ is(span.textContent, 5, "Should have the right value in the box model.");
+}
+
+function* testDeleting(inspector, view, testActor) {
+ info("When all properties are set on the node deleting one should work");
+
+ yield selectNode("#div1", inspector);
+
+ let span = view.doc.querySelector(".boxmodel-padding.boxmodel-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((yield getStyle(testActor, "#div1", "padding-left")), "",
+ "Should have updated the padding");
+
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView);
+
+ is((yield getStyle(testActor, "#div1", "padding-left")), "",
+ "Should be the right padding.");
+ is(span.textContent, 3, "Should have the right value in the box model.");
+}
+
+function* testDeletingAndCanceling(inspector, view, testActor) {
+ info("When all properties are set on the node deleting one then cancelling " +
+ "should work");
+
+ yield setStyle(testActor, "#div1", "padding", "5px");
+ yield waitForUpdate(inspector);
+
+ yield selectNode("#div1", inspector);
+
+ let span = view.doc.querySelector(".boxmodel-padding.boxmodel-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((yield getStyle(testActor, "#div1", "padding-left")), "",
+ "Should have updated the padding");
+
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, view.doc.defaultView);
+ yield waitForUpdate(inspector);
+
+ is((yield getStyle(testActor, "#div1", "padding-left")), "5px",
+ "Should be the right padding.");
+ is(span.textContent, 5, "Should have the right value in the box model.");
+}
diff --git a/devtools/client/inspector/components/test/browser_boxmodel_editablemodel_bluronclick.js b/devtools/client/inspector/components/test/browser_boxmodel_editablemodel_bluronclick.js
new file mode 100644
index 000000000..9e65e4dc7
--- /dev/null
+++ b/devtools/client/inspector/components/test/browser_boxmodel_editablemodel_bluronclick.js
@@ -0,0 +1,74 @@
+/* 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 inplace editors can be blurred by clicking outside of the editor.
+
+const TEST_URI =
+ `<style>
+ #div1 {
+ margin: 10px;
+ padding: 3px;
+ }
+ </style>
+ <div id="div1"></div>`;
+
+add_task(function* () {
+ // Make sure the toolbox is tall enough to have empty space below the
+ // boxmodel-container.
+ yield pushPref("devtools.toolbox.footer.height", 500);
+
+ yield addTab("data:text/html," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openBoxModelView();
+
+ yield selectNode("#div1", inspector);
+ yield testClickingOutsideEditor(view);
+ yield testClickingBelowContainer(view);
+});
+
+function* testClickingOutsideEditor(view) {
+ info("Test that clicking outside the editor blurs it");
+ let span = view.doc.querySelector(".boxmodel-margin.boxmodel-top > 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.");
+
+ info("Click next to the opened editor input.");
+ let onBlur = once(editor, "blur");
+ let rect = editor.getBoundingClientRect();
+ EventUtils.synthesizeMouse(editor, rect.width + 10, rect.height / 2, {},
+ view.doc.defaultView);
+ yield onBlur;
+
+ is(view.doc.querySelector(".styleinspector-propertyeditor"), null,
+ "Inplace editor has been removed.");
+}
+
+function* testClickingBelowContainer(view) {
+ info("Test that clicking below the box-model container blurs it");
+ let span = view.doc.querySelector(".boxmodel-margin.boxmodel-top > span");
+ is(span.textContent, 10, "Should have the right value in the box model.");
+
+ info("Test that clicking below the boxmodel-container blurs the opened editor");
+ EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
+ let editor = view.doc.querySelector(".styleinspector-propertyeditor");
+ ok(editor, "Should have opened the editor.");
+
+ let onBlur = once(editor, "blur");
+ let container = view.doc.querySelector("#boxmodel-container");
+ // Using getBoxQuads here because getBoundingClientRect (and therefore synthesizeMouse)
+ // use an erroneous height of ~50px for the boxmodel-container.
+ let bounds = container.getBoxQuads({relativeTo: view.doc})[0].bounds;
+ EventUtils.synthesizeMouseAtPoint(
+ bounds.left + 10,
+ bounds.top + bounds.height + 10,
+ {}, view.doc.defaultView);
+ yield onBlur;
+
+ is(view.doc.querySelector(".styleinspector-propertyeditor"), null,
+ "Inplace editor has been removed.");
+}
diff --git a/devtools/client/inspector/components/test/browser_boxmodel_editablemodel_border.js b/devtools/client/inspector/components/test/browser_boxmodel_editablemodel_border.js
new file mode 100644
index 000000000..6e9c04b14
--- /dev/null
+++ b/devtools/client/inspector/components/test/browser_boxmodel_editablemodel_border.js
@@ -0,0 +1,52 @@
+/* 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>";
+
+add_task(function* () {
+ yield addTab("data:text/html," + encodeURIComponent(TEST_URI));
+ let {inspector, view, testActor} = yield openBoxModelView();
+
+ is((yield getStyle(testActor, "#div1", "border-top-width")), "",
+ "Should have the right border");
+ is((yield getStyle(testActor, "#div1", "border-top-style")), "",
+ "Should have the right border");
+ yield selectNode("#div1", inspector);
+
+ let span = view.doc.querySelector(".boxmodel-border.boxmodel-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((yield getStyle(testActor, "#div1", "border-top-width")), "1px",
+ "Should have the right border");
+ is((yield getStyle(testActor, "#div1", "border-top-style")), "solid",
+ "Should have the right border");
+
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, view.doc.defaultView);
+ yield waitForUpdate(inspector);
+
+ is((yield getStyle(testActor, "#div1", "border-top-width")), "",
+ "Should be the right padding.");
+ is((yield getStyle(testActor, "#div1", "border-top-style")), "",
+ "Should have the right border");
+ is(span.textContent, 0, "Should have the right value in the box model.");
+});
diff --git a/devtools/client/inspector/components/test/browser_boxmodel_editablemodel_stylerules.js b/devtools/client/inspector/components/test/browser_boxmodel_editablemodel_stylerules.js
new file mode 100644
index 000000000..43346fa15
--- /dev/null
+++ b/devtools/client/inspector/components/test/browser_boxmodel_editablemodel_stylerules.js
@@ -0,0 +1,113 @@
+/* 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>";
+
+add_task(function* () {
+ yield addTab("data:text/html," + encodeURIComponent(TEST_URI));
+ let {inspector, view, testActor} = yield openBoxModelView();
+
+ yield testUnits(inspector, view, testActor);
+ yield testValueComesFromStyleRule(inspector, view, testActor);
+ yield testShorthandsAreParsed(inspector, view, testActor);
+});
+
+function* testUnits(inspector, view, testActor) {
+ info("Test that entering units works");
+
+ is((yield getStyle(testActor, "#div1", "padding-top")), "",
+ "Should have the right padding");
+ yield selectNode("#div1", inspector);
+
+ let span = view.doc.querySelector(".boxmodel-padding.boxmodel-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((yield getStyle(testActor, "#div1", "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((yield getStyle(testActor, "#div1", "padding-top")),
+ "1em", "Should have updated the padding.");
+
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView);
+
+ is((yield getStyle(testActor, "#div1", "padding-top")), "1em",
+ "Should be the right padding.");
+ is(span.textContent, 16, "Should have the right value in the box model.");
+}
+
+function* testValueComesFromStyleRule(inspector, view, testActor) {
+ info("Test that we pick up the value from a higher style rule");
+
+ is((yield getStyle(testActor, "#div2", "border-bottom-width")), "",
+ "Should have the right border-bottom-width");
+ yield selectNode("#div2", inspector);
+
+ let span = view.doc.querySelector(".boxmodel-border.boxmodel-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((yield getStyle(testActor, "#div2", "border-bottom-width")), "0px",
+ "Should have updated the border.");
+
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView);
+
+ is((yield getStyle(testActor, "#div2", "border-bottom-width")), "0px",
+ "Should be the right border-bottom-width.");
+ is(span.textContent, 0, "Should have the right value in the box model.");
+}
+
+function* testShorthandsAreParsed(inspector, view, testActor) {
+ info("Test that shorthand properties are parsed correctly");
+
+ is((yield getStyle(testActor, "#div3", "padding-right")), "",
+ "Should have the right padding");
+ yield selectNode("#div3", inspector);
+
+ let span = view.doc.querySelector(".boxmodel-padding.boxmodel-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((yield getStyle(testActor, "#div3", "padding-right")), "",
+ "Should be the right padding.");
+ is(span.textContent, 32, "Should have the right value in the box model.");
+}
diff --git a/devtools/client/inspector/components/test/browser_boxmodel_guides.js b/devtools/client/inspector/components/test/browser_boxmodel_guides.js
new file mode 100644
index 000000000..612d9ace6
--- /dev/null
+++ b/devtools/client/inspector/components/test/browser_boxmodel_guides.js
@@ -0,0 +1,56 @@
+/* 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
+// box model 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);
+
+var highlightedNodeFront, highlighterOptions;
+
+add_task(function* () {
+ yield addTab(TEST_URL);
+ let {toolbox, inspector, view} = yield openBoxModelView();
+ 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("boxmodel-margins");
+ yield testGuideOnLayoutHover(elt, "margin", inspector, view);
+
+ elt = view.doc.getElementById("boxmodel-borders");
+ yield testGuideOnLayoutHover(elt, "border", inspector, view);
+
+ elt = view.doc.getElementById("boxmodel-padding");
+ yield testGuideOnLayoutHover(elt, "padding", inspector, view);
+
+ elt = view.doc.getElementById("boxmodel-content");
+ yield testGuideOnLayoutHover(elt, "content", inspector, view);
+});
+
+function* testGuideOnLayoutHover(elt, expectedRegion, inspector) {
+ info("Synthesizing mouseover on the boxmodel-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/devtools/client/inspector/components/test/browser_boxmodel_rotate-labels-on-sides.js b/devtools/client/inspector/components/test/browser_boxmodel_rotate-labels-on-sides.js
new file mode 100644
index 000000000..954cd298b
--- /dev/null
+++ b/devtools/client/inspector/components/test/browser_boxmodel_rotate-labels-on-sides.js
@@ -0,0 +1,49 @@
+/* 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: ".boxmodel-margin.boxmodel-top > span", value: 30},
+ {selector: ".boxmodel-margin.boxmodel-left > span", value: "auto"},
+ {selector: ".boxmodel-margin.boxmodel-bottom > span", value: 30},
+ {selector: ".boxmodel-margin.boxmodel-right > span", value: "auto"},
+ {selector: ".boxmodel-padding.boxmodel-top > span", value: 20},
+ {selector: ".boxmodel-padding.boxmodel-left > span", value: 2000000},
+ {selector: ".boxmodel-padding.boxmodel-bottom > span", value: 20},
+ {selector: ".boxmodel-padding.boxmodel-right > span", value: 20},
+ {selector: ".boxmodel-border.boxmodel-top > span", value: 10},
+ {selector: ".boxmodel-border.boxmodel-left > span", value: 10},
+ {selector: ".boxmodel-border.boxmodel-bottom > span", value: 10},
+ {selector: ".boxmodel-border.boxmodel-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 {inspector, view} = yield openBoxModelView();
+ 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("boxmodel-left") ||
+ classList.contains("boxmodel-right");
+ let isRotated = classList.contains("boxmodel-rotate");
+
+ is(canBeRotated && isLong,
+ isRotated, res1[i].selector + " correctly rotated.");
+ }
+});
diff --git a/devtools/client/inspector/components/test/browser_boxmodel_sync.js b/devtools/client/inspector/components/test/browser_boxmodel_sync.js
new file mode 100644
index 000000000..a896bfe06
--- /dev/null
+++ b/devtools/client/inspector/components/test/browser_boxmodel_sync.js
@@ -0,0 +1,44 @@
+/* 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 syncs with the rule view.
+
+const TEST_URI = "<p>hello</p>";
+
+add_task(function* () {
+ yield addTab("data:text/html," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openBoxModelView();
+
+ info("When a property is edited, it should sync in the rule view");
+
+ yield selectNode("p", inspector);
+
+ info("Modify padding-bottom in box model view");
+ let span = view.doc.querySelector(".boxmodel-padding.boxmodel-bottom > span");
+ EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
+ let editor = view.doc.querySelector(".styleinspector-propertyeditor");
+
+ EventUtils.synthesizeKey("7", {}, view.doc.defaultView);
+ yield waitForUpdate(inspector);
+ is(editor.value, "7", "Should have the right value in the editor.");
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView);
+
+ let onRuleViewRefreshed = once(inspector, "rule-view-refreshed");
+ let onRuleViewSelected = once(inspector.sidebar, "ruleview-selected");
+ info("Select the rule view and check that the property was synced there");
+ let ruleView = selectRuleView(inspector);
+
+ info("Wait for the rule view to be selected");
+ yield onRuleViewSelected;
+
+ info("Wait for the rule view to be refreshed");
+ yield onRuleViewRefreshed;
+ ok(true, "The rule view was refreshed");
+
+ let ruleEditor = getRuleViewRuleEditor(ruleView, 0);
+ let textProp = ruleEditor.rule.textProps[0];
+ is(textProp.value, "7px", "The property has the right value");
+});
diff --git a/devtools/client/inspector/components/test/browser_boxmodel_tooltips.js b/devtools/client/inspector/components/test/browser_boxmodel_tooltips.js
new file mode 100644
index 000000000..b65d2446a
--- /dev/null
+++ b/devtools/client/inspector/components/test/browser_boxmodel_tooltips.js
@@ -0,0 +1,126 @@
+/* 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 regions in the box model view have tooltips, and that individual
+// values too. Also test that values that are set from a css rule have tooltips
+// referencing the rule.
+
+const TEST_URI = "<style>" +
+ "#div1 { color: red; margin: 3em; }\n" +
+ "#div2 { border-bottom: 1px solid black; background: red; }\n" +
+ "html, body, #div3 { box-sizing: border-box; padding: 0 2em; }" +
+ "</style>" +
+ "<div id='div1'></div><div id='div2'></div><div id='div3'></div>";
+
+// Test data for the tooltips over individual values.
+// Each entry should contain:
+// - selector: The selector for the node to be selected before starting to test
+// - values: An array containing objects for each of the values that are defined
+// by css rules. Each entry should contain:
+// - name: the name of the property that is set by the css rule
+// - ruleSelector: the selector of the rule
+// - styleSheetLocation: the fileName:lineNumber
+const VALUES_TEST_DATA = [{
+ selector: "#div1",
+ values: [{
+ name: "margin-top",
+ ruleSelector: "#div1",
+ styleSheetLocation: "inline:1"
+ }, {
+ name: "margin-right",
+ ruleSelector: "#div1",
+ styleSheetLocation: "inline:1"
+ }, {
+ name: "margin-bottom",
+ ruleSelector: "#div1",
+ styleSheetLocation: "inline:1"
+ }, {
+ name: "margin-left",
+ ruleSelector: "#div1",
+ styleSheetLocation: "inline:1"
+ }]
+}, {
+ selector: "#div2",
+ values: [{
+ name: "border-bottom-width",
+ ruleSelector: "#div2",
+ styleSheetLocation: "inline:2"
+ }]
+}, {
+ selector: "#div3",
+ values: [{
+ name: "padding-top",
+ ruleSelector: "html, body, #div3",
+ styleSheetLocation: "inline:3"
+ }, {
+ name: "padding-right",
+ ruleSelector: "html, body, #div3",
+ styleSheetLocation: "inline:3"
+ }, {
+ name: "padding-bottom",
+ ruleSelector: "html, body, #div3",
+ styleSheetLocation: "inline:3"
+ }, {
+ name: "padding-left",
+ ruleSelector: "html, body, #div3",
+ styleSheetLocation: "inline:3"
+ }]
+}];
+
+add_task(function* () {
+ yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ let {inspector, view} = yield openBoxModelView();
+
+ info("Checking the regions tooltips");
+
+ ok(view.doc.querySelector("#boxmodel-margins").hasAttribute("title"),
+ "The margin region has a tooltip");
+ is(view.doc.querySelector("#boxmodel-margins").getAttribute("title"), "margin",
+ "The margin region has the correct tooltip content");
+
+ ok(view.doc.querySelector("#boxmodel-borders").hasAttribute("title"),
+ "The border region has a tooltip");
+ is(view.doc.querySelector("#boxmodel-borders").getAttribute("title"), "border",
+ "The border region has the correct tooltip content");
+
+ ok(view.doc.querySelector("#boxmodel-padding").hasAttribute("title"),
+ "The padding region has a tooltip");
+ is(view.doc.querySelector("#boxmodel-padding").getAttribute("title"), "padding",
+ "The padding region has the correct tooltip content");
+
+ ok(view.doc.querySelector("#boxmodel-content").hasAttribute("title"),
+ "The content region has a tooltip");
+ is(view.doc.querySelector("#boxmodel-content").getAttribute("title"), "content",
+ "The content region has the correct tooltip content");
+
+ for (let {selector, values} of VALUES_TEST_DATA) {
+ info("Selecting " + selector + " and checking the values tooltips");
+ yield selectNode(selector, inspector);
+
+ info("Iterate over all values");
+ for (let key in view.map) {
+ if (key === "position") {
+ continue;
+ }
+
+ let name = view.map[key].property;
+ let expectedTooltipData = values.find(o => o.name === name);
+ let el = view.doc.querySelector(view.map[key].selector);
+
+ ok(el.hasAttribute("title"), "The " + name + " value has a tooltip");
+
+ if (expectedTooltipData) {
+ info("The " + name + " value comes from a css rule");
+ let expectedTooltip = name + "\n" + expectedTooltipData.ruleSelector +
+ "\n" + expectedTooltipData.styleSheetLocation;
+ is(el.getAttribute("title"), expectedTooltip, "The tooltip is correct");
+ } else {
+ info("The " + name + " isn't set by a css rule");
+ is(el.getAttribute("title"), name, "The tooltip is correct");
+ }
+ }
+ }
+});
diff --git a/devtools/client/inspector/components/test/browser_boxmodel_update-after-navigation.js b/devtools/client/inspector/components/test/browser_boxmodel_update-after-navigation.js
new file mode 100644
index 000000000..cb5960229
--- /dev/null
+++ b/devtools/client/inspector/components/test/browser_boxmodel_update-after-navigation.js
@@ -0,0 +1,91 @@
+/* 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 box model view continues to work after a page navigation and that
+// it also works after going back
+
+const IFRAME1 = URL_ROOT + "doc_boxmodel_iframe1.html";
+const IFRAME2 = URL_ROOT + "doc_boxmodel_iframe2.html";
+
+add_task(function* () {
+ yield addTab(IFRAME1);
+ let {inspector, view, testActor} = yield openBoxModelView();
+
+ yield testFirstPage(inspector, view, testActor);
+
+ info("Navigate to the second page");
+ yield testActor.eval(`content.location.href="${IFRAME2}"`);
+ yield inspector.once("markuploaded");
+
+ yield testSecondPage(inspector, view, testActor);
+
+ info("Go back to the first page");
+ yield testActor.eval("content.history.back();");
+ yield inspector.once("markuploaded");
+
+ yield testBackToFirstPage(inspector, view, testActor);
+});
+
+function* testFirstPage(inspector, view, testActor) {
+ info("Test that the box model view works on the first page");
+
+ info("Selecting the test node");
+ yield selectNode("p", inspector);
+
+ info("Checking that the box model view shows the right value");
+ let paddingElt = view.doc.querySelector(".boxmodel-padding.boxmodel-top > span");
+ is(paddingElt.textContent, "50");
+
+ info("Listening for box model view changes and modifying the padding");
+ let onUpdated = waitForUpdate(inspector);
+ yield setStyle(testActor, "p", "padding", "20px");
+ yield onUpdated;
+ ok(true, "Box model view got updated");
+
+ info("Checking that the box model view shows the right value after update");
+ is(paddingElt.textContent, "20");
+}
+
+function* testSecondPage(inspector, view, testActor) {
+ info("Test that the box model view works on the second page");
+
+ info("Selecting the test node");
+ yield selectNode("p", inspector);
+
+ info("Checking that the box model view shows the right value");
+ let sizeElt = view.doc.querySelector(".boxmodel-size > span");
+ is(sizeElt.textContent, "100" + "\u00D7" + "100");
+
+ info("Listening for box model view changes and modifying the size");
+ let onUpdated = waitForUpdate(inspector);
+ yield setStyle(testActor, "p", "width", "200px");
+ yield onUpdated;
+ ok(true, "Box model view got updated");
+
+ info("Checking that the box model view shows the right value after update");
+ is(sizeElt.textContent, "200" + "\u00D7" + "100");
+}
+
+function* testBackToFirstPage(inspector, view, testActor) {
+ info("Test that the box model view works on the first page after going back");
+
+ info("Selecting the test node");
+ yield selectNode("p", inspector);
+
+ info("Checking that the box model view shows the right value, which is the" +
+ "modified value from step one because of the bfcache");
+ let paddingElt = view.doc.querySelector(".boxmodel-padding.boxmodel-top > span");
+ is(paddingElt.textContent, "20");
+
+ info("Listening for box model view changes and modifying the padding");
+ let onUpdated = waitForUpdate(inspector);
+ yield setStyle(testActor, "p", "padding", "100px");
+ yield onUpdated;
+ ok(true, "Box model view got updated");
+
+ info("Checking that the box model view shows the right value after update");
+ is(paddingElt.textContent, "100");
+}
diff --git a/devtools/client/inspector/components/test/browser_boxmodel_update-after-reload.js b/devtools/client/inspector/components/test/browser_boxmodel_update-after-reload.js
new file mode 100644
index 000000000..7fc09bfa3
--- /dev/null
+++ b/devtools/client/inspector/components/test/browser_boxmodel_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 box model view continues to work after the page is reloaded
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_boxmodel_iframe1.html");
+ let {inspector, view, testActor} = yield openBoxModelView();
+
+ info("Test that the box model view works on the first page");
+ yield assertBoxModelView(inspector, view, testActor);
+
+ info("Reload the page");
+ yield testActor.reload();
+ yield inspector.once("markuploaded");
+
+ info("Test that the box model view works on the reloaded page");
+ yield assertBoxModelView(inspector, view, testActor);
+});
+
+function* assertBoxModelView(inspector, view, testActor) {
+ info("Selecting the test node");
+ yield selectNode("p", inspector);
+
+ info("Checking that the box model view shows the right value");
+ let paddingElt = view.doc.querySelector(".boxmodel-padding.boxmodel-top > span");
+ is(paddingElt.textContent, "50");
+
+ info("Listening for box model view changes and modifying the padding");
+ let onUpdated = waitForUpdate(inspector);
+ yield setStyle(testActor, "p", "padding", "20px");
+ yield onUpdated;
+ ok(true, "Box model view got updated");
+
+ info("Checking that the box model view shows the right value after update");
+ is(paddingElt.textContent, "20");
+}
diff --git a/devtools/client/inspector/components/test/browser_boxmodel_update-in-iframes.js b/devtools/client/inspector/components/test/browser_boxmodel_update-in-iframes.js
new file mode 100644
index 000000000..50014ad1c
--- /dev/null
+++ b/devtools/client/inspector/components/test/browser_boxmodel_update-in-iframes.js
@@ -0,0 +1,101 @@
+/* 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 box model view for elements within iframes also updates when they
+// change
+
+add_task(function* () {
+ yield addTab(URL_ROOT + "doc_boxmodel_iframe1.html");
+ let {inspector, view, testActor} = yield openBoxModelView();
+
+ yield testResizingInIframe(inspector, view, testActor);
+ yield testReflowsAfterIframeDeletion(inspector, view, testActor);
+});
+
+function* testResizingInIframe(inspector, view, testActor) {
+ info("Test that resizing an element in an iframe updates its box model");
+
+ info("Selecting the nested test node");
+ yield selectNodeInIframe2("div", inspector);
+
+ info("Checking that the box model view shows the right value");
+ let sizeElt = view.doc.querySelector(".boxmodel-size > span");
+ is(sizeElt.textContent, "400\u00D7200");
+
+ info("Listening for box model view changes and modifying its size");
+ let onUpdated = waitForUpdate(inspector);
+ yield setStyleInIframe2(testActor, "div", "width", "200px");
+ yield onUpdated;
+ ok(true, "Box model view got updated");
+
+ info("Checking that the box model view shows the right value after update");
+ is(sizeElt.textContent, "200\u00D7200");
+}
+
+function* testReflowsAfterIframeDeletion(inspector, view, testActor) {
+ info("Test reflows are still sent to the box model view after deleting an " +
+ "iframe");
+
+ info("Deleting the iframe2");
+ yield removeIframe2(testActor);
+ yield inspector.once("inspector-updated");
+
+ info("Selecting the test node in iframe1");
+ yield selectNodeInIframe1("p", inspector);
+
+ info("Checking that the box model view shows the right value");
+ let sizeElt = view.doc.querySelector(".boxmodel-size > span");
+ is(sizeElt.textContent, "100\u00D7100");
+
+ info("Listening for box model view changes and modifying its size");
+ let onUpdated = waitForUpdate(inspector);
+ yield setStyleInIframe1(testActor, "p", "width", "200px");
+ yield onUpdated;
+ ok(true, "Box model view got updated");
+
+ info("Checking that the box model view shows the right value after update");
+ is(sizeElt.textContent, "200\u00D7100");
+}
+
+function* selectNodeInIframe1(selector, inspector) {
+ let iframe1 = yield getNodeFront("iframe", inspector);
+ let node = yield getNodeFrontInFrame(selector, iframe1, inspector);
+ yield selectNode(node, inspector);
+}
+
+function* selectNodeInIframe2(selector, inspector) {
+ let iframe1 = yield getNodeFront("iframe", inspector);
+ let iframe2 = yield getNodeFrontInFrame("iframe", iframe1, inspector);
+ let node = yield getNodeFrontInFrame(selector, iframe2, inspector);
+ yield selectNode(node, inspector);
+}
+
+function* setStyleInIframe1(testActor, selector, propertyName, value) {
+ yield testActor.eval(`
+ content.document.querySelector("iframe")
+ .contentDocument.querySelector("${selector}")
+ .style.${propertyName} = "${value}";
+ `);
+}
+
+function* setStyleInIframe2(testActor, selector, propertyName, value) {
+ yield testActor.eval(`
+ content.document.querySelector("iframe")
+ .contentDocument
+ .querySelector("iframe")
+ .contentDocument.querySelector("${selector}")
+ .style.${propertyName} = "${value}";
+ `);
+}
+
+function* removeIframe2(testActor) {
+ yield testActor.eval(`
+ content.document.querySelector("iframe")
+ .contentDocument
+ .querySelector("iframe")
+ .remove();
+ `);
+}
diff --git a/devtools/client/inspector/components/test/doc_boxmodel_iframe1.html b/devtools/client/inspector/components/test/doc_boxmodel_iframe1.html
new file mode 100644
index 000000000..eef48ce07
--- /dev/null
+++ b/devtools/client/inspector/components/test/doc_boxmodel_iframe1.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<p style="padding:50px;color:#f06;">Root page</p>
+<iframe src="doc_boxmodel_iframe2.html"></iframe>
diff --git a/devtools/client/inspector/components/test/doc_boxmodel_iframe2.html b/devtools/client/inspector/components/test/doc_boxmodel_iframe2.html
new file mode 100644
index 000000000..1f1b0463c
--- /dev/null
+++ b/devtools/client/inspector/components/test/doc_boxmodel_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>
diff --git a/devtools/client/inspector/components/test/head.js b/devtools/client/inspector/components/test/head.js
new file mode 100644
index 000000000..fa86b5e9e
--- /dev/null
+++ b/devtools/client/inspector/components/test/head.js
@@ -0,0 +1,87 @@
+/* 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/ */
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+/* import-globals-from ../../../framework/test/shared-head.js */
+/* import-globals-from ../../test/head.js */
+"use strict";
+
+// Import the inspector's head.js first (which itself imports shared-head.js).
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/inspector/test/head.js",
+ this);
+
+Services.prefs.setIntPref("devtools.toolbox.footer.height", 350);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.toolbox.footer.height");
+});
+
+/**
+ * Highlight a node and set the inspector's current selection to the node or
+ * the first match of the given css selector.
+ * @param {String|NodeFront} selectorOrNodeFront
+ * The selector for the node to be set, or the nodeFront
+ * @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(selectorOrNodeFront, inspector) {
+ info("Highlighting and selecting the node " + selectorOrNodeFront);
+
+ let nodeFront = yield getNodeFront(selectorOrNodeFront, inspector);
+ let updated = inspector.toolbox.once("highlighter-ready");
+ inspector.selection.setNodeFront(nodeFront, "test-highlight");
+ yield updated;
+}
+
+/**
+ * Open the toolbox, with the inspector tool visible, and the computed view
+ * sidebar tab selected to display the box model view.
+ * @return a promise that resolves when the inspector is ready and the box model
+ * view is visible and ready
+ */
+function openBoxModelView() {
+ return openInspectorSidebarTab("computedview").then(data => {
+ // The actual highligher show/hide methods are mocked in box model tests.
+ // The highlighter is tested in devtools/inspector/test.
+ function mockHighlighter({highlighter}) {
+ highlighter.showBoxModel = function () {
+ return promise.resolve();
+ };
+ highlighter.hideBoxModel = function () {
+ return promise.resolve();
+ };
+ }
+ mockHighlighter(data.toolbox);
+
+ return {
+ toolbox: data.toolbox,
+ inspector: data.inspector,
+ view: data.inspector.computedview.boxModelView,
+ testActor: data.testActor
+ };
+ });
+}
+
+/**
+ * Wait for the boxmodel-view-updated event.
+ * @return a promise
+ */
+function waitForUpdate(inspector) {
+ return inspector.once("boxmodel-view-updated");
+}
+
+function getStyle(testActor, selector, propertyName) {
+ return testActor.eval(`
+ content.document.querySelector("${selector}")
+ .style.getPropertyValue("${propertyName}");
+ `);
+}
+
+function setStyle(testActor, selector, propertyName, value) {
+ return testActor.eval(`
+ content.document.querySelector("${selector}")
+ .style.${propertyName} = "${value}";
+ `);
+}