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