From 5f8de423f190bbb79a62f804151bc24824fa32d8 Mon Sep 17 00:00:00 2001 From: "Matt A. Tobin" Date: Fri, 2 Feb 2018 04:16:08 -0500 Subject: Add m-esr52 at 52.6.0 --- .../client/inspector/shared/dom-node-preview.js | 352 +++++++++++++ .../inspector/shared/highlighters-overlay.js | 315 ++++++++++++ devtools/client/inspector/shared/moz.build | 16 + devtools/client/inspector/shared/node-types.js | 17 + .../inspector/shared/style-inspector-menu.js | 510 +++++++++++++++++++ devtools/client/inspector/shared/test/.eslintrc.js | 6 + devtools/client/inspector/shared/test/browser.ini | 41 ++ ...er_styleinspector_context-menu-copy-color_01.js | 118 +++++ ...er_styleinspector_context-menu-copy-color_02.js | 99 ++++ ...rowser_styleinspector_context-menu-copy-urls.js | 109 ++++ ..._styleinspector_csslogic-content-stylesheets.js | 82 +++ .../test/browser_styleinspector_output-parser.js | 341 +++++++++++++ .../browser_styleinspector_refresh_when_active.js | 43 ++ ...wser_styleinspector_tooltip-background-image.js | 125 +++++ ...yleinspector_tooltip-closes-on-new-selection.js | 73 +++ ...r_styleinspector_tooltip-longhand-fontfamily.js | 120 +++++ ...inspector_tooltip-multiple-background-images.js | 63 +++ ..._styleinspector_tooltip-shorthand-fontfamily.js | 58 +++ .../test/browser_styleinspector_tooltip-size.js | 86 ++++ ...wser_styleinspector_transform-highlighter-01.js | 48 ++ ...wser_styleinspector_transform-highlighter-02.js | 57 +++ ...wser_styleinspector_transform-highlighter-03.js | 103 ++++ ...wser_styleinspector_transform-highlighter-04.js | 60 +++ .../inspector/shared/test/doc_author-sheet.html | 37 ++ .../shared/test/doc_content_stylesheet.html | 32 ++ .../shared/test/doc_content_stylesheet.xul | 9 + .../test/doc_content_stylesheet_imported.css | 5 + .../test/doc_content_stylesheet_imported2.css | 3 + .../shared/test/doc_content_stylesheet_linked.css | 3 + .../shared/test/doc_content_stylesheet_script.css | 5 + .../shared/test/doc_content_stylesheet_xul.css | 3 + .../inspector/shared/test/doc_frame_script.js | 115 +++++ devtools/client/inspector/shared/test/head.js | 557 +++++++++++++++++++++ .../client/inspector/shared/tooltips-overlay.js | 319 ++++++++++++ devtools/client/inspector/shared/utils.js | 161 ++++++ 35 files changed, 4091 insertions(+) create mode 100644 devtools/client/inspector/shared/dom-node-preview.js create mode 100644 devtools/client/inspector/shared/highlighters-overlay.js create mode 100644 devtools/client/inspector/shared/moz.build create mode 100644 devtools/client/inspector/shared/node-types.js create mode 100644 devtools/client/inspector/shared/style-inspector-menu.js create mode 100644 devtools/client/inspector/shared/test/.eslintrc.js create mode 100644 devtools/client/inspector/shared/test/browser.ini create mode 100644 devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_01.js create mode 100644 devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_02.js create mode 100644 devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-urls.js create mode 100644 devtools/client/inspector/shared/test/browser_styleinspector_csslogic-content-stylesheets.js create mode 100644 devtools/client/inspector/shared/test/browser_styleinspector_output-parser.js create mode 100644 devtools/client/inspector/shared/test/browser_styleinspector_refresh_when_active.js create mode 100644 devtools/client/inspector/shared/test/browser_styleinspector_tooltip-background-image.js create mode 100644 devtools/client/inspector/shared/test/browser_styleinspector_tooltip-closes-on-new-selection.js create mode 100644 devtools/client/inspector/shared/test/browser_styleinspector_tooltip-longhand-fontfamily.js create mode 100644 devtools/client/inspector/shared/test/browser_styleinspector_tooltip-multiple-background-images.js create mode 100644 devtools/client/inspector/shared/test/browser_styleinspector_tooltip-shorthand-fontfamily.js create mode 100644 devtools/client/inspector/shared/test/browser_styleinspector_tooltip-size.js create mode 100644 devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-01.js create mode 100644 devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-02.js create mode 100644 devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-03.js create mode 100644 devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-04.js create mode 100644 devtools/client/inspector/shared/test/doc_author-sheet.html create mode 100644 devtools/client/inspector/shared/test/doc_content_stylesheet.html create mode 100644 devtools/client/inspector/shared/test/doc_content_stylesheet.xul create mode 100644 devtools/client/inspector/shared/test/doc_content_stylesheet_imported.css create mode 100644 devtools/client/inspector/shared/test/doc_content_stylesheet_imported2.css create mode 100644 devtools/client/inspector/shared/test/doc_content_stylesheet_linked.css create mode 100644 devtools/client/inspector/shared/test/doc_content_stylesheet_script.css create mode 100644 devtools/client/inspector/shared/test/doc_content_stylesheet_xul.css create mode 100644 devtools/client/inspector/shared/test/doc_frame_script.js create mode 100644 devtools/client/inspector/shared/test/head.js create mode 100644 devtools/client/inspector/shared/tooltips-overlay.js create mode 100644 devtools/client/inspector/shared/utils.js (limited to 'devtools/client/inspector/shared') diff --git a/devtools/client/inspector/shared/dom-node-preview.js b/devtools/client/inspector/shared/dom-node-preview.js new file mode 100644 index 000000000..d96384785 --- /dev/null +++ b/devtools/client/inspector/shared/dom-node-preview.js @@ -0,0 +1,352 @@ +/* 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 EventEmitter = require("devtools/shared/event-emitter"); +const {createNode} = require("devtools/client/animationinspector/utils"); +const { LocalizationHelper } = require("devtools/shared/l10n"); + +const STRINGS_URI = "devtools/client/locales/inspector.properties"; +const L10N = new LocalizationHelper(STRINGS_URI); + +/** + * UI component responsible for displaying a preview of a dom node. + * @param {InspectorPanel} inspector Requires a reference to the inspector-panel + * to highlight and select the node, as well as refresh it when there are + * mutations. + * @param {Object} options Supported properties are: + * - compact {Boolean} Defaults to false. + * By default, nodes are previewed like + * If true, nodes will be previewed like tag#id.class instead. + */ +function DomNodePreview(inspector, options = {}) { + this.inspector = inspector; + this.options = options; + + this.onPreviewMouseOver = this.onPreviewMouseOver.bind(this); + this.onPreviewMouseOut = this.onPreviewMouseOut.bind(this); + this.onSelectElClick = this.onSelectElClick.bind(this); + this.onMarkupMutations = this.onMarkupMutations.bind(this); + this.onHighlightElClick = this.onHighlightElClick.bind(this); + this.onHighlighterLocked = this.onHighlighterLocked.bind(this); + + EventEmitter.decorate(this); +} + +exports.DomNodePreview = DomNodePreview; + +DomNodePreview.prototype = { + init: function (containerEl) { + let document = containerEl.ownerDocument; + + // Init the markup for displaying the target node. + this.el = createNode({ + parent: containerEl, + attributes: { + "class": "animation-target" + } + }); + + // Icon to select the node in the inspector. + this.highlightNodeEl = createNode({ + parent: this.el, + nodeType: "span", + attributes: { + "class": "node-highlighter", + "title": L10N.getStr("inspector.nodePreview.highlightNodeLabel") + } + }); + + // Wrapper used for mouseover/out event handling. + this.previewEl = createNode({ + parent: this.el, + nodeType: "span", + attributes: { + "title": L10N.getStr("inspector.nodePreview.selectNodeLabel") + } + }); + + if (!this.options.compact) { + this.previewEl.appendChild(document.createTextNode("<")); + } + + // Only used for ::before and ::after pseudo-elements. + this.pseudoEl = createNode({ + parent: this.previewEl, + nodeType: "span", + attributes: { + "class": "pseudo-element theme-fg-color5" + } + }); + + // Tag name. + this.tagNameEl = createNode({ + parent: this.previewEl, + nodeType: "span", + attributes: { + "class": "tag-name theme-fg-color3" + } + }); + + // Id attribute container. + this.idEl = createNode({ + parent: this.previewEl, + nodeType: "span" + }); + + if (!this.options.compact) { + createNode({ + parent: this.idEl, + nodeType: "span", + attributes: { + "class": "attribute-name theme-fg-color2" + }, + textContent: "id" + }); + this.idEl.appendChild(document.createTextNode("=\"")); + } else { + createNode({ + parent: this.idEl, + nodeType: "span", + attributes: { + "class": "theme-fg-color6" + }, + textContent: "#" + }); + } + + createNode({ + parent: this.idEl, + nodeType: "span", + attributes: { + "class": "attribute-value theme-fg-color6" + } + }); + + if (!this.options.compact) { + this.idEl.appendChild(document.createTextNode("\"")); + } + + // Class attribute container. + this.classEl = createNode({ + parent: this.previewEl, + nodeType: "span" + }); + + if (!this.options.compact) { + createNode({ + parent: this.classEl, + nodeType: "span", + attributes: { + "class": "attribute-name theme-fg-color2" + }, + textContent: "class" + }); + this.classEl.appendChild(document.createTextNode("=\"")); + } else { + createNode({ + parent: this.classEl, + nodeType: "span", + attributes: { + "class": "theme-fg-color6" + }, + textContent: "." + }); + } + + createNode({ + parent: this.classEl, + nodeType: "span", + attributes: { + "class": "attribute-value theme-fg-color6" + } + }); + + if (!this.options.compact) { + this.classEl.appendChild(document.createTextNode("\"")); + this.previewEl.appendChild(document.createTextNode(">")); + } + + this.startListeners(); + }, + + startListeners: function () { + // Init events for highlighting and selecting the node. + this.previewEl.addEventListener("mouseover", this.onPreviewMouseOver); + this.previewEl.addEventListener("mouseout", this.onPreviewMouseOut); + this.previewEl.addEventListener("click", this.onSelectElClick); + this.highlightNodeEl.addEventListener("click", this.onHighlightElClick); + + // Start to listen for markupmutation events. + this.inspector.on("markupmutation", this.onMarkupMutations); + + // Listen to the target node highlighter. + HighlighterLock.on("highlighted", this.onHighlighterLocked); + }, + + stopListeners: function () { + HighlighterLock.off("highlighted", this.onHighlighterLocked); + this.inspector.off("markupmutation", this.onMarkupMutations); + this.previewEl.removeEventListener("mouseover", this.onPreviewMouseOver); + this.previewEl.removeEventListener("mouseout", this.onPreviewMouseOut); + this.previewEl.removeEventListener("click", this.onSelectElClick); + this.highlightNodeEl.removeEventListener("click", this.onHighlightElClick); + }, + + destroy: function () { + HighlighterLock.unhighlight().catch(e => console.error(e)); + + this.stopListeners(); + + this.el.remove(); + this.el = this.tagNameEl = this.idEl = this.classEl = this.pseudoEl = null; + this.highlightNodeEl = this.previewEl = null; + this.nodeFront = this.inspector = null; + }, + + get highlighterUtils() { + if (this.inspector && this.inspector.toolbox) { + return this.inspector.toolbox.highlighterUtils; + } + return null; + }, + + onPreviewMouseOver: function () { + if (!this.nodeFront || !this.highlighterUtils) { + return; + } + this.highlighterUtils.highlightNodeFront(this.nodeFront) + .catch(e => console.error(e)); + }, + + onPreviewMouseOut: function () { + if (!this.nodeFront || !this.highlighterUtils) { + return; + } + this.highlighterUtils.unhighlight() + .catch(e => console.error(e)); + }, + + onSelectElClick: function () { + if (!this.nodeFront) { + return; + } + this.inspector.selection.setNodeFront(this.nodeFront, "dom-node-preview"); + }, + + onHighlightElClick: function (e) { + e.stopPropagation(); + + let classList = this.highlightNodeEl.classList; + let isHighlighted = classList.contains("selected"); + + if (isHighlighted) { + classList.remove("selected"); + HighlighterLock.unhighlight().then(() => { + this.emit("target-highlighter-unlocked"); + }, error => console.error(error)); + } else { + classList.add("selected"); + HighlighterLock.highlight(this).then(() => { + this.emit("target-highlighter-locked"); + }, error => console.error(error)); + } + }, + + onHighlighterLocked: function (e, domNodePreview) { + if (domNodePreview !== this) { + this.highlightNodeEl.classList.remove("selected"); + } + }, + + onMarkupMutations: function (e, mutations) { + if (!this.nodeFront) { + return; + } + + for (let {target} of mutations) { + if (target === this.nodeFront) { + // Re-render with the same nodeFront to update the output. + this.render(this.nodeFront); + break; + } + } + }, + + render: function (nodeFront) { + this.nodeFront = nodeFront; + let {displayName, attributes} = nodeFront; + + if (nodeFront.isPseudoElement) { + this.pseudoEl.textContent = nodeFront.isBeforePseudoElement + ? "::before" + : "::after"; + this.pseudoEl.style.display = "inline"; + this.tagNameEl.style.display = "none"; + } else { + this.tagNameEl.textContent = displayName; + this.pseudoEl.style.display = "none"; + this.tagNameEl.style.display = "inline"; + } + + let idIndex = attributes.findIndex(({name}) => name === "id"); + if (idIndex > -1 && attributes[idIndex].value) { + this.idEl.querySelector(".attribute-value").textContent = + attributes[idIndex].value; + this.idEl.style.display = "inline"; + } else { + this.idEl.style.display = "none"; + } + + let classIndex = attributes.findIndex(({name}) => name === "class"); + if (classIndex > -1 && attributes[classIndex].value) { + let value = attributes[classIndex].value; + if (this.options.compact) { + value = value.split(" ").join("."); + } + + this.classEl.querySelector(".attribute-value").textContent = value; + this.classEl.style.display = "inline"; + } else { + this.classEl.style.display = "none"; + } + } +}; + +/** + * HighlighterLock is a helper used to lock the highlighter on DOM nodes in the + * page. + * It instantiates a new highlighter that is then shared amongst all instances + * of DomNodePreview. This is useful because that means showing the highlighter + * on one node will unhighlight the previously highlighted one, but will not + * interfere with the default inspector highlighter. + */ +var HighlighterLock = { + highlighter: null, + isShown: false, + + highlight: Task.async(function* (animationTargetNode) { + if (!this.highlighter) { + let util = animationTargetNode.inspector.toolbox.highlighterUtils; + this.highlighter = yield util.getHighlighterByType("BoxModelHighlighter"); + } + + yield this.highlighter.show(animationTargetNode.nodeFront); + this.isShown = true; + this.emit("highlighted", animationTargetNode); + }), + + unhighlight: Task.async(function* () { + if (!this.highlighter || !this.isShown) { + return; + } + + yield this.highlighter.hide(); + this.isShown = false; + this.emit("unhighlighted"); + }) +}; + +EventEmitter.decorate(HighlighterLock); diff --git a/devtools/client/inspector/shared/highlighters-overlay.js b/devtools/client/inspector/shared/highlighters-overlay.js new file mode 100644 index 000000000..c054c72af --- /dev/null +++ b/devtools/client/inspector/shared/highlighters-overlay.js @@ -0,0 +1,315 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set 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"; + +/** + * The highlighter overlays are in-content highlighters that appear when hovering over + * property values. + */ + +const promise = require("promise"); +const EventEmitter = require("devtools/shared/event-emitter"); +const { VIEW_NODE_VALUE_TYPE } = require("devtools/client/inspector/shared/node-types"); + +/** + * Manages all highlighters in the style-inspector. + * + * @param {CssRuleView|CssComputedView} view + * Either the rule-view or computed-view panel + */ +function HighlightersOverlay(view) { + this.view = view; + + let {CssRuleView} = require("devtools/client/inspector/rules/rules"); + this.isRuleView = view instanceof CssRuleView; + + this.highlighters = {}; + + // NodeFront of the grid container that is highlighted. + this.gridHighlighterShown = null; + // Name of the highlighter shown on mouse hover. + this.hoveredHighlighterShown = null; + // Name of the selector highlighter shown. + this.selectorHighlighterShown = null; + + this.highlighterUtils = this.view.inspector.toolbox.highlighterUtils; + + // Only initialize the overlay if at least one of the highlighter types is + // supported. + this.supportsHighlighters = + this.highlighterUtils.supportsCustomHighlighters(); + + this._onClick = this._onClick.bind(this); + this._onMouseMove = this._onMouseMove.bind(this); + this._onMouseOut = this._onMouseOut.bind(this); + this._onWillNavigate = this._onWillNavigate.bind(this); + + EventEmitter.decorate(this); +} + +HighlightersOverlay.prototype = { + /** + * Add the highlighters overlay to the view. This will start tracking mouse + * movements and display highlighters when needed. + */ + addToView: function () { + if (!this.supportsHighlighters || this._isStarted || this._isDestroyed) { + return; + } + + let el = this.view.element; + el.addEventListener("click", this._onClick, true); + el.addEventListener("mousemove", this._onMouseMove, false); + el.addEventListener("mouseout", this._onMouseOut, false); + el.ownerDocument.defaultView.addEventListener("mouseout", this._onMouseOut, false); + + if (this.isRuleView) { + this.view.inspector.target.on("will-navigate", this._onWillNavigate); + } + + this._isStarted = true; + }, + + /** + * Remove the overlay from the current view. This will stop tracking mouse + * movement and showing highlighters. + */ + removeFromView: function () { + if (!this.supportsHighlighters || !this._isStarted || this._isDestroyed) { + return; + } + + let el = this.view.element; + el.removeEventListener("click", this._onClick, true); + el.removeEventListener("mousemove", this._onMouseMove, false); + el.removeEventListener("mouseout", this._onMouseOut, false); + + if (this.isRuleView) { + this.view.inspector.target.off("will-navigate", this._onWillNavigate); + } + + this._isStarted = false; + }, + + _onClick: function (event) { + // Bail out if the target is not a grid property value. + if (!this._isDisplayGridValue(event.target)) { + return; + } + + event.stopPropagation(); + + this._getHighlighter("CssGridHighlighter").then(highlighter => { + let node = this.view.inspector.selection.nodeFront; + + // Toggle off the grid highlighter if the grid highlighter toggle is clicked + // for the current highlighted grid. + if (node === this.gridHighlighterShown) { + return highlighter.hide(); + } + + return highlighter.show(node); + }).then(isGridShown => { + // Toggle all the grid icons in the current rule view. + for (let gridIcon of this.view.element.querySelectorAll(".ruleview-grid")) { + gridIcon.classList.toggle("active", isGridShown); + } + + if (isGridShown) { + this.gridHighlighterShown = this.view.inspector.selection.nodeFront; + this.emit("highlighter-shown"); + } else { + this.gridHighlighterShown = null; + this.emit("highlighter-hidden"); + } + }).catch(e => console.error(e)); + }, + + _onMouseMove: function (event) { + // Bail out if the target is the same as for the last mousemove. + if (event.target === this._lastHovered) { + return; + } + + // Only one highlighter can be displayed at a time, hide the currently shown. + this._hideHoveredHighlighter(); + + this._lastHovered = event.target; + + let nodeInfo = this.view.getNodeInfo(event.target); + if (!nodeInfo) { + return; + } + + // Choose the type of highlighter required for the hovered node. + let type; + if (this._isRuleViewTransform(nodeInfo) || + this._isComputedViewTransform(nodeInfo)) { + type = "CssTransformHighlighter"; + } + + if (type) { + this.hoveredHighlighterShown = type; + let node = this.view.inspector.selection.nodeFront; + this._getHighlighter(type) + .then(highlighter => highlighter.show(node)) + .then(shown => { + if (shown) { + this.emit("highlighter-shown"); + } + }); + } + }, + + _onMouseOut: function (event) { + // Only hide the highlighter if the mouse leaves the currently hovered node. + if (!this._lastHovered || + (event && this._lastHovered.contains(event.relatedTarget))) { + return; + } + + // Otherwise, hide the highlighter. + this._lastHovered = null; + this._hideHoveredHighlighter(); + }, + + /** + * Clear saved highlighter shown properties on will-navigate. + */ + _onWillNavigate: function () { + this.gridHighlighterShown = null; + this.hoveredHighlighterShown = null; + this.selectorHighlighterShown = null; + }, + + /** + * Is the current hovered node a css transform property value in the rule-view. + * + * @param {Object} nodeInfo + * @return {Boolean} + */ + _isRuleViewTransform: function (nodeInfo) { + let isTransform = nodeInfo.type === VIEW_NODE_VALUE_TYPE && + nodeInfo.value.property === "transform"; + let isEnabled = nodeInfo.value.enabled && + !nodeInfo.value.overridden && + !nodeInfo.value.pseudoElement; + return this.isRuleView && isTransform && isEnabled; + }, + + /** + * Is the current hovered node a css transform property value in the + * computed-view. + * + * @param {Object} nodeInfo + * @return {Boolean} + */ + _isComputedViewTransform: function (nodeInfo) { + let isTransform = nodeInfo.type === VIEW_NODE_VALUE_TYPE && + nodeInfo.value.property === "transform"; + return !this.isRuleView && isTransform; + }, + + /** + * Is the current clicked node a grid display property value in the + * rule-view. + * + * @param {DOMNode} node + * @return {Boolean} + */ + _isDisplayGridValue: function (node) { + return this.isRuleView && node.classList.contains("ruleview-grid"); + }, + + /** + * Hide the currently shown grid highlighter. + */ + _hideGridHighlighter: function () { + if (!this.gridHighlighterShown || !this.highlighters.CssGridHighlighter) { + return; + } + + let onHidden = this.highlighters.CssGridHighlighter.hide(); + if (onHidden) { + onHidden.then(null, e => console.error(e)); + } + + this.gridHighlighterShown = null; + this.emit("highlighter-hidden"); + }, + + /** + * Hide the currently shown hovered highlighter. + */ + _hideHoveredHighlighter: function () { + if (!this.hoveredHighlighterShown || + !this.highlighters[this.hoveredHighlighterShown]) { + return; + } + + // For some reason, the call to highlighter.hide doesn't always return a + // promise. This causes some tests to fail when trying to install a + // rejection handler on the result of the call. To avoid this, check + // whether the result is truthy before installing the handler. + let onHidden = this.highlighters[this.hoveredHighlighterShown].hide(); + if (onHidden) { + onHidden.then(null, e => console.error(e)); + } + + this.hoveredHighlighterShown = null; + this.emit("highlighter-hidden"); + }, + + /** + * Get a highlighter front given a type. It will only be initialized once. + * + * @param {String} type + * The highlighter type. One of this.highlighters. + * @return {Promise} that resolves to the highlighter + */ + _getHighlighter: function (type) { + let utils = this.highlighterUtils; + + if (this.highlighters[type]) { + return promise.resolve(this.highlighters[type]); + } + + return utils.getHighlighterByType(type).then(highlighter => { + this.highlighters[type] = highlighter; + return highlighter; + }); + }, + + /** + * Destroy this overlay instance, removing it from the view and destroying + * all initialized highlighters. + */ + destroy: function () { + this.removeFromView(); + + for (let type in this.highlighters) { + if (this.highlighters[type]) { + this.highlighters[type].finalize(); + this.highlighters[type] = null; + } + } + + this.highlighters = null; + + this.gridHighlighterShown = null; + this.hoveredHighlighterShown = null; + this.selectorHighlighterShown = null; + + this.highlighterUtils = null; + this.isRuleView = null; + this.view = null; + + this._isDestroyed = true; + } +}; + +module.exports = HighlightersOverlay; diff --git a/devtools/client/inspector/shared/moz.build b/devtools/client/inspector/shared/moz.build new file mode 100644 index 000000000..fd2239b60 --- /dev/null +++ b/devtools/client/inspector/shared/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( + 'dom-node-preview.js', + 'highlighters-overlay.js', + 'node-types.js', + 'style-inspector-menu.js', + 'tooltips-overlay.js', + 'utils.js' +) + +BROWSER_CHROME_MANIFESTS += ['test/browser.ini'] diff --git a/devtools/client/inspector/shared/node-types.js b/devtools/client/inspector/shared/node-types.js new file mode 100644 index 000000000..4f31ee9fe --- /dev/null +++ b/devtools/client/inspector/shared/node-types.js @@ -0,0 +1,17 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set 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"; + +/** + * Types of nodes used in the rule and omputed view. + */ + +exports.VIEW_NODE_SELECTOR_TYPE = 1; +exports.VIEW_NODE_PROPERTY_TYPE = 2; +exports.VIEW_NODE_VALUE_TYPE = 3; +exports.VIEW_NODE_IMAGE_URL_TYPE = 4; +exports.VIEW_NODE_LOCATION_TYPE = 5; diff --git a/devtools/client/inspector/shared/style-inspector-menu.js b/devtools/client/inspector/shared/style-inspector-menu.js new file mode 100644 index 000000000..975074609 --- /dev/null +++ b/devtools/client/inspector/shared/style-inspector-menu.js @@ -0,0 +1,510 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set 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 {PREF_ORIG_SOURCES} = require("devtools/client/styleeditor/utils"); +const Services = require("Services"); +const {Task} = require("devtools/shared/task"); + +const Menu = require("devtools/client/framework/menu"); +const MenuItem = require("devtools/client/framework/menu-item"); + +const { + VIEW_NODE_SELECTOR_TYPE, + VIEW_NODE_PROPERTY_TYPE, + VIEW_NODE_VALUE_TYPE, + VIEW_NODE_IMAGE_URL_TYPE, + VIEW_NODE_LOCATION_TYPE, +} = require("devtools/client/inspector/shared/node-types"); +const clipboardHelper = require("devtools/shared/platform/clipboard"); + +const STYLE_INSPECTOR_PROPERTIES = "devtools/shared/locales/styleinspector.properties"; +const {LocalizationHelper} = require("devtools/shared/l10n"); +const STYLE_INSPECTOR_L10N = new LocalizationHelper(STYLE_INSPECTOR_PROPERTIES); + +const PREF_ENABLE_MDN_DOCS_TOOLTIP = + "devtools.inspector.mdnDocsTooltip.enabled"; + +/** + * Style inspector context menu + * + * @param {RuleView|ComputedView} view + * RuleView or ComputedView instance controlling this menu + * @param {Object} options + * Option menu configuration + */ +function StyleInspectorMenu(view, options) { + this.view = view; + this.inspector = this.view.inspector; + this.styleDocument = this.view.styleDocument; + this.styleWindow = this.view.styleWindow; + + this.isRuleView = options.isRuleView; + + this._onAddNewRule = this._onAddNewRule.bind(this); + this._onCopy = this._onCopy.bind(this); + this._onCopyColor = this._onCopyColor.bind(this); + this._onCopyImageDataUrl = this._onCopyImageDataUrl.bind(this); + this._onCopyLocation = this._onCopyLocation.bind(this); + this._onCopyPropertyDeclaration = this._onCopyPropertyDeclaration.bind(this); + this._onCopyPropertyName = this._onCopyPropertyName.bind(this); + this._onCopyPropertyValue = this._onCopyPropertyValue.bind(this); + this._onCopyRule = this._onCopyRule.bind(this); + this._onCopySelector = this._onCopySelector.bind(this); + this._onCopyUrl = this._onCopyUrl.bind(this); + this._onSelectAll = this._onSelectAll.bind(this); + this._onShowMdnDocs = this._onShowMdnDocs.bind(this); + this._onToggleOrigSources = this._onToggleOrigSources.bind(this); +} + +module.exports = StyleInspectorMenu; + +StyleInspectorMenu.prototype = { + /** + * Display the style inspector context menu + */ + show: function (event) { + try { + this._openMenu({ + target: event.explicitOriginalTarget, + screenX: event.screenX, + screenY: event.screenY, + }); + } catch (e) { + console.error(e); + } + }, + + _openMenu: function ({ target, screenX = 0, screenY = 0 } = { }) { + // In the sidebar we do not have this.styleDocument.popupNode + // so we need to save the node ourselves. + this.styleDocument.popupNode = target; + this.styleWindow.focus(); + + let menu = new Menu(); + + let menuitemCopy = new MenuItem({ + label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copy"), + accesskey: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copy.accessKey"), + click: () => { + this._onCopy(); + }, + disabled: !this._hasTextSelected(), + }); + let menuitemCopyLocation = new MenuItem({ + label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyLocation"), + click: () => { + this._onCopyLocation(); + }, + visible: false, + }); + let menuitemCopyRule = new MenuItem({ + label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyRule"), + click: () => { + this._onCopyRule(); + }, + visible: this.isRuleView, + }); + let copyColorAccessKey = "styleinspector.contextmenu.copyColor.accessKey"; + let menuitemCopyColor = new MenuItem({ + label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyColor"), + accesskey: STYLE_INSPECTOR_L10N.getStr(copyColorAccessKey), + click: () => { + this._onCopyColor(); + }, + visible: this._isColorPopup(), + }); + let copyUrlAccessKey = "styleinspector.contextmenu.copyUrl.accessKey"; + let menuitemCopyUrl = new MenuItem({ + label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyUrl"), + accesskey: STYLE_INSPECTOR_L10N.getStr(copyUrlAccessKey), + click: () => { + this._onCopyUrl(); + }, + visible: this._isImageUrl(), + }); + let copyImageAccessKey = "styleinspector.contextmenu.copyImageDataUrl.accessKey"; + let menuitemCopyImageDataUrl = new MenuItem({ + label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyImageDataUrl"), + accesskey: STYLE_INSPECTOR_L10N.getStr(copyImageAccessKey), + click: () => { + this._onCopyImageDataUrl(); + }, + visible: this._isImageUrl(), + }); + let copyPropDeclarationLabel = "styleinspector.contextmenu.copyPropertyDeclaration"; + let menuitemCopyPropertyDeclaration = new MenuItem({ + label: STYLE_INSPECTOR_L10N.getStr(copyPropDeclarationLabel), + click: () => { + this._onCopyPropertyDeclaration(); + }, + visible: false, + }); + let menuitemCopyPropertyName = new MenuItem({ + label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyPropertyName"), + click: () => { + this._onCopyPropertyName(); + }, + visible: false, + }); + let menuitemCopyPropertyValue = new MenuItem({ + label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyPropertyValue"), + click: () => { + this._onCopyPropertyValue(); + }, + visible: false, + }); + let menuitemCopySelector = new MenuItem({ + label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copySelector"), + click: () => { + this._onCopySelector(); + }, + visible: false, + }); + + this._clickedNodeInfo = this._getClickedNodeInfo(); + if (this.isRuleView && this._clickedNodeInfo) { + switch (this._clickedNodeInfo.type) { + case VIEW_NODE_PROPERTY_TYPE : + menuitemCopyPropertyDeclaration.visible = true; + menuitemCopyPropertyName.visible = true; + break; + case VIEW_NODE_VALUE_TYPE : + menuitemCopyPropertyDeclaration.visible = true; + menuitemCopyPropertyValue.visible = true; + break; + case VIEW_NODE_SELECTOR_TYPE : + menuitemCopySelector.visible = true; + break; + case VIEW_NODE_LOCATION_TYPE : + menuitemCopyLocation.visible = true; + break; + } + } + + menu.append(menuitemCopy); + menu.append(menuitemCopyLocation); + menu.append(menuitemCopyRule); + menu.append(menuitemCopyColor); + menu.append(menuitemCopyUrl); + menu.append(menuitemCopyImageDataUrl); + menu.append(menuitemCopyPropertyDeclaration); + menu.append(menuitemCopyPropertyName); + menu.append(menuitemCopyPropertyValue); + menu.append(menuitemCopySelector); + + menu.append(new MenuItem({ + type: "separator", + })); + + // Select All + let selectAllAccessKey = "styleinspector.contextmenu.selectAll.accessKey"; + let menuitemSelectAll = new MenuItem({ + label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.selectAll"), + accesskey: STYLE_INSPECTOR_L10N.getStr(selectAllAccessKey), + click: () => { + this._onSelectAll(); + }, + }); + menu.append(menuitemSelectAll); + + menu.append(new MenuItem({ + type: "separator", + })); + + // Add new rule + let addRuleAccessKey = "styleinspector.contextmenu.addNewRule.accessKey"; + let menuitemAddRule = new MenuItem({ + label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.addNewRule"), + accesskey: STYLE_INSPECTOR_L10N.getStr(addRuleAccessKey), + click: () => { + this._onAddNewRule(); + }, + visible: this.isRuleView, + disabled: !this.isRuleView || + this.inspector.selection.isAnonymousNode(), + }); + menu.append(menuitemAddRule); + + // Show MDN Docs + let mdnDocsAccessKey = "styleinspector.contextmenu.showMdnDocs.accessKey"; + let menuitemShowMdnDocs = new MenuItem({ + label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.showMdnDocs"), + accesskey: STYLE_INSPECTOR_L10N.getStr(mdnDocsAccessKey), + click: () => { + this._onShowMdnDocs(); + }, + visible: (Services.prefs.getBoolPref(PREF_ENABLE_MDN_DOCS_TOOLTIP) && + this._isPropertyName()), + }); + menu.append(menuitemShowMdnDocs); + + // Show Original Sources + let sourcesAccessKey = "styleinspector.contextmenu.toggleOrigSources.accessKey"; + let menuitemSources = new MenuItem({ + label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.toggleOrigSources"), + accesskey: STYLE_INSPECTOR_L10N.getStr(sourcesAccessKey), + click: () => { + this._onToggleOrigSources(); + }, + type: "checkbox", + checked: Services.prefs.getBoolPref(PREF_ORIG_SOURCES), + }); + menu.append(menuitemSources); + + menu.popup(screenX, screenY, this.inspector._toolbox); + return menu; + }, + + _hasTextSelected: function () { + let hasTextSelected; + let selection = this.styleWindow.getSelection(); + + let node = this._getClickedNode(); + if (node.nodeName == "input" || node.nodeName == "textarea") { + let { selectionStart, selectionEnd } = node; + hasTextSelected = isFinite(selectionStart) && isFinite(selectionEnd) + && selectionStart !== selectionEnd; + } else { + hasTextSelected = selection.toString() && !selection.isCollapsed; + } + + return hasTextSelected; + }, + + /** + * Get the type of the currently clicked node + */ + _getClickedNodeInfo: function () { + let node = this._getClickedNode(); + return this.view.getNodeInfo(node); + }, + + /** + * A helper that determines if the popup was opened with a click to a color + * value and saves the color to this._colorToCopy. + * + * @return {Boolean} + * true if click on color opened the popup, false otherwise. + */ + _isColorPopup: function () { + this._colorToCopy = ""; + + let container = this._getClickedNode(); + if (!container) { + return false; + } + + let isColorNode = el => el.dataset && "color" in el.dataset; + + while (!isColorNode(container)) { + container = container.parentNode; + if (!container) { + return false; + } + } + + this._colorToCopy = container.dataset.color; + return true; + }, + + _isPropertyName: function () { + let nodeInfo = this._getClickedNodeInfo(); + if (!nodeInfo) { + return false; + } + return nodeInfo.type == VIEW_NODE_PROPERTY_TYPE; + }, + + /** + * Check if the current node (clicked node) is an image URL + * + * @return {Boolean} true if the node is an image url + */ + _isImageUrl: function () { + let nodeInfo = this._getClickedNodeInfo(); + if (!nodeInfo) { + return false; + } + return nodeInfo.type == VIEW_NODE_IMAGE_URL_TYPE; + }, + + /** + * Get the DOM Node container for the current popupNode. + * If popupNode is a textNode, return the parent node, otherwise return + * popupNode itself. + * + * @return {DOMNode} + */ + _getClickedNode: function () { + let container = null; + let node = this.styleDocument.popupNode; + + if (node) { + let isTextNode = node.nodeType == node.TEXT_NODE; + container = isTextNode ? node.parentElement : node; + } + + return container; + }, + + /** + * Select all text. + */ + _onSelectAll: function () { + let selection = this.styleWindow.getSelection(); + selection.selectAllChildren(this.view.element); + }, + + /** + * Copy the most recently selected color value to clipboard. + */ + _onCopy: function () { + this.view.copySelection(this.styleDocument.popupNode); + }, + + /** + * Copy the most recently selected color value to clipboard. + */ + _onCopyColor: function () { + clipboardHelper.copyString(this._colorToCopy); + }, + + /* + * Retrieve the url for the selected image and copy it to the clipboard + */ + _onCopyUrl: function () { + if (!this._clickedNodeInfo) { + return; + } + + clipboardHelper.copyString(this._clickedNodeInfo.value.url); + }, + + /** + * Retrieve the image data for the selected image url and copy it to the + * clipboard + */ + _onCopyImageDataUrl: Task.async(function* () { + if (!this._clickedNodeInfo) { + return; + } + + let message; + try { + let inspectorFront = this.inspector.inspector; + let imageUrl = this._clickedNodeInfo.value.url; + let data = yield inspectorFront.getImageDataFromURL(imageUrl); + message = yield data.data.string(); + } catch (e) { + message = + STYLE_INSPECTOR_L10N.getStr("styleinspector.copyImageDataUrlError"); + } + + clipboardHelper.copyString(message); + }), + + /** + * Show docs from MDN for a CSS property. + */ + _onShowMdnDocs: function () { + let cssPropertyName = this.styleDocument.popupNode.textContent; + let anchor = this.styleDocument.popupNode.parentNode; + let cssDocsTooltip = this.view.tooltips.cssDocs; + cssDocsTooltip.show(anchor, cssPropertyName); + }, + + /** + * Add a new rule to the current element. + */ + _onAddNewRule: function () { + this.view._onAddRule(); + }, + + /** + * Copy the rule source location of the current clicked node. + */ + _onCopyLocation: function () { + if (!this._clickedNodeInfo) { + return; + } + + clipboardHelper.copyString(this._clickedNodeInfo.value); + }, + + /** + * Copy the rule property declaration of the current clicked node. + */ + _onCopyPropertyDeclaration: function () { + if (!this._clickedNodeInfo) { + return; + } + + let textProp = this._clickedNodeInfo.value.textProperty; + clipboardHelper.copyString(textProp.stringifyProperty()); + }, + + /** + * Copy the rule property name of the current clicked node. + */ + _onCopyPropertyName: function () { + if (!this._clickedNodeInfo) { + return; + } + + clipboardHelper.copyString(this._clickedNodeInfo.value.property); + }, + + /** + * Copy the rule property value of the current clicked node. + */ + _onCopyPropertyValue: function () { + if (!this._clickedNodeInfo) { + return; + } + + clipboardHelper.copyString(this._clickedNodeInfo.value.value); + }, + + /** + * Copy the rule of the current clicked node. + */ + _onCopyRule: function () { + let ruleEditor = + this.styleDocument.popupNode.parentNode.offsetParent._ruleEditor; + let rule = ruleEditor.rule; + clipboardHelper.copyString(rule.stringifyRule()); + }, + + /** + * Copy the rule selector of the current clicked node. + */ + _onCopySelector: function () { + if (!this._clickedNodeInfo) { + return; + } + + clipboardHelper.copyString(this._clickedNodeInfo.value); + }, + + /** + * Toggle the original sources pref. + */ + _onToggleOrigSources: function () { + let isEnabled = Services.prefs.getBoolPref(PREF_ORIG_SOURCES); + Services.prefs.setBoolPref(PREF_ORIG_SOURCES, !isEnabled); + }, + + destroy: function () { + this.popupNode = null; + this.styleDocument.popupNode = null; + this.view = null; + this.inspector = null; + this.styleDocument = null; + this.styleWindow = null; + } +}; diff --git a/devtools/client/inspector/shared/test/.eslintrc.js b/devtools/client/inspector/shared/test/.eslintrc.js new file mode 100644 index 000000000..698ae9181 --- /dev/null +++ b/devtools/client/inspector/shared/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/shared/test/browser.ini b/devtools/client/inspector/shared/test/browser.ini new file mode 100644 index 000000000..ce85ee80e --- /dev/null +++ b/devtools/client/inspector/shared/test/browser.ini @@ -0,0 +1,41 @@ +[DEFAULT] +tags = devtools +subsuite = devtools +support-files = + doc_author-sheet.html + doc_content_stylesheet.html + doc_content_stylesheet.xul + doc_content_stylesheet_imported.css + doc_content_stylesheet_imported2.css + doc_content_stylesheet_linked.css + doc_content_stylesheet_script.css + doc_content_stylesheet_xul.css + doc_frame_script.js + 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_styleinspector_context-menu-copy-color_01.js] +[browser_styleinspector_context-menu-copy-color_02.js] +subsuite = clipboard +[browser_styleinspector_context-menu-copy-urls.js] +subsuite = clipboard +[browser_styleinspector_csslogic-content-stylesheets.js] +skip-if = e10s && debug # Bug 1250058 (docshell leak when opening 2 toolboxes) +[browser_styleinspector_output-parser.js] +[browser_styleinspector_refresh_when_active.js] +[browser_styleinspector_tooltip-background-image.js] +[browser_styleinspector_tooltip-closes-on-new-selection.js] +skip-if = e10s # Bug 1111546 (e10s) +[browser_styleinspector_tooltip-longhand-fontfamily.js] +[browser_styleinspector_tooltip-multiple-background-images.js] +[browser_styleinspector_tooltip-shorthand-fontfamily.js] +[browser_styleinspector_tooltip-size.js] +[browser_styleinspector_transform-highlighter-01.js] +[browser_styleinspector_transform-highlighter-02.js] +[browser_styleinspector_transform-highlighter-03.js] +[browser_styleinspector_transform-highlighter-04.js] diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_01.js b/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_01.js new file mode 100644 index 000000000..5a27edf16 --- /dev/null +++ b/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_01.js @@ -0,0 +1,118 @@ +/* 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/ */ +"use strict"; + +// Test "Copy color" item of the context menu #1: Test _isColorPopup. + +const TEST_URI = ` +
+ Test "Copy color" context menu option +
+`; + +add_task(function* () { + // Test is slow on Linux EC2 instances - Bug 1137765 + requestLongerTimeout(2); + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector} = yield openInspector(); + yield testView("ruleview", inspector); + yield testView("computedview", inspector); +}); + +function* testView(viewId, inspector) { + info("Testing " + viewId); + + yield inspector.sidebar.select(viewId); + let view = inspector[viewId].view || inspector[viewId].computedView; + yield selectNode("div", inspector); + + testIsColorValueNode(view); + testIsColorPopupOnAllNodes(view); + yield clearCurrentNodeSelection(inspector); +} + +/** + * A function testing that isColorValueNode correctly detects nodes part of + * color values. + */ +function testIsColorValueNode(view) { + info("Testing that child nodes of color nodes are detected."); + let root = rootElement(view); + let colorNode = root.querySelector("span[data-color]"); + + ok(colorNode, "Color node found"); + for (let node of iterateNodes(colorNode)) { + ok(isColorValueNode(node), "Node is part of color value."); + } +} + +/** + * A function testing that _isColorPopup returns a correct value for all nodes + * in the view. + */ +function testIsColorPopupOnAllNodes(view) { + let root = rootElement(view); + for (let node of iterateNodes(root)) { + testIsColorPopupOnNode(view, node); + } +} + +/** + * Test result of _isColorPopup with given node. + * @param object view + * A CSSRuleView or CssComputedView instance. + * @param Node node + * A node to check. + */ +function testIsColorPopupOnNode(view, node) { + info("Testing node " + node); + view.styleDocument.popupNode = node; + view._contextmenu._colorToCopy = ""; + + let result = view._contextmenu._isColorPopup(); + let correct = isColorValueNode(node); + + is(result, correct, "_isColorPopup returned the expected value " + correct); + is(view._contextmenu._colorToCopy, (correct) ? "rgb(18, 58, 188)" : "", + "_colorToCopy was set to the expected value"); +} + +/** + * Check if a node is part of color value i.e. it has parent with a 'data-color' + * attribute. + */ +function isColorValueNode(node) { + let container = (node.nodeType == node.TEXT_NODE) ? + node.parentElement : node; + + let isColorNode = el => el.dataset && "color" in el.dataset; + + while (!isColorNode(container)) { + container = container.parentNode; + if (!container) { + info("No color. Node is not part of color value."); + return false; + } + } + + info("Found a color. Node is part of color value."); + + return true; +} + +/** + * A generator that iterates recursively trough all child nodes of baseNode. + */ +function* iterateNodes(baseNode) { + yield baseNode; + + for (let child of baseNode.childNodes) { + yield* iterateNodes(child); + } +} + +/** + * Returns the root element for the given view, rule or computed. + */ +var rootElement = view => (view.element) ? view.element : view.styleDocument; diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_02.js b/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_02.js new file mode 100644 index 000000000..afae7a2b6 --- /dev/null +++ b/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_02.js @@ -0,0 +1,99 @@ +/* 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/ */ +"use strict"; + +// Test "Copy color" item of the context menu #2: Test that correct color is +// copied if the color changes. + +const TEST_URI = ` + +
Testing the color picker tooltip!
+`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + + let {inspector, view} = yield openRuleView(); + + yield testCopyToClipboard(inspector, view); + yield testManualEdit(inspector, view); + yield testColorPickerEdit(inspector, view); +}); + +function* testCopyToClipboard(inspector, view) { + info("Testing that color is copied to clipboard"); + + yield selectNode("div", inspector); + + let element = getRuleViewProperty(view, "div", "color").valueSpan + .querySelector(".ruleview-colorswatch"); + + let allMenuItems = openStyleContextMenuAndGetAllItems(view, element); + let menuitemCopyColor = allMenuItems.find(item => item.label === + STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyColor")); + + ok(menuitemCopyColor.visible, "Copy color is visible"); + + yield waitForClipboardPromise(() => menuitemCopyColor.click(), + "#123ABC"); + + EventUtils.synthesizeKey("VK_ESCAPE", { }); +} + +function* testManualEdit(inspector, view) { + info("Testing manually edited colors"); + yield selectNode("div", inspector); + + let {valueSpan} = getRuleViewProperty(view, "div", "color"); + + let newColor = "#C9184E"; + let editor = yield focusEditableField(view, valueSpan); + + info("Typing new value"); + let input = editor.input; + let onBlur = once(input, "blur"); + EventUtils.sendString(newColor + ";", view.styleWindow); + yield onBlur; + yield wait(1); + + let colorValueElement = getRuleViewProperty(view, "div", "color") + .valueSpan.firstChild; + is(colorValueElement.dataset.color, newColor, "data-color was updated"); + + view.styleDocument.popupNode = colorValueElement; + + let contextMenu = view._contextmenu; + contextMenu._isColorPopup(); + is(contextMenu._colorToCopy, newColor, "_colorToCopy has the new value"); +} + +function* testColorPickerEdit(inspector, view) { + info("Testing colors edited via color picker"); + yield selectNode("div", inspector); + + let swatchElement = getRuleViewProperty(view, "div", "color").valueSpan + .querySelector(".ruleview-colorswatch"); + + info("Opening the color picker"); + let picker = view.tooltips.colorPicker; + let onColorPickerReady = picker.once("ready"); + swatchElement.click(); + yield onColorPickerReady; + + let rgbaColor = [83, 183, 89, 1]; + let rgbaColorText = "rgba(83, 183, 89, 1)"; + yield simulateColorPickerChange(view, picker, rgbaColor); + + is(swatchElement.parentNode.dataset.color, rgbaColorText, + "data-color was updated"); + view.styleDocument.popupNode = swatchElement; + + let contextMenu = view._contextmenu; + contextMenu._isColorPopup(); + is(contextMenu._colorToCopy, rgbaColorText, "_colorToCopy has the new value"); +} diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-urls.js b/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-urls.js new file mode 100644 index 000000000..412137825 --- /dev/null +++ b/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-urls.js @@ -0,0 +1,109 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* Tests both Copy URL and Copy Data URL context menu items */ + +const TEST_DATA_URI = "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="; + +// Invalid URL still needs to be reachable otherwise getImageDataUrl will +// timeout. DevTools chrome:// URLs aren't content accessible, so use some +// random resource:// URL here. +const INVALID_IMAGE_URI = "resource://devtools/client/definitions.js"; +const ERROR_MESSAGE = STYLE_INSPECTOR_L10N.getStr("styleinspector.copyImageDataUrlError"); + +add_task(function* () { + const TEST_URI = ` +
Valid background image
+
Invalid background image
`; + + yield addTab("data:text/html;charset=utf8," + encodeURIComponent(TEST_URI)); + + yield startTest(); +}); + +function* startTest() { + info("Opening rule view"); + let {inspector, view} = yield openRuleView(); + + info("Test valid background image URL in rule view"); + yield testCopyUrlToClipboard({view, inspector}, "data-uri", + ".valid-background", TEST_DATA_URI); + yield testCopyUrlToClipboard({view, inspector}, "url", + ".valid-background", TEST_DATA_URI); + + info("Test invalid background image URL in rue view"); + yield testCopyUrlToClipboard({view, inspector}, "data-uri", + ".invalid-background", ERROR_MESSAGE); + yield testCopyUrlToClipboard({view, inspector}, "url", + ".invalid-background", INVALID_IMAGE_URI); + + info("Opening computed view"); + view = selectComputedView(inspector); + + info("Test valid background image URL in computed view"); + yield testCopyUrlToClipboard({view, inspector}, "data-uri", + ".valid-background", TEST_DATA_URI); + yield testCopyUrlToClipboard({view, inspector}, "url", + ".valid-background", TEST_DATA_URI); + + info("Test invalid background image URL in computed view"); + yield testCopyUrlToClipboard({view, inspector}, "data-uri", + ".invalid-background", ERROR_MESSAGE); + yield testCopyUrlToClipboard({view, inspector}, "url", + ".invalid-background", INVALID_IMAGE_URI); +} + +function* testCopyUrlToClipboard({view, inspector}, type, selector, expected) { + info("Select node in inspector panel"); + yield selectNode(selector, inspector); + + info("Retrieve background-image link for selected node in current " + + "styleinspector view"); + let property = getBackgroundImageProperty(view, selector); + let imageLink = property.valueSpan.querySelector(".theme-link"); + ok(imageLink, "Background-image link element found"); + + info("Simulate right click on the background-image URL"); + let allMenuItems = openStyleContextMenuAndGetAllItems(view, imageLink); + let menuitemCopyUrl = allMenuItems.find(item => item.label === + STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyUrl")); + let menuitemCopyImageDataUrl = allMenuItems.find(item => item.label === + STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyImageDataUrl")); + + info("Context menu is displayed"); + ok(menuitemCopyUrl.visible, + "\"Copy URL\" menu entry is displayed"); + ok(menuitemCopyImageDataUrl.visible, + "\"Copy Image Data-URL\" menu entry is displayed"); + + if (type == "data-uri") { + info("Click Copy Data URI and wait for clipboard"); + yield waitForClipboardPromise(() => { + return menuitemCopyImageDataUrl.click(); + }, expected); + } else { + info("Click Copy URL and wait for clipboard"); + yield waitForClipboardPromise(() => { + return menuitemCopyUrl.click(); + }, expected); + } + + info("Hide context menu"); +} + +function getBackgroundImageProperty(view, selector) { + let isRuleView = view instanceof CssRuleView; + if (isRuleView) { + return getRuleViewProperty(view, selector, "background-image"); + } + return getComputedViewProperty(view, "background-image"); +} diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_csslogic-content-stylesheets.js b/devtools/client/inspector/shared/test/browser_styleinspector_csslogic-content-stylesheets.js new file mode 100644 index 000000000..421a2bb47 --- /dev/null +++ b/devtools/client/inspector/shared/test/browser_styleinspector_csslogic-content-stylesheets.js @@ -0,0 +1,82 @@ +/* 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/ */ + +"use strict"; + +// Check stylesheets on HMTL and XUL document + +// FIXME: this test opens the devtools for nothing, it should be changed into a +// devtools/server/tests/mochitest/test_css-logic-...something...html +// test + +const TEST_URI_HTML = TEST_URL_ROOT + "doc_content_stylesheet.html"; +const TEST_URI_AUTHOR = TEST_URL_ROOT + "doc_author-sheet.html"; +const TEST_URI_XUL = TEST_URL_ROOT + "doc_content_stylesheet.xul"; +const XUL_URI = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService) + .newURI(TEST_URI_XUL, null, null); +var ssm = Components.classes["@mozilla.org/scriptsecuritymanager;1"] + .getService(Ci.nsIScriptSecurityManager); +const XUL_PRINCIPAL = ssm.createCodebasePrincipal(XUL_URI, {}); + +add_task(function* () { + requestLongerTimeout(2); + + info("Checking stylesheets on HTML document"); + yield addTab(TEST_URI_HTML); + + let {inspector, testActor} = yield openInspector(); + yield selectNode("#target", inspector); + + info("Checking stylesheets"); + yield checkSheets("#target", testActor); + + info("Checking authored stylesheets"); + yield addTab(TEST_URI_AUTHOR); + + ({inspector} = yield openInspector()); + yield selectNode("#target", inspector); + yield checkSheets("#target", testActor); + + info("Checking stylesheets on XUL document"); + info("Allowing XUL content"); + allowXUL(); + yield addTab(TEST_URI_XUL); + + ({inspector} = yield openInspector()); + yield selectNode("#target", inspector); + + yield checkSheets("#target", testActor); + info("Disallowing XUL content"); + disallowXUL(); +}); + +function allowXUL() { + Cc["@mozilla.org/permissionmanager;1"].getService(Ci.nsIPermissionManager) + .addFromPrincipal(XUL_PRINCIPAL, "allowXULXBL", + Ci.nsIPermissionManager.ALLOW_ACTION); +} + +function disallowXUL() { + Cc["@mozilla.org/permissionmanager;1"].getService(Ci.nsIPermissionManager) + .addFromPrincipal(XUL_PRINCIPAL, "allowXULXBL", + Ci.nsIPermissionManager.DENY_ACTION); +} + +function* checkSheets(targetSelector, testActor) { + let sheets = yield testActor.getStyleSheetsInfoForNode(targetSelector); + + for (let sheet of sheets) { + if (!sheet.href || + /doc_content_stylesheet_/.test(sheet.href) || + // For the "authored" case. + /^data:.*seagreen/.test(sheet.href)) { + ok(sheet.isContentSheet, + sheet.href + " identified as content stylesheet"); + } else { + ok(!sheet.isContentSheet, + sheet.href + " identified as non-content stylesheet"); + } + } +} diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_output-parser.js b/devtools/client/inspector/shared/test/browser_styleinspector_output-parser.js new file mode 100644 index 000000000..f1f846f5d --- /dev/null +++ b/devtools/client/inspector/shared/test/browser_styleinspector_output-parser.js @@ -0,0 +1,341 @@ +/* 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/ */ + +"use strict"; + +// Test expected outputs of the output-parser's parseCssProperty function. + +// This is more of a unit test than a mochitest-browser test, but can't be +// tested with an xpcshell test as the output-parser requires the DOM to work. + +const {OutputParser} = require("devtools/client/shared/output-parser"); +const {initCssProperties, getCssProperties} = require("devtools/shared/fronts/css-properties"); + +const COLOR_CLASS = "color-class"; +const URL_CLASS = "url-class"; +const CUBIC_BEZIER_CLASS = "bezier-class"; +const ANGLE_CLASS = "angle-class"; + +const TEST_DATA = [ + { + name: "width", + value: "100%", + test: fragment => { + is(countAll(fragment), 0); + is(fragment.textContent, "100%"); + } + }, + { + name: "width", + value: "blue", + test: fragment => { + is(countAll(fragment), 0); + } + }, + { + name: "content", + value: "'red url(test.png) repeat top left'", + test: fragment => { + is(countAll(fragment), 0); + } + }, + { + name: "content", + value: "\"blue\"", + test: fragment => { + is(countAll(fragment), 0); + } + }, + { + name: "margin-left", + value: "url(something.jpg)", + test: fragment => { + is(countAll(fragment), 0); + } + }, + { + name: "background-color", + value: "transparent", + test: fragment => { + is(countAll(fragment), 2); + is(countColors(fragment), 1); + is(fragment.textContent, "transparent"); + } + }, + { + name: "color", + value: "red", + test: fragment => { + is(countColors(fragment), 1); + is(fragment.textContent, "red"); + } + }, + { + name: "color", + value: "#F06", + test: fragment => { + is(countColors(fragment), 1); + is(fragment.textContent, "#F06"); + } + }, + { + name: "border", + value: "80em dotted pink", + test: fragment => { + is(countAll(fragment), 2); + is(countColors(fragment), 1); + is(getColor(fragment), "pink"); + } + }, + { + name: "color", + value: "red !important", + test: fragment => { + is(countColors(fragment), 1); + is(fragment.textContent, "red !important"); + } + }, + { + name: "background", + value: "red url(test.png) repeat top left", + test: fragment => { + is(countColors(fragment), 1); + is(countUrls(fragment), 1); + is(getColor(fragment), "red"); + is(getUrl(fragment), "test.png"); + is(countAll(fragment), 3); + } + }, + { + name: "background", + value: "blue url(test.png) repeat top left !important", + test: fragment => { + is(countColors(fragment), 1); + is(countUrls(fragment), 1); + is(getColor(fragment), "blue"); + is(getUrl(fragment), "test.png"); + is(countAll(fragment), 3); + } + }, + { + name: "list-style-image", + value: "url(\"images/arrow.gif\")", + test: fragment => { + is(countAll(fragment), 1); + is(getUrl(fragment), "images/arrow.gif"); + } + }, + { + name: "list-style-image", + value: "url(\"images/arrow.gif\")!important", + test: fragment => { + is(countAll(fragment), 1); + is(getUrl(fragment), "images/arrow.gif"); + is(fragment.textContent, "url(\"images/arrow.gif\")!important"); + } + }, + { + name: "-moz-binding", + value: "url(http://somesite.com/path/to/binding.xml#someid)", + test: fragment => { + is(countAll(fragment), 1); + is(countUrls(fragment), 1); + is(getUrl(fragment), "http://somesite.com/path/to/binding.xml#someid"); + } + }, + { + name: "background", + value: "linear-gradient(to right, rgba(183,222,237,1) 0%, " + + "rgba(33,180,226,1) 30%, rgba(31,170,217,.5) 44%, " + + "#F06 75%, red 100%)", + test: fragment => { + is(countAll(fragment), 10); + let allSwatches = fragment.querySelectorAll("." + COLOR_CLASS); + is(allSwatches.length, 5); + is(allSwatches[0].textContent, "rgba(183,222,237,1)"); + is(allSwatches[1].textContent, "rgba(33,180,226,1)"); + is(allSwatches[2].textContent, "rgba(31,170,217,.5)"); + is(allSwatches[3].textContent, "#F06"); + is(allSwatches[4].textContent, "red"); + } + }, + { + name: "background", + value: "-moz-radial-gradient(center 45deg, circle closest-side, " + + "orange 0%, red 100%)", + test: fragment => { + is(countAll(fragment), 6); + let colorSwatches = fragment.querySelectorAll("." + COLOR_CLASS); + is(colorSwatches.length, 2); + is(colorSwatches[0].textContent, "orange"); + is(colorSwatches[1].textContent, "red"); + let angleSwatches = fragment.querySelectorAll("." + ANGLE_CLASS); + is(angleSwatches.length, 1); + is(angleSwatches[0].textContent, "45deg"); + } + }, + { + name: "background", + value: "white url(http://test.com/wow_such_image.png) no-repeat top left", + test: fragment => { + is(countAll(fragment), 3); + is(countUrls(fragment), 1); + is(countColors(fragment), 1); + } + }, + { + name: "background", + value: "url(\"http://test.com/wow_such_(oh-noes)image.png?testid=1&color=red#w00t\")", + test: fragment => { + is(countAll(fragment), 1); + is(getUrl(fragment), "http://test.com/wow_such_(oh-noes)image.png?testid=1&color=red#w00t"); + } + }, + { + name: "background-image", + value: "url(this-is-an-incredible-image.jpeg)", + test: fragment => { + is(countAll(fragment), 1); + is(getUrl(fragment), "this-is-an-incredible-image.jpeg"); + } + }, + { + name: "background", + value: "red url( \"http://wow.com/cool/../../../you're(doingit)wrong\" ) repeat center", + test: fragment => { + is(countAll(fragment), 3); + is(countColors(fragment), 1); + is(getUrl(fragment), "http://wow.com/cool/../../../you're(doingit)wrong"); + } + }, + { + name: "background-image", + value: "url(../../../look/at/this/folder/structure/../" + + "../red.blue.green.svg )", + test: fragment => { + is(countAll(fragment), 1); + is(getUrl(fragment), "../../../look/at/this/folder/structure/../" + + "../red.blue.green.svg"); + } + }, + { + name: "transition-timing-function", + value: "linear", + test: fragment => { + is(countCubicBeziers(fragment), 1); + is(getCubicBezier(fragment), "linear"); + } + }, + { + name: "animation-timing-function", + value: "ease-in-out", + test: fragment => { + is(countCubicBeziers(fragment), 1); + is(getCubicBezier(fragment), "ease-in-out"); + } + }, + { + name: "animation-timing-function", + value: "cubic-bezier(.1, 0.55, .9, -3.45)", + test: fragment => { + is(countCubicBeziers(fragment), 1); + is(getCubicBezier(fragment), "cubic-bezier(.1, 0.55, .9, -3.45)"); + } + }, + { + name: "animation", + value: "move 3s cubic-bezier(.1, 0.55, .9, -3.45)", + test: fragment => { + is(countCubicBeziers(fragment), 1); + is(getCubicBezier(fragment), "cubic-bezier(.1, 0.55, .9, -3.45)"); + } + }, + { + name: "transition", + value: "top 1s ease-in", + test: fragment => { + is(countCubicBeziers(fragment), 1); + is(getCubicBezier(fragment), "ease-in"); + } + }, + { + name: "transition", + value: "top 3s steps(4, end)", + test: fragment => { + is(countAll(fragment), 0); + } + }, + { + name: "transition", + value: "top 3s step-start", + test: fragment => { + is(countAll(fragment), 0); + } + }, + { + name: "transition", + value: "top 3s step-end", + test: fragment => { + is(countAll(fragment), 0); + } + }, + { + name: "background", + value: "rgb(255, var(--g-value), 192)", + test: fragment => { + is(fragment.textContent, "rgb(255, var(--g-value), 192)"); + } + }, + { + name: "background", + value: "rgb(255, var(--g-value, 0), 192)", + test: fragment => { + is(fragment.textContent, "rgb(255, var(--g-value, 0), 192)"); + } + } +]; + +add_task(function* () { + // Mock the toolbox that initCssProperties expect so we get the fallback css properties. + let toolbox = {target: {client: {}, hasActor: () => false}}; + yield initCssProperties(toolbox); + let cssProperties = getCssProperties(toolbox); + + let parser = new OutputParser(document, cssProperties); + for (let i = 0; i < TEST_DATA.length; i++) { + let data = TEST_DATA[i]; + info("Output-parser test data " + i + ". {" + data.name + " : " + + data.value + ";}"); + data.test(parser.parseCssProperty(data.name, data.value, { + colorClass: COLOR_CLASS, + urlClass: URL_CLASS, + bezierClass: CUBIC_BEZIER_CLASS, + angleClass: ANGLE_CLASS, + defaultColorType: false + })); + } +}); + +function countAll(fragment) { + return fragment.querySelectorAll("*").length; +} +function countColors(fragment) { + return fragment.querySelectorAll("." + COLOR_CLASS).length; +} +function countUrls(fragment) { + return fragment.querySelectorAll("." + URL_CLASS).length; +} +function countCubicBeziers(fragment) { + return fragment.querySelectorAll("." + CUBIC_BEZIER_CLASS).length; +} +function getColor(fragment, index) { + return fragment.querySelectorAll("." + COLOR_CLASS)[index||0].textContent; +} +function getUrl(fragment, index) { + return fragment.querySelectorAll("." + URL_CLASS)[index||0].textContent; +} +function getCubicBezier(fragment, index) { + return fragment.querySelectorAll("." + CUBIC_BEZIER_CLASS)[index||0] + .textContent; +} diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_refresh_when_active.js b/devtools/client/inspector/shared/test/browser_styleinspector_refresh_when_active.js new file mode 100644 index 000000000..942fe05e2 --- /dev/null +++ b/devtools/client/inspector/shared/test/browser_styleinspector_refresh_when_active.js @@ -0,0 +1,43 @@ +/* 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/ */ + +"use strict"; + +// Test that the style-inspector views only refresh when they are active. + +const TEST_URI = ` +
one
+
two
+`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + + yield selectNode("#one", inspector); + + is(getRuleViewPropertyValue(view, "element", "color"), "red", + "The rule-view shows the properties for test node one"); + + let cView = inspector.computedview.computedView; + let prop = getComputedViewProperty(cView, "color"); + ok(!prop, "The computed-view doesn't show the properties for test node one"); + + info("Switching to the computed-view"); + let onComputedViewReady = inspector.once("computed-view-refreshed"); + selectComputedView(inspector); + yield onComputedViewReady; + + ok(getComputedViewPropertyValue(cView, "color"), "#F00", + "The computed-view shows the properties for test node one"); + + info("Selecting test node two"); + yield selectNode("#two", inspector); + + ok(getComputedViewPropertyValue(cView, "color"), "#00F", + "The computed-view shows the properties for test node two"); + + is(getRuleViewPropertyValue(view, "element", "color"), "red", + "The rule-view doesn't the properties for test node two"); +}); diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-background-image.js b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-background-image.js new file mode 100644 index 000000000..bd467b800 --- /dev/null +++ b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-background-image.js @@ -0,0 +1,125 @@ +/* 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/ */ + +"use strict"; + +// Test that background-image URLs have image preview tooltips in the rule-view +// and computed-view + +const TEST_URI = ` + +
test element
+`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + + info("Testing the background-image property on the body rule"); + yield testBodyRuleView(view); + + info("Selecting the test div node"); + yield selectNode(".test-element", inspector); + info("Testing the the background property on the .test-element rule"); + yield testDivRuleView(view); + + info("Testing that image preview tooltips show even when there are " + + "fields being edited"); + yield testTooltipAppearsEvenInEditMode(view); + + info("Switching over to the computed-view"); + let onComputedViewReady = inspector.once("computed-view-refreshed"); + view = selectComputedView(inspector); + yield onComputedViewReady; + + info("Testing that the background-image computed style has a tooltip too"); + yield testComputedView(view); +}); + +function* testBodyRuleView(view) { + info("Testing tooltips in the rule view"); + let panel = view.tooltips.previewTooltip.panel; + + // Check that the rule view has a tooltip and that a XUL panel has + // been created + ok(view.tooltips.previewTooltip, "Tooltip instance exists"); + ok(panel, "XUL panel exists"); + + // Get the background-image property inside the rule view + let {valueSpan} = getRuleViewProperty(view, "body", "background-image"); + let uriSpan = valueSpan.querySelector(".theme-link"); + + yield assertHoverTooltipOn(view.tooltips.previewTooltip, uriSpan); + + let images = panel.getElementsByTagName("img"); + is(images.length, 1, "Tooltip contains an image"); + ok(images[0].getAttribute("src") + .indexOf("iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHe") !== -1, + "The image URL seems fine"); +} + +function* testDivRuleView(view) { + let panel = view.tooltips.previewTooltip.panel; + + // Get the background property inside the rule view + let {valueSpan} = getRuleViewProperty(view, ".test-element", "background"); + let uriSpan = valueSpan.querySelector(".theme-link"); + + yield assertHoverTooltipOn(view.tooltips.previewTooltip, uriSpan); + + let images = panel.getElementsByTagName("img"); + is(images.length, 1, "Tooltip contains an image"); + ok(images[0].getAttribute("src").startsWith("data:"), + "Tooltip contains a data-uri image as expected"); +} + +function* testTooltipAppearsEvenInEditMode(view) { + info("Switching to edit mode in the rule view"); + let editor = yield turnToEditMode(view); + + info("Now trying to show the preview tooltip"); + let {valueSpan} = getRuleViewProperty(view, ".test-element", "background"); + let uriSpan = valueSpan.querySelector(".theme-link"); + yield assertHoverTooltipOn(view.tooltips.previewTooltip, uriSpan); + + is(view.styleDocument.activeElement, editor.input, + "Tooltip was shown in edit mode, and inplace-editor still focused"); +} + +function turnToEditMode(ruleView) { + let brace = ruleView.styleDocument.querySelector(".ruleview-ruleclose"); + return focusEditableField(ruleView, brace); +} + +function* testComputedView(view) { + let tooltip = view.tooltips.previewTooltip; + ok(tooltip, "The computed-view has a tooltip defined"); + + let panel = tooltip.panel; + ok(panel, "The computed-view tooltip has a XUL panel"); + + let {valueSpan} = getComputedViewProperty(view, "background-image"); + let uriSpan = valueSpan.querySelector(".theme-link"); + + yield assertHoverTooltipOn(view.tooltips.previewTooltip, uriSpan); + + let images = panel.getElementsByTagName("img"); + is(images.length, 1, "Tooltip contains an image"); + + ok(images[0].getAttribute("src").startsWith("data:"), + "Tooltip contains a data-uri in the computed-view too"); +} diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-closes-on-new-selection.js b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-closes-on-new-selection.js new file mode 100644 index 000000000..7f15d4fbe --- /dev/null +++ b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-closes-on-new-selection.js @@ -0,0 +1,73 @@ +/* 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/ */ + +"use strict"; + +// Test that if a tooltip is visible when a new selection is made, it closes + +const TEST_URI = "
el 1
el 2
"; +const XHTML_NS = "http://www.w3.org/1999/xhtml"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode(".one", inspector); + + info("Testing rule view tooltip closes on new selection"); + yield testRuleView(view, inspector); + + info("Testing computed view tooltip closes on new selection"); + view = selectComputedView(inspector); + yield testComputedView(view, inspector); +}); + +function* testRuleView(ruleView, inspector) { + info("Showing the tooltip"); + + let tooltip = ruleView.tooltips.previewTooltip; + let tooltipContent = ruleView.styleDocument.createElementNS(XHTML_NS, "div"); + yield tooltip.setContent(tooltipContent, {width: 100, height: 30}); + + // Stop listening for mouse movements because it's not needed for this test, + // and causes intermittent failures on Linux. When this test runs in the suite + // sometimes a mouseleave event is dispatched at the start, which causes the + // tooltip to hide in the middle of being shown, which causes timeouts later. + tooltip.stopTogglingOnHover(); + + let onShown = tooltip.once("shown"); + tooltip.show(ruleView.styleDocument.firstElementChild); + yield onShown; + + info("Selecting a new node"); + let onHidden = tooltip.once("hidden"); + yield selectNode(".two", inspector); + yield onHidden; + + ok(true, "Rule view tooltip closed after a new node got selected"); +} + +function* testComputedView(computedView, inspector) { + info("Showing the tooltip"); + + let tooltip = computedView.tooltips.previewTooltip; + let tooltipContent = computedView.styleDocument.createElementNS(XHTML_NS, "div"); + yield tooltip.setContent(tooltipContent, {width: 100, height: 30}); + + // Stop listening for mouse movements because it's not needed for this test, + // and causes intermittent failures on Linux. When this test runs in the suite + // sometimes a mouseleave event is dispatched at the start, which causes the + // tooltip to hide in the middle of being shown, which causes timeouts later. + tooltip.stopTogglingOnHover(); + + let onShown = tooltip.once("shown"); + tooltip.show(computedView.styleDocument.firstElementChild); + yield onShown; + + info("Selecting a new node"); + let onHidden = tooltip.once("hidden"); + yield selectNode(".one", inspector); + yield onHidden; + + ok(true, "Computed view tooltip closed after a new node got selected"); +} diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-longhand-fontfamily.js b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-longhand-fontfamily.js new file mode 100644 index 000000000..6bce367ae --- /dev/null +++ b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-longhand-fontfamily.js @@ -0,0 +1,120 @@ +/* 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/ */ + +"use strict"; + +// Test the fontfamily tooltip on longhand properties + +const TEST_URI = ` + +
test element
+`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testElement", inspector); + yield testRuleView(view, inspector.selection.nodeFront); + + info("Opening the computed view"); + let onComputedViewReady = inspector.once("computed-view-refreshed"); + view = selectComputedView(inspector); + yield onComputedViewReady; + + yield testComputedView(view, inspector.selection.nodeFront); + + yield testExpandedComputedViewProperty(view, inspector.selection.nodeFront); +}); + +function* testRuleView(ruleView, nodeFront) { + info("Testing font-family tooltips in the rule view"); + + let tooltip = ruleView.tooltips.previewTooltip; + let panel = tooltip.panel; + + // Check that the rule view has a tooltip and that a XUL panel has + // been created + ok(tooltip, "Tooltip instance exists"); + ok(panel, "XUL panel exists"); + + // Get the font family property inside the rule view + let {valueSpan} = getRuleViewProperty(ruleView, "#testElement", + "font-family"); + + // And verify that the tooltip gets shown on this property + yield assertHoverTooltipOn(tooltip, valueSpan); + + let images = panel.getElementsByTagName("img"); + is(images.length, 1, "Tooltip contains an image"); + ok(images[0].getAttribute("src").startsWith("data:"), + "Tooltip contains a data-uri image as expected"); + + let dataURL = yield getFontFamilyDataURL(valueSpan.textContent, nodeFront); + is(images[0].getAttribute("src"), dataURL, + "Tooltip contains the correct data-uri image"); +} + +function* testComputedView(computedView, nodeFront) { + info("Testing font-family tooltips in the computed view"); + + let tooltip = computedView.tooltips.previewTooltip; + let panel = tooltip.panel; + let {valueSpan} = getComputedViewProperty(computedView, "font-family"); + + yield assertHoverTooltipOn(tooltip, valueSpan); + + let images = panel.getElementsByTagName("img"); + is(images.length, 1, "Tooltip contains an image"); + ok(images[0].getAttribute("src").startsWith("data:"), + "Tooltip contains a data-uri image as expected"); + + let dataURL = yield getFontFamilyDataURL(valueSpan.textContent, nodeFront); + is(images[0].getAttribute("src"), dataURL, + "Tooltip contains the correct data-uri image"); +} + +function* testExpandedComputedViewProperty(computedView, nodeFront) { + info("Testing font-family tooltips in expanded properties of the " + + "computed view"); + + info("Expanding the font-family property to reveal matched selectors"); + let propertyView = getPropertyView(computedView, "font-family"); + propertyView.matchedExpanded = true; + yield propertyView.refreshMatchedSelectors(); + + let valueSpan = propertyView.matchedSelectorsContainer + .querySelector(".bestmatch .other-property-value"); + + let tooltip = computedView.tooltips.previewTooltip; + let panel = tooltip.panel; + + yield assertHoverTooltipOn(tooltip, valueSpan); + + let images = panel.getElementsByTagName("img"); + is(images.length, 1, "Tooltip contains an image"); + ok(images[0].getAttribute("src").startsWith("data:"), + "Tooltip contains a data-uri image as expected"); + + let dataURL = yield getFontFamilyDataURL(valueSpan.textContent, nodeFront); + is(images[0].getAttribute("src"), dataURL, + "Tooltip contains the correct data-uri image"); +} + +function getPropertyView(computedView, name) { + let propertyView = null; + computedView.propertyViews.some(function (view) { + if (view.name == name) { + propertyView = view; + return true; + } + return false; + }); + return propertyView; +} diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-multiple-background-images.js b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-multiple-background-images.js new file mode 100644 index 000000000..60d747a45 --- /dev/null +++ b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-multiple-background-images.js @@ -0,0 +1,63 @@ +/* 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/ */ +"use strict"; + +// Test for bug 1026921: Ensure the URL of hovered url() node is used instead +// of the first found from the declaration as there might be multiple urls. + +const YELLOW_DOT = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gYcDCwCr0o5ngAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAANSURBVAjXY/j/n6EeAAd9An7Z55GEAAAAAElFTkSuQmCC"; +const BLUE_DOT = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gYcDCwlCkCM9QAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAANSURBVAjXY2Bg+F8PAAKCAX/tPkrkAAAAAElFTkSuQmCC"; +const TEST_STYLE = `h1 {background: url(${YELLOW_DOT}), url(${BLUE_DOT});}`; +const TEST_URI = `

test element

`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector} = yield openInspector(); + + yield testRuleViewUrls(inspector); + yield testComputedViewUrls(inspector); +}); + +function* testRuleViewUrls(inspector) { + info("Testing tooltips in the rule view"); + let view = selectRuleView(inspector); + yield selectNode("h1", inspector); + + let {valueSpan} = getRuleViewProperty(view, "h1", "background"); + yield performChecks(view, valueSpan); +} + +function* testComputedViewUrls(inspector) { + info("Testing tooltips in the computed view"); + + let onComputedViewReady = inspector.once("computed-view-refreshed"); + let view = selectComputedView(inspector); + yield onComputedViewReady; + + let {valueSpan} = getComputedViewProperty(view, "background-image"); + + yield performChecks(view, valueSpan); +} + +/** + * A helper that checks url() tooltips contain correct images + */ +function* performChecks(view, propertyValue) { + function checkTooltip(panel, imageSrc) { + let images = panel.getElementsByTagName("img"); + is(images.length, 1, "Tooltip contains an image"); + is(images[0].getAttribute("src"), imageSrc, "The image URL is correct"); + } + + let links = propertyValue.querySelectorAll(".theme-link"); + let panel = view.tooltips.previewTooltip.panel; + + info("Checking first link tooltip"); + yield assertHoverTooltipOn(view.tooltips.previewTooltip, links[0]); + checkTooltip(panel, YELLOW_DOT); + + info("Checking second link tooltip"); + yield assertHoverTooltipOn(view.tooltips.previewTooltip, links[1]); + checkTooltip(panel, BLUE_DOT); +} diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-shorthand-fontfamily.js b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-shorthand-fontfamily.js new file mode 100644 index 000000000..bb851ec92 --- /dev/null +++ b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-shorthand-fontfamily.js @@ -0,0 +1,58 @@ +/* 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/ */ + +"use strict"; + +// Test the fontfamily tooltip on shorthand properties + +const TEST_URI = ` + +
test element
+`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + + yield selectNode("#testElement", inspector); + yield testRuleView(view, inspector.selection.nodeFront); +}); + +function* testRuleView(ruleView, nodeFront) { + info("Testing font-family tooltips in the rule view"); + + let tooltip = ruleView.tooltips.previewTooltip; + let panel = tooltip.panel; + + // Check that the rule view has a tooltip and that a XUL panel has + // been created + ok(tooltip, "Tooltip instance exists"); + ok(panel, "XUL panel exists"); + + // Get the computed font family property inside the font rule view + let propertyList = ruleView.element + .querySelectorAll(".ruleview-propertylist"); + let fontExpander = propertyList[1].querySelectorAll(".ruleview-expander")[0]; + fontExpander.click(); + + let rule = getRuleViewRule(ruleView, "#testElement"); + let valueSpan = rule + .querySelector(".ruleview-computed .ruleview-propertyvalue"); + + // And verify that the tooltip gets shown on this property + yield assertHoverTooltipOn(tooltip, valueSpan); + + let images = panel.getElementsByTagName("img"); + is(images.length, 1, "Tooltip contains an image"); + ok(images[0].getAttribute("src") + .startsWith("data:"), "Tooltip contains a data-uri image as expected"); + + let dataURL = yield getFontFamilyDataURL(valueSpan.textContent, nodeFront); + is(images[0].getAttribute("src"), dataURL, + "Tooltip contains the correct data-uri image"); +} diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-size.js b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-size.js new file mode 100644 index 000000000..b231fe1b1 --- /dev/null +++ b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-size.js @@ -0,0 +1,86 @@ +/* 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/ */ + +"use strict"; + +// Checking tooltips dimensions, to make sure their big enough to display their +// content + +const TEST_URI = ` + +
+`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("div", inspector); + yield testImageDimension(view); + yield testPickerDimension(view); +}); + +function* testImageDimension(ruleView) { + info("Testing background-image tooltip dimensions"); + + let tooltip = ruleView.tooltips.previewTooltip; + let panel = tooltip.panel; + let {valueSpan} = getRuleViewProperty(ruleView, "div", "background"); + let uriSpan = valueSpan.querySelector(".theme-link"); + + // Make sure there is a hover tooltip for this property, this also will fill + // the tooltip with its content + yield assertHoverTooltipOn(tooltip, uriSpan); + + info("Showing the tooltip"); + let onShown = tooltip.once("shown"); + tooltip.show(uriSpan); + yield onShown; + + // Let's not test for a specific size, but instead let's make sure it's at + // least as big as the image + let imageRect = panel.querySelector("img").getBoundingClientRect(); + let panelRect = panel.getBoundingClientRect(); + + ok(panelRect.width >= imageRect.width, + "The panel is wide enough to show the image"); + ok(panelRect.height >= imageRect.height, + "The panel is high enough to show the image"); + + let onHidden = tooltip.once("hidden"); + tooltip.hide(); + yield onHidden; +} + +function* testPickerDimension(ruleView) { + info("Testing color-picker tooltip dimensions"); + + let {valueSpan} = getRuleViewProperty(ruleView, "div", "background"); + let swatch = valueSpan.querySelector(".ruleview-colorswatch"); + let cPicker = ruleView.tooltips.colorPicker; + + let onReady = cPicker.once("ready"); + swatch.click(); + yield onReady; + + // The colorpicker spectrum's iframe has a fixed width height, so let's + // make sure the tooltip is at least as big as that + let spectrumRect = cPicker.spectrum.element.getBoundingClientRect(); + let panelRect = cPicker.tooltip.container.getBoundingClientRect(); + + ok(panelRect.width >= spectrumRect.width, + "The panel is wide enough to show the picker"); + ok(panelRect.height >= spectrumRect.height, + "The panel is high enough to show the picker"); + + let onHidden = cPicker.tooltip.once("hidden"); + let onRuleViewChanged = ruleView.once("ruleview-changed"); + cPicker.hide(); + yield onHidden; + yield onRuleViewChanged; +} diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-01.js b/devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-01.js new file mode 100644 index 000000000..68a91ff95 --- /dev/null +++ b/devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-01.js @@ -0,0 +1,48 @@ +/* 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/ */ + +"use strict"; + +// Test that the css transform highlighter is created only when asked + +const TEST_URI = ` + + Test the css transform highlighter +`; + +const TYPE = "CssTransformHighlighter"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + + let overlay = view.highlighters; + + ok(!overlay.highlighters[TYPE], "No highlighter exists in the rule-view"); + let h = yield overlay._getHighlighter(TYPE); + ok(overlay.highlighters[TYPE], + "The highlighter has been created in the rule-view"); + is(h, overlay.highlighters[TYPE], "The right highlighter has been created"); + let h2 = yield overlay._getHighlighter(TYPE); + is(h, h2, + "The same instance of highlighter is returned everytime in the rule-view"); + + let onComputedViewReady = inspector.once("computed-view-refreshed"); + let cView = selectComputedView(inspector); + yield onComputedViewReady; + overlay = cView.highlighters; + + ok(!overlay.highlighters[TYPE], "No highlighter exists in the computed-view"); + h = yield overlay._getHighlighter(TYPE); + ok(overlay.highlighters[TYPE], + "The highlighter has been created in the computed-view"); + is(h, overlay.highlighters[TYPE], "The right highlighter has been created"); + h2 = yield overlay._getHighlighter(TYPE); + is(h, h2, "The same instance of highlighter is returned everytime " + + "in the computed-view"); +}); diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-02.js b/devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-02.js new file mode 100644 index 000000000..a44a31422 --- /dev/null +++ b/devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-02.js @@ -0,0 +1,57 @@ +/* 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/ */ + +"use strict"; + +// Test that the css transform highlighter is created when hovering over a +// transform property + +const TEST_URI = ` + + Test the css transform highlighter +`; + +var TYPE = "CssTransformHighlighter"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + let hs = view.highlighters; + + ok(!hs.highlighters[TYPE], "No highlighter exists in the rule-view (1)"); + + info("Faking a mousemove on a non-transform property"); + let {valueSpan} = getRuleViewProperty(view, "body", "color"); + hs._onMouseMove({target: valueSpan}); + ok(!hs.highlighters[TYPE], "No highlighter exists in the rule-view (2)"); + + info("Faking a mousemove on a transform property"); + ({valueSpan} = getRuleViewProperty(view, "body", "transform")); + let onHighlighterShown = hs.once("highlighter-shown"); + hs._onMouseMove({target: valueSpan}); + yield onHighlighterShown; + + let onComputedViewReady = inspector.once("computed-view-refreshed"); + let cView = selectComputedView(inspector); + yield onComputedViewReady; + hs = cView.highlighters; + + ok(!hs.highlighters[TYPE], "No highlighter exists in the computed-view (1)"); + + info("Faking a mousemove on a non-transform property"); + ({valueSpan} = getComputedViewProperty(cView, "color")); + hs._onMouseMove({target: valueSpan}); + ok(!hs.highlighters[TYPE], "No highlighter exists in the computed-view (2)"); + + info("Faking a mousemove on a transform property"); + ({valueSpan} = getComputedViewProperty(cView, "transform")); + onHighlighterShown = hs.once("highlighter-shown"); + hs._onMouseMove({target: valueSpan}); + yield onHighlighterShown; +}); diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-03.js b/devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-03.js new file mode 100644 index 000000000..1ecdf279e --- /dev/null +++ b/devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-03.js @@ -0,0 +1,103 @@ +/* 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/ */ + +"use strict"; + +// Test that the css transform highlighter is shown when hovering over transform +// properties + +// Note that in this test, we mock the highlighter front, merely testing the +// behavior of the style-inspector UI for now + +const TEST_URI = ` + + Test the css transform highlighter +`; + +const TYPE = "CssTransformHighlighter"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + + // Mock the highlighter front to get the reference of the NodeFront + let HighlighterFront = { + isShown: false, + nodeFront: null, + nbOfTimesShown: 0, + show: function (nodeFront) { + this.nodeFront = nodeFront; + this.isShown = true; + this.nbOfTimesShown ++; + return promise.resolve(true); + }, + hide: function () { + this.nodeFront = null; + this.isShown = false; + return promise.resolve(); + }, + finalize: function () {} + }; + + // Inject the mock highlighter in the rule-view + let hs = view.highlighters; + hs.highlighters[TYPE] = HighlighterFront; + + let {valueSpan} = getRuleViewProperty(view, "body", "transform"); + + info("Checking that the HighlighterFront's show/hide methods are called"); + let onHighlighterShown = hs.once("highlighter-shown"); + hs._onMouseMove({target: valueSpan}); + yield onHighlighterShown; + ok(HighlighterFront.isShown, "The highlighter is shown"); + let onHighlighterHidden = hs.once("highlighter-hidden"); + hs._onMouseOut(); + yield onHighlighterHidden; + ok(!HighlighterFront.isShown, "The highlighter is hidden"); + + info("Checking that hovering several times over the same property doesn't" + + " show the highlighter several times"); + let nb = HighlighterFront.nbOfTimesShown; + onHighlighterShown = hs.once("highlighter-shown"); + hs._onMouseMove({target: valueSpan}); + yield onHighlighterShown; + is(HighlighterFront.nbOfTimesShown, nb + 1, "The highlighter was shown once"); + hs._onMouseMove({target: valueSpan}); + hs._onMouseMove({target: valueSpan}); + is(HighlighterFront.nbOfTimesShown, nb + 1, + "The highlighter was shown once, after several mousemove"); + + info("Checking that the right NodeFront reference is passed"); + yield selectNode("html", inspector); + ({valueSpan} = getRuleViewProperty(view, "html", "transform")); + onHighlighterShown = hs.once("highlighter-shown"); + hs._onMouseMove({target: valueSpan}); + yield onHighlighterShown; + is(HighlighterFront.nodeFront.tagName, "HTML", + "The right NodeFront is passed to the highlighter (1)"); + + yield selectNode("body", inspector); + ({valueSpan} = getRuleViewProperty(view, "body", "transform")); + onHighlighterShown = hs.once("highlighter-shown"); + hs._onMouseMove({target: valueSpan}); + yield onHighlighterShown; + is(HighlighterFront.nodeFront.tagName, "BODY", + "The right NodeFront is passed to the highlighter (2)"); + + info("Checking that the highlighter gets hidden when hovering a " + + "non-transform property"); + ({valueSpan} = getRuleViewProperty(view, "body", "color")); + onHighlighterHidden = hs.once("highlighter-hidden"); + hs._onMouseMove({target: valueSpan}); + yield onHighlighterHidden; + ok(!HighlighterFront.isShown, "The highlighter is hidden"); +}); diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-04.js b/devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-04.js new file mode 100644 index 000000000..9d81e2649 --- /dev/null +++ b/devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-04.js @@ -0,0 +1,60 @@ +/* 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/ */ + +"use strict"; + +// Test that the css transform highlighter is shown only when hovering over a +// transform declaration that isn't overriden or disabled + +// Note that unlike the other browser_styleinspector_transform-highlighter-N.js +// tests, this one only tests the rule-view as only this view features disabled +// and overriden properties + +const TEST_URI = ` + +
+`; + +const TYPE = "CssTransformHighlighter"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode(".test", inspector); + + let hs = view.highlighters; + + info("Faking a mousemove on the overriden property"); + let {valueSpan} = getRuleViewProperty(view, "div", "transform"); + hs._onMouseMove({target: valueSpan}); + ok(!hs.highlighters[TYPE], + "No highlighter was created for the overriden property"); + + info("Disabling the applied property"); + let classRuleEditor = getRuleViewRuleEditor(view, 1); + let propEditor = classRuleEditor.rule.textProps[0].editor; + propEditor.enable.click(); + yield classRuleEditor.rule._applyingModifications; + + info("Faking a mousemove on the disabled property"); + ({valueSpan} = getRuleViewProperty(view, ".test", "transform")); + hs._onMouseMove({target: valueSpan}); + ok(!hs.highlighters[TYPE], + "No highlighter was created for the disabled property"); + + info("Faking a mousemove on the now unoverriden property"); + ({valueSpan} = getRuleViewProperty(view, "div", "transform")); + let onHighlighterShown = hs.once("highlighter-shown"); + hs._onMouseMove({target: valueSpan}); + yield onHighlighterShown; +}); diff --git a/devtools/client/inspector/shared/test/doc_author-sheet.html b/devtools/client/inspector/shared/test/doc_author-sheet.html new file mode 100644 index 000000000..d611bb387 --- /dev/null +++ b/devtools/client/inspector/shared/test/doc_author-sheet.html @@ -0,0 +1,37 @@ + + + + authored sheet test + + + + +
the ocean
+ + + + + +
+
+      inspect user agent styles
+    
+
+ + diff --git a/devtools/client/inspector/shared/test/doc_content_stylesheet.html b/devtools/client/inspector/shared/test/doc_content_stylesheet.html new file mode 100644 index 000000000..f9b52f78d --- /dev/null +++ b/devtools/client/inspector/shared/test/doc_content_stylesheet.html @@ -0,0 +1,32 @@ + + + test + + + + + + + + + +
+

Simple test

+
+ + diff --git a/devtools/client/inspector/shared/test/doc_content_stylesheet.xul b/devtools/client/inspector/shared/test/doc_content_stylesheet.xul new file mode 100644 index 000000000..efd53815d --- /dev/null +++ b/devtools/client/inspector/shared/test/doc_content_stylesheet.xul @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/devtools/client/inspector/shared/test/doc_content_stylesheet_imported.css b/devtools/client/inspector/shared/test/doc_content_stylesheet_imported.css new file mode 100644 index 000000000..ea1a3d986 --- /dev/null +++ b/devtools/client/inspector/shared/test/doc_content_stylesheet_imported.css @@ -0,0 +1,5 @@ +@import url("./doc_content_stylesheet_imported2.css"); + +#target { + text-decoration: underline; +} diff --git a/devtools/client/inspector/shared/test/doc_content_stylesheet_imported2.css b/devtools/client/inspector/shared/test/doc_content_stylesheet_imported2.css new file mode 100644 index 000000000..77c73299e --- /dev/null +++ b/devtools/client/inspector/shared/test/doc_content_stylesheet_imported2.css @@ -0,0 +1,3 @@ +#target { + text-decoration: underline; +} diff --git a/devtools/client/inspector/shared/test/doc_content_stylesheet_linked.css b/devtools/client/inspector/shared/test/doc_content_stylesheet_linked.css new file mode 100644 index 000000000..712ba78fb --- /dev/null +++ b/devtools/client/inspector/shared/test/doc_content_stylesheet_linked.css @@ -0,0 +1,3 @@ +table { + border-collapse: collapse; +} diff --git a/devtools/client/inspector/shared/test/doc_content_stylesheet_script.css b/devtools/client/inspector/shared/test/doc_content_stylesheet_script.css new file mode 100644 index 000000000..5aa5e2c6c --- /dev/null +++ b/devtools/client/inspector/shared/test/doc_content_stylesheet_script.css @@ -0,0 +1,5 @@ +@import url("./doc_content_stylesheet_imported.css"); + +table { + opacity: 1; +} diff --git a/devtools/client/inspector/shared/test/doc_content_stylesheet_xul.css b/devtools/client/inspector/shared/test/doc_content_stylesheet_xul.css new file mode 100644 index 000000000..a14ae7f6f --- /dev/null +++ b/devtools/client/inspector/shared/test/doc_content_stylesheet_xul.css @@ -0,0 +1,3 @@ +#target { + font-size: 200px; +} diff --git a/devtools/client/inspector/shared/test/doc_frame_script.js b/devtools/client/inspector/shared/test/doc_frame_script.js new file mode 100644 index 000000000..aeb73a115 --- /dev/null +++ b/devtools/client/inspector/shared/test/doc_frame_script.js @@ -0,0 +1,115 @@ +/* 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/ */ +/* globals addMessageListener, sendAsyncMessage */ + +"use strict"; + +// A helper frame-script for brower/devtools/styleinspector tests. +// +// Most listeners in the script expect "Test:"-namespaced messages from chrome, +// then execute code upon receiving, and immediately send back a message. +// This is so that chrome test code can execute code in content and wait for a +// response this way: +// let response = yield executeInContent(browser, "Test:MsgName", data, true); +// The response message should have the same name "Test:MsgName" +// +// Some listeners do not send a response message back. + +var {utils: Cu} = Components; +var {require} = Cu.import("resource://devtools/shared/Loader.jsm", {}); +var defer = require("devtools/shared/defer"); + +/** + * Get a value for a given property name in a css rule in a stylesheet, given + * their indexes + * @param {Object} data Expects a data object with the following properties + * - {Number} styleSheetIndex + * - {Number} ruleIndex + * - {String} name + * @return {String} The value, if found, null otherwise + */ +addMessageListener("Test:GetRulePropertyValue", function (msg) { + let {name, styleSheetIndex, ruleIndex} = msg.data; + let value = null; + + dumpn("Getting the value for property name " + name + " in sheet " + + styleSheetIndex + " and rule " + ruleIndex); + + let sheet = content.document.styleSheets[styleSheetIndex]; + if (sheet) { + let rule = sheet.cssRules[ruleIndex]; + if (rule) { + value = rule.style.getPropertyValue(name); + } + } + + sendAsyncMessage("Test:GetRulePropertyValue", value); +}); + +/** + * Get the property value from the computed style for an element. + * @param {Object} data Expects a data object with the following properties + * - {String} selector: The selector used to obtain the element. + * - {String} pseudo: pseudo id to query, or null. + * - {String} name: name of the property + * @return {String} The value, if found, null otherwise + */ +addMessageListener("Test:GetComputedStylePropertyValue", function (msg) { + let {selector, pseudo, name} = msg.data; + let doc = content.document; + + let element = doc.querySelector(selector); + let value = content.getComputedStyle(element, pseudo).getPropertyValue(name); + sendAsyncMessage("Test:GetComputedStylePropertyValue", value); +}); + +/** + * Wait the property value from the computed style for an element and + * compare it with the expected value + * @param {Object} data Expects a data object with the following properties + * - {String} selector: The selector used to obtain the element. + * - {String} pseudo: pseudo id to query, or null. + * - {String} name: name of the property + * - {String} expected: the expected value for property + */ +addMessageListener("Test:WaitForComputedStylePropertyValue", function (msg) { + let {selector, pseudo, name, expected} = msg.data; + let element = content.document.querySelector(selector); + waitForSuccess(() => { + let value = content.document.defaultView.getComputedStyle(element, pseudo) + .getPropertyValue(name); + + return value === expected; + }).then(() => { + sendAsyncMessage("Test:WaitForComputedStylePropertyValue"); + }); +}); + +var dumpn = msg => dump(msg + "\n"); + +/** + * Polls a given function waiting for it to return true. + * + * @param {Function} validatorFn A validator function that returns a boolean. + * This is called every few milliseconds to check if the result is true. When + * it is true, the promise resolves. + * @param {String} name Optional name of the test. This is used to generate + * the success and failure messages. + * @return a promise that resolves when the function returned true or rejects + * if the timeout is reached + */ +function waitForSuccess(validatorFn, name = "untitled") { + let def = defer(); + + function wait(fn) { + if (fn()) { + def.resolve(); + } else { + setTimeout(() => wait(fn), 200); + } + } + wait(validatorFn); + + return def.promise; +} diff --git a/devtools/client/inspector/shared/test/head.js b/devtools/client/inspector/shared/test/head.js new file mode 100644 index 000000000..bcc2ec2c7 --- /dev/null +++ b/devtools/client/inspector/shared/test/head.js @@ -0,0 +1,557 @@ +/* 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 ../../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); + +var {CssRuleView} = require("devtools/client/inspector/rules/rules"); +var {getInplaceEditorForSpan: inplaceEditor} = + require("devtools/client/shared/inplace-editor"); +const {getColor: getThemeColor} = require("devtools/client/shared/theme"); + +const TEST_URL_ROOT = + "http://example.com/browser/devtools/client/inspector/shared/test/"; +const TEST_URL_ROOT_SSL = + "https://example.com/browser/devtools/client/inspector/shared/test/"; +const ROOT_TEST_DIR = getRootDirectory(gTestPath); +const FRAME_SCRIPT_URL = ROOT_TEST_DIR + "doc_frame_script.js"; +const STYLE_INSPECTOR_L10N = + new LocalizationHelper("devtools/shared/locales/styleinspector.properties"); + +// Clean-up all prefs that might have been changed during a test run +// (safer here because if the test fails, then the pref is never reverted) +registerCleanupFunction(() => { + Services.prefs.clearUserPref("devtools.defaultColorUnit"); +}); + +/** + * The functions found below are here to ease test development and maintenance. + * Most of these functions are stateless and will require some form of context + * (the instance of the current toolbox, or inspector panel for instance). + * + * Most of these functions are async too and return promises. + * + * All tests should follow the following pattern: + * + * add_task(function*() { + * yield addTab(TEST_URI); + * let {toolbox, inspector} = yield openInspector(); + * inspector.sidebar.select(viewId); + * let view = inspector[viewId].view; + * yield selectNode("#test", inspector); + * yield someAsyncTestFunction(view); + * }); + * + * add_task is the way to define the testcase in the test file. It accepts + * a single generator-function argument. + * The generator function should yield any async call. + * + * There is no need to clean tabs up at the end of a test as this is done + * automatically. + * + * It is advised not to store any references on the global scope. There + * shouldn't be a need to anyway. Thanks to add_task, test steps, even + * though asynchronous, can be described in a nice flat way, and + * if/for/while/... control flow can be used as in sync code, making it + * possible to write the outline of the test case all in add_task, and delegate + * actual processing and assertions to other functions. + */ + +/* ********************************************* + * UTILS + * ********************************************* + * General test utilities. + * Add new tabs, open the toolbox and switch to the various panels, select + * nodes, get node references, ... + */ + +/** + * The rule-view tests rely on a frame-script to be injected in the content test + * page. So override the shared-head's addTab to load the frame script after the + * tab was added. + * FIXME: Refactor the rule-view tests to use the testActor instead of a frame + * script, so they can run on remote targets too. + */ +var _addTab = addTab; +addTab = function (url) { + return _addTab(url).then(tab => { + info("Loading the helper frame script " + FRAME_SCRIPT_URL); + let browser = tab.linkedBrowser; + browser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false); + return tab; + }); +}; + +/** + * Wait for a content -> chrome message on the message manager (the window + * messagemanager is used). + * + * @param {String} name + * The message name + * @return {Promise} A promise that resolves to the response data when the + * message has been received + */ +function waitForContentMessage(name) { + info("Expecting message " + name + " from content"); + + let mm = gBrowser.selectedBrowser.messageManager; + + let def = defer(); + mm.addMessageListener(name, function onMessage(msg) { + mm.removeMessageListener(name, onMessage); + def.resolve(msg.data); + }); + return def.promise; +} + +/** + * Send an async message to the frame script (chrome -> content) and wait for a + * response message with the same name (content -> chrome). + * + * @param {String} name + * The message name. Should be one of the messages defined + * in doc_frame_script.js + * @param {Object} data + * Optional data to send along + * @param {Object} objects + * Optional CPOW objects to send along + * @param {Boolean} expectResponse + * If set to false, don't wait for a response with the same name + * from the content script. Defaults to true. + * @return {Promise} Resolves to the response data if a response is expected, + * immediately resolves otherwise + */ +function executeInContent(name, data = {}, objects = {}, + expectResponse = true) { + info("Sending message " + name + " to content"); + let mm = gBrowser.selectedBrowser.messageManager; + + mm.sendAsyncMessage(name, data, objects); + if (expectResponse) { + return waitForContentMessage(name); + } + + return promise.resolve(); +} + +/** + * Send an async message to the frame script and get back the requested + * computed style property. + * + * @param {String} selector + * The selector used to obtain the element. + * @param {String} pseudo + * pseudo id to query, or null. + * @param {String} name + * name of the property. + */ +function* getComputedStyleProperty(selector, pseudo, propName) { + return yield executeInContent("Test:GetComputedStylePropertyValue", + {selector, + pseudo, + name: propName}); +} + +/** + * Send an async message to the frame script and wait until the requested + * computed style property has the expected value. + * + * @param {String} selector + * The selector used to obtain the element. + * @param {String} pseudo + * pseudo id to query, or null. + * @param {String} prop + * name of the property. + * @param {String} expected + * expected value of property + * @param {String} name + * the name used in test message + */ +function* waitForComputedStyleProperty(selector, pseudo, name, expected) { + return yield executeInContent("Test:WaitForComputedStylePropertyValue", + {selector, + pseudo, + expected, + name}); +} + +/** + * Given an inplace editable element, click to switch it to edit mode, wait for + * focus + * + * @return a promise that resolves to the inplace-editor element when ready + */ +var focusEditableField = Task.async(function* (ruleView, editable, xOffset = 1, + yOffset = 1, options = {}) { + let onFocus = once(editable.parentNode, "focus", true); + info("Clicking on editable field to turn to edit mode"); + EventUtils.synthesizeMouse(editable, xOffset, yOffset, options, + editable.ownerDocument.defaultView); + yield onFocus; + + info("Editable field gained focus, returning the input field now"); + let onEdit = inplaceEditor(editable.ownerDocument.activeElement); + + return onEdit; +}); + +/** + * Polls a given function waiting for it to return true. + * + * @param {Function} validatorFn + * A validator function that returns a boolean. + * This is called every few milliseconds to check if the result is true. + * When it is true, the promise resolves. + * @param {String} name + * Optional name of the test. This is used to generate + * the success and failure messages. + * @return a promise that resolves when the function returned true or rejects + * if the timeout is reached + */ +function waitForSuccess(validatorFn, name = "untitled") { + let def = defer(); + + function wait(validator) { + if (validator()) { + ok(true, "Validator function " + name + " returned true"); + def.resolve(); + } else { + setTimeout(() => wait(validator), 200); + } + } + wait(validatorFn); + + return def.promise; +} + +/** + * Get the dataURL for the font family tooltip. + * + * @param {String} font + * The font family value. + * @param {object} nodeFront + * The NodeActor that will used to retrieve the dataURL for the + * font family tooltip contents. + */ +var getFontFamilyDataURL = Task.async(function* (font, nodeFront) { + let fillStyle = getThemeColor("body-color"); + + let {data} = yield nodeFront.getFontFamilyDataURL(font, fillStyle); + let dataURL = yield data.string(); + return dataURL; +}); + +/* ********************************************* + * RULE-VIEW + * ********************************************* + * Rule-view related test utility functions + * This object contains functions to get rules, get properties, ... + */ + +/** + * Get the DOMNode for a css rule in the rule-view that corresponds to the given + * selector + * + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {String} selectorText + * The selector in the rule-view for which the rule + * object is wanted + * @return {DOMNode} + */ +function getRuleViewRule(view, selectorText) { + let rule; + for (let r of view.styleDocument.querySelectorAll(".ruleview-rule")) { + let selector = r.querySelector(".ruleview-selectorcontainer, " + + ".ruleview-selector-matched"); + if (selector && selector.textContent === selectorText) { + rule = r; + break; + } + } + + return rule; +} + +/** + * Get references to the name and value span nodes corresponding to a given + * selector and property name in the rule-view + * + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {String} selectorText + * The selector in the rule-view to look for the property in + * @param {String} propertyName + * The name of the property + * @return {Object} An object like {nameSpan: DOMNode, valueSpan: DOMNode} + */ +function getRuleViewProperty(view, selectorText, propertyName) { + let prop; + + let rule = getRuleViewRule(view, selectorText); + if (rule) { + // Look for the propertyName in that rule element + for (let p of rule.querySelectorAll(".ruleview-property")) { + let nameSpan = p.querySelector(".ruleview-propertyname"); + let valueSpan = p.querySelector(".ruleview-propertyvalue"); + + if (nameSpan.textContent === propertyName) { + prop = {nameSpan: nameSpan, valueSpan: valueSpan}; + break; + } + } + } + return prop; +} + +/** + * Get the text value of the property corresponding to a given selector and name + * in the rule-view + * + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {String} selectorText + * The selector in the rule-view to look for the property in + * @param {String} propertyName + * The name of the property + * @return {String} The property value + */ +function getRuleViewPropertyValue(view, selectorText, propertyName) { + return getRuleViewProperty(view, selectorText, propertyName) + .valueSpan.textContent; +} + +/** + * Get a reference to the selector DOM element corresponding to a given selector + * in the rule-view + * + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {String} selectorText + * The selector in the rule-view to look for + * @return {DOMNode} The selector DOM element + */ +function getRuleViewSelector(view, selectorText) { + let rule = getRuleViewRule(view, selectorText); + return rule.querySelector(".ruleview-selector, .ruleview-selector-matched"); +} + +/** + * Get a reference to the selectorhighlighter icon DOM element corresponding to + * a given selector in the rule-view + * + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {String} selectorText + * The selector in the rule-view to look for + * @return {DOMNode} The selectorhighlighter icon DOM element + */ +function getRuleViewSelectorHighlighterIcon(view, selectorText) { + let rule = getRuleViewRule(view, selectorText); + return rule.querySelector(".ruleview-selectorhighlighter"); +} + +/** + * Simulate a color change in a given color picker tooltip, and optionally wait + * for a given element in the page to have its style changed as a result + * + * @param {RuleView} ruleView + * The related rule view instance + * @param {SwatchColorPickerTooltip} colorPicker + * @param {Array} newRgba + * The new color to be set [r, g, b, a] + * @param {Object} expectedChange + * Optional object that needs the following props: + * - {DOMNode} element The element in the page that will have its + * style changed. + * - {String} name The style name that will be changed + * - {String} value The expected style value + * The style will be checked like so: getComputedStyle(element)[name] === value + */ +var simulateColorPickerChange = Task.async(function* (ruleView, colorPicker, + newRgba, expectedChange) { + let onRuleViewChanged = ruleView.once("ruleview-changed"); + info("Getting the spectrum colorpicker object"); + let spectrum = yield colorPicker.spectrum; + info("Setting the new color"); + spectrum.rgb = newRgba; + info("Applying the change"); + spectrum.updateUI(); + spectrum.onChange(); + info("Waiting for rule-view to update"); + yield onRuleViewChanged; + + if (expectedChange) { + info("Waiting for the style to be applied on the page"); + yield waitForSuccess(() => { + let {element, name, value} = expectedChange; + return content.getComputedStyle(element)[name] === value; + }, "Color picker change applied on the page"); + } +}); + +/** + * Get a rule-link from the rule-view given its index + * + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {Number} index + * The index of the link to get + * @return {DOMNode} The link if any at this index + */ +function getRuleViewLinkByIndex(view, index) { + let links = view.styleDocument.querySelectorAll(".ruleview-rule-source"); + return links[index]; +} + +/** + * Get rule-link text from the rule-view given its index + * + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {Number} index + * The index of the link to get + * @return {String} The string at this index + */ +function getRuleViewLinkTextByIndex(view, index) { + let link = getRuleViewLinkByIndex(view, index); + return link.querySelector(".ruleview-rule-source-label").textContent; +} + +/** + * Click on a rule-view's close brace to focus a new property name editor + * + * @param {RuleEditor} ruleEditor + * An instance of RuleEditor that will receive the new property + * @return a promise that resolves to the newly created editor when ready and + * focused + */ +var focusNewRuleViewProperty = Task.async(function* (ruleEditor) { + info("Clicking on a close ruleEditor brace to start editing a new property"); + ruleEditor.closeBrace.scrollIntoView(); + let editor = yield focusEditableField(ruleEditor.ruleView, + ruleEditor.closeBrace); + + is(inplaceEditor(ruleEditor.newPropSpan), editor, + "Focused editor is the new property editor."); + + return editor; +}); + +/** + * Create a new property name in the rule-view, focusing a new property editor + * by clicking on the close brace, and then entering the given text. + * Keep in mind that the rule-view knows how to handle strings with multiple + * properties, so the input text may be like: "p1:v1;p2:v2;p3:v3". + * + * @param {RuleEditor} ruleEditor + * The instance of RuleEditor that will receive the new property(ies) + * @param {String} inputValue + * The text to be entered in the new property name field + * @return a promise that resolves when the new property name has been entered + * and once the value field is focused + */ +var createNewRuleViewProperty = Task.async(function* (ruleEditor, inputValue) { + info("Creating a new property editor"); + let editor = yield focusNewRuleViewProperty(ruleEditor); + + info("Entering the value " + inputValue); + editor.input.value = inputValue; + + info("Submitting the new value and waiting for value field focus"); + let onFocus = once(ruleEditor.element, "focus", true); + EventUtils.synthesizeKey("VK_RETURN", {}, + ruleEditor.element.ownerDocument.defaultView); + yield onFocus; +}); + +/** + * Set the search value for the rule-view filter styles search box. + * + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {String} searchValue + * The filter search value + * @return a promise that resolves when the rule-view is filtered for the + * search term + */ +var setSearchFilter = Task.async(function* (view, searchValue) { + info("Setting filter text to \"" + searchValue + "\""); + let win = view.styleWindow; + let searchField = view.searchField; + searchField.focus(); + synthesizeKeys(searchValue, win); + yield view.inspector.once("ruleview-filtered"); +}); + +/* ********************************************* + * COMPUTED-VIEW + * ********************************************* + * Computed-view related utility functions. + * Allows to get properties, links, expand properties, ... + */ + +/** + * Get references to the name and value span nodes corresponding to a given + * property name in the computed-view + * + * @param {CssComputedView} view + * The instance of the computed view panel + * @param {String} name + * The name of the property to retrieve + * @return an object {nameSpan, valueSpan} + */ +function getComputedViewProperty(view, name) { + let prop; + for (let property of view.styleDocument.querySelectorAll(".property-view")) { + let nameSpan = property.querySelector(".property-name"); + let valueSpan = property.querySelector(".property-value"); + + if (nameSpan.textContent === name) { + prop = {nameSpan: nameSpan, valueSpan: valueSpan}; + break; + } + } + return prop; +} + +/** + * Get the text value of the property corresponding to a given name in the + * computed-view + * + * @param {CssComputedView} view + * The instance of the computed view panel + * @param {String} name + * The name of the property to retrieve + * @return {String} The property value + */ +function getComputedViewPropertyValue(view, name, propertyName) { + return getComputedViewProperty(view, name, propertyName) + .valueSpan.textContent; +} + +/** + * Open the style editor context menu and return all of it's items in a flat array + * @param {CssRuleView} view + * The instance of the rule-view panel + * @return An array of MenuItems + */ +function openStyleContextMenuAndGetAllItems(view, target) { + let menu = view._contextmenu._openMenu({target: target}); + + // Flatten all menu items into a single array to make searching through it easier + let allItems = [].concat.apply([], menu.items.map(function addItem(item) { + if (item.submenu) { + return addItem(item.submenu.items); + } + return item; + })); + + return allItems; +} diff --git a/devtools/client/inspector/shared/tooltips-overlay.js b/devtools/client/inspector/shared/tooltips-overlay.js new file mode 100644 index 000000000..8a02d7e3d --- /dev/null +++ b/devtools/client/inspector/shared/tooltips-overlay.js @@ -0,0 +1,319 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set 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"; + +/** + * The tooltip overlays are tooltips that appear when hovering over property values and + * editor tooltips that appear when clicking swatch based editors. + */ + +const { Task } = require("devtools/shared/task"); +const Services = require("Services"); +const { + VIEW_NODE_VALUE_TYPE, + VIEW_NODE_IMAGE_URL_TYPE, +} = require("devtools/client/inspector/shared/node-types"); +const { getColor } = require("devtools/client/shared/theme"); +const { getCssProperties } = require("devtools/shared/fronts/css-properties"); +const CssDocsTooltip = require("devtools/client/shared/widgets/tooltip/CssDocsTooltip"); +const { HTMLTooltip } = require("devtools/client/shared/widgets/tooltip/HTMLTooltip"); +const { + getImageDimensions, + setImageTooltip, + setBrokenImageTooltip, +} = require("devtools/client/shared/widgets/tooltip/ImageTooltipHelper"); +const SwatchColorPickerTooltip = require("devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip"); +const SwatchCubicBezierTooltip = require("devtools/client/shared/widgets/tooltip/SwatchCubicBezierTooltip"); +const SwatchFilterTooltip = require("devtools/client/shared/widgets/tooltip/SwatchFilterTooltip"); + +const PREF_IMAGE_TOOLTIP_SIZE = "devtools.inspector.imagePreviewTooltipSize"; + +// Types of existing tooltips +const TOOLTIP_IMAGE_TYPE = "image"; +const TOOLTIP_FONTFAMILY_TYPE = "font-family"; + +/** + * Manages all tooltips in the style-inspector. + * + * @param {CssRuleView|CssComputedView} view + * Either the rule-view or computed-view panel + */ +function TooltipsOverlay(view) { + this.view = view; + + let {CssRuleView} = require("devtools/client/inspector/rules/rules"); + this.isRuleView = view instanceof CssRuleView; + this._cssProperties = getCssProperties(this.view.inspector.toolbox); + + this._onNewSelection = this._onNewSelection.bind(this); + this.view.inspector.selection.on("new-node-front", this._onNewSelection); +} + +TooltipsOverlay.prototype = { + get isEditing() { + return this.colorPicker.tooltip.isVisible() || + this.colorPicker.eyedropperOpen || + this.cubicBezier.tooltip.isVisible() || + this.filterEditor.tooltip.isVisible(); + }, + + /** + * Add the tooltips overlay to the view. This will start tracking mouse + * movements and display tooltips when needed + */ + addToView: function () { + if (this._isStarted || this._isDestroyed) { + return; + } + + let { toolbox } = this.view.inspector; + + // Initializing the different tooltips that are used in the inspector. + // These tooltips are attached to the toolbox document if they require a popup panel. + // Otherwise, it is attached to the inspector panel document if it is an inline + // editor. + this.previewTooltip = new HTMLTooltip(toolbox.doc, { + type: "arrow", + useXulWrapper: true + }); + this.previewTooltip.startTogglingOnHover(this.view.element, + this._onPreviewTooltipTargetHover.bind(this)); + + // MDN CSS help tooltip + this.cssDocs = new CssDocsTooltip(toolbox.doc); + + if (this.isRuleView) { + // Color picker tooltip + this.colorPicker = new SwatchColorPickerTooltip(toolbox.doc, this.view.inspector); + // Cubic bezier tooltip + this.cubicBezier = new SwatchCubicBezierTooltip(toolbox.doc); + // Filter editor tooltip + this.filterEditor = new SwatchFilterTooltip(toolbox.doc, + this._cssProperties.getValidityChecker(this.view.inspector.panelDoc)); + } + + this._isStarted = true; + }, + + /** + * Remove the tooltips overlay from the view. This will stop tracking mouse + * movements and displaying tooltips + */ + removeFromView: function () { + if (!this._isStarted || this._isDestroyed) { + return; + } + + this.previewTooltip.stopTogglingOnHover(this.view.element); + this.previewTooltip.destroy(); + + if (this.colorPicker) { + this.colorPicker.destroy(); + } + + if (this.cubicBezier) { + this.cubicBezier.destroy(); + } + + if (this.cssDocs) { + this.cssDocs.destroy(); + } + + if (this.filterEditor) { + this.filterEditor.destroy(); + } + + this._isStarted = false; + }, + + /** + * Given a hovered node info, find out which type of tooltip should be shown, + * if any + * + * @param {Object} nodeInfo + * @return {String} The tooltip type to be shown, or null + */ + _getTooltipType: function ({type, value: prop}) { + let tooltipType = null; + let inspector = this.view.inspector; + + // Image preview tooltip + if (type === VIEW_NODE_IMAGE_URL_TYPE && + inspector.hasUrlToImageDataResolver) { + tooltipType = TOOLTIP_IMAGE_TYPE; + } + + // Font preview tooltip + if (type === VIEW_NODE_VALUE_TYPE && prop.property === "font-family") { + let value = prop.value.toLowerCase(); + if (value !== "inherit" && value !== "unset" && value !== "initial") { + tooltipType = TOOLTIP_FONTFAMILY_TYPE; + } + } + + return tooltipType; + }, + + /** + * Executed by the tooltip when the pointer hovers over an element of the + * view. Used to decide whether the tooltip should be shown or not and to + * actually put content in it. + * Checks if the hovered target is a css value we support tooltips for. + * + * @param {DOMNode} target The currently hovered node + * @return {Promise} + */ + _onPreviewTooltipTargetHover: Task.async(function* (target) { + let nodeInfo = this.view.getNodeInfo(target); + if (!nodeInfo) { + // The hovered node isn't something we care about + return false; + } + + let type = this._getTooltipType(nodeInfo); + if (!type) { + // There is no tooltip type defined for the hovered node + return false; + } + + if (this.isRuleView && this.colorPicker.tooltip.isVisible()) { + this.colorPicker.revert(); + this.colorPicker.hide(); + } + + if (this.isRuleView && this.cubicBezier.tooltip.isVisible()) { + this.cubicBezier.revert(); + this.cubicBezier.hide(); + } + + if (this.isRuleView && this.cssDocs.tooltip.isVisible()) { + this.cssDocs.hide(); + } + + if (this.isRuleView && this.filterEditor.tooltip.isVisible()) { + this.filterEditor.revert(); + this.filterEdtior.hide(); + } + + let inspector = this.view.inspector; + + if (type === TOOLTIP_IMAGE_TYPE) { + try { + yield this._setImagePreviewTooltip(nodeInfo.value.url); + } catch (e) { + yield setBrokenImageTooltip(this.previewTooltip, this.view.inspector.panelDoc); + } + return true; + } + + if (type === TOOLTIP_FONTFAMILY_TYPE) { + let font = nodeInfo.value.value; + let nodeFront = inspector.selection.nodeFront; + yield this._setFontPreviewTooltip(font, nodeFront); + return true; + } + + return false; + }), + + /** + * Set the content of the preview tooltip to display an image preview. The image URL can + * be relative, a call will be made to the debuggee to retrieve the image content as an + * imageData URI. + * + * @param {String} imageUrl + * The image url value (may be relative or absolute). + * @return {Promise} A promise that resolves when the preview tooltip content is ready + */ + _setImagePreviewTooltip: Task.async(function* (imageUrl) { + let doc = this.view.inspector.panelDoc; + let maxDim = Services.prefs.getIntPref(PREF_IMAGE_TOOLTIP_SIZE); + + let naturalWidth, naturalHeight; + if (imageUrl.startsWith("data:")) { + // If the imageUrl already is a data-url, save ourselves a round-trip + let size = yield getImageDimensions(doc, imageUrl); + naturalWidth = size.naturalWidth; + naturalHeight = size.naturalHeight; + } else { + let inspectorFront = this.view.inspector.inspector; + let {data, size} = yield inspectorFront.getImageDataFromURL(imageUrl, maxDim); + imageUrl = yield data.string(); + naturalWidth = size.naturalWidth; + naturalHeight = size.naturalHeight; + } + + yield setImageTooltip(this.previewTooltip, doc, imageUrl, + {maxDim, naturalWidth, naturalHeight}); + }), + + /** + * Set the content of the preview tooltip to display a font family preview. + * + * @param {String} font + * The font family value. + * @param {object} nodeFront + * The NodeActor that will used to retrieve the dataURL for the font + * family tooltip contents. + * @return {Promise} A promise that resolves when the preview tooltip content is ready + */ + _setFontPreviewTooltip: Task.async(function* (font, nodeFront) { + if (!font || !nodeFront || typeof nodeFront.getFontFamilyDataURL !== "function") { + throw new Error("Unable to create font preview tooltip content."); + } + + font = font.replace(/"/g, "'"); + font = font.replace("!important", ""); + font = font.trim(); + + let fillStyle = getColor("body-color"); + let {data, size: maxDim} = yield nodeFront.getFontFamilyDataURL(font, fillStyle); + + let imageUrl = yield data.string(); + let doc = this.view.inspector.panelDoc; + let {naturalWidth, naturalHeight} = yield getImageDimensions(doc, imageUrl); + + yield setImageTooltip(this.previewTooltip, doc, imageUrl, + {hideDimensionLabel: true, maxDim, naturalWidth, naturalHeight}); + }), + + _onNewSelection: function () { + if (this.previewTooltip) { + this.previewTooltip.hide(); + } + + if (this.colorPicker) { + this.colorPicker.hide(); + } + + if (this.cubicBezier) { + this.cubicBezier.hide(); + } + + if (this.cssDocs) { + this.cssDocs.hide(); + } + + if (this.filterEditor) { + this.filterEditor.hide(); + } + }, + + /** + * Destroy this overlay instance, removing it from the view + */ + destroy: function () { + this.removeFromView(); + + this.view.inspector.selection.off("new-node-front", this._onNewSelection); + this.view = null; + + this._isDestroyed = true; + } +}; + +module.exports = TooltipsOverlay; diff --git a/devtools/client/inspector/shared/utils.js b/devtools/client/inspector/shared/utils.js new file mode 100644 index 000000000..60dda914c --- /dev/null +++ b/devtools/client/inspector/shared/utils.js @@ -0,0 +1,161 @@ +/* -*- 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 {parseDeclarations} = require("devtools/shared/css/parsing-utils"); +const promise = require("promise"); +const {getCSSLexer} = require("devtools/shared/css/lexer"); +const {KeyCodes} = require("devtools/client/shared/keycodes"); + +const HTML_NS = "http://www.w3.org/1999/xhtml"; + +/** + * Create a child element with a set of attributes. + * + * @param {Element} parent + * The parent node. + * @param {string} tagName + * The tag name. + * @param {object} attributes + * A set of attributes to set on the node. + */ +function createChild(parent, tagName, attributes = {}) { + let elt = parent.ownerDocument.createElementNS(HTML_NS, tagName); + for (let attr in attributes) { + if (attributes.hasOwnProperty(attr)) { + if (attr === "textContent") { + elt.textContent = attributes[attr]; + } else if (attr === "child") { + elt.appendChild(attributes[attr]); + } else { + elt.setAttribute(attr, attributes[attr]); + } + } + } + parent.appendChild(elt); + return elt; +} + +exports.createChild = createChild; + +/** + * Append a text node to an element. + * + * @param {Element} parent + * The parent node. + * @param {string} text + * The text content for the text node. + */ +function appendText(parent, text) { + parent.appendChild(parent.ownerDocument.createTextNode(text)); +} + +exports.appendText = appendText; + +/** + * Called when a character is typed in a value editor. This decides + * whether to advance or not, first by checking to see if ";" was + * typed, and then by lexing the input and seeing whether the ";" + * would be a terminator at this point. + * + * @param {number} keyCode + * Key code to be checked. + * @param {string} aValue + * Current text editor value. + * @param {number} insertionPoint + * The index of the insertion point. + * @return {Boolean} True if the focus should advance; false if + * the character should be inserted. + */ +function advanceValidate(keyCode, value, insertionPoint) { + // Only ";" has special handling here. + if (keyCode !== KeyCodes.DOM_VK_SEMICOLON) { + return false; + } + + // Insert the character provisionally and see what happens. If we + // end up with a ";" symbol token, then the semicolon terminates the + // value. Otherwise it's been inserted in some spot where it has a + // valid meaning, like a comment or string. + value = value.slice(0, insertionPoint) + ";" + value.slice(insertionPoint); + let lexer = getCSSLexer(value); + while (true) { + let token = lexer.nextToken(); + if (token.endOffset > insertionPoint) { + if (token.tokenType === "symbol" && token.text === ";") { + // The ";" is a terminator. + return true; + } + // The ";" is not a terminator in this context. + break; + } + } + return false; +} + +exports.advanceValidate = advanceValidate; + +/** + * Create a throttling function wrapper to regulate its frequency. + * + * @param {Function} func + * The function to throttle + * @param {number} wait + * The throttling period + * @param {Object} scope + * The scope to use for func + * @return {Function} The throttled function + */ +function throttle(func, wait, scope) { + let timer = null; + + return function () { + if (timer) { + clearTimeout(timer); + } + + let args = arguments; + timer = setTimeout(function () { + timer = null; + func.apply(scope, args); + }, wait); + }; +} + +exports.throttle = throttle; + +/** + * Event handler that causes a blur on the target if the input has + * multiple CSS properties as the value. + */ +function blurOnMultipleProperties(cssProperties) { + return (e) => { + setTimeout(() => { + let props = parseDeclarations(cssProperties.isKnown, e.target.value); + if (props.length > 1) { + e.target.blur(); + } + }, 0); + }; +} + +exports.blurOnMultipleProperties = blurOnMultipleProperties; + +/** + * Log the provided error to the console and return a rejected Promise for + * this error. + * + * @param {Error} error + * The error to log + * @return {Promise} A rejected promise + */ +function promiseWarn(error) { + console.error(error); + return promise.reject(error); +} + +exports.promiseWarn = promiseWarn; -- cgit v1.2.3