summaryrefslogtreecommitdiffstats
path: root/devtools/client/inspector/boxmodel
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /devtools/client/inspector/boxmodel
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/inspector/boxmodel')
-rw-r--r--devtools/client/inspector/boxmodel/actions/box-model-highlighter.js86
-rw-r--r--devtools/client/inspector/boxmodel/actions/box-model.js46
-rw-r--r--devtools/client/inspector/boxmodel/actions/index.js21
-rw-r--r--devtools/client/inspector/boxmodel/actions/moz.build11
-rw-r--r--devtools/client/inspector/boxmodel/box-model.js446
-rw-r--r--devtools/client/inspector/boxmodel/components/BoxModel.js97
-rw-r--r--devtools/client/inspector/boxmodel/components/BoxModelEditable.js109
-rw-r--r--devtools/client/inspector/boxmodel/components/BoxModelInfo.js79
-rw-r--r--devtools/client/inspector/boxmodel/components/BoxModelMain.js774
-rw-r--r--devtools/client/inspector/boxmodel/components/BoxModelProperties.js142
-rw-r--r--devtools/client/inspector/boxmodel/components/ComputedProperty.js123
-rw-r--r--devtools/client/inspector/boxmodel/components/moz.build14
-rw-r--r--devtools/client/inspector/boxmodel/moz.build19
-rw-r--r--devtools/client/inspector/boxmodel/reducers/box-model.js45
-rw-r--r--devtools/client/inspector/boxmodel/reducers/moz.build9
-rw-r--r--devtools/client/inspector/boxmodel/test/browser.toml72
-rw-r--r--devtools/client/inspector/boxmodel/test/browser_boxmodel.js201
-rw-r--r--devtools/client/inspector/boxmodel/test/browser_boxmodel_edit-position-visible-position-change.js56
-rw-r--r--devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel.js279
-rw-r--r--devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_allproperties.js191
-rw-r--r--devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_bluronclick.js97
-rw-r--r--devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_border.js75
-rw-r--r--devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_pseudo.js76
-rw-r--r--devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_stylerules.js153
-rw-r--r--devtools/client/inspector/boxmodel/test/browser_boxmodel_guides.js69
-rw-r--r--devtools/client/inspector/boxmodel/test/browser_boxmodel_jump-to-rule-on-hover.js56
-rw-r--r--devtools/client/inspector/boxmodel/test/browser_boxmodel_layout-accordion-state.js103
-rw-r--r--devtools/client/inspector/boxmodel/test/browser_boxmodel_navigation.js200
-rw-r--r--devtools/client/inspector/boxmodel/test/browser_boxmodel_offsetparent.js104
-rw-r--r--devtools/client/inspector/boxmodel/test/browser_boxmodel_positions.js67
-rw-r--r--devtools/client/inspector/boxmodel/test/browser_boxmodel_properties.js129
-rw-r--r--devtools/client/inspector/boxmodel/test/browser_boxmodel_pseudo-element.js122
-rw-r--r--devtools/client/inspector/boxmodel/test/browser_boxmodel_rotate-labels-on-sides.js54
-rw-r--r--devtools/client/inspector/boxmodel/test/browser_boxmodel_show-tooltip-for-unassociated-rule.js50
-rw-r--r--devtools/client/inspector/boxmodel/test/browser_boxmodel_sync.js39
-rw-r--r--devtools/client/inspector/boxmodel/test/browser_boxmodel_tooltips.js166
-rw-r--r--devtools/client/inspector/boxmodel/test/browser_boxmodel_update-after-navigation.js96
-rw-r--r--devtools/client/inspector/boxmodel/test/browser_boxmodel_update-after-reload.js42
-rw-r--r--devtools/client/inspector/boxmodel/test/browser_boxmodel_update-in-iframes.js84
-rw-r--r--devtools/client/inspector/boxmodel/test/doc_boxmodel_iframe1.html3
-rw-r--r--devtools/client/inspector/boxmodel/test/doc_boxmodel_iframe2.html3
-rw-r--r--devtools/client/inspector/boxmodel/test/head.js122
-rw-r--r--devtools/client/inspector/boxmodel/types.js21
-rw-r--r--devtools/client/inspector/boxmodel/utils/editing-session.js188
-rw-r--r--devtools/client/inspector/boxmodel/utils/moz.build9
45 files changed, 4948 insertions, 0 deletions
diff --git a/devtools/client/inspector/boxmodel/actions/box-model-highlighter.js b/devtools/client/inspector/boxmodel/actions/box-model-highlighter.js
new file mode 100644
index 0000000000..9033dc46ae
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/actions/box-model-highlighter.js
@@ -0,0 +1,86 @@
+/* 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";
+
+/**
+ * This module exports thunks.
+ * Thunks are functions that can be dispatched to the Inspector Redux store.
+ *
+ * These functions receive one object with options that contains:
+ * - dispatch() => function to dispatch Redux actions to the store
+ * - getState() => function to get the current state of the entire Inspector Redux store
+ * - inspector => object instance of Inspector client
+ *
+ * They provide a shortcut for React components to invoke the box model highlighter
+ * without having to know where the highlighter exists.
+ */
+
+module.exports = {
+ /**
+ * Show the box model highlighter for the currently selected node front.
+ * The selected node is obtained from the Selection instance on the Inspector.
+ *
+ * @param {Object} options
+ * Optional configuration options passed to the box model highlighter
+ */
+ highlightSelectedNode(options = {}) {
+ return async thunkOptions => {
+ const { inspector } = thunkOptions;
+ if (!inspector || inspector._destroyed) {
+ return;
+ }
+
+ const { nodeFront } = inspector.selection;
+ if (!nodeFront) {
+ return;
+ }
+
+ await inspector.highlighters.showHighlighterTypeForNode(
+ inspector.highlighters.TYPES.BOXMODEL,
+ nodeFront,
+ options
+ );
+ };
+ },
+
+ /**
+ * Show the box model highlighter for the given node front.
+ *
+ * @param {NodeFront} nodeFront
+ * Node that should be highlighted.
+ * @param {Object} options
+ * Optional configuration options passed to the box model highlighter
+ */
+ highlightNode(nodeFront, options = {}) {
+ return async thunkOptions => {
+ const { inspector } = thunkOptions;
+ if (!inspector || inspector._destroyed) {
+ return;
+ }
+
+ await inspector.highlighters.showHighlighterTypeForNode(
+ inspector.highlighters.TYPES.BOXMODEL,
+ nodeFront,
+ options
+ );
+ };
+ },
+
+ /**
+ * Hide the box model highlighter for any highlighted node.
+ */
+ unhighlightNode() {
+ return async thunkOptions => {
+ const { inspector } = thunkOptions;
+ if (!inspector || inspector._destroyed) {
+ return;
+ }
+
+ await inspector.highlighters.hideHighlighterType(
+ inspector.highlighters.TYPES.BOXMODEL
+ );
+ };
+ },
+};
diff --git a/devtools/client/inspector/boxmodel/actions/box-model.js b/devtools/client/inspector/boxmodel/actions/box-model.js
new file mode 100644
index 0000000000..322cf6cb00
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/actions/box-model.js
@@ -0,0 +1,46 @@
+/* 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 {
+ UPDATE_GEOMETRY_EDITOR_ENABLED,
+ UPDATE_LAYOUT,
+ UPDATE_OFFSET_PARENT,
+} = require("resource://devtools/client/inspector/boxmodel/actions/index.js");
+
+module.exports = {
+ /**
+ * Updates the geometry editor's enabled state.
+ *
+ * @param {Boolean} enabled
+ * Whether or not the geometry editor is enabled or not.
+ */
+ updateGeometryEditorEnabled(enabled) {
+ return {
+ type: UPDATE_GEOMETRY_EDITOR_ENABLED,
+ enabled,
+ };
+ },
+
+ /**
+ * Updates the layout state with the new layout properties.
+ */
+ updateLayout(layout) {
+ return {
+ type: UPDATE_LAYOUT,
+ layout,
+ };
+ },
+
+ /**
+ * Updates the offset parent state with the new DOM node.
+ */
+ updateOffsetParent(offsetParent) {
+ return {
+ type: UPDATE_OFFSET_PARENT,
+ offsetParent,
+ };
+ },
+};
diff --git a/devtools/client/inspector/boxmodel/actions/index.js b/devtools/client/inspector/boxmodel/actions/index.js
new file mode 100644
index 0000000000..813b4e9e11
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/actions/index.js
@@ -0,0 +1,21 @@
+/* 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 { createEnum } = require("resource://devtools/client/shared/enum.js");
+
+createEnum(
+ [
+ // Updates the geometry editor's enabled state.
+ "UPDATE_GEOMETRY_EDITOR_ENABLED",
+
+ // Updates the layout state with the latest layout properties.
+ "UPDATE_LAYOUT",
+
+ // Updates the offset parent state with the new DOM node.
+ "UPDATE_OFFSET_PARENT",
+ ],
+ module.exports
+);
diff --git a/devtools/client/inspector/boxmodel/actions/moz.build b/devtools/client/inspector/boxmodel/actions/moz.build
new file mode 100644
index 0000000000..7ee681b35c
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/actions/moz.build
@@ -0,0 +1,11 @@
+# -*- 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-highlighter.js",
+ "box-model.js",
+ "index.js",
+)
diff --git a/devtools/client/inspector/boxmodel/box-model.js b/devtools/client/inspector/boxmodel/box-model.js
new file mode 100644
index 0000000000..c6870ef016
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/box-model.js
@@ -0,0 +1,446 @@
+/* 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 boxModelReducer = require("resource://devtools/client/inspector/boxmodel/reducers/box-model.js");
+const {
+ updateGeometryEditorEnabled,
+ updateLayout,
+ updateOffsetParent,
+} = require("resource://devtools/client/inspector/boxmodel/actions/box-model.js");
+
+loader.lazyRequireGetter(
+ this,
+ "EditingSession",
+ "resource://devtools/client/inspector/boxmodel/utils/editing-session.js"
+);
+loader.lazyRequireGetter(
+ this,
+ "InplaceEditor",
+ "resource://devtools/client/shared/inplace-editor.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "RulePreviewTooltip",
+ "resource://devtools/client/shared/widgets/tooltip/RulePreviewTooltip.js"
+);
+
+const NUMERIC = /^-?[\d\.]+$/;
+
+/**
+ * A singleton instance of the box model controllers.
+ *
+ * @param {Inspector} inspector
+ * An instance of the Inspector currently loaded in the toolbox.
+ * @param {Window} window
+ * The document window of the toolbox.
+ */
+function BoxModel(inspector, window) {
+ this.document = window.document;
+ this.inspector = inspector;
+ this.store = inspector.store;
+
+ this.store.injectReducer("boxModel", boxModelReducer);
+
+ this.updateBoxModel = this.updateBoxModel.bind(this);
+
+ this.onHideGeometryEditor = this.onHideGeometryEditor.bind(this);
+ this.onMarkupViewLeave = this.onMarkupViewLeave.bind(this);
+ this.onMarkupViewNodeHover = this.onMarkupViewNodeHover.bind(this);
+ this.onNewSelection = this.onNewSelection.bind(this);
+ this.onShowBoxModelEditor = this.onShowBoxModelEditor.bind(this);
+ this.onShowRulePreviewTooltip = this.onShowRulePreviewTooltip.bind(this);
+ this.onSidebarSelect = this.onSidebarSelect.bind(this);
+ this.onToggleGeometryEditor = this.onToggleGeometryEditor.bind(this);
+
+ this.inspector.selection.on("new-node-front", this.onNewSelection);
+ this.inspector.sidebar.on("select", this.onSidebarSelect);
+}
+
+BoxModel.prototype = {
+ /**
+ * Destruction function called when the inspector is destroyed. Removes event listeners
+ * and cleans up references.
+ */
+ destroy() {
+ this.inspector.selection.off("new-node-front", this.onNewSelection);
+ this.inspector.sidebar.off("select", this.onSidebarSelect);
+
+ if (this._geometryEditorEventsAbortController) {
+ this._geometryEditorEventsAbortController.abort();
+ this._geometryEditorEventsAbortController = null;
+ }
+
+ if (this._tooltip) {
+ this._tooltip.destroy();
+ }
+
+ this.untrackReflows();
+
+ this.elementRules = null;
+ this._highlighters = null;
+ this._tooltip = null;
+ this.document = null;
+ this.inspector = null;
+ },
+
+ get highlighters() {
+ if (!this._highlighters) {
+ // highlighters is a lazy getter in the inspector.
+ this._highlighters = this.inspector.highlighters;
+ }
+
+ return this._highlighters;
+ },
+
+ get rulePreviewTooltip() {
+ if (!this._tooltip) {
+ this._tooltip = new RulePreviewTooltip(this.inspector.toolbox.doc);
+ }
+
+ return this._tooltip;
+ },
+
+ /**
+ * Returns an object containing the box model's handler functions used in the box
+ * model's React component props.
+ */
+ getComponentProps() {
+ return {
+ onShowBoxModelEditor: this.onShowBoxModelEditor,
+ onShowRulePreviewTooltip: this.onShowRulePreviewTooltip,
+ onToggleGeometryEditor: this.onToggleGeometryEditor,
+ };
+ },
+
+ /**
+ * Returns true if the layout panel is visible, and false otherwise.
+ */
+ isPanelVisible() {
+ return (
+ this.inspector.toolbox &&
+ this.inspector.sidebar &&
+ this.inspector.toolbox.currentToolId === "inspector" &&
+ this.inspector.sidebar.getCurrentTabID() === "layoutview"
+ );
+ },
+
+ /**
+ * Returns true if the layout panel is visible and the current element is valid to
+ * be displayed in the view.
+ */
+ isPanelVisibleAndNodeValid() {
+ return (
+ this.isPanelVisible() &&
+ this.inspector.selection.isConnected() &&
+ this.inspector.selection.isElementNode()
+ );
+ },
+
+ /**
+ * Starts listening to reflows in the current tab.
+ */
+ trackReflows() {
+ this.inspector.on("reflow-in-selected-target", this.updateBoxModel);
+ },
+
+ /**
+ * Stops listening to reflows in the current tab.
+ */
+ untrackReflows() {
+ this.inspector.off("reflow-in-selected-target", this.updateBoxModel);
+ },
+
+ /**
+ * Updates the box model panel by dispatching the new layout data.
+ *
+ * @param {String} reason
+ * Optional string describing the reason why the boxmodel is updated.
+ */
+ updateBoxModel(reason) {
+ this._updateReasons = this._updateReasons || [];
+ if (reason) {
+ this._updateReasons.push(reason);
+ }
+
+ const lastRequest = async function () {
+ if (
+ !this.inspector ||
+ !this.isPanelVisible() ||
+ !this.inspector.selection.isConnected() ||
+ !this.inspector.selection.isElementNode()
+ ) {
+ return null;
+ }
+
+ const { nodeFront } = this.inspector.selection;
+ const inspectorFront = this.getCurrentInspectorFront();
+ const { pageStyle } = inspectorFront;
+
+ let layout = await pageStyle.getLayout(nodeFront, {
+ autoMargins: true,
+ });
+
+ const styleEntries = await pageStyle.getApplied(nodeFront, {
+ // We don't need styles applied to pseudo elements of the current node.
+ skipPseudo: true,
+ });
+ this.elementRules = styleEntries.map(e => e.rule);
+
+ // Update the layout properties with whether or not the element's position is
+ // editable with the geometry editor.
+ const isPositionEditable = await pageStyle.isPositionEditable(nodeFront);
+
+ layout = Object.assign({}, layout, {
+ isPositionEditable,
+ });
+
+ // Update the redux store with the latest offset parent DOM node
+ const offsetParent = await inspectorFront.walker.getOffsetParent(
+ nodeFront
+ );
+ this.store.dispatch(updateOffsetParent(offsetParent));
+
+ // Update the redux store with the latest layout properties and update the box
+ // model view.
+ this.store.dispatch(updateLayout(layout));
+
+ // If a subsequent request has been made, wait for that one instead.
+ if (this._lastRequest != lastRequest) {
+ return this._lastRequest;
+ }
+
+ this.inspector.emit("boxmodel-view-updated", this._updateReasons);
+
+ this._lastRequest = null;
+ this._updateReasons = [];
+
+ return null;
+ }
+ .bind(this)()
+ .catch(error => {
+ // If we failed because we were being destroyed while waiting for a request, ignore.
+ if (this.document) {
+ console.error(error);
+ }
+ });
+
+ this._lastRequest = lastRequest;
+ },
+
+ /**
+ * Hides the geometry editor and updates the box moodel store with the new
+ * geometry editor enabled state.
+ */
+ onHideGeometryEditor() {
+ this.highlighters.hideGeometryEditor();
+ this.store.dispatch(updateGeometryEditorEnabled(false));
+
+ if (this._geometryEditorEventsAbortController) {
+ this._geometryEditorEventsAbortController.abort();
+ this._geometryEditorEventsAbortController = null;
+ }
+ },
+
+ /**
+ * Handler function that re-shows the geometry editor for an element that already
+ * had the geometry editor enabled. This handler function is called on a "leave" event
+ * on the markup view.
+ */
+ onMarkupViewLeave() {
+ const state = this.store.getState();
+ const enabled = state.boxModel.geometryEditorEnabled;
+
+ if (!enabled) {
+ return;
+ }
+
+ const nodeFront = this.inspector.selection.nodeFront;
+ this.highlighters.showGeometryEditor(nodeFront);
+ },
+
+ /**
+ * Handler function that temporarily hides the geomery editor when the
+ * markup view has a "node-hover" event.
+ */
+ onMarkupViewNodeHover() {
+ this.highlighters.hideGeometryEditor();
+ },
+
+ /**
+ * Selection 'new-node-front' event handler.
+ */
+ onNewSelection() {
+ if (!this.isPanelVisibleAndNodeValid()) {
+ return;
+ }
+
+ if (
+ this.inspector.selection.isConnected() &&
+ this.inspector.selection.isElementNode()
+ ) {
+ this.trackReflows();
+ }
+
+ this.updateBoxModel("new-selection");
+ },
+
+ /**
+ * Shows the RulePreviewTooltip when a box model editable value is hovered on the
+ * box model panel.
+ *
+ * @param {Element} target
+ * The target element.
+ * @param {String} property
+ * The name of the property.
+ */
+ onShowRulePreviewTooltip(target, property) {
+ const { highlightProperty } = this.inspector.getPanel("ruleview").view;
+ const isHighlighted = highlightProperty(property);
+
+ // Only show the tooltip if the property is not highlighted.
+ // TODO: In the future, use an associated ruleId for toggling the tooltip instead of
+ // the Boolean returned from highlightProperty.
+ if (!isHighlighted) {
+ this.rulePreviewTooltip.show(target);
+ }
+ },
+
+ /**
+ * Shows the inplace editor when a box model editable value is clicked on the
+ * box model panel.
+ *
+ * @param {DOMNode} element
+ * The element that was clicked.
+ * @param {Event} event
+ * The event object.
+ * @param {String} property
+ * The name of the property.
+ */
+ onShowBoxModelEditor(element, event, property) {
+ const session = new EditingSession({
+ inspector: this.inspector,
+ doc: this.document,
+ elementRules: this.elementRules,
+ });
+ const initialValue = session.getProperty(property);
+
+ const editor = new InplaceEditor(
+ {
+ element,
+ initial: initialValue,
+ contentType: InplaceEditor.CONTENT_TYPES.CSS_VALUE,
+ property: {
+ name: property,
+ },
+ start: self => {
+ self.elt.parentNode.classList.add("boxmodel-editing");
+ },
+ change: value => {
+ if (NUMERIC.test(value)) {
+ value += "px";
+ }
+
+ const properties = [{ name: property, value }];
+
+ if (property.substring(0, 7) == "border-") {
+ const bprop = property.substring(0, property.length - 5) + "style";
+ const style = session.getProperty(bprop);
+ if (!style || style == "none" || style == "hidden") {
+ properties.push({ name: bprop, value: "solid" });
+ }
+ }
+
+ if (property.substring(0, 9) == "position-") {
+ properties[0].name = property.substring(9);
+ }
+
+ session.setProperties(properties).catch(console.error);
+ },
+ done: (value, commit) => {
+ editor.elt.parentNode.classList.remove("boxmodel-editing");
+ if (!commit) {
+ session.revert().then(() => {
+ session.destroy();
+ }, console.error);
+ return;
+ }
+
+ this.updateBoxModel("editable-value-change");
+ },
+ cssProperties: this.inspector.cssProperties,
+ },
+ event
+ );
+ },
+
+ /**
+ * Handler for the inspector sidebar select event. Starts tracking reflows if the
+ * layout panel is visible. Otherwise, stop tracking reflows. Finally, refresh the box
+ * model view if it is visible.
+ */
+ onSidebarSelect() {
+ if (!this.isPanelVisible()) {
+ this.untrackReflows();
+ return;
+ }
+
+ if (
+ this.inspector.selection.isConnected() &&
+ this.inspector.selection.isElementNode()
+ ) {
+ this.trackReflows();
+ }
+
+ this.updateBoxModel();
+ },
+
+ /**
+ * Toggles on/off the geometry editor for the current element when the geometry editor
+ * toggle button is clicked.
+ */
+ onToggleGeometryEditor() {
+ const { markup, selection, toolbox } = this.inspector;
+ const nodeFront = this.inspector.selection.nodeFront;
+ const state = this.store.getState();
+ const enabled = !state.boxModel.geometryEditorEnabled;
+
+ this.highlighters.toggleGeometryHighlighter(nodeFront);
+ this.store.dispatch(updateGeometryEditorEnabled(enabled));
+
+ if (enabled) {
+ this._geometryEditorEventsAbortController = new AbortController();
+ const eventListenersConfig = {
+ signal: this._geometryEditorEventsAbortController.signal,
+ };
+ // Hide completely the geometry editor if:
+ // - the picker is clicked
+ // - or if a new node is selected
+ toolbox.nodePicker.on(
+ "picker-started",
+ this.onHideGeometryEditor,
+ eventListenersConfig
+ );
+ selection.on(
+ "new-node-front",
+ this.onHideGeometryEditor,
+ eventListenersConfig
+ );
+ // Temporarily hide the geometry editor
+ markup.on("leave", this.onMarkupViewLeave, eventListenersConfig);
+ markup.on("node-hover", this.onMarkupViewNodeHover, eventListenersConfig);
+ } else if (this._geometryEditorEventsAbortController) {
+ this._geometryEditorEventsAbortController.abort();
+ this._geometryEditorEventsAbortController = null;
+ }
+ },
+
+ getCurrentInspectorFront() {
+ return this.inspector.selection.nodeFront.inspectorFront;
+ },
+};
+
+module.exports = BoxModel;
diff --git a/devtools/client/inspector/boxmodel/components/BoxModel.js b/devtools/client/inspector/boxmodel/components/BoxModel.js
new file mode 100644
index 0000000000..23d6fc0ed4
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/components/BoxModel.js
@@ -0,0 +1,97 @@
+/* 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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const BoxModelInfo = createFactory(
+ require("resource://devtools/client/inspector/boxmodel/components/BoxModelInfo.js")
+);
+const BoxModelMain = createFactory(
+ require("resource://devtools/client/inspector/boxmodel/components/BoxModelMain.js")
+);
+const BoxModelProperties = createFactory(
+ require("resource://devtools/client/inspector/boxmodel/components/BoxModelProperties.js")
+);
+
+const Types = require("resource://devtools/client/inspector/boxmodel/types.js");
+
+class BoxModel extends PureComponent {
+ static get propTypes() {
+ return {
+ boxModel: PropTypes.shape(Types.boxModel).isRequired,
+ dispatch: PropTypes.func.isRequired,
+ onShowBoxModelEditor: PropTypes.func.isRequired,
+ onShowRulePreviewTooltip: PropTypes.func.isRequired,
+ onToggleGeometryEditor: PropTypes.func.isRequired,
+ showBoxModelProperties: PropTypes.bool.isRequired,
+ setSelectedNode: PropTypes.func.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.onKeyDown = this.onKeyDown.bind(this);
+ }
+
+ onKeyDown(event) {
+ const { target } = event;
+
+ if (target == this.boxModelContainer) {
+ this.boxModelMain.onKeyDown(event);
+ }
+ }
+
+ render() {
+ const {
+ boxModel,
+ dispatch,
+ onShowBoxModelEditor,
+ onShowRulePreviewTooltip,
+ onToggleGeometryEditor,
+ setSelectedNode,
+ showBoxModelProperties,
+ } = this.props;
+
+ return dom.div(
+ {
+ className: "boxmodel-container",
+ tabIndex: 0,
+ ref: div => {
+ this.boxModelContainer = div;
+ },
+ onKeyDown: this.onKeyDown,
+ },
+ BoxModelMain({
+ boxModel,
+ boxModelContainer: this.boxModelContainer,
+ dispatch,
+ onShowBoxModelEditor,
+ onShowRulePreviewTooltip,
+ ref: boxModelMain => {
+ this.boxModelMain = boxModelMain;
+ },
+ }),
+ BoxModelInfo({
+ boxModel,
+ onToggleGeometryEditor,
+ }),
+ showBoxModelProperties
+ ? BoxModelProperties({
+ boxModel,
+ dispatch,
+ setSelectedNode,
+ })
+ : null
+ );
+ }
+}
+
+module.exports = BoxModel;
diff --git a/devtools/client/inspector/boxmodel/components/BoxModelEditable.js b/devtools/client/inspector/boxmodel/components/BoxModelEditable.js
new file mode 100644
index 0000000000..c60a3da6be
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/components/BoxModelEditable.js
@@ -0,0 +1,109 @@
+/* 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 {
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ editableItem,
+} = require("resource://devtools/client/shared/inplace-editor.js");
+
+const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
+const SHARED_STRINGS_URI = "devtools/client/locales/shared.properties";
+const SHARED_L10N = new LocalizationHelper(SHARED_STRINGS_URI);
+
+const LONG_TEXT_ROTATE_LIMIT = 3;
+const HIGHLIGHT_RULE_PREF = Services.prefs.getBoolPref(
+ "devtools.layout.boxmodel.highlightProperty"
+);
+
+class BoxModelEditable extends PureComponent {
+ static get propTypes() {
+ return {
+ box: PropTypes.string.isRequired,
+ direction: PropTypes.string,
+ focusable: PropTypes.bool.isRequired,
+ level: PropTypes.string,
+ onShowBoxModelEditor: PropTypes.func.isRequired,
+ onShowRulePreviewTooltip: PropTypes.func.isRequired,
+ property: PropTypes.string.isRequired,
+ textContent: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
+ .isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.onMouseOver = this.onMouseOver.bind(this);
+ }
+
+ componentDidMount() {
+ const { property, onShowBoxModelEditor } = this.props;
+
+ editableItem(
+ {
+ element: this.boxModelEditable,
+ },
+ (element, event) => {
+ onShowBoxModelEditor(element, event, property);
+ }
+ );
+ }
+
+ onMouseOver(event) {
+ const { onShowRulePreviewTooltip, property } = this.props;
+
+ if (event.shiftKey && HIGHLIGHT_RULE_PREF) {
+ onShowRulePreviewTooltip(event.target, property);
+ }
+ }
+
+ render() {
+ const { box, direction, focusable, level, property, textContent } =
+ this.props;
+
+ const rotate =
+ direction &&
+ (direction == "left" || direction == "right") &&
+ box !== "position" &&
+ textContent.toString().length > LONG_TEXT_ROTATE_LIMIT;
+
+ return dom.p(
+ {
+ className: `boxmodel-${box}
+ ${
+ direction
+ ? " boxmodel-" + direction
+ : "boxmodel-" + property
+ }
+ ${rotate ? " boxmodel-rotate" : ""}`,
+ id: property + "-id",
+ },
+ dom.span(
+ {
+ className: "boxmodel-editable",
+ "aria-label": SHARED_L10N.getFormatStr(
+ "boxModelEditable.accessibleLabel",
+ property,
+ textContent
+ ),
+ "data-box": box,
+ tabIndex: box === level && focusable ? 0 : -1,
+ title: property,
+ onMouseOver: this.onMouseOver,
+ ref: span => {
+ this.boxModelEditable = span;
+ },
+ },
+ textContent
+ )
+ );
+ }
+}
+
+module.exports = BoxModelEditable;
diff --git a/devtools/client/inspector/boxmodel/components/BoxModelInfo.js b/devtools/client/inspector/boxmodel/components/BoxModelInfo.js
new file mode 100644
index 0000000000..e64faba05a
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/components/BoxModelInfo.js
@@ -0,0 +1,79 @@
+/* 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 {
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
+
+const Types = require("resource://devtools/client/inspector/boxmodel/types.js");
+
+const BOXMODEL_STRINGS_URI = "devtools/client/locales/boxmodel.properties";
+const BOXMODEL_L10N = new LocalizationHelper(BOXMODEL_STRINGS_URI);
+
+const SHARED_STRINGS_URI = "devtools/client/locales/shared.properties";
+const SHARED_L10N = new LocalizationHelper(SHARED_STRINGS_URI);
+
+class BoxModelInfo extends PureComponent {
+ static get propTypes() {
+ return {
+ boxModel: PropTypes.shape(Types.boxModel).isRequired,
+ onToggleGeometryEditor: PropTypes.func.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.onToggleGeometryEditor = this.onToggleGeometryEditor.bind(this);
+ }
+
+ onToggleGeometryEditor(e) {
+ this.props.onToggleGeometryEditor();
+ }
+
+ render() {
+ const { boxModel } = this.props;
+ const { geometryEditorEnabled, layout } = boxModel;
+ const { height = "-", isPositionEditable, position, width = "-" } = layout;
+
+ let buttonClass = "layout-geometry-editor devtools-button";
+ if (geometryEditorEnabled) {
+ buttonClass += " checked";
+ }
+
+ return dom.div(
+ {
+ className: "boxmodel-info",
+ role: "region",
+ "aria-label": SHARED_L10N.getFormatStr(
+ "boxModelInfo.accessibleLabel",
+ width,
+ height,
+ position
+ ),
+ },
+ dom.span(
+ { className: "boxmodel-element-size" },
+ SHARED_L10N.getFormatStr("dimensions", width, height)
+ ),
+ dom.section(
+ { className: "boxmodel-position-group" },
+ isPositionEditable
+ ? dom.button({
+ className: buttonClass,
+ title: BOXMODEL_L10N.getStr("boxmodel.geometryButton.tooltip"),
+ onClick: this.onToggleGeometryEditor,
+ })
+ : null,
+ dom.span({ className: "boxmodel-element-position" }, position)
+ )
+ );
+ }
+}
+
+module.exports = BoxModelInfo;
diff --git a/devtools/client/inspector/boxmodel/components/BoxModelMain.js b/devtools/client/inspector/boxmodel/components/BoxModelMain.js
new file mode 100644
index 0000000000..e7065f797a
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/components/BoxModelMain.js
@@ -0,0 +1,774 @@
+/* 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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const { KeyCodes } = require("resource://devtools/client/shared/keycodes.js");
+const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
+
+const BoxModelEditable = createFactory(
+ require("resource://devtools/client/inspector/boxmodel/components/BoxModelEditable.js")
+);
+
+const Types = require("resource://devtools/client/inspector/boxmodel/types.js");
+
+const {
+ highlightSelectedNode,
+ unhighlightNode,
+} = require("resource://devtools/client/inspector/boxmodel/actions/box-model-highlighter.js");
+
+const SHARED_STRINGS_URI = "devtools/client/locales/shared.properties";
+const SHARED_L10N = new LocalizationHelper(SHARED_STRINGS_URI);
+
+class BoxModelMain extends PureComponent {
+ static get propTypes() {
+ return {
+ boxModel: PropTypes.shape(Types.boxModel).isRequired,
+ boxModelContainer: PropTypes.object,
+ dispatch: PropTypes.func.isRequired,
+ onShowBoxModelEditor: PropTypes.func.isRequired,
+ onShowRulePreviewTooltip: PropTypes.func.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ activeDescendant: null,
+ focusable: false,
+ };
+
+ this.getActiveDescendant = this.getActiveDescendant.bind(this);
+ this.getBorderOrPaddingValue = this.getBorderOrPaddingValue.bind(this);
+ this.getContextBox = this.getContextBox.bind(this);
+ this.getDisplayPosition = this.getDisplayPosition.bind(this);
+ this.getHeightValue = this.getHeightValue.bind(this);
+ this.getMarginValue = this.getMarginValue.bind(this);
+ this.getPositionValue = this.getPositionValue.bind(this);
+ this.getWidthValue = this.getWidthValue.bind(this);
+ this.moveFocus = this.moveFocus.bind(this);
+ this.onHighlightMouseOver = this.onHighlightMouseOver.bind(this);
+ this.onKeyDown = this.onKeyDown.bind(this);
+ this.onLevelClick = this.onLevelClick.bind(this);
+ this.setActive = this.setActive.bind(this);
+ }
+
+ componentDidUpdate() {
+ const displayPosition = this.getDisplayPosition();
+ const isContentBox = this.getContextBox();
+
+ this.layouts = {
+ position: new Map([
+ [KeyCodes.DOM_VK_ESCAPE, this.positionLayout],
+ [KeyCodes.DOM_VK_DOWN, this.marginLayout],
+ [KeyCodes.DOM_VK_RETURN, this.positionEditable],
+ [KeyCodes.DOM_VK_UP, null],
+ ["click", this.positionLayout],
+ ]),
+ margin: new Map([
+ [KeyCodes.DOM_VK_ESCAPE, this.marginLayout],
+ [KeyCodes.DOM_VK_DOWN, this.borderLayout],
+ [KeyCodes.DOM_VK_RETURN, this.marginEditable],
+ [KeyCodes.DOM_VK_UP, displayPosition ? this.positionLayout : null],
+ ["click", this.marginLayout],
+ ]),
+ border: new Map([
+ [KeyCodes.DOM_VK_ESCAPE, this.borderLayout],
+ [KeyCodes.DOM_VK_DOWN, this.paddingLayout],
+ [KeyCodes.DOM_VK_RETURN, this.borderEditable],
+ [KeyCodes.DOM_VK_UP, this.marginLayout],
+ ["click", this.borderLayout],
+ ]),
+ padding: new Map([
+ [KeyCodes.DOM_VK_ESCAPE, this.paddingLayout],
+ [KeyCodes.DOM_VK_DOWN, isContentBox ? this.contentLayout : null],
+ [KeyCodes.DOM_VK_RETURN, this.paddingEditable],
+ [KeyCodes.DOM_VK_UP, this.borderLayout],
+ ["click", this.paddingLayout],
+ ]),
+ content: new Map([
+ [KeyCodes.DOM_VK_ESCAPE, this.contentLayout],
+ [KeyCodes.DOM_VK_DOWN, null],
+ [KeyCodes.DOM_VK_RETURN, this.contentEditable],
+ [KeyCodes.DOM_VK_UP, this.paddingLayout],
+ ["click", this.contentLayout],
+ ]),
+ };
+ }
+
+ getActiveDescendant() {
+ let { activeDescendant } = this.state;
+
+ if (!activeDescendant) {
+ const displayPosition = this.getDisplayPosition();
+ const nextLayout = displayPosition
+ ? this.positionLayout
+ : this.marginLayout;
+ activeDescendant = nextLayout.getAttribute("data-box");
+ this.setActive(nextLayout);
+ }
+
+ return activeDescendant;
+ }
+
+ getBorderOrPaddingValue(property) {
+ const { layout } = this.props.boxModel;
+ return layout[property] ? parseFloat(layout[property]) : "-";
+ }
+
+ /**
+ * Returns true if the layout box sizing is context box and false otherwise.
+ */
+ getContextBox() {
+ const { layout } = this.props.boxModel;
+ return layout["box-sizing"] == "content-box";
+ }
+
+ /**
+ * Returns true if the position is displayed and false otherwise.
+ */
+ getDisplayPosition() {
+ const { layout } = this.props.boxModel;
+ return layout.position && layout.position != "static";
+ }
+
+ getHeightValue(property) {
+ if (property == undefined) {
+ return "-";
+ }
+
+ const { layout } = this.props.boxModel;
+
+ property -=
+ parseFloat(layout["border-top-width"]) +
+ parseFloat(layout["border-bottom-width"]) +
+ parseFloat(layout["padding-top"]) +
+ parseFloat(layout["padding-bottom"]);
+ property = parseFloat(property.toPrecision(6));
+
+ return property;
+ }
+
+ getMarginValue(property, direction) {
+ const { layout } = this.props.boxModel;
+ const autoMargins = layout.autoMargins || {};
+ let value = "-";
+
+ if (direction in autoMargins) {
+ value = autoMargins[direction];
+ } else if (layout[property]) {
+ const parsedValue = parseFloat(layout[property]);
+
+ if (Number.isNaN(parsedValue)) {
+ // Not a number. We use the raw string.
+ // Useful for pseudo-elements with auto margins since they
+ // don't appear in autoMargins.
+ value = layout[property];
+ } else {
+ value = parsedValue;
+ }
+ }
+
+ return value;
+ }
+
+ getPositionValue(property) {
+ const { layout } = this.props.boxModel;
+ let value = "-";
+
+ if (!layout[property]) {
+ return value;
+ }
+
+ const parsedValue = parseFloat(layout[property]);
+
+ if (Number.isNaN(parsedValue)) {
+ // Not a number. We use the raw string.
+ value = layout[property];
+ } else {
+ value = parsedValue;
+ }
+
+ return value;
+ }
+
+ getWidthValue(property) {
+ if (property == undefined) {
+ return "-";
+ }
+
+ const { layout } = this.props.boxModel;
+
+ property -=
+ parseFloat(layout["border-left-width"]) +
+ parseFloat(layout["border-right-width"]) +
+ parseFloat(layout["padding-left"]) +
+ parseFloat(layout["padding-right"]);
+ property = parseFloat(property.toPrecision(6));
+
+ return property;
+ }
+
+ /**
+ * Move the focus to the next/previous editable element of the current layout.
+ *
+ * @param {Element} target
+ * Node to be observed
+ * @param {Boolean} shiftKey
+ * Determines if shiftKey was pressed
+ */
+ moveFocus({ target, shiftKey }) {
+ const editBoxes = [
+ ...this.positionLayout.querySelectorAll("[data-box].boxmodel-editable"),
+ ];
+ const editingMode = target.tagName === "input";
+ // target.nextSibling is input field
+ let position = editingMode
+ ? editBoxes.indexOf(target.nextSibling)
+ : editBoxes.indexOf(target);
+
+ if (position === editBoxes.length - 1 && !shiftKey) {
+ position = 0;
+ } else if (position === 0 && shiftKey) {
+ position = editBoxes.length - 1;
+ } else {
+ shiftKey ? position-- : position++;
+ }
+
+ const editBox = editBoxes[position];
+ this.setActive(editBox);
+ editBox.focus();
+
+ if (editingMode) {
+ editBox.click();
+ }
+ }
+
+ /**
+ * Active level set to current layout.
+ *
+ * @param {Element} nextLayout
+ * Element of next layout that user has navigated to
+ */
+ setActive(nextLayout) {
+ const { boxModelContainer } = this.props;
+
+ // We set this attribute for testing purposes.
+ if (boxModelContainer) {
+ boxModelContainer.dataset.activeDescendantClassName =
+ nextLayout.className;
+ }
+
+ this.setState({
+ activeDescendant: nextLayout.getAttribute("data-box"),
+ });
+ }
+
+ onHighlightMouseOver(event) {
+ let region = event.target.getAttribute("data-box");
+
+ if (!region) {
+ let el = event.target;
+
+ do {
+ el = el.parentNode;
+
+ if (el && el.getAttribute("data-box")) {
+ region = el.getAttribute("data-box");
+ break;
+ }
+ } while (el.parentNode);
+
+ this.props.dispatch(unhighlightNode());
+ }
+
+ this.props.dispatch(
+ highlightSelectedNode({
+ region,
+ showOnly: region,
+ onlyRegionArea: true,
+ })
+ );
+
+ event.preventDefault();
+ }
+
+ /**
+ * Handle keyboard navigation and focus for box model layouts.
+ *
+ * Updates active layout on arrow key navigation
+ * Focuses next layout's editboxes on enter key
+ * Unfocuses current layout's editboxes when active layout changes
+ * Controls tabbing between editBoxes
+ *
+ * @param {Event} event
+ * The event triggered by a keypress on the box model
+ */
+ onKeyDown(event) {
+ const { target, keyCode } = event;
+ const isEditable = target._editable || target.editor;
+
+ const level = this.getActiveDescendant();
+ const editingMode = target.tagName === "input";
+
+ switch (keyCode) {
+ case KeyCodes.DOM_VK_RETURN:
+ if (!isEditable) {
+ this.setState({ focusable: true }, () => {
+ const editableBox = this.layouts[level].get(keyCode);
+ if (editableBox) {
+ editableBox.boxModelEditable.focus();
+ }
+ });
+ }
+ break;
+ case KeyCodes.DOM_VK_DOWN:
+ case KeyCodes.DOM_VK_UP:
+ if (!editingMode) {
+ event.preventDefault();
+ event.stopPropagation();
+ this.setState({ focusable: false }, () => {
+ const nextLayout = this.layouts[level].get(keyCode);
+
+ if (!nextLayout) {
+ return;
+ }
+
+ this.setActive(nextLayout);
+
+ if (target?._editable) {
+ target.blur();
+ }
+
+ this.props.boxModelContainer.focus();
+ });
+ }
+ break;
+ case KeyCodes.DOM_VK_TAB:
+ if (isEditable) {
+ event.preventDefault();
+ this.moveFocus(event);
+ }
+ break;
+ case KeyCodes.DOM_VK_ESCAPE:
+ if (target._editable) {
+ event.preventDefault();
+ event.stopPropagation();
+ this.setState({ focusable: false }, () => {
+ this.props.boxModelContainer.focus();
+ });
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ /**
+ * Update active on mouse click.
+ *
+ * @param {Event} event
+ * The event triggered by a mouse click on the box model
+ */
+ onLevelClick(event) {
+ const { target } = event;
+ const displayPosition = this.getDisplayPosition();
+ const isContentBox = this.getContextBox();
+
+ // Avoid switching the active descendant to the position or content layout
+ // if those are not editable.
+ if (
+ (!displayPosition && target == this.positionLayout) ||
+ (!isContentBox && target == this.contentLayout)
+ ) {
+ return;
+ }
+
+ const nextLayout =
+ this.layouts[target.getAttribute("data-box")].get("click");
+ this.setActive(nextLayout);
+
+ if (target?._editable) {
+ target.blur();
+ }
+ }
+
+ render() {
+ const {
+ boxModel,
+ dispatch,
+ onShowBoxModelEditor,
+ onShowRulePreviewTooltip,
+ } = this.props;
+ const { layout } = boxModel;
+ let { height, width } = layout;
+ const { activeDescendant: level, focusable } = this.state;
+
+ const borderTop = this.getBorderOrPaddingValue("border-top-width");
+ const borderRight = this.getBorderOrPaddingValue("border-right-width");
+ const borderBottom = this.getBorderOrPaddingValue("border-bottom-width");
+ const borderLeft = this.getBorderOrPaddingValue("border-left-width");
+
+ const paddingTop = this.getBorderOrPaddingValue("padding-top");
+ const paddingRight = this.getBorderOrPaddingValue("padding-right");
+ const paddingBottom = this.getBorderOrPaddingValue("padding-bottom");
+ const paddingLeft = this.getBorderOrPaddingValue("padding-left");
+
+ const displayPosition = this.getDisplayPosition();
+ const positionTop = this.getPositionValue("top");
+ const positionRight = this.getPositionValue("right");
+ const positionBottom = this.getPositionValue("bottom");
+ const positionLeft = this.getPositionValue("left");
+
+ const marginTop = this.getMarginValue("margin-top", "top");
+ const marginRight = this.getMarginValue("margin-right", "right");
+ const marginBottom = this.getMarginValue("margin-bottom", "bottom");
+ const marginLeft = this.getMarginValue("margin-left", "left");
+
+ height = this.getHeightValue(height);
+ width = this.getWidthValue(width);
+
+ const contentBox =
+ layout["box-sizing"] == "content-box"
+ ? dom.div(
+ { className: "boxmodel-size" },
+ BoxModelEditable({
+ box: "content",
+ focusable,
+ level,
+ property: "width",
+ ref: editable => {
+ this.contentEditable = editable;
+ },
+ textContent: width,
+ onShowBoxModelEditor,
+ onShowRulePreviewTooltip,
+ }),
+ dom.span({}, "\u00D7"),
+ BoxModelEditable({
+ box: "content",
+ focusable,
+ level,
+ property: "height",
+ textContent: height,
+ onShowBoxModelEditor,
+ onShowRulePreviewTooltip,
+ })
+ )
+ : dom.p(
+ {
+ className: "boxmodel-size",
+ id: "boxmodel-size-id",
+ },
+ dom.span(
+ { title: "content" },
+ SHARED_L10N.getFormatStr("dimensions", width, height)
+ )
+ );
+
+ return dom.div(
+ {
+ className: "boxmodel-main devtools-monospace",
+ "data-box": "position",
+ ref: div => {
+ this.positionLayout = div;
+ },
+ onClick: this.onLevelClick,
+ onKeyDown: this.onKeyDown,
+ onMouseOver: this.onHighlightMouseOver,
+ onMouseOut: () => dispatch(unhighlightNode()),
+ },
+ displayPosition
+ ? dom.span(
+ {
+ className: "boxmodel-legend",
+ "data-box": "position",
+ title: "position",
+ },
+ "position"
+ )
+ : null,
+ dom.div(
+ { className: "boxmodel-box" },
+ dom.span(
+ {
+ className: "boxmodel-legend",
+ "data-box": "margin",
+ title: "margin",
+ role: "region",
+ "aria-level": "1", // margin, outermost box
+ "aria-owns":
+ "margin-top-id margin-right-id margin-bottom-id margin-left-id margins-div",
+ },
+ "margin"
+ ),
+ dom.div(
+ {
+ className: "boxmodel-margins",
+ id: "margins-div",
+ "data-box": "margin",
+ title: "margin",
+ ref: div => {
+ this.marginLayout = div;
+ },
+ },
+ dom.span(
+ {
+ className: "boxmodel-legend",
+ "data-box": "border",
+ title: "border",
+ role: "region",
+ "aria-level": "2", // margin -> border, second box
+ "aria-owns":
+ "border-top-width-id border-right-width-id border-bottom-width-id border-left-width-id borders-div",
+ },
+ "border"
+ ),
+ dom.div(
+ {
+ className: "boxmodel-borders",
+ id: "borders-div",
+ "data-box": "border",
+ title: "border",
+ ref: div => {
+ this.borderLayout = div;
+ },
+ },
+ dom.span(
+ {
+ className: "boxmodel-legend",
+ "data-box": "padding",
+ title: "padding",
+ role: "region",
+ "aria-level": "3", // margin -> border -> padding
+ "aria-owns":
+ "padding-top-id padding-right-id padding-bottom-id padding-left-id padding-div",
+ },
+ "padding"
+ ),
+ dom.div(
+ {
+ className: "boxmodel-paddings",
+ id: "padding-div",
+ "data-box": "padding",
+ title: "padding",
+ "aria-owns": "boxmodel-contents-id",
+ ref: div => {
+ this.paddingLayout = div;
+ },
+ },
+ dom.div({
+ className: "boxmodel-contents",
+ id: "boxmodel-contents-id",
+ "data-box": "content",
+ title: "content",
+ role: "region",
+ "aria-level": "4", // margin -> border -> padding -> content
+ "aria-label": SHARED_L10N.getFormatStr(
+ "boxModelSize.accessibleLabel",
+ width,
+ height
+ ),
+ "aria-owns": "boxmodel-size-id",
+ ref: div => {
+ this.contentLayout = div;
+ },
+ })
+ )
+ )
+ )
+ ),
+ displayPosition
+ ? BoxModelEditable({
+ box: "position",
+ direction: "top",
+ focusable,
+ level,
+ property: "position-top",
+ ref: editable => {
+ this.positionEditable = editable;
+ },
+ textContent: positionTop,
+ onShowBoxModelEditor,
+ onShowRulePreviewTooltip,
+ })
+ : null,
+ displayPosition
+ ? BoxModelEditable({
+ box: "position",
+ direction: "right",
+ focusable,
+ level,
+ property: "position-right",
+ textContent: positionRight,
+ onShowBoxModelEditor,
+ onShowRulePreviewTooltip,
+ })
+ : null,
+ displayPosition
+ ? BoxModelEditable({
+ box: "position",
+ direction: "bottom",
+ focusable,
+ level,
+ property: "position-bottom",
+ textContent: positionBottom,
+ onShowBoxModelEditor,
+ onShowRulePreviewTooltip,
+ })
+ : null,
+ displayPosition
+ ? BoxModelEditable({
+ box: "position",
+ direction: "left",
+ focusable,
+ level,
+ property: "position-left",
+ textContent: positionLeft,
+ onShowBoxModelEditor,
+ onShowRulePreviewTooltip,
+ })
+ : null,
+ BoxModelEditable({
+ box: "margin",
+ direction: "top",
+ focusable,
+ level,
+ property: "margin-top",
+ ref: editable => {
+ this.marginEditable = editable;
+ },
+ textContent: marginTop,
+ onShowBoxModelEditor,
+ onShowRulePreviewTooltip,
+ }),
+ BoxModelEditable({
+ box: "margin",
+ direction: "right",
+ focusable,
+ level,
+ property: "margin-right",
+ textContent: marginRight,
+ onShowBoxModelEditor,
+ onShowRulePreviewTooltip,
+ }),
+ BoxModelEditable({
+ box: "margin",
+ direction: "bottom",
+ focusable,
+ level,
+ property: "margin-bottom",
+ textContent: marginBottom,
+ onShowBoxModelEditor,
+ onShowRulePreviewTooltip,
+ }),
+ BoxModelEditable({
+ box: "margin",
+ direction: "left",
+ focusable,
+ level,
+ property: "margin-left",
+ textContent: marginLeft,
+ onShowBoxModelEditor,
+ onShowRulePreviewTooltip,
+ }),
+ BoxModelEditable({
+ box: "border",
+ direction: "top",
+ focusable,
+ level,
+ property: "border-top-width",
+ ref: editable => {
+ this.borderEditable = editable;
+ },
+ textContent: borderTop,
+ onShowBoxModelEditor,
+ onShowRulePreviewTooltip,
+ }),
+ BoxModelEditable({
+ box: "border",
+ direction: "right",
+ focusable,
+ level,
+ property: "border-right-width",
+ textContent: borderRight,
+ onShowBoxModelEditor,
+ onShowRulePreviewTooltip,
+ }),
+ BoxModelEditable({
+ box: "border",
+ direction: "bottom",
+ focusable,
+ level,
+ property: "border-bottom-width",
+ textContent: borderBottom,
+ onShowBoxModelEditor,
+ onShowRulePreviewTooltip,
+ }),
+ BoxModelEditable({
+ box: "border",
+ direction: "left",
+ focusable,
+ level,
+ property: "border-left-width",
+ textContent: borderLeft,
+ onShowBoxModelEditor,
+ onShowRulePreviewTooltip,
+ }),
+ BoxModelEditable({
+ box: "padding",
+ direction: "top",
+ focusable,
+ level,
+ property: "padding-top",
+ ref: editable => {
+ this.paddingEditable = editable;
+ },
+ textContent: paddingTop,
+ onShowBoxModelEditor,
+ onShowRulePreviewTooltip,
+ }),
+ BoxModelEditable({
+ box: "padding",
+ direction: "right",
+ focusable,
+ level,
+ property: "padding-right",
+ textContent: paddingRight,
+ onShowBoxModelEditor,
+ onShowRulePreviewTooltip,
+ }),
+ BoxModelEditable({
+ box: "padding",
+ direction: "bottom",
+ focusable,
+ level,
+ property: "padding-bottom",
+ textContent: paddingBottom,
+ onShowBoxModelEditor,
+ onShowRulePreviewTooltip,
+ }),
+ BoxModelEditable({
+ box: "padding",
+ direction: "left",
+ focusable,
+ level,
+ property: "padding-left",
+ textContent: paddingLeft,
+ onShowBoxModelEditor,
+ onShowRulePreviewTooltip,
+ }),
+ contentBox
+ );
+ }
+}
+
+module.exports = BoxModelMain;
diff --git a/devtools/client/inspector/boxmodel/components/BoxModelProperties.js b/devtools/client/inspector/boxmodel/components/BoxModelProperties.js
new file mode 100644
index 0000000000..8b314a46ed
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/components/BoxModelProperties.js
@@ -0,0 +1,142 @@
+/* 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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
+
+const ComputedProperty = createFactory(
+ require("resource://devtools/client/inspector/boxmodel/components/ComputedProperty.js")
+);
+
+const Types = require("resource://devtools/client/inspector/boxmodel/types.js");
+
+const BOXMODEL_STRINGS_URI = "devtools/client/locales/boxmodel.properties";
+const BOXMODEL_L10N = new LocalizationHelper(BOXMODEL_STRINGS_URI);
+
+class BoxModelProperties extends PureComponent {
+ static get propTypes() {
+ return {
+ boxModel: PropTypes.shape(Types.boxModel).isRequired,
+ dispatch: PropTypes.func.isRequired,
+ setSelectedNode: PropTypes.func.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ isOpen: true,
+ };
+
+ this.getReferenceElement = this.getReferenceElement.bind(this);
+ this.onToggleExpander = this.onToggleExpander.bind(this);
+ }
+
+ /**
+ * Various properties can display a reference element. E.g. position displays an offset
+ * parent if its value is other than fixed or static. Or z-index displays a stacking
+ * context, etc.
+ * This returns the right element if there needs to be one, and one was passed in the
+ * props.
+ *
+ * @return {Object} An object with 2 properties:
+ * - referenceElement {NodeFront}
+ * - referenceElementType {String}
+ */
+ getReferenceElement(propertyName) {
+ const value = this.props.boxModel.layout[propertyName];
+
+ if (
+ propertyName === "position" &&
+ value !== "static" &&
+ value !== "fixed" &&
+ this.props.boxModel.offsetParent
+ ) {
+ return {
+ referenceElement: this.props.boxModel.offsetParent,
+ referenceElementType: BOXMODEL_L10N.getStr("boxmodel.offsetParent"),
+ };
+ }
+
+ return {};
+ }
+
+ onToggleExpander(event) {
+ this.setState({
+ isOpen: !this.state.isOpen,
+ });
+ event.stopPropagation();
+ }
+
+ render() {
+ const { boxModel, dispatch, setSelectedNode } = this.props;
+ const { layout } = boxModel;
+
+ const layoutInfo = [
+ "box-sizing",
+ "display",
+ "float",
+ "line-height",
+ "position",
+ "z-index",
+ ];
+
+ const properties = layoutInfo.map(info => {
+ const { referenceElement, referenceElementType } =
+ this.getReferenceElement(info);
+
+ return ComputedProperty({
+ dispatch,
+ key: info,
+ name: info,
+ referenceElement,
+ referenceElementType,
+ setSelectedNode,
+ value: layout[info],
+ });
+ });
+
+ return dom.div(
+ { className: "layout-properties" },
+ dom.div(
+ {
+ className: "layout-properties-header",
+ role: "heading",
+ "aria-level": "3",
+ onDoubleClick: this.onToggleExpander,
+ },
+ dom.span({
+ className: "layout-properties-expander theme-twisty",
+ open: this.state.isOpen,
+ role: "button",
+ "aria-label": BOXMODEL_L10N.getStr(
+ this.state.isOpen
+ ? "boxmodel.propertiesHideLabel"
+ : "boxmodel.propertiesShowLabel"
+ ),
+ onClick: this.onToggleExpander,
+ }),
+ BOXMODEL_L10N.getStr("boxmodel.propertiesLabel")
+ ),
+ dom.div(
+ {
+ className: "layout-properties-wrapper devtools-monospace",
+ hidden: !this.state.isOpen,
+ role: "table",
+ },
+ properties
+ )
+ );
+ }
+}
+
+module.exports = BoxModelProperties;
diff --git a/devtools/client/inspector/boxmodel/components/ComputedProperty.js b/devtools/client/inspector/boxmodel/components/ComputedProperty.js
new file mode 100644
index 0000000000..330ae7512e
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/components/ComputedProperty.js
@@ -0,0 +1,123 @@
+/* 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 {
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
+
+loader.lazyRequireGetter(
+ this,
+ "getNodeRep",
+ "resource://devtools/client/inspector/shared/node-reps.js"
+);
+
+const {
+ highlightNode,
+ unhighlightNode,
+} = require("resource://devtools/client/inspector/boxmodel/actions/box-model-highlighter.js");
+
+const BOXMODEL_STRINGS_URI = "devtools/client/locales/boxmodel.properties";
+const BOXMODEL_L10N = new LocalizationHelper(BOXMODEL_STRINGS_URI);
+
+class ComputedProperty extends PureComponent {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ name: PropTypes.string.isRequired,
+ referenceElement: PropTypes.object,
+ referenceElementType: PropTypes.string,
+ setSelectedNode: PropTypes.func,
+ value: PropTypes.string,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.renderReferenceElementPreview =
+ this.renderReferenceElementPreview.bind(this);
+ }
+
+ renderReferenceElementPreview() {
+ const {
+ dispatch,
+ referenceElement,
+ referenceElementType,
+ setSelectedNode,
+ } = this.props;
+
+ if (!referenceElement) {
+ return null;
+ }
+
+ return dom.div(
+ { className: "reference-element" },
+ dom.span(
+ {
+ className: "reference-element-type",
+ role: "button",
+ title: BOXMODEL_L10N.getStr("boxmodel.offsetParent.title"),
+ },
+ referenceElementType
+ ),
+ getNodeRep(referenceElement, {
+ onInspectIconClick: () =>
+ setSelectedNode(referenceElement, { reason: "box-model" }),
+ onDOMNodeMouseOver: () => dispatch(highlightNode(referenceElement)),
+ onDOMNodeMouseOut: () => dispatch(unhighlightNode()),
+ })
+ );
+ }
+
+ render() {
+ const { name, value } = this.props;
+
+ return dom.div(
+ {
+ className: "computed-property-view",
+ role: "row",
+ "data-property-name": name,
+ ref: container => {
+ this.container = container;
+ },
+ },
+ dom.div(
+ {
+ className: "computed-property-name-container",
+ role: "presentation",
+ },
+ dom.div(
+ {
+ className: "computed-property-name theme-fg-color3",
+ role: "cell",
+ title: name,
+ },
+ name
+ )
+ ),
+ dom.div(
+ {
+ className: "computed-property-value-container",
+ role: "presentation",
+ },
+ dom.div(
+ {
+ className: "computed-property-value theme-fg-color1",
+ dir: "ltr",
+ role: "cell",
+ },
+ value
+ ),
+ this.renderReferenceElementPreview()
+ )
+ );
+ }
+}
+
+module.exports = ComputedProperty;
diff --git a/devtools/client/inspector/boxmodel/components/moz.build b/devtools/client/inspector/boxmodel/components/moz.build
new file mode 100644
index 0000000000..ed57c93eb7
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/components/moz.build
@@ -0,0 +1,14 @@
+# -*- 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(
+ "BoxModel.js",
+ "BoxModelEditable.js",
+ "BoxModelInfo.js",
+ "BoxModelMain.js",
+ "BoxModelProperties.js",
+ "ComputedProperty.js",
+)
diff --git a/devtools/client/inspector/boxmodel/moz.build b/devtools/client/inspector/boxmodel/moz.build
new file mode 100644
index 0000000000..df8e53009e
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/moz.build
@@ -0,0 +1,19 @@
+# -*- 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/.
+
+DIRS += [
+ "actions",
+ "components",
+ "reducers",
+ "utils",
+]
+
+DevToolsModules(
+ "box-model.js",
+ "types.js",
+)
+
+BROWSER_CHROME_MANIFESTS += ["test/browser.toml"]
diff --git a/devtools/client/inspector/boxmodel/reducers/box-model.js b/devtools/client/inspector/boxmodel/reducers/box-model.js
new file mode 100644
index 0000000000..ea3863e56a
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/reducers/box-model.js
@@ -0,0 +1,45 @@
+/* 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 {
+ UPDATE_GEOMETRY_EDITOR_ENABLED,
+ UPDATE_LAYOUT,
+ UPDATE_OFFSET_PARENT,
+} = require("resource://devtools/client/inspector/boxmodel/actions/index.js");
+
+const INITIAL_BOX_MODEL = {
+ geometryEditorEnabled: false,
+ layout: {},
+ offsetParent: null,
+};
+
+const reducers = {
+ [UPDATE_GEOMETRY_EDITOR_ENABLED](boxModel, { enabled }) {
+ return Object.assign({}, boxModel, {
+ geometryEditorEnabled: enabled,
+ });
+ },
+
+ [UPDATE_LAYOUT](boxModel, { layout }) {
+ return Object.assign({}, boxModel, {
+ layout,
+ });
+ },
+
+ [UPDATE_OFFSET_PARENT](boxModel, { offsetParent }) {
+ return Object.assign({}, boxModel, {
+ offsetParent,
+ });
+ },
+};
+
+module.exports = function (boxModel = INITIAL_BOX_MODEL, action) {
+ const reducer = reducers[action.type];
+ if (!reducer) {
+ return boxModel;
+ }
+ return reducer(boxModel, action);
+};
diff --git a/devtools/client/inspector/boxmodel/reducers/moz.build b/devtools/client/inspector/boxmodel/reducers/moz.build
new file mode 100644
index 0000000000..fe216631de
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/reducers/moz.build
@@ -0,0 +1,9 @@
+# -*- 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",
+)
diff --git a/devtools/client/inspector/boxmodel/test/browser.toml b/devtools/client/inspector/boxmodel/test/browser.toml
new file mode 100644
index 0000000000..4da0e53eeb
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/test/browser.toml
@@ -0,0 +1,72 @@
+[DEFAULT]
+tags = "devtools"
+subsuite = "devtools"
+support-files = [
+ "doc_boxmodel_iframe1.html",
+ "doc_boxmodel_iframe2.html",
+ "head.js",
+ "!/devtools/client/inspector/test/head.js",
+ "!/devtools/client/inspector/test/shared-head.js",
+ "!/devtools/client/shared/test/shared-head.js",
+ "!/devtools/client/shared/test/telemetry-test-helpers.js",
+ "!/devtools/client/shared/test/highlighter-test-actor.js",
+]
+
+["browser_boxmodel.js"]
+
+["browser_boxmodel_edit-position-visible-position-change.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_boxmodel_editablemodel.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_boxmodel_editablemodel_allproperties.js"]
+disabled = "too many intermittent failures (bug 1009322)"
+
+["browser_boxmodel_editablemodel_bluronclick.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_boxmodel_editablemodel_border.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_boxmodel_editablemodel_pseudo.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_boxmodel_editablemodel_stylerules.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_boxmodel_guides.js"]
+
+["browser_boxmodel_jump-to-rule-on-hover.js"]
+
+["browser_boxmodel_layout-accordion-state.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_boxmodel_navigation.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_boxmodel_offsetparent.js"]
+
+["browser_boxmodel_positions.js"]
+
+["browser_boxmodel_properties.js"]
+
+["browser_boxmodel_pseudo-element.js"]
+
+["browser_boxmodel_rotate-labels-on-sides.js"]
+
+["browser_boxmodel_show-tooltip-for-unassociated-rule.js"]
+
+["browser_boxmodel_sync.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_boxmodel_tooltips.js"]
+skip-if = ["true"] # Bug 1336198
+
+["browser_boxmodel_update-after-navigation.js"]
+skip-if = ["(os == 'linux' || os == 'win') && bits == 64"] #Bug 1582395
+
+["browser_boxmodel_update-after-reload.js"]
+
+["browser_boxmodel_update-in-iframes.js"]
+disabled = "Bug 1020038 boxmodel-view updates for iframe elements changes"
diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel.js
new file mode 100644
index 0000000000..f5017a5f70
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel.js
@@ -0,0 +1,201 @@
+/* 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 > .boxmodel-width",
+ value: "100",
+ },
+ {
+ selector: ".boxmodel-size > .boxmodel-height",
+ value: "100.117",
+ },
+ {
+ selector: ".boxmodel-position.boxmodel-top > span",
+ value: "42",
+ },
+ {
+ selector: ".boxmodel-position.boxmodel-left > span",
+ value: "42",
+ },
+ {
+ 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 > .boxmodel-width",
+ value: "100",
+ },
+ {
+ selector: ".boxmodel-size > .boxmodel-height",
+ value: "150",
+ },
+ {
+ selector: ".boxmodel-position.boxmodel-top > span",
+ value: "50",
+ },
+ {
+ selector: ".boxmodel-position.boxmodel-left > span",
+ value: "42",
+ },
+ {
+ 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(async function () {
+ const style =
+ "div { position: absolute; top: 42px; left: 42px; " +
+ "height: 100.111px; width: 100px; border: 10px solid black; " +
+ "padding: 20px; margin: 30px auto;}";
+ const html = "<style>" + style + "</style><div></div>";
+
+ await addTab("data:text/html," + encodeURIComponent(html));
+ const { inspector, boxmodel } = await openLayoutView();
+ await selectNode("div", inspector);
+
+ await testInitialValues(inspector, boxmodel);
+ await testChangingValues(inspector, boxmodel);
+});
+
+function testInitialValues(inspector, boxmodel) {
+ info("Test that the initial values of the box model are correct");
+ const doc = boxmodel.document;
+
+ for (let i = 0; i < res1.length; i++) {
+ const elt = doc.querySelector(res1[i].selector);
+ is(
+ elt.textContent,
+ res1[i].value,
+ res1[i].selector + " has the right value."
+ );
+ }
+}
+
+async function testChangingValues(inspector, boxmodel) {
+ info("Test that changing the document updates the box model");
+ const doc = boxmodel.document;
+
+ const onUpdated = waitForUpdate(inspector);
+ await setContentPageElementAttribute(
+ "div",
+ "style",
+ "height:150px;padding-right:50px;top:50px"
+ );
+ await onUpdated;
+
+ for (let i = 0; i < res2.length; i++) {
+ const elt = doc.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/boxmodel/test/browser_boxmodel_edit-position-visible-position-change.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_edit-position-visible-position-change.js
new file mode 100644
index 0000000000..0cae75aaaf
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_edit-position-visible-position-change.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the 'Edit Position' button is still visible after
+// layout is changed.
+// see bug 1398722
+
+const TEST_URI = `
+ <div id="mydiv" style="background:tomato;
+ position:absolute;
+ top:10px;
+ left:10px;
+ width:100px;
+ height:100px">
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, boxmodel } = await openLayoutView();
+
+ await selectNode("#mydiv", inspector);
+ let editPositionButton = boxmodel.document.querySelector(
+ ".layout-geometry-editor"
+ );
+
+ ok(
+ isNodeVisible(editPositionButton),
+ "Edit Position button is visible initially"
+ );
+
+ const positionLeftTextbox = boxmodel.document.querySelector(
+ ".boxmodel-editable[title=position-left]"
+ );
+ ok(isNodeVisible(positionLeftTextbox), "Position-left edit box exists");
+
+ info("Change the value of position-left and submit");
+ const onUpdate = waitForUpdate(inspector);
+ EventUtils.synthesizeMouseAtCenter(
+ positionLeftTextbox,
+ {},
+ boxmodel.document.defaultView
+ );
+ EventUtils.synthesizeKey("8", {}, boxmodel.document.defaultView);
+ EventUtils.synthesizeKey("VK_RETURN", {}, boxmodel.document.defaultView);
+
+ await onUpdate;
+ editPositionButton = boxmodel.document.querySelector(
+ ".layout-geometry-editor"
+ );
+ ok(
+ isNodeVisible(editPositionButton),
+ "Edit Position button is still visible after layout change"
+ );
+});
diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel.js
new file mode 100644
index 0000000000..262a28cc5f
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel.js
@@ -0,0 +1,279 @@
+/* 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(async function () {
+ // Make sure the toolbox is tall enough to have empty space below the
+ // boxmodel-container.
+ await pushPref("devtools.toolbox.footer.height", 500);
+
+ const tab = await addTab("data:text/html," + encodeURIComponent(TEST_URI));
+ const { inspector, boxmodel } = await openLayoutView();
+
+ const browser = tab.linkedBrowser;
+ await testEditingMargins(inspector, boxmodel, browser);
+ await testKeyBindings(inspector, boxmodel, browser);
+ await testEscapeToUndo(inspector, boxmodel, browser);
+ await testDeletingValue(inspector, boxmodel, browser);
+ await testRefocusingOnClick(inspector, boxmodel, browser);
+});
+
+async function testEditingMargins(inspector, boxmodel, browser) {
+ info(
+ "Test that editing margin dynamically updates the document, pressing " +
+ "escape cancels the changes"
+ );
+
+ is(
+ await getStyle(browser, "#div1", "margin-top"),
+ "",
+ "Should be no margin-top on the element."
+ );
+ await selectNode("#div1", inspector);
+
+ const span = boxmodel.document.querySelector(
+ ".boxmodel-margin.boxmodel-top > span"
+ );
+ await waitForElementTextContent(span, "5");
+
+ EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView);
+ const editor = boxmodel.document.querySelector(
+ ".styleinspector-propertyeditor"
+ );
+ ok(editor, "Should have opened the editor.");
+ is(editor.value, "5px", "Should have the right value in the editor.");
+
+ EventUtils.synthesizeKey("3", {}, boxmodel.document.defaultView);
+ await waitForUpdate(inspector);
+
+ is(
+ await getStyle(browser, "#div1", "margin-top"),
+ "3px",
+ "Should have updated the margin."
+ );
+
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, boxmodel.document.defaultView);
+ await waitForUpdate(inspector);
+
+ is(
+ await getStyle(browser, "#div1", "margin-top"),
+ "",
+ "Should be no margin-top on the element."
+ );
+
+ await waitForElementTextContent(span, "5");
+}
+
+async function testKeyBindings(inspector, boxmodel, browser) {
+ info(
+ "Test that arrow keys work correctly and pressing enter commits the " +
+ "changes"
+ );
+
+ is(
+ await getStyle(browser, "#div1", "margin-left"),
+ "",
+ "Should be no margin-top on the element."
+ );
+ await selectNode("#div1", inspector);
+
+ const span = boxmodel.document.querySelector(
+ ".boxmodel-margin.boxmodel-left > span"
+ );
+ is(span.textContent, "10", "Should have the right value in the box model.");
+
+ EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView);
+ const editor = boxmodel.document.querySelector(
+ ".styleinspector-propertyeditor"
+ );
+ ok(editor, "Should have opened the editor.");
+ is(editor.value, "10px", "Should have the right value in the editor.");
+
+ EventUtils.synthesizeKey("VK_UP", {}, boxmodel.document.defaultView);
+ await waitForUpdate(inspector);
+
+ is(editor.value, "11px", "Should have the right value in the editor.");
+ is(
+ await getStyle(browser, "#div1", "margin-left"),
+ "11px",
+ "Should have updated the margin."
+ );
+
+ EventUtils.synthesizeKey("VK_DOWN", {}, boxmodel.document.defaultView);
+ await waitForUpdate(inspector);
+
+ is(editor.value, "10px", "Should have the right value in the editor.");
+ is(
+ await getStyle(browser, "#div1", "margin-left"),
+ "10px",
+ "Should have updated the margin."
+ );
+
+ EventUtils.synthesizeKey(
+ "VK_UP",
+ { shiftKey: true },
+ boxmodel.document.defaultView
+ );
+ await waitForUpdate(inspector);
+
+ is(editor.value, "20px", "Should have the right value in the editor.");
+ is(
+ await getStyle(browser, "#div1", "margin-left"),
+ "20px",
+ "Should have updated the margin."
+ );
+ EventUtils.synthesizeKey("VK_RETURN", {}, boxmodel.document.defaultView);
+
+ is(
+ await getStyle(browser, "#div1", "margin-left"),
+ "20px",
+ "Should be the right margin-top on the element."
+ );
+
+ await waitForElementTextContent(span, "20");
+}
+
+async function testEscapeToUndo(inspector, boxmodel, browser) {
+ info(
+ "Test that deleting the value removes the property but escape undoes " +
+ "that"
+ );
+
+ is(
+ await getStyle(browser, "#div1", "margin-left"),
+ "20px",
+ "Should be the right margin-top on the element."
+ );
+ await selectNode("#div1", inspector);
+
+ const span = boxmodel.document.querySelector(
+ ".boxmodel-margin.boxmodel-left > span"
+ );
+ is(span.textContent, "20", "Should have the right value in the box model.");
+
+ EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView);
+ const editor = boxmodel.document.querySelector(
+ ".styleinspector-propertyeditor"
+ );
+ ok(editor, "Should have opened the editor.");
+ is(editor.value, "20px", "Should have the right value in the editor.");
+
+ EventUtils.synthesizeKey("VK_DELETE", {}, boxmodel.document.defaultView);
+ await waitForUpdate(inspector);
+
+ is(editor.value, "", "Should have the right value in the editor.");
+ is(
+ await getStyle(browser, "#div1", "margin-left"),
+ "",
+ "Should have updated the margin."
+ );
+
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, boxmodel.document.defaultView);
+ await waitForUpdate(inspector);
+
+ is(
+ await getStyle(browser, "#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.");
+}
+
+async function testDeletingValue(inspector, boxmodel, browser) {
+ info("Test that deleting the value removes the property");
+
+ await setStyle(browser, "#div1", "marginRight", "15px");
+ await waitForUpdate(inspector);
+
+ await selectNode("#div1", inspector);
+
+ const span = boxmodel.document.querySelector(
+ ".boxmodel-margin.boxmodel-right > span"
+ );
+ is(span.textContent, "15", "Should have the right value in the box model.");
+
+ EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView);
+ const editor = boxmodel.document.querySelector(
+ ".styleinspector-propertyeditor"
+ );
+ ok(editor, "Should have opened the editor.");
+ is(editor.value, "15px", "Should have the right value in the editor.");
+
+ EventUtils.synthesizeKey("VK_DELETE", {}, boxmodel.document.defaultView);
+ await waitForUpdate(inspector);
+
+ is(editor.value, "", "Should have the right value in the editor.");
+ is(
+ await getStyle(browser, "#div1", "margin-right"),
+ "",
+ "Should have updated the margin."
+ );
+
+ EventUtils.synthesizeKey("VK_RETURN", {}, boxmodel.document.defaultView);
+
+ is(
+ await getStyle(browser, "#div1", "margin-right"),
+ "",
+ "Should be the right margin-top on the element."
+ );
+ await waitForElementTextContent(span, "10");
+}
+
+async function testRefocusingOnClick(inspector, boxmodel, browser) {
+ info("Test that clicking in the editor input does not remove focus");
+
+ await selectNode("#div4", inspector);
+
+ const span = boxmodel.document.querySelector(
+ ".boxmodel-margin.boxmodel-top > span"
+ );
+ is(span.textContent, "1", "Should have the right value in the box model.");
+
+ EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView);
+ const editor = boxmodel.document.querySelector(
+ ".styleinspector-propertyeditor"
+ );
+ ok(editor, "Should have opened the editor.");
+
+ info("Click in the already opened editor input");
+ EventUtils.synthesizeMouseAtCenter(editor, {}, boxmodel.document.defaultView);
+ is(
+ editor,
+ boxmodel.document.activeElement,
+ "Inplace editor input should still have focus."
+ );
+
+ info("Check the input can still be used as expected");
+ EventUtils.synthesizeKey("VK_UP", {}, boxmodel.document.defaultView);
+ await waitForUpdate(inspector);
+
+ is(editor.value, "2px", "Should have the right value in the editor.");
+ is(
+ await getStyle(browser, "#div4", "margin-top"),
+ "2px",
+ "Should have updated the margin."
+ );
+ EventUtils.synthesizeKey("VK_RETURN", {}, boxmodel.document.defaultView);
+
+ is(
+ await getStyle(browser, "#div4", "margin-top"),
+ "2px",
+ "Should be the right margin-top on the element."
+ );
+ await waitForElementTextContent(span, "2");
+}
diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_allproperties.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_allproperties.js
new file mode 100644
index 0000000000..a465d50e4f
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_allproperties.js
@@ -0,0 +1,191 @@
+/* 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(async function () {
+ const tab = await addTab("data:text/html," + encodeURIComponent(TEST_URI));
+ const { inspector, boxmodel } = await openLayoutView();
+
+ const browser = tab.linkedBrowser;
+ await testEditing(inspector, boxmodel, browser);
+ await testEditingAndCanceling(inspector, boxmodel, browser);
+ await testDeleting(inspector, boxmodel, browser);
+ await testDeletingAndCanceling(inspector, boxmodel, browser);
+});
+
+async function testEditing(inspector, boxmodel, browser) {
+ info("When all properties are set on the node editing one should work");
+
+ await setStyle(browser, "#div1", "padding", "5px");
+ await waitForUpdate(inspector);
+
+ await selectNode("#div1", inspector);
+
+ const span = boxmodel.document.querySelector(
+ ".boxmodel-padding.boxmodel-bottom > span"
+ );
+ await waitForElementTextContent(span, "5");
+
+ EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView);
+ const editor = boxmodel.document.querySelector(
+ ".styleinspector-propertyeditor"
+ );
+ ok(editor, "Should have opened the editor.");
+ is(editor.value, "5px", "Should have the right value in the editor.");
+
+ EventUtils.synthesizeKey("7", {}, boxmodel.document.defaultView);
+ await waitForUpdate(inspector);
+
+ is(editor.value, "7", "Should have the right value in the editor.");
+ is(
+ await getStyle(browser, "#div1", "padding-bottom"),
+ "7px",
+ "Should have updated the padding"
+ );
+
+ EventUtils.synthesizeKey("VK_RETURN", {}, boxmodel.document.defaultView);
+
+ is(
+ await getStyle(browser, "#div1", "padding-bottom"),
+ "7px",
+ "Should be the right padding."
+ );
+ await waitForElementTextContent(span, "7");
+}
+
+async function testEditingAndCanceling(inspector, boxmodel, browser) {
+ info(
+ "When all properties are set on the node editing one and then " +
+ "cancelling with ESCAPE should work"
+ );
+
+ await setStyle(browser, "#div1", "padding", "5px");
+ await waitForUpdate(inspector);
+
+ await selectNode("#div1", inspector);
+
+ const span = boxmodel.document.querySelector(
+ ".boxmodel-padding.boxmodel-left > span"
+ );
+ await waitForElementTextContent(span, "5");
+
+ EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView);
+ const editor = boxmodel.document.querySelector(
+ ".styleinspector-propertyeditor"
+ );
+ ok(editor, "Should have opened the editor.");
+ is(editor.value, "5px", "Should have the right value in the editor.");
+
+ EventUtils.synthesizeKey("8", {}, boxmodel.document.defaultView);
+ await waitForUpdate(inspector);
+
+ is(editor.value, "8", "Should have the right value in the editor.");
+ is(
+ await getStyle(browser, "#div1", "padding-left"),
+ "8px",
+ "Should have updated the padding"
+ );
+
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, boxmodel.document.defaultView);
+ await waitForUpdate(inspector);
+
+ is(
+ await getStyle(browser, "#div1", "padding-left"),
+ "5px",
+ "Should be the right padding."
+ );
+ await waitForElementTextContent(span, "5");
+}
+
+async function testDeleting(inspector, boxmodel, browser) {
+ info("When all properties are set on the node deleting one should work");
+
+ await selectNode("#div1", inspector);
+
+ const span = boxmodel.document.querySelector(
+ ".boxmodel-padding.boxmodel-left > span"
+ );
+ await waitForElementTextContent(span, "5");
+
+ EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView);
+ const editor = boxmodel.document.querySelector(
+ ".styleinspector-propertyeditor"
+ );
+ ok(editor, "Should have opened the editor.");
+ is(editor.value, "5px", "Should have the right value in the editor.");
+
+ EventUtils.synthesizeKey("VK_DELETE", {}, boxmodel.document.defaultView);
+ await waitForUpdate(inspector);
+
+ is(editor.value, "", "Should have the right value in the editor.");
+ is(
+ await getStyle(browser, "#div1", "padding-left"),
+ "",
+ "Should have updated the padding"
+ );
+
+ EventUtils.synthesizeKey("VK_RETURN", {}, boxmodel.document.defaultView);
+
+ is(
+ await getStyle(browser, "#div1", "padding-left"),
+ "",
+ "Should be the right padding."
+ );
+ await waitForElementTextContent(span, "3");
+}
+
+async function testDeletingAndCanceling(inspector, boxmodel, browser) {
+ info(
+ "When all properties are set on the node deleting one then cancelling " +
+ "should work"
+ );
+
+ await setStyle(browser, "#div1", "padding", "5px");
+ await waitForUpdate(inspector);
+
+ await selectNode("#div1", inspector);
+
+ const span = boxmodel.document.querySelector(
+ ".boxmodel-padding.boxmodel-left > span"
+ );
+ await waitForElementTextContent(span, "5");
+
+ EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView);
+ const editor = boxmodel.document.querySelector(
+ ".styleinspector-propertyeditor"
+ );
+ ok(editor, "Should have opened the editor.");
+ is(editor.value, "5px", "Should have the right value in the editor.");
+
+ EventUtils.synthesizeKey("VK_DELETE", {}, boxmodel.document.defaultView);
+ await waitForUpdate(inspector);
+
+ is(editor.value, "", "Should have the right value in the editor.");
+ is(
+ await getStyle(browser, "#div1", "padding-left"),
+ "",
+ "Should have updated the padding"
+ );
+
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, boxmodel.document.defaultView);
+ await waitForUpdate(inspector);
+
+ is(
+ await getStyle(browser, "#div1", "padding-left"),
+ "5px",
+ "Should be the right padding."
+ );
+ await waitForElementTextContent(span, "5");
+}
diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_bluronclick.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_bluronclick.js
new file mode 100644
index 0000000000..3e40eda951
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_bluronclick.js
@@ -0,0 +1,97 @@
+/* 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(async function () {
+ // Make sure the toolbox is tall enough to have empty space below the
+ // boxmodel-container.
+ await pushPref("devtools.toolbox.footer.height", 500);
+
+ await addTab("data:text/html," + encodeURIComponent(TEST_URI));
+ const { inspector, boxmodel } = await openLayoutView();
+
+ await selectNode("#div1", inspector);
+ await testClickingOutsideEditor(boxmodel);
+ await testClickingBelowContainer(boxmodel);
+});
+
+async function testClickingOutsideEditor(boxmodel) {
+ info("Test that clicking outside the editor blurs it");
+ const span = boxmodel.document.querySelector(
+ ".boxmodel-margin.boxmodel-top > span"
+ );
+ await waitForElementTextContent(span, "10");
+
+ EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView);
+ const editor = boxmodel.document.querySelector(
+ ".styleinspector-propertyeditor"
+ );
+ ok(editor, "Should have opened the editor.");
+
+ info("Click next to the opened editor input.");
+ const onBlur = once(editor, "blur");
+ const rect = editor.getBoundingClientRect();
+ EventUtils.synthesizeMouse(
+ editor,
+ rect.width + 10,
+ rect.height / 2,
+ {},
+ boxmodel.document.defaultView
+ );
+ await onBlur;
+
+ is(
+ boxmodel.document.querySelector(".styleinspector-propertyeditor"),
+ null,
+ "Inplace editor has been removed."
+ );
+}
+
+async function testClickingBelowContainer(boxmodel) {
+ info("Test that clicking below the box-model container blurs it");
+ const span = boxmodel.document.querySelector(
+ ".boxmodel-margin.boxmodel-top > span"
+ );
+ await waitForElementTextContent(span, "10");
+
+ info(
+ "Test that clicking below the boxmodel-container blurs the opened editor"
+ );
+ EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView);
+ const editor = boxmodel.document.querySelector(
+ ".styleinspector-propertyeditor"
+ );
+ ok(editor, "Should have opened the editor.");
+
+ const onBlur = once(editor, "blur");
+ const container = boxmodel.document.querySelector(".boxmodel-container");
+ // Using getBoxQuads here because getBoundingClientRect (and therefore synthesizeMouse)
+ // use an erroneous height of ~50px for the boxmodel-container.
+ const bounds = container
+ .getBoxQuads({ relativeTo: boxmodel.document })[0]
+ .getBounds();
+ EventUtils.synthesizeMouseAtPoint(
+ bounds.left + 10,
+ bounds.top + bounds.height + 10,
+ {},
+ boxmodel.document.defaultView
+ );
+ await onBlur;
+
+ is(
+ boxmodel.document.querySelector(".styleinspector-propertyeditor"),
+ null,
+ "Inplace editor has been removed."
+ );
+}
diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_border.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_border.js
new file mode 100644
index 0000000000..98372cabbc
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_border.js
@@ -0,0 +1,75 @@
+/* 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(async function () {
+ const tab = await addTab("data:text/html," + encodeURIComponent(TEST_URI));
+ const { inspector, boxmodel } = await openLayoutView();
+
+ const browser = tab.linkedBrowser;
+ is(
+ await getStyle(browser, "#div1", "border-top-width"),
+ "",
+ "Should have the right border"
+ );
+ is(
+ await getStyle(browser, "#div1", "border-top-style"),
+ "",
+ "Should have the right border"
+ );
+ await selectNode("#div1", inspector);
+
+ const span = boxmodel.document.querySelector(
+ ".boxmodel-border.boxmodel-top > span"
+ );
+ await waitForElementTextContent(span, "0");
+
+ EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView);
+ const editor = boxmodel.document.querySelector(
+ ".styleinspector-propertyeditor"
+ );
+ ok(editor, "Should have opened the editor.");
+ is(editor.value, "0", "Should have the right value in the editor.");
+
+ EventUtils.synthesizeKey("1", {}, boxmodel.document.defaultView);
+ await waitForUpdate(inspector);
+
+ is(editor.value, "1", "Should have the right value in the editor.");
+ is(
+ await getStyle(browser, "#div1", "border-top-width"),
+ "1px",
+ "Should have the right border"
+ );
+ is(
+ await getStyle(browser, "#div1", "border-top-style"),
+ "solid",
+ "Should have the right border"
+ );
+
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, boxmodel.document.defaultView);
+ await waitForUpdate(inspector);
+
+ is(
+ await getStyle(browser, "#div1", "border-top-width"),
+ "",
+ "Should be the right padding."
+ );
+ is(
+ await getStyle(browser, "#div1", "border-top-style"),
+ "",
+ "Should have the right border"
+ );
+ await waitForElementTextContent(span, "0");
+});
diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_pseudo.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_pseudo.js
new file mode 100644
index 0000000000..b2f96fc522
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_pseudo.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that pseudo elements have no side effect on the box model widget for their
+// container. See bug 1350499.
+
+const TEST_URI = `<style>
+ .test::before {
+ content: 'before';
+ margin-top: 5px;
+ padding-top: 5px;
+ width: 5px;
+ }
+ </style>
+ <div style='width:200px;'>
+ <div class=test></div>
+ </div>`;
+
+add_task(async function () {
+ const tab = await addTab("data:text/html," + encodeURIComponent(TEST_URI));
+ const { inspector, boxmodel } = await openLayoutView();
+ const browser = tab.linkedBrowser;
+
+ await selectNode(".test", inspector);
+
+ // No margin-top defined.
+ info("Test that margins are not impacted by a pseudo element");
+ is(
+ await getStyle(browser, ".test", "margin-top"),
+ "",
+ "margin-top is correct"
+ );
+ await checkValueInBoxModel(
+ ".boxmodel-margin.boxmodel-top",
+ "0",
+ boxmodel.document
+ );
+
+ // No padding-top defined.
+ info("Test that paddings are not impacted by a pseudo element");
+ is(
+ await getStyle(browser, ".test", "padding-top"),
+ "",
+ "padding-top is correct"
+ );
+ await checkValueInBoxModel(
+ ".boxmodel-padding.boxmodel-top",
+ "0",
+ boxmodel.document
+ );
+
+ // Width should be driven by the parent div.
+ info("Test that dimensions are not impacted by a pseudo element");
+ is(await getStyle(browser, ".test", "width"), "", "width is correct");
+ await checkValueInBoxModel(
+ ".boxmodel-content.boxmodel-width",
+ "200",
+ boxmodel.document
+ );
+});
+
+async function checkValueInBoxModel(selector, expectedValue, doc) {
+ const span = doc.querySelector(selector + " > span");
+ await waitForElementTextContent(span, expectedValue);
+
+ EventUtils.synthesizeMouseAtCenter(span, {}, doc.defaultView);
+ const editor = doc.querySelector(".styleinspector-propertyeditor");
+ ok(editor, "Should have opened the editor.");
+ is(editor.value, expectedValue, "Should have the right value in the editor.");
+
+ const onBlur = once(editor, "blur");
+ EventUtils.synthesizeKey("VK_RETURN", {}, doc.defaultView);
+ await onBlur;
+}
diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_stylerules.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_stylerules.js
new file mode 100644
index 0000000000..06b2467d1d
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_editablemodel_stylerules.js
@@ -0,0 +1,153 @@
+/* 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(async function () {
+ const tab = await addTab("data:text/html," + encodeURIComponent(TEST_URI));
+ const browser = tab.linkedBrowser;
+ const { inspector, boxmodel } = await openLayoutView();
+
+ await testUnits(inspector, boxmodel, browser);
+ await testValueComesFromStyleRule(inspector, boxmodel, browser);
+ await testShorthandsAreParsed(inspector, boxmodel, browser);
+});
+
+async function testUnits(inspector, boxmodel, browser) {
+ info("Test that entering units works");
+
+ is(
+ await getStyle(browser, "#div1", "padding-top"),
+ "",
+ "Should have the right padding"
+ );
+ await selectNode("#div1", inspector);
+
+ const span = boxmodel.document.querySelector(
+ ".boxmodel-padding.boxmodel-top > span"
+ );
+ await waitForElementTextContent(span, "3");
+
+ EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView);
+ const editor = boxmodel.document.querySelector(
+ ".styleinspector-propertyeditor"
+ );
+ ok(editor, "Should have opened the editor.");
+ is(editor.value, "3px", "Should have the right value in the editor.");
+
+ EventUtils.synthesizeKey("1", {}, boxmodel.document.defaultView);
+ await waitForUpdate(inspector);
+ EventUtils.synthesizeKey("e", {}, boxmodel.document.defaultView);
+ await waitForUpdate(inspector);
+
+ is(
+ await getStyle(browser, "#div1", "padding-top"),
+ "",
+ "An invalid value is handled cleanly"
+ );
+
+ EventUtils.synthesizeKey("m", {}, boxmodel.document.defaultView);
+ await waitForUpdate(inspector);
+
+ is(editor.value, "1em", "Should have the right value in the editor.");
+ is(
+ await getStyle(browser, "#div1", "padding-top"),
+ "1em",
+ "Should have updated the padding."
+ );
+
+ EventUtils.synthesizeKey("VK_RETURN", {}, boxmodel.document.defaultView);
+
+ is(
+ await getStyle(browser, "#div1", "padding-top"),
+ "1em",
+ "Should be the right padding."
+ );
+ await waitForElementTextContent(span, "16");
+}
+
+async function testValueComesFromStyleRule(inspector, boxmodel, browser) {
+ info("Test that we pick up the value from a higher style rule");
+
+ is(
+ await getStyle(browser, "#div2", "border-bottom-width"),
+ "",
+ "Should have the right border-bottom-width"
+ );
+ await selectNode("#div2", inspector);
+
+ const span = boxmodel.document.querySelector(
+ ".boxmodel-border.boxmodel-bottom > span"
+ );
+ await waitForElementTextContent(span, "16");
+
+ EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView);
+ const editor = boxmodel.document.querySelector(
+ ".styleinspector-propertyeditor"
+ );
+ ok(editor, "Should have opened the editor.");
+ is(editor.value, "1em", "Should have the right value in the editor.");
+
+ EventUtils.synthesizeKey("0", {}, boxmodel.document.defaultView);
+ await waitForUpdate(inspector);
+
+ is(editor.value, "0", "Should have the right value in the editor.");
+ is(
+ await getStyle(browser, "#div2", "border-bottom-width"),
+ "0px",
+ "Should have updated the border."
+ );
+
+ EventUtils.synthesizeKey("VK_RETURN", {}, boxmodel.document.defaultView);
+
+ is(
+ await getStyle(browser, "#div2", "border-bottom-width"),
+ "0px",
+ "Should be the right border-bottom-width."
+ );
+ await waitForElementTextContent(span, "0");
+}
+
+async function testShorthandsAreParsed(inspector, boxmodel, browser) {
+ info("Test that shorthand properties are parsed correctly");
+
+ is(
+ await getStyle(browser, "#div3", "padding-right"),
+ "",
+ "Should have the right padding"
+ );
+ await selectNode("#div3", inspector);
+
+ const span = boxmodel.document.querySelector(
+ ".boxmodel-padding.boxmodel-right > span"
+ );
+ await waitForElementTextContent(span, "32");
+
+ EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView);
+ const editor = boxmodel.document.querySelector(
+ ".styleinspector-propertyeditor"
+ );
+ ok(editor, "Should have opened the editor.");
+ is(editor.value, "2em", "Should have the right value in the editor.");
+
+ EventUtils.synthesizeKey("VK_RETURN", {}, boxmodel.document.defaultView);
+
+ is(
+ await getStyle(browser, "#div3", "padding-right"),
+ "",
+ "Should be the right padding."
+ );
+ await waitForElementTextContent(span, "32");
+}
diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_guides.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_guides.js
new file mode 100644
index 0000000000..341b8f525a
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_guides.js
@@ -0,0 +1,69 @@
+/* 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);
+
+add_task(async function () {
+ await addTab(TEST_URL);
+ const { inspector, boxmodel } = await openLayoutView();
+ await selectNode("div", inspector);
+
+ let elt = boxmodel.document.querySelector(".boxmodel-margins");
+ await testGuideOnLayoutHover(elt, "margin", inspector);
+
+ elt = boxmodel.document.querySelector(".boxmodel-borders");
+ await testGuideOnLayoutHover(elt, "border", inspector);
+
+ elt = boxmodel.document.querySelector(".boxmodel-paddings");
+ await testGuideOnLayoutHover(elt, "padding", inspector);
+
+ elt = boxmodel.document.querySelector(".boxmodel-content");
+ await testGuideOnLayoutHover(elt, "content", inspector);
+});
+
+async function testGuideOnLayoutHover(elt, expectedRegion, inspector) {
+ const { waitForHighlighterTypeShown } = getHighlighterTestHelpers(inspector);
+ const onHighlighterShown = waitForHighlighterTypeShown(
+ inspector.highlighters.TYPES.BOXMODEL
+ );
+
+ info("Synthesizing mouseover on the boxmodel-view");
+ EventUtils.synthesizeMouse(
+ elt,
+ 50,
+ 2,
+ { type: "mouseover" },
+ elt.ownerDocument.defaultView
+ );
+
+ info("Waiting for the node-highlight event from the toolbox");
+ const { nodeFront, options } = await onHighlighterShown;
+
+ // Wait for the next event tick to make sure the remaining part of the
+ // test is executed after finishing synthesizing mouse event.
+ await new Promise(executeSoon);
+
+ is(
+ nodeFront,
+ inspector.selection.nodeFront,
+ "The right nodeFront was highlighted"
+ );
+ is(
+ options.region,
+ expectedRegion,
+ "Region " + expectedRegion + " was highlighted"
+ );
+}
diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_jump-to-rule-on-hover.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_jump-to-rule-on-hover.js
new file mode 100644
index 0000000000..74f382259e
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_jump-to-rule-on-hover.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 a box model value will jump to its source CSS rule in the
+// rules view when the shift key is pressed.
+
+const TEST_URI = `<style>
+ #box {
+ margin: 5px;
+ }
+ </style>
+ <div id="box"></div>`;
+
+add_task(async function () {
+ await pushPref("devtools.layout.boxmodel.highlightProperty", true);
+ await addTab("data:text/html," + encodeURIComponent(TEST_URI));
+ const { inspector, boxmodel } = await openLayoutView();
+ await selectNode("#box", inspector);
+
+ info(
+ "Test that hovering over margin-top value highlights the property in rules view."
+ );
+ const ruleView = await inspector.getPanel("ruleview").view;
+ const el = boxmodel.document.querySelector(
+ ".boxmodel-margin.boxmodel-top > span"
+ );
+
+ info("Wait for mouse to hover over margin-top element.");
+ const onHighlightProperty = ruleView.once("scrolled-to-element");
+ EventUtils.synthesizeMouseAtCenter(
+ el,
+ { type: "mousemove", shiftKey: true },
+ boxmodel.document.defaultView
+ );
+ await onHighlightProperty;
+
+ info("Check that margin-top is visible in the rule view.");
+ const { rules, styleWindow } = ruleView;
+ const marginTop = rules[1].textProps[0].computed[0];
+ ok(
+ isInViewport(marginTop.element, styleWindow),
+ "margin-top is visible in the rule view."
+ );
+});
+
+function isInViewport(element, win) {
+ const { top, left, bottom, right } = element.getBoundingClientRect();
+ return (
+ top >= 0 &&
+ bottom <= win.innerHeight &&
+ left >= 0 &&
+ right <= win.innerWidth
+ );
+}
diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_layout-accordion-state.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_layout-accordion-state.js
new file mode 100644
index 0000000000..d6802f4e5d
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_layout-accordion-state.js
@@ -0,0 +1,103 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the box model's accordion state is persistent through hide/show in the
+// layout view.
+
+const TEST_URI = `
+ <style>
+ #div1 {
+ margin: 10px;
+ padding: 3px;
+ }
+ </style>
+ <div id="div1"></div>
+`;
+
+const BOXMODEL_OPENED_PREF = "devtools.layout.boxmodel.opened";
+const ACCORDION_HEADER_SELECTOR = ".accordion-header";
+const ACCORDION_CONTENT_SELECTOR = ".accordion-content";
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, boxmodel, toolbox } = await openLayoutView();
+ const { document: doc } = boxmodel;
+
+ await testAccordionStateAfterClickingHeader(doc);
+ await testAccordionStateAfterSwitchingSidebars(inspector, doc);
+ await testAccordionStateAfterReopeningLayoutView(toolbox);
+
+ Services.prefs.clearUserPref(BOXMODEL_OPENED_PREF);
+});
+
+function testAccordionStateAfterClickingHeader(doc) {
+ const item = doc.querySelector("#layout-section-boxmodel");
+ const header = item.querySelector(ACCORDION_HEADER_SELECTOR);
+ const content = item.querySelector(ACCORDION_CONTENT_SELECTOR);
+
+ info("Checking initial state of the box model panel.");
+ ok(
+ !content.hidden && content.childElementCount > 0,
+ "The box model panel content is visible."
+ );
+ ok(
+ Services.prefs.getBoolPref(BOXMODEL_OPENED_PREF),
+ `${BOXMODEL_OPENED_PREF} is pref on by default.`
+ );
+
+ info("Clicking the box model header to hide the box model panel.");
+ header.click();
+
+ info("Checking the new state of the box model panel.");
+ ok(content.hidden, "The box model panel content is hidden.");
+ ok(
+ !Services.prefs.getBoolPref(BOXMODEL_OPENED_PREF),
+ `${BOXMODEL_OPENED_PREF} is pref off.`
+ );
+}
+
+function testAccordionStateAfterSwitchingSidebars(inspector, doc) {
+ info(
+ "Checking the box model accordion state is persistent after switching sidebars."
+ );
+
+ info("Selecting the computed view.");
+ inspector.sidebar.select("computedview");
+
+ info("Selecting the layout view.");
+ inspector.sidebar.select("layoutview");
+
+ info("Checking the state of the box model panel.");
+ const item = doc.querySelector("#layout-section-boxmodel");
+ const content = item.querySelector(ACCORDION_CONTENT_SELECTOR);
+
+ ok(content.hidden, "The box model panel content is hidden.");
+ ok(
+ !Services.prefs.getBoolPref(BOXMODEL_OPENED_PREF),
+ `${BOXMODEL_OPENED_PREF} is pref off.`
+ );
+}
+
+async function testAccordionStateAfterReopeningLayoutView(toolbox) {
+ info(
+ "Checking the box model accordion state is persistent after closing and " +
+ "re-opening the layout view."
+ );
+
+ info("Closing the toolbox.");
+ await toolbox.destroy();
+
+ info("Re-opening the layout view.");
+ const { boxmodel } = await openLayoutView();
+ const item = boxmodel.document.querySelector("#layout-section-boxmodel");
+ const content = item.querySelector(ACCORDION_CONTENT_SELECTOR);
+
+ info("Checking the state of the box model panel.");
+ ok(content.hidden, "The box model panel content is hidden.");
+ ok(
+ !Services.prefs.getBoolPref(BOXMODEL_OPENED_PREF),
+ `${BOXMODEL_OPENED_PREF} is pref off.`
+ );
+}
diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_navigation.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_navigation.js
new file mode 100644
index 0000000000..7274092a1a
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_navigation.js
@@ -0,0 +1,200 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that keyboard and mouse navigation updates aria-active and focus
+// of elements.
+
+const TEST_URI = `
+ <style>
+ div { position: absolute; top: 42px; left: 42px;
+ height: 100.111px; width: 100px; border: 10px solid black;
+ padding: 20px; margin: 30px auto;}
+ </style><div></div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html," + encodeURIComponent(TEST_URI));
+ const { inspector, boxmodel } = await openLayoutView();
+ await selectNode("div", inspector);
+
+ await testInitialFocus(inspector, boxmodel);
+ await testChangingLevels(inspector, boxmodel);
+ await testTabbingThroughItems(inspector, boxmodel);
+ await testChangingLevelsByClicking(inspector, boxmodel);
+});
+
+function testInitialFocus(inspector, boxmodel) {
+ info("Test that the focus is(on margin layout.");
+ const doc = boxmodel.document;
+ const container = doc.querySelector(".boxmodel-container");
+ container.focus();
+ EventUtils.synthesizeKey("KEY_Enter");
+
+ is(
+ container.dataset.activeDescendantClassName,
+ "boxmodel-main devtools-monospace",
+ "Should be set to the position layout."
+ );
+}
+
+function testChangingLevels(inspector, boxmodel) {
+ info("Test that using arrow keys updates level.");
+ const doc = boxmodel.document;
+ const container = doc.querySelector(".boxmodel-container");
+ container.focus();
+ EventUtils.synthesizeKey("KEY_Enter");
+ EventUtils.synthesizeKey("KEY_Escape");
+
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ is(
+ container.dataset.activeDescendantClassName,
+ "boxmodel-margins",
+ "Should be set to the margin layout."
+ );
+
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ is(
+ container.dataset.activeDescendantClassName,
+ "boxmodel-borders",
+ "Should be set to the border layout."
+ );
+
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ is(
+ container.dataset.activeDescendantClassName,
+ "boxmodel-paddings",
+ "Should be set to the padding layout."
+ );
+
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ is(
+ container.dataset.activeDescendantClassName,
+ "boxmodel-contents",
+ "Should be set to the content layout."
+ );
+
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ is(
+ container.dataset.activeDescendantClassName,
+ "boxmodel-paddings",
+ "Should be set to the padding layout."
+ );
+
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ is(
+ container.dataset.activeDescendantClassName,
+ "boxmodel-borders",
+ "Should be set to the border layout."
+ );
+
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ is(
+ container.dataset.activeDescendantClassName,
+ "boxmodel-margins",
+ "Should be set to the margin layout."
+ );
+
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ is(
+ container.dataset.activeDescendantClassName,
+ "boxmodel-main devtools-monospace",
+ "Should be set to the position layout."
+ );
+}
+
+function testTabbingThroughItems(inspector, boxmodel) {
+ info("Test that using Tab key moves focus to next/previous input field.");
+ const doc = boxmodel.document;
+ const container = doc.querySelector(".boxmodel-container");
+ container.focus();
+ EventUtils.synthesizeKey("KEY_Enter");
+
+ const editBoxes = [...doc.querySelectorAll("[data-box].boxmodel-editable")];
+
+ const editBoxesInfo = [
+ { name: "position-top", itemId: "position-top-id" },
+ { name: "position-right", itemId: "position-right-id" },
+ { name: "position-bottom", itemId: "position-bottom-id" },
+ { name: "position-left", itemId: "position-left-id" },
+ { name: "margin-top", itemId: "margin-top-id" },
+ { name: "margin-right", itemId: "margin-right-id" },
+ { name: "margin-bottom", itemId: "margin-bottom-id" },
+ { name: "margin-left", itemId: "margin-left-id" },
+ { name: "border-top-width", itemId: "border-top-width-id" },
+ { name: "border-right-width", itemId: "border-right-width-id" },
+ { name: "border-bottom-width", itemId: "border-bottom-width-id" },
+ { name: "border-left-width", itemId: "border-left-width-id" },
+ { name: "padding-top", itemId: "padding-top-id" },
+ { name: "padding-right", itemId: "padding-right-id" },
+ { name: "padding-bottom", itemId: "padding-bottom-id" },
+ { name: "padding-left", itemId: "padding-left-id" },
+ { name: "width", itemId: "width-id" },
+ { name: "height", itemId: "height-id" },
+ ];
+
+ // Check whether tabbing through box model items works
+ // Note that the test checks whether wrapping around the box model works
+ // by letting the loop run beyond the number of indexes to start with
+ // the first item again.
+ for (let i = 0; i <= editBoxesInfo.length; i++) {
+ const itemIndex = i % editBoxesInfo.length;
+ const editBoxInfo = editBoxesInfo[itemIndex];
+ is(
+ editBoxes[itemIndex].parentElement.id,
+ editBoxInfo.itemId,
+ `${editBoxInfo.name} item is current`
+ );
+ is(
+ editBoxes[itemIndex].previousElementSibling?.localName,
+ "input",
+ `Input shown for ${editBoxInfo.name} item`
+ );
+
+ // Pressing Tab should not be synthesized for the last item to
+ // wrap to the very last item again when tabbing in reversed order.
+ if (i < editBoxesInfo.length) {
+ EventUtils.synthesizeKey("KEY_Tab");
+ }
+ }
+
+ // Check whether reversed tabbing through box model items works
+ for (let i = editBoxesInfo.length; i >= 0; i--) {
+ const itemIndex = i % editBoxesInfo.length;
+ const editBoxInfo = editBoxesInfo[itemIndex];
+ is(
+ editBoxes[itemIndex].parentElement.id,
+ editBoxInfo.itemId,
+ `${editBoxInfo.name} item is current`
+ );
+ is(
+ editBoxes[itemIndex].previousElementSibling?.localName,
+ "input",
+ `Input shown for ${editBoxInfo.name} item`
+ );
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
+ }
+}
+
+function testChangingLevelsByClicking(inspector, boxmodel) {
+ info("Test that clicking on levels updates level.");
+ const doc = boxmodel.document;
+ const container = doc.querySelector(".boxmodel-container");
+ container.focus();
+
+ const marginLayout = doc.querySelector(".boxmodel-margins");
+ const borderLayout = doc.querySelector(".boxmodel-borders");
+ const paddingLayout = doc.querySelector(".boxmodel-paddings");
+ const contentLayout = doc.querySelector(".boxmodel-contents");
+ const layouts = [contentLayout, paddingLayout, borderLayout, marginLayout];
+
+ layouts.forEach(layout => {
+ layout.click();
+ is(
+ container.dataset.activeDescendantClassName,
+ layout.className,
+ `Should be set to ${layout.getAttribute("data-box")} layout.`
+ );
+ });
+}
diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_offsetparent.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_offsetparent.js
new file mode 100644
index 0000000000..000d548bdd
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_offsetparent.js
@@ -0,0 +1,104 @@
+/* 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 for the offset parent and that it
+// updates when the node's style is changed
+
+const TEST_URI = `
+ <div id="relative_parent" style="position: relative">
+ <div id="absolute_child" style="position: absolute"></div>
+ </div>
+ <div id="static"></div>
+ <div id="no_parent" style="position: absolute"></div>
+ <div id="fixed" style="position: fixed"></div>
+`;
+
+const OFFSET_PARENT_SELECTOR =
+ ".computed-property-value-container .objectBox-node";
+
+const res1 = [
+ {
+ selector: "#absolute_child",
+ offsetParentValue: "div#relative_parent",
+ },
+ {
+ selector: "#no_parent",
+ offsetParentValue: "body",
+ },
+ {
+ selector: "#relative_parent",
+ offsetParentValue: "body",
+ },
+ {
+ selector: "#static",
+ offsetParentValue: null,
+ },
+ {
+ selector: "#fixed",
+ offsetParentValue: null,
+ },
+];
+
+const updates = [
+ {
+ selector: "#absolute_child",
+ update: "position: static",
+ },
+];
+
+const res2 = [
+ {
+ selector: "#absolute_child",
+ offsetParentValue: null,
+ },
+];
+
+add_task(async function () {
+ await addTab("data:text/html," + encodeURIComponent(TEST_URI));
+ const { inspector, boxmodel } = await openLayoutView();
+
+ await testInitialValues(inspector, boxmodel);
+ await testChangingValues(inspector, boxmodel);
+});
+
+async function testInitialValues(inspector, boxmodel) {
+ info(
+ "Test that the initial values of the box model offset parent are correct"
+ );
+ const viewdoc = boxmodel.document;
+
+ for (const { selector, offsetParentValue } of res1) {
+ await selectNode(selector, inspector);
+
+ const elt = viewdoc.querySelector(OFFSET_PARENT_SELECTOR);
+ is(
+ elt && elt.textContent,
+ offsetParentValue,
+ selector + " has the right value."
+ );
+ }
+}
+
+async function testChangingValues(inspector, boxmodel) {
+ info("Test that changing the document updates the box model");
+ const viewdoc = boxmodel.document;
+
+ for (const { selector, update } of updates) {
+ const onUpdated = waitForUpdate(inspector);
+ await setContentPageElementAttribute(selector, "style", update);
+ await onUpdated;
+ }
+
+ for (const { selector, offsetParentValue } of res2) {
+ await selectNode(selector, inspector);
+
+ const elt = viewdoc.querySelector(OFFSET_PARENT_SELECTOR);
+ is(
+ elt && elt.textContent,
+ offsetParentValue,
+ selector + " has the right value after style update."
+ );
+ }
+}
diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_positions.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_positions.js
new file mode 100644
index 0000000000..ca182d7130
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_positions.js
@@ -0,0 +1,67 @@
+/* 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 for positions.
+
+const TEST_URI = `
+ <style type='text/css'>
+ div {
+ position: absolute;
+ left: 0;
+ margin: 0;
+ padding: 0;
+ display: none;
+ height: 100px;
+ width: 100px;
+ border: 10px solid black;
+ }
+ </style>
+ <div>Test Node</div>
+`;
+
+// Expected values:
+const res1 = [
+ {
+ selector: ".boxmodel-position.boxmodel-top > span",
+ value: "auto",
+ },
+ {
+ selector: ".boxmodel-position.boxmodel-right > span",
+ value: "auto",
+ },
+ {
+ selector: ".boxmodel-position.boxmodel-bottom > span",
+ value: "auto",
+ },
+ {
+ selector: ".boxmodel-position.boxmodel-left > span",
+ value: "0",
+ },
+];
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, boxmodel } = await openLayoutView();
+ const node = await getNodeFront("div", inspector);
+ const children = await inspector.markup.walker.children(node);
+ const beforeElement = children.nodes[0];
+
+ await selectNode(beforeElement, inspector);
+ await testPositionValues(inspector, boxmodel);
+});
+
+function testPositionValues(inspector, boxmodel) {
+ info("Test that the position values of the box model are correct");
+ const doc = boxmodel.document;
+
+ for (let i = 0; i < res1.length; i++) {
+ const elt = doc.querySelector(res1[i].selector);
+ is(
+ elt.textContent,
+ res1[i].value,
+ res1[i].selector + " has the right value."
+ );
+ }
+}
diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_properties.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_properties.js
new file mode 100644
index 0000000000..6ebf5ab761
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_properties.js
@@ -0,0 +1,129 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the box model properties list displays the right values
+// and that it updates when the node's style is changed.
+
+const TEST_URI = `
+ <style type='text/css'>
+ div {
+ box-sizing: border-box;
+ display: block;
+ float: left;
+ line-height: 20px;
+ position: relative;
+ z-index: 2;
+ height: 100px;
+ width: 100px;
+ border: 10px solid black;
+ padding: 20px;
+ margin: 30px auto;
+ }
+ </style>
+ <div>Test Node</div>
+`;
+
+const res1 = [
+ {
+ property: "box-sizing",
+ value: "border-box",
+ },
+ {
+ property: "display",
+ value: "block",
+ },
+ {
+ property: "float",
+ value: "left",
+ },
+ {
+ property: "line-height",
+ value: "20px",
+ },
+ {
+ property: "position",
+ value: "relative",
+ },
+ {
+ property: "z-index",
+ value: "2",
+ },
+];
+
+const res2 = [
+ {
+ property: "box-sizing",
+ value: "content-box",
+ },
+ {
+ property: "display",
+ value: "block",
+ },
+ {
+ property: "float",
+ value: "right",
+ },
+ {
+ property: "line-height",
+ value: "10px",
+ },
+ {
+ property: "position",
+ value: "static",
+ },
+ {
+ property: "z-index",
+ value: "5",
+ },
+];
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, boxmodel } = await openLayoutView();
+ await selectNode("div", inspector);
+
+ await testInitialValues(inspector, boxmodel);
+ await testChangingValues(inspector, boxmodel);
+});
+
+function testInitialValues(inspector, boxmodel) {
+ info("Test that the initial values of the box model are correct");
+ const doc = boxmodel.document;
+
+ for (const { property, value } of res1) {
+ const elt = doc.querySelector(getPropertySelector(property));
+ is(elt.textContent, value, property + " has the right value.");
+ }
+}
+
+async function testChangingValues(inspector, boxmodel) {
+ info("Test that changing the document updates the box model");
+ const doc = boxmodel.document;
+
+ const onUpdated = waitForUpdate(inspector);
+ await setContentPageElementAttribute(
+ "div",
+ "style",
+ "box-sizing:content-box;float:right;" +
+ "line-height:10px;position:static;z-index:5;"
+ );
+ await onUpdated;
+
+ for (const { property, value } of res2) {
+ const elt = doc.querySelector(getPropertySelector(property));
+ is(
+ elt.textContent,
+ value,
+ property + " has the right value after style update."
+ );
+ }
+}
+
+function getPropertySelector(propertyName) {
+ return (
+ `.boxmodel-container .computed-property-view` +
+ `[data-property-name=${propertyName}] .computed-property-value`
+ );
+}
diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_pseudo-element.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_pseudo-element.js
new file mode 100644
index 0000000000..046ee067ba
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_pseudo-element.js
@@ -0,0 +1,122 @@
+/* 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 for a pseudo-element.
+
+const TEST_URI = `
+ <style type='text/css'>
+ div {
+ box-sizing: border-box;
+ display: block;
+ float: left;
+ line-height: 20px;
+ position: relative;
+ z-index: 2;
+ height: 100px;
+ width: 100px;
+ border: 10px solid black;
+ padding: 20px;
+ margin: 30px auto;
+ }
+
+ div::before {
+ content: 'before';
+ display: block;
+ width: 32px;
+ height: 32px;
+ margin: 0 auto 6px;
+ }
+ </style>
+ <div>Test Node</div>
+`;
+
+// Expected values:
+const res1 = [
+ {
+ selector: ".boxmodel-element-size",
+ value: "32" + "\u00D7" + "32",
+ },
+ {
+ selector: ".boxmodel-size > .boxmodel-width",
+ value: "32",
+ },
+ {
+ selector: ".boxmodel-size > .boxmodel-height",
+ value: "32",
+ },
+ {
+ selector: ".boxmodel-margin.boxmodel-top > span",
+ value: "0",
+ },
+ {
+ selector: ".boxmodel-margin.boxmodel-left > span",
+ value: "4", // (100 - (10 * 2) - (20 * 2) - 32) / 2
+ },
+ {
+ selector: ".boxmodel-margin.boxmodel-bottom > span",
+ value: "6",
+ },
+ {
+ selector: ".boxmodel-margin.boxmodel-right > span",
+ value: "4", // (100 - (10 * 2) - (20 * 2) - 32) / 2
+ },
+ {
+ selector: ".boxmodel-padding.boxmodel-top > span",
+ value: "0",
+ },
+ {
+ selector: ".boxmodel-padding.boxmodel-left > span",
+ value: "0",
+ },
+ {
+ selector: ".boxmodel-padding.boxmodel-bottom > span",
+ value: "0",
+ },
+ {
+ selector: ".boxmodel-padding.boxmodel-right > span",
+ value: "0",
+ },
+ {
+ selector: ".boxmodel-border.boxmodel-top > span",
+ value: "0",
+ },
+ {
+ selector: ".boxmodel-border.boxmodel-left > span",
+ value: "0",
+ },
+ {
+ selector: ".boxmodel-border.boxmodel-bottom > span",
+ value: "0",
+ },
+ {
+ selector: ".boxmodel-border.boxmodel-right > span",
+ value: "0",
+ },
+];
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, boxmodel } = await openLayoutView();
+ const node = await getNodeFront("div", inspector);
+ const children = await inspector.markup.walker.children(node);
+ const beforeElement = children.nodes[0];
+
+ await selectNode(beforeElement, inspector);
+ await testInitialValues(inspector, boxmodel);
+});
+
+function testInitialValues(inspector, boxmodel) {
+ info("Test that the initial values of the box model are correct");
+ const doc = boxmodel.document;
+
+ for (let i = 0; i < res1.length; i++) {
+ const elt = doc.querySelector(res1[i].selector);
+ is(
+ elt.textContent,
+ res1[i].value,
+ res1[i].selector + " has the right value."
+ );
+ }
+}
diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_rotate-labels-on-sides.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_rotate-labels-on-sides.js
new file mode 100644
index 0000000000..f84ca9d8b0
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_rotate-labels-on-sides.js
@@ -0,0 +1,54 @@
+/* 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(async function () {
+ await addTab("data:text/html," + TEST_URI);
+ const { inspector, boxmodel } = await openLayoutView();
+ await selectNode("div", inspector);
+
+ for (let i = 0; i < res1.length; i++) {
+ const elt = boxmodel.document.querySelector(res1[i].selector);
+ const isLong = elt.textContent.length > LONG_TEXT_ROTATE_LIMIT;
+ const classList = elt.parentNode.classList;
+ const canBeRotated =
+ classList.contains("boxmodel-left") ||
+ classList.contains("boxmodel-right");
+ const isRotated = classList.contains("boxmodel-rotate");
+
+ is(
+ canBeRotated && isLong,
+ isRotated,
+ res1[i].selector + " correctly rotated."
+ );
+ }
+});
diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_show-tooltip-for-unassociated-rule.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_show-tooltip-for-unassociated-rule.js
new file mode 100644
index 0000000000..ffb911b342
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_show-tooltip-for-unassociated-rule.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const L10N = new LocalizationHelper(
+ "devtools/client/locales/inspector.properties"
+);
+
+// Test that hovering over a box model value with no associated rule will show a tooltip
+// saying: "No associated rule".
+
+const TEST_URI = `<style>
+ #box {}
+ </style>
+ <div id="box"></div>`;
+
+add_task(async function () {
+ await pushPref("devtools.layout.boxmodel.highlightProperty", true);
+ await addTab("data:text/html," + encodeURIComponent(TEST_URI));
+ const { inspector, boxmodel } = await openLayoutView();
+ const { rulePreviewTooltip } = boxmodel;
+ await selectNode("#box", inspector);
+
+ info(
+ "Test that hovering over margin-top shows tooltip showing 'No associated rule'."
+ );
+ const el = boxmodel.document.querySelector(
+ ".boxmodel-margin.boxmodel-top > span"
+ );
+
+ info("Wait for mouse to hover over margin-top element.");
+ const onRulePreviewTooltipShown = rulePreviewTooltip._tooltip.once(
+ "shown",
+ () => {
+ ok(true, "Tooltip shown.");
+ is(
+ rulePreviewTooltip.message.textContent,
+ L10N.getStr("rulePreviewTooltip.noAssociatedRule"),
+ `Tooltip shows ${L10N.getStr("rulePreviewTooltip.noAssociatedRule")}`
+ );
+ }
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ el,
+ { type: "mousemove", shiftKey: true },
+ boxmodel.document.defaultView
+ );
+ await onRulePreviewTooltipShown;
+});
diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_sync.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_sync.js
new file mode 100644
index 0000000000..fed1e85519
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_sync.js
@@ -0,0 +1,39 @@
+/* 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(async function () {
+ await addTab("data:text/html," + encodeURIComponent(TEST_URI));
+ const { inspector, boxmodel } = await openLayoutView();
+
+ info("When a property is edited, it should sync in the rule view");
+
+ await selectNode("p", inspector);
+
+ info("Modify padding-bottom in box model view");
+ const span = boxmodel.document.querySelector(
+ ".boxmodel-padding.boxmodel-bottom > span"
+ );
+ EventUtils.synthesizeMouseAtCenter(span, {}, boxmodel.document.defaultView);
+ const editor = boxmodel.document.querySelector(
+ ".styleinspector-propertyeditor"
+ );
+
+ const onRuleViewRefreshed = once(inspector, "rule-view-refreshed");
+ EventUtils.synthesizeKey("7", {}, boxmodel.document.defaultView);
+ await waitForUpdate(inspector);
+ await onRuleViewRefreshed;
+ is(editor.value, "7", "Should have the right value in the editor.");
+ EventUtils.synthesizeKey("VK_RETURN", {}, boxmodel.document.defaultView);
+
+ info("Check that the property was synced with the rule view");
+ const ruleView = selectRuleView(inspector);
+ const ruleEditor = getRuleViewRuleEditor(ruleView, 0);
+ const textProp = ruleEditor.rule.textProps[0];
+ is(textProp.value, "7px", "The property has the right value");
+});
diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_tooltips.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_tooltips.js
new file mode 100644
index 0000000000..de1ab24936
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_tooltips.js
@@ -0,0 +1,166 @@
+/* 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(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, boxmodel } = await openLayoutView();
+
+ info("Checking the regions tooltips");
+
+ ok(
+ boxmodel.document.querySelector(".boxmodel-margins").hasAttribute("title"),
+ "The margin region has a tooltip"
+ );
+ is(
+ boxmodel.document.querySelector(".boxmodel-margins").getAttribute("title"),
+ "margin",
+ "The margin region has the correct tooltip content"
+ );
+
+ ok(
+ boxmodel.document.querySelector(".boxmodel-borders").hasAttribute("title"),
+ "The border region has a tooltip"
+ );
+ is(
+ boxmodel.document.querySelector(".boxmodel-borders").getAttribute("title"),
+ "border",
+ "The border region has the correct tooltip content"
+ );
+
+ ok(
+ boxmodel.document.querySelector(".boxmodel-paddings").hasAttribute("title"),
+ "The padding region has a tooltip"
+ );
+ is(
+ boxmodel.document.querySelector(".boxmodel-paddings").getAttribute("title"),
+ "padding",
+ "The padding region has the correct tooltip content"
+ );
+
+ ok(
+ boxmodel.document.querySelector(".boxmodel-content").hasAttribute("title"),
+ "The content region has a tooltip"
+ );
+ is(
+ boxmodel.document.querySelector(".boxmodel-content").getAttribute("title"),
+ "content",
+ "The content region has the correct tooltip content"
+ );
+
+ for (const { selector, values } of VALUES_TEST_DATA) {
+ info("Selecting " + selector + " and checking the values tooltips");
+ await selectNode(selector, inspector);
+
+ info("Iterate over all values");
+ for (const key in boxmodel.map) {
+ if (key === "position") {
+ continue;
+ }
+
+ const name = boxmodel.map[key].property;
+ const expectedTooltipData = values.find(o => o.name === name);
+ const el = boxmodel.document.querySelector(boxmodel.map[key].selector);
+
+ ok(el.hasAttribute("title"), "The " + name + " value has a tooltip");
+
+ if (expectedTooltipData) {
+ info("The " + name + " value comes from a css rule");
+ const 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/boxmodel/test/browser_boxmodel_update-after-navigation.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_update-after-navigation.js
new file mode 100644
index 0000000000..4cd83590b0
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_update-after-navigation.js
@@ -0,0 +1,96 @@
+/* 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_SSL + "doc_boxmodel_iframe1.html";
+const IFRAME2 = URL_ROOT_SSL + "doc_boxmodel_iframe2.html";
+
+add_task(async function () {
+ const tab = await addTab(IFRAME1);
+ const browser = tab.linkedBrowser;
+ const { inspector, boxmodel } = await openLayoutView();
+
+ await testFirstPage(inspector, boxmodel, browser);
+
+ info("Navigate to the second page");
+ let onMarkupLoaded = waitForMarkupLoaded(inspector);
+ await navigateTo(IFRAME2);
+ await onMarkupLoaded;
+
+ await testSecondPage(inspector, boxmodel, browser);
+
+ info("Go back to the first page");
+ onMarkupLoaded = waitForMarkupLoaded(inspector);
+ gBrowser.goBack();
+ await onMarkupLoaded;
+
+ await testBackToFirstPage(inspector, boxmodel, browser);
+});
+
+async function testFirstPage(inspector, boxmodel, browser) {
+ info("Test that the box model view works on the first page");
+
+ await selectNode("p", inspector);
+
+ info("Checking that the box model view shows the right value");
+ const paddingElt = boxmodel.document.querySelector(
+ ".boxmodel-padding.boxmodel-top > span"
+ );
+ is(paddingElt.textContent, "50");
+
+ info("Listening for box model view changes and modifying the padding");
+ const onUpdated = waitForUpdate(inspector);
+ await setStyle(browser, "p", "padding", "20px");
+ await 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");
+}
+
+async function testSecondPage(inspector, boxmodel, browser) {
+ info("Test that the box model view works on the second page");
+
+ await selectNode("p", inspector);
+
+ info("Checking that the box model view shows the right value");
+ const sizeElt = boxmodel.document.querySelector(".boxmodel-size > span");
+ is(sizeElt.textContent, "100" + "\u00D7" + "100");
+
+ info("Listening for box model view changes and modifying the size");
+ const onUpdated = waitForUpdate(inspector);
+ await setStyle(browser, "p", "width", "200px");
+ await 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");
+}
+
+async function testBackToFirstPage(inspector, boxmodel, browser) {
+ info("Test that the box model view works on the first page after going back");
+
+ await 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"
+ );
+ const paddingElt = boxmodel.document.querySelector(
+ ".boxmodel-padding.boxmodel-top > span"
+ );
+ is(paddingElt.textContent, "20");
+
+ info("Listening for box model view changes and modifying the padding");
+ const onUpdated = waitForUpdate(inspector);
+ await setStyle(browser, "p", "padding", "100px");
+ await 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/boxmodel/test/browser_boxmodel_update-after-reload.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_update-after-reload.js
new file mode 100644
index 0000000000..f6d309c8e2
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_update-after-reload.js
@@ -0,0 +1,42 @@
+/* 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(async function () {
+ const tab = await addTab(URL_ROOT + "doc_boxmodel_iframe1.html");
+ const browser = tab.linkedBrowser;
+ const { inspector, boxmodel } = await openLayoutView();
+
+ info("Test that the box model view works on the first page");
+ await assertBoxModelView(inspector, boxmodel, browser);
+
+ info("Reload the page");
+ const onMarkupLoaded = waitForMarkupLoaded(inspector);
+ await reloadBrowser();
+ await onMarkupLoaded;
+
+ info("Test that the box model view works on the reloaded page");
+ await assertBoxModelView(inspector, boxmodel, browser);
+});
+
+async function assertBoxModelView(inspector, boxmodel, browser) {
+ await selectNode("p", inspector);
+
+ info("Checking that the box model view shows the right value");
+ const paddingElt = boxmodel.document.querySelector(
+ ".boxmodel-padding.boxmodel-top > span"
+ );
+ is(paddingElt.textContent, "50");
+
+ info("Listening for box model view changes and modifying the padding");
+ const onUpdated = waitForUpdate(inspector);
+ await setStyle(browser, "p", "padding", "20px");
+ await 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/boxmodel/test/browser_boxmodel_update-in-iframes.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_update-in-iframes.js
new file mode 100644
index 0000000000..eee72f696c
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_update-in-iframes.js
@@ -0,0 +1,84 @@
+/* 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(async function () {
+ const tab = await addTab(URL_ROOT + "doc_boxmodel_iframe1.html");
+ const browser = tab.linkedBrowser;
+ const { inspector, boxmodel } = await openLayoutView();
+
+ await testResizingInIframe(inspector, boxmodel, browser);
+ await testReflowsAfterIframeDeletion(inspector, boxmodel, browser);
+});
+
+async function testResizingInIframe(inspector, boxmodel, browser) {
+ info("Test that resizing an element in an iframe updates its box model");
+
+ info("Selecting the nested test node");
+ await selectNodeInFrames(["iframe", "iframe", "div"], inspector);
+
+ info("Checking that the box model view shows the right value");
+ const sizeElt = boxmodel.document.querySelector(".boxmodel-size > span");
+ is(sizeElt.textContent, "400\u00D7200");
+
+ info("Listening for box model view changes and modifying its size");
+ const onUpdated = waitForUpdate(inspector);
+ await setStyleInNestedIframe(browser, "div", "width", "200px");
+ await 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");
+}
+
+async function testReflowsAfterIframeDeletion(inspector, boxmodel, browser) {
+ info(
+ "Test reflows are still sent to the box model view after deleting an " +
+ "iframe"
+ );
+
+ info("Deleting the iframe2");
+ const onInspectorUpdated = inspector.once("inspector-updated");
+ await removeNestedIframe(browser);
+ await onInspectorUpdated;
+
+ info("Selecting the test node in iframe1");
+ await selectNodeInFrames(["iframe", "p"], inspector);
+
+ info("Checking that the box model view shows the right value");
+ const sizeElt = boxmodel.document.querySelector(".boxmodel-size > span");
+ is(sizeElt.textContent, "100\u00D7100");
+
+ info("Listening for box model view changes and modifying its size");
+ const onUpdated = waitForUpdate(inspector);
+ await setStyleInIframe(browser, "p", "width", "200px");
+ await 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");
+}
+
+async function setStyleInIframe(browser, selector, propertyName, value) {
+ const context = await getBrowsingContextInFrames(browser, ["iframe"]);
+ return setStyle(context, selector, propertyName, value);
+}
+
+async function setStyleInNestedIframe(browser, selector, propertyName, value) {
+ const context = await getBrowsingContextInFrames(browser, [
+ "iframe",
+ "iframe",
+ ]);
+ return setStyle(context, selector, propertyName, value);
+}
+
+async function removeNestedIframe(browser) {
+ const context = await getBrowsingContextInFrames(browser, ["iframe"]);
+ await SpecialPowers.spawn(context, [], () =>
+ content.document.querySelector("iframe").remove()
+ );
+}
diff --git a/devtools/client/inspector/boxmodel/test/doc_boxmodel_iframe1.html b/devtools/client/inspector/boxmodel/test/doc_boxmodel_iframe1.html
new file mode 100644
index 0000000000..eef48ce079
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/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/boxmodel/test/doc_boxmodel_iframe2.html b/devtools/client/inspector/boxmodel/test/doc_boxmodel_iframe2.html
new file mode 100644
index 0000000000..0fa6dc02e9
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/test/doc_boxmodel_iframe2.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<p style="width:100px;height:100px;background:red;box-sizing:border-box">iframe 1</p>
+<iframe src="data:text/html,<div style='width:400px;height:200px;background:yellow;box-sizing:border-box'>iframe 2</div>"></iframe>
diff --git a/devtools/client/inspector/boxmodel/test/head.js b/devtools/client/inspector/boxmodel/test/head.js
new file mode 100644
index 0000000000..f15424acfb
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/test/head.js
@@ -0,0 +1,122 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+
+"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");
+});
+
+/**
+ * Is the given node visible in the page (rendered in the frame tree).
+ * @param {DOMNode}
+ * @return {Boolean}
+ */
+function isNodeVisible(node) {
+ return !!node.getClientRects().length;
+}
+
+/**
+ * Wait for the boxmodel-view-updated event.
+ *
+ * @param {InspectorPanel} inspector
+ * The instance of InspectorPanel currently loaded in the toolbox.
+ * @param {Boolean} waitForSelectionUpdate
+ * Should the boxmodel-view-updated event come from a new selection.
+ * @return {Promise} a promise
+ */
+async function waitForUpdate(inspector, waitForSelectionUpdate) {
+ /**
+ * While the highlighter is visible (mouse over the fields of the box model editor),
+ * reflow events are prevented; see ReflowActor -> setIgnoreLayoutChanges()
+ * The box model view updates in response to reflow events.
+ * To ensure reflow events are fired, hide the highlighter.
+ */
+ await inspector.highlighters.hideHighlighterType(
+ inspector.highlighters.TYPES.BOXMODEL
+ );
+
+ return new Promise(resolve => {
+ inspector.on("boxmodel-view-updated", function onUpdate(reasons) {
+ // Wait for another update event if we are waiting for a selection related event.
+ if (waitForSelectionUpdate && !reasons.includes("new-selection")) {
+ return;
+ }
+
+ inspector.off("boxmodel-view-updated", onUpdate);
+ resolve();
+ });
+ });
+}
+
+/**
+ * Wait for both boxmode-view-updated and markuploaded events.
+ *
+ * @return {Promise} a promise that resolves when both events have been received.
+ */
+function waitForMarkupLoaded(inspector) {
+ return Promise.all([
+ waitForUpdate(inspector),
+ inspector.once("markuploaded"),
+ ]);
+}
+
+function getStyle(browser, selector, propertyName) {
+ return SpecialPowers.spawn(
+ browser,
+ [selector, propertyName],
+ async function (_selector, _propertyName) {
+ return content.document
+ .querySelector(_selector)
+ .style.getPropertyValue(_propertyName);
+ }
+ );
+}
+
+function setStyle(browser, selector, propertyName, value) {
+ return SpecialPowers.spawn(
+ browser,
+ [selector, propertyName, value],
+ async function (_selector, _propertyName, _value) {
+ content.document.querySelector(_selector).style[_propertyName] = _value;
+ }
+ );
+}
+
+/**
+ * The box model doesn't participate in the inspector's update mechanism, so simply
+ * calling the default selectNode isn't enough to guarantee that the box model view has
+ * finished updating. We also need to wait for the "boxmodel-view-updated" event.
+ */
+var _selectNode = selectNode;
+selectNode = async function (node, inspector, reason) {
+ const onUpdate = waitForUpdate(inspector, true);
+ await _selectNode(node, inspector, reason);
+ await onUpdate;
+};
+
+/**
+ * Wait until the provided element's text content matches the provided text.
+ * Based on the waitFor helper, see documentation in
+ * devtools/client/shared/test/shared-head.js
+ *
+ * @param {DOMNode} element
+ * The element to check.
+ * @param {String} expectedText
+ * The text that is expected to be set as textContent of the element.
+ */
+async function waitForElementTextContent(element, expectedText) {
+ await waitFor(
+ () => element.textContent === expectedText,
+ `Couldn't get "${expectedText}" as the text content of the given element`
+ );
+ ok(true, `Found the expected text (${expectedText}) for the given element`);
+}
diff --git a/devtools/client/inspector/boxmodel/types.js b/devtools/client/inspector/boxmodel/types.js
new file mode 100644
index 0000000000..b477974b65
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/types.js
@@ -0,0 +1,21 @@
+/* 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 PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+/**
+ * The box model data for the current selected node.
+ */
+exports.boxModel = {
+ // Whether or not the geometry editor is enabled
+ geometryEditorEnabled: PropTypes.bool,
+
+ // The layout information of the current selected node
+ layout: PropTypes.object,
+
+ // The offset parent for the selected node
+ offsetParent: PropTypes.object,
+};
diff --git a/devtools/client/inspector/boxmodel/utils/editing-session.js b/devtools/client/inspector/boxmodel/utils/editing-session.js
new file mode 100644
index 0000000000..3175375f22
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/utils/editing-session.js
@@ -0,0 +1,188 @@
+/* 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";
+
+/**
+ * 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 {InspectorPanel} inspector
+ * The inspector panel.
+ * @param {Document} doc
+ * A DOM document that can be used to test style rules.
+ * @param {Array} 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._inspector = inspector;
+ this._rules = elementRules;
+ this._modifications = new Map();
+}
+
+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(rule, property) {
+ // Use the parsed declarations in the StyleRuleFront object if available.
+ const index = this.getPropertyIndex(property, rule);
+ if (index !== -1) {
+ return rule.declarations[index].value;
+ }
+
+ // Fallback to parsing the cssText locally otherwise.
+ const 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 {String} property
+ * The name of the property as a string
+ */
+ getProperty(property) {
+ // Create a hidden element for getPropertyFromRule to use
+ const div = this._doc.createElement("div");
+ div.setAttribute("style", "display: none");
+ this._doc.getElementById("inspector-main-content").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 (const rule of this._rules) {
+ const 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(name, rule = this._rules[0]) {
+ if (!rule.declarations.length) {
+ return -1;
+ }
+
+ return rule.declarations.findIndex(p => p.name === name);
+ },
+
+ /**
+ * Sets a number of properties on the node.
+ *
+ * @param {Array} 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.
+ */
+ async setProperties(properties) {
+ for (const 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.
+ const modifications = this._rules[0].startModifyingProperties(
+ this._inspector.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, "");
+ }
+
+ await modifications.apply();
+ }
+ },
+
+ /**
+ * Reverts all of the property changes made by this instance.
+ *
+ * @return {Promise} Resolves when all properties have been reverted.
+ */
+ async revert() {
+ // Revert each property that we modified previously, one by one. See
+ // setProperties for information about why.
+ for (const [property, value] of this._modifications) {
+ const modifications = this._rules[0].startModifyingProperties(
+ this._inspector.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);
+ }
+
+ await modifications.apply();
+ }
+ },
+
+ destroy() {
+ this._modifications.clear();
+
+ this._cssProperties = null;
+ this._doc = null;
+ this._inspector = null;
+ this._modifications = null;
+ this._rules = null;
+ },
+};
+
+module.exports = EditingSession;
diff --git a/devtools/client/inspector/boxmodel/utils/moz.build b/devtools/client/inspector/boxmodel/utils/moz.build
new file mode 100644
index 0000000000..76a5656294
--- /dev/null
+++ b/devtools/client/inspector/boxmodel/utils/moz.build
@@ -0,0 +1,9 @@
+# -*- 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(
+ "editing-session.js",
+)