mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-07 08:18:41 +09:00
import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo
This commit is contained in:
commit
dcd9973243
150858 changed files with 23884658 additions and 0 deletions
352
devtools/client/inspector/shared/dom-node-preview.js
Normal file
352
devtools/client/inspector/shared/dom-node-preview.js
Normal file
|
|
@ -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 <tag id="id" class="class">
|
||||
* 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);
|
||||
315
devtools/client/inspector/shared/highlighters-overlay.js
Normal file
315
devtools/client/inspector/shared/highlighters-overlay.js
Normal file
|
|
@ -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;
|
||||
16
devtools/client/inspector/shared/moz.build
Normal file
16
devtools/client/inspector/shared/moz.build
Normal file
|
|
@ -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']
|
||||
17
devtools/client/inspector/shared/node-types.js
Normal file
17
devtools/client/inspector/shared/node-types.js
Normal file
|
|
@ -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;
|
||||
510
devtools/client/inspector/shared/style-inspector-menu.js
Normal file
510
devtools/client/inspector/shared/style-inspector-menu.js
Normal file
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
6
devtools/client/inspector/shared/test/.eslintrc.js
Normal file
6
devtools/client/inspector/shared/test/.eslintrc.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
// Extend from the shared list of defined globals for mochitests.
|
||||
"extends": "../../../../.eslintrc.mochitests.js"
|
||||
};
|
||||
41
devtools/client/inspector/shared/test/browser.ini
Normal file
41
devtools/client/inspector/shared/test/browser.ini
Normal file
|
|
@ -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]
|
||||
|
|
@ -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 = `
|
||||
<div style="color:rgb(18, 58, 188);margin:0px;background:span[data-color];">
|
||||
Test "Copy color" context menu option
|
||||
</div>
|
||||
`;
|
||||
|
||||
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;
|
||||
|
|
@ -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 = `
|
||||
<style type="text/css">
|
||||
div {
|
||||
color: #123ABC;
|
||||
}
|
||||
</style>
|
||||
<div>Testing the color picker tooltip!</div>
|
||||
`;
|
||||
|
||||
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");
|
||||
}
|
||||
|
|
@ -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 = `<style type="text/css">
|
||||
.valid-background {
|
||||
background-image: url(${TEST_DATA_URI});
|
||||
}
|
||||
.invalid-background {
|
||||
background-image: url(${INVALID_IMAGE_URI});
|
||||
}
|
||||
</style>
|
||||
<div class="valid-background">Valid background image</div>
|
||||
<div class="invalid-background">Invalid background image</div>`;
|
||||
|
||||
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");
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 = `
|
||||
<div id="one" style="color:red;">one</div>
|
||||
<div id="two" style="color:blue;">two</div>
|
||||
`;
|
||||
|
||||
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");
|
||||
});
|
||||
|
|
@ -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 = `
|
||||
<style type="text/css">
|
||||
body {
|
||||
padding: 1em;
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAADI5JREFUeNrsWwuQFNUVPf1m5z87szv7HWSWj8CigBFMEFZKiQsB1PgJwUAZg1HBpIQsKmokEhNjWUnFVPnDWBT+KolJYbRMoqUVq0yCClpqiX8sCchPWFwVlt2db7+X93pez7zu6Vn2NxsVWh8987p7pu+9555z7+tZjTGGY3kjOMa34w447oBjfKsY7i/UNM3Y8eFSAkD50Plgw03K5P9gvGv7U5ieeR3PszeREiPNX3/0DL4hjslzhm8THh+OITfXk3dhiv4GDtGPVzCaeJmPLYzuu5qJuWfuw2QTlcN1X9pwQU7LhdZ/ZAseD45cOh9hHvDkc/yAF/DNhdb5Mrr3PvBMaAYW8fMSIi2G497IMEK/YutGtAYr6+ej+nxu/NN8Ks3N7AR6HgcLz0Eg1Ljg1UcxZzi5qewIkMYLRweTr2Kzp+nmyXAd5pS3XQDd+N/4h4zgu9FI7brlXf90nMEnuwQxlvv+hosE3TuexmWeysmT4W+WxkMaLzf9Y8ATgjcUn7T9H1gqrpFq8eV1gMn6t16NhngjfoX6q4DUP032Rd4LJgpSLwJ1yzFqBG69eRkah0MVyo0Acfe+yy9AG4nMiYCkeM53KKFXncBLAXqEm+wCqZwaueq7WCmuLTcKSJmj737ol2hurA9eq9VdyiO8yWa3NNyog+SB5CZodSsQq/dfu34tJpYbBaTMzvVddDZu16q5smXf4G8zEvqm4cyaAmJPuTJk3oJWdS4WzcVtfMZbThSQckb/pYfRGgo3zNOqZnEHbJPGK4abaDCQIIsT8V/qTaBqHkLh6LzXH8XZQhbLhYKyyCC/WeHYcNdmvOgfe8skzbWL270/T3wf7tSx/lGCbTu8xlzzmCSWLc5iwmgikcCHi3Mga0Ry913vBFvQwg90l6M4ImWKfsWOp7DSWxmfpPlCFuPFfsNfKrCnPYpQKIRgqBK7D0SxYaNHwkEiJMtl0ReDp3Lc5D3PGoTo/sKngCl7a5chFqvBatKwjBd7WwqIlzB/78NcoUcp5VSgGxm+7b8eqQRGnHMO634epO4S1EZww09/iFg5UmGoESDuznP1xVhTUX1WWHPzjpd25wyH0hRxI3LGM75nxmuNEEUVpAN0XgxmPoKralakbQnWlIMQyVBD/w+3orkq4lvualjKyWwzt4MaxqspQHVhPOWG64bxYuhZXSFGWhipbSDVragOu5Y9eAsmDDUKyBA703vemVhHoueD6e9wAzJK1WfmN0Umk5GGM4kEMZcuIECqgjm0nldAqmbjwtm4VxZH5AvlADP6mx9Eqy9Q0+KqW8Ch+47FaMMYmnNGfY1iPMshoC6qFxme4wQ+0p+ARE6H3+9veWEDWgUhDhUKyFARn4jM5BNxT0XsMg7bfymGK1ov3wtjDfhL4w0HVGUVBEjDaaE+QNdrcNWch1PG4W6xrjBUXECGivg++Cva3JUT4iQUz3V2RsSVaKLwOuDT89A3HdBQoxhNC+fnVm74ual2EG893P6G+PuP4SfiO4cCBWQooL9qCWKNXPbcI37Aa/lnlZxXRt4RFONGwSDCPAHqOuqjWct1QiEMw5mChM5X4K47FyNqcd3aK9AwFH0CGYLoe1ctxk2eWi57rg5JfGp9rzC6ggCdFlAgHBDw5Yxlcg6G8SyHCjMlsgmDD9zhSeHlF+JnAgWDTQUy2NxfdwOao1UVV3pi3+bE97YSbWpLAbn6zefHNQkp1PMpIBwwvslKgIYTKM2nEpNzrGcH3FXTEal0L38kJ4uDQgEZbO4vnI173LXf5NHZaiUxtaCxyZuo/rK6LpUg54yg3zTWRAArvDcRIPZ6BqzrQ1REpmL+DNw32OKIDCb3X1qPVn8wNNMT4w2bvs+q4bAZrqBh2skaL3yyhhIIZ4i6oHkUK0RckcB8GigEyRIH4A6Mgc8fatl0/+BkkQxC9gIT4ljna1rIZW9rEdNbjJcNjsnoYj7LHWCUwpITzEgzRQKZ3XAFHbTzA3hrz8TEUUZxFBhoKpABQt/97p+w0hMZG68I8R6FtlsJT3FELndZntjM+VMnylKYq8GJI3UZaRMpquGSGFVOEfv0YZBMNzz+uvjbfzS6xQERIhlI9FcvQWNdFVb7x1zCb+QNK8vb9NsiifmI5hBgVoOCBC1sb0ab5RomqENxLO3eA1/0NDRU47q2RQNbRCUDIb7lF2CNL3ZGxEV4n08TVvZWYG4pZyV0zUdS45tyCBByOHWiyvZmxFXDCyRo1ge5+Sy0TA+8lWMiP/6O0S32exGV9Jf4fr8azdUR3zL/CZz4MtvzdX5uOYs6NDOmpkuj5Huh+7qUQSYl0ThHzw0YQzcGo6bhzEqoYq5rN3yRiYiG3Vfe2Ybm/qKA9NNZ3nNm4F7/yDkg9AN+U1mHiBcXP8zuDN76jj8hg1QyiWQigalj02BJPhK8I0zxijAjhp5zhlpLUDvS+BCy2HMAvvB4XDgL9/SXC0g/ou/5+6/xLX8w0uJrOIkXfPvyhY0F6gr7M8H0KWFYikcqAXakB+xwD9CdREBLoau7Gz3cAdSIdLFxFtJTCqRChSjnutvhDcREtzjz2Tswtz+yeNRFUeXZXtWux7C1fuoVcbd3J//ipDX3uZZDLGrwweS+UBLL5TDliVBnF8P7H+XI8aRRGsIBJg/Zlslt1+W+D1JWoSyi+kD9jfhs78t7mhZhSl+fLfY1Bdyv3I8V/qpY3B1McgN7ZFT5/vNO0I5DPLLdPBIJA8qc4h2I0QplYfDpJwHT+aj0246r5S8rToG8OjCle8wk4OLvvYGa+Ovr84uo2qBSwJS9G5egoZFLTfiEqWDtbwGfHgKOdPHcS+ai7XDzMPW/FJRLGGcxnBbK4YJC2K+h+T6Bdu5CqHqCWERd3bawb7JI+iJ735+LNaHaprBLLHBm08U3XxShEsdt+f3eTh3v7aC95Dct4RCWL5OZWh/oXBZThxAIxyOXLzBk8aiEWJID8rK3CpPOmeHaGpvCS+7EHv5FujVHUSJPLXvIFeHcNc+9xrB2gws9KZdxuLFax/WLM5gzzSm/lTXF/OdAcapyvjxPqxqHjr2v4ckX2bS2dRBrc5lSdpKjEJ9/9tdwX2WMd53ZQ2IVo3RES+UwVSpCPvYepNx4gmTGDUKIMQ4eduPnD7mx9xOn/KZKOlFbStjONxHTtR+BYAPmnoZ1Zp8wkBRwP/EL3u0F/C2hGl7vpz7vW37T3vP7if8wroKuoh8ribknX9BK5rcF+mo1qKaKyRPJTgTDjbzY8szcuLb3bpH00u35T47j7prRpwDJTxzyG0dHgxPp5bPG8VdkpfPbUg3SgoOo2mwVukb98D5EqpswZTTulCggTk4gpYhv0++wIhCJxr0+Hq1sondis0SE2oxQe3qWXwWyO4DSQg9gJ8Iiw1VFcGqXxet0N9xE4ygIxv/9W6wo9WyROEX/R+eiobYSq2vHTOR631Eiv2lRfh9dvxkumkXh92Qsx8XrAJ+7YGbWuhxOi/U+31NQmzyqNYG8N/3wfo6CRtRHcN01FzkvojohwLu0VVvDa56IS/xcj2b7nN+O+m0jqpE1wMPXZxAN9iCVThtDvH7gmiRGRpU8Lspv1Uhq4wIVdQoyuGSLNYPKUCS8+CzNURbzMmjK3i8u0U793lmuV0ef9nWQ5MGC/DiUqEUSaCtXna9RJEspZS1lrXINK/pcq+SpT50t98QKMq1FRmDfx3vxty102k0PM4ssEnvuz5+G26Ij4yDpz6z9fV8bkyIkqBFkhej0Ib+ZQ34XJK9AfozaiimqIoX3Jp3tiISrcfYpuN2+iFph/02P36PNC9fVcCnp6H9jYouKyfaWufz5Tp9tVxcUniw7IohZv4dZz81/ns67z3AYPrc2n0+Ix2q8k0PWjgBy88XaibnfK9A+5LdDY2Ivhy36fbT8Zv3Lb1U1qLqUxorXEEXIs0mjjrtxoTZWtdvigNs2sgPiujTv6DIZLld6b/V5742JZV3fUsUVFy5gdsNtKWFzUCEVbNepD1MkSMVbsb6SZm7jI3/zODtQKgUMsOw8wDZ63t5xcV1TnaEAxoc6wrqY+Fj+N4DsqOnhOIdicrQSm1MPYCPlIqHn5bbHg8/bj2D3QfZnCX3mpAICDZV8jH5kpbZqTD0W+DxaA74CWzLN2nd14OlL72J38Lf7+TjC7dadZFDoZJQPrtaIKL/G0L6ktptPZVJ8fMqHYPZOKYPMyQGadIJfDvdXwAFiZOTvDBPydf5vk4rWA+RfdhBlaF/yDDBRoMu9pfnSjv/p7DG+HXfAcQcc49v/BBgAcFAO4DmB2GQAAAAASUVORK5CYII=);
|
||||
background-repeat: repeat-y;
|
||||
background-position: right top;
|
||||
}
|
||||
.test-element {
|
||||
font-family: verdana;
|
||||
color: #333;
|
||||
background: url(chrome://global/skin/icons/warning-64.png) no-repeat left center;
|
||||
padding-left: 70px;
|
||||
}
|
||||
</style>
|
||||
<div class="test-element">test element</div>
|
||||
`;
|
||||
|
||||
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");
|
||||
}
|
||||
|
|
@ -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 = "<div class='one'>el 1</div><div class='two'>el 2</div>";
|
||||
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");
|
||||
}
|
||||
|
|
@ -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 = `
|
||||
<style type="text/css">
|
||||
#testElement {
|
||||
font-family: cursive;
|
||||
color: #333;
|
||||
padding-left: 70px;
|
||||
}
|
||||
</style>
|
||||
<div id="testElement">test element</div>
|
||||
`;
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
@ -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 = `<style>${TEST_STYLE}</style><h1>test element</h1>`;
|
||||
|
||||
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);
|
||||
}
|
||||
|
|
@ -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 = `
|
||||
<style type="text/css">
|
||||
#testElement {
|
||||
font: italic bold .8em/1.2 Arial;
|
||||
}
|
||||
</style>
|
||||
<div id="testElement">test element</div>
|
||||
`;
|
||||
|
||||
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");
|
||||
}
|
||||
|
|
@ -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 = `
|
||||
<style type="text/css">
|
||||
div {
|
||||
width: 300px;height: 300px;border-radius: 50%;
|
||||
background: red url(chrome://global/skin/icons/warning-64.png);
|
||||
}
|
||||
</style>
|
||||
<div></div>
|
||||
`;
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
@ -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 = `
|
||||
<style type="text/css">
|
||||
body {
|
||||
transform: skew(16deg);
|
||||
}
|
||||
</style>
|
||||
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");
|
||||
});
|
||||
|
|
@ -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 = `
|
||||
<style type="text/css">
|
||||
body {
|
||||
transform: skew(16deg);
|
||||
color: yellow;
|
||||
}
|
||||
</style>
|
||||
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;
|
||||
});
|
||||
|
|
@ -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 = `
|
||||
<style type="text/css">
|
||||
html {
|
||||
transform: scale(.9);
|
||||
}
|
||||
body {
|
||||
transform: skew(16deg);
|
||||
color: purple;
|
||||
}
|
||||
</style>
|
||||
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");
|
||||
});
|
||||
|
|
@ -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 = `
|
||||
<style type="text/css">
|
||||
div {
|
||||
background: purple;
|
||||
width:300px;height:300px;
|
||||
transform: rotate(16deg);
|
||||
}
|
||||
.test {
|
||||
transform: skew(25deg);
|
||||
}
|
||||
</style>
|
||||
<div class="test"></div>
|
||||
`;
|
||||
|
||||
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;
|
||||
});
|
||||
37
devtools/client/inspector/shared/test/doc_author-sheet.html
Normal file
37
devtools/client/inspector/shared/test/doc_author-sheet.html
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<!-- Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ -->
|
||||
<html>
|
||||
<head>
|
||||
<title>authored sheet test</title>
|
||||
<style>
|
||||
#target {
|
||||
color: chartreuse;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
"use strict";
|
||||
var gIOService = SpecialPowers.Cc["@mozilla.org/network/io-service;1"]
|
||||
.getService(SpecialPowers.Ci.nsIIOService);
|
||||
|
||||
var style = "data:text/css,div { background-color: seagreen; }";
|
||||
var uri = gIOService.newURI(style, null, null);
|
||||
var windowUtils = SpecialPowers.wrap(window)
|
||||
.QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor)
|
||||
.getInterface(SpecialPowers.Ci.nsIDOMWindowUtils);
|
||||
windowUtils.loadSheet(uri, windowUtils.AUTHOR_SHEET);
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="target"> the ocean </div>
|
||||
<input type=text placeholder=test></input>
|
||||
<input type=color></input>
|
||||
<input type=range></input>
|
||||
<input type=number></input>
|
||||
<progress></progress>
|
||||
<blockquote type=cite>
|
||||
<pre _moz_quote=true>
|
||||
inspect <a href="foo">user agent</a> styles
|
||||
</pre>
|
||||
</blockquote>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>test</title>
|
||||
<link href="./doc_content_stylesheet_linked.css" rel="stylesheet" type="text/css">
|
||||
<script>
|
||||
/* exported loadCSS */
|
||||
"use strict";
|
||||
// Load script.css
|
||||
function loadCSS() {
|
||||
let link = document.createElement("link");
|
||||
link.rel = "stylesheet";
|
||||
link.type = "text/css";
|
||||
link.href = "./doc_content_stylesheet_script.css";
|
||||
document.getElementsByTagName("head")[0].appendChild(link);
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
table {
|
||||
border: 1px solid #000;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body onload="loadCSS();">
|
||||
<table id="target">
|
||||
<tr>
|
||||
<td>
|
||||
<h3>Simple test</h3>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0"?>
|
||||
<?xml-stylesheet href="chrome://global/skin/xul.css" type="text/css"?>
|
||||
<?xml-stylesheet href="./doc_content_stylesheet_xul.css"
|
||||
type="text/css"?>
|
||||
<!DOCTYPE window>
|
||||
<window id="testwindow" xmlns:html="http://www.w3.org/1999/xhtml"
|
||||
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
|
||||
<label id="target" value="Simple XUL document" />
|
||||
</window>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
@import url("./doc_content_stylesheet_imported2.css");
|
||||
|
||||
#target {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
#target {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
@import url("./doc_content_stylesheet_imported.css");
|
||||
|
||||
table {
|
||||
opacity: 1;
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
#target {
|
||||
font-size: 200px;
|
||||
}
|
||||
115
devtools/client/inspector/shared/test/doc_frame_script.js
Normal file
115
devtools/client/inspector/shared/test/doc_frame_script.js
Normal file
|
|
@ -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;
|
||||
}
|
||||
557
devtools/client/inspector/shared/test/head.js
Normal file
557
devtools/client/inspector/shared/test/head.js
Normal file
|
|
@ -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;
|
||||
}
|
||||
319
devtools/client/inspector/shared/tooltips-overlay.js
Normal file
319
devtools/client/inspector/shared/tooltips-overlay.js
Normal file
|
|
@ -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;
|
||||
161
devtools/client/inspector/shared/utils.js
Normal file
161
devtools/client/inspector/shared/utils.js
Normal file
|
|
@ -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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue