diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /devtools/client/inspector/flexbox | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/inspector/flexbox')
64 files changed, 4438 insertions, 0 deletions
diff --git a/devtools/client/inspector/flexbox/actions/flexbox-highlighter.js b/devtools/client/inspector/flexbox/actions/flexbox-highlighter.js new file mode 100644 index 0000000000..487a4c3b12 --- /dev/null +++ b/devtools/client/inspector/flexbox/actions/flexbox-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 panel + * + * They provide a shortcut for React components to invoke the flexbox highlighter + * without having to know where the highlighter exists. + */ + +module.exports = { + /** + * Toggle the flexbox highlighter for the given node front. + * + * @param {NodeFront} nodeFront + * Node for which the highlighter should be toggled. + * @param {String} reason + * Reason why the highlighter was toggled; used in telemetry. + */ + toggleFlexboxHighlighter(nodeFront, reason) { + return async thunkOptions => { + const { inspector } = thunkOptions; + if (!inspector || inspector._destroyed) { + return; + } + + await inspector.highlighters.toggleFlexboxHighlighter(nodeFront, reason); + }; + }, +}; diff --git a/devtools/client/inspector/flexbox/actions/flexbox.js b/devtools/client/inspector/flexbox/actions/flexbox.js new file mode 100644 index 0000000000..64c1e1f074 --- /dev/null +++ b/devtools/client/inspector/flexbox/actions/flexbox.js @@ -0,0 +1,59 @@ +/* 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 { + CLEAR_FLEXBOX, + UPDATE_FLEXBOX, + UPDATE_FLEXBOX_COLOR, + UPDATE_FLEXBOX_HIGHLIGHTED, +} = require("resource://devtools/client/inspector/flexbox/actions/index.js"); + +module.exports = { + /** + * Clears the flexbox state by resetting it back to the initial flexbox state. + */ + clearFlexbox() { + return { + type: CLEAR_FLEXBOX, + }; + }, + + /** + * Updates the flexbox state with the newly selected flexbox. + */ + updateFlexbox(flexbox) { + return { + type: UPDATE_FLEXBOX, + flexbox, + }; + }, + + /** + * Updates the color used for the flexbox's highlighter. + * + * @param {String} color + * The color to use for this nodeFront's flexbox highlighter. + */ + updateFlexboxColor(color) { + return { + type: UPDATE_FLEXBOX_COLOR, + color, + }; + }, + + /** + * Updates the flexbox highlighted state. + * + * @param {Boolean} highlighted + * Whether or not the flexbox highlighter is highlighting the flexbox. + */ + updateFlexboxHighlighted(highlighted) { + return { + type: UPDATE_FLEXBOX_HIGHLIGHTED, + highlighted, + }; + }, +}; diff --git a/devtools/client/inspector/flexbox/actions/index.js b/devtools/client/inspector/flexbox/actions/index.js new file mode 100644 index 0000000000..bc4e30044e --- /dev/null +++ b/devtools/client/inspector/flexbox/actions/index.js @@ -0,0 +1,24 @@ +/* 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( + [ + // Clears the flexbox state by resetting it back to the initial flexbox state. + "CLEAR_FLEXBOX", + + // Updates the flexbox state with the newly selected flexbox. + "UPDATE_FLEXBOX", + + // Updates the color used for the overlay of a flexbox. + "UPDATE_FLEXBOX_COLOR", + + // Updates the flexbox highlighted state. + "UPDATE_FLEXBOX_HIGHLIGHTED", + ], + module.exports +); diff --git a/devtools/client/inspector/flexbox/actions/moz.build b/devtools/client/inspector/flexbox/actions/moz.build new file mode 100644 index 0000000000..3e5ab1eda8 --- /dev/null +++ b/devtools/client/inspector/flexbox/actions/moz.build @@ -0,0 +1,11 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + "flexbox-highlighter.js", + "flexbox.js", + "index.js", +) diff --git a/devtools/client/inspector/flexbox/components/FlexContainer.js b/devtools/client/inspector/flexbox/components/FlexContainer.js new file mode 100644 index 0000000000..c8c264defa --- /dev/null +++ b/devtools/client/inspector/flexbox/components/FlexContainer.js @@ -0,0 +1,125 @@ +/* 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"); +const { + getFormatStr, +} = require("resource://devtools/client/inspector/layout/utils/l10n.js"); + +loader.lazyRequireGetter( + this, + "getNodeRep", + "resource://devtools/client/inspector/shared/node-reps.js" +); + +const Types = require("resource://devtools/client/inspector/flexbox/types.js"); + +const { + highlightNode, + unhighlightNode, +} = require("resource://devtools/client/inspector/boxmodel/actions/box-model-highlighter.js"); + +class FlexContainer extends PureComponent { + static get propTypes() { + return { + dispatch: PropTypes.func.isRequired, + color: PropTypes.string.isRequired, + flexContainer: PropTypes.shape(Types.flexContainer).isRequired, + getSwatchColorPickerTooltip: PropTypes.func.isRequired, + onSetFlexboxOverlayColor: PropTypes.func.isRequired, + }; + } + + constructor(props) { + super(props); + + this.swatchEl = createRef(); + + this.setFlexboxColor = this.setFlexboxColor.bind(this); + } + + componentDidMount() { + const tooltip = this.props.getSwatchColorPickerTooltip(); + + let previousColor; + tooltip.addSwatch(this.swatchEl.current, { + onCommit: this.setFlexboxColor, + onPreview: this.setFlexboxColor, + onRevert: () => { + this.props.onSetFlexboxOverlayColor(previousColor); + }, + onShow: () => { + previousColor = this.props.color; + }, + }); + } + + componentWillUnmount() { + const tooltip = this.props.getSwatchColorPickerTooltip(); + tooltip.removeSwatch(this.swatchEl.current); + } + + setFlexboxColor() { + const color = this.swatchEl.current.dataset.color; + this.props.onSetFlexboxOverlayColor(color); + } + + render() { + const { color, flexContainer, dispatch } = this.props; + const { nodeFront, properties } = flexContainer; + + return createElement( + Fragment, + null, + dom.div( + { + className: "flex-header-container-label", + }, + getNodeRep(nodeFront, { + onDOMNodeMouseOut: () => dispatch(unhighlightNode()), + onDOMNodeMouseOver: () => dispatch(highlightNode(nodeFront)), + }), + dom.button({ + className: "layout-color-swatch", + "data-color": color, + ref: this.swatchEl, + style: { + backgroundColor: color, + }, + title: getFormatStr("layout.colorSwatch.tooltip", color), + }) + ), + dom.div( + { className: "flex-header-container-properties" }, + dom.div( + { + className: "inspector-badge", + role: "figure", + title: `flex-direction: ${properties["flex-direction"]}`, + }, + properties["flex-direction"] + ), + dom.div( + { + className: "inspector-badge", + role: "figure", + title: `flex-wrap: ${properties["flex-wrap"]}`, + }, + properties["flex-wrap"] + ) + ) + ); + } +} + +module.exports = FlexContainer; diff --git a/devtools/client/inspector/flexbox/components/FlexItem.js b/devtools/client/inspector/flexbox/components/FlexItem.js new file mode 100644 index 0000000000..b29da8243e --- /dev/null +++ b/devtools/client/inspector/flexbox/components/FlexItem.js @@ -0,0 +1,60 @@ +/* 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"); + +loader.lazyRequireGetter( + this, + "getNodeRep", + "resource://devtools/client/inspector/shared/node-reps.js" +); + +const Types = require("resource://devtools/client/inspector/flexbox/types.js"); + +const { + highlightNode, + unhighlightNode, +} = require("resource://devtools/client/inspector/boxmodel/actions/box-model-highlighter.js"); + +class FlexItem extends PureComponent { + static get propTypes() { + return { + dispatch: PropTypes.func.isRequired, + flexItem: PropTypes.shape(Types.flexItem).isRequired, + index: PropTypes.number.isRequired, + scrollToTop: PropTypes.func.isRequired, + setSelectedNode: PropTypes.func.isRequired, + }; + } + + render() { + const { dispatch, flexItem, index, scrollToTop, setSelectedNode } = + this.props; + const { nodeFront } = flexItem; + + return dom.button( + { + className: "devtools-button devtools-monospace", + onClick: e => { + e.stopPropagation(); + scrollToTop(); + setSelectedNode(nodeFront); + dispatch(unhighlightNode()); + }, + onMouseOut: () => dispatch(unhighlightNode()), + onMouseOver: () => dispatch(highlightNode(nodeFront)), + }, + dom.span({ className: "flex-item-index" }, index), + getNodeRep(nodeFront) + ); + } +} + +module.exports = FlexItem; diff --git a/devtools/client/inspector/flexbox/components/FlexItemList.js b/devtools/client/inspector/flexbox/components/FlexItemList.js new file mode 100644 index 0000000000..0824ee1175 --- /dev/null +++ b/devtools/client/inspector/flexbox/components/FlexItemList.js @@ -0,0 +1,62 @@ +/* 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 FlexItem = createFactory( + require("resource://devtools/client/inspector/flexbox/components/FlexItem.js") +); + +const Types = require("resource://devtools/client/inspector/flexbox/types.js"); + +class FlexItemList extends PureComponent { + static get propTypes() { + return { + dispatch: PropTypes.func.isRequired, + flexItems: PropTypes.arrayOf(PropTypes.shape(Types.flexItem)).isRequired, + scrollToTop: PropTypes.func.isRequired, + setSelectedNode: PropTypes.func.isRequired, + }; + } + + render() { + const { dispatch, flexItems, scrollToTop, setSelectedNode } = this.props; + + return dom.div( + { className: "flex-item-list" }, + dom.div( + { + className: "flex-item-list-header", + role: "heading", + "aria-level": "3", + }, + !flexItems.length + ? getStr("flexbox.noFlexItems") + : getStr("flexbox.flexItems") + ), + flexItems.map((flexItem, index) => + FlexItem({ + key: flexItem.actorID, + dispatch, + flexItem, + index: index + 1, + scrollToTop, + setSelectedNode, + }) + ) + ); + } +} + +module.exports = FlexItemList; diff --git a/devtools/client/inspector/flexbox/components/FlexItemSelector.js b/devtools/client/inspector/flexbox/components/FlexItemSelector.js new file mode 100644 index 0000000000..d8dd2db7a3 --- /dev/null +++ b/devtools/client/inspector/flexbox/components/FlexItemSelector.js @@ -0,0 +1,81 @@ +/* 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 { + getSelectorFromGrip, + translateNodeFrontToGrip, +} = require("resource://devtools/client/inspector/shared/utils.js"); + +loader.lazyRequireGetter( + this, + "getNodeRep", + "resource://devtools/client/inspector/shared/node-reps.js" +); + +const Types = require("resource://devtools/client/inspector/flexbox/types.js"); + +loader.lazyRequireGetter( + this, + "showMenu", + "resource://devtools/client/shared/components/menu/utils.js", + true +); + +class FlexItemSelector extends PureComponent { + static get propTypes() { + return { + flexItem: PropTypes.shape(Types.flexItem).isRequired, + flexItems: PropTypes.arrayOf(PropTypes.shape(Types.flexItem)).isRequired, + setSelectedNode: PropTypes.func.isRequired, + }; + } + + constructor(props) { + super(props); + this.onShowFlexItemMenu = this.onShowFlexItemMenu.bind(this); + } + + onShowFlexItemMenu(event) { + event.stopPropagation(); + + const { flexItem, flexItems, setSelectedNode } = this.props; + const menuItems = []; + + for (const item of flexItems) { + const grip = translateNodeFrontToGrip(item.nodeFront); + menuItems.push({ + label: getSelectorFromGrip(grip), + type: "checkbox", + checked: item === flexItem, + click: () => setSelectedNode(item.nodeFront), + }); + } + + showMenu(menuItems, { + button: event.target, + }); + } + + render() { + const { flexItem } = this.props; + + return dom.button( + { + id: "flex-item-selector", + className: "devtools-button devtools-dropdown-button", + onClick: this.onShowFlexItemMenu, + }, + getNodeRep(flexItem.nodeFront) + ); + } +} + +module.exports = FlexItemSelector; diff --git a/devtools/client/inspector/flexbox/components/FlexItemSizingOutline.js b/devtools/client/inspector/flexbox/components/FlexItemSizingOutline.js new file mode 100644 index 0000000000..f0c35cd6be --- /dev/null +++ b/devtools/client/inspector/flexbox/components/FlexItemSizingOutline.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 { + 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 Types = require("resource://devtools/client/inspector/flexbox/types.js"); + +class FlexItemSizingOutline extends PureComponent { + static get propTypes() { + return { + flexDirection: PropTypes.string.isRequired, + flexItem: PropTypes.shape(Types.flexItem).isRequired, + }; + } + + renderBasisOutline(mainBaseSize) { + return dom.div({ + className: "flex-outline-basis" + (!mainBaseSize ? " zero-basis" : ""), + }); + } + + renderDeltaOutline(mainDeltaSize) { + if (!mainDeltaSize) { + return null; + } + + return dom.div({ + className: "flex-outline-delta", + }); + } + + renderFinalOutline(isClamped) { + return dom.div({ + className: "flex-outline-final" + (isClamped ? " clamped" : ""), + }); + } + + renderPoint(className, label = className) { + return dom.div({ + key: className, + className: `flex-outline-point ${className}`, + "data-label": label, + }); + } + + render() { + const { flexItemSizing } = this.props.flexItem; + const { + mainAxisDirection, + mainBaseSize, + mainDeltaSize, + mainMaxSize, + mainMinSize, + clampState, + } = flexItemSizing; + + // Calculate the final size. This is base + delta, then clamped by min or max. + let mainFinalSize = mainBaseSize + mainDeltaSize; + mainFinalSize = Math.max(mainFinalSize, mainMinSize); + mainFinalSize = + mainMaxSize === null + ? mainFinalSize + : Math.min(mainFinalSize, mainMaxSize); + + // Just don't display anything if there isn't anything useful. + if (!mainFinalSize && !mainBaseSize && !mainDeltaSize) { + return null; + } + + // The max size is only interesting to show if it did clamp the item. + const showMax = clampState === "clamped_to_max"; + + // The min size is only really interesting if it actually clamped the item. + // TODO: We might also want to check if the min-size property is defined. + const showMin = clampState === "clamped_to_min"; + + // Create an array of all of the sizes we want to display that we can use to create + // a grid track template. + let sizes = [ + { name: "basis-start", size: 0 }, + { name: "basis-end", size: mainBaseSize }, + { name: "final-start", size: 0 }, + { name: "final-end", size: mainFinalSize }, + ]; + + // Because mainDeltaSize is just a delta from base, make sure to make it absolute like + // the other sizes in the array, so we can later sort all sizes in the same way. + if (mainDeltaSize > 0) { + sizes.push({ name: "delta-start", size: mainBaseSize }); + sizes.push({ name: "delta-end", size: mainBaseSize + mainDeltaSize }); + } else { + sizes.push({ name: "delta-start", size: mainBaseSize + mainDeltaSize }); + sizes.push({ name: "delta-end", size: mainBaseSize }); + } + + if (showMax) { + sizes.push({ name: "max", size: mainMaxSize }); + } + if (showMin) { + sizes.push({ name: "min", size: mainMinSize }); + } + + // Sort all of the dimensions so we can create the grid track template correctly. + sizes = sizes.sort((a, b) => a.size - b.size); + + // In some cases, the delta-start may be negative (when an item wanted to shrink more + // than the item's base size). As a negative value would break the grid track template + // offset all values so they're all positive. + const offsetBy = sizes.reduce( + (acc, curr) => (curr.size < acc ? curr.size : acc), + 0 + ); + sizes = sizes.map(entry => ({ + size: entry.size - offsetBy, + name: entry.name, + })); + + let gridTemplateColumns = "["; + let accumulatedSize = 0; + for (const { name, size } of sizes) { + const breadth = Math.round(size - accumulatedSize); + if (breadth === 0) { + gridTemplateColumns += ` ${name}`; + continue; + } + + gridTemplateColumns += `] ${breadth}fr [${name}`; + accumulatedSize = size; + } + gridTemplateColumns += "]"; + + // Check the final and basis points to see if they are the same and if so, combine + // them into a single rendered point. + const renderedBaseAndFinalPoints = []; + if (mainFinalSize === mainBaseSize) { + renderedBaseAndFinalPoints.push( + this.renderPoint("basisfinal", "basis/final") + ); + } else { + renderedBaseAndFinalPoints.push(this.renderPoint("basis")); + renderedBaseAndFinalPoints.push(this.renderPoint("final")); + } + + return dom.div( + { className: "flex-outline-container" }, + dom.div( + { + className: + `flex-outline ${mainAxisDirection}` + + (mainDeltaSize > 0 ? " growing" : " shrinking"), + style: { + gridTemplateColumns, + }, + }, + renderedBaseAndFinalPoints, + showMin ? this.renderPoint("min") : null, + showMax ? this.renderPoint("max") : null, + this.renderBasisOutline(mainBaseSize), + this.renderDeltaOutline(mainDeltaSize), + this.renderFinalOutline(clampState !== "unclamped") + ) + ); + } +} + +module.exports = FlexItemSizingOutline; diff --git a/devtools/client/inspector/flexbox/components/FlexItemSizingProperties.js b/devtools/client/inspector/flexbox/components/FlexItemSizingProperties.js new file mode 100644 index 0000000000..00bea31e57 --- /dev/null +++ b/devtools/client/inspector/flexbox/components/FlexItemSizingProperties.js @@ -0,0 +1,326 @@ +/* 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/flexbox/types.js"); + +const getFlexibilityReasons = ({ + lineGrowthState, + computedFlexGrow, + computedFlexShrink, + grew, + shrank, +}) => { + const reasons = []; + + // Tell users whether the item was set to grow or shrink. + if (computedFlexGrow && lineGrowthState === "growing") { + reasons.push(getStr("flexbox.itemSizing.setToGrow")); + } + if (computedFlexShrink && lineGrowthState === "shrinking") { + reasons.push(getStr("flexbox.itemSizing.setToShrink")); + } + if (!computedFlexGrow && !grew && !shrank && lineGrowthState === "growing") { + reasons.push(getStr("flexbox.itemSizing.notSetToGrow")); + } + if ( + !computedFlexShrink && + !grew && + !shrank && + lineGrowthState === "shrinking" + ) { + reasons.push(getStr("flexbox.itemSizing.notSetToShrink")); + } + + return reasons; +}; + +class FlexItemSizingProperties extends PureComponent { + static get propTypes() { + return { + flexDirection: PropTypes.string.isRequired, + flexItem: PropTypes.shape(Types.flexItem).isRequired, + }; + } + + /** + * Rounds some size in pixels and render it. + * The rendered value will end with 'px' (unless the dimension is 0 in which case the + * unit will be omitted) + * + * @param {Number} value + * The number to be rounded + * @param {Boolean} prependPlusSign + * If set to true, the + sign will be printed before a positive value + * @return {Object} + * The React component representing this rounded size + */ + renderSize(value, prependPlusSign) { + if (value == 0) { + return dom.span({ className: "value" }, "0"); + } + + value = Math.round(value * 100) / 100; + if (prependPlusSign && value > 0) { + value = "+" + value; + } + + return dom.span( + { className: "value" }, + value, + dom.span({ className: "unit" }, "px") + ); + } + + /** + * Render an authored CSS property. + * + * @param {String} name + * The name for this CSS property + * @param {String} value + * The property value + * @param {Booleam} isDefaultValue + * Whether the value come from the browser default style + * @return {Object} + * The React component representing this CSS property + */ + renderCssProperty(name, value, isDefaultValue) { + return dom.span({ className: "css-property-link" }, `(${name}: ${value})`); + } + + /** + * Render a list of sentences to be displayed in the UI as reasons why a certain sizing + * value happened. + * + * @param {Array} sentences + * The list of sentences as Strings + * @return {Object} + * The React component representing these sentences + */ + renderReasons(sentences) { + return dom.ul( + { className: "reasons" }, + sentences.map(sentence => dom.li({}, sentence)) + ); + } + + renderBaseSizeSection({ mainBaseSize, clampState }, properties, dimension) { + const flexBasisValue = properties["flex-basis"]; + const dimensionValue = properties[dimension]; + + let title = getStr("flexbox.itemSizing.baseSizeSectionHeader"); + let property = null; + + if (flexBasisValue) { + // If flex-basis is defined, then that's what is used for the base size. + property = this.renderCssProperty("flex-basis", flexBasisValue); + } else if (dimensionValue) { + // If not and width/height is defined, then that's what defines the base size. + property = this.renderCssProperty(dimension, dimensionValue); + } else { + // Finally, if nothing is set, then the base size is the max-content size. + // In this case replace the section's title. + title = getStr("flexbox.itemSizing.itemContentSize"); + } + + const className = "section base"; + return dom.li( + { className: className + (property ? "" : " no-property") }, + dom.span({ className: "name" }, title, property), + this.renderSize(mainBaseSize) + ); + } + + renderFlexibilitySection( + flexItemSizing, + mainFinalSize, + properties, + computedStyle + ) { + const { mainDeltaSize, mainBaseSize, lineGrowthState } = flexItemSizing; + + // Don't display anything if all interesting sizes are 0. + if (!mainFinalSize && !mainBaseSize && !mainDeltaSize) { + return null; + } + + // Also don't display anything if the item did not grow or shrink. + const grew = mainDeltaSize > 0; + const shrank = mainDeltaSize < 0; + if (!grew && !shrank) { + return null; + } + + const definedFlexGrow = properties["flex-grow"]; + const computedFlexGrow = computedStyle.flexGrow; + const definedFlexShrink = properties["flex-shrink"]; + const computedFlexShrink = computedStyle.flexShrink; + + const reasons = getFlexibilityReasons({ + lineGrowthState, + computedFlexGrow, + computedFlexShrink, + grew, + shrank, + }); + + let property = null; + + if (grew && definedFlexGrow && computedFlexGrow) { + // If the item grew it's normally because it was set to grow (flex-grow is non 0). + property = this.renderCssProperty("flex-grow", definedFlexGrow); + } else if (shrank && definedFlexShrink && computedFlexShrink) { + // If the item shrank it's either because flex-shrink is non 0. + property = this.renderCssProperty("flex-shrink", definedFlexShrink); + } else if (shrank && computedFlexShrink) { + // Or also because it's default value is 1 anyway. + property = this.renderCssProperty( + "flex-shrink", + computedFlexShrink, + true + ); + } + + // Don't display the section at all if there's nothing useful to show users. + if (!property && !reasons.length) { + return null; + } + + const className = "section flexibility"; + return dom.li( + { className: className + (property ? "" : " no-property") }, + dom.span( + { className: "name" }, + getStr("flexbox.itemSizing.flexibilitySectionHeader"), + property + ), + this.renderSize(mainDeltaSize, true), + this.renderReasons(reasons) + ); + } + + renderMinimumSizeSection(flexItemSizing, properties, dimension) { + const { clampState, mainMinSize, mainDeltaSize } = flexItemSizing; + const grew = mainDeltaSize > 0; + const shrank = mainDeltaSize < 0; + const minDimensionValue = properties[`min-${dimension}`]; + + // We only display the minimum size when the item actually violates that size during + // layout & is clamped. + if (clampState !== "clamped_to_min") { + return null; + } + + const reasons = []; + if (grew || shrank) { + // The item may have wanted to grow less, but was min-clamped to a larger size. + // Or the item may have wanted to shrink more but was min-clamped to a larger size. + reasons.push(getStr("flexbox.itemSizing.clampedToMin")); + } + + return dom.li( + { className: "section min" }, + dom.span( + { className: "name" }, + getStr("flexbox.itemSizing.minSizeSectionHeader"), + minDimensionValue.length + ? this.renderCssProperty(`min-${dimension}`, minDimensionValue) + : null + ), + this.renderSize(mainMinSize), + this.renderReasons(reasons) + ); + } + + renderMaximumSizeSection(flexItemSizing, properties, dimension) { + const { clampState, mainMaxSize, mainDeltaSize } = flexItemSizing; + const grew = mainDeltaSize > 0; + const shrank = mainDeltaSize < 0; + const maxDimensionValue = properties[`max-${dimension}`]; + + if (clampState !== "clamped_to_max") { + return null; + } + + const reasons = []; + if (grew || shrank) { + // The item may have wanted to grow more than it did, because it was max-clamped. + // Or the item may have wanted shrink more, but it was clamped to its max size. + reasons.push(getStr("flexbox.itemSizing.clampedToMax")); + } + + return dom.li( + { className: "section max" }, + dom.span( + { className: "name" }, + getStr("flexbox.itemSizing.maxSizeSectionHeader"), + maxDimensionValue.length + ? this.renderCssProperty(`max-${dimension}`, maxDimensionValue) + : null + ), + this.renderSize(mainMaxSize), + this.renderReasons(reasons) + ); + } + + renderFinalSizeSection(mainFinalSize) { + return dom.li( + { className: "section final no-property" }, + dom.span( + { className: "name" }, + getStr("flexbox.itemSizing.finalSizeSectionHeader") + ), + this.renderSize(mainFinalSize) + ); + } + + render() { + const { flexItem } = this.props; + const { computedStyle, flexItemSizing, properties } = flexItem; + const { + mainAxisDirection, + mainBaseSize, + mainDeltaSize, + mainMaxSize, + mainMinSize, + } = flexItemSizing; + const dimension = mainAxisDirection.startsWith("horizontal") + ? "width" + : "height"; + + // Calculate the final size. This is base + delta, then clamped by min or max. + let mainFinalSize = mainBaseSize + mainDeltaSize; + mainFinalSize = Math.max(mainFinalSize, mainMinSize); + mainFinalSize = + mainMaxSize === null + ? mainFinalSize + : Math.min(mainFinalSize, mainMaxSize); + + return dom.ul( + { className: "flex-item-sizing" }, + this.renderBaseSizeSection(flexItemSizing, properties, dimension), + this.renderFlexibilitySection( + flexItemSizing, + mainFinalSize, + properties, + computedStyle + ), + this.renderMinimumSizeSection(flexItemSizing, properties, dimension), + this.renderMaximumSizeSection(flexItemSizing, properties, dimension), + this.renderFinalSizeSection(mainFinalSize) + ); + } +} + +module.exports = FlexItemSizingProperties; diff --git a/devtools/client/inspector/flexbox/components/Flexbox.js b/devtools/client/inspector/flexbox/components/Flexbox.js new file mode 100644 index 0000000000..a0d79d92eb --- /dev/null +++ b/devtools/client/inspector/flexbox/components/Flexbox.js @@ -0,0 +1,128 @@ +/* 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, + createFactory, + 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"); +const { + getStr, +} = require("resource://devtools/client/inspector/layout/utils/l10n.js"); + +loader.lazyGetter(this, "FlexItemList", function () { + return createFactory( + require("resource://devtools/client/inspector/flexbox/components/FlexItemList.js") + ); +}); +loader.lazyGetter(this, "FlexItemSizingOutline", function () { + return createFactory( + require("resource://devtools/client/inspector/flexbox/components/FlexItemSizingOutline.js") + ); +}); +loader.lazyGetter(this, "FlexItemSizingProperties", function () { + return createFactory( + require("resource://devtools/client/inspector/flexbox/components/FlexItemSizingProperties.js") + ); +}); +loader.lazyGetter(this, "Header", function () { + return createFactory( + require("resource://devtools/client/inspector/flexbox/components/Header.js") + ); +}); + +const Types = require("resource://devtools/client/inspector/flexbox/types.js"); + +class Flexbox extends PureComponent { + static get propTypes() { + return { + dispatch: PropTypes.func.isRequired, + flexbox: PropTypes.shape(Types.flexbox).isRequired, + flexContainer: PropTypes.shape(Types.flexContainer).isRequired, + getSwatchColorPickerTooltip: PropTypes.func.isRequired, + onSetFlexboxOverlayColor: PropTypes.func.isRequired, + scrollToTop: PropTypes.func.isRequired, + setSelectedNode: PropTypes.func.isRequired, + }; + } + + renderFlexItemList() { + const { dispatch, scrollToTop, setSelectedNode } = this.props; + const { flexItems } = this.props.flexContainer; + + return FlexItemList({ + dispatch, + flexItems, + scrollToTop, + setSelectedNode, + }); + } + + renderFlexItemSizing() { + const { flexItems, flexItemShown, properties } = this.props.flexContainer; + + const flexItem = flexItems.find( + item => item.nodeFront.actorID === flexItemShown + ); + if (!flexItem) { + return null; + } + + return createElement( + Fragment, + null, + FlexItemSizingOutline({ + flexDirection: properties["flex-direction"], + flexItem, + }), + FlexItemSizingProperties({ + flexDirection: properties["flex-direction"], + flexItem, + }) + ); + } + + render() { + const { + dispatch, + flexbox, + flexContainer, + getSwatchColorPickerTooltip, + onSetFlexboxOverlayColor, + setSelectedNode, + } = this.props; + + if (!flexContainer.actorID) { + return dom.div( + { className: "devtools-sidepanel-no-result" }, + getStr("flexbox.noFlexboxeOnThisPage") + ); + } + + const { flexItemShown } = flexContainer; + const { color, highlighted } = flexbox; + + return dom.div( + { className: "layout-flexbox-wrapper" }, + Header({ + color, + dispatch, + flexContainer, + getSwatchColorPickerTooltip, + highlighted, + onSetFlexboxOverlayColor, + setSelectedNode, + }), + !flexItemShown ? this.renderFlexItemList() : null, + flexItemShown ? this.renderFlexItemSizing() : null + ); + } +} + +module.exports = Flexbox; diff --git a/devtools/client/inspector/flexbox/components/Header.js b/devtools/client/inspector/flexbox/components/Header.js new file mode 100644 index 0000000000..44b19703e3 --- /dev/null +++ b/devtools/client/inspector/flexbox/components/Header.js @@ -0,0 +1,142 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createElement, + createFactory, + 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"); +const { + getStr, +} = require("resource://devtools/client/inspector/layout/utils/l10n.js"); + +const FlexContainer = createFactory( + require("resource://devtools/client/inspector/flexbox/components/FlexContainer.js") +); +const FlexItemSelector = createFactory( + require("resource://devtools/client/inspector/flexbox/components/FlexItemSelector.js") +); + +const Types = require("resource://devtools/client/inspector/flexbox/types.js"); + +const { + toggleFlexboxHighlighter, +} = require("resource://devtools/client/inspector/flexbox/actions/flexbox-highlighter.js"); + +class Header extends PureComponent { + static get propTypes() { + return { + color: PropTypes.string.isRequired, + dispatch: PropTypes.func.isRequired, + flexContainer: PropTypes.shape(Types.flexContainer).isRequired, + getSwatchColorPickerTooltip: PropTypes.func.isRequired, + highlighted: PropTypes.bool.isRequired, + onSetFlexboxOverlayColor: PropTypes.func.isRequired, + setSelectedNode: PropTypes.func.isRequired, + }; + } + + renderFlexboxHighlighterToggle() { + const { dispatch, flexContainer, highlighted } = this.props; + // Don't show the flexbox highlighter toggle for the parent flex container of the + // selected element. + if (flexContainer.isFlexItemContainer) { + return null; + } + + return createElement( + Fragment, + null, + dom.div({ className: "devtools-separator" }), + dom.input({ + id: "flexbox-checkbox-toggle", + className: "devtools-checkbox-toggle", + checked: highlighted, + onChange: () => + dispatch(toggleFlexboxHighlighter(flexContainer.nodeFront, "layout")), + title: getStr("flexbox.togglesFlexboxHighlighter2"), + type: "checkbox", + }) + ); + } + + renderFlexContainer() { + if (this.props.flexContainer.flexItemShown) { + return null; + } + + const { + color, + dispatch, + flexContainer, + getSwatchColorPickerTooltip, + onSetFlexboxOverlayColor, + } = this.props; + + return FlexContainer({ + color, + dispatch, + flexContainer, + getSwatchColorPickerTooltip, + onSetFlexboxOverlayColor, + }); + } + + renderFlexItemSelector() { + if (!this.props.flexContainer.flexItemShown) { + return null; + } + + const { flexContainer, setSelectedNode } = this.props; + const { flexItems, flexItemShown } = flexContainer; + const flexItem = flexItems.find( + item => item.nodeFront.actorID === flexItemShown + ); + + if (!flexItem) { + return null; + } + + return FlexItemSelector({ + flexItem, + flexItems, + setSelectedNode, + }); + } + + render() { + const { flexContainer, setSelectedNode } = this.props; + const { flexItemShown, nodeFront } = flexContainer; + + return dom.div( + { className: "flex-header devtools-monospace" }, + flexItemShown + ? dom.button({ + className: "flex-header-button-prev devtools-button", + "aria-label": getStr("flexbox.backButtonLabel"), + onClick: e => { + e.stopPropagation(); + setSelectedNode(nodeFront); + }, + }) + : null, + dom.div( + { + className: + "flex-header-content" + (flexItemShown ? " flex-item-shown" : ""), + }, + this.renderFlexContainer(), + this.renderFlexItemSelector() + ), + this.renderFlexboxHighlighterToggle() + ); + } +} + +module.exports = Header; diff --git a/devtools/client/inspector/flexbox/components/moz.build b/devtools/client/inspector/flexbox/components/moz.build new file mode 100644 index 0000000000..3e077a217f --- /dev/null +++ b/devtools/client/inspector/flexbox/components/moz.build @@ -0,0 +1,16 @@ +# -*- 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( + "Flexbox.js", + "FlexContainer.js", + "FlexItem.js", + "FlexItemList.js", + "FlexItemSelector.js", + "FlexItemSizingOutline.js", + "FlexItemSizingProperties.js", + "Header.js", +) diff --git a/devtools/client/inspector/flexbox/flexbox.js b/devtools/client/inspector/flexbox/flexbox.js new file mode 100644 index 0000000000..3947da3cf9 --- /dev/null +++ b/devtools/client/inspector/flexbox/flexbox.js @@ -0,0 +1,569 @@ +/* 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 { throttle } = require("resource://devtools/shared/throttle.js"); + +const { + clearFlexbox, + updateFlexbox, + updateFlexboxColor, + updateFlexboxHighlighted, +} = require("resource://devtools/client/inspector/flexbox/actions/flexbox.js"); +const flexboxReducer = require("resource://devtools/client/inspector/flexbox/reducers/flexbox.js"); + +loader.lazyRequireGetter( + this, + "parseURL", + "resource://devtools/client/shared/source-utils.js", + true +); +loader.lazyRequireGetter( + this, + "asyncStorage", + "resource://devtools/shared/async-storage.js" +); + +const FLEXBOX_COLOR = "#9400FF"; + +class FlexboxInspector { + constructor(inspector, window) { + this.document = window.document; + this.inspector = inspector; + this.selection = inspector.selection; + this.store = inspector.store; + + this.store.injectReducer("flexbox", flexboxReducer); + + 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.onSetFlexboxOverlayColor = this.onSetFlexboxOverlayColor.bind(this); + this.onSidebarSelect = this.onSidebarSelect.bind(this); + this.onUpdatePanel = this.onUpdatePanel.bind(this); + + this.init(); + } + + init() { + if (!this.inspector) { + return; + } + + this.inspector.highlighters.on( + "highlighter-shown", + this.onHighlighterShown + ); + this.inspector.highlighters.on( + "highlighter-hidden", + this.onHighlighterHidden + ); + this.inspector.sidebar.on("select", this.onSidebarSelect); + + this.onSidebarSelect(); + } + + destroy() { + this.selection.off("new-node-front", this.onUpdatePanel); + this.inspector.off("new-root", this.onNavigate); + this.inspector.off("reflow-in-selected-target", this.onReflow); + this.inspector.highlighters.off( + "highlighter-shown", + this.onHighlighterShown + ); + this.inspector.highlighters.off( + "highlighter-hidden", + this.onHighlighterHidden + ); + this.inspector.sidebar.off("select", this.onSidebarSelect); + + this._customHostColors = null; + this._overlayColor = null; + this.document = null; + this.inspector = null; + this.selection = null; + this.store = null; + } + + getComponentProps() { + return { + onSetFlexboxOverlayColor: this.onSetFlexboxOverlayColor, + }; + } + + /** + * Returns an object containing the custom flexbox colors for different hosts. + * + * @return {Object} that maps a host name to a custom flexbox color for a given host. + */ + async getCustomHostColors() { + if (this._customHostColors) { + return this._customHostColors; + } + + // Cache the custom host colors to avoid refetching from async storage. + this._customHostColors = + (await asyncStorage.getItem("flexboxInspectorHostColors")) || {}; + return this._customHostColors; + } + + /** + * Returns the flex container properties for a given node. If the given node is a flex + * item, it attempts to fetch the flex container of the parent node of the given node. + * + * @param {NodeFront} nodeFront + * The NodeFront to fetch the flex container properties. + * @param {Boolean} onlyLookAtParents + * Whether or not to only consider the parent node of the given node. + * @return {Object} consisting of the given node's flex container's properties. + */ + async getFlexContainerProps(nodeFront, onlyLookAtParents = false) { + const layoutFront = await nodeFront.walkerFront.getLayoutInspector(); + const flexboxFront = await layoutFront.getCurrentFlexbox( + nodeFront, + onlyLookAtParents + ); + + if (!flexboxFront) { + return null; + } + + // If the FlexboxFront doesn't yet have access to the NodeFront for its container, + // then get it from the walker. This happens when the walker hasn't seen this + // particular DOM Node in the tree yet or when we are connected to an older server. + let containerNodeFront = flexboxFront.containerNodeFront; + if (!containerNodeFront) { + containerNodeFront = await flexboxFront.walkerFront.getNodeFromActor( + flexboxFront.actorID, + ["containerEl"] + ); + } + + const flexItems = await this.getFlexItems(flexboxFront); + + // If the current selected node is a flex item, display its flex item sizing + // properties. + let flexItemShown = null; + if (onlyLookAtParents) { + flexItemShown = this.selection.nodeFront.actorID; + } else { + const selectedFlexItem = flexItems.find( + item => item.nodeFront === this.selection.nodeFront + ); + if (selectedFlexItem) { + flexItemShown = selectedFlexItem.nodeFront.actorID; + } + } + + return { + actorID: flexboxFront.actorID, + flexItems, + flexItemShown, + isFlexItemContainer: onlyLookAtParents, + nodeFront: containerNodeFront, + properties: flexboxFront.properties, + }; + } + + /** + * Returns an array of flex items object for the given flex container front. + * + * @param {FlexboxFront} flexboxFront + * A flex container FlexboxFront. + * @return {Array} of objects containing the flex item front properties. + */ + async getFlexItems(flexboxFront) { + const flexItemFronts = await flexboxFront.getFlexItems(); + const flexItems = []; + + for (const flexItemFront of flexItemFronts) { + // Fetch the NodeFront of the flex items. + let itemNodeFront = flexItemFront.nodeFront; + if (!itemNodeFront) { + itemNodeFront = await flexItemFront.walkerFront.getNodeFromActor( + flexItemFront.actorID, + ["element"] + ); + } + + flexItems.push({ + actorID: flexItemFront.actorID, + computedStyle: flexItemFront.computedStyle, + flexItemSizing: flexItemFront.flexItemSizing, + nodeFront: itemNodeFront, + properties: flexItemFront.properties, + }); + } + + return flexItems; + } + + /** + * Returns the custom overlay color for the current host or the default flexbox color. + * + * @return {String} overlay color. + */ + async getOverlayColor() { + if (this._overlayColor) { + return this._overlayColor; + } + + // Cache the overlay color for the current host to avoid repeatably parsing the host + // and fetching the custom color from async storage. + const customColors = await this.getCustomHostColors(); + 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; + this._overlayColor = customColors[hostname] + ? customColors[hostname] + : FLEXBOX_COLOR; + return this._overlayColor; + } + + /** + * 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" + ); + } + + /** + * Handler for "highlighter-shown" events emitted by HighlightersOverlay. + * If the event is dispatched on behalf of a flex highlighter, toggle the + * corresponding flex container's highlighted state in the Redux store. + * + * @param {Object} data + * Object with data associated with the highlighter event. + * {NodeFront} data.nodeFront + * The NodeFront of the flex container element for which the flexbox + * highlighter is shown for. + * {String} data.type + * Highlighter type + */ + onHighlighterShown(data) { + if (data.type === this.inspector.highlighters.TYPES.FLEXBOX) { + this.onHighlighterChange(true, data.nodeFront); + } + } + + /** + * Handler for "highlighter-shown" events emitted by HighlightersOverlay. + * If the event is dispatched on behalf of a flex highlighter, toggle the + * corresponding flex container's highlighted state in the Redux store. + * + * @param {Object} data + * Object with data associated with the highlighter event. + * {NodeFront} data.nodeFront + * The NodeFront of the flex container element for which the flexbox + * highlighter was previously shown for. + * {String} data.type + * Highlighter type + */ + onHighlighterHidden(data) { + if (data.type === this.inspector.highlighters.TYPES.FLEXBOX) { + this.onHighlighterChange(false, data.nodeFront); + } + } + + /** + * Updates the flex container highlighted state in the Redux store if the provided + * NodeFront is the current selected flex container. + * + * @param {Boolean} highlighted + * Whether the change is to highlight or hide the overlay. + * @param {NodeFront} nodeFront + * The NodeFront of the flex container element for which the flexbox + * highlighter is shown for. + */ + onHighlighterChange(highlighted, nodeFront) { + const { flexbox } = this.store.getState(); + + if ( + flexbox.flexContainer.nodeFront === nodeFront && + flexbox.highlighted !== highlighted + ) { + this.store.dispatch(updateFlexboxHighlighted(highlighted)); + } + } + + /** + * Handler for the "new-root" event fired by the inspector. Clears the cached overlay + * color for the flexbox highlighter and updates the panel. + */ + onNavigate() { + this._overlayColor = null; + this.onUpdatePanel(); + } + + /** + * Handler for reflow events fired by the inspector when a node is selected. On reflows, + * update the flexbox panel because the shape of the flexbox on the page may have + * changed. + */ + async onReflow() { + if ( + !this.isPanelVisible() || + !this.store || + !this.selection.nodeFront || + this._isUpdating + ) { + return; + } + + try { + const flexContainer = await this.getFlexContainerProps( + this.selection.nodeFront + ); + + // Clear the flexbox panel if there is no flex container for the current node + // selection. + if (!flexContainer) { + this.store.dispatch(clearFlexbox()); + return; + } + + const { flexbox } = this.store.getState(); + + // Compare the new flexbox state of the current selected nodeFront with the old + // flexbox state to determine if we need to update. + if (hasFlexContainerChanged(flexbox.flexContainer, flexContainer)) { + this.update(flexContainer); + return; + } + + let flexItemContainer = null; + // If the current selected node is also the flex container node, check if it is + // a flex item of a parent flex container. + if (flexContainer.nodeFront === this.selection.nodeFront) { + flexItemContainer = await this.getFlexContainerProps( + this.selection.nodeFront, + true + ); + } + + // Compare the new and old state of the parent flex container properties. + if ( + hasFlexContainerChanged(flexbox.flexItemContainer, flexItemContainer) + ) { + this.update(flexContainer, flexItemContainer); + } + } catch (e) { + // This call might fail if called asynchrously after the toolbox is finished + // closing. + } + } + + /** + * Handler for a change in the flexbox overlay color picker for a flex container. + * + * @param {String} color + * A hex string representing the color to use. + */ + async onSetFlexboxOverlayColor(color) { + this.store.dispatch(updateFlexboxColor(color)); + + const { flexbox } = this.store.getState(); + + if (flexbox.highlighted) { + this.inspector.highlighters.showFlexboxHighlighter( + flexbox.flexContainer.nodeFront + ); + } + + this._overlayColor = color; + + 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 customColors = await this.getCustomHostColors(); + customColors[hostname] = color; + this._customHostColors = customColors; + await asyncStorage.setItem("flexboxInspectorHostColors", customColors); + } + + /** + * Handler for the inspector sidebar "select" event. Updates the flexbox panel if it + * is visible. + */ + onSidebarSelect() { + if (!this.isPanelVisible()) { + this.inspector.off("reflow-in-selected-target", this.onReflow); + this.inspector.off("new-root", this.onNavigate); + this.selection.off("new-node-front", this.onUpdatePanel); + return; + } + + this.inspector.on("reflow-in-selected-target", this.onReflow); + this.inspector.on("new-root", this.onNavigate); + this.selection.on("new-node-front", this.onUpdatePanel); + + this.update(); + } + + /** + * Handler for "new-root" event fired by the inspector and "new-node-front" event fired + * by the inspector selection. Updates the flexbox panel if it is visible. + * + * @param {Object} + * This callback is sometimes executed on "new-node-front" events which means + * that a first param is passed here (the nodeFront), which we don't care about. + * @param {String} reason + * On "new-node-front" events, a reason is passed here, and we need it to detect + * if this update was caused by a node selection from the markup-view. + */ + onUpdatePanel(_, reason) { + if (!this.isPanelVisible()) { + return; + } + + this.update(null, null, reason === "treepanel"); + } + + /** + * Updates the flexbox panel by dispatching the new flexbox data. This is called when + * the layout view becomes visible or a new node is selected and needs to be update + * with new flexbox data. + * + * @param {Object|null} flexContainer + * An object consisting of the current flex container's flex items and + * properties. + * @param {Object|null} flexItemContainer + * An object consisting of the parent flex container's flex items and + * properties. + * @param {Boolean} initiatedByMarkupViewSelection + * True if the update was due to a node selection in the markup-view. + */ + async update( + flexContainer, + flexItemContainer, + initiatedByMarkupViewSelection + ) { + this._isUpdating = true; + + // Stop refreshing if the inspector or store is already destroyed or no node is + // selected. + if (!this.inspector || !this.store || !this.selection.nodeFront) { + this._isUpdating = false; + return; + } + + try { + // Fetch the current flexbox if no flexbox front was passed into this update. + if (!flexContainer) { + flexContainer = await this.getFlexContainerProps( + this.selection.nodeFront + ); + } + + // Clear the flexbox panel if there is no flex container for the current node + // selection. + if (!flexContainer) { + this.store.dispatch(clearFlexbox()); + this._isUpdating = false; + return; + } + + if ( + !flexItemContainer && + flexContainer.nodeFront === this.selection.nodeFront + ) { + flexItemContainer = await this.getFlexContainerProps( + this.selection.nodeFront, + true + ); + } + + const highlighted = + flexContainer.nodeFront === + this.inspector.highlighters.getNodeForActiveHighlighter( + this.inspector.highlighters.TYPES.FLEXBOX + ); + const color = await this.getOverlayColor(); + + this.store.dispatch( + updateFlexbox({ + color, + flexContainer, + flexItemContainer, + highlighted, + initiatedByMarkupViewSelection, + }) + ); + } catch (e) { + // This call might fail if called asynchrously after the toolbox is finished + // closing. + } + + this._isUpdating = false; + } +} + +/** + * For a given flex container object, returns the flex container properties that can be + * used to check if 2 flex container objects are the same. + * + * @param {Object|null} flexContainer + * Object consisting of the flex container's properties. + * @return {Object|null} consisting of the comparable flex container's properties. + */ +function getComparableFlexContainerProperties(flexContainer) { + if (!flexContainer) { + return null; + } + + return { + flexItems: getComparableFlexItemsProperties(flexContainer.flexItems), + nodeFront: flexContainer.nodeFront.actorID, + properties: flexContainer.properties, + }; +} + +/** + * Given an array of flex item objects, returns the relevant flex item properties that can + * be compared to check if any changes has occurred. + * + * @param {Array} flexItems + * Array of objects containing the flex item properties. + * @return {Array} of objects consisting of the comparable flex item's properties. + */ +function getComparableFlexItemsProperties(flexItems) { + return flexItems.map(item => { + return { + computedStyle: item.computedStyle, + flexItemSizing: item.flexItemSizing, + nodeFront: item.nodeFront.actorID, + properties: item.properties, + }; + }); +} + +/** + * Compares the old and new flex container properties + * + * @param {Object} oldFlexContainer + * Object consisting of the old flex container's properties. + * @param {Object} newFlexContainer + * Object consisting of the new flex container's properties. + * @return {Boolean} true if the flex container properties are the same, false otherwise. + */ +function hasFlexContainerChanged(oldFlexContainer, newFlexContainer) { + return ( + JSON.stringify(getComparableFlexContainerProperties(oldFlexContainer)) !== + JSON.stringify(getComparableFlexContainerProperties(newFlexContainer)) + ); +} + +module.exports = FlexboxInspector; diff --git a/devtools/client/inspector/flexbox/moz.build b/devtools/client/inspector/flexbox/moz.build new file mode 100644 index 0000000000..b38f12db55 --- /dev/null +++ b/devtools/client/inspector/flexbox/moz.build @@ -0,0 +1,18 @@ +# -*- 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", +] + +DevToolsModules( + "flexbox.js", + "types.js", +) + +BROWSER_CHROME_MANIFESTS += ["test/browser.toml"] diff --git a/devtools/client/inspector/flexbox/reducers/flexbox.js b/devtools/client/inspector/flexbox/reducers/flexbox.js new file mode 100644 index 0000000000..856294a549 --- /dev/null +++ b/devtools/client/inspector/flexbox/reducers/flexbox.js @@ -0,0 +1,90 @@ +/* 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 { + CLEAR_FLEXBOX, + UPDATE_FLEXBOX, + UPDATE_FLEXBOX_COLOR, + UPDATE_FLEXBOX_HIGHLIGHTED, +} = require("resource://devtools/client/inspector/flexbox/actions/index.js"); + +const INITIAL_FLEXBOX = { + // The color of the flexbox highlighter overlay. + color: "", + // The flex container of the selected element. + flexContainer: { + // The actor ID of the selected flex container. + actorID: "", + // An array of flex items belonging to the selected flex container. + flexItems: [], + // The NodeFront actor ID of the flex item to display in the flex item sizing + // properties. + flexItemShown: null, + // This flag specifies that the flex container data represents the selected flex + // container. + isFlexItemContainer: false, + // The NodeFront of the selected flex container. + nodeFront: null, + // The computed style properties of the selected flex container. + properties: null, + }, + // The selected flex container can also be a flex item. This object contains the + // parent flex container properties of the selected element. + flexItemContainer: { + // The actor ID of the parent flex container. + actorID: "", + // An array of flex items belonging to the parent flex container. + flexItems: [], + // The NodeFront actor ID of the flex item to display in the flex item sizing + // properties. + flexItemShown: null, + // This flag specifies that the flex container data represents the parent flex + // container of the selected element. + isFlexItemContainer: true, + // The NodeFront of the parent flex container. + nodeFront: null, + // The computed styles properties of the parent flex container. + properties: null, + }, + // Whether or not the flexbox highlighter is highlighting the flex container. + highlighted: false, + // Whether or not the node selection that led to the flexbox tool being shown came from + // the user selecting a node in the markup-view (whereas, say, selecting in the flex + // items list) + initiatedByMarkupViewSelection: false, +}; + +const reducers = { + [CLEAR_FLEXBOX](flexbox, _) { + return INITIAL_FLEXBOX; + }, + + [UPDATE_FLEXBOX](_, { flexbox }) { + return flexbox; + }, + + [UPDATE_FLEXBOX_COLOR](flexbox, { color }) { + return { + ...flexbox, + color, + }; + }, + + [UPDATE_FLEXBOX_HIGHLIGHTED](flexbox, { highlighted }) { + return { + ...flexbox, + highlighted, + }; + }, +}; + +module.exports = function (flexbox = INITIAL_FLEXBOX, action) { + const reducer = reducers[action.type]; + if (!reducer) { + return flexbox; + } + return reducer(flexbox, action); +}; diff --git a/devtools/client/inspector/flexbox/reducers/index.js b/devtools/client/inspector/flexbox/reducers/index.js new file mode 100644 index 0000000000..7f415cb163 --- /dev/null +++ b/devtools/client/inspector/flexbox/reducers/index.js @@ -0,0 +1,7 @@ +/* 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"; + +exports.flexbox = require("resource://devtools/client/inspector/flexbox/reducers/flexbox.js"); diff --git a/devtools/client/inspector/flexbox/reducers/moz.build b/devtools/client/inspector/flexbox/reducers/moz.build new file mode 100644 index 0000000000..4327940888 --- /dev/null +++ b/devtools/client/inspector/flexbox/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( + "flexbox.js", + "index.js", +) diff --git a/devtools/client/inspector/flexbox/test/Ahem.ttf b/devtools/client/inspector/flexbox/test/Ahem.ttf Binary files differnew file mode 100644 index 0000000000..ac81cb0316 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/Ahem.ttf diff --git a/devtools/client/inspector/flexbox/test/browser.toml b/devtools/client/inspector/flexbox/test/browser.toml new file mode 100644 index 0000000000..764dd1ffba --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser.toml @@ -0,0 +1,91 @@ +[DEFAULT] +tags = "devtools" +subsuite = "devtools" +support-files = [ + "Ahem.ttf", + "doc_flexbox_CSS_property_with_!important.html", + "doc_flexbox_pseudos.html", + "doc_flexbox_specific_cases.html", + "doc_flexbox_text_nodes.html", + "doc_flexbox_unauthored_min_dimension.html", + "doc_flexbox_writing_modes.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_flexbox_accordion_state.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_flexbox_container_and_item.js"] + +["browser_flexbox_container_and_item_accordion_state.js"] + +["browser_flexbox_container_and_item_updates_on_change.js"] + +["browser_flexbox_container_element_rep.js"] + +["browser_flexbox_container_properties.js"] + +["browser_flexbox_empty_state.js"] + +["browser_flexbox_grand_parent_flex.js"] + +["browser_flexbox_highlighter_color_picker_on_ESC.js"] + +["browser_flexbox_highlighter_color_picker_on_RETURN.js"] + +["browser_flexbox_highlighter_opened_telemetry.js"] + +["browser_flexbox_item_list_01.js"] + +["browser_flexbox_item_list_02.js"] + +["browser_flexbox_item_list_updates_on_change.js"] + +["browser_flexbox_item_outline_exists.js"] + +["browser_flexbox_item_outline_has_correct_layout.js"] + +["browser_flexbox_item_outline_hidden_when_useless.js"] + +["browser_flexbox_item_outline_renders_basisfinal_points_correctly.js"] + +["browser_flexbox_item_outline_rotates_for_column.js"] + +["browser_flexbox_item_outline_rotates_for_different_writing_modes.js"] + +["browser_flexbox_non_flex_item_is_not_shown.js"] + +["browser_flexbox_pseudo_elements_are_listed.js"] + +["browser_flexbox_sizing_flexibility_not_displayed_when_useless.js"] + +["browser_flexbox_sizing_info_do_not_show_unspecified_min_dimension.js"] + +["browser_flexbox_sizing_info_exists.js"] + +["browser_flexbox_sizing_info_for_different_writing_modes.js"] + +["browser_flexbox_sizing_info_for_pseudos.js"] + +["browser_flexbox_sizing_info_for_text_nodes.js"] + +["browser_flexbox_sizing_info_has_correct_sections.js"] + +["browser_flexbox_sizing_info_matches_properties_with_!important.js"] + +["browser_flexbox_sizing_info_updates_on_change.js"] + +["browser_flexbox_sizing_wanted_to_grow_but_was_clamped.js"] + +["browser_flexbox_text_nodes_are_listed.js"] + +["browser_flexbox_text_nodes_are_not_inlined.js"] + +["browser_flexbox_toggle_flexbox_highlighter_01.js"] + +["browser_flexbox_toggle_flexbox_highlighter_02.js"] diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_accordion_state.js b/devtools/client/inspector/flexbox/test/browser_flexbox_accordion_state.js new file mode 100644 index 0000000000..eb4acd8a1b --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_accordion_state.js @@ -0,0 +1,120 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the flexbox accordions state is persistent through hide/show in the layout +// view. + +const TEST_URI = URL_ROOT + "doc_flexbox_specific_cases.html"; +const ACCORDION_HEADER_SELECTOR = ".accordion-header"; +const ACCORDION_CONTENT_SELECTOR = ".accordion-content"; + +add_task(async function () { + await addTab(TEST_URI); + + await testAccordionState( + ":root", + FLEXBOX_OPENED_PREF, + "#layout-section-flex" + ); + await testAccordionState( + "#container-only", + FLEX_CONTAINER_OPENED_PREF, + "#layout-section-flex-container" + ); + await testAccordionState( + "#item-only", + FLEX_ITEM_OPENED_PREF, + "#layout-section-flex-item" + ); +}); + +async function testAccordionState(target, pref, selector) { + const context = await openLayoutViewAndSelectNode(target); + + await testAccordionStateAfterClickingHeader(pref, selector, context); + await testAccordionStateAfterSwitchingSidebars(pref, selector, context); + await testAccordionStateAfterReopeningLayoutView(pref, selector, context); + + Services.prefs.clearUserPref(pref); +} + +async function testAccordionStateAfterClickingHeader(pref, selector, { doc }) { + info("Checking initial state of the flexbox panel."); + + const item = await waitFor(() => doc.querySelector(selector)); + const header = item.querySelector(ACCORDION_HEADER_SELECTOR); + const content = item.querySelector(ACCORDION_CONTENT_SELECTOR); + + ok( + !content.hidden && content.childElementCount > 0, + "The flexbox panel content is visible." + ); + ok(Services.prefs.getBoolPref(pref), `${pref} is pref on by default.`); + + info("Clicking the flexbox header to hide the flexbox panel."); + header.click(); + + info("Checking the new state of the flexbox panel."); + ok(content.hidden, "The flexbox panel content is hidden."); + ok(!Services.prefs.getBoolPref(pref), `${pref} is pref off.`); +} + +async function testAccordionStateAfterSwitchingSidebars( + pref, + selector, + { doc, inspector } +) { + info( + "Checking the flexbox accordion state is persistent after switching sidebars." + ); + + const item = await waitFor(() => doc.querySelector(selector)); + + info("Selecting the computed view."); + inspector.sidebar.select("computedview"); + + info("Selecting the layout view."); + inspector.sidebar.select("layoutview"); + + info("Checking the state of the flexbox panel."); + const content = item.querySelector(ACCORDION_CONTENT_SELECTOR); + + ok(content.hidden, "The flexbox panel content is hidden."); + ok(!Services.prefs.getBoolPref(pref), `${pref} is pref off.`); +} + +async function testAccordionStateAfterReopeningLayoutView( + pref, + selector, + { target, toolbox } +) { + info( + "Checking the flexbox 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 { doc } = await openLayoutViewAndSelectNode(target); + const item = await waitFor(() => doc.querySelector(selector)); + const content = item.querySelector(ACCORDION_CONTENT_SELECTOR); + + info("Checking the state of the flexbox panel."); + ok(content.hidden, "The flexbox panel content is hidden."); + ok(!Services.prefs.getBoolPref(pref), `${pref} is pref off.`); +} + +async function openLayoutViewAndSelectNode(target) { + const { inspector, flexboxInspector, toolbox } = await openLayoutView(); + await selectNode(target, inspector); + + return { + doc: flexboxInspector.document, + inspector, + target, + toolbox, + }; +} diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_container_and_item.js b/devtools/client/inspector/flexbox/test/browser_flexbox_container_and_item.js new file mode 100644 index 0000000000..d77ec6d9e4 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_container_and_item.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the flex container accordion and flex item accordion are both rendered when +// the selected element is both a flex container and item. + +const TEST_URI = ` + <style type='text/css'> + .container { + display: flex; + } + </style> + <div id="container" class="container"> + <div id="item" class="container"> + <div></div> + </div> + </div> +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + const onAccordionsRendered = waitForDOM(doc, ".accordion-item", 4); + await selectNode("#item", inspector); + const [flexItemPane, flexContainerPane] = await onAccordionsRendered; + + ok(flexItemPane, "The flex item accordion pane is rendered."); + ok(flexContainerPane, "The flex container accordion pane is rendered."); + is( + flexItemPane.children[0].textContent, + "Flex Item of div#container.container", + "Got the correct header for the flex item pane." + ); + is( + flexContainerPane.children[0].textContent, + "Flex Container", + "Got the correct header for the flex container pane." + ); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_container_and_item_accordion_state.js b/devtools/client/inspector/flexbox/test/browser_flexbox_container_and_item_accordion_state.js new file mode 100644 index 0000000000..0339845618 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_container_and_item_accordion_state.js @@ -0,0 +1,107 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the order in which accordion items are shown for container-item elements. +// For those combined types, the container accordion is shown first if the selection came +// from the markup-view, because we assume in this case that users do want to see the +// element selected as a container first. +// However when users select an item in the list of items in the container accordion (or +// in the item selector dropdown), then the item accordion should be shown first. + +const TEST_URI = URL_ROOT + "doc_flexbox_specific_cases.html"; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + info("Select a flex container-only node"); + await selectNode("#container-only", inspector); + await waitUntil( + () => doc.querySelectorAll(".flex-header-container-properties").length + ); + + info("Check that there is only 1 accordion for displayed"); + let accordions = doc.querySelectorAll(".flex-accordion"); + is(accordions.length, 1, "There's only 1 accordion"); + is( + accordions[0].id, + "layout-section-flex-container", + "The accordion is the container type" + ); + + info("Select a flex container+item node by clicking in the markup-view"); + await clickOnNodeInMarkupView("#container-and-item", inspector); + await waitUntil(() => doc.querySelectorAll(".flex-accordion").length === 2); + + info( + "Check that the 2 accordions are displayed, with container type being first" + ); + accordions = doc.querySelectorAll(".flex-accordion"); + is(accordions.length, 2, "There are 2 accordions"); + is( + accordions[0].id, + "layout-section-flex-container", + "The first accordion is the container type" + ); + is( + accordions[1].id, + "layout-section-flex-item", + "The second accordion is the item type" + ); + + info("Select the container-only node again"); + await selectNode("#container-only", inspector); + await waitUntil(() => doc.querySelectorAll(".flex-accordion").length === 1); + + info("Wait until the accordion item list points to the correct item"); + await waitUntil(() => + doc + .querySelector(".flex-item-list button") + .textContent.includes("container-and-item") + ); + info( + "Click on the container+item node right there in the accordion item list" + ); + doc.querySelector(".flex-item-list button").click(); + await waitUntil(() => doc.querySelectorAll(".flex-accordion").length === 2); + + info( + "Check that the 2 accordions are displayed again, with item type being first" + ); + accordions = doc.querySelectorAll(".flex-accordion"); + is(accordions.length, 2, "There are 2 accordions again"); + is( + accordions[0].id, + "layout-section-flex-item", + "The first accordion is the item type" + ); + is( + accordions[1].id, + "layout-section-flex-container", + "The second accordion is the container type" + ); +}); + +async function clickOnNodeInMarkupView(selector, inspector) { + const { selection, markup } = inspector; + + await markup.expandAll(selection.nodeFront); + const nodeFront = await getNodeFront(selector, inspector); + const markupContainer = markup.getContainer(nodeFront); + + const onSelected = inspector.once("inspector-updated"); + EventUtils.synthesizeMouseAtCenter( + markupContainer.tagLine, + { type: "mousedown" }, + markup.doc.defaultView + ); + EventUtils.synthesizeMouseAtCenter( + markupContainer.tagLine, + { type: "mouseup" }, + markup.doc.defaultView + ); + await onSelected; +} diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_container_and_item_updates_on_change.js b/devtools/client/inspector/flexbox/test/browser_flexbox_container_and_item_updates_on_change.js new file mode 100644 index 0000000000..af62ac2ff0 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_container_and_item_updates_on_change.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the flex container accordion is rendered when a flex item is updated to +// also be a flex container. + +const TEST_URI = ` + <style> + .container { + display: flex; + } + </style> + <div id="container" class="container"> + <div id="item"> + <div></div> + </div> + </div> +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + const onFlexItemSizingRendered = waitForDOM(doc, "ul.flex-item-sizing"); + await selectNode("#item", inspector); + const [flexSizingContainer] = await onFlexItemSizingRendered; + + ok(flexSizingContainer, "The flex sizing info is rendered."); + + info("Changing the flexbox in the page."); + const onAccordionsChanged = waitForDOM(doc, ".accordion-item", 4); + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + () => (content.document.getElementById("item").className = "container") + ); + const [flexItemPane, flexContainerPane] = await onAccordionsChanged; + + ok(flexItemPane, "The flex item accordion pane is rendered."); + ok(flexContainerPane, "The flex container accordion pane is rendered."); + is( + flexItemPane.children[0].textContent, + "Flex Item of div#container.container", + "Got the correct header for the flex item pane." + ); + is( + flexContainerPane.children[0].textContent, + "Flex Container", + "Got the correct header for the flex container pane." + ); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_container_element_rep.js b/devtools/client/inspector/flexbox/test/browser_flexbox_container_element_rep.js new file mode 100644 index 0000000000..d2d83527b9 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_container_element_rep.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the flex container's element rep will display the box model highlighter on +// hover. + +const TEST_URI = URL_ROOT + "doc_flexbox_specific_cases.html"; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + const { waitForHighlighterTypeShown } = getHighlighterTestHelpers(inspector); + + const onFlexContainerRepRendered = waitForDOM( + doc, + ".flex-header-content .objectBox" + ); + await selectNode("#container", inspector); + const [flexContainerRep] = await onFlexContainerRepRendered; + + ok(flexContainerRep, "The flex container element rep is rendered."); + + info("Listen to node-highlight event and mouse over the rep"); + const onHighlight = waitForHighlighterTypeShown( + inspector.highlighters.TYPES.BOXMODEL + ); + EventUtils.synthesizeMouse( + flexContainerRep, + 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, + "container", + "The highlighted node has the correct id." + ); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_container_properties.js b/devtools/client/inspector/flexbox/test/browser_flexbox_container_properties.js new file mode 100644 index 0000000000..cf75d74ffe --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_container_properties.js @@ -0,0 +1,59 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the flex container properties are shown when a flex container is selected. + +const TEST_URI = ` + <style type='text/css'> + #container1 { + display: flex; + } + </style> + <div id="container1"> + <div id="item"></div> + </div> +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + info( + "Selecting the flex container #container1 and checking the values of the flex " + + "container properties for #container1." + ); + const onFlexContainerPropertiesRendered = waitForDOM( + doc, + ".flex-header-container-properties" + ); + await selectNode("#container1", inspector); + const [flexContainerProperties] = await onFlexContainerPropertiesRendered; + + ok(flexContainerProperties, "The flex container properties is rendered."); + is( + flexContainerProperties.children[0].textContent, + "row", + "Got expected flex-direction." + ); + is( + flexContainerProperties.children[1].textContent, + "nowrap", + "Got expected flex-wrap." + ); + + info( + "Selecting a flex item and expecting the flex container properties to not be " + + "shown." + ); + const onFlexHeaderRendered = waitForDOM(doc, ".flex-header"); + await selectNode("#item", inspector); + const [flexHeader] = await onFlexHeaderRendered; + + ok( + !flexHeader.querySelector(".flex-header-container-properties"), + "The flex container properties is not shown in the header." + ); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_empty_state.js b/devtools/client/inspector/flexbox/test/browser_flexbox_empty_state.js new file mode 100644 index 0000000000..82e523ebe0 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_empty_state.js @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that a message is displayed when no flex container is selected. + +const TEST_URI = ` + <div></div> +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + info("Checking the initial state of the Flexbox Inspector."); + ok( + doc.querySelector( + ".flex-accordion .devtools-sidepanel-no-result", + "A message is shown when no flex container is selected." + ) + ); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_grand_parent_flex.js b/devtools/client/inspector/flexbox/test/browser_flexbox_grand_parent_flex.js new file mode 100644 index 0000000000..8617def08b --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_grand_parent_flex.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that a flex container is not shown as a flex item of its grandparent flex +// container. + +const TEST_URI = ` +<style> +.flex { + display: flex; +} +</style> +<div class="flex"> + <div> + <div id="grandchild" class="flex"> + This is a flex item of a flex container. + Its parent isn't a flex container, but its grandparent is. + </div> + </div> +</div> +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + info("Select the flex container's grandchild."); + const onFlexContainerHeaderRendered = waitForDOM( + doc, + ".flex-header-container-label" + ); + await selectNode("#grandchild", inspector); + await onFlexContainerHeaderRendered; + + info("Check that only the Flex Container accordion item is showing."); + const flexPanes = doc.querySelectorAll(".flex-accordion"); + is( + flexPanes.length, + 1, + "There should only be one flex accordion item showing." + ); + + info("Check that the container header shows Flex Container."); + const flexAccordionHeader = flexPanes[0].querySelector( + ".accordion-header-label" + ); + is( + flexAccordionHeader.textContent, + "Flex Container", + "The flexbox pane shows a flex container accordion item." + ); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_highlighter_color_picker_on_ESC.js b/devtools/client/inspector/flexbox/test/browser_flexbox_highlighter_color_picker_on_ESC.js new file mode 100644 index 0000000000..59b24ba512 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_highlighter_color_picker_on_ESC.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const asyncStorage = require("resource://devtools/shared/async-storage.js"); + +// Test that the flexbox highlighter color change in the color picker is reverted when +// ESCAPE is pressed. + +const TEST_URI = URL_ROOT + "doc_flexbox_specific_cases.html"; + +add_task(async function () { + // Make sure there are no custom highlighter colors stored before starting. + await asyncStorage.removeItem("flexboxInspectorHostColors"); + + await addTab(TEST_URI); + const { inspector, flexboxInspector, layoutView } = await openLayoutView(); + const { document: doc } = flexboxInspector; + const { store } = inspector; + const cPicker = layoutView.swatchColorPickerTooltip; + const spectrum = cPicker.spectrum; + + const onColorSwatchRendered = waitForDOM( + doc, + ".layout-flexbox-wrapper .layout-color-swatch" + ); + await selectNode("#container", inspector); + const [swatch] = await onColorSwatchRendered; + + info("Checking the initial state of the Flexbox Inspector color picker."); + is( + swatch.style.backgroundColor, + "rgb(148, 0, 255)", + "The color swatch's background is correct." + ); + is( + store.getState().flexbox.color, + "#9400FF", + "The flexbox color state is correct." + ); + + info("Opening the color picker by clicking on the 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 onColorUpdate = waitUntilState( + store, + state => state.flexbox.color === "#9400FF" + ); + const onColorPickerHidden = cPicker.tooltip.once("hidden"); + focusAndSendKey(spectrum.element.ownerDocument.defaultView, "ESCAPE"); + await onColorPickerHidden; + await onColorUpdate; + + is( + swatch.style.backgroundColor, + "rgb(148, 0, 255)", + "The color swatch's background was reverted after ESCAPE." + ); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_highlighter_color_picker_on_RETURN.js b/devtools/client/inspector/flexbox/test/browser_flexbox_highlighter_color_picker_on_RETURN.js new file mode 100644 index 0000000000..46c6a7022c --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_highlighter_color_picker_on_RETURN.js @@ -0,0 +1,92 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const asyncStorage = require("resource://devtools/shared/async-storage.js"); + +// Test that the flexbox highlighter color change in the color picker is committed when +// RETURN is pressed. + +const TEST_URI = URL_ROOT + "doc_flexbox_specific_cases.html"; + +add_task(async function () { + // Make sure there are no custom highlighter colors stored before starting. + await asyncStorage.removeItem("flexboxInspectorHostColors"); + + await addTab(TEST_URI); + const { inspector, flexboxInspector, layoutView } = await openLayoutView(); + const { document: doc } = flexboxInspector; + const { store } = inspector; + const HIGHLIGHTER_TYPE = inspector.highlighters.TYPES.FLEXBOX; + const { waitForHighlighterTypeShown, waitForHighlighterTypeHidden } = + getHighlighterTestHelpers(inspector); + const cPicker = layoutView.swatchColorPickerTooltip; + const spectrum = cPicker.spectrum; + + const onColorSwatchRendered = waitForDOM( + doc, + ".layout-flexbox-wrapper .layout-color-swatch" + ); + await selectNode("#container", inspector); + const [swatch] = await onColorSwatchRendered; + + const checkbox = doc.getElementById("flexbox-checkbox-toggle"); + + info("Checking the initial state of the Flexbox Inspector color picker."); + ok(!checkbox.checked, "Flexbox highlighter toggle is unchecked."); + is( + swatch.style.backgroundColor, + "rgb(148, 0, 255)", + "The color swatch's background is correct." + ); + is( + store.getState().flexbox.color, + "#9400FF", + "The flexbox color state is correct." + ); + + info("Toggling ON the flexbox highlighter."); + const onHighlighterShown = waitForHighlighterTypeShown(HIGHLIGHTER_TYPE); + const onCheckboxChange = waitUntilState( + store, + state => state.flexbox.highlighted + ); + checkbox.click(); + await onHighlighterShown; + await onCheckboxChange; + + info("Opening the color picker by clicking on the 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 onColorUpdate = waitUntilState( + store, + state => state.flexbox.color === "#00FF0080" + ); + const onColorPickerHidden = cPicker.tooltip.once("hidden"); + focusAndSendKey(spectrum.element.ownerDocument.defaultView, "RETURN"); + await onColorPickerHidden; + await onColorUpdate; + + is( + swatch.style.backgroundColor, + "rgba(0, 255, 0, 0.5)", + "The color swatch's background was kept after RETURN." + ); + + info("Toggling OFF the flexbox highlighter."); + const onHighlighterHidden = waitForHighlighterTypeHidden(HIGHLIGHTER_TYPE); + checkbox.click(); + await onHighlighterHidden; +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_highlighter_opened_telemetry.js b/devtools/client/inspector/flexbox/test/browser_flexbox_highlighter_opened_telemetry.js new file mode 100644 index 0000000000..62c63e8b9f --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_highlighter_opened_telemetry.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the telemetry is correct when the flexbox highlighter is activated from +// the layout view. + +const TEST_URI = URL_ROOT + "doc_flexbox_specific_cases.html"; + +add_task(async function () { + await addTab(TEST_URI); + startTelemetry(); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + const onFlexHighlighterToggleRendered = waitForDOM( + doc, + "#flexbox-checkbox-toggle" + ); + await selectNode("#container", inspector); + const [flexHighlighterToggle] = await onFlexHighlighterToggleRendered; + + await toggleHighlighterON(flexHighlighterToggle, inspector); + await toggleHighlighterOFF(flexHighlighterToggle, inspector); + + checkResults(); +}); + +function checkResults() { + checkTelemetry("devtools.layout.flexboxhighlighter.opened", "", 1, "scalar"); + checkTelemetry( + "DEVTOOLS_FLEXBOX_HIGHLIGHTER_TIME_ACTIVE_SECONDS", + "", + null, + "hasentries" + ); +} diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_item_list_01.js b/devtools/client/inspector/flexbox/test/browser_flexbox_item_list_01.js new file mode 100644 index 0000000000..d7a8ae184a --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_item_list_01.js @@ -0,0 +1,49 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { + getStr, +} = require("resource://devtools/client/inspector/layout/utils/l10n.js"); + +// Test the flex item list is empty when there are no flex items for the selected flex +// container. + +const TEST_URI = ` + <div id="container" style="display:flex"> + </div> +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + const HIGHLIGHTER_TYPE = inspector.highlighters.TYPES.FLEXBOX; + const { getActiveHighlighter } = getHighlighterTestHelpers(inspector); + + const onFlexHeaderRendered = waitForDOM(doc, ".flex-header"); + await selectNode("#container", inspector); + const [flexHeader] = await onFlexHeaderRendered; + const flexHighlighterToggle = flexHeader.querySelector( + "#flexbox-checkbox-toggle" + ); + const flexItemListHeader = doc.querySelector(".flex-item-list-header"); + + info("Checking the state of the Flexbox Inspector."); + ok(flexHeader, "The flex container header is rendered."); + ok(flexHighlighterToggle, "The flexbox highlighter toggle is rendered."); + is( + flexItemListHeader.textContent, + getStr("flexbox.noFlexItems"), + "The flex item list header shows 'No flex items' when there are no items." + ); + ok( + !flexHighlighterToggle.checked, + "The flexbox highlighter toggle is unchecked." + ); + ok( + !getActiveHighlighter(HIGHLIGHTER_TYPE), + "No flexbox highlighter is shown." + ); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_item_list_02.js b/devtools/client/inspector/flexbox/test/browser_flexbox_item_list_02.js new file mode 100644 index 0000000000..5433727c6a --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_item_list_02.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the flex item list can be used to navigated to the selected flex item. + +const TEST_URI = URL_ROOT + "doc_flexbox_specific_cases.html"; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + const onFlexItemListRendered = waitForDOM(doc, ".flex-item-list"); + await selectNode("#container", inspector); + const [flexItemList] = await onFlexItemListRendered; + + info("Checking the initial state of the flex item list."); + ok(flexItemList, "The flex item list is rendered."); + is( + flexItemList.querySelectorAll("button").length, + 1, + "Got the correct number of flex items in the list." + ); + + info("Clicking on the first flex item to navigate to the flex item."); + const onFlexItemOutlineRendered = waitForDOM(doc, ".flex-outline-container"); + flexItemList.querySelector("button").click(); + const [flexOutlineContainer] = await onFlexItemOutlineRendered; + + info("Checking the selected flex item state."); + ok(flexOutlineContainer, "The flex outline is rendered."); + ok(!doc.querySelector(".flex-item-list"), "The flex item list is not shown."); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_item_list_updates_on_change.js b/devtools/client/inspector/flexbox/test/browser_flexbox_item_list_updates_on_change.js new file mode 100644 index 0000000000..95cbb2da2d --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_item_list_updates_on_change.js @@ -0,0 +1,47 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the flex item list updates on changes to the number of flex items in the +// flex container. + +const TEST_URI = ` + <style> + #container { + display: flex; + } + </style> + <div id="container"> + <div></div> + </div> +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + const onFlexItemListRendered = waitForDOM(doc, ".flex-item-list"); + await selectNode("#container", inspector); + const [flexItemList] = await onFlexItemListRendered; + + info("Checking the initial state of the flex item list."); + ok(flexItemList, "The flex item list is rendered."); + is( + flexItemList.querySelectorAll("button").length, + 1, + "Got the correct number of flex items in the list." + ); + + info("Changing the flexbox in the page."); + const onFlexItemListChanged = waitForDOM(doc, ".flex-item-list > button", 2); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + const div = content.document.createElement("div"); + content.document.getElementById("container").appendChild(div); + }); + const elements = await onFlexItemListChanged; + + info("Checking the flex item list is correct."); + is(elements.length, 2, "Flex item list was changed."); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_item_outline_exists.js b/devtools/client/inspector/flexbox/test/browser_flexbox_item_outline_exists.js new file mode 100644 index 0000000000..eba6ec03f0 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_item_outline_exists.js @@ -0,0 +1,30 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the flex item outline exists when a flex item is selected. + +const TEST_URI = URL_ROOT + "doc_flexbox_specific_cases.html"; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + // Select a flex item in the test document and wait for the outline to be rendered. + const onFlexItemOutlineRendered = waitForDOM(doc, ".flex-outline-container"); + await selectNode(".item", inspector); + const [flexOutlineContainer] = await onFlexItemOutlineRendered; + + ok(flexOutlineContainer, "The flex outline exists in the DOM"); + + const [basis, final] = [ + ...flexOutlineContainer.querySelectorAll( + ".flex-outline-basis, .flex-outline-final" + ), + ]; + + ok(basis, "The basis outline exists"); + ok(final, "The final outline exists"); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_item_outline_has_correct_layout.js b/devtools/client/inspector/flexbox/test/browser_flexbox_item_outline_has_correct_layout.js new file mode 100644 index 0000000000..e81d6f5677 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_item_outline_has_correct_layout.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the flex item outline has a correct layout. This outline is built using css +// grid under the hood to position everything. So we want to check that the template for +// this grid has been correctly generated depending on the item that is currently +// selected. + +const TEST_URI = URL_ROOT + "doc_flexbox_specific_cases.html"; + +const TEST_DATA = [ + { + selector: ".shrinking .item", + expectedGridTemplate: + "[basis-start final-start] 300fr [final-end delta-start] " + + "200fr [basis-end delta-end]", + }, + { + selector: ".shrinking.is-clamped .item", + expectedGridTemplate: + "[basis-start final-start] 300fr [delta-start] " + + "50fr [final-end min] 150fr [basis-end delta-end]", + }, + { + selector: ".growing .item", + expectedGridTemplate: + "[basis-start final-start] 200fr [basis-end delta-start] " + + "100fr [final-end delta-end]", + }, + { + selector: ".growing.is-clamped .item", + expectedGridTemplate: + "[basis-start final-start] 200fr [basis-end delta-start] " + + "50fr [final-end max] 50fr [delta-end]", + }, + { + selector: "#wanted-to-shrink-more-than-basis div:first-child", + expectedGridTemplate: + "[delta-start] 63fr [basis-start final-start] " + + "60fr [final-end min] 140fr [basis-end delta-end]", + }, +]; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + for (const { selector, expectedGridTemplate } of TEST_DATA) { + info( + `Checking the grid template for the flex item outline for ${selector}` + ); + + await selectNode(selector, inspector); + await waitUntil(() => { + const flexOutline = doc.querySelector(".flex-outline"); + return ( + flexOutline && + flexOutline.style.gridTemplateColumns === expectedGridTemplate + ); + }); + + ok(true, "Grid template is correct"); + } +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_item_outline_hidden_when_useless.js b/devtools/client/inspector/flexbox/test/browser_flexbox_item_outline_hidden_when_useless.js new file mode 100644 index 0000000000..e0c1cab942 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_item_outline_hidden_when_useless.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the flex item outline is not rendered when it isn't useful. +// For now, that means when the item's base, delta and final sizes are all 0. + +const TEST_URI = ` + <div style="display:flex"> + <div></div> + </div> +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + info( + "Select the item in the document and wait for the sizing section to appear" + ); + const onFlexItemSizingRendered = waitForDOM(doc, "ul.flex-item-sizing"); + await selectNode("div > div", inspector); + const [flexSizingContainer] = await onFlexItemSizingRendered; + + const outlineEls = doc.querySelectorAll(".flex-outline-container"); + is(outlineEls.length, 0, "The outline has not been rendered for this item"); + + info("Also check that the sizing section shows the correct information"); + const allSections = [ + ...flexSizingContainer.querySelectorAll(".section .name"), + ]; + + is(allSections.length, 2, "There are 2 parts in the sizing section"); + is( + allSections[0].textContent, + "Content Size", + "The first part is the content size" + ); + is( + allSections[1].textContent, + "Final Size", + "The second part is the final size" + ); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_item_outline_renders_basisfinal_points_correctly.js b/devtools/client/inspector/flexbox/test/browser_flexbox_item_outline_renders_basisfinal_points_correctly.js new file mode 100644 index 0000000000..d2a2ce94fd --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_item_outline_renders_basisfinal_points_correctly.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the flex item outline renders the basis and final points as a single point +// if their sizes are equal. If not, then render as separate points. + +const TEST_URI = URL_ROOT + "doc_flexbox_specific_cases.html"; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc, store } = flexboxInspector; + + info("Select a flex item whose basis size matches its final size."); + let onUpdate = waitForDispatch(store, "UPDATE_FLEXBOX"); + await selectNode(".item", inspector); + await onUpdate; + + const [basisFinalPoint] = [ + ...doc.querySelectorAll(".flex-outline-point.basisfinal"), + ]; + + ok(basisFinalPoint, "The basis/final point exists"); + + info("Select a flex item whose basis size is different than its final size."); + onUpdate = waitForDispatch(store, "UPDATE_FLEXBOX"); + await selectNode(".shrinking .item", inspector); + await onUpdate; + + const [basis, final] = [ + ...doc.querySelectorAll( + ".flex-outline-point.basis, .flex-outline-point.final" + ), + ]; + + ok(basis, "The basis point exists"); + ok(final, "The final point exists"); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_item_outline_rotates_for_column.js b/devtools/client/inspector/flexbox/test/browser_flexbox_item_outline_rotates_for_column.js new file mode 100644 index 0000000000..f26ca751cd --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_item_outline_rotates_for_column.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the flex item outline is rotated for flex items in a column flexbox layout. + +const TEST_URI = URL_ROOT + "doc_flexbox_specific_cases.html"; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + // Select a flex item in the row flexbox layout. + let onFlexItemOutlineRendered = waitForDOM( + doc, + ".flex-outline-container .flex-outline" + ); + await selectNode(".container .item", inspector); + let [flexOutline] = await onFlexItemOutlineRendered; + + ok( + flexOutline.classList.contains("horizontal-lr"), + "The flex outline has the horizontal-lr class" + ); + + // Check that the outline is wider than it is tall in the configuration. + let bounds = flexOutline.getBoxQuads()[0].getBounds(); + Assert.greater(bounds.width, bounds.height, "The outline looks like a row"); + + // Select a flex item in the column flexbox layout. + onFlexItemOutlineRendered = waitForDOM( + doc, + ".flex-outline-container .flex-outline" + ); + await selectNode(".container.column .item", inspector); + await waitUntil(() => { + flexOutline = doc.querySelector( + ".flex-outline-container .flex-outline.vertical-tb" + ); + return flexOutline; + }); + ok(true, "The flex outline has the vertical-tb class"); + + // Check that the outline is taller than it is wide in the configuration. + bounds = flexOutline.getBoxQuads()[0].getBounds(); + Assert.greater( + bounds.height, + bounds.width, + "The outline looks like a column" + ); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_item_outline_rotates_for_different_writing_modes.js b/devtools/client/inspector/flexbox/test/browser_flexbox_item_outline_rotates_for_different_writing_modes.js new file mode 100644 index 0000000000..860f2c2337 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_item_outline_rotates_for_different_writing_modes.js @@ -0,0 +1,42 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the flex item outline is rotated to match its main axis direction. + +const TEST_URI = URL_ROOT + "doc_flexbox_writing_modes.html"; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + info("Check that the row flex item rotated to vertical-bt."); + let onFlexItemOutlineRendered = waitForDOM( + doc, + ".flex-outline-container .flex-outline" + ); + await selectNode(".row.vertical-bt.item", inspector); + let [flexOutline] = await onFlexItemOutlineRendered; + + ok( + flexOutline.classList.contains("vertical-bt"), + "Row outline has been rotated to vertical-bt." + ); + + info("Check that the column flex item rotated to horizontal-rl."); + onFlexItemOutlineRendered = waitForDOM( + doc, + ".flex-outline-container .flex-outline" + ); + await selectNode(".column.horizontal-rl.item", inspector); + await waitUntil(() => { + flexOutline = doc.querySelector( + ".flex-outline-container .flex-outline.horizontal-rl" + ); + return flexOutline; + }); + + ok(true, "Column outline has been rotated to horizontal-rl."); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_non_flex_item_is_not_shown.js b/devtools/client/inspector/flexbox/test/browser_flexbox_non_flex_item_is_not_shown.js new file mode 100644 index 0000000000..92422ea490 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_non_flex_item_is_not_shown.js @@ -0,0 +1,30 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that an element that is the child of a flex container but is not actually a flex +// item is not shown in the sidebar when selected. + +const TEST_URI = ` +<div style="display:flex"> + <div id="item" style="position:absolute;">test</div> +</div> +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + info("Select the container's supposed flex item."); + await selectNode("#item", inspector); + const noFlexContainerOrItemSelected = doc.querySelector( + ".flex-accordion .devtools-sidepanel-no-result" + ); + + ok( + noFlexContainerOrItemSelected, + "The flexbox pane shows a message to select a flex container or item to continue." + ); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_pseudo_elements_are_listed.js b/devtools/client/inspector/flexbox/test/browser_flexbox_pseudo_elements_are_listed.js new file mode 100644 index 0000000000..c69e4c92b6 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_pseudo_elements_are_listed.js @@ -0,0 +1,28 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that pseudo-elements that are flex items do appear in the list of items. + +const TEST_URI = URL_ROOT + "doc_flexbox_pseudos.html"; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + // Select the flex container in the inspector. + const onItemsListRendered = waitForDOM( + doc, + ".layout-flexbox-wrapper .flex-item-list" + ); + await selectNode(".container", inspector); + const [flexItemList] = await onItemsListRendered; + + const items = [...flexItemList.querySelectorAll("button .objectBox")]; + is(items.length, 2, "There are 2 items displayed in the list"); + + is(items[0].textContent, "::before", "The first item is ::before"); + is(items[1].textContent, "::after", "The second item is ::after"); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_flexibility_not_displayed_when_useless.js b/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_flexibility_not_displayed_when_useless.js new file mode 100644 index 0000000000..e25ebd5a6a --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_flexibility_not_displayed_when_useless.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the flexibility section in the flex item sizing properties is not displayed +// when the item did not grow or shrink. + +const TEST_URI = URL_ROOT + "doc_flexbox_specific_cases.html"; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc, store } = flexboxInspector; + + info( + "Select an item with flex:0 and wait for the sizing info to be rendered" + ); + let onUpdate = waitForDispatch(store, "UPDATE_FLEXBOX"); + await selectNode("#did-not-grow-or-shrink div", inspector); + await onUpdate; + + let flexSections = doc.querySelectorAll( + ".flex-item-sizing .section.flexibility" + ); + is( + flexSections.length, + 0, + "The flexibility section was not found in the DOM" + ); + + info( + "Select a more complex item which also doesn't flex and wait for the sizing info" + ); + onUpdate = waitForDispatch(store, "UPDATE_FLEXBOX"); + await selectNode( + "#just-enough-space-for-clamped-items div:last-child", + inspector + ); + await onUpdate; + + flexSections = doc.querySelectorAll(".flex-item-sizing .section.flexibility"); + is( + flexSections.length, + 0, + "The flexibility section was not found in the DOM" + ); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_do_not_show_unspecified_min_dimension.js b/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_do_not_show_unspecified_min_dimension.js new file mode 100644 index 0000000000..45cae70d7b --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_do_not_show_unspecified_min_dimension.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that a flex item's min width/height value is not displayed if it's unspecified in +// the CSS. + +const TEST_URI = URL_ROOT + "doc_flexbox_unauthored_min_dimension.html"; + +async function checkFlexItemCSSProperty(inspector, store, doc, selector) { + info("Select the container's flex item sizing info."); + const onUpdate = waitForDispatch(store, "UPDATE_FLEXBOX"); + await selectNode(selector, inspector); + await onUpdate; + + info( + "Check that the minimum size section does not display minimum dimension text." + ); + const [sectionMinRowItem] = [ + ...doc.querySelectorAll(".flex-item-sizing .section.min"), + ]; + const minDimension = sectionMinRowItem.querySelector(".css-property-link"); + + ok(!minDimension, "Minimum dimension property should not be displayed."); +} + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc, store } = flexboxInspector; + + await checkFlexItemCSSProperty( + inspector, + store, + doc, + "#flex-item-with-unauthored-min-width" + ); + await checkFlexItemCSSProperty( + inspector, + store, + doc, + "#flex-item-with-unauthored-min-height" + ); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_exists.js b/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_exists.js new file mode 100644 index 0000000000..e353eea0db --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_exists.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the flex item sizing information exists when a flex item is selected. + +const TEST_URI = URL_ROOT + "doc_flexbox_specific_cases.html"; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + // Select a flex item in the test document and wait for the sizing info to be rendered. + // Note that we select an item that has base, delta and final sizes, so we can check + // those sections exists. + const onFlexItemSizingRendered = waitForDOM(doc, "ul.flex-item-sizing"); + await selectNode(".container.growing .item", inspector); + const [flexSizingContainer] = await onFlexItemSizingRendered; + + ok(flexSizingContainer, "The flex sizing exists in the DOM"); + + info("Check that the base, flexibility and final sizes are displayed"); + const allSections = [ + ...flexSizingContainer.querySelectorAll(".section .name"), + ]; + const allSectionTitles = allSections.map(el => el.textContent); + + ["Base Size", "Flexibility", "Final Size"].forEach((expectedTitle, i) => { + ok( + allSectionTitles[i].includes(expectedTitle), + `Sizing section #${i + 1} (${expectedTitle}) was found` + ); + }); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_for_different_writing_modes.js b/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_for_different_writing_modes.js new file mode 100644 index 0000000000..91a99d3da5 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_for_different_writing_modes.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the flex item sizing info shows the correct dimension values for different +// writing modes. For vertical writing modes, row items should display height values and +// column items should display width values. The opposite is true for horizontal mode +// where rows display width values and columns display height. + +const TEST_URI = URL_ROOT + "doc_flexbox_writing_modes.html"; + +async function checkFlexItemDimension( + inspector, + store, + doc, + selector, + expected +) { + info("Select the container's flex item."); + const onUpdate = waitForDispatch(store, "UPDATE_FLEXBOX"); + await selectNode(selector, inspector); + await onUpdate; + + info("Check that the minimum size section shows the correct dimension."); + const sectionMinRowItem = doc.querySelector(".flex-item-sizing .section.min"); + const minDimension = sectionMinRowItem.querySelector(".css-property-link"); + + ok( + minDimension.textContent.includes(expected), + "The flex item sizing has the correct dimension value." + ); +} + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc, store } = flexboxInspector; + + await checkFlexItemDimension( + inspector, + store, + doc, + ".row.vertical-rl.item", + "min-height" + ); + await checkFlexItemDimension( + inspector, + store, + doc, + ".column.vertical-tb.item", + "min-height" + ); + await checkFlexItemDimension( + inspector, + store, + doc, + ".row.vertical-bt.item", + "min-height" + ); + await checkFlexItemDimension( + inspector, + store, + doc, + ".column.horizontal-rl.item", + "min-width" + ); + await checkFlexItemDimension( + inspector, + store, + doc, + ".row.horizontal-lr.item", + "min-width" + ); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_for_pseudos.js b/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_for_pseudos.js new file mode 100644 index 0000000000..75b3a00a4f --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_for_pseudos.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the flex item sizing UI also appears for pseudo elements. + +const TEST_URI = URL_ROOT + "doc_flexbox_pseudos.html"; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + info("Select the ::before pseudo-element in the inspector"); + const containerNode = await getNodeFront(".container", inspector); + const { nodes } = await inspector.walker.children(containerNode); + const beforeNode = nodes[0]; + + const onFlexItemSizingRendered = waitForDOM(doc, "ul.flex-item-sizing"); + const onFlexItemOutlineRendered = waitForDOM(doc, ".flex-outline-container"); + await selectNode(beforeNode, inspector); + const [flexSizingContainer] = await onFlexItemSizingRendered; + const [flexOutlineContainer] = await onFlexItemOutlineRendered; + + ok(flexSizingContainer, "The flex sizing exists in the DOM"); + ok(flexOutlineContainer, "The flex outline exists in the DOM"); + + info("Check that the various sizing sections are displayed"); + const allSections = [...flexSizingContainer.querySelectorAll(".section")]; + ok(allSections.length, "Sizing sections are displayed"); + + info("Check that the various parts of the outline are displayed"); + const [basis, final] = [ + ...flexOutlineContainer.querySelectorAll( + ".flex-outline-basis, .flex-outline-final" + ), + ]; + ok(basis && final, "The final and basis parts of the outline exist"); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_for_text_nodes.js b/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_for_text_nodes.js new file mode 100644 index 0000000000..fc96505208 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_for_text_nodes.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the flex item sizing UI also appears for text nodes. + +const TEST_URI = URL_ROOT + "doc_flexbox_text_nodes.html"; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + info("Select the first text node in the flex container"); + const containerNode = await getNodeFront(".container", inspector); + const { nodes } = await inspector.walker.children(containerNode); + const firstTextNode = nodes[0]; + + const onFlexItemSizingRendered = waitForDOM(doc, "ul.flex-item-sizing"); + const onFlexItemOutlineRendered = waitForDOM(doc, ".flex-outline-container"); + await selectNode(firstTextNode, inspector); + const [flexSizingContainer] = await onFlexItemSizingRendered; + const [flexOutlineContainer] = await onFlexItemOutlineRendered; + + ok(flexSizingContainer, "The flex sizing exists in the DOM"); + ok(flexOutlineContainer, "The flex outline exists in the DOM"); + + info("Check that the various sizing sections are displayed"); + const allSections = [...flexSizingContainer.querySelectorAll(".section")]; + ok(allSections.length, "Sizing sections are displayed"); + + info("Check that the various parts of the outline are displayed"); + const [basis, final] = [ + ...flexOutlineContainer.querySelectorAll( + ".flex-outline-basis, .flex-outline-final" + ), + ]; + ok(basis && final, "The final and basis parts of the outline exist"); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_has_correct_sections.js b/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_has_correct_sections.js new file mode 100644 index 0000000000..e31366da15 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_has_correct_sections.js @@ -0,0 +1,86 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the flex item sizing UI contains the right sections, depending on which +// element is selected. Some items may be clamped, others not, so not all sections are +// visible at all times. + +const TEST_URI = URL_ROOT + "doc_flexbox_specific_cases.html"; + +const TEST_DATA = [ + { + selector: ".shrinking .item", + expectedSections: ["Base Size", "Flexibility", "Final Size"], + }, + { + selector: ".shrinking.is-clamped .item", + expectedSections: [ + "Base Size", + "Flexibility", + "Minimum Size", + "Final Size", + ], + }, + { + selector: ".growing .item", + expectedSections: ["Base Size", "Flexibility", "Final Size"], + }, + { + selector: ".growing.is-clamped .item", + expectedSections: [ + "Base Size", + "Flexibility", + "Maximum Size", + "Final Size", + ], + }, +]; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc, store } = flexboxInspector; + + for (const { selector, expectedSections } of TEST_DATA) { + info(`Checking the list of sections for the flex item ${selector}`); + const sections = await selectNodeAndGetFlexSizingSections( + selector, + store, + inspector, + doc + ); + + is( + sections.length, + expectedSections.length, + "Correct number of sections found" + ); + expectedSections.forEach((expectedSection, i) => { + ok( + sections[i].includes(expectedSection), + `The ${expectedSection} section was found` + ); + }); + } +}); + +async function selectNodeAndGetFlexSizingSections( + selector, + store, + inspector, + doc +) { + const onUpdate = waitForDispatch(store, "UPDATE_FLEXBOX"); + await selectNode(selector, inspector); + await onUpdate; + + info(`Getting the list of displayed sections for ${selector}`); + const allSections = [ + ...doc.querySelectorAll("ul.flex-item-sizing .section .name"), + ]; + const allSectionTitles = allSections.map(el => el.textContent); + + return allSectionTitles; +} diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_matches_properties_with_!important.js b/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_matches_properties_with_!important.js new file mode 100644 index 0000000000..ba14ef7a62 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_matches_properties_with_!important.js @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that a flex item's sizing section shows the highest value of specificity for its +// CSS property. + +const TEST_URI = URL_ROOT + "doc_flexbox_CSS_property_with_!important.html"; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + info("Select the container's flex item sizing info."); + const onFlexItemSizingRendered = waitForDOM(doc, "ul.flex-item-sizing"); + await selectNode(".item", inspector); + const [flexItemSizingContainer] = await onFlexItemSizingRendered; + + info( + "Check that the max and flexibility sections show correct CSS property value." + ); + const flexSection = flexItemSizingContainer.querySelector( + ".section.flexibility" + ); + const maxSection = flexItemSizingContainer.querySelector(".section.max"); + const flexGrow = flexSection.querySelector(".css-property-link"); + const maxSize = maxSection.querySelector(".css-property-link"); + + is( + maxSize.textContent, + "(max-width: 400px !important)", + "Maximum size section shows CSS property value with highest specificity." + ); + is( + flexGrow.textContent, + "(flex-grow: 5 !important)", + "Flexibility size section shows CSS property value with highest specificity." + ); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_updates_on_change.js b/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_updates_on_change.js new file mode 100644 index 0000000000..9218022b02 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_updates_on_change.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the flexbox sizing info updates on changes to the flex item properties. + +const TEST_URI = ` + <style> + #container { + display: flex; + } + </style> + <div id="container"> + <div id="item"></div> + </div> +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + const onFlexItemSizingRendered = waitForDOM(doc, "ul.flex-item-sizing"); + await selectNode("#item", inspector); + const [flexItemSizingContainer] = await onFlexItemSizingRendered; + + info("Checking the initial state of the flex item list."); + is( + flexItemSizingContainer.querySelectorAll("li").length, + 2, + "Got the correct number of flex item sizing properties in the list." + ); + + info("Changing the flexbox in the page."); + const onFlexItemSizingChanged = waitForDOM( + doc, + "ul.flex-item-sizing > li", + 3 + ); + SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + () => (content.document.getElementById("item").style.minWidth = "100px") + ); + const elements = await onFlexItemSizingChanged; + + info("Checking the flex item sizing info is correct."); + is(elements.length, 3, "Flex item sizing info was changed."); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_wanted_to_grow_but_was_clamped.js b/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_wanted_to_grow_but_was_clamped.js new file mode 100644 index 0000000000..72e2861ef3 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_wanted_to_grow_but_was_clamped.js @@ -0,0 +1,44 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { + getStr, +} = require("resource://devtools/client/inspector/layout/utils/l10n.js"); + +// Test the specific max-clamping scenario where an item wants to grow a certain amount +// but its max-size prevents it from growing that much. + +const TEST_URI = URL_ROOT + "doc_flexbox_specific_cases.html"; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + info( + "Select the test item in the document and wait for the sizing info to render" + ); + const onRendered = waitForDOM(doc, ".flex-outline, .flex-item-sizing", 2); + await selectNode("#want-to-grow-more-than-max div", inspector); + const [outlineContainer, sizingContainer] = await onRendered; + + info( + "Check that the outline contains the max point and that it's equal to final" + ); + const maxPoint = outlineContainer.querySelector(".flex-outline-point.max"); + ok(maxPoint, "The max point is displayed"); + ok( + outlineContainer.style.gridTemplateColumns.includes("[final-end max]"), + "The final and max points are at the same position" + ); + + info("Check that the maximum sizing section displays the right info"); + const reasons = [...sizingContainer.querySelectorAll(".reasons li")]; + const expectedReason = getStr("flexbox.itemSizing.clampedToMax"); + ok( + reasons.some(r => r.textContent === expectedReason), + "The clampedToMax reason was found" + ); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_text_nodes_are_listed.js b/devtools/client/inspector/flexbox/test/browser_flexbox_text_nodes_are_listed.js new file mode 100644 index 0000000000..374ec77962 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_text_nodes_are_listed.js @@ -0,0 +1,28 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that text nodes that are flex items do appear in the list of items. + +const TEST_URI = URL_ROOT + "doc_flexbox_text_nodes.html"; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + // Select the flex container in the inspector. + const onItemsListRendered = waitForDOM( + doc, + ".layout-flexbox-wrapper .flex-item-list" + ); + await selectNode(".container", inspector); + const [flexItemList] = await onItemsListRendered; + + const items = [...flexItemList.querySelectorAll("button .objectBox")]; + is(items.length, 3, "There are 3 items displayed in the list"); + + is(items[0].textContent, "#text", "The first item is a text node"); + is(items[2].textContent, "#text", "The third item is a text node"); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_text_nodes_are_not_inlined.js b/devtools/client/inspector/flexbox/test/browser_flexbox_text_nodes_are_not_inlined.js new file mode 100644 index 0000000000..2494ac1dcb --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_text_nodes_are_not_inlined.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that single child text nodes that are also flex items can be selected in the +// flexbox inspector. +// This means that they are not inlined like normal single child text nodes, since +// selecting them in the flexbox inspector also means selecting them in the markup view. + +const TEST_URI = URL_ROOT + "doc_flexbox_text_nodes.html"; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + + // Select the flex container in the inspector. + const onItemsListRendered = waitForDOM( + doc, + ".layout-flexbox-wrapper .flex-item-list" + ); + await selectNode(".container.single-child", inspector); + const [flexItemList] = await onItemsListRendered; + + const items = [...flexItemList.querySelectorAll("button .objectBox")]; + is(items.length, 1, "There is 1 item displayed in the list"); + is(items[0].textContent, "#text", "The item in the list is a text node"); + + info("Click on the item to select it"); + const onFlexItemOutlineRendered = waitForDOM(doc, ".flex-outline-container"); + items[0].closest("button").click(); + const [flexOutlineContainer] = await onFlexItemOutlineRendered; + ok( + flexOutlineContainer, + "The flex outline is displayed for a single child short text node too" + ); + + ok( + inspector.selection.isTextNode(), + "The current inspector selection is the text node" + ); + + const markupContainer = inspector.markup.getContainer( + inspector.selection.nodeFront + ); + is( + markupContainer.elt.textContent, + "short text", + "This is the right text node" + ); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_toggle_flexbox_highlighter_01.js b/devtools/client/inspector/flexbox/test/browser_flexbox_toggle_flexbox_highlighter_01.js new file mode 100644 index 0000000000..2fbfccb162 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_toggle_flexbox_highlighter_01.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test toggling ON/OFF the flexbox highlighter from the flexbox inspector panel. + +const TEST_URI = URL_ROOT + "doc_flexbox_specific_cases.html"; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + const HIGHLIGHTER_TYPE = inspector.highlighters.TYPES.FLEXBOX; + const { getActiveHighlighter } = getHighlighterTestHelpers(inspector); + + const onFlexHighlighterToggleRendered = waitForDOM( + doc, + "#flexbox-checkbox-toggle" + ); + await selectNode("#container", inspector); + const [flexHighlighterToggle] = await onFlexHighlighterToggleRendered; + + info("Checking the initial state of the Flexbox Inspector."); + ok(flexHighlighterToggle, "The flexbox highlighter toggle is rendered."); + ok( + !flexHighlighterToggle.checked, + "The flexbox highlighter toggle is unchecked." + ); + ok( + !getActiveHighlighter(HIGHLIGHTER_TYPE), + "No flexbox highlighter is shown." + ); + + await toggleHighlighterON(flexHighlighterToggle, inspector); + + info("Checking the flexbox highlighter is created."); + ok(getActiveHighlighter(HIGHLIGHTER_TYPE), "Flexbox highlighter is shown."); + ok( + flexHighlighterToggle.checked, + "The flexbox highlighter toggle is checked." + ); + + await toggleHighlighterOFF(flexHighlighterToggle, inspector); + + info("Checking the flexbox highlighter is not shown."); + ok( + !getActiveHighlighter(HIGHLIGHTER_TYPE), + "No flexbox highlighter is shown." + ); + ok( + !flexHighlighterToggle.checked, + "The flexbox highlighter toggle is unchecked." + ); +}); diff --git a/devtools/client/inspector/flexbox/test/browser_flexbox_toggle_flexbox_highlighter_02.js b/devtools/client/inspector/flexbox/test/browser_flexbox_toggle_flexbox_highlighter_02.js new file mode 100644 index 0000000000..c7d7c35d9a --- /dev/null +++ b/devtools/client/inspector/flexbox/test/browser_flexbox_toggle_flexbox_highlighter_02.js @@ -0,0 +1,102 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test toggling ON/OFF the flexbox highlighter on different flex containers from the +// flexbox inspector panel. + +const TEST_URI = URL_ROOT + "doc_flexbox_specific_cases.html"; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, flexboxInspector } = await openLayoutView(); + const { document: doc } = flexboxInspector; + const { store } = inspector; + const HIGHLIGHTER_TYPE = inspector.highlighters.TYPES.FLEXBOX; + const { getActiveHighlighter, getNodeForActiveHighlighter } = + getHighlighterTestHelpers(inspector); + + const onFlexHighlighterToggleRendered = waitForDOM( + doc, + "#flexbox-checkbox-toggle" + ); + await selectNode("#container", inspector); + const [flexHighlighterToggle] = await onFlexHighlighterToggleRendered; + + info("Checking the #container state of the Flexbox Inspector."); + ok(flexHighlighterToggle, "The flexbox highlighter toggle is rendered."); + ok( + !flexHighlighterToggle.checked, + "The flexbox highlighter toggle is unchecked." + ); + ok( + !getActiveHighlighter(HIGHLIGHTER_TYPE), + "No flexbox highlighter is shown." + ); + + info( + "Toggling ON the flexbox highlighter for #container from the layout panel." + ); + await toggleHighlighterON(flexHighlighterToggle, inspector); + + info("Checking the flexbox highlighter is created for #container."); + const highlightedNodeFront = store.getState().flexbox.flexContainer.nodeFront; + is( + getNodeForActiveHighlighter(HIGHLIGHTER_TYPE), + highlightedNodeFront, + "Flexbox highlighter is shown for #container." + ); + ok( + flexHighlighterToggle.checked, + "The flexbox highlighter toggle is checked." + ); + + info("Switching the selected flex container to .container.column"); + const onToggleChange = waitUntilState( + store, + state => !state.flexbox.highlighted + ); + await selectNode(".container.column", inspector); + await onToggleChange; + + info("Checking the .container.column state of the Flexbox Inspector."); + ok( + !flexHighlighterToggle.checked, + "The flexbox highlighter toggle is unchecked." + ); + is( + getNodeForActiveHighlighter(HIGHLIGHTER_TYPE), + highlightedNodeFront, + "Flexbox highlighter is still shown for #container." + ); + + info( + "Toggling ON the flexbox highlighter for .container.column from the layout " + + "panel." + ); + await toggleHighlighterON(flexHighlighterToggle, inspector); + + info("Checking the flexbox highlighter is created for .container.column"); + is( + getNodeForActiveHighlighter(HIGHLIGHTER_TYPE), + store.getState().flexbox.flexContainer.nodeFront, + "Flexbox highlighter is shown for .container.column." + ); + ok( + flexHighlighterToggle.checked, + "The flexbox highlighter toggle is checked." + ); + + await toggleHighlighterOFF(flexHighlighterToggle, inspector); + + info("Checking the flexbox highlighter is not shown."); + ok( + !getActiveHighlighter(HIGHLIGHTER_TYPE), + "No flexbox highlighter is shown." + ); + ok( + !flexHighlighterToggle.checked, + "The flexbox highlighter toggle is unchecked." + ); +}); diff --git a/devtools/client/inspector/flexbox/test/doc_flexbox_CSS_property_with_!important.html b/devtools/client/inspector/flexbox/test/doc_flexbox_CSS_property_with_!important.html new file mode 100644 index 0000000000..097d91215b --- /dev/null +++ b/devtools/client/inspector/flexbox/test/doc_flexbox_CSS_property_with_!important.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<style> +.container { + display: flex; + width: 500px; + height: 150px; + margin: 10px; +} + +.item { + background: yellow; + color: red; + flex-grow: 1 !important; + max-width: 350px; + max-width: 400px !important; + padding: 10px; +} +</style> +<div class="container"> + <div class="item" style="flex-grow: 5 !important">Item</div> +</div> diff --git a/devtools/client/inspector/flexbox/test/doc_flexbox_pseudos.html b/devtools/client/inspector/flexbox/test/doc_flexbox_pseudos.html new file mode 100644 index 0000000000..76474cbc37 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/doc_flexbox_pseudos.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<style> +.container { + width: 300px; + height: 150px; + margin: 10px; + display: flex; +} + +.container::before, +.container::after { + color: #f06; + border: 1px solid #f06; + background: gold; + padding: 1em; +} + +.container::before { + content: "::before pseudo-element item"; +} + +.container::after { + content: "::after pseudo-element item"; +} +</style> +<div class="container"></div> diff --git a/devtools/client/inspector/flexbox/test/doc_flexbox_specific_cases.html b/devtools/client/inspector/flexbox/test/doc_flexbox_specific_cases.html new file mode 100644 index 0000000000..f5a6aaad24 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/doc_flexbox_specific_cases.html @@ -0,0 +1,121 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<style> +@font-face { + font-family: Ahem; + src: url("Ahem.ttf"); +} +.container { + width: 300px; + height: 150px; + margin: 10px; + display: flex; +} +.container.column { + height: 300px; + width: 150px; + flex-direction: column; +} +.item { + background: #0004; +} +.shrinking .item { + flex-basis: 500px; + flex-shrink: 1; +} +.shrinking.is-clamped .item { + min-width: 350px; +} +.growing .item { + flex-basis: 200px; + flex-grow: 1; +} +.growing.is-clamped .item { + max-width: 250px; +} + +#want-to-grow-more-than-max { + width: 500px; + display: flex; +} +#want-to-grow-more-than-max div { + flex: 1; + max-width: 200px; +} + +#did-not-grow-or-shrink { + width: 500px; + display: flex; +} +#did-not-grow-or-shrink div { + flex: 0 300px; +} + +#just-enough-space-for-clamped-items { + display:flex; + width:100px; + height:40px +} +#just-enough-space-for-clamped-items div:first-child { + flex: 1 300px; + max-width: 20px; + background: teal; +} +#just-enough-space-for-clamped-items div:last-child { + flex: 1 10px; + min-width: 80px; + background: salmon; +} + +#wanted-to-shrink-more-than-basis { + display: flex; + width: 5px; +} +#wanted-to-shrink-more-than-basis div:first-child { + flex: 0 2 200px; + /* Using the Ahem test font to make sure the text has the exact same size on all test + platforms */ + font-family: Ahem; + font-size: 10px; +} +#wanted-to-shrink-more-than-basis div:last-child { + flex: 0 1 200px; +} +</style> +<div id="container" class="container"> + <div class="item">flex item in a row flex container</div> +</div> +<div class="container column"> + <div class="item">flex item in a column flex container</div> +</div> +<div class="container shrinking"> + <div class="item">Shrinking flex item</div> +</div> +<div class="container shrinking is-clamped"> + <div class="item">Shrinking and clamped flex item</div> +</div> +<div class="container growing"> + <div class="item">Growing flex item</div> +</div> +<div class="container growing is-clamped"> + <div class="item">Growing and clamped flex item</div> +</div> +<div id="want-to-grow-more-than-max"> + <div>item wants to grow more</div> +</div> +<div id="did-not-grow-or-shrink"> + <div>item did not grow or shrink</div> +</div> +<div id="just-enough-space-for-clamped-items"> + <div></div> + <div></div> +</div> +<div id="wanted-to-shrink-more-than-basis"> + <div>item wants to shrink more than its basis</div> + <div></div> +</div> +<div class="container" id="container-only"> + <div class="container" id="container-and-item"> + <div id="item-only">This item is inside a container-item element</div> + </div> +</div> diff --git a/devtools/client/inspector/flexbox/test/doc_flexbox_text_nodes.html b/devtools/client/inspector/flexbox/test/doc_flexbox_text_nodes.html new file mode 100644 index 0000000000..626d3aa47e --- /dev/null +++ b/devtools/client/inspector/flexbox/test/doc_flexbox_text_nodes.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<style> +.container { + width: 400px; + display: flex; +} +.container div { + flex-basis: 100px; + flex-shrink: 0; + background: #f06; + align-self: stretch; +} +</style> +<div class="container"> + A text node will be wrapped into an anonymous block container + <div></div> + Here is yet another text node +</div> +<div class="container single-child">short text</div> diff --git a/devtools/client/inspector/flexbox/test/doc_flexbox_unauthored_min_dimension.html b/devtools/client/inspector/flexbox/test/doc_flexbox_unauthored_min_dimension.html new file mode 100644 index 0000000000..2f05dbb7f8 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/doc_flexbox_unauthored_min_dimension.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<style> +.flex-container { + display: flex; + height: 100vh; +} + +.column { + flex-direction: column; +} + +.flex-child { + height: 100%; + width: 100% +} +</style> +<div class="flex-container"> + <div class="flex-child"></div> + <div id="flex-item-with-unauthored-min-width"> + <h1>AAA</h1> + </div> +</div> +<div class="flex-container column"> + <div class="flex-child"></div> + <div id="flex-item-with-unauthored-min-height"> + <h1>BBB</h1> + </div> +</div> diff --git a/devtools/client/inspector/flexbox/test/doc_flexbox_writing_modes.html b/devtools/client/inspector/flexbox/test/doc_flexbox_writing_modes.html new file mode 100644 index 0000000000..b518c39631 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/doc_flexbox_writing_modes.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<style> +.flex-container { + display: flex; +} + +.flex-container.vertical-rl { + writing-mode: vertical-rl; +} + +.flex-container.sideways-lr { + writing-mode: sideways-lr; +} + +.column { + flex-direction: column; +} + +.item { + min-width: 300px; + min-height: 300px; +} + +</style> +<div class="flex-container vertical-rl"> + <span class="row vertical-rl item">Vertical-tb Row content</span> +</div> +<div class="flex-container column"> + <span class="column vertical-tb item">Vertical-tb Column Content</span> +</div> +<div class="flex-container sideways-lr"> + <span class="row vertical-bt item">Vertical-bt Row Content</span> +</div> +<div class="flex-container vertical-rl column"> + <span class="column horizontal-rl item">Horizontal-rl Column Content</span> +</div> +<div class="flex-container"> + <span class="row horizontal-lr item">Horizontal-lr Row Content</span> +</div> diff --git a/devtools/client/inspector/flexbox/test/head.js b/devtools/client/inspector/flexbox/test/head.js new file mode 100644 index 0000000000..269dcea904 --- /dev/null +++ b/devtools/client/inspector/flexbox/test/head.js @@ -0,0 +1,81 @@ +/* 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 FLEXBOX_OPENED_PREF = "devtools.layout.flexbox.opened"; +const FLEX_CONTAINER_OPENED_PREF = "devtools.layout.flex-container.opened"; +const FLEX_ITEM_OPENED_PREF = "devtools.layout.flex-item.opened"; +const GRID_OPENED_PREF = "devtools.layout.grid.opened"; +const BOXMODEL_OPENED_PREF = "devtools.layout.boxmodel.opened"; + +// Make sure only the flexbox layout accordions are opened, and the others are closed. +Services.prefs.setBoolPref(FLEXBOX_OPENED_PREF, true); +Services.prefs.setBoolPref(FLEX_CONTAINER_OPENED_PREF, true); +Services.prefs.setBoolPref(FLEX_ITEM_OPENED_PREF, true); +Services.prefs.setBoolPref(BOXMODEL_OPENED_PREF, false); +Services.prefs.setBoolPref(GRID_OPENED_PREF, false); + +// Clear all set prefs. +registerCleanupFunction(() => { + Services.prefs.clearUserPref(FLEXBOX_OPENED_PREF); + Services.prefs.clearUserPref(FLEX_CONTAINER_OPENED_PREF); + Services.prefs.clearUserPref(FLEX_ITEM_OPENED_PREF); + Services.prefs.clearUserPref(BOXMODEL_OPENED_PREF); + Services.prefs.clearUserPref(GRID_OPENED_PREF); +}); + +/** + * Toggles ON the flexbox highlighter given the flexbox highlighter button from the + * layout panel. + * + * @param {DOMNode} button + * The flexbox highlighter toggle button in the flex container panel. + * @param {Inspector} inspector + * Inspector panel instance. + */ +async function toggleHighlighterON(button, inspector) { + info("Toggling ON the flexbox highlighter from the layout panel."); + const { waitForHighlighterTypeShown } = getHighlighterTestHelpers(inspector); + const onHighlighterShown = waitForHighlighterTypeShown( + inspector.highlighters.TYPES.FLEXBOX + ); + const { store } = inspector; + const onToggleChange = waitUntilState( + store, + state => state.flexbox.highlighted + ); + button.click(); + await Promise.all([onHighlighterShown, onToggleChange]); +} + +/** + * Toggles OFF the flexbox highlighter given the flexbox highlighter button from the + * layout panel. + * + * @param {DOMNode} button + * The flexbox highlighter toggle button in the flex container panel. + * @param {Inspector} inspector + * Inspector panel instance. + */ +async function toggleHighlighterOFF(button, inspector) { + info("Toggling OFF the flexbox highlighter from the layout panel."); + const { waitForHighlighterTypeHidden } = getHighlighterTestHelpers(inspector); + const onHighlighterHidden = waitForHighlighterTypeHidden( + inspector.highlighters.TYPES.FLEXBOX + ); + const { store } = inspector; + const onToggleChange = waitUntilState( + store, + state => !state.flexbox.highlighted + ); + button.click(); + await Promise.all([onHighlighterHidden, onToggleChange]); +} diff --git a/devtools/client/inspector/flexbox/types.js b/devtools/client/inspector/flexbox/types.js new file mode 100644 index 0000000000..34f45ed8ae --- /dev/null +++ b/devtools/client/inspector/flexbox/types.js @@ -0,0 +1,145 @@ +/* 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 flex item computed style properties. + */ +const flexItemProperties = (exports.flexItemProperties = { + // The computed value of flex-basis. + "flex-basis": PropTypes.string, + + // The computed value of flex-grow. + "flex-grow": PropTypes.string, + + // The computed value of min-height. + "min-height": PropTypes.string, + + // The computed value of min-width. + "min-width": PropTypes.string, + + // The computed value of max-height. + "max-height": PropTypes.string, + + // The computed value of max-width. + "max-width": PropTypes.string, + + // The computed height of the flex item element. + height: PropTypes.string, + + // The computed width of the flex item element. + width: PropTypes.string, +}); + +/** + * A flex item sizing data. + */ +const flexItemSizing = (exports.flexItemSizing = { + // The max size of the flex item in the cross axis. + crossMaxSize: PropTypes.number, + + // The min size of the flex item in the cross axis. + crossMinSize: PropTypes.number, + + // The size of the flex item in the main axis before the flex sizing algorithm is + // applied. This is also called the "flex base size" in the spec. + mainBaseSize: PropTypes.number, + + // The value that the flex sizing algorithm "wants" to use to stretch or shrink the + // item, before clamping to the item's main min and max sizes. + mainDeltaSize: PropTypes.number, + + // The max size of the flex item in the main axis. + mainMaxSize: PropTypes.number, + + // The min size of the flex item in the maxin axis. + mainMinSize: PropTypes.number, +}); + +/** + * A flex item data. + */ +const flexItem = (exports.flexItem = { + // The actor ID of the flex item. + actorID: PropTypes.string, + + // The computed style properties of the flex item. + computedStyle: PropTypes.object, + + // The flex item sizing data. + flexItemSizing: PropTypes.shape(flexItemSizing), + + // The NodeFront of the flex item. + nodeFront: PropTypes.object, + + // The authored style properties of the flex item. + properties: PropTypes.shape(flexItemProperties), +}); + +/** + * A flex container computed style properties. + */ +const flexContainerProperties = (exports.flexContainerProperties = { + // The computed value of align-content. + "align-content": PropTypes.string, + + // The computed value of align-items. + "align-items": PropTypes.string, + + // The computed value of flex-direction. + "flex-direction": PropTypes.string, + + // The computed value of flex-wrap. + "flex-wrap": PropTypes.string, + + // The computed value of justify-content. + "justify-content": PropTypes.string, +}); + +/** + * A flex container data. + */ +const flexContainer = (exports.flexContainer = { + // The actor ID of the flex container. + actorID: PropTypes.string, + + // An array of flex items belonging to the flex container. + flexItems: PropTypes.arrayOf(PropTypes.shape(flexItem)), + + // Whether or not the flex container data represents the selected flex container + // or the parent flex container. This is true if the flex container data represents + // the parent flex container. + isFlexItemContainer: PropTypes.bool, + + // The NodeFront actor ID of the flex item to display in the flex item sizing + // properties. + flexItemShown: PropTypes.string, + + // The NodeFront of the flex container. + nodeFront: PropTypes.object, + + // The computed style properties of the flex container. + properties: PropTypes.shape(flexContainerProperties), +}); + +/** + * The Flexbox UI state. + */ +exports.flexbox = { + // The color of the flexbox highlighter overlay. + color: PropTypes.string, + + // The selected flex container. + flexContainer: PropTypes.shape(flexContainer), + + // The selected flex container can also be a flex item. This object contains the + // parent flex container properties. + flexItemContainer: PropTypes.shape(flexContainer), + + // Whether or not the flexbox highlighter is highlighting the flex container. + highlighted: PropTypes.bool, +}; |