summaryrefslogtreecommitdiffstats
path: root/devtools/client/inspector/grids
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/inspector/grids')
-rw-r--r--devtools/client/inspector/grids/actions/grid-highlighter.js39
-rw-r--r--devtools/client/inspector/grids/actions/grids.js55
-rw-r--r--devtools/client/inspector/grids/actions/highlighter-settings.js52
-rw-r--r--devtools/client/inspector/grids/actions/index.js30
-rw-r--r--devtools/client/inspector/grids/actions/moz.build12
-rw-r--r--devtools/client/inspector/grids/components/Grid.js106
-rw-r--r--devtools/client/inspector/grids/components/GridDisplaySettings.js116
-rw-r--r--devtools/client/inspector/grids/components/GridItem.js173
-rw-r--r--devtools/client/inspector/grids/components/GridList.js79
-rw-r--r--devtools/client/inspector/grids/components/GridOutline.js436
-rw-r--r--devtools/client/inspector/grids/components/moz.build13
-rw-r--r--devtools/client/inspector/grids/grid-inspector.js776
-rw-r--r--devtools/client/inspector/grids/moz.build20
-rw-r--r--devtools/client/inspector/grids/reducers/grids.js87
-rw-r--r--devtools/client/inspector/grids/reducers/highlighter-settings.js54
-rw-r--r--devtools/client/inspector/grids/reducers/moz.build10
-rw-r--r--devtools/client/inspector/grids/test/browser.ini49
-rw-r--r--devtools/client/inspector/grids/test/browser_grids_accordion-state.js108
-rw-r--r--devtools/client/inspector/grids/test/browser_grids_color-in-rules-grid-toggle.js93
-rw-r--r--devtools/client/inspector/grids/test/browser_grids_display-setting-extend-grid-lines.js59
-rw-r--r--devtools/client/inspector/grids/test/browser_grids_display-setting-show-grid-areas.js59
-rw-r--r--devtools/client/inspector/grids/test/browser_grids_display-setting-show-grid-line-numbers.js65
-rw-r--r--devtools/client/inspector/grids/test/browser_grids_grid-list-color-picker-on-ESC.js75
-rw-r--r--devtools/client/inspector/grids/test/browser_grids_grid-list-color-picker-on-RETURN.js75
-rw-r--r--devtools/client/inspector/grids/test/browser_grids_grid-list-element-rep.js68
-rw-r--r--devtools/client/inspector/grids/test/browser_grids_grid-list-no-grids.js37
-rw-r--r--devtools/client/inspector/grids/test/browser_grids_grid-list-on-iframe-reloaded.js57
-rw-r--r--devtools/client/inspector/grids/test/browser_grids_grid-list-on-mutation-element-added.js100
-rw-r--r--devtools/client/inspector/grids/test/browser_grids_grid-list-on-mutation-element-removed.js63
-rw-r--r--devtools/client/inspector/grids/test/browser_grids_grid-list-on-target-added-removed.js203
-rw-r--r--devtools/client/inspector/grids/test/browser_grids_grid-list-subgrids-z-order.js83
-rw-r--r--devtools/client/inspector/grids/test/browser_grids_grid-list-subgrids_01.js138
-rw-r--r--devtools/client/inspector/grids/test/browser_grids_grid-list-subgrids_02.js72
-rw-r--r--devtools/client/inspector/grids/test/browser_grids_grid-list-toggle-grids_01.js63
-rw-r--r--devtools/client/inspector/grids/test/browser_grids_grid-list-toggle-grids_02.js96
-rw-r--r--devtools/client/inspector/grids/test/browser_grids_grid-list-toggle-multiple-grids.js243
-rw-r--r--devtools/client/inspector/grids/test/browser_grids_grid-outline-cannot-show-outline.js57
-rw-r--r--devtools/client/inspector/grids/test/browser_grids_grid-outline-highlight-area.js77
-rw-r--r--devtools/client/inspector/grids/test/browser_grids_grid-outline-highlight-cell.js65
-rw-r--r--devtools/client/inspector/grids/test/browser_grids_grid-outline-multiple-grids.js76
-rw-r--r--devtools/client/inspector/grids/test/browser_grids_grid-outline-selected-grid.js51
-rw-r--r--devtools/client/inspector/grids/test/browser_grids_grid-outline-updates-on-grid-change.js64
-rw-r--r--devtools/client/inspector/grids/test/browser_grids_grid-outline-writing-mode.js156
-rw-r--r--devtools/client/inspector/grids/test/browser_grids_highlighter-setting-rules-grid-toggle.js75
-rw-r--r--devtools/client/inspector/grids/test/browser_grids_highlighter-toggle-telemetry.js63
-rw-r--r--devtools/client/inspector/grids/test/browser_grids_number-of-css-grids-telemetry.js56
-rw-r--r--devtools/client/inspector/grids/test/browser_grids_persist-color-palette.js58
-rw-r--r--devtools/client/inspector/grids/test/browser_grids_restored-after-reload.js115
-rw-r--r--devtools/client/inspector/grids/test/browser_grids_restored-multiple-grids-after-reload.js156
-rw-r--r--devtools/client/inspector/grids/test/doc_iframe_reloaded.html9
-rw-r--r--devtools/client/inspector/grids/test/doc_subgrid.html56
-rw-r--r--devtools/client/inspector/grids/test/head.js40
-rw-r--r--devtools/client/inspector/grids/test/xpcshell/.eslintrc.js6
-rw-r--r--devtools/client/inspector/grids/test/xpcshell/head.js10
-rw-r--r--devtools/client/inspector/grids/test/xpcshell/test_compare_fragments_geometry.js129
-rw-r--r--devtools/client/inspector/grids/test/xpcshell/xpcshell.ini6
-rw-r--r--devtools/client/inspector/grids/types.js57
-rw-r--r--devtools/client/inspector/grids/utils/moz.build9
-rw-r--r--devtools/client/inspector/grids/utils/utils.js58
59 files changed, 5313 insertions, 0 deletions
diff --git a/devtools/client/inspector/grids/actions/grid-highlighter.js b/devtools/client/inspector/grids/actions/grid-highlighter.js
new file mode 100644
index 0000000000..6706dc88cd
--- /dev/null
+++ b/devtools/client/inspector/grids/actions/grid-highlighter.js
@@ -0,0 +1,39 @@
+/* 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 grid highlighter for the given node front.
+ *
+ * @param {NodeFront} nodeFront
+ * Node that should be highlighted.
+ * @param {Object} options
+ * Optional configuration options passed to the grid highlighter
+ */
+ showGridHighlighter(nodeFront, options = {}) {
+ return async thunkOptions => {
+ const { inspector } = thunkOptions;
+ if (!inspector) {
+ return;
+ }
+
+ await inspector.highlighters.showGridHighlighter(nodeFront, options);
+ };
+ },
+};
diff --git a/devtools/client/inspector/grids/actions/grids.js b/devtools/client/inspector/grids/actions/grids.js
new file mode 100644
index 0000000000..724582c3d5
--- /dev/null
+++ b/devtools/client/inspector/grids/actions/grids.js
@@ -0,0 +1,55 @@
+/* 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_GRID_COLOR,
+ UPDATE_GRID_HIGHLIGHTED,
+ UPDATE_GRIDS,
+} = require("resource://devtools/client/inspector/grids/actions/index.js");
+
+module.exports = {
+ /**
+ * Updates the color used for the grid's highlighter.
+ *
+ * @param {NodeFront} nodeFront
+ * The NodeFront of the DOM node to toggle the grid highlighter.
+ * @param {String} color
+ * The color to use for this nodeFront's grid highlighter.
+ */
+ updateGridColor(nodeFront, color) {
+ return {
+ type: UPDATE_GRID_COLOR,
+ color,
+ nodeFront,
+ };
+ },
+
+ /**
+ * Updates the grid highlighted state.
+ *
+ * @param {NodeFront} nodeFront
+ * The NodeFront of the DOM node to toggle the grid highlighter.
+ * @param {Boolean} highlighted
+ * Whether or not the grid highlighter is highlighting the grid.
+ */
+ updateGridHighlighted(nodeFront, highlighted) {
+ return {
+ type: UPDATE_GRID_HIGHLIGHTED,
+ highlighted,
+ nodeFront,
+ };
+ },
+
+ /**
+ * Updates the grid state with the new list of grids.
+ */
+ updateGrids(grids) {
+ return {
+ type: UPDATE_GRIDS,
+ grids,
+ };
+ },
+};
diff --git a/devtools/client/inspector/grids/actions/highlighter-settings.js b/devtools/client/inspector/grids/actions/highlighter-settings.js
new file mode 100644
index 0000000000..82397a7944
--- /dev/null
+++ b/devtools/client/inspector/grids/actions/highlighter-settings.js
@@ -0,0 +1,52 @@
+/* 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_SHOW_GRID_AREAS,
+ UPDATE_SHOW_GRID_LINE_NUMBERS,
+ UPDATE_SHOW_INFINITE_LINES,
+} = require("resource://devtools/client/inspector/grids/actions/index.js");
+
+module.exports = {
+ /**
+ * Updates the grid highlighter's show grid areas preference.
+ *
+ * @param {Boolean} enabled
+ * Whether or not the grid highlighter should show the grid areas.
+ */
+ updateShowGridAreas(enabled) {
+ return {
+ type: UPDATE_SHOW_GRID_AREAS,
+ enabled,
+ };
+ },
+
+ /**
+ * Updates the grid highlighter's show grid line numbers preference.
+ *
+ * @param {Boolean} enabled
+ * Whether or not the grid highlighter should show the grid line numbers.
+ */
+ updateShowGridLineNumbers(enabled) {
+ return {
+ type: UPDATE_SHOW_GRID_LINE_NUMBERS,
+ enabled,
+ };
+ },
+
+ /**
+ * Updates the grid highlighter's show infinite lines preference.
+ *
+ * @param {Boolean} enabled
+ * Whether or not the grid highlighter should extend grid lines infinitely.
+ */
+ updateShowInfiniteLines(enabled) {
+ return {
+ type: UPDATE_SHOW_INFINITE_LINES,
+ enabled,
+ };
+ },
+};
diff --git a/devtools/client/inspector/grids/actions/index.js b/devtools/client/inspector/grids/actions/index.js
new file mode 100644
index 0000000000..1b0c18d0e7
--- /dev/null
+++ b/devtools/client/inspector/grids/actions/index.js
@@ -0,0 +1,30 @@
+/* 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 color used for the overlay of a grid.
+ "UPDATE_GRID_COLOR",
+
+ // Updates the grid highlighted state.
+ "UPDATE_GRID_HIGHLIGHTED",
+
+ // Updates the entire grids state with the new list of grids.
+ "UPDATE_GRIDS",
+
+ // Updates the grid highlighter's show grid areas state.
+ "UPDATE_SHOW_GRID_AREAS",
+
+ // Updates the grid highlighter's show grid line numbers state.
+ "UPDATE_SHOW_GRID_LINE_NUMBERS",
+
+ // Updates the grid highlighter's show infinite lines state.
+ "UPDATE_SHOW_INFINITE_LINES",
+ ],
+ module.exports
+);
diff --git a/devtools/client/inspector/grids/actions/moz.build b/devtools/client/inspector/grids/actions/moz.build
new file mode 100644
index 0000000000..733ac57ede
--- /dev/null
+++ b/devtools/client/inspector/grids/actions/moz.build
@@ -0,0 +1,12 @@
+# -*- 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(
+ "grid-highlighter.js",
+ "grids.js",
+ "highlighter-settings.js",
+ "index.js",
+)
diff --git a/devtools/client/inspector/grids/components/Grid.js b/devtools/client/inspector/grids/components/Grid.js
new file mode 100644
index 0000000000..0378f1e702
--- /dev/null
+++ b/devtools/client/inspector/grids/components/Grid.js
@@ -0,0 +1,106 @@
+/* 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 {
+ getStr,
+} = require("resource://devtools/client/inspector/layout/utils/l10n.js");
+
+// Normally, we would only lazy load GridOutline, but we also lazy load
+// GridDisplaySettings and GridList because we assume the CSS grid usage is low
+// and usually will not appear on the page.
+loader.lazyGetter(this, "GridDisplaySettings", function () {
+ return createFactory(
+ require("resource://devtools/client/inspector/grids/components/GridDisplaySettings.js")
+ );
+});
+loader.lazyGetter(this, "GridList", function () {
+ return createFactory(
+ require("resource://devtools/client/inspector/grids/components/GridList.js")
+ );
+});
+loader.lazyGetter(this, "GridOutline", function () {
+ return createFactory(
+ require("resource://devtools/client/inspector/grids/components/GridOutline.js")
+ );
+});
+
+const Types = require("resource://devtools/client/inspector/grids/types.js");
+
+class Grid extends PureComponent {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ getSwatchColorPickerTooltip: PropTypes.func.isRequired,
+ grids: PropTypes.arrayOf(PropTypes.shape(Types.grid)).isRequired,
+ highlighterSettings: PropTypes.shape(Types.highlighterSettings)
+ .isRequired,
+ onSetGridOverlayColor: PropTypes.func.isRequired,
+ onToggleGridHighlighter: PropTypes.func.isRequired,
+ onToggleShowGridAreas: PropTypes.func.isRequired,
+ onToggleShowGridLineNumbers: PropTypes.func.isRequired,
+ onToggleShowInfiniteLines: PropTypes.func.isRequired,
+ setSelectedNode: PropTypes.func.isRequired,
+ };
+ }
+
+ render() {
+ if (!this.props.grids.length) {
+ return dom.div(
+ { className: "devtools-sidepanel-no-result" },
+ getStr("layout.noGridsOnThisPage")
+ );
+ }
+
+ const {
+ dispatch,
+ getSwatchColorPickerTooltip,
+ grids,
+ highlighterSettings,
+ onSetGridOverlayColor,
+ onToggleShowGridAreas,
+ onToggleGridHighlighter,
+ onToggleShowGridLineNumbers,
+ onToggleShowInfiniteLines,
+ setSelectedNode,
+ } = this.props;
+ const highlightedGrids = grids.filter(grid => grid.highlighted);
+
+ return dom.div(
+ { id: "layout-grid-container" },
+ dom.div(
+ { className: "grid-content" },
+ GridList({
+ dispatch,
+ getSwatchColorPickerTooltip,
+ grids,
+ onSetGridOverlayColor,
+ onToggleGridHighlighter,
+ setSelectedNode,
+ }),
+ GridDisplaySettings({
+ highlighterSettings,
+ onToggleShowGridAreas,
+ onToggleShowGridLineNumbers,
+ onToggleShowInfiniteLines,
+ })
+ ),
+ highlightedGrids.length === 1
+ ? GridOutline({
+ dispatch,
+ grids,
+ })
+ : null
+ );
+ }
+}
+
+module.exports = Grid;
diff --git a/devtools/client/inspector/grids/components/GridDisplaySettings.js b/devtools/client/inspector/grids/components/GridDisplaySettings.js
new file mode 100644
index 0000000000..af525cc8c7
--- /dev/null
+++ b/devtools/client/inspector/grids/components/GridDisplaySettings.js
@@ -0,0 +1,116 @@
+/* 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 {
+ getStr,
+} = require("resource://devtools/client/inspector/layout/utils/l10n.js");
+
+const Types = require("resource://devtools/client/inspector/grids/types.js");
+
+class GridDisplaySettings extends PureComponent {
+ static get propTypes() {
+ return {
+ highlighterSettings: PropTypes.shape(Types.highlighterSettings)
+ .isRequired,
+ onToggleShowGridAreas: PropTypes.func.isRequired,
+ onToggleShowGridLineNumbers: PropTypes.func.isRequired,
+ onToggleShowInfiniteLines: PropTypes.func.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.onShowGridAreasCheckboxClick =
+ this.onShowGridAreasCheckboxClick.bind(this);
+ this.onShowGridLineNumbersCheckboxClick =
+ this.onShowGridLineNumbersCheckboxClick.bind(this);
+ this.onShowInfiniteLinesCheckboxClick =
+ this.onShowInfiniteLinesCheckboxClick.bind(this);
+ }
+
+ onShowGridAreasCheckboxClick() {
+ const { highlighterSettings, onToggleShowGridAreas } = this.props;
+
+ onToggleShowGridAreas(!highlighterSettings.showGridAreasOverlay);
+ }
+
+ onShowGridLineNumbersCheckboxClick() {
+ const { highlighterSettings, onToggleShowGridLineNumbers } = this.props;
+
+ onToggleShowGridLineNumbers(!highlighterSettings.showGridLineNumbers);
+ }
+
+ onShowInfiniteLinesCheckboxClick() {
+ const { highlighterSettings, onToggleShowInfiniteLines } = this.props;
+
+ onToggleShowInfiniteLines(!highlighterSettings.showInfiniteLines);
+ }
+
+ render() {
+ const { highlighterSettings } = this.props;
+
+ return dom.div(
+ { className: "grid-container" },
+ dom.span(
+ {
+ role: "heading",
+ "aria-level": "3",
+ },
+ getStr("layout.gridDisplaySettings")
+ ),
+ dom.ul(
+ {},
+ dom.li(
+ { className: "grid-settings-item" },
+ dom.label(
+ {},
+ dom.input({
+ id: "grid-setting-show-grid-line-numbers",
+ type: "checkbox",
+ checked: highlighterSettings.showGridLineNumbers,
+ onChange: this.onShowGridLineNumbersCheckboxClick,
+ }),
+ getStr("layout.displayLineNumbers")
+ )
+ ),
+ dom.li(
+ { className: "grid-settings-item" },
+ dom.label(
+ {},
+ dom.input({
+ id: "grid-setting-show-grid-areas",
+ type: "checkbox",
+ checked: highlighterSettings.showGridAreasOverlay,
+ onChange: this.onShowGridAreasCheckboxClick,
+ }),
+ getStr("layout.displayAreaNames")
+ )
+ ),
+ dom.li(
+ { className: "grid-settings-item" },
+ dom.label(
+ {},
+ dom.input({
+ id: "grid-setting-extend-grid-lines",
+ type: "checkbox",
+ checked: highlighterSettings.showInfiniteLines,
+ onChange: this.onShowInfiniteLinesCheckboxClick,
+ }),
+ getStr("layout.extendLinesInfinitely")
+ )
+ )
+ )
+ );
+ }
+}
+
+module.exports = GridDisplaySettings;
diff --git a/devtools/client/inspector/grids/components/GridItem.js b/devtools/client/inspector/grids/components/GridItem.js
new file mode 100644
index 0000000000..984f57c6a7
--- /dev/null
+++ b/devtools/client/inspector/grids/components/GridItem.js
@@ -0,0 +1,173 @@
+/* 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 {
+ createElement,
+ createRef,
+ Fragment,
+ 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");
+
+loader.lazyGetter(this, "Rep", function () {
+ return require("resource://devtools/client/shared/components/reps/index.js")
+ .REPS.Rep;
+});
+loader.lazyGetter(this, "MODE", function () {
+ return require("resource://devtools/client/shared/components/reps/index.js")
+ .MODE;
+});
+
+loader.lazyRequireGetter(
+ this,
+ "translateNodeFrontToGrip",
+ "resource://devtools/client/inspector/shared/utils.js",
+ true
+);
+
+const Types = require("resource://devtools/client/inspector/grids/types.js");
+
+const {
+ highlightNode,
+ unhighlightNode,
+} = require("resource://devtools/client/inspector/boxmodel/actions/box-model-highlighter.js");
+
+class GridItem extends PureComponent {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ getSwatchColorPickerTooltip: PropTypes.func.isRequired,
+ grid: PropTypes.shape(Types.grid).isRequired,
+ grids: PropTypes.arrayOf(PropTypes.shape(Types.grid)).isRequired,
+ onSetGridOverlayColor: PropTypes.func.isRequired,
+ onToggleGridHighlighter: PropTypes.func.isRequired,
+ setSelectedNode: PropTypes.func.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.swatchEl = createRef();
+
+ this.onGridCheckboxClick = this.onGridCheckboxClick.bind(this);
+ this.onGridInspectIconClick = this.onGridInspectIconClick.bind(this);
+ this.setGridColor = this.setGridColor.bind(this);
+ }
+
+ componentDidMount() {
+ const tooltip = this.props.getSwatchColorPickerTooltip();
+
+ let previousColor;
+ tooltip.addSwatch(this.swatchEl.current, {
+ onCommit: this.setGridColor,
+ onPreview: this.setGridColor,
+ onRevert: () => {
+ this.props.onSetGridOverlayColor(
+ this.props.grid.nodeFront,
+ previousColor
+ );
+ },
+ onShow: () => {
+ previousColor = this.props.grid.color;
+ },
+ });
+ }
+
+ componentWillUnmount() {
+ const tooltip = this.props.getSwatchColorPickerTooltip();
+ tooltip.removeSwatch(this.swatchEl.current);
+ }
+
+ setGridColor() {
+ const color = this.swatchEl.current.dataset.color;
+ this.props.onSetGridOverlayColor(this.props.grid.nodeFront, color);
+ }
+
+ onGridCheckboxClick() {
+ const { grid, onToggleGridHighlighter } = this.props;
+ onToggleGridHighlighter(grid.nodeFront);
+ }
+
+ onGridInspectIconClick(nodeFront) {
+ const { setSelectedNode } = this.props;
+ setSelectedNode(nodeFront, { reason: "layout-panel" });
+ nodeFront.scrollIntoView().catch(e => console.error(e));
+ }
+
+ renderSubgrids() {
+ const { grid, grids } = this.props;
+
+ if (!grid.subgrids.length) {
+ return null;
+ }
+
+ const subgrids = grids.filter(g => grid.subgrids.includes(g.id));
+
+ return dom.ul(
+ {},
+ subgrids.map(g => {
+ return createElement(GridItem, {
+ key: g.id,
+ dispatch: this.props.dispatch,
+ getSwatchColorPickerTooltip: this.props.getSwatchColorPickerTooltip,
+ grid: g,
+ grids,
+ onSetGridOverlayColor: this.props.onSetGridOverlayColor,
+ onToggleGridHighlighter: this.props.onToggleGridHighlighter,
+ setSelectedNode: this.props.setSelectedNode,
+ });
+ })
+ );
+ }
+
+ render() {
+ const { dispatch, grid } = this.props;
+
+ return createElement(
+ Fragment,
+ null,
+ dom.li(
+ {},
+ dom.label(
+ {},
+ dom.input({
+ checked: grid.highlighted,
+ disabled: grid.disabled,
+ type: "checkbox",
+ value: grid.id,
+ onChange: this.onGridCheckboxClick,
+ }),
+ Rep({
+ defaultRep: Rep.ElementNode,
+ mode: MODE.TINY,
+ object: translateNodeFrontToGrip(grid.nodeFront),
+ onDOMNodeMouseOut: () => dispatch(unhighlightNode()),
+ onDOMNodeMouseOver: () => dispatch(highlightNode(grid.nodeFront)),
+ onInspectIconClick: (_, e) => {
+ // Stoping click propagation to avoid firing onGridCheckboxClick()
+ e.stopPropagation();
+ this.onGridInspectIconClick(grid.nodeFront);
+ },
+ })
+ ),
+ dom.div({
+ className: "layout-color-swatch",
+ "data-color": grid.color,
+ ref: this.swatchEl,
+ style: {
+ backgroundColor: grid.color,
+ },
+ title: grid.color,
+ })
+ ),
+ this.renderSubgrids()
+ );
+ }
+}
+
+module.exports = GridItem;
diff --git a/devtools/client/inspector/grids/components/GridList.js b/devtools/client/inspector/grids/components/GridList.js
new file mode 100644
index 0000000000..62c7ae5b13
--- /dev/null
+++ b/devtools/client/inspector/grids/components/GridList.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 {
+ 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 {
+ getStr,
+} = require("resource://devtools/client/inspector/layout/utils/l10n.js");
+
+const GridItem = createFactory(
+ require("resource://devtools/client/inspector/grids/components/GridItem.js")
+);
+
+const Types = require("resource://devtools/client/inspector/grids/types.js");
+
+class GridList extends PureComponent {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ getSwatchColorPickerTooltip: PropTypes.func.isRequired,
+ grids: PropTypes.arrayOf(PropTypes.shape(Types.grid)).isRequired,
+ onSetGridOverlayColor: PropTypes.func.isRequired,
+ onToggleGridHighlighter: PropTypes.func.isRequired,
+ setSelectedNode: PropTypes.func.isRequired,
+ };
+ }
+
+ render() {
+ const {
+ dispatch,
+ getSwatchColorPickerTooltip,
+ grids,
+ onSetGridOverlayColor,
+ onToggleGridHighlighter,
+ setSelectedNode,
+ } = this.props;
+
+ return dom.div(
+ { className: "grid-container" },
+ dom.span(
+ {
+ role: "heading",
+ "aria-level": "3",
+ },
+ getStr("layout.overlayGrid")
+ ),
+ dom.ul(
+ {
+ id: "grid-list",
+ className: "devtools-monospace",
+ },
+ grids
+ // Skip subgrids since they are rendered by their parent grids in GridItem.
+ .filter(grid => !grid.isSubgrid)
+ .map(grid =>
+ GridItem({
+ dispatch,
+ key: grid.id,
+ getSwatchColorPickerTooltip,
+ grid,
+ grids,
+ onSetGridOverlayColor,
+ onToggleGridHighlighter,
+ setSelectedNode,
+ })
+ )
+ )
+ );
+ }
+}
+
+module.exports = GridList;
diff --git a/devtools/client/inspector/grids/components/GridOutline.js b/devtools/client/inspector/grids/components/GridOutline.js
new file mode 100644
index 0000000000..65771f3f45
--- /dev/null
+++ b/devtools/client/inspector/grids/components/GridOutline.js
@@ -0,0 +1,436 @@
+/* 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 {
+ getStr,
+} = require("resource://devtools/client/inspector/layout/utils/l10n.js");
+const {
+ getWritingModeMatrix,
+ getCSSMatrixTransform,
+} = require("resource://devtools/shared/layout/dom-matrix-2d.js");
+
+const Types = require("resource://devtools/client/inspector/grids/types.js");
+
+// The delay prior to executing the grid cell highlighting.
+const GRID_HIGHLIGHTING_DEBOUNCE = 50;
+
+// Prefs for the max number of rows/cols a grid container can have for
+// the outline to display.
+const GRID_OUTLINE_MAX_ROWS_PREF = Services.prefs.getIntPref(
+ "devtools.gridinspector.gridOutlineMaxRows"
+);
+const GRID_OUTLINE_MAX_COLUMNS_PREF = Services.prefs.getIntPref(
+ "devtools.gridinspector.gridOutlineMaxColumns"
+);
+
+// Move SVG grid to the right 100 units, so that it is not flushed against the edge of
+// layout border
+const TRANSLATE_X = 0;
+const TRANSLATE_Y = 0;
+
+const GRID_CELL_SCALE_FACTOR = 50;
+
+const VIEWPORT_MIN_HEIGHT = 100;
+const VIEWPORT_MAX_HEIGHT = 150;
+
+const {
+ showGridHighlighter,
+} = require("resource://devtools/client/inspector/grids/actions/grid-highlighter.js");
+
+class GridOutline extends PureComponent {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ grids: PropTypes.arrayOf(PropTypes.shape(Types.grid)).isRequired,
+ };
+ }
+
+ static getDerivedStateFromProps(props) {
+ const selectedGrid = props.grids.find(grid => grid.highlighted);
+
+ // Store the height of the grid container in the component state to prevent overflow
+ // issues. We want to store the width of the grid container as well so that the
+ // viewbox is only the calculated width of the grid outline.
+ const { width, height } = selectedGrid?.gridFragments.length
+ ? getTotalWidthAndHeight(selectedGrid)
+ : { width: 0, height: 0 };
+ let showOutline;
+
+ if (selectedGrid?.gridFragments.length) {
+ const { cols, rows } = selectedGrid.gridFragments[0];
+
+ // Show the grid outline if both the rows/columns are less than or equal
+ // to their max prefs.
+ showOutline =
+ cols.lines.length <= GRID_OUTLINE_MAX_COLUMNS_PREF &&
+ rows.lines.length <= GRID_OUTLINE_MAX_ROWS_PREF;
+ }
+
+ return { height, width, selectedGrid, showOutline };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ height: 0,
+ selectedGrid: null,
+ showOutline: true,
+ width: 0,
+ };
+
+ this.doHighlightCell = this.doHighlightCell.bind(this);
+ this.getGridAreaName = this.getGridAreaName.bind(this);
+ this.getHeight = this.getHeight.bind(this);
+ this.onHighlightCell = this.onHighlightCell.bind(this);
+ this.renderCannotShowOutlineText =
+ this.renderCannotShowOutlineText.bind(this);
+ this.renderGrid = this.renderGrid.bind(this);
+ this.renderGridCell = this.renderGridCell.bind(this);
+ this.renderGridOutline = this.renderGridOutline.bind(this);
+ this.renderGridOutlineBorder = this.renderGridOutlineBorder.bind(this);
+ this.renderOutline = this.renderOutline.bind(this);
+ }
+
+ doHighlightCell(target, hide) {
+ const { dispatch, grids } = this.props;
+ const name = target.dataset.gridAreaName;
+ const id = target.dataset.gridId;
+ const gridFragmentIndex = target.dataset.gridFragmentIndex;
+ const rowNumber = target.dataset.gridRow;
+ const columnNumber = target.dataset.gridColumn;
+ const nodeFront = grids[id].nodeFront;
+
+ // The options object has the following properties which corresponds to the
+ // required parameters for showing the grid cell or area highlights.
+ // See devtools/server/actors/highlighters/css-grid.js
+ // {
+ // showGridArea: String,
+ // showGridCell: {
+ // gridFragmentIndex: Number,
+ // rowNumber: Number,
+ // columnNumber: Number,
+ // },
+ // }
+ const options = {
+ showGridArea: name,
+ showGridCell: {
+ gridFragmentIndex,
+ rowNumber,
+ columnNumber,
+ },
+ };
+
+ if (hide) {
+ // Reset the grid highlighter to default state; no options = hide cell/area outline.
+ dispatch(showGridHighlighter(nodeFront));
+ } else {
+ dispatch(showGridHighlighter(nodeFront, options));
+ }
+ }
+
+ /**
+ * Returns the grid area name if the given grid cell is part of a grid area, otherwise
+ * null.
+ *
+ * @param {Number} columnNumber
+ * The column number of the grid cell.
+ * @param {Number} rowNumber
+ * The row number of the grid cell.
+ * @param {Array} areas
+ * Array of grid areas data stored in the grid fragment.
+ * @return {String} If there is a grid area return area name, otherwise null.
+ */
+ getGridAreaName(columnNumber, rowNumber, areas) {
+ const gridArea = areas.find(
+ area =>
+ area.rowStart <= rowNumber &&
+ area.rowEnd > rowNumber &&
+ area.columnStart <= columnNumber &&
+ area.columnEnd > columnNumber
+ );
+
+ if (!gridArea) {
+ return null;
+ }
+
+ return gridArea.name;
+ }
+
+ /**
+ * Returns the height of the grid outline ranging between a minimum and maximum height.
+ *
+ * @return {Number} The height of the grid outline.
+ */
+ getHeight() {
+ const { height } = this.state;
+
+ if (height >= VIEWPORT_MAX_HEIGHT) {
+ return VIEWPORT_MAX_HEIGHT;
+ } else if (height <= VIEWPORT_MIN_HEIGHT) {
+ return VIEWPORT_MIN_HEIGHT;
+ }
+
+ return height;
+ }
+
+ /**
+ * Displays a message text "Cannot show outline for this grid".
+ */
+ renderCannotShowOutlineText() {
+ return dom.div(
+ { className: "grid-outline-text" },
+ dom.span({
+ className: "grid-outline-text-icon",
+ title: getStr("layout.cannotShowGridOutline.title"),
+ }),
+ getStr("layout.cannotShowGridOutline")
+ );
+ }
+
+ /**
+ * Renders the grid outline for the given grid container object.
+ *
+ * @param {Object} grid
+ * A single grid container in the document.
+ */
+ renderGrid(grid) {
+ // TODO: We are drawing the first fragment since only one is currently being stored.
+ // In the future we will need to iterate over all fragments of a grid.
+ const gridFragmentIndex = 0;
+ const { id, color, gridFragments } = grid;
+ const { rows, cols, areas } = gridFragments[gridFragmentIndex];
+
+ const numberOfColumns = cols.lines.length - 1;
+ const numberOfRows = rows.lines.length - 1;
+ const rectangles = [];
+ let x = 0;
+ let y = 0;
+ let width = 0;
+ let height = 0;
+
+ // Draw the cells contained within the grid outline border.
+ for (let rowNumber = 1; rowNumber <= numberOfRows; rowNumber++) {
+ height =
+ GRID_CELL_SCALE_FACTOR * (rows.tracks[rowNumber - 1].breadth / 100);
+
+ for (
+ let columnNumber = 1;
+ columnNumber <= numberOfColumns;
+ columnNumber++
+ ) {
+ width =
+ GRID_CELL_SCALE_FACTOR *
+ (cols.tracks[columnNumber - 1].breadth / 100);
+
+ const gridAreaName = this.getGridAreaName(
+ columnNumber,
+ rowNumber,
+ areas
+ );
+ const gridCell = this.renderGridCell(
+ id,
+ gridFragmentIndex,
+ x,
+ y,
+ rowNumber,
+ columnNumber,
+ color,
+ gridAreaName,
+ width,
+ height
+ );
+
+ rectangles.push(gridCell);
+ x += width;
+ }
+
+ x = 0;
+ y += height;
+ }
+
+ // Transform the cells as needed to match the grid container's writing mode.
+ const cellGroupStyle = {};
+ const writingModeMatrix = getWritingModeMatrix(this.state, grid);
+ cellGroupStyle.transform = getCSSMatrixTransform(writingModeMatrix);
+ const cellGroup = dom.g(
+ {
+ id: "grid-cell-group",
+ style: cellGroupStyle,
+ },
+ rectangles
+ );
+
+ // Draw a rectangle that acts as the grid outline border.
+ const border = this.renderGridOutlineBorder(
+ this.state.width,
+ this.state.height,
+ color
+ );
+
+ return [border, cellGroup];
+ }
+
+ /**
+ * Renders the grid cell of a grid fragment.
+ *
+ * @param {Number} id
+ * The grid id stored on the grid fragment
+ * @param {Number} gridFragmentIndex
+ * The index of the grid fragment rendered to the document.
+ * @param {Number} x
+ * The x-position of the grid cell.
+ * @param {Number} y
+ * The y-position of the grid cell.
+ * @param {Number} rowNumber
+ * The row number of the grid cell.
+ * @param {Number} columnNumber
+ * The column number of the grid cell.
+ * @param {String|null} gridAreaName
+ * The grid area name or null if the grid cell is not part of a grid area.
+ * @param {Number} width
+ * The width of grid cell.
+ * @param {Number} height
+ * The height of the grid cell.
+ */
+ renderGridCell(
+ id,
+ gridFragmentIndex,
+ x,
+ y,
+ rowNumber,
+ columnNumber,
+ color,
+ gridAreaName,
+ width,
+ height
+ ) {
+ return dom.rect({
+ key: `${id}-${rowNumber}-${columnNumber}`,
+ className: "grid-outline-cell",
+ "data-grid-area-name": gridAreaName,
+ "data-grid-fragment-index": gridFragmentIndex,
+ "data-grid-id": id,
+ "data-grid-row": rowNumber,
+ "data-grid-column": columnNumber,
+ x,
+ y,
+ width,
+ height,
+ fill: "none",
+ onMouseEnter: this.onHighlightCell,
+ onMouseLeave: this.onHighlightCell,
+ });
+ }
+
+ renderGridOutline(grid) {
+ const { color } = grid;
+
+ return dom.g(
+ {
+ id: "grid-outline-group",
+ className: "grid-outline-group",
+ style: { color },
+ },
+ this.renderGrid(grid)
+ );
+ }
+
+ renderGridOutlineBorder(borderWidth, borderHeight, color) {
+ return dom.rect({
+ key: "border",
+ className: "grid-outline-border",
+ x: 0,
+ y: 0,
+ width: borderWidth,
+ height: borderHeight,
+ });
+ }
+
+ renderOutline() {
+ const { height, selectedGrid, showOutline, width } = this.state;
+
+ return showOutline
+ ? dom.svg(
+ {
+ id: "grid-outline",
+ width: "100%",
+ height: this.getHeight(),
+ viewBox: `${TRANSLATE_X} ${TRANSLATE_Y} ${width} ${height}`,
+ },
+ this.renderGridOutline(selectedGrid)
+ )
+ : this.renderCannotShowOutlineText();
+ }
+
+ onHighlightCell({ target, type }) {
+ // Debounce the highlighting of cells.
+ // This way we don't end up sending many requests to the server for highlighting when
+ // cells get hovered in a rapid succession We only send a request if the user settles
+ // on a cell for some time.
+ if (this.highlightTimeout) {
+ clearTimeout(this.highlightTimeout);
+ }
+
+ this.highlightTimeout = setTimeout(() => {
+ this.doHighlightCell(target, type === "mouseleave");
+ this.highlightTimeout = null;
+ }, GRID_HIGHLIGHTING_DEBOUNCE);
+ }
+
+ render() {
+ const { selectedGrid } = this.state;
+
+ return selectedGrid?.gridFragments.length
+ ? dom.div(
+ {
+ id: "grid-outline-container",
+ className: "grid-outline-container",
+ },
+ this.renderOutline()
+ )
+ : null;
+ }
+}
+
+/**
+ * Get the width and height of a given grid.
+ *
+ * @param {Object} grid
+ * A single grid container in the document.
+ * @return {Object} An object like { width, height }
+ */
+function getTotalWidthAndHeight(grid) {
+ // TODO: We are drawing the first fragment since only one is currently being stored.
+ // In the future we will need to iterate over all fragments of a grid.
+ const { gridFragments } = grid;
+ const { rows, cols } = gridFragments[0];
+
+ let height = 0;
+ for (let i = 0; i < rows.lines.length - 1; i++) {
+ height += GRID_CELL_SCALE_FACTOR * (rows.tracks[i].breadth / 100);
+ }
+
+ let width = 0;
+ for (let i = 0; i < cols.lines.length - 1; i++) {
+ width += GRID_CELL_SCALE_FACTOR * (cols.tracks[i].breadth / 100);
+ }
+
+ // All writing modes other than horizontal-tb (the initial value) involve a 90 deg
+ // rotation, so swap width and height.
+ if (grid.writingMode != "horizontal-tb") {
+ [width, height] = [height, width];
+ }
+
+ return { width, height };
+}
+
+module.exports = GridOutline;
diff --git a/devtools/client/inspector/grids/components/moz.build b/devtools/client/inspector/grids/components/moz.build
new file mode 100644
index 0000000000..e938e51ad1
--- /dev/null
+++ b/devtools/client/inspector/grids/components/moz.build
@@ -0,0 +1,13 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "Grid.js",
+ "GridDisplaySettings.js",
+ "GridItem.js",
+ "GridList.js",
+ "GridOutline.js",
+)
diff --git a/devtools/client/inspector/grids/grid-inspector.js b/devtools/client/inspector/grids/grid-inspector.js
new file mode 100644
index 0000000000..c6aac90592
--- /dev/null
+++ b/devtools/client/inspector/grids/grid-inspector.js
@@ -0,0 +1,776 @@
+/* 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 flags = require("resource://devtools/shared/flags.js");
+const { throttle } = require("resource://devtools/shared/throttle.js");
+
+const gridsReducer = require("resource://devtools/client/inspector/grids/reducers/grids.js");
+const highlighterSettingsReducer = require("resource://devtools/client/inspector/grids/reducers/highlighter-settings.js");
+const {
+ updateGridColor,
+ updateGridHighlighted,
+ updateGrids,
+} = require("resource://devtools/client/inspector/grids/actions/grids.js");
+const {
+ updateShowGridAreas,
+ updateShowGridLineNumbers,
+ updateShowInfiniteLines,
+} = require("resource://devtools/client/inspector/grids/actions/highlighter-settings.js");
+
+loader.lazyRequireGetter(
+ this,
+ "compareFragmentsGeometry",
+ "resource://devtools/client/inspector/grids/utils/utils.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "parseURL",
+ "resource://devtools/client/shared/source-utils.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "asyncStorage",
+ "resource://devtools/shared/async-storage.js"
+);
+
+const CSS_GRID_COUNT_HISTOGRAM_ID = "DEVTOOLS_NUMBER_OF_CSS_GRIDS_IN_A_PAGE";
+
+const SHOW_GRID_AREAS = "devtools.gridinspector.showGridAreas";
+const SHOW_GRID_LINE_NUMBERS = "devtools.gridinspector.showGridLineNumbers";
+const SHOW_INFINITE_LINES_PREF = "devtools.gridinspector.showInfiniteLines";
+
+const TELEMETRY_GRID_AREAS_OVERLAY_CHECKED =
+ "devtools.grid.showGridAreasOverlay.checked";
+const TELEMETRY_GRID_LINE_NUMBERS_CHECKED =
+ "devtools.grid.showGridLineNumbers.checked";
+const TELEMETRY_INFINITE_LINES_CHECKED =
+ "devtools.grid.showInfiniteLines.checked";
+
+// Default grid colors.
+const GRID_COLORS = [
+ "#9400FF",
+ "#DF00A9",
+ "#0A84FF",
+ "#12BC00",
+ "#EA8000",
+ "#00B0BD",
+ "#D70022",
+ "#4B42FF",
+ "#B5007F",
+ "#058B00",
+ "#A47F00",
+ "#005A71",
+];
+
+class GridInspector {
+ constructor(inspector, window) {
+ this.document = window.document;
+ this.inspector = inspector;
+ this.store = inspector.store;
+ this.telemetry = inspector.telemetry;
+
+ // Maximum number of grid highlighters that can be displayed.
+ this.maxHighlighters = Services.prefs.getIntPref(
+ "devtools.gridinspector.maxHighlighters"
+ );
+
+ this.store.injectReducer("grids", gridsReducer);
+ this.store.injectReducer("highlighterSettings", highlighterSettingsReducer);
+
+ this.onHighlighterShown = this.onHighlighterShown.bind(this);
+ this.onHighlighterHidden = this.onHighlighterHidden.bind(this);
+ this.onNavigate = this.onNavigate.bind(this);
+ this.onReflow = throttle(this.onReflow, 500, this);
+ this.onSetGridOverlayColor = this.onSetGridOverlayColor.bind(this);
+ this.onSidebarSelect = this.onSidebarSelect.bind(this);
+ this.onToggleGridHighlighter = this.onToggleGridHighlighter.bind(this);
+ this.onToggleShowGridAreas = this.onToggleShowGridAreas.bind(this);
+ this.onToggleShowGridLineNumbers =
+ this.onToggleShowGridLineNumbers.bind(this);
+ this.onToggleShowInfiniteLines = this.onToggleShowInfiniteLines.bind(this);
+ this.updateGridPanel = this.updateGridPanel.bind(this);
+ this.listenForGridHighlighterEvents =
+ this.listenForGridHighlighterEvents.bind(this);
+
+ this.init();
+ }
+
+ get highlighters() {
+ if (!this._highlighters) {
+ this._highlighters = this.inspector.highlighters;
+ }
+
+ return this._highlighters;
+ }
+
+ /**
+ * Initializes the grid inspector by fetching the LayoutFront from the walker and
+ * loading the highlighter settings.
+ */
+ async init() {
+ if (!this.inspector) {
+ return;
+ }
+
+ if (flags.testing) {
+ // In tests, we start listening immediately to avoid having to simulate a mousemove.
+ this.listenForGridHighlighterEvents();
+ } else {
+ this.document.addEventListener(
+ "mousemove",
+ this.listenForGridHighlighterEvents,
+ {
+ once: true,
+ }
+ );
+ }
+
+ this.inspector.sidebar.on("select", this.onSidebarSelect);
+ this.inspector.on("new-root", this.onNavigate);
+
+ this.onSidebarSelect();
+ }
+
+ listenForGridHighlighterEvents() {
+ this.highlighters.on("grid-highlighter-hidden", this.onHighlighterHidden);
+ this.highlighters.on("grid-highlighter-shown", this.onHighlighterShown);
+ }
+
+ /**
+ * Get the LayoutActor fronts for all interesting targets where we have inspectors.
+ *
+ * @return {Array} The list of LayoutActor fronts
+ */
+ async getLayoutFronts() {
+ const inspectorFronts = await this.inspector.getAllInspectorFronts();
+ const layoutFronts = await Promise.all(
+ inspectorFronts.map(({ walker }) => walker.getLayoutInspector())
+ );
+ return layoutFronts.filter(front => !front.isDestroyed());
+ }
+
+ /**
+ * Destruction function called when the inspector is destroyed. Removes event listeners
+ * and cleans up references.
+ */
+ destroy() {
+ if (this._highlighters) {
+ this.highlighters.off(
+ "grid-highlighter-hidden",
+ this.onHighlighterHidden
+ );
+ this.highlighters.off("grid-highlighter-shown", this.onHighlighterShown);
+ }
+ this.document.removeEventListener(
+ "mousemove",
+ this.listenForGridHighlighterEvents
+ );
+
+ this.inspector.sidebar.off("select", this.onSidebarSelect);
+ this.inspector.off("new-root", this.onNavigate);
+
+ this.inspector.off("reflow-in-selected-target", this.onReflow);
+
+ this._highlighters = null;
+ this.document = null;
+ this.inspector = null;
+ this.store = null;
+ }
+
+ getComponentProps() {
+ return {
+ onSetGridOverlayColor: this.onSetGridOverlayColor,
+ onToggleGridHighlighter: this.onToggleGridHighlighter,
+ onToggleShowGridAreas: this.onToggleShowGridAreas,
+ onToggleShowGridLineNumbers: this.onToggleShowGridLineNumbers,
+ onToggleShowInfiniteLines: this.onToggleShowInfiniteLines,
+ };
+ }
+
+ /**
+ * Returns the initial color linked to a grid container. Will attempt to check the
+ * current grid highlighter state and the store.
+ *
+ * @param {NodeFront} nodeFront
+ * The NodeFront for which we need the color.
+ * @param {String} customColor
+ * The color fetched from the custom palette, if it exists.
+ * @param {String} fallbackColor
+ * The color to use if no color could be found for the node front.
+ * @return {String} color
+ * The color to use.
+ */
+ getInitialGridColor(nodeFront, customColor, fallbackColor) {
+ const highlighted = this.highlighters.gridHighlighters.has(nodeFront);
+
+ let color;
+ if (customColor) {
+ color = customColor;
+ } else if (
+ highlighted &&
+ this.highlighters.state.grids.has(nodeFront.actorID)
+ ) {
+ // If the node front is currently highlighted, use the color from the highlighter
+ // options.
+ color = this.highlighters.state.grids.get(nodeFront.actorID).options
+ .color;
+ } else {
+ // Otherwise use the color defined in the store for this node front.
+ color = this.getGridColorForNodeFront(nodeFront);
+ }
+
+ return color || fallbackColor;
+ }
+
+ /**
+ * Returns the color set for the grid highlighter associated with the provided
+ * nodeFront.
+ *
+ * @param {NodeFront} nodeFront
+ * The NodeFront for which we need the color.
+ */
+ getGridColorForNodeFront(nodeFront) {
+ const { grids } = this.store.getState();
+
+ for (const grid of grids) {
+ if (grid.nodeFront === nodeFront) {
+ return grid.color;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Given a list of new grid fronts, and if there are highlighted grids, check
+ * if their fragments have changed.
+ *
+ * @param {Array} newGridFronts
+ * A list of GridFront objects.
+ * @return {Boolean}
+ */
+ haveCurrentFragmentsChanged(newGridFronts) {
+ const gridHighlighters = this.highlighters.gridHighlighters;
+
+ if (!gridHighlighters.size) {
+ return false;
+ }
+
+ const gridFronts = newGridFronts.filter(g =>
+ gridHighlighters.has(g.containerNodeFront)
+ );
+ if (!gridFronts.length) {
+ return false;
+ }
+
+ const { grids } = this.store.getState();
+
+ for (const node of gridHighlighters.keys()) {
+ const oldFragments = grids.find(g => g.nodeFront === node).gridFragments;
+ const newFragments = newGridFronts.find(
+ g => g.containerNodeFront === node
+ ).gridFragments;
+
+ if (!compareFragmentsGeometry(oldFragments, newFragments)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns true if the layout panel is visible, and false otherwise.
+ */
+ isPanelVisible() {
+ return (
+ this.inspector &&
+ this.inspector.toolbox &&
+ this.inspector.sidebar &&
+ this.inspector.toolbox.currentToolId === "inspector" &&
+ this.inspector.sidebar.getCurrentTabID() === "layoutview"
+ );
+ }
+
+ /**
+ * Updates the grid panel by dispatching the new grid data. This is called when the
+ * layout view becomes visible or the view needs to be updated with new grid data.
+ */
+ async updateGridPanel() {
+ // Stop refreshing if the inspector or store is already destroyed.
+ if (!this.inspector || !this.store) {
+ return;
+ }
+
+ try {
+ await this._updateGridPanel();
+ } catch (e) {
+ this._throwUnlessDestroyed(
+ e,
+ "Inspector destroyed while executing updateGridPanel"
+ );
+ }
+ }
+
+ async _updateGridPanel() {
+ const gridFronts = await this.getGrids();
+
+ if (!gridFronts.length) {
+ try {
+ this.store.dispatch(updateGrids([]));
+ this.inspector.emit("grid-panel-updated");
+ return;
+ } catch (e) {
+ // This call might fail if called asynchrously after the toolbox is finished
+ // closing.
+ return;
+ }
+ }
+
+ const currentUrl = this.inspector.currentTarget.url;
+
+ // Log how many CSS Grid elements DevTools sees.
+ if (currentUrl != this.inspector.previousURL) {
+ this.telemetry
+ .getHistogramById(CSS_GRID_COUNT_HISTOGRAM_ID)
+ .add(gridFronts.length);
+ this.inspector.previousURL = currentUrl;
+ }
+
+ // Get the hostname, if there is no hostname, fall back on protocol
+ // ex: `data:` uri, and `about:` pages
+ const hostname =
+ parseURL(currentUrl).hostname || parseURL(currentUrl).protocol;
+ const customColors =
+ (await asyncStorage.getItem("gridInspectorHostColors")) || {};
+
+ const grids = [];
+ for (let i = 0; i < gridFronts.length; i++) {
+ const grid = gridFronts[i];
+ let nodeFront = grid.containerNodeFront;
+
+ // If the GridFront didn't yet have access to the NodeFront for its container, then
+ // get it from the walker. This happens when the walker hasn't yet seen this
+ // particular DOM Node in the tree yet, or when we are connected to an older server.
+ if (!nodeFront) {
+ try {
+ nodeFront = await grid.walkerFront.getNodeFromActor(grid.actorID, [
+ "containerEl",
+ ]);
+ } catch (e) {
+ // This call might fail if called asynchrously after the toolbox is finished
+ // closing.
+ return;
+ }
+ }
+
+ const colorForHost = customColors[hostname]
+ ? customColors[hostname][i]
+ : null;
+ const fallbackColor = GRID_COLORS[i % GRID_COLORS.length];
+ const color = this.getInitialGridColor(
+ nodeFront,
+ colorForHost,
+ fallbackColor
+ );
+ const highlighted = this.highlighters.gridHighlighters.has(nodeFront);
+ const disabled =
+ !highlighted &&
+ this.maxHighlighters > 1 &&
+ this.highlighters.gridHighlighters.size === this.maxHighlighters;
+ const isSubgrid = grid.isSubgrid;
+ const gridData = {
+ id: i,
+ actorID: grid.actorID,
+ color,
+ disabled,
+ direction: grid.direction,
+ gridFragments: grid.gridFragments,
+ highlighted,
+ isSubgrid,
+ nodeFront,
+ parentNodeActorID: null,
+ subgrids: [],
+ writingMode: grid.writingMode,
+ };
+
+ if (isSubgrid) {
+ let parentGridNodeFront;
+
+ try {
+ parentGridNodeFront = await nodeFront.walkerFront.getParentGridNode(
+ nodeFront
+ );
+ } catch (e) {
+ // This call might fail if called asynchrously after the toolbox is finished
+ // closing.
+ return;
+ }
+
+ if (!parentGridNodeFront) {
+ return;
+ }
+
+ const parentIndex = grids.findIndex(
+ g => g.nodeFront.actorID === parentGridNodeFront.actorID
+ );
+ gridData.parentNodeActorID = parentGridNodeFront.actorID;
+ grids[parentIndex].subgrids.push(gridData.id);
+ }
+
+ grids.push(gridData);
+ }
+
+ // We need to make sure that nested subgrids are displayed above their parent grid
+ // containers, so update the z-index of each grid before rendering them.
+ for (const root of grids.filter(g => !g.parentNodeActorID)) {
+ this._updateZOrder(grids, root);
+ }
+
+ this.store.dispatch(updateGrids(grids));
+ this.inspector.emit("grid-panel-updated");
+ }
+
+ /**
+ * Get all GridFront instances from the server(s).
+ *
+ *
+ * @return {Array} The list of GridFronts
+ */
+ async getGrids() {
+ const promises = [];
+ try {
+ const layoutFronts = await this.getLayoutFronts();
+ for (const layoutFront of layoutFronts) {
+ promises.push(layoutFront.getAllGrids());
+ }
+ } catch (e) {
+ // This call might fail if called asynchrously after the toolbox is finished closing
+ }
+
+ const gridFronts = (await Promise.all(promises)).flat();
+ return gridFronts;
+ }
+
+ /**
+ * Handler for "grid-highlighter-shown" events emitted from the
+ * HighlightersOverlay. Passes nodefront and event name to handleHighlighterChange.
+ * Required since on and off events need the same reference object.
+ *
+ * @param {NodeFront} nodeFront
+ * The NodeFront of the grid container element for which the grid
+ * highlighter is shown for.
+ */
+ onHighlighterShown(nodeFront) {
+ this.onHighlighterChange(nodeFront, true);
+ }
+
+ /**
+ * Handler for "grid-highlighter-hidden" events emitted from the
+ * HighlightersOverlay. Passes nodefront and event name to handleHighlighterChange.
+ * Required since on and off events need the same reference object.
+ *
+ * @param {NodeFront} nodeFront
+ * The NodeFront of the grid container element for which the grid highlighter
+ * is hidden for.
+ */
+ onHighlighterHidden(nodeFront) {
+ this.onHighlighterChange(nodeFront, false);
+ }
+
+ /**
+ * Handler for "grid-highlighter-shown" and "grid-highlighter-hidden" events emitted
+ * from the HighlightersOverlay. Updates the NodeFront's grid highlighted state.
+ *
+ * @param {NodeFront} nodeFront
+ * The NodeFront of the grid container element for which the grid highlighter
+ * is shown for.
+ * @param {Boolean} highlighted
+ * If the grid should be updated to highlight or hide.
+ */
+ onHighlighterChange(nodeFront, highlighted) {
+ if (!this.isPanelVisible()) {
+ return;
+ }
+
+ const { grids } = this.store.getState();
+ const grid = grids.find(g => g.nodeFront === nodeFront);
+
+ if (!grid || grid.highlighted === highlighted) {
+ return;
+ }
+
+ this.store.dispatch(updateGridHighlighted(nodeFront, highlighted));
+ }
+
+ /**
+ * Handler for "new-root" event fired by the inspector, which indicates a page
+ * navigation. Updates grid panel contents.
+ */
+ onNavigate() {
+ if (this.isPanelVisible()) {
+ this.updateGridPanel();
+ }
+ }
+
+ /**
+ * Handler for reflow events fired by the inspector when a node is selected. On reflows,
+ * update the grid panel content, because the shape or number of grids on the page may
+ * have changed.
+ *
+ * Note that there may be frequent reflows on the page and that not all of them actually
+ * cause the grids to change. So, we want to limit how many times we update the grid
+ * panel to only reflows that actually either change the list of grids, or those that
+ * change the current outlined grid.
+ * To achieve this, this function compares the list of grid containers from before and
+ * after the reflow, as well as the grid fragment data on the currently highlighted
+ * grid.
+ */
+ async onReflow() {
+ try {
+ if (!this.isPanelVisible()) {
+ return;
+ }
+
+ // The list of grids currently displayed.
+ const { grids } = this.store.getState();
+
+ // The new list of grids from the server.
+ const newGridFronts = await this.getGrids();
+
+ // In some cases, the nodes for current grids may have been removed from the DOM in
+ // which case we need to update.
+ if (grids.length && grids.some(grid => !grid.nodeFront.actorID)) {
+ await this.updateGridPanel(newGridFronts);
+ return;
+ }
+
+ // Get the node front(s) from the current grid(s) so we can compare them to them to
+ // the node(s) of the new grids.
+ const oldNodeFronts = grids.map(grid => grid.nodeFront.actorID);
+ const newNodeFronts = newGridFronts
+ .filter(grid => grid.containerNode)
+ .map(grid => grid.containerNodeFront.actorID);
+
+ if (
+ grids.length === newGridFronts.length &&
+ oldNodeFronts.sort().join(",") == newNodeFronts.sort().join(",") &&
+ !this.haveCurrentFragmentsChanged(newGridFronts)
+ ) {
+ // Same list of containers and the geometry of all the displayed grids remained the
+ // same, we can safely abort.
+ return;
+ }
+
+ // Either the list of containers or the current fragments have changed, do update.
+ await this.updateGridPanel(newGridFronts);
+ } catch (e) {
+ this._throwUnlessDestroyed(
+ e,
+ "Inspector destroyed while executing onReflow callback"
+ );
+ }
+ }
+
+ /**
+ * Handler for a change in the grid overlay color picker for a grid container.
+ *
+ * @param {NodeFront} node
+ * The NodeFront of the grid container element for which the grid color is
+ * being updated.
+ * @param {String} color
+ * A hex string representing the color to use.
+ */
+ async onSetGridOverlayColor(node, color) {
+ this.store.dispatch(updateGridColor(node, color));
+
+ const { grids } = this.store.getState();
+ const currentUrl = this.inspector.currentTarget.url;
+ // Get the hostname, if there is no hostname, fall back on protocol
+ // ex: `data:` uri, and `about:` pages
+ const hostname =
+ parseURL(currentUrl).hostname || parseURL(currentUrl).protocol;
+ const customGridColors =
+ (await asyncStorage.getItem("gridInspectorHostColors")) || {};
+
+ for (const grid of grids) {
+ if (grid.nodeFront === node) {
+ if (!customGridColors[hostname]) {
+ customGridColors[hostname] = [];
+ }
+ // Update the custom color for the grid in this position.
+ customGridColors[hostname][grid.id] = color;
+ await asyncStorage.setItem("gridInspectorHostColors", customGridColors);
+
+ if (!this.isPanelVisible()) {
+ // This call might fail if called asynchrously after the toolbox is finished
+ // closing.
+ return;
+ }
+
+ // If the grid for which the color was updated currently has a highlighter, update
+ // the color. If the node is not explicitly highlighted, we assume it's the
+ // parent grid for a subgrid.
+ if (this.highlighters.gridHighlighters.has(node)) {
+ this.highlighters.showGridHighlighter(node);
+ } else {
+ this.highlighters.showParentGridHighlighter(node);
+ }
+ }
+ }
+ }
+
+ /**
+ * Handler for the inspector sidebar "select" event. Starts tracking reflows
+ * if the layout panel is visible. Otherwise, stop tracking reflows.
+ * Finally, refresh the layout view if it is visible.
+ */
+ onSidebarSelect() {
+ if (!this.isPanelVisible()) {
+ this.inspector.off("reflow-in-selected-target", this.onReflow);
+ return;
+ }
+
+ this.inspector.on("reflow-in-selected-target", this.onReflow);
+ this.updateGridPanel();
+ }
+
+ /**
+ * Handler for a change in the input checkboxes in the GridList component.
+ * Toggles on/off the grid highlighter for the provided grid container element.
+ *
+ * @param {NodeFront} node
+ * The NodeFront of the grid container element for which the grid
+ * highlighter is toggled on/off for.
+ */
+ onToggleGridHighlighter(node) {
+ const { grids } = this.store.getState();
+ const grid = grids.find(g => g.nodeFront === node);
+ this.store.dispatch(updateGridHighlighted(node, !grid.highlighted));
+ this.highlighters.toggleGridHighlighter(node, "grid");
+ }
+
+ /**
+ * Handler for a change in the show grid areas checkbox in the GridDisplaySettings
+ * component. Toggles on/off the option to show the grid areas in the grid highlighter.
+ * Refreshes the shown grid highlighter for the grids currently highlighted.
+ *
+ * @param {Boolean} enabled
+ * Whether or not the grid highlighter should show the grid areas.
+ */
+ onToggleShowGridAreas(enabled) {
+ this.store.dispatch(updateShowGridAreas(enabled));
+ Services.prefs.setBoolPref(SHOW_GRID_AREAS, enabled);
+
+ if (enabled) {
+ this.telemetry.scalarSet(TELEMETRY_GRID_AREAS_OVERLAY_CHECKED, 1);
+ }
+
+ const { grids } = this.store.getState();
+
+ for (const grid of grids) {
+ if (grid.highlighted) {
+ this.highlighters.showGridHighlighter(grid.nodeFront);
+ }
+ }
+ }
+
+ /**
+ * Handler for a change in the show grid line numbers checkbox in the
+ * GridDisplaySettings component. Toggles on/off the option to show the grid line
+ * numbers in the grid highlighter. Refreshes the shown grid highlighter for the
+ * grids currently highlighted.
+ *
+ * @param {Boolean} enabled
+ * Whether or not the grid highlighter should show the grid line numbers.
+ */
+ onToggleShowGridLineNumbers(enabled) {
+ this.store.dispatch(updateShowGridLineNumbers(enabled));
+ Services.prefs.setBoolPref(SHOW_GRID_LINE_NUMBERS, enabled);
+
+ if (enabled) {
+ this.telemetry.scalarSet(TELEMETRY_GRID_LINE_NUMBERS_CHECKED, 1);
+ }
+
+ const { grids } = this.store.getState();
+
+ for (const grid of grids) {
+ if (grid.highlighted) {
+ this.highlighters.showGridHighlighter(grid.nodeFront);
+ }
+ }
+ }
+
+ /**
+ * Handler for a change in the extend grid lines infinitely checkbox in the
+ * GridDisplaySettings component. Toggles on/off the option to extend the grid
+ * lines infinitely in the grid highlighter. Refreshes the shown grid highlighter
+ * for grids currently highlighted.
+ *
+ * @param {Boolean} enabled
+ * Whether or not the grid highlighter should extend grid lines infinitely.
+ */
+ onToggleShowInfiniteLines(enabled) {
+ this.store.dispatch(updateShowInfiniteLines(enabled));
+ Services.prefs.setBoolPref(SHOW_INFINITE_LINES_PREF, enabled);
+
+ if (enabled) {
+ this.telemetry.scalarSet(TELEMETRY_INFINITE_LINES_CHECKED, 1);
+ }
+
+ const { grids } = this.store.getState();
+
+ for (const grid of grids) {
+ if (grid.highlighted) {
+ this.highlighters.showGridHighlighter(grid.nodeFront);
+ }
+ }
+ }
+
+ /**
+ * Some grid-inspector methods are highly asynchronous and might still run
+ * after the inspector was destroyed. Swallow errors if the grid inspector is
+ * already destroyed, throw otherwise.
+ *
+ * @param {Error} error
+ * The original error object.
+ * @param {String} message
+ * The message to log in case the inspector is already destroyed and
+ * the error is swallowed.
+ */
+ _throwUnlessDestroyed(error, message) {
+ if (!this.inspector) {
+ console.warn(message);
+ } else {
+ // If the grid inspector was not destroyed, this is an unexpected error.
+ throw error;
+ }
+ }
+
+ /**
+ * Set z-index of each grids so that nested subgrids are always above their parent grid
+ * container.
+ *
+ * @param {Array} grids
+ * A list of grid data.
+ * @param {Object} parent
+ * A grid data of parent.
+ * @param {Number} zIndex
+ * z-index for the parent.
+ */
+ _updateZOrder(grids, parent, zIndex = 0) {
+ parent.zIndex = zIndex;
+
+ for (const childIndex of parent.subgrids) {
+ // Recurse into children grids.
+ this._updateZOrder(grids, grids[childIndex], zIndex + 1);
+ }
+ }
+}
+
+module.exports = GridInspector;
diff --git a/devtools/client/inspector/grids/moz.build b/devtools/client/inspector/grids/moz.build
new file mode 100644
index 0000000000..4c85701c64
--- /dev/null
+++ b/devtools/client/inspector/grids/moz.build
@@ -0,0 +1,20 @@
+# -*- 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(
+ "grid-inspector.js",
+ "types.js",
+)
+
+BROWSER_CHROME_MANIFESTS += ["test/browser.ini"]
+XPCSHELL_TESTS_MANIFESTS += ["test/xpcshell/xpcshell.ini"]
diff --git a/devtools/client/inspector/grids/reducers/grids.js b/devtools/client/inspector/grids/reducers/grids.js
new file mode 100644
index 0000000000..5ab8bb5060
--- /dev/null
+++ b/devtools/client/inspector/grids/reducers/grids.js
@@ -0,0 +1,87 @@
+/* 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_GRID_COLOR,
+ UPDATE_GRID_HIGHLIGHTED,
+ UPDATE_GRIDS,
+} = require("resource://devtools/client/inspector/grids/actions/index.js");
+
+const INITIAL_GRIDS = [];
+
+const reducers = {
+ [UPDATE_GRID_COLOR](grids, { nodeFront, color }) {
+ const newGrids = grids.map(g => {
+ if (g.nodeFront === nodeFront) {
+ g = Object.assign({}, g, { color });
+ }
+
+ return g;
+ });
+
+ return newGrids;
+ },
+
+ [UPDATE_GRID_HIGHLIGHTED](grids, { nodeFront, highlighted }) {
+ const maxHighlighters = Services.prefs.getIntPref(
+ "devtools.gridinspector.maxHighlighters"
+ );
+ const highlightedNodeFronts = grids
+ .filter(g => g.highlighted)
+ .map(g => g.nodeFront);
+ let numHighlighted = highlightedNodeFronts.length;
+
+ // Get the total number of highlighted grids including the one that will be
+ // highlighted/unhighlighted.
+ if (!highlightedNodeFronts.includes(nodeFront) && highlighted) {
+ numHighlighted += 1;
+ } else if (highlightedNodeFronts.includes(nodeFront) && !highlighted) {
+ numHighlighted -= 1;
+ }
+
+ return grids.map(g => {
+ if (maxHighlighters === 1) {
+ // When there is only one grid highlighter available, only the given grid
+ // container nodeFront can be highlighted, and all the other grid containers
+ // are unhighlighted.
+ return Object.assign({}, g, {
+ highlighted: g.nodeFront === nodeFront && highlighted,
+ });
+ } else if (
+ numHighlighted === maxHighlighters &&
+ g.nodeFront !== nodeFront
+ ) {
+ // The maximum number of highlighted grids have been reached. Disable all the
+ // other non-highlighted grids.
+ return Object.assign({}, g, {
+ disabled: !g.highlighted,
+ });
+ } else if (g.nodeFront === nodeFront) {
+ // This is the provided grid nodeFront to highlight/unhighlight.
+ return Object.assign({}, g, {
+ disabled: false,
+ highlighted,
+ });
+ }
+
+ return Object.assign({}, g, {
+ disabled: false,
+ });
+ });
+ },
+
+ [UPDATE_GRIDS](_, { grids }) {
+ return grids;
+ },
+};
+
+module.exports = function (grids = INITIAL_GRIDS, action) {
+ const reducer = reducers[action.type];
+ if (!reducer) {
+ return grids;
+ }
+ return reducer(grids, action);
+};
diff --git a/devtools/client/inspector/grids/reducers/highlighter-settings.js b/devtools/client/inspector/grids/reducers/highlighter-settings.js
new file mode 100644
index 0000000000..3568390df6
--- /dev/null
+++ b/devtools/client/inspector/grids/reducers/highlighter-settings.js
@@ -0,0 +1,54 @@
+/* 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_SHOW_GRID_AREAS,
+ UPDATE_SHOW_GRID_LINE_NUMBERS,
+ UPDATE_SHOW_INFINITE_LINES,
+} = require("resource://devtools/client/inspector/grids/actions/index.js");
+
+const SHOW_GRID_AREAS = "devtools.gridinspector.showGridAreas";
+const SHOW_GRID_LINE_NUMBERS = "devtools.gridinspector.showGridLineNumbers";
+const SHOW_INFINITE_LINES = "devtools.gridinspector.showInfiniteLines";
+
+const INITIAL_HIGHLIGHTER_SETTINGS = () => {
+ return {
+ showGridAreasOverlay: Services.prefs.getBoolPref(SHOW_GRID_AREAS),
+ showGridLineNumbers: Services.prefs.getBoolPref(SHOW_GRID_LINE_NUMBERS),
+ showInfiniteLines: Services.prefs.getBoolPref(SHOW_INFINITE_LINES),
+ };
+};
+
+const reducers = {
+ [UPDATE_SHOW_GRID_AREAS](highlighterSettings, { enabled }) {
+ return Object.assign({}, highlighterSettings, {
+ showGridAreasOverlay: enabled,
+ });
+ },
+
+ [UPDATE_SHOW_GRID_LINE_NUMBERS](highlighterSettings, { enabled }) {
+ return Object.assign({}, highlighterSettings, {
+ showGridLineNumbers: enabled,
+ });
+ },
+
+ [UPDATE_SHOW_INFINITE_LINES](highlighterSettings, { enabled }) {
+ return Object.assign({}, highlighterSettings, {
+ showInfiniteLines: enabled,
+ });
+ },
+};
+
+module.exports = function (
+ highlighterSettings = INITIAL_HIGHLIGHTER_SETTINGS(),
+ action
+) {
+ const reducer = reducers[action.type];
+ if (!reducer) {
+ return highlighterSettings;
+ }
+ return reducer(highlighterSettings, action);
+};
diff --git a/devtools/client/inspector/grids/reducers/moz.build b/devtools/client/inspector/grids/reducers/moz.build
new file mode 100644
index 0000000000..768e29b542
--- /dev/null
+++ b/devtools/client/inspector/grids/reducers/moz.build
@@ -0,0 +1,10 @@
+# -*- 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(
+ "grids.js",
+ "highlighter-settings.js",
+)
diff --git a/devtools/client/inspector/grids/test/browser.ini b/devtools/client/inspector/grids/test/browser.ini
new file mode 100644
index 0000000000..bb0f9f9136
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser.ini
@@ -0,0 +1,49 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ doc_iframe_reloaded.html
+ doc_subgrid.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_grids_accordion-state.js]
+[browser_grids_color-in-rules-grid-toggle.js]
+[browser_grids_display-setting-extend-grid-lines.js]
+[browser_grids_display-setting-show-grid-line-numbers.js]
+[browser_grids_display-setting-show-grid-areas.js]
+[browser_grids_grid-list-color-picker-on-ESC.js]
+[browser_grids_grid-list-color-picker-on-RETURN.js]
+[browser_grids_grid-list-element-rep.js]
+[browser_grids_grid-list-no-grids.js]
+[browser_grids_grid-list-on-iframe-reloaded.js]
+skip-if = (verify && (os == 'win' || os == 'linux'))
+[browser_grids_grid-list-on-mutation-element-added.js]
+skip-if = true #Bug 1557326
+[browser_grids_grid-list-on-mutation-element-removed.js]
+[browser_grids_grid-list-on-target-added-removed.js]
+[browser_grids_grid-list-subgrids-z-order.js]
+[browser_grids_grid-list-subgrids_01.js]
+[browser_grids_grid-list-subgrids_02.js]
+[browser_grids_grid-list-toggle-grids_01.js]
+[browser_grids_grid-list-toggle-grids_02.js]
+[browser_grids_grid-list-toggle-multiple-grids.js]
+[browser_grids_grid-outline-cannot-show-outline.js]
+[browser_grids_grid-outline-highlight-area.js]
+[browser_grids_grid-outline-highlight-cell.js]
+[browser_grids_grid-outline-multiple-grids.js]
+[browser_grids_grid-outline-selected-grid.js]
+[browser_grids_grid-outline-updates-on-grid-change.js]
+skip-if = (os == "linux") || (os == 'mac') || (os == 'win' && (debug || asan)) #Bug 1557181
+[browser_grids_grid-outline-writing-mode.js]
+skip-if = (verify && (os == 'win'))
+[browser_grids_highlighter-setting-rules-grid-toggle.js]
+[browser_grids_highlighter-toggle-telemetry.js]
+[browser_grids_number-of-css-grids-telemetry.js]
+[browser_grids_persist-color-palette.js]
+[browser_grids_restored-after-reload.js]
+[browser_grids_restored-multiple-grids-after-reload.js]
diff --git a/devtools/client/inspector/grids/test/browser_grids_accordion-state.js b/devtools/client/inspector/grids/test/browser_grids_accordion-state.js
new file mode 100644
index 0000000000..c02fc85dfa
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser_grids_accordion-state.js
@@ -0,0 +1,108 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the grid's accordion state is persistent through hide/show in the layout
+// view.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #grid {
+ display: grid;
+ }
+ </style>
+ <div id="grid">
+ <div id="cell1">cell1</div>
+ <div id="cell2">cell2</div>
+ </div>
+`;
+
+const GRID_OPENED_PREF = "devtools.layout.grid.opened";
+const GRID_PANE_SELECTOR = "#layout-grid-section";
+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, gridInspector, toolbox } = await openLayoutView();
+ const { document: doc } = gridInspector;
+
+ await testAccordionStateAfterClickingHeader(doc);
+ await testAccordionStateAfterSwitchingSidebars(inspector, doc);
+ await testAccordionStateAfterReopeningLayoutView(toolbox);
+
+ Services.prefs.clearUserPref(GRID_OPENED_PREF);
+});
+
+async function testAccordionStateAfterClickingHeader(doc) {
+ const item = await waitFor(() => doc.querySelector(GRID_PANE_SELECTOR));
+ const header = item.querySelector(ACCORDION_HEADER_SELECTOR);
+ const content = item.querySelector(ACCORDION_CONTENT_SELECTOR);
+
+ info("Checking initial state of the grid panel.");
+ ok(
+ !content.hidden && content.childElementCount > 0,
+ "The grid panel content is visible."
+ );
+ ok(
+ Services.prefs.getBoolPref(GRID_OPENED_PREF),
+ `${GRID_OPENED_PREF} is pref on by default.`
+ );
+
+ info("Clicking the grid header to hide the grid panel.");
+ header.click();
+
+ info("Checking the new state of the grid panel.");
+ ok(content.hidden, "The grid panel content is hidden.");
+ ok(
+ !Services.prefs.getBoolPref(GRID_OPENED_PREF),
+ `${GRID_OPENED_PREF} is pref off.`
+ );
+}
+
+async function testAccordionStateAfterSwitchingSidebars(inspector, doc) {
+ info(
+ "Checking the grid 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");
+
+ const item = await waitFor(() => doc.querySelector(GRID_PANE_SELECTOR));
+ const content = item.querySelector(ACCORDION_CONTENT_SELECTOR);
+
+ info("Checking the state of the grid panel.");
+ ok(content.hidden, "The grid panel content is hidden.");
+ ok(
+ !Services.prefs.getBoolPref(GRID_OPENED_PREF),
+ `${GRID_OPENED_PREF} is pref off.`
+ );
+}
+
+async function testAccordionStateAfterReopeningLayoutView(toolbox) {
+ info(
+ "Checking the grid 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 { gridInspector } = await openLayoutView();
+ const { document: doc } = gridInspector;
+
+ const item = await waitFor(() => doc.querySelector(GRID_PANE_SELECTOR));
+ const content = item.querySelector(ACCORDION_CONTENT_SELECTOR);
+
+ info("Checking the state of the grid panel.");
+ ok(content.hidden, "The grid panel content is hidden.");
+ ok(
+ !Services.prefs.getBoolPref(GRID_OPENED_PREF),
+ `${GRID_OPENED_PREF} is pref off.`
+ );
+}
diff --git a/devtools/client/inspector/grids/test/browser_grids_color-in-rules-grid-toggle.js b/devtools/client/inspector/grids/test/browser_grids_color-in-rules-grid-toggle.js
new file mode 100644
index 0000000000..a12f4f2435
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser_grids_color-in-rules-grid-toggle.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test toggling the grid highlighter in the rule view with changes to the grid color
+// from the layout view.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #grid {
+ display: grid;
+ }
+ </style>
+ <div id="grid">
+ <div id="cell1">cell1</div>
+ <div id="cell2">cell2</div>
+ </div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, gridInspector, layoutView } = await openLayoutView();
+ const { document: doc } = gridInspector;
+ const { store } = inspector;
+ const cPicker = layoutView.swatchColorPickerTooltip;
+ const spectrum = cPicker.spectrum;
+ const swatch = doc.querySelector(
+ "#layout-grid-container .layout-color-swatch"
+ );
+
+ info("Scrolling into view of the #grid color swatch.");
+ swatch.scrollIntoView();
+
+ info("Opening the color picker by clicking on the #grid color swatch.");
+ const onColorPickerReady = cPicker.once("ready");
+ swatch.click();
+ await onColorPickerReady;
+
+ await simulateColorPickerChange(cPicker, [0, 255, 0, 0.5]);
+
+ is(
+ swatch.style.backgroundColor,
+ "rgba(0, 255, 0, 0.5)",
+ "The color swatch's background was updated."
+ );
+
+ info("Pressing RETURN to commit the color change.");
+ const onGridColorUpdate = waitUntilState(
+ store,
+ state => state.grids[0].color === "#00FF0080"
+ );
+ const onColorPickerHidden = cPicker.tooltip.once("hidden");
+ focusAndSendKey(spectrum.element.ownerDocument.defaultView, "RETURN");
+ await onColorPickerHidden;
+ await onGridColorUpdate;
+
+ is(
+ swatch.style.backgroundColor,
+ "rgba(0, 255, 0, 0.5)",
+ "The color swatch's background was kept after RETURN."
+ );
+
+ info("Selecting the rule view.");
+ const ruleView = selectRuleView(inspector);
+ const highlighters = ruleView.highlighters;
+
+ await selectNode("#grid", inspector);
+
+ const container = getRuleViewProperty(ruleView, "#grid", "display").valueSpan;
+ const gridToggle = container.querySelector(".js-toggle-grid-highlighter");
+
+ info("Toggling ON the CSS grid highlighter from the rule-view.");
+ const onHighlighterShown = highlighters.once(
+ "grid-highlighter-shown",
+ (nodeFront, options) => {
+ info("Checking the grid highlighter display settings.");
+ const {
+ color,
+ showGridAreasOverlay,
+ showGridLineNumbers,
+ showInfiniteLines,
+ } = options;
+
+ is(color, "#00FF0080", "CSS grid highlighter color is correct.");
+ ok(!showGridAreasOverlay, "Show grid areas overlay option is off.");
+ ok(!showGridLineNumbers, "Show grid line numbers option is off.");
+ ok(!showInfiniteLines, "Show infinite lines option is off.");
+ }
+ );
+ gridToggle.click();
+ await onHighlighterShown;
+});
diff --git a/devtools/client/inspector/grids/test/browser_grids_display-setting-extend-grid-lines.js b/devtools/client/inspector/grids/test/browser_grids_display-setting-extend-grid-lines.js
new file mode 100644
index 0000000000..89f2bb36dd
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser_grids_display-setting-extend-grid-lines.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the 'Extend grid lines infinitely' grid highlighter setting will update
+// the redux store and pref setting.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #grid {
+ display: grid;
+ }
+ </style>
+ <div id="grid">
+ <div id="cell1">cell1</div>
+ <div id="cell2">cell2</div>
+ </div>
+`;
+
+const SHOW_INFINITE_LINES_PREF = "devtools.gridinspector.showInfiniteLines";
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, gridInspector } = await openLayoutView();
+ const { document: doc } = gridInspector;
+ const { store } = inspector;
+
+ await selectNode("#grid", inspector);
+ const checkbox = doc.getElementById("grid-setting-extend-grid-lines");
+
+ ok(
+ !Services.prefs.getBoolPref(SHOW_INFINITE_LINES_PREF),
+ "'Extend grid lines infinitely' is pref off by default."
+ );
+
+ info("Toggling ON the 'Extend grid lines infinitely' setting.");
+ let onCheckboxChange = waitUntilState(
+ store,
+ state => state.highlighterSettings.showInfiniteLines
+ );
+ checkbox.click();
+ await onCheckboxChange;
+
+ info("Toggling OFF the 'Extend grid lines infinitely' setting.");
+ onCheckboxChange = waitUntilState(
+ store,
+ state => !state.highlighterSettings.showInfiniteLines
+ );
+ checkbox.click();
+ await onCheckboxChange;
+
+ ok(
+ !Services.prefs.getBoolPref(SHOW_INFINITE_LINES_PREF),
+ "'Extend grid lines infinitely' is pref off."
+ );
+
+ Services.prefs.clearUserPref(SHOW_INFINITE_LINES_PREF);
+});
diff --git a/devtools/client/inspector/grids/test/browser_grids_display-setting-show-grid-areas.js b/devtools/client/inspector/grids/test/browser_grids_display-setting-show-grid-areas.js
new file mode 100644
index 0000000000..afc091b7ab
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser_grids_display-setting-show-grid-areas.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the 'Display grid areas' grid highlighter setting will update
+// the redux store and pref setting.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #grid {
+ display: grid;
+ }
+ </style>
+ <div id="grid">
+ <div id="cell1">cell1</div>
+ <div id="cell2">cell2</div>
+ </div>
+`;
+
+const SHOW_GRID_AREAS_PREF = "devtools.gridinspector.showGridAreas";
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, gridInspector } = await openLayoutView();
+ const { document: doc } = gridInspector;
+ const { store } = inspector;
+
+ await selectNode("#grid", inspector);
+ const checkbox = doc.getElementById("grid-setting-show-grid-areas");
+
+ ok(
+ !Services.prefs.getBoolPref(SHOW_GRID_AREAS_PREF),
+ "'Display grid areas' is pref off by default."
+ );
+
+ info("Toggling ON the 'Display grid areas' setting.");
+ let onCheckboxChange = waitUntilState(
+ store,
+ state => state.highlighterSettings.showGridAreasOverlay
+ );
+ checkbox.click();
+ await onCheckboxChange;
+
+ info("Toggling OFF the 'Display grid areas' setting.");
+ onCheckboxChange = waitUntilState(
+ store,
+ state => !state.highlighterSettings.showGridAreasOverlay
+ );
+ checkbox.click();
+ await onCheckboxChange;
+
+ ok(
+ !Services.prefs.getBoolPref(SHOW_GRID_AREAS_PREF),
+ "'Display grid areas' is pref off."
+ );
+
+ Services.prefs.clearUserPref(SHOW_GRID_AREAS_PREF);
+});
diff --git a/devtools/client/inspector/grids/test/browser_grids_display-setting-show-grid-line-numbers.js b/devtools/client/inspector/grids/test/browser_grids_display-setting-show-grid-line-numbers.js
new file mode 100644
index 0000000000..3dee2533cd
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser_grids_display-setting-show-grid-line-numbers.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the 'Display numbers on lines' grid highlighter setting will update
+// the redux store and pref setting.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #grid {
+ display: grid;
+ }
+ </style>
+ <div id="grid">
+ <div id="cell1">cell1</div>
+ <div id="cell2">cell2</div>
+ </div>
+`;
+
+const SHOW_GRID_LINE_NUMBERS = "devtools.gridinspector.showGridLineNumbers";
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, gridInspector } = await openLayoutView();
+ const { document: doc } = gridInspector;
+ const { store } = inspector;
+
+ await selectNode("#grid", inspector);
+ const checkbox = doc.getElementById("grid-setting-show-grid-line-numbers");
+
+ info("Checking the initial state of the CSS grid highlighter setting.");
+ ok(
+ !Services.prefs.getBoolPref(SHOW_GRID_LINE_NUMBERS),
+ "'Display numbers on lines' is pref off by default."
+ );
+
+ info("Toggling ON the 'Display numbers on lines' setting.");
+ let onCheckboxChange = waitUntilState(
+ store,
+ state => state.highlighterSettings.showGridLineNumbers
+ );
+ checkbox.click();
+ await onCheckboxChange;
+
+ ok(
+ Services.prefs.getBoolPref(SHOW_GRID_LINE_NUMBERS),
+ "'Display numbers on lines' is pref on."
+ );
+
+ info("Toggling OFF the 'Display numbers on lines' setting.");
+ onCheckboxChange = waitUntilState(
+ store,
+ state => !state.highlighterSettings.showGridLineNumbers
+ );
+ checkbox.click();
+ await onCheckboxChange;
+
+ ok(
+ !Services.prefs.getBoolPref(SHOW_GRID_LINE_NUMBERS),
+ "'Display numbers on lines' is pref off."
+ );
+
+ Services.prefs.clearUserPref(SHOW_GRID_LINE_NUMBERS);
+});
diff --git a/devtools/client/inspector/grids/test/browser_grids_grid-list-color-picker-on-ESC.js b/devtools/client/inspector/grids/test/browser_grids_grid-list-color-picker-on-ESC.js
new file mode 100644
index 0000000000..661c65c6e0
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser_grids_grid-list-color-picker-on-ESC.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the grid item's color change in the colorpicker is reverted when ESCAPE is
+// pressed.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #grid {
+ display: grid;
+ }
+ </style>
+ <div id="grid">
+ <div id="cell1">cell1</div>
+ <div id="cell2">cell2</div>
+ </div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, gridInspector, layoutView } = await openLayoutView();
+ const { document: doc } = gridInspector;
+ const { store } = inspector;
+ const cPicker = layoutView.swatchColorPickerTooltip;
+ const spectrum = cPicker.spectrum;
+ const swatch = doc.querySelector(
+ "#layout-grid-container .layout-color-swatch"
+ );
+
+ info("Checking the initial state of the Grid Inspector.");
+ is(
+ swatch.style.backgroundColor,
+ "rgb(148, 0, 255)",
+ "The color swatch's background is correct."
+ );
+ is(
+ store.getState().grids[0].color,
+ "#9400FF",
+ "The grid color state is correct."
+ );
+
+ info("Scrolling into view of the #grid color swatch.");
+ swatch.scrollIntoView();
+
+ info("Opening the color picker by clicking on the #grid color swatch.");
+ const onColorPickerReady = cPicker.once("ready");
+ swatch.click();
+ await onColorPickerReady;
+
+ await simulateColorPickerChange(cPicker, [0, 255, 0, 0.5]);
+
+ is(
+ swatch.style.backgroundColor,
+ "rgba(0, 255, 0, 0.5)",
+ "The color swatch's background was updated."
+ );
+
+ info("Pressing ESCAPE to close the tooltip.");
+ const onGridColorUpdate = waitUntilState(
+ store,
+ state => state.grids[0].color === "#9400FF"
+ );
+ const onColorPickerHidden = cPicker.tooltip.once("hidden");
+ focusAndSendKey(spectrum.element.ownerDocument.defaultView, "ESCAPE");
+ await onColorPickerHidden;
+ await onGridColorUpdate;
+
+ is(
+ swatch.style.backgroundColor,
+ "rgb(148, 0, 255)",
+ "The color swatch's background was reverted after ESCAPE."
+ );
+});
diff --git a/devtools/client/inspector/grids/test/browser_grids_grid-list-color-picker-on-RETURN.js b/devtools/client/inspector/grids/test/browser_grids_grid-list-color-picker-on-RETURN.js
new file mode 100644
index 0000000000..599730fb4a
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser_grids_grid-list-color-picker-on-RETURN.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the grid item's color change in the colorpicker is committed when RETURN is
+// pressed.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #grid {
+ display: grid;
+ }
+ </style>
+ <div id="grid">
+ <div id="cell1">cell1</div>
+ <div id="cell2">cell2</div>
+ </div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, gridInspector, layoutView } = await openLayoutView();
+ const { document: doc } = gridInspector;
+ const { store } = inspector;
+ const cPicker = layoutView.swatchColorPickerTooltip;
+ const spectrum = cPicker.spectrum;
+ const swatch = doc.querySelector(
+ "#layout-grid-container .layout-color-swatch"
+ );
+
+ info("Checking the initial state of the Grid Inspector.");
+ is(
+ swatch.style.backgroundColor,
+ "rgb(148, 0, 255)",
+ "The color swatch's background is correct."
+ );
+ is(
+ store.getState().grids[0].color,
+ "#9400FF",
+ "The grid color state is correct."
+ );
+
+ info("Scrolling into view of the #grid color swatch.");
+ swatch.scrollIntoView();
+
+ info("Opening the color picker by clicking on the #grid color swatch.");
+ const onColorPickerReady = cPicker.once("ready");
+ swatch.click();
+ await onColorPickerReady;
+
+ await simulateColorPickerChange(cPicker, [0, 255, 0, 0.5]);
+
+ is(
+ swatch.style.backgroundColor,
+ "rgba(0, 255, 0, 0.5)",
+ "The color swatch's background was updated."
+ );
+
+ info("Pressing RETURN to commit the color change.");
+ const onGridColorUpdate = waitUntilState(
+ store,
+ state => state.grids[0].color === "#00FF0080"
+ );
+ const onColorPickerHidden = cPicker.tooltip.once("hidden");
+ focusAndSendKey(spectrum.element.ownerDocument.defaultView, "RETURN");
+ await onColorPickerHidden;
+ await onGridColorUpdate;
+
+ is(
+ swatch.style.backgroundColor,
+ "rgba(0, 255, 0, 0.5)",
+ "The color swatch's background was kept after RETURN."
+ );
+});
diff --git a/devtools/client/inspector/grids/test/browser_grids_grid-list-element-rep.js b/devtools/client/inspector/grids/test/browser_grids_grid-list-element-rep.js
new file mode 100644
index 0000000000..a1fba8d5e4
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser_grids_grid-list-element-rep.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the grid item's element rep will display the box model higlighter on hover
+// and select the node on click.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #grid {
+ display: grid;
+ }
+ </style>
+ <div id="grid">
+ <div id="cell1">cell1</div>
+ <div id="cell2">cell2</div>
+ </div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, gridInspector } = await openLayoutView();
+ const { document: doc } = gridInspector;
+ const { store } = inspector;
+ const { waitForHighlighterTypeShown } = getHighlighterTestHelpers(inspector);
+
+ const gridList = doc.querySelector("#grid-list");
+ const elementRep = gridList.children[0].querySelector(".open-inspector");
+ info("Scrolling into the view the #grid element node rep.");
+ elementRep.scrollIntoView();
+
+ info("Listen to node-highlight event and mouse over the widget");
+ const onHighlight = waitForHighlighterTypeShown(
+ inspector.highlighters.TYPES.BOXMODEL
+ );
+ EventUtils.synthesizeMouse(
+ elementRep,
+ 10,
+ 5,
+ { type: "mouseover" },
+ doc.defaultView
+ );
+ const { nodeFront } = await onHighlight;
+
+ ok(nodeFront, "nodeFront was returned from highlighting the node.");
+ is(nodeFront.tagName, "DIV", "The highlighted node has the correct tagName.");
+ is(
+ nodeFront.attributes[0].name,
+ "id",
+ "The highlighted node has the correct attributes."
+ );
+ is(
+ nodeFront.attributes[0].value,
+ "grid",
+ "The highlighted node has the correct id."
+ );
+
+ const onSelection = inspector.selection.once("new-node-front");
+ EventUtils.sendMouseEvent({ type: "click" }, elementRep, doc.defaultView);
+ await onSelection;
+
+ is(
+ inspector.selection.nodeFront,
+ store.getState().grids[0].nodeFront,
+ "The selected node is the one stored on the grid item's state."
+ );
+});
diff --git a/devtools/client/inspector/grids/test/browser_grids_grid-list-no-grids.js b/devtools/client/inspector/grids/test/browser_grids_grid-list-no-grids.js
new file mode 100644
index 0000000000..ce9cbc7866
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser_grids_grid-list-no-grids.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that no grid list items and a "no grids available" message is displayed when
+// there are no grid containers on the page.
+
+const TEST_URI = `
+ <style type='text/css'>
+ </style>
+ <div id="grid">
+ <div id="cell1">cell1</div>
+ <div id="cell2">cell2</div>
+ </div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, gridInspector } = await openLayoutView();
+ const { document: doc } = gridInspector;
+ const { highlighters } = inspector;
+
+ await selectNode("#grid", inspector);
+ const noGridList = doc.querySelector(
+ "#layout-grid-section .devtools-sidepanel-no-result"
+ );
+ const gridList = doc.getElementById("grid-list");
+
+ info("Checking the initial state of the Grid Inspector.");
+ ok(noGridList, "The message no grid containers is displayed.");
+ ok(!gridList, "No grid containers are listed.");
+ ok(
+ !highlighters.gridHighlighters.size,
+ "No CSS grid highlighter exists in the highlighters overlay."
+ );
+});
diff --git a/devtools/client/inspector/grids/test/browser_grids_grid-list-on-iframe-reloaded.js b/devtools/client/inspector/grids/test/browser_grids_grid-list-on-iframe-reloaded.js
new file mode 100644
index 0000000000..b5871414f8
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser_grids_grid-list-on-iframe-reloaded.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the list of grids does refresh when an iframe containing a grid is removed
+// and re-created.
+// See bug 1378306 where this happened with jsfiddle.
+
+const TEST_URI = URL_ROOT + "doc_iframe_reloaded.html";
+
+add_task(async function () {
+ await addTab(TEST_URI);
+ const { gridInspector, inspector } = await openLayoutView();
+ const { document: doc } = gridInspector;
+ const { highlighters, store } = inspector;
+ const gridList = doc.getElementById("grid-list");
+ const checkbox = gridList.children[0].querySelector("input");
+
+ info("Clicking on the first checkbox to highlight the grid");
+ const onHighlighterShown = highlighters.once("grid-highlighter-shown");
+ const onCheckboxChange = waitUntilState(
+ store,
+ state => state.grids.length == 1 && state.grids[0].highlighted
+ );
+ checkbox.click();
+ await onHighlighterShown;
+ await onCheckboxChange;
+
+ ok(checkbox.checked, "The checkbox is checked");
+ is(gridList.childNodes.length, 1, "There's one grid in the list");
+ is(highlighters.gridHighlighters.size, 1, "There's a highlighter shown");
+ is(
+ highlighters.state.grids.size,
+ 1,
+ "There's a saved grid state to be restored."
+ );
+
+ info("Reload the iframe in content and expect the grid list to update");
+ const oldGrid = store.getState().grids[0];
+ const onNewListUnchecked = waitUntilState(
+ store,
+ state =>
+ state.grids.length == 1 &&
+ state.grids[0].actorID !== oldGrid.actorID &&
+ !state.grids[0].highlighted
+ );
+ const onHighlighterHidden = highlighters.once("grid-highlighter-hidden");
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [], () =>
+ content.wrappedJSObject.reloadIFrame()
+ );
+ await onNewListUnchecked;
+ await onHighlighterHidden;
+
+ is(gridList.childNodes.length, 1, "There's still one grid in the list");
+ ok(!highlighters.state.grids.size, "No grids to be restored on page reload.");
+});
diff --git a/devtools/client/inspector/grids/test/browser_grids_grid-list-on-mutation-element-added.js b/devtools/client/inspector/grids/test/browser_grids_grid-list-on-mutation-element-added.js
new file mode 100644
index 0000000000..e197f03b02
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser_grids_grid-list-on-mutation-element-added.js
@@ -0,0 +1,100 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the grid list updates when a new grid container is added to the page.
+
+const TEST_URI = `
+ <style type='text/css'>
+ .grid {
+ display: grid;
+ }
+ </style>
+ <div id="grid1" class="grid">
+ <div class="cell1">cell1</div>
+ <div class="cell2">cell2</div>
+ </div>
+ <div id="grid2">
+ <div class="cell1">cell1</div>
+ <div class="cell2">cell2</div>
+ </div>
+`;
+
+add_task(async function () {
+ await pushPref("devtools.gridinspector.maxHighlighters", 1);
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, gridInspector } = await openLayoutView();
+ const { document: doc } = gridInspector;
+ const { highlighters, store } = inspector;
+
+ await selectNode("#grid", inspector);
+ const gridList = doc.getElementById("grid-list");
+ const checkbox1 = gridList.children[0].querySelector("input");
+
+ info("Checking the initial state of the Grid Inspector.");
+ is(gridList.childNodes.length, 1, "One grid container is listed.");
+ ok(
+ !highlighters.gridHighlighters.size,
+ "No CSS grid highlighter exists in the highlighters overlay."
+ );
+
+ info("Toggling ON the CSS grid highlighter from the layout panel.");
+ let onHighlighterShown = highlighters.once("grid-highlighter-shown");
+ checkbox1.click();
+ await onHighlighterShown;
+
+ info("Checking the CSS grid highlighter is created.");
+ is(highlighters.gridHighlighters.size, 1, "CSS grid highlighter is shown.");
+
+ info("Adding the #grid2 container in the content page.");
+ const onGridListUpdate = waitUntilState(
+ store,
+ state =>
+ state.grids.length == 2 &&
+ state.grids[0].highlighted &&
+ !state.grids[1].highlighted
+ );
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [], () =>
+ content.document.getElementById("grid2").classList.add("grid")
+ );
+ await onGridListUpdate;
+
+ info("Checking the new Grid Inspector state.");
+ is(gridList.childNodes.length, 2, "Two grid containers are listed.");
+ is(highlighters.gridHighlighters.size, 1, "CSS grid highlighter is shown.");
+
+ const checkbox2 = gridList.children[1].querySelector("input");
+
+ info("Toggling ON the CSS grid highlighter for #grid2.");
+ onHighlighterShown = highlighters.once("grid-highlighter-shown");
+ let onCheckboxChange = waitUntilState(
+ store,
+ state =>
+ state.grids.length == 2 &&
+ !state.grids[0].highlighted &&
+ state.grids[1].highlighted
+ );
+ checkbox2.click();
+ await onHighlighterShown;
+ await onCheckboxChange;
+
+ info("Checking the CSS grid highlighter is still shown.");
+ is(highlighters.gridHighlighters.size, 1, "CSS grid highlighter is shown.");
+
+ info("Toggling OFF the CSS grid highlighter from the layout panel.");
+ const onHighlighterHidden = highlighters.once("grid-highlighter-hidden");
+ onCheckboxChange = waitUntilState(
+ store,
+ state =>
+ state.grids.length == 2 &&
+ !state.grids[0].highlighted &&
+ !state.grids[1].highlighted
+ );
+ checkbox2.click();
+ await onHighlighterHidden;
+ await onCheckboxChange;
+
+ info("Checking the CSS grid highlighter is not shown.");
+ ok(!highlighters.gridHighlighters.size, "No CSS grid highlighter is shown.");
+});
diff --git a/devtools/client/inspector/grids/test/browser_grids_grid-list-on-mutation-element-removed.js b/devtools/client/inspector/grids/test/browser_grids_grid-list-on-mutation-element-removed.js
new file mode 100644
index 0000000000..7a60759071
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser_grids_grid-list-on-mutation-element-removed.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the grid item is removed from the grid list when the grid container is
+// removed from the page.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #grid {
+ display: grid;
+ }
+ </style>
+ <div id="grid">
+ <div id="cell1">cell1</div>
+ <div id="cell2">cell2</div>
+ </div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, gridInspector } = await openLayoutView();
+ const { document: doc } = gridInspector;
+ const { highlighters, store } = inspector;
+
+ await selectNode("#grid", inspector);
+ const gridList = doc.getElementById("grid-list");
+ const checkbox = gridList.children[0].querySelector("input");
+
+ info("Checking the initial state of the Grid Inspector.");
+ is(gridList.childNodes.length, 1, "One grid container is listed.");
+ ok(!highlighters.gridHighlighters.size, "No CSS grid highlighter is shown.");
+
+ info("Toggling ON the CSS grid highlighter from the layout panel.");
+ const onHighlighterShown = highlighters.once("grid-highlighter-shown");
+ let onCheckboxChange = waitUntilState(
+ store,
+ state => state.grids.length == 1 && state.grids[0].highlighted
+ );
+ checkbox.click();
+ await onHighlighterShown;
+ await onCheckboxChange;
+
+ info("Checking the CSS grid highlighter is created.");
+ is(highlighters.gridHighlighters.size, 1, "CSS grid highlighter is shown.");
+
+ info("Removing the #grid container in the content page.");
+ const onHighlighterHidden = highlighters.once("grid-highlighter-hidden");
+ onCheckboxChange = waitUntilState(store, state => !state.grids.length);
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [], () =>
+ content.document.getElementById("grid").remove()
+ );
+ await onHighlighterHidden;
+ await onCheckboxChange;
+
+ info("Checking the CSS grid highlighter is not shown.");
+ ok(!highlighters.gridHighlighters.size, "No CSS grid highlighter is shown.");
+ const noGridList = doc.querySelector(
+ "#layout-grid-section .devtools-sidepanel-no-result"
+ );
+ ok(noGridList, "The message no grid containers is displayed.");
+});
diff --git a/devtools/client/inspector/grids/test/browser_grids_grid-list-on-target-added-removed.js b/devtools/client/inspector/grids/test/browser_grids_grid-list-on-target-added-removed.js
new file mode 100644
index 0000000000..051fc14e53
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser_grids_grid-list-on-target-added-removed.js
@@ -0,0 +1,203 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the list of grids does refresh when targets are added or removed (e.g. when
+// there's a navigation and iframe are added or removed)
+
+add_task(async function () {
+ await addTab(getDocumentBuilderUrl("example.com", "top-level-com-grid"));
+ const { gridInspector } = await openLayoutView();
+ const { document: doc } = gridInspector;
+
+ const checkGridList = (expected, assertionMessage) =>
+ checkGridListItems(doc, expected, assertionMessage);
+
+ checkGridList(
+ ["div#top-level-com-grid"],
+ "One grid item is displayed at first"
+ );
+
+ info(
+ "Check that adding same-origin iframe with a grid will update the grid list"
+ );
+ const sameOriginIframeBrowsingContext = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [getDocumentBuilderUrl("example.com", "iframe-com-grid")],
+ src => {
+ const iframe = content.document.createElement("iframe");
+ iframe.id = "same-origin";
+ iframe.src = src;
+ content.document.body.append(iframe);
+ return iframe.browsingContext;
+ }
+ );
+
+ await waitFor(() => getGridListItems(doc).length == 2);
+ checkGridList(
+ ["div#top-level-com-grid", "div#iframe-com-grid"],
+ "The same-origin iframe grid is displayed"
+ );
+
+ info("Check that adding remote iframe with a grid will update the grid list");
+ const remoteIframeBrowsingContext = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [getDocumentBuilderUrl("example.org", "iframe-org-grid")],
+ src => {
+ const iframe = content.document.createElement("iframe");
+ iframe.id = "remote";
+ iframe.src = src;
+ content.document.body.append(iframe);
+ return iframe.browsingContext;
+ }
+ );
+
+ await waitFor(() => getGridListItems(doc).length == 3);
+ checkGridList(
+ ["div#top-level-com-grid", "div#iframe-com-grid", "div#iframe-org-grid"],
+ "The remote iframe grid is displayed"
+ );
+
+ info("Check that adding new grids in iframes does update the grid list");
+ SpecialPowers.spawn(sameOriginIframeBrowsingContext, [], () => {
+ const section = content.document.createElement("section");
+ section.id = "com-added-grid-container";
+ section.style = "display: grid;";
+ content.document.body.append(section);
+ });
+
+ await waitFor(() => getGridListItems(doc).length == 4);
+ checkGridList(
+ [
+ "div#top-level-com-grid",
+ "div#iframe-com-grid",
+ "section#com-added-grid-container",
+ "div#iframe-org-grid",
+ ],
+ "The new grid in the same origin iframe is displayed"
+ );
+
+ SpecialPowers.spawn(remoteIframeBrowsingContext, [], () => {
+ const section = content.document.createElement("section");
+ section.id = "org-added-grid-container";
+ section.style = "display: grid;";
+ content.document.body.append(section);
+ });
+
+ await waitFor(() => getGridListItems(doc).length == 5);
+ checkGridList(
+ [
+ "div#top-level-com-grid",
+ "div#iframe-com-grid",
+ "section#com-added-grid-container",
+ "div#iframe-org-grid",
+ "section#org-added-grid-container",
+ ],
+ "The new grid in the same origin iframe is displayed"
+ );
+
+ info("Check that removing iframes will update the grid list");
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.document.querySelector("iframe#same-origin").remove();
+ });
+
+ await waitFor(() => getGridListItems(doc).length == 3);
+ checkGridList(
+ [
+ "div#top-level-com-grid",
+ "div#iframe-org-grid",
+ "section#org-added-grid-container",
+ ],
+ "The same-origin iframe grids were removed from the list"
+ );
+
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.document.querySelector("iframe#remote").remove();
+ });
+
+ await waitFor(() => getGridListItems(doc).length == 1);
+ checkGridList(
+ ["div#top-level-com-grid"],
+ "The remote iframe grids were removed as well"
+ );
+
+ info("Navigate to a new origin");
+ await navigateTo(getDocumentBuilderUrl("example.org", "top-level-org-grid"));
+ await waitFor(() => {
+ const listItems = getGridListItems(doc);
+ return (
+ listItems.length == 1 &&
+ listItems[0].textContent.includes("#top-level-org-grid")
+ );
+ });
+ checkGridList(
+ ["div#top-level-org-grid"],
+ "The grid from the new origin document is displayed"
+ );
+
+ info("Check that adding remote iframe will still update the grid list");
+ SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [getDocumentBuilderUrl("example.com", "iframe-com-grid-remote")],
+ src => {
+ const iframe = content.document.createElement("iframe");
+ iframe.id = "remote";
+ iframe.src = src;
+ content.document.body.append(iframe);
+ }
+ );
+
+ await waitFor(() => getGridListItems(doc).length == 2);
+ checkGridList(
+ ["div#top-level-org-grid", "div#iframe-com-grid-remote"],
+ "The grid from the new origin document is displayed"
+ );
+
+ info(
+ "Check that adding same-origin iframe with a grid will update the grid list"
+ );
+ SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [getDocumentBuilderUrl("example.org", "iframe-org-grid-same-origin")],
+ src => {
+ const iframe = content.document.createElement("iframe");
+ iframe.id = "same-origin";
+ iframe.src = src;
+ content.document.body.append(iframe);
+ }
+ );
+ await waitFor(() => getGridListItems(doc).length == 3);
+ checkGridList(
+ [
+ "div#top-level-org-grid",
+ "div#iframe-com-grid-remote",
+ "div#iframe-org-grid-same-origin",
+ ],
+ "The grid from the new same-origin iframe is displayed"
+ );
+});
+
+function getDocumentBuilderUrl(origin, gridContainerId) {
+ return `https://${origin}/document-builder.sjs?html=${encodeURIComponent(
+ `<style>
+ #${gridContainerId} {
+ display: grid;
+ }
+ </style>
+ <div id="${gridContainerId}"></div>`
+ )}`;
+}
+
+function getGridListItems(doc) {
+ return Array.from(doc.querySelectorAll("#grid-list .objectBox-node"));
+}
+
+function checkGridListItems(doc, expectedItems, assertionText) {
+ const gridItems = getGridListItems(doc).map(el => el.textContent);
+ is(
+ JSON.stringify(gridItems.sort()),
+ JSON.stringify(expectedItems.sort()),
+ assertionText
+ );
+}
diff --git a/devtools/client/inspector/grids/test/browser_grids_grid-list-subgrids-z-order.js b/devtools/client/inspector/grids/test/browser_grids_grid-list-subgrids-z-order.js
new file mode 100644
index 0000000000..da1b11f4c8
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser_grids_grid-list-subgrids-z-order.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the z order of grids.
+
+const TEST_URI = URL_ROOT + "doc_subgrid.html";
+
+add_task(async () => {
+ await addTab(TEST_URI);
+ const { gridInspector, inspector } = await openLayoutView();
+ const { document: doc } = gridInspector;
+ const { highlighters, store } = inspector;
+
+ await selectNode("#grid", inspector);
+ await waitUntilState(store, state => state.grids.length === 5);
+
+ const parentEl = doc.getElementById("grid-list");
+ // Input for .container
+ const parentInput = parentEl.children[0].querySelector("input");
+ const subgridEl = parentEl.children[1];
+ // Input for <main>
+ const subgridInput = subgridEl.children[1].querySelector("input");
+ const grandSubgridEl = subgridEl.children[2];
+ // Input for .aside1
+ const grandSubgridInput = grandSubgridEl.children[0].querySelector("input");
+
+ info(
+ "Toggling ON the CSS grid highlighters for .container, <main> and .aside1"
+ );
+ const grandSubgridFront = await toggle(grandSubgridInput, highlighters);
+ const subgridFront = await toggle(subgridInput, highlighters);
+ let parentFront = await toggle(parentInput, highlighters);
+ await waitUntilState(
+ store,
+ state => state.grids.filter(g => g.highlighted).length === 3
+ );
+
+ info("Check z-index of grid highlighting");
+ is(getZIndex(store, parentFront), 0, "z-index of parent grid is 0");
+ is(getZIndex(store, subgridFront), 1, "z-index of subgrid is 1");
+ is(getZIndex(store, grandSubgridFront), 2, "z-index of subgrid is 2");
+
+ info("Toggling OFF the CSS grid highlighters for .container");
+ await toggle(parentInput, highlighters);
+ await waitUntilState(
+ store,
+ state => state.grids.filter(g => g.highlighted).length === 2
+ );
+
+ info("Check z-index keeps even if the parent grid is hidden");
+ is(getZIndex(store, subgridFront), 1, "z-index of subgrid is 1");
+ is(getZIndex(store, grandSubgridFront), 2, "z-index of subgrid is 2");
+
+ info("Toggling ON again the CSS grid highlighters for .container");
+ parentFront = await toggle(parentInput, highlighters);
+ await waitUntilState(
+ store,
+ state => state.grids.filter(g => g.highlighted).length === 3
+ );
+
+ info("Check z-index of all of grids highlighting keeps");
+ is(getZIndex(store, parentFront), 0, "z-index of parent grid is 0");
+ is(getZIndex(store, subgridFront), 1, "z-index of subgrid is 1");
+ is(getZIndex(store, grandSubgridFront), 2, "z-index of subgrid is 2");
+});
+
+function getZIndex(store, nodeFront) {
+ const grids = store.getState().grids;
+ const gridData = grids.find(g => g.nodeFront === nodeFront);
+ return gridData.zIndex;
+}
+
+async function toggle(input, highlighters) {
+ const eventName = input.checked
+ ? "grid-highlighter-hidden"
+ : "grid-highlighter-shown";
+ const onHighlighterEvent = highlighters.once(eventName);
+ input.click();
+ const nodeFront = await onHighlighterEvent;
+ return nodeFront;
+}
diff --git a/devtools/client/inspector/grids/test/browser_grids_grid-list-subgrids_01.js b/devtools/client/inspector/grids/test/browser_grids_grid-list-subgrids_01.js
new file mode 100644
index 0000000000..bcb0faeef8
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser_grids_grid-list-subgrids_01.js
@@ -0,0 +1,138 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the list of grids show the subgrids in the correct nested list and toggling
+// the CSS grid highlighter for a subgrid.
+
+const TEST_URI = URL_ROOT + "doc_subgrid.html";
+
+add_task(async function () {
+ await addTab(TEST_URI);
+ const { gridInspector, inspector } = await openLayoutView();
+ const { document: doc } = gridInspector;
+ const { highlighters, store } = inspector;
+
+ await selectNode(".container", inspector);
+ const gridListEl = doc.getElementById("grid-list");
+ const containerSubgridListEl = gridListEl.children[1];
+ const mainSubgridListEl = containerSubgridListEl.querySelector("ul");
+
+ info("Checking the initial state of the Grid Inspector.");
+ is(
+ getGridItemElements(gridListEl).length,
+ 1,
+ "One grid container is listed."
+ );
+ is(
+ getGridItemElements(containerSubgridListEl).length,
+ 2,
+ "Got the correct number of subgrids in div.container"
+ );
+ is(
+ getGridItemElements(mainSubgridListEl).length,
+ 2,
+ "Got the correct number of subgrids in main.subgrid"
+ );
+ ok(
+ !highlighters.gridHighlighters.size &&
+ !highlighters.parentGridHighlighters.size,
+ "No CSS grid highlighter is shown."
+ );
+
+ info("Toggling ON the CSS grid highlighter for header.");
+ let onHighlighterShown = highlighters.once("grid-highlighter-shown");
+ let onCheckboxChange = waitUntilState(
+ store,
+ state => state.grids[1].highlighted
+ );
+ let checkbox = containerSubgridListEl.children[0].querySelector("input");
+ checkbox.click();
+ await onHighlighterShown;
+ await onCheckboxChange;
+
+ info(
+ "Checking the CSS grid highlighter and parent grid highlighter are created."
+ );
+ is(highlighters.gridHighlighters.size, 1, "CSS grid highlighter is shown.");
+ is(
+ highlighters.parentGridHighlighters.size,
+ 1,
+ "CSS grid highlighter for parent grid container is shown."
+ );
+
+ info("Toggling ON the CSS grid highlighter for main.");
+ onHighlighterShown = highlighters.once("grid-highlighter-shown");
+ onCheckboxChange = waitUntilState(
+ store,
+ state => state.grids[1].highlighted && state.grids[2].highlighted
+ );
+ checkbox = containerSubgridListEl.children[1].querySelector("input");
+ checkbox.click();
+ await onHighlighterShown;
+ await onCheckboxChange;
+
+ info("Checking the number of CSS grid highlighters present.");
+ is(
+ highlighters.gridHighlighters.size,
+ 2,
+ "Got the correct number of CSS grid highlighter shown."
+ );
+ is(
+ highlighters.parentGridHighlighters.size,
+ 1,
+ "Only 1 parent grid highlighter should be shown for the same subgrid parent."
+ );
+
+ info("Toggling OFF the CSS grid highlighter for main.");
+ let onHighlighterHidden = highlighters.once("grid-highlighter-hidden");
+ onCheckboxChange = waitUntilState(
+ store,
+ state => state.grids[1].highlighted && !state.grids[2].highlighted
+ );
+ checkbox.click();
+ await onHighlighterHidden;
+ await onCheckboxChange;
+
+ info("Checking the number of CSS grid highlighters present.");
+ is(
+ highlighters.gridHighlighters.size,
+ 1,
+ "Got the correct number of CSS grid highlighter shown."
+ );
+ is(
+ highlighters.parentGridHighlighters.size,
+ 1,
+ "Got the correct number of CSS grid parent highlighter shown."
+ );
+
+ info("Toggling OFF the CSS grid highlighter for header.");
+ onHighlighterHidden = highlighters.once("grid-highlighter-hidden");
+ onCheckboxChange = waitUntilState(
+ store,
+ state => !state.grids[1].highlighted
+ );
+ checkbox = containerSubgridListEl.children[0].querySelector("input");
+ checkbox.click();
+ await onHighlighterHidden;
+ await onCheckboxChange;
+
+ info("Checking the CSS grid highlighter is not shown.");
+ ok(
+ !highlighters.gridHighlighters.size &&
+ !highlighters.parentGridHighlighters.size,
+ "No CSS grid highlighter is shown."
+ );
+});
+
+/**
+ * Returns the grid item elements <li> from the grid list element <ul>.
+ *
+ * @param {Element} gridListEl
+ * The grid list element <ul>.
+ * @return {Array<Element>} containing the grid item elements <li>.
+ */
+function getGridItemElements(gridListEl) {
+ return [...gridListEl.children].filter(node => node.nodeName === "li");
+}
diff --git a/devtools/client/inspector/grids/test/browser_grids_grid-list-subgrids_02.js b/devtools/client/inspector/grids/test/browser_grids_grid-list-subgrids_02.js
new file mode 100644
index 0000000000..83f07e339a
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser_grids_grid-list-subgrids_02.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the state of grid highlighters after toggling the checkbox of subgrids.
+
+const TEST_URI = URL_ROOT + "doc_subgrid.html";
+
+add_task(async () => {
+ await addTab(TEST_URI);
+ const { gridInspector, inspector } = await openLayoutView();
+ const { document: doc } = gridInspector;
+ const { highlighters, store } = inspector;
+
+ await selectNode(".container", inspector);
+ const parentEl = doc.getElementById("grid-list");
+ // Input for .container
+ const parentInput = parentEl.children[0].querySelector("input");
+ const subgridEl = parentEl.children[1];
+ // Input for <main>
+ const subgridInput = subgridEl.children[1].querySelector("input");
+ const grandSubgridEl = subgridEl.children[2];
+ // Input for .aside1
+ const grandSubgridInput = grandSubgridEl.children[0].querySelector("input");
+
+ info(
+ "Toggling ON the CSS grid highlighters for .container, <main> and .aside1"
+ );
+ await toggleHighlighter(parentInput, highlighters);
+ await toggleHighlighter(subgridInput, highlighters);
+ await toggleHighlighter(grandSubgridInput, highlighters);
+ await waitUntilState(
+ store,
+ state => state.grids.filter(g => g.highlighted).length === 3
+ );
+
+ info("Check the state of highlighters");
+ is(
+ highlighters.gridHighlighters.size,
+ 3,
+ "All highlighters are use as normal highlighter"
+ );
+
+ info("Toggling OFF the CSS grid highlighter for <main>");
+ await toggleHighlighter(subgridInput, highlighters);
+ await waitUntilState(
+ store,
+ state => state.grids.filter(g => g.highlighted).length === 2
+ );
+
+ info("Check the state of highlighters after hiding subgrid for <main>");
+ is(
+ highlighters.gridHighlighters.size,
+ 2,
+ "2 highlighters are use as normal highlighter"
+ );
+ is(
+ highlighters.parentGridHighlighters.size,
+ 1,
+ "The highlighter for <main> is used as parent highlighter"
+ );
+});
+
+async function toggleHighlighter(input, highlighters) {
+ const eventName = input.checked
+ ? "grid-highlighter-hidden"
+ : "grid-highlighter-shown";
+ const onHighlighterEvent = highlighters.once(eventName);
+ input.click();
+ await onHighlighterEvent;
+}
diff --git a/devtools/client/inspector/grids/test/browser_grids_grid-list-toggle-grids_01.js b/devtools/client/inspector/grids/test/browser_grids_grid-list-toggle-grids_01.js
new file mode 100644
index 0000000000..f66e70042c
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser_grids_grid-list-toggle-grids_01.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests toggling ON/OFF the grid highlighter from the grid ispector panel.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #grid {
+ display: grid;
+ }
+ </style>
+ <div id="grid">
+ <div id="cell1">cell1</div>
+ <div id="cell2">cell2</div>
+ </div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { gridInspector, inspector } = await openLayoutView();
+ const { document: doc } = gridInspector;
+ const { highlighters, store } = inspector;
+
+ await selectNode("#grid", inspector);
+ const gridList = doc.getElementById("grid-list");
+ const checkbox = gridList.children[0].querySelector("input");
+
+ info("Checking the initial state of the Grid Inspector.");
+ is(gridList.childNodes.length, 1, "One grid container is listed.");
+ ok(
+ !checkbox.checked,
+ `Grid item ${checkbox.value} is unchecked in the grid list.`
+ );
+ ok(!highlighters.gridHighlighters.size, "No CSS grid highlighter is shown.");
+
+ info("Toggling ON the CSS grid highlighter from the layout panel.");
+ const onHighlighterShown = highlighters.once("grid-highlighter-shown");
+ let onCheckboxChange = waitUntilState(
+ store,
+ state => state.grids.length == 1 && state.grids[0].highlighted
+ );
+ checkbox.click();
+ await onHighlighterShown;
+ await onCheckboxChange;
+
+ info("Checking the CSS grid highlighter is created.");
+ is(highlighters.gridHighlighters.size, 1, "CSS grid highlighter is shown.");
+
+ info("Toggling OFF the CSS grid highlighter from the layout panel.");
+ const onHighlighterHidden = highlighters.once("grid-highlighter-hidden");
+ onCheckboxChange = waitUntilState(
+ store,
+ state => state.grids.length == 1 && !state.grids[0].highlighted
+ );
+ checkbox.click();
+ await onHighlighterHidden;
+ await onCheckboxChange;
+
+ info("Checking the CSS grid highlighter is not shown.");
+ ok(!highlighters.gridHighlighters.size, "No CSS grid highlighter is shown.");
+});
diff --git a/devtools/client/inspector/grids/test/browser_grids_grid-list-toggle-grids_02.js b/devtools/client/inspector/grids/test/browser_grids_grid-list-toggle-grids_02.js
new file mode 100644
index 0000000000..82f3d9bef4
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser_grids_grid-list-toggle-grids_02.js
@@ -0,0 +1,96 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test toggling the grid highlighter in the grid inspector panel with multiple grids in
+// the page.
+
+const TEST_URI = `
+ <style type='text/css'>
+ .grid {
+ display: grid;
+ }
+ </style>
+ <div id="grid1" class="grid">
+ <div class="cell1">cell1</div>
+ <div class="cell2">cell2</div>
+ </div>
+ <div id="grid2" class="grid">
+ <div class="cell1">cell1</div>
+ <div class="cell2">cell2</div>
+ </div>
+`;
+
+add_task(async function () {
+ await pushPref("devtools.gridinspector.maxHighlighters", 1);
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, gridInspector } = await openLayoutView();
+ const { document: doc } = gridInspector;
+ const { highlighters, store } = inspector;
+
+ await selectNode("#grid1", inspector);
+ const gridList = doc.getElementById("grid-list");
+ const checkbox1 = gridList.children[0].querySelector("input");
+ const checkbox2 = gridList.children[1].querySelector("input");
+
+ info("Checking the initial state of the Grid Inspector.");
+ is(gridList.childNodes.length, 2, "2 grid containers are listed.");
+ ok(
+ !checkbox1.checked,
+ `Grid item ${checkbox1.value} is unchecked in the grid list.`
+ );
+ ok(
+ !checkbox2.checked,
+ `Grid item ${checkbox2.value} is unchecked in the grid list.`
+ );
+ ok(!highlighters.gridHighlighters.size, "No CSS grid highlighter is shown.");
+
+ info("Toggling ON the CSS grid highlighter for #grid1.");
+ let onHighlighterShown = highlighters.once("grid-highlighter-shown");
+ let onCheckboxChange = waitUntilState(
+ store,
+ state =>
+ state.grids.length == 2 &&
+ state.grids[0].highlighted &&
+ !state.grids[1].highlighted
+ );
+ checkbox1.click();
+ await onHighlighterShown;
+ await onCheckboxChange;
+
+ info("Checking the CSS grid highlighter is created.");
+ is(highlighters.gridHighlighters.size, 1, "CSS grid highlighter is shown.");
+
+ info("Toggling ON the CSS grid highlighter for #grid2.");
+ onHighlighterShown = highlighters.once("grid-highlighter-shown");
+ onCheckboxChange = waitUntilState(
+ store,
+ state =>
+ state.grids.length == 2 &&
+ !state.grids[0].highlighted &&
+ state.grids[1].highlighted
+ );
+ checkbox2.click();
+ await onHighlighterShown;
+ await onCheckboxChange;
+
+ info("Checking the CSS grid highlighter is still shown.");
+ is(highlighters.gridHighlighters.size, 1, "CSS grid highlighter is shown.");
+
+ info("Toggling OFF the CSS grid highlighter from the layout panel.");
+ const onHighlighterHidden = highlighters.once("grid-highlighter-hidden");
+ onCheckboxChange = waitUntilState(
+ store,
+ state =>
+ state.grids.length == 2 &&
+ !state.grids[0].highlighted &&
+ !state.grids[1].highlighted
+ );
+ checkbox2.click();
+ await onHighlighterHidden;
+ await onCheckboxChange;
+
+ info("Checking the CSS grid highlighter is not shown.");
+ ok(!highlighters.gridHighlighters.size, "No CSS grid highlighter is shown.");
+});
diff --git a/devtools/client/inspector/grids/test/browser_grids_grid-list-toggle-multiple-grids.js b/devtools/client/inspector/grids/test/browser_grids_grid-list-toggle-multiple-grids.js
new file mode 100644
index 0000000000..857ff75912
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser_grids_grid-list-toggle-multiple-grids.js
@@ -0,0 +1,243 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test toggling multiple grid highlighters in the grid inspector panel.
+
+const TEST_URI = `
+ <style type='text/css'>
+ .grid {
+ display: grid;
+ }
+ </style>
+ <div id="grid1" class="grid">
+ <div class="cell1">cell1</div>
+ <div class="cell2">cell2</div>
+ </div>
+ <div id="grid2" class="grid">
+ <div class="cell1">cell1</div>
+ <div class="cell2">cell2</div>
+ </div>
+ <div id="grid3" class="grid">
+ <div class="cell1">cell1</div>
+ <div class="cell2">cell2</div>
+ </div>
+ <div id="grid4" class="grid">
+ <div class="cell1">cell1</div>
+ <div class="cell2">cell2</div>
+ </div>
+`;
+
+add_task(async function () {
+ await pushPref("devtools.gridinspector.maxHighlighters", 3);
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, gridInspector } = await openLayoutView();
+ const { document: doc } = gridInspector;
+ const { highlighters, store } = inspector;
+
+ await selectNode("#grid1", inspector);
+ const gridList = doc.getElementById("grid-list");
+ const checkbox1 = gridList.children[0].querySelector("input");
+ const checkbox2 = gridList.children[1].querySelector("input");
+ const checkbox3 = gridList.children[2].querySelector("input");
+ const checkbox4 = gridList.children[3].querySelector("input");
+
+ info("Checking the initial state of the Grid Inspector.");
+ is(gridList.childNodes.length, 4, "4 grid containers are listed.");
+ ok(
+ !checkbox1.checked,
+ `Grid item ${checkbox1.value} is unchecked in the grid list.`
+ );
+ ok(
+ !checkbox2.checked,
+ `Grid item ${checkbox2.value} is unchecked in the grid list.`
+ );
+ ok(
+ !checkbox3.checked,
+ `Grid item ${checkbox3.value} is unchecked in the grid list.`
+ );
+ ok(
+ !checkbox4.checked,
+ `Grid item ${checkbox4.value} is unchecked in the grid list.`
+ );
+ ok(!highlighters.gridHighlighters.size, "No CSS grid highlighter is shown.");
+
+ info("Toggling ON the CSS grid highlighter for #grid1.");
+ let onHighlighterShown = highlighters.once("grid-highlighter-shown");
+ let onCheckboxChange = waitUntilState(
+ store,
+ state =>
+ state.grids.length == 4 &&
+ state.grids[0].highlighted &&
+ !state.grids[0].disabled &&
+ !state.grids[1].highlighted &&
+ !state.grids[1].disabled &&
+ !state.grids[2].highlighted &&
+ !state.grids[2].disabled &&
+ !state.grids[3].highlighted &&
+ !state.grids[3].disabled
+ );
+ checkbox1.click();
+ await onHighlighterShown;
+ await onCheckboxChange;
+
+ info(
+ "Check that the CSS grid highlighter is created and the saved grid state."
+ );
+ is(
+ highlighters.gridHighlighters.size,
+ 1,
+ "Got expected number of grid highlighters shown."
+ );
+ is(
+ highlighters.state.grids.size,
+ 1,
+ "Got expected number of grids in the saved state"
+ );
+
+ info("Toggling ON the CSS grid highlighter for #grid2.");
+ onHighlighterShown = highlighters.once("grid-highlighter-shown");
+ onCheckboxChange = waitUntilState(
+ store,
+ state =>
+ state.grids.length == 4 &&
+ state.grids[0].highlighted &&
+ !state.grids[0].disabled &&
+ state.grids[1].highlighted &&
+ !state.grids[1].disabled &&
+ !state.grids[2].highlighted &&
+ !state.grids[2].disabled &&
+ !state.grids[3].highlighted &&
+ !state.grids[3].disabled
+ );
+ checkbox2.click();
+ await onHighlighterShown;
+ await onCheckboxChange;
+
+ is(
+ highlighters.gridHighlighters.size,
+ 2,
+ "Got expected number of grid highlighters shown."
+ );
+ is(
+ highlighters.state.grids.size,
+ 2,
+ "Got expected number of grids in the saved state"
+ );
+
+ info("Toggling ON the CSS grid highlighter for #grid3.");
+ onHighlighterShown = highlighters.once("grid-highlighter-shown");
+ onCheckboxChange = waitUntilState(
+ store,
+ state =>
+ state.grids.length == 4 &&
+ state.grids[0].highlighted &&
+ !state.grids[0].disabled &&
+ state.grids[1].highlighted &&
+ !state.grids[1].disabled &&
+ state.grids[2].highlighted &&
+ !state.grids[2].disabled &&
+ !state.grids[3].highlighted &&
+ state.grids[3].disabled
+ );
+ checkbox3.click();
+ await onHighlighterShown;
+ await onCheckboxChange;
+
+ is(
+ highlighters.gridHighlighters.size,
+ 3,
+ "Got expected number of grid highlighters shown."
+ );
+ is(
+ highlighters.state.grids.size,
+ 3,
+ "Got expected number of grids in the saved state"
+ );
+
+ info("Toggling OFF the CSS grid highlighter for #grid3.");
+ let onHighlighterHidden = highlighters.once("grid-highlighter-hidden");
+ onCheckboxChange = waitUntilState(
+ store,
+ state =>
+ state.grids.length == 4 &&
+ state.grids[0].highlighted &&
+ !state.grids[0].disabled &&
+ state.grids[1].highlighted &&
+ !state.grids[1].disabled &&
+ !state.grids[2].highlighted &&
+ !state.grids[2].disabled &&
+ !state.grids[3].highlighted &&
+ !state.grids[3].disabled
+ );
+ checkbox3.click();
+ await onHighlighterHidden;
+ await onCheckboxChange;
+
+ is(
+ highlighters.gridHighlighters.size,
+ 2,
+ "Got expected number of grid highlighters shown."
+ );
+ is(
+ highlighters.state.grids.size,
+ 2,
+ "Got expected number of grids in the saved state"
+ );
+
+ info("Toggling OFF the CSS grid highlighter for #grid2.");
+ onHighlighterHidden = highlighters.once("grid-highlighter-hidden");
+ onCheckboxChange = waitUntilState(
+ store,
+ state =>
+ state.grids.length == 4 &&
+ state.grids[0].highlighted &&
+ !state.grids[0].disabled &&
+ !state.grids[1].highlighted &&
+ !state.grids[1].disabled &&
+ !state.grids[2].highlighted &&
+ !state.grids[2].disabled &&
+ !state.grids[3].highlighted &&
+ !state.grids[3].disabled
+ );
+ checkbox2.click();
+ await onHighlighterHidden;
+ await onCheckboxChange;
+
+ is(
+ highlighters.gridHighlighters.size,
+ 1,
+ "Got expected number of grid highlighters shown."
+ );
+ is(
+ highlighters.state.grids.size,
+ 1,
+ "Got expected number of grids in the saved state"
+ );
+
+ info("Toggling OFF the CSS grid highlighter for #grid1.");
+ onHighlighterHidden = highlighters.once("grid-highlighter-hidden");
+ onCheckboxChange = waitUntilState(
+ store,
+ state =>
+ state.grids.length == 4 &&
+ !state.grids[0].highlighted &&
+ !state.grids[0].disabled &&
+ !state.grids[1].highlighted &&
+ !state.grids[1].disabled &&
+ !state.grids[2].highlighted &&
+ !state.grids[2].disabled &&
+ !state.grids[3].highlighted &&
+ !state.grids[3].disabled
+ );
+ checkbox1.click();
+ await onHighlighterHidden;
+ await onCheckboxChange;
+
+ info(
+ "Check that the CSS grid highlighter is not shown and the saved grid state."
+ );
+ ok(!highlighters.gridHighlighters.size, "No CSS grid highlighter is shown.");
+ ok(!highlighters.state.grids.size, "No grids in the saved state");
+});
diff --git a/devtools/client/inspector/grids/test/browser_grids_grid-outline-cannot-show-outline.js b/devtools/client/inspector/grids/test/browser_grids_grid-outline-cannot-show-outline.js
new file mode 100644
index 0000000000..a2f293c44d
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser_grids_grid-outline-cannot-show-outline.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that grid outline does not show when cells are too small to be drawn and that
+// "Cannot show outline for this grid." message is displayed.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #grid {
+ display: grid;
+ grid-template-columns: repeat(51, 20px);
+ grid-template-rows: repeat(51, 20px);
+ }
+ </style>
+ <div id="grid">
+ <div id="cellA">cell A</div>
+ <div id="cellB">cell B</div>
+ </div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+
+ const { inspector, gridInspector } = await openLayoutView();
+ const { document: doc } = gridInspector;
+ const { highlighters, store } = inspector;
+
+ await selectNode("#grid", inspector);
+ const outline = doc.getElementById("grid-outline-container");
+ const gridList = doc.getElementById("grid-list");
+ const checkbox = gridList.children[0].querySelector("input");
+
+ info("Toggling ON the CSS grid highlighter from the layout panel.");
+ const onHighlighterShown = highlighters.once("grid-highlighter-shown");
+ const onGridOutlineRendered = waitForDOM(doc, ".grid-outline-text", 1);
+ const onCheckboxChange = waitUntilState(
+ store,
+ state => state.grids.length == 1 && state.grids[0].highlighted
+ );
+ checkbox.click();
+ await onHighlighterShown;
+ await onCheckboxChange;
+ const elements = await onGridOutlineRendered;
+
+ const cannotShowGridOutline = elements[0];
+
+ info(
+ "Checking the grid outline is not rendered and an appropriate message is shown."
+ );
+ ok(!outline, "Outline component is not shown.");
+ ok(
+ cannotShowGridOutline,
+ "The message 'Cannot show outline for this grid' is displayed."
+ );
+});
diff --git a/devtools/client/inspector/grids/test/browser_grids_grid-outline-highlight-area.js b/devtools/client/inspector/grids/test/browser_grids_grid-outline-highlight-area.js
new file mode 100644
index 0000000000..7a93e561cc
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser_grids_grid-outline-highlight-area.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the grid area and cell are highlighted when hovering over a grid area in the
+// grid outline.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #grid {
+ display: grid;
+ grid-template-areas:
+ "header"
+ "footer";
+ }
+ .top {
+ grid-area: header;
+ }
+ .bottom {
+ grid-area: footer;
+ }
+ </style>
+ <div id="grid">
+ <div id="cella" className="top">Cell A</div>
+ <div id="cellb" className="bottom">Cell B</div>
+ </div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+
+ const { inspector, gridInspector } = await openLayoutView();
+ const { document: doc } = gridInspector;
+ const { store } = inspector;
+ const HIGHLIGHTER_TYPE = inspector.highlighters.TYPES.GRID;
+ const { waitForHighlighterTypeShown } = getHighlighterTestHelpers(inspector);
+
+ const gridList = doc.getElementById("grid-list");
+ const checkbox = gridList.children[0].querySelector("input");
+
+ info("Toggling ON the CSS grid highlighter from the layout panel.");
+ const onHighlighterShown = waitForHighlighterTypeShown(HIGHLIGHTER_TYPE);
+ const onGridOutlineRendered = waitForDOM(doc, "#grid-cell-group rect", 2);
+ const onCheckboxChange = waitUntilState(
+ store,
+ state => state.grids.length == 1 && state.grids[0].highlighted
+ );
+ checkbox.click();
+
+ info("Wait for checkbox to change");
+ await onCheckboxChange;
+
+ info("Wait for highlighter to be shown");
+ await onHighlighterShown;
+
+ info("Wait for outline to be rendered");
+ await onGridOutlineRendered;
+
+ info("Hovering over grid cell A in the grid outline.");
+ const onCellAHighlight = waitForHighlighterTypeShown(HIGHLIGHTER_TYPE);
+
+ synthesizeMouseOverOnGridCell(doc, 0);
+
+ const { options } = await onCellAHighlight;
+
+ info(
+ "Checking the grid highlighter options for the show grid area and cell parameters."
+ );
+ const { showGridCell, showGridArea } = options;
+ const { gridFragmentIndex, rowNumber, columnNumber } = showGridCell;
+
+ is(gridFragmentIndex, "0", "Should be the first grid fragment index.");
+ is(rowNumber, "1", "Should be the first grid row.");
+ is(columnNumber, "1", "Should be the first grid column.");
+ is(showGridArea, "header", "Grid area name should be 'header'.");
+});
diff --git a/devtools/client/inspector/grids/test/browser_grids_grid-outline-highlight-cell.js b/devtools/client/inspector/grids/test/browser_grids_grid-outline-highlight-cell.js
new file mode 100644
index 0000000000..0f5f329f9b
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser_grids_grid-outline-highlight-cell.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the grid cell is highlighted when hovering over the grid outline of a
+// grid cell.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #grid {
+ display: grid;
+ }
+ </style>
+ <div id="grid">
+ <div id="cella">Cell A</div>
+ <div id="cellb">Cell B</div>
+ </div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+
+ const { inspector, gridInspector } = await openLayoutView();
+ const { document: doc } = gridInspector;
+ const { store } = inspector;
+ const HIGHLIGHTER_TYPE = inspector.highlighters.TYPES.GRID;
+ const { waitForHighlighterTypeShown } = getHighlighterTestHelpers(inspector);
+
+ const gridList = doc.getElementById("grid-list");
+ const checkbox = gridList.children[0].querySelector("input");
+
+ info("Toggling ON the CSS grid highlighter from the layout panel.");
+ const onHighlighterShown = waitForHighlighterTypeShown(HIGHLIGHTER_TYPE);
+ const onGridOutlineRendered = waitForDOM(doc, "#grid-cell-group rect", 2);
+ const onCheckboxChange = waitUntilState(
+ store,
+ state => state.grids.length == 1 && state.grids[0].highlighted
+ );
+ checkbox.click();
+
+ info("Wait for checkbox to change");
+ await onCheckboxChange;
+
+ info("Wait for highlighter to be shown");
+ await onHighlighterShown;
+
+ info("Wait for outline to be rendered");
+ await onGridOutlineRendered;
+
+ info("Hovering over grid cell A in the grid outline.");
+ const onCellAHighlight = waitForHighlighterTypeShown(HIGHLIGHTER_TYPE);
+
+ synthesizeMouseOverOnGridCell(doc, 0);
+
+ const { options } = await onCellAHighlight;
+
+ info("Checking show grid cell options are correct.");
+ const { showGridCell } = options;
+ const { gridFragmentIndex, rowNumber, columnNumber } = showGridCell;
+
+ is(gridFragmentIndex, "0", "Should be the first grid fragment index.");
+ is(rowNumber, "1", "Should be the first grid row.");
+ is(columnNumber, "1", "Should be the first grid column.");
+});
diff --git a/devtools/client/inspector/grids/test/browser_grids_grid-outline-multiple-grids.js b/devtools/client/inspector/grids/test/browser_grids_grid-outline-multiple-grids.js
new file mode 100644
index 0000000000..a65cfd7528
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser_grids_grid-outline-multiple-grids.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the grid outline is not shown when more than one grid is highlighted.
+
+const TEST_URI = `
+ <style type='text/css'>
+ .grid {
+ display: grid;
+ }
+ </style>
+ <div id="grid1" class="grid">
+ <div class="cell1">cell1</div>
+ <div class="cell2">cell2</div>
+ </div>
+ <div id="grid2" class="grid">
+ <div class="cell1">cell1</div>
+ <div class="cell2">cell2</div>
+ </div>
+`;
+
+add_task(async function () {
+ await pushPref("devtools.gridinspector.maxHighlighters", 2);
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, gridInspector } = await openLayoutView();
+ const { document: doc } = gridInspector;
+ const { highlighters, store } = inspector;
+
+ await selectNode("#grid1", inspector);
+ const gridList = doc.getElementById("grid-list");
+ const checkbox1 = gridList.children[0].querySelector("input");
+ const checkbox2 = gridList.children[1].querySelector("input");
+
+ info("Toggling ON the CSS grid highlighter for #grid1.");
+ let onHighlighterShown = highlighters.once("grid-highlighter-shown");
+ const onGridOutlineRendered = waitForDOM(doc, "#grid-cell-group rect", 2);
+ let onCheckboxChange = waitUntilState(
+ store,
+ state =>
+ state.grids.length === 2 &&
+ state.grids[0].highlighted &&
+ !state.grids[1].highlighted
+ );
+ checkbox1.click();
+ await onHighlighterShown;
+ await onCheckboxChange;
+ const elements = await onGridOutlineRendered;
+
+ info("Checking the grid outline for #grid1 is shown.");
+ ok(
+ doc.getElementById("grid-outline-container"),
+ "Grid outline container is rendered."
+ );
+ is(elements.length, 2, "Grid outline is shown.");
+
+ info("Toggling ON the CSS grid highlighter for #grid2.");
+ onHighlighterShown = highlighters.once("grid-highlighter-shown");
+ onCheckboxChange = waitUntilState(
+ store,
+ state =>
+ state.grids.length === 2 &&
+ state.grids[0].highlighted &&
+ state.grids[1].highlighted
+ );
+ checkbox2.click();
+ await onHighlighterShown;
+ await onCheckboxChange;
+
+ info("Checking the grid outline is not shown.");
+ ok(
+ !doc.getElementById("grid-outline-container"),
+ "Grid outline is not rendered."
+ );
+});
diff --git a/devtools/client/inspector/grids/test/browser_grids_grid-outline-selected-grid.js b/devtools/client/inspector/grids/test/browser_grids_grid-outline-selected-grid.js
new file mode 100644
index 0000000000..ada2a635e4
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser_grids_grid-outline-selected-grid.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the grid outline is shown when a grid container is selected.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #grid {
+ display: grid;
+ }
+ </style>
+ <div id="grid">
+ <div id="cella">Cell A</div>
+ <div id="cellb">Cell B</div>
+ <div id="cellc">Cell C</div>
+ </div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+
+ const { inspector, gridInspector } = await openLayoutView();
+ const { document: doc } = gridInspector;
+ const { highlighters, store } = inspector;
+
+ const gridList = doc.getElementById("grid-list");
+ const checkbox = gridList.children[0].querySelector("input");
+
+ info("Checking the initial state of the Grid Inspector.");
+ ok(
+ !doc.getElementById("grid-outline-container"),
+ "There should be no grid outline shown."
+ );
+
+ info("Toggling ON the CSS grid highlighter from the layout panel.");
+ const onHighlighterShown = highlighters.once("grid-highlighter-shown");
+ const onCheckboxChange = waitUntilState(
+ store,
+ state => state.grids.length == 1 && state.grids[0].highlighted
+ );
+ const onGridOutlineRendered = waitForDOM(doc, "#grid-cell-group rect", 3);
+ checkbox.click();
+ await onHighlighterShown;
+ await onCheckboxChange;
+ const elements = await onGridOutlineRendered;
+
+ info("Checking the grid outline is shown.");
+ is(elements.length, 3, "Grid outline is shown.");
+});
diff --git a/devtools/client/inspector/grids/test/browser_grids_grid-outline-updates-on-grid-change.js b/devtools/client/inspector/grids/test/browser_grids_grid-outline-updates-on-grid-change.js
new file mode 100644
index 0000000000..778217cc40
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser_grids_grid-outline-updates-on-grid-change.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the grid outline does reflect the grid in the page even after the grid has
+// changed.
+
+const TEST_URI = `
+ <style>
+ .container {
+ display: grid;
+ grid-template-columns: repeat(2, 20vw);
+ grid-auto-rows: 20px;
+ }
+ </style>
+ <div class="container">
+ <div>item 1</div>
+ <div>item 2</div>
+ </div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+
+ const { inspector, gridInspector } = await openLayoutView();
+ const { document: doc } = gridInspector;
+ const { highlighters, store } = inspector;
+
+ info("Clicking on the first checkbox to highlight the grid");
+ const checkbox = doc.querySelector("#grid-list input");
+
+ const onHighlighterShown = highlighters.once("grid-highlighter-shown");
+ const onCheckboxChange = waitUntilState(
+ store,
+ state => state.grids.length == 1 && state.grids[0].highlighted
+ );
+ const onGridOutlineRendered = waitForDOM(doc, ".grid-outline-cell", 2);
+
+ checkbox.click();
+
+ await onHighlighterShown;
+ await onCheckboxChange;
+ let elements = await onGridOutlineRendered;
+
+ info("Checking the grid outline is shown.");
+ is(elements.length, 2, "Grid outline is shown.");
+
+ info("Changing the grid in the page");
+ const onReflow = inspector.once("reflow-in-selected-target");
+ const onGridOutlineChanged = waitForDOM(doc, ".grid-outline-cell", 4);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ const div = content.document.createElement("div");
+ div.textContent = "item 3";
+ content.document.querySelector(".container").appendChild(div);
+ });
+
+ await onReflow;
+ elements = await onGridOutlineChanged;
+
+ info("Checking the grid outline is correct.");
+ is(elements.length, 4, "Grid outline was changed.");
+});
diff --git a/devtools/client/inspector/grids/test/browser_grids_grid-outline-writing-mode.js b/devtools/client/inspector/grids/test/browser_grids_grid-outline-writing-mode.js
new file mode 100644
index 0000000000..c27a8b481f
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser_grids_grid-outline-writing-mode.js
@@ -0,0 +1,156 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the grid outline adjusts to match the container's writing mode.
+
+const TEST_URI = `
+ <style type='text/css'>
+ .grid {
+ display: grid;
+ width: 400px;
+ height: 300px;
+ }
+ .rtl {
+ direction: rtl;
+ }
+ .v-rl {
+ writing-mode: vertical-rl;
+ }
+ .v-lr {
+ writing-mode: vertical-lr;
+ }
+ .s-rl {
+ writing-mode: sideways-rl;
+ }
+ .s-lr {
+ writing-mode: sideways-lr;
+ }
+ </style>
+ <div class="grid">
+ <div id="cella">Cell A</div>
+ <div id="cellb">Cell B</div>
+ <div id="cellc">Cell C</div>
+ </div>
+ <div class="grid rtl">
+ <div id="cella">Cell A</div>
+ <div id="cellb">Cell B</div>
+ <div id="cellc">Cell C</div>
+ </div>
+ <div class="grid v-rl">
+ <div id="cella">Cell A</div>
+ <div id="cellb">Cell B</div>
+ <div id="cellc">Cell C</div>
+ </div>
+ <div class="grid v-lr">
+ <div id="cella">Cell A</div>
+ <div id="cellb">Cell B</div>
+ <div id="cellc">Cell C</div>
+ </div>
+ <div class="grid s-rl">
+ <div id="cella">Cell A</div>
+ <div id="cellb">Cell B</div>
+ <div id="cellc">Cell C</div>
+ </div>
+ <div class="grid s-lr">
+ <div id="cella">Cell A</div>
+ <div id="cellb">Cell B</div>
+ <div id="cellc">Cell C</div>
+ </div>
+`;
+
+add_task(async function () {
+ await pushPref("devtools.gridinspector.maxHighlighters", 1);
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+
+ const { inspector, gridInspector } = await openLayoutView();
+ const { document: doc } = gridInspector;
+ const { highlighters, store } = inspector;
+
+ info("Checking the initial state of the Grid Inspector.");
+ ok(
+ !doc.getElementById("grid-outline-container"),
+ "There should be no grid outline shown."
+ );
+
+ let elements;
+
+ elements = await enableGrid(doc, highlighters, store, 0);
+ is(
+ elements[0].style.transform,
+ "matrix(1, 0, 0, 1, 0, 0)",
+ "Transform matches for horizontal-tb and ltr."
+ );
+ await disableGrid(doc, highlighters, store, 0);
+
+ elements = await enableGrid(doc, highlighters, store, 1);
+ is(
+ elements[0].style.transform,
+ "matrix(-1, 0, 0, 1, 200, 0)",
+ "Transform matches for horizontal-tb and rtl"
+ );
+ await disableGrid(doc, highlighters, store, 1);
+
+ elements = await enableGrid(doc, highlighters, store, 2);
+ is(
+ elements[0].style.transform,
+ "matrix(6.12323e-17, 1, -1, 6.12323e-17, 200, 0)",
+ "Transform matches for vertical-rl and ltr"
+ );
+ await disableGrid(doc, highlighters, store, 2);
+
+ elements = await enableGrid(doc, highlighters, store, 3);
+ is(
+ elements[0].style.transform,
+ "matrix(-6.12323e-17, 1, 1, 6.12323e-17, 0, 0)",
+ "Transform matches for vertical-lr and ltr"
+ );
+ await disableGrid(doc, highlighters, store, 3);
+
+ elements = await enableGrid(doc, highlighters, store, 4);
+ is(
+ elements[0].style.transform,
+ "matrix(6.12323e-17, 1, -1, 6.12323e-17, 200, 0)",
+ "Transform matches for sideways-rl and ltr"
+ );
+ await disableGrid(doc, highlighters, store, 4);
+
+ elements = await enableGrid(doc, highlighters, store, 5);
+ is(
+ elements[0].style.transform,
+ "matrix(6.12323e-17, -1, 1, 6.12323e-17, -9.18485e-15, 150)",
+ "Transform matches for sideways-lr and ltr"
+ );
+ await disableGrid(doc, highlighters, store, 5);
+});
+
+async function enableGrid(doc, highlighters, store, index) {
+ info(`Enabling the CSS grid highlighter for grid ${index}.`);
+ const onHighlighterShown = highlighters.once("grid-highlighter-shown");
+ const onCheckboxChange = waitUntilState(
+ store,
+ state => state.grids.length == 6 && state.grids[index].highlighted
+ );
+ const onGridOutlineRendered = waitForDOM(doc, "#grid-cell-group");
+ const gridList = doc.getElementById("grid-list");
+ gridList.children[index].querySelector("input").click();
+ await onHighlighterShown;
+ await onCheckboxChange;
+ return onGridOutlineRendered;
+}
+
+async function disableGrid(doc, highlighters, store, index) {
+ info(`Disabling the CSS grid highlighter for grid ${index}.`);
+ const onHighlighterShown = highlighters.once("grid-highlighter-hidden");
+ const onCheckboxChange = waitUntilState(
+ store,
+ state => state.grids.length == 6 && !state.grids[index].highlighted
+ );
+ const onGridOutlineRemoved = waitForDOM(doc, "#grid-cell-group", 0);
+ const gridList = doc.getElementById("grid-list");
+ gridList.children[index].querySelector("input").click();
+ await onHighlighterShown;
+ await onCheckboxChange;
+ return onGridOutlineRemoved;
+}
diff --git a/devtools/client/inspector/grids/test/browser_grids_highlighter-setting-rules-grid-toggle.js b/devtools/client/inspector/grids/test/browser_grids_highlighter-setting-rules-grid-toggle.js
new file mode 100644
index 0000000000..c2c22b8b87
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser_grids_highlighter-setting-rules-grid-toggle.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test toggling the grid highlighter in the rule view with changes in the grid
+// display setting from the layout view.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #grid {
+ display: grid;
+ }
+ </style>
+ <div id="grid">
+ <div id="cell1">cell1</div>
+ <div id="cell2">cell2</div>
+ </div>
+`;
+
+const SHOW_INFINITE_LINES_PREF = "devtools.gridinspector.showInfiniteLines";
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, gridInspector } = await openLayoutView();
+ const { document: doc } = gridInspector;
+ const { store } = inspector;
+
+ const checkbox = doc.getElementById("grid-setting-extend-grid-lines");
+
+ ok(
+ !Services.prefs.getBoolPref(SHOW_INFINITE_LINES_PREF),
+ "'Extend grid lines infinitely' is pref off by default."
+ );
+
+ info("Toggling ON the 'Extend grid lines infinitely' setting.");
+ const onCheckboxChange = waitUntilState(
+ store,
+ state => state.highlighterSettings.showInfiniteLines
+ );
+ checkbox.click();
+ await onCheckboxChange;
+
+ info("Selecting the rule view.");
+ const ruleView = selectRuleView(inspector);
+ const highlighters = ruleView.highlighters;
+
+ await selectNode("#grid", inspector);
+
+ const container = getRuleViewProperty(ruleView, "#grid", "display").valueSpan;
+ const gridToggle = container.querySelector(".js-toggle-grid-highlighter");
+
+ info("Toggling ON the CSS grid highlighter from the rule-view.");
+ const onHighlighterShown = highlighters.once(
+ "grid-highlighter-shown",
+ (nodeFront, options) => {
+ info("Checking the grid highlighter display settings.");
+ const {
+ color,
+ showGridAreasOverlay,
+ showGridLineNumbers,
+ showInfiniteLines,
+ } = options;
+
+ is(color, "#9400FF", "CSS grid highlighter color is correct.");
+ ok(!showGridAreasOverlay, "Show grid areas overlay option is off.");
+ ok(!showGridLineNumbers, "Show grid line numbers option is off.");
+ ok(showInfiniteLines, "Show infinite lines option is on.");
+ }
+ );
+ gridToggle.click();
+ await onHighlighterShown;
+
+ Services.prefs.clearUserPref(SHOW_INFINITE_LINES_PREF);
+});
diff --git a/devtools/client/inspector/grids/test/browser_grids_highlighter-toggle-telemetry.js b/devtools/client/inspector/grids/test/browser_grids_highlighter-toggle-telemetry.js
new file mode 100644
index 0000000000..329acf3713
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser_grids_highlighter-toggle-telemetry.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the telemetry count is correct when the grid highlighter is activated from
+// the layout view.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #grid {
+ display: grid;
+ }
+ </style>
+ <div id="grid">
+ <div id="cell1">cell1</div>
+ <div id="cell2">cell2</div>
+ </div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ startTelemetry();
+ const { gridInspector, inspector } = await openLayoutView();
+ const { document: doc } = gridInspector;
+ const { highlighters, store } = inspector;
+
+ await selectNode("#grid", inspector);
+ const gridList = doc.getElementById("grid-list");
+ const checkbox = gridList.children[0].querySelector("input");
+
+ info("Toggling ON the CSS grid highlighter from the layout panel.");
+ const onHighlighterShown = highlighters.once("grid-highlighter-shown");
+ let onCheckboxChange = waitUntilState(
+ store,
+ state => state.grids.length == 1 && state.grids[0].highlighted
+ );
+ checkbox.click();
+ await onHighlighterShown;
+ await onCheckboxChange;
+
+ info("Toggling OFF the CSS grid highlighter from the layout panel.");
+ const onHighlighterHidden = highlighters.once("grid-highlighter-hidden");
+ onCheckboxChange = waitUntilState(
+ store,
+ state => state.grids.length == 1 && !state.grids[0].highlighted
+ );
+ checkbox.click();
+ await onHighlighterHidden;
+ await onCheckboxChange;
+
+ checkResults();
+});
+
+function checkResults() {
+ checkTelemetry("devtools.grid.gridinspector.opened", "", 1, "scalar");
+ checkTelemetry(
+ "DEVTOOLS_GRID_HIGHLIGHTER_TIME_ACTIVE_SECONDS",
+ "",
+ null,
+ "hasentries"
+ );
+}
diff --git a/devtools/client/inspector/grids/test/browser_grids_number-of-css-grids-telemetry.js b/devtools/client/inspector/grids/test/browser_grids_number-of-css-grids-telemetry.js
new file mode 100644
index 0000000000..6ec6b32a63
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser_grids_number-of-css-grids-telemetry.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 telemetry count for the number of CSS Grid Elements on a page navigation
+// is correct when the toolbox is opened.
+
+const TEST_URI1 = `
+ <div></div>
+`;
+
+const TEST_URI2 = `
+ <style type='text/css'>
+ #grid {
+ display: grid;
+ }
+ </style>
+ <div id="grid">
+ <div id="cell1">cell1</div>
+ <div id="cell2">cell2</div>
+ </div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI1));
+
+ startTelemetry();
+
+ const { inspector } = await openLayoutView();
+ const { store } = inspector;
+
+ info("Navigate to TEST_URI2");
+
+ const onGridListUpdate = waitUntilState(
+ store,
+ state => state.grids.length == 1
+ );
+ await navigateTo(
+ "data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI2)
+ );
+ await onGridListUpdate;
+
+ checkResults();
+});
+
+function checkResults() {
+ // Check for:
+ // - 1 CSS Grid Element
+ checkTelemetry(
+ "DEVTOOLS_NUMBER_OF_CSS_GRIDS_IN_A_PAGE",
+ "",
+ { 0: 0, 1: 1, 2: 0 },
+ "array"
+ );
+}
diff --git a/devtools/client/inspector/grids/test/browser_grids_persist-color-palette.js b/devtools/client/inspector/grids/test/browser_grids_persist-color-palette.js
new file mode 100644
index 0000000000..0a4e10dfa0
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser_grids_persist-color-palette.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the when a custom color has been previously set, we initialize
+// the grid with that color.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #grid {
+ display: grid;
+ }
+ </style>
+ <div id="grid">
+ <div class="cell1">cell1</div>
+ <div class="cell2">cell2</div>
+ </div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector, gridInspector, layoutView, toolbox } =
+ await openLayoutView();
+ const { document: doc } = gridInspector;
+ const { store } = inspector;
+ const cPicker = layoutView.swatchColorPickerTooltip;
+ const swatch = doc.querySelector(
+ "#layout-grid-container .layout-color-swatch"
+ );
+
+ info("Scrolling into view of the #grid color swatch.");
+ swatch.scrollIntoView();
+
+ info("Opening the color picker by clicking on the #grid color swatch.");
+ const onColorPickerReady = cPicker.once("ready");
+ swatch.click();
+ await onColorPickerReady;
+
+ await simulateColorPickerChange(cPicker, [51, 48, 0, 1]);
+
+ info("Closing the toolbox.");
+ await toolbox.destroy();
+ info("Open the toolbox again.");
+ await openLayoutView();
+
+ info("Check that the previously set custom color is used.");
+ is(
+ swatch.style.backgroundColor,
+ "rgb(51, 48, 0)",
+ "The color swatch's background is correct."
+ );
+ is(
+ store.getState().grids[0].color,
+ "#333000",
+ "The grid color state is correct."
+ );
+});
diff --git a/devtools/client/inspector/grids/test/browser_grids_restored-after-reload.js b/devtools/client/inspector/grids/test/browser_grids_restored-after-reload.js
new file mode 100644
index 0000000000..412bf98af9
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser_grids_restored-after-reload.js
@@ -0,0 +1,115 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the grid highlighter is re-displayed after reloading a page and the grid
+// item is highlighted.
+
+const TEST_URI = `
+ <style type='text/css'>
+ #grid {
+ display: grid;
+ }
+ </style>
+ <div id="grid">
+ <div id="cell1">cell1</div>
+ <div id="cell2">cell2</div>
+ </div>
+`;
+
+const OTHER_URI = `
+ <style type='text/css'>
+ #grid {
+ display: grid;
+ }
+ </style>
+ <div id="grid">
+ <div id="cell1">cell1</div>
+ <div id="cell2">cell2</div>
+ <div id="cell3">cell3</div>
+ </div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { gridInspector, inspector } = await openLayoutView();
+ const { document: doc } = gridInspector;
+ const { highlighters, store } = inspector;
+ const HIGHLIGHTER_TYPE = inspector.highlighters.TYPES.GRID;
+ const { waitForHighlighterTypeRestored, waitForHighlighterTypeDiscarded } =
+ getHighlighterTestHelpers(inspector);
+
+ await selectNode("#grid", inspector);
+ const gridList = doc.getElementById("grid-list");
+ const checkbox = gridList.children[0].querySelector("input");
+
+ info("Toggling ON the CSS grid highlighter from the layout panel.");
+ const onHighlighterShown = highlighters.once("grid-highlighter-shown");
+ const onCheckboxChange = waitUntilState(
+ store,
+ state => state.grids.length == 1 && state.grids[0].highlighted
+ );
+ checkbox.click();
+ await onHighlighterShown;
+ await onCheckboxChange;
+
+ info(
+ "Check that the CSS grid highlighter is created and the saved grid state."
+ );
+ is(highlighters.gridHighlighters.size, 1, "CSS grid highlighter is shown.");
+ is(
+ highlighters.state.grids.size,
+ 1,
+ "There's a saved grid state to be restored."
+ );
+
+ info(
+ "Reload the page, expect the highlighter to be displayed once again and " +
+ "grid is checked"
+ );
+ const onRestored = waitForHighlighterTypeRestored(HIGHLIGHTER_TYPE);
+ let onGridListRestored = waitUntilState(
+ store,
+ state => state.grids.length == 1 && state.grids[0].highlighted
+ );
+
+ const onReloaded = inspector.once("reloaded");
+ await reloadBrowser();
+ info("Wait for inspector to be reloaded after page reload");
+ await onReloaded;
+
+ await onRestored;
+ await onGridListRestored;
+
+ info(
+ "Check that the grid highlighter can be displayed after reloading the page"
+ );
+ is(highlighters.gridHighlighters.size, 1, "CSS grid highlighter is shown.");
+ is(
+ highlighters.state.grids.size,
+ 1,
+ "The saved grid state has the correct number of saved states."
+ );
+
+ info(
+ "Navigate to another URL, and check that the highlighter is hidden and " +
+ "grid is unchecked"
+ );
+ const otherUri =
+ "data:text/html;charset=utf-8," + encodeURIComponent(OTHER_URI);
+ const onDiscarded = waitForHighlighterTypeDiscarded(HIGHLIGHTER_TYPE);
+ onGridListRestored = waitUntilState(
+ store,
+ state => state.grids.length == 1 && !state.grids[0].highlighted
+ );
+ await navigateTo(otherUri);
+ await onDiscarded;
+ await onGridListRestored;
+
+ info(
+ "Check that the grid highlighter is hidden after navigating to a different page"
+ );
+ ok(!highlighters.gridHighlighters.size, "CSS grid highlighter is hidden.");
+ ok(!highlighters.state.grids.size, "No grids to be restored on page reload.");
+});
diff --git a/devtools/client/inspector/grids/test/browser_grids_restored-multiple-grids-after-reload.js b/devtools/client/inspector/grids/test/browser_grids_restored-multiple-grids-after-reload.js
new file mode 100644
index 0000000000..81ac7619ff
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser_grids_restored-multiple-grids-after-reload.js
@@ -0,0 +1,156 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the grid highlighters are re-displayed after reloading a page and multiple
+// grids are highlighted.
+
+const TEST_URI = `
+ <style type='text/css'>
+ .grid {
+ display: grid;
+ }
+ </style>
+ <div id="grid1" class="grid">
+ <div class="cell1">cell1</div>
+ <div class="cell2">cell2</div>
+ </div>
+ <div id="grid2" class="grid">
+ <div class="cell1">cell1</div>
+ <div class="cell2">cell2</div>
+ </div>
+ <div id="grid3" class="grid">
+ <div class="cell1">cell1</div>
+ <div class="cell2">cell2</div>
+ </div>
+ <div id="grid4" class="grid">
+ <div class="cell1">cell1</div>
+ <div class="cell2">cell2</div>
+ </div>
+`;
+
+add_task(async function () {
+ await pushPref("devtools.gridinspector.maxHighlighters", 3);
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { gridInspector, inspector } = await openLayoutView();
+ const { document: doc } = gridInspector;
+ const { highlighters, store } = inspector;
+
+ await selectNode("#grid1", inspector);
+ const gridList = doc.getElementById("grid-list");
+ const checkbox1 = gridList.children[0].querySelector("input");
+ const checkbox2 = gridList.children[1].querySelector("input");
+ const checkbox3 = gridList.children[2].querySelector("input");
+
+ info("Toggling ON the CSS grid highlighter for #grid1.");
+ let onHighlighterShown = highlighters.once("grid-highlighter-shown");
+ let onCheckboxChange = waitUntilState(
+ store,
+ state =>
+ state.grids.length == 4 &&
+ state.grids[0].highlighted &&
+ !state.grids[0].disabled &&
+ !state.grids[1].highlighted &&
+ !state.grids[1].disabled &&
+ !state.grids[2].highlighted &&
+ !state.grids[2].disabled &&
+ !state.grids[3].highlighted &&
+ !state.grids[3].disabled
+ );
+ checkbox1.click();
+ await onHighlighterShown;
+ await onCheckboxChange;
+
+ info("Toggling ON the CSS grid highlighter for #grid2.");
+ onHighlighterShown = highlighters.once("grid-highlighter-shown");
+ onCheckboxChange = waitUntilState(
+ store,
+ state =>
+ state.grids.length == 4 &&
+ state.grids[0].highlighted &&
+ !state.grids[0].disabled &&
+ state.grids[1].highlighted &&
+ !state.grids[1].disabled &&
+ !state.grids[2].highlighted &&
+ !state.grids[2].disabled &&
+ !state.grids[3].highlighted &&
+ !state.grids[3].disabled
+ );
+ checkbox2.click();
+ await onHighlighterShown;
+ await onCheckboxChange;
+
+ info("Toggling ON the CSS grid highlighter for #grid3.");
+ onHighlighterShown = highlighters.once("grid-highlighter-shown");
+ onCheckboxChange = waitUntilState(
+ store,
+ state =>
+ state.grids.length == 4 &&
+ state.grids[0].highlighted &&
+ !state.grids[0].disabled &&
+ state.grids[1].highlighted &&
+ !state.grids[1].disabled &&
+ state.grids[2].highlighted &&
+ !state.grids[2].disabled &&
+ !state.grids[3].highlighted &&
+ state.grids[3].disabled
+ );
+ checkbox3.click();
+ await onHighlighterShown;
+ await onCheckboxChange;
+
+ info(
+ "Check that the CSS grid highlighters are created and the saved grid state."
+ );
+ is(
+ highlighters.gridHighlighters.size,
+ 3,
+ "Got expected number of grid highlighters shown."
+ );
+ is(
+ highlighters.state.grids.size,
+ 3,
+ "Got expected number of grids in the saved state"
+ );
+
+ info(
+ "Reload the page, expect the highlighters to be displayed once again and " +
+ "grids are checked"
+ );
+ const onStateRestored = waitForNEvents(
+ highlighters,
+ "highlighter-restored",
+ 3
+ );
+ const onGridListRestored = waitUntilState(
+ store,
+ state =>
+ state.grids.length == 4 &&
+ state.grids[0].highlighted &&
+ !state.grids[0].disabled &&
+ state.grids[1].highlighted &&
+ !state.grids[1].disabled &&
+ state.grids[2].highlighted &&
+ !state.grids[2].disabled &&
+ !state.grids[3].highlighted &&
+ state.grids[3].disabled
+ );
+ await reloadBrowser();
+ await onStateRestored;
+ await onGridListRestored;
+
+ info(
+ "Check that the grid highlighters can be displayed after reloading the page"
+ );
+ is(
+ highlighters.gridHighlighters.size,
+ 3,
+ "Got expected number of grid highlighters shown."
+ );
+ is(
+ highlighters.state.grids.size,
+ 3,
+ "Got expected number of grids in the saved state"
+ );
+});
diff --git a/devtools/client/inspector/grids/test/doc_iframe_reloaded.html b/devtools/client/inspector/grids/test/doc_iframe_reloaded.html
new file mode 100644
index 0000000000..a452dd4d8c
--- /dev/null
+++ b/devtools/client/inspector/grids/test/doc_iframe_reloaded.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<iframe srcdoc="<style>.grid{display:grid;}</style><div class='grid'><span>a</span><span>b</span></div>"></iframe>
+<script>
+"use strict";
+function reloadIFrame() { // eslint-disable-line no-unused-vars
+ const iFrame = document.querySelector("iframe");
+ iFrame.setAttribute("srcdoc", iFrame.getAttribute("srcdoc"));
+}
+</script>
diff --git a/devtools/client/inspector/grids/test/doc_subgrid.html b/devtools/client/inspector/grids/test/doc_subgrid.html
new file mode 100644
index 0000000000..fef13bcc5c
--- /dev/null
+++ b/devtools/client/inspector/grids/test/doc_subgrid.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <style>
+ .container {
+ display: grid;
+ grid-gap: 5px;
+ grid-template: auto / 1fr 3fr 1fr;
+ background: lightyellow;
+ }
+
+ .subgrid {
+ display: grid;
+ grid: subgrid / subgrid;
+ }
+
+ header, aside, section, footer {
+ background: lightblue;
+ font-family: sans-serif;
+ font-size: 3em;
+ }
+
+ header, footer {
+ grid-column: span 3;
+ }
+
+ main {
+ grid-column: span 3;
+ }
+
+ .aside1 {
+ grid-column: 1;
+ }
+
+ .aside2 {
+ grid-column: 3;
+ }
+
+ section {
+ grid-column: 2;
+ }
+ </style>
+</head>
+<body>
+ <div class="container">
+ <header class="subgrid">Header</header>
+ <main class="subgrid">
+ <aside class="aside1 subgrid">aside</aside>
+ <section>section</section>
+ <aside class="aside2 subgrid">aside2</aside>
+ </main>
+ <footer>footer</footer>
+ </div>
+</body>
+</html>
diff --git a/devtools/client/inspector/grids/test/head.js b/devtools/client/inspector/grids/test/head.js
new file mode 100644
index 0000000000..5b54004abc
--- /dev/null
+++ b/devtools/client/inspector/grids/test/head.js
@@ -0,0 +1,40 @@
+/* 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
+);
+
+const asyncStorage = require("resource://devtools/shared/async-storage.js");
+
+Services.prefs.setIntPref("devtools.toolbox.footer.height", 350);
+registerCleanupFunction(async function () {
+ Services.prefs.clearUserPref("devtools.toolbox.footer.height");
+ await asyncStorage.removeItem("gridInspectorHostColors");
+});
+
+/**
+ * Simulate a mouseover event on a grid cell currently rendered in the grid
+ * inspector.
+ *
+ * @param {Document} doc
+ * The owner document for the grid inspector.
+ * @param {Number} gridCellIndex
+ * The index (0-based) of the grid cell that should be hovered.
+ */
+function synthesizeMouseOverOnGridCell(doc, gridCellIndex = 0) {
+ // Make sure to retrieve the current live grid item before attempting to
+ // interact with it using mouse APIs.
+ const gridCell = doc.querySelectorAll("#grid-cell-group rect")[gridCellIndex];
+
+ EventUtils.synthesizeMouseAtCenter(
+ gridCell,
+ { type: "mouseover" },
+ doc.defaultView
+ );
+}
diff --git a/devtools/client/inspector/grids/test/xpcshell/.eslintrc.js b/devtools/client/inspector/grids/test/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..86bd54c245
--- /dev/null
+++ b/devtools/client/inspector/grids/test/xpcshell/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the common devtools xpcshell eslintrc config.
+ extends: "../../../../../.eslintrc.xpcshell.js",
+};
diff --git a/devtools/client/inspector/grids/test/xpcshell/head.js b/devtools/client/inspector/grids/test/xpcshell/head.js
new file mode 100644
index 0000000000..733c0400da
--- /dev/null
+++ b/devtools/client/inspector/grids/test/xpcshell/head.js
@@ -0,0 +1,10 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+
+const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+);
diff --git a/devtools/client/inspector/grids/test/xpcshell/test_compare_fragments_geometry.js b/devtools/client/inspector/grids/test/xpcshell/test_compare_fragments_geometry.js
new file mode 100644
index 0000000000..57f617a3ec
--- /dev/null
+++ b/devtools/client/inspector/grids/test/xpcshell/test_compare_fragments_geometry.js
@@ -0,0 +1,129 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {
+ compareFragmentsGeometry,
+} = require("resource://devtools/client/inspector/grids/utils/utils.js");
+
+const TESTS = [
+ {
+ desc: "No fragments",
+ grids: [[], []],
+ expected: true,
+ },
+ {
+ desc: "Different number of fragments",
+ grids: [
+ [{}, {}, {}],
+ [{}, {}],
+ ],
+ expected: false,
+ },
+ {
+ desc: "Different number of columns",
+ grids: [
+ [{ cols: { lines: [{}, {}] }, rows: { lines: [] } }],
+ [{ cols: { lines: [{}] }, rows: { lines: [] } }],
+ ],
+ expected: false,
+ },
+ {
+ desc: "Different number of rows",
+ grids: [
+ [{ cols: { lines: [{}, {}] }, rows: { lines: [{}] } }],
+ [{ cols: { lines: [{}, {}] }, rows: { lines: [{}, {}] } }],
+ ],
+ expected: false,
+ },
+ {
+ desc: "Different number of rows and columns",
+ grids: [
+ [{ cols: { lines: [{}] }, rows: { lines: [{}] } }],
+ [{ cols: { lines: [{}, {}] }, rows: { lines: [{}, {}] } }],
+ ],
+ expected: false,
+ },
+ {
+ desc: "Different column sizes",
+ grids: [
+ [
+ {
+ cols: { lines: [{ start: 0 }, { start: 500 }] },
+ rows: { lines: [] },
+ },
+ ],
+ [
+ {
+ cols: { lines: [{ start: 0 }, { start: 1000 }] },
+ rows: { lines: [] },
+ },
+ ],
+ ],
+ expected: false,
+ },
+ {
+ desc: "Different row sizes",
+ grids: [
+ [
+ {
+ cols: { lines: [{ start: 0 }, { start: 500 }] },
+ rows: { lines: [{ start: -100 }] },
+ },
+ ],
+ [
+ {
+ cols: { lines: [{ start: 0 }, { start: 500 }] },
+ rows: { lines: [{ start: 0 }] },
+ },
+ ],
+ ],
+ expected: false,
+ },
+ {
+ desc: "Different row and column sizes",
+ grids: [
+ [
+ {
+ cols: { lines: [{ start: 0 }, { start: 500 }] },
+ rows: { lines: [{ start: -100 }] },
+ },
+ ],
+ [
+ {
+ cols: { lines: [{ start: 0 }, { start: 505 }] },
+ rows: { lines: [{ start: 0 }] },
+ },
+ ],
+ ],
+ expected: false,
+ },
+ {
+ desc: "Complete structure, same fragments",
+ grids: [
+ [
+ {
+ cols: { lines: [{ start: 0 }, { start: 100.3 }, { start: 200.6 }] },
+ rows: { lines: [{ start: 0 }, { start: 1000 }, { start: 2000 }] },
+ },
+ ],
+ [
+ {
+ cols: { lines: [{ start: 0 }, { start: 100.3 }, { start: 200.6 }] },
+ rows: { lines: [{ start: 0 }, { start: 1000 }, { start: 2000 }] },
+ },
+ ],
+ ],
+ expected: true,
+ },
+];
+
+function run_test() {
+ for (const { desc, grids, expected } of TESTS) {
+ if (desc) {
+ info(desc);
+ }
+ equal(compareFragmentsGeometry(grids[0], grids[1]), expected);
+ }
+}
diff --git a/devtools/client/inspector/grids/test/xpcshell/xpcshell.ini b/devtools/client/inspector/grids/test/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..c52a930c71
--- /dev/null
+++ b/devtools/client/inspector/grids/test/xpcshell/xpcshell.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+tags = devtools
+firefox-appdir = browser
+head = head.js
+
+[test_compare_fragments_geometry.js]
diff --git a/devtools/client/inspector/grids/types.js b/devtools/client/inspector/grids/types.js
new file mode 100644
index 0000000000..b2a936fef2
--- /dev/null
+++ b/devtools/client/inspector/grids/types.js
@@ -0,0 +1,57 @@
+/* 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");
+
+/**
+ * A single grid container in the document.
+ */
+exports.grid = {
+ // The id of the grid
+ id: PropTypes.number,
+
+ // The color for the grid overlay highlighter
+ color: PropTypes.string,
+
+ // The text direction of the grid container
+ direction: PropTypes.string,
+
+ // Whether or not the grid checkbox is disabled as a result of hitting the
+ // maximum number of grid highlighters shown.
+ disabled: PropTypes.bool,
+
+ // The grid fragment object of the grid container
+ gridFragments: PropTypes.array,
+
+ // Whether or not the grid highlighter is highlighting the grid
+ highlighted: PropTypes.bool,
+
+ // Whether or not the grid is a subgrid
+ isSubgrid: PropTypes.bool,
+
+ // The node front of the grid container
+ nodeFront: PropTypes.object,
+
+ // If the grid is a subgrid, this references the parent node front actor ID
+ parentNodeActorID: PropTypes.string,
+
+ // Array of ids belonging to the subgrid within the grid container
+ subgrids: PropTypes.arrayOf(PropTypes.number),
+
+ // The writing mode of the grid container
+ writingMode: PropTypes.string,
+};
+
+/**
+ * The grid highlighter settings on what to display in its grid overlay in the document.
+ */
+exports.highlighterSettings = {
+ // Whether or not the grid highlighter should show the grid line numbers
+ showGridLineNumbers: PropTypes.bool,
+
+ // Whether or not the grid highlighter extends the grid lines infinitely
+ showInfiniteLines: PropTypes.bool,
+};
diff --git a/devtools/client/inspector/grids/utils/moz.build b/devtools/client/inspector/grids/utils/moz.build
new file mode 100644
index 0000000000..c74e63e617
--- /dev/null
+++ b/devtools/client/inspector/grids/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(
+ "utils.js",
+)
diff --git a/devtools/client/inspector/grids/utils/utils.js b/devtools/client/inspector/grids/utils/utils.js
new file mode 100644
index 0000000000..fc25de8775
--- /dev/null
+++ b/devtools/client/inspector/grids/utils/utils.js
@@ -0,0 +1,58 @@
+/* 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";
+
+/**
+ * Compares 2 sets of grid fragments to each other and checks if they have the same
+ * general geometry.
+ * This means that things like areas, area names or line names are ignored.
+ * This only checks if the 2 sets of fragments have as many fragments, as many lines, and
+ * that those lines are at the same distance.
+ *
+ * @param {Array} fragments1
+ * A list of gridFragment objects.
+ * @param {Array} fragments2
+ * Another list of gridFragment objects to compare to the first list.
+ * @return {Boolean}
+ * True if the fragments are the same, false otherwise.
+ */
+function compareFragmentsGeometry(fragments1, fragments2) {
+ // Compare the number of fragments.
+ if (fragments1.length !== fragments2.length) {
+ return false;
+ }
+
+ // Compare the number of areas, rows and columns.
+ for (let i = 0; i < fragments1.length; i++) {
+ if (
+ fragments1[i].cols.lines.length !== fragments2[i].cols.lines.length ||
+ fragments1[i].rows.lines.length !== fragments2[i].rows.lines.length
+ ) {
+ return false;
+ }
+ }
+
+ // Compare the offset of lines.
+ for (let i = 0; i < fragments1.length; i++) {
+ for (let j = 0; j < fragments1[i].cols.lines.length; j++) {
+ if (
+ fragments1[i].cols.lines[j].start !== fragments2[i].cols.lines[j].start
+ ) {
+ return false;
+ }
+ }
+ for (let j = 0; j < fragments1[i].rows.lines.length; j++) {
+ if (
+ fragments1[i].rows.lines[j].start !== fragments2[i].rows.lines[j].start
+ ) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
+module.exports.compareFragmentsGeometry = compareFragmentsGeometry;