diff options
Diffstat (limited to 'devtools/client/inspector/grids')
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; |