diff options
Diffstat (limited to 'devtools/client/inspector/rules')
216 files changed, 19180 insertions, 0 deletions
diff --git a/devtools/client/inspector/rules/models/element-style.js b/devtools/client/inspector/rules/models/element-style.js new file mode 100644 index 000000000..7f015ba08 --- /dev/null +++ b/devtools/client/inspector/rules/models/element-style.js @@ -0,0 +1,412 @@ +/* -*- 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 promise = require("promise"); +const {Rule} = require("devtools/client/inspector/rules/models/rule"); +const {promiseWarn} = require("devtools/client/inspector/shared/utils"); +const {ELEMENT_STYLE} = require("devtools/shared/specs/styles"); +const {getCssProperties} = require("devtools/shared/fronts/css-properties"); + +/** + * ElementStyle is responsible for the following: + * Keeps track of which properties are overridden. + * Maintains a list of Rule objects for a given element. + * + * @param {Element} element + * The element whose style we are viewing. + * @param {CssRuleView} ruleView + * The instance of the rule-view panel. + * @param {Object} store + * The ElementStyle can use this object to store metadata + * that might outlast the rule view, particularly the current + * set of disabled properties. + * @param {PageStyleFront} pageStyle + * Front for the page style actor that will be providing + * the style information. + * @param {Boolean} showUserAgentStyles + * Should user agent styles be inspected? + */ +function ElementStyle(element, ruleView, store, pageStyle, + showUserAgentStyles) { + this.element = element; + this.ruleView = ruleView; + this.store = store || {}; + this.pageStyle = pageStyle; + this.showUserAgentStyles = showUserAgentStyles; + this.rules = []; + this.cssProperties = getCssProperties(this.ruleView.inspector.toolbox); + + // We don't want to overwrite this.store.userProperties so we only create it + // if it doesn't already exist. + if (!("userProperties" in this.store)) { + this.store.userProperties = new UserProperties(); + } + + if (!("disabled" in this.store)) { + this.store.disabled = new WeakMap(); + } +} + +ElementStyle.prototype = { + // The element we're looking at. + element: null, + + destroy: function () { + if (this.destroyed) { + return; + } + this.destroyed = true; + + for (let rule of this.rules) { + if (rule.editor) { + rule.editor.destroy(); + } + } + }, + + /** + * Called by the Rule object when it has been changed through the + * setProperty* methods. + */ + _changed: function () { + if (this.onChanged) { + this.onChanged(); + } + }, + + /** + * Refresh the list of rules to be displayed for the active element. + * Upon completion, this.rules[] will hold a list of Rule objects. + * + * Returns a promise that will be resolved when the elementStyle is + * ready. + */ + populate: function () { + let populated = this.pageStyle.getApplied(this.element, { + inherited: true, + matchedSelectors: true, + filter: this.showUserAgentStyles ? "ua" : undefined, + }).then(entries => { + if (this.destroyed) { + return promise.resolve(undefined); + } + + if (this.populated !== populated) { + // Don't care anymore. + return promise.resolve(undefined); + } + + // Store the current list of rules (if any) during the population + // process. They will be reused if possible. + let existingRules = this.rules; + + this.rules = []; + + for (let entry of entries) { + this._maybeAddRule(entry, existingRules); + } + + // Mark overridden computed styles. + this.markOverriddenAll(); + + this._sortRulesForPseudoElement(); + + // We're done with the previous list of rules. + for (let r of existingRules) { + if (r && r.editor) { + r.editor.destroy(); + } + } + + return undefined; + }).then(null, e => { + // populate is often called after a setTimeout, + // the connection may already be closed. + if (this.destroyed) { + return promise.resolve(undefined); + } + return promiseWarn(e); + }); + this.populated = populated; + return this.populated; + }, + + /** + * Put pseudo elements in front of others. + */ + _sortRulesForPseudoElement: function () { + this.rules = this.rules.sort((a, b) => { + return (a.pseudoElement || "z") > (b.pseudoElement || "z"); + }); + }, + + /** + * Add a rule if it's one we care about. Filters out duplicates and + * inherited styles with no inherited properties. + * + * @param {Object} options + * Options for creating the Rule, see the Rule constructor. + * @param {Array} existingRules + * Rules to reuse if possible. If a rule is reused, then it + * it will be deleted from this array. + * @return {Boolean} true if we added the rule. + */ + _maybeAddRule: function (options, existingRules) { + // If we've already included this domRule (for example, when a + // common selector is inherited), ignore it. + if (options.rule && + this.rules.some(rule => rule.domRule === options.rule)) { + return false; + } + + if (options.system) { + return false; + } + + let rule = null; + + // If we're refreshing and the rule previously existed, reuse the + // Rule object. + if (existingRules) { + let ruleIndex = existingRules.findIndex((r) => r.matches(options)); + if (ruleIndex >= 0) { + rule = existingRules[ruleIndex]; + rule.refresh(options); + existingRules.splice(ruleIndex, 1); + } + } + + // If this is a new rule, create its Rule object. + if (!rule) { + rule = new Rule(this, options); + } + + // Ignore inherited rules with no visible properties. + if (options.inherited && !rule.hasAnyVisibleProperties()) { + return false; + } + + this.rules.push(rule); + return true; + }, + + /** + * Calls markOverridden with all supported pseudo elements + */ + markOverriddenAll: function () { + this.markOverridden(); + for (let pseudo of this.cssProperties.pseudoElements) { + this.markOverridden(pseudo); + } + }, + + /** + * Mark the properties listed in this.rules for a given pseudo element + * with an overridden flag if an earlier property overrides it. + * + * @param {String} pseudo + * Which pseudo element to flag as overridden. + * Empty string or undefined will default to no pseudo element. + */ + markOverridden: function (pseudo = "") { + // Gather all the text properties applied by these rules, ordered + // from more- to less-specific. Text properties from keyframes rule are + // excluded from being marked as overridden since a number of criteria such + // as time, and animation overlay are required to be check in order to + // determine if the property is overridden. + let textProps = []; + for (let rule of this.rules) { + if ((rule.matchedSelectors.length > 0 || + rule.domRule.type === ELEMENT_STYLE) && + rule.pseudoElement === pseudo && !rule.keyframes) { + for (let textProp of rule.textProps.slice(0).reverse()) { + if (textProp.enabled) { + textProps.push(textProp); + } + } + } + } + + // Gather all the computed properties applied by those text + // properties. + let computedProps = []; + for (let textProp of textProps) { + computedProps = computedProps.concat(textProp.computed); + } + + // Walk over the computed properties. As we see a property name + // for the first time, mark that property's name as taken by this + // property. + // + // If we come across a property whose name is already taken, check + // its priority against the property that was found first: + // + // If the new property is a higher priority, mark the old + // property overridden and mark the property name as taken by + // the new property. + // + // If the new property is a lower or equal priority, mark it as + // overridden. + // + // _overriddenDirty will be set on each prop, indicating whether its + // dirty status changed during this pass. + let taken = {}; + for (let computedProp of computedProps) { + let earlier = taken[computedProp.name]; + + // Prevent -webkit-gradient from being selected after unchecking + // linear-gradient in this case: + // -moz-linear-gradient: ...; + // -webkit-linear-gradient: ...; + // linear-gradient: ...; + if (!computedProp.textProp.isValid()) { + computedProp.overridden = true; + continue; + } + let overridden; + if (earlier && + computedProp.priority === "important" && + earlier.priority !== "important" && + (earlier.textProp.rule.inherited || + !computedProp.textProp.rule.inherited)) { + // New property is higher priority. Mark the earlier property + // overridden (which will reverse its dirty state). + earlier._overriddenDirty = !earlier._overriddenDirty; + earlier.overridden = true; + overridden = false; + } else { + overridden = !!earlier; + } + + computedProp._overriddenDirty = + (!!computedProp.overridden !== overridden); + computedProp.overridden = overridden; + if (!computedProp.overridden && computedProp.textProp.enabled) { + taken[computedProp.name] = computedProp; + } + } + + // For each TextProperty, mark it overridden if all of its + // computed properties are marked overridden. Update the text + // property's associated editor, if any. This will clear the + // _overriddenDirty state on all computed properties. + for (let textProp of textProps) { + // _updatePropertyOverridden will return true if the + // overridden state has changed for the text property. + if (this._updatePropertyOverridden(textProp)) { + textProp.updateEditor(); + } + } + }, + + /** + * Mark a given TextProperty as overridden or not depending on the + * state of its computed properties. Clears the _overriddenDirty state + * on all computed properties. + * + * @param {TextProperty} prop + * The text property to update. + * @return {Boolean} true if the TextProperty's overridden state (or any of + * its computed properties overridden state) changed. + */ + _updatePropertyOverridden: function (prop) { + let overridden = true; + let dirty = false; + for (let computedProp of prop.computed) { + if (!computedProp.overridden) { + overridden = false; + } + dirty = computedProp._overriddenDirty || dirty; + delete computedProp._overriddenDirty; + } + + dirty = (!!prop.overridden !== overridden) || dirty; + prop.overridden = overridden; + return dirty; + } +}; + +/** + * Store of CSSStyleDeclarations mapped to properties that have been changed by + * the user. + */ +function UserProperties() { + this.map = new Map(); +} + +UserProperties.prototype = { + /** + * Get a named property for a given CSSStyleDeclaration. + * + * @param {CSSStyleDeclaration} style + * The CSSStyleDeclaration against which the property is mapped. + * @param {String} name + * The name of the property to get. + * @param {String} value + * Default value. + * @return {String} + * The property value if it has previously been set by the user, null + * otherwise. + */ + getProperty: function (style, name, value) { + let key = this.getKey(style); + let entry = this.map.get(key, null); + + if (entry && name in entry) { + return entry[name]; + } + return value; + }, + + /** + * Set a named property for a given CSSStyleDeclaration. + * + * @param {CSSStyleDeclaration} style + * The CSSStyleDeclaration against which the property is to be mapped. + * @param {String} bame + * The name of the property to set. + * @param {String} userValue + * The value of the property to set. + */ + setProperty: function (style, bame, userValue) { + let key = this.getKey(style, bame); + let entry = this.map.get(key, null); + + if (entry) { + entry[bame] = userValue; + } else { + let props = {}; + props[bame] = userValue; + this.map.set(key, props); + } + }, + + /** + * Check whether a named property for a given CSSStyleDeclaration is stored. + * + * @param {CSSStyleDeclaration} style + * The CSSStyleDeclaration against which the property would be mapped. + * @param {String} name + * The name of the property to check. + */ + contains: function (style, name) { + let key = this.getKey(style, name); + let entry = this.map.get(key, null); + return !!entry && name in entry; + }, + + getKey: function (style, name) { + return style.actorID + ":" + name; + }, + + clear: function () { + this.map.clear(); + } +}; + +exports.ElementStyle = ElementStyle; diff --git a/devtools/client/inspector/rules/models/moz.build b/devtools/client/inspector/rules/models/moz.build new file mode 100644 index 000000000..1c5c0f89f --- /dev/null +++ b/devtools/client/inspector/rules/models/moz.build @@ -0,0 +1,11 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + 'element-style.js', + 'rule.js', + 'text-property.js', +) diff --git a/devtools/client/inspector/rules/models/rule.js b/devtools/client/inspector/rules/models/rule.js new file mode 100644 index 000000000..1a3fa057a --- /dev/null +++ b/devtools/client/inspector/rules/models/rule.js @@ -0,0 +1,686 @@ +/* -*- 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 promise = require("promise"); +const CssLogic = require("devtools/shared/inspector/css-logic"); +const {ELEMENT_STYLE} = require("devtools/shared/specs/styles"); +const {TextProperty} = + require("devtools/client/inspector/rules/models/text-property"); +const {promiseWarn} = require("devtools/client/inspector/shared/utils"); +const {parseDeclarations} = require("devtools/shared/css/parsing-utils"); +const Services = require("Services"); + +const STYLE_INSPECTOR_PROPERTIES = "devtools/shared/locales/styleinspector.properties"; +const {LocalizationHelper} = require("devtools/shared/l10n"); +const STYLE_INSPECTOR_L10N = new LocalizationHelper(STYLE_INSPECTOR_PROPERTIES); + +/** + * Rule is responsible for the following: + * Manages a single style declaration or rule. + * Applies changes to the properties in a rule. + * Maintains a list of TextProperty objects. + * + * @param {ElementStyle} elementStyle + * The ElementStyle to which this rule belongs. + * @param {Object} options + * The information used to construct this rule. Properties include: + * rule: A StyleRuleActor + * inherited: An element this rule was inherited from. If omitted, + * the rule applies directly to the current element. + * isSystem: Is this a user agent style? + * isUnmatched: True if the rule does not match the current selected + * element, otherwise, false. + */ +function Rule(elementStyle, options) { + this.elementStyle = elementStyle; + this.domRule = options.rule || null; + this.style = options.rule; + this.matchedSelectors = options.matchedSelectors || []; + this.pseudoElement = options.pseudoElement || ""; + + this.isSystem = options.isSystem; + this.isUnmatched = options.isUnmatched || false; + this.inherited = options.inherited || null; + this.keyframes = options.keyframes || null; + this._modificationDepth = 0; + + if (this.domRule && this.domRule.mediaText) { + this.mediaText = this.domRule.mediaText; + } + + this.cssProperties = this.elementStyle.ruleView.cssProperties; + + // Populate the text properties with the style's current authoredText + // value, and add in any disabled properties from the store. + this.textProps = this._getTextProperties(); + this.textProps = this.textProps.concat(this._getDisabledProperties()); +} + +Rule.prototype = { + mediaText: "", + + get title() { + let title = CssLogic.shortSource(this.sheet); + if (this.domRule.type !== ELEMENT_STYLE && this.ruleLine > 0) { + title += ":" + this.ruleLine; + } + + return title + (this.mediaText ? " @media " + this.mediaText : ""); + }, + + get inheritedSource() { + if (this._inheritedSource) { + return this._inheritedSource; + } + this._inheritedSource = ""; + if (this.inherited) { + let eltText = this.inherited.displayName; + if (this.inherited.id) { + eltText += "#" + this.inherited.id; + } + this._inheritedSource = + STYLE_INSPECTOR_L10N.getFormatStr("rule.inheritedFrom", eltText); + } + return this._inheritedSource; + }, + + get keyframesName() { + if (this._keyframesName) { + return this._keyframesName; + } + this._keyframesName = ""; + if (this.keyframes) { + this._keyframesName = + STYLE_INSPECTOR_L10N.getFormatStr("rule.keyframe", this.keyframes.name); + } + return this._keyframesName; + }, + + get selectorText() { + return this.domRule.selectors ? this.domRule.selectors.join(", ") : + CssLogic.l10n("rule.sourceElement"); + }, + + /** + * The rule's stylesheet. + */ + get sheet() { + return this.domRule ? this.domRule.parentStyleSheet : null; + }, + + /** + * The rule's line within a stylesheet + */ + get ruleLine() { + return this.domRule ? this.domRule.line : ""; + }, + + /** + * The rule's column within a stylesheet + */ + get ruleColumn() { + return this.domRule ? this.domRule.column : null; + }, + + /** + * Get display name for this rule based on the original source + * for this rule's style sheet. + * + * @return {Promise} + * Promise which resolves with location as an object containing + * both the full and short version of the source string. + */ + getOriginalSourceStrings: function () { + return this.domRule.getOriginalLocation().then(({href, + line, mediaText}) => { + let mediaString = mediaText ? " @" + mediaText : ""; + let linePart = line > 0 ? (":" + line) : ""; + + let sourceStrings = { + full: (href || CssLogic.l10n("rule.sourceInline")) + linePart + + mediaString, + short: CssLogic.shortSource({href: href}) + linePart + mediaString + }; + + return sourceStrings; + }); + }, + + /** + * Returns true if the rule matches the creation options + * specified. + * + * @param {Object} options + * Creation options. See the Rule constructor for documentation. + */ + matches: function (options) { + return this.style === options.rule; + }, + + /** + * Create a new TextProperty to include in the rule. + * + * @param {String} name + * The text property name (such as "background" or "border-top"). + * @param {String} value + * The property's value (not including priority). + * @param {String} priority + * The property's priority (either "important" or an empty string). + * @param {Boolean} enabled + * True if the property should be enabled. + * @param {TextProperty} siblingProp + * Optional, property next to which the new property will be added. + */ + createProperty: function (name, value, priority, enabled, siblingProp) { + let prop = new TextProperty(this, name, value, priority, enabled); + + let ind; + if (siblingProp) { + ind = this.textProps.indexOf(siblingProp) + 1; + this.textProps.splice(ind, 0, prop); + } else { + ind = this.textProps.length; + this.textProps.push(prop); + } + + this.applyProperties((modifications) => { + modifications.createProperty(ind, name, value, priority, enabled); + // Now that the rule has been updated, the server might have given us data + // that changes the state of the property. Update it now. + prop.updateEditor(); + }); + + return prop; + }, + + /** + * Helper function for applyProperties that is called when the actor + * does not support as-authored styles. Store disabled properties + * in the element style's store. + */ + _applyPropertiesNoAuthored: function (modifications) { + this.elementStyle.markOverriddenAll(); + + let disabledProps = []; + + for (let prop of this.textProps) { + if (prop.invisible) { + continue; + } + if (!prop.enabled) { + disabledProps.push({ + name: prop.name, + value: prop.value, + priority: prop.priority + }); + continue; + } + if (prop.value.trim() === "") { + continue; + } + + modifications.setProperty(-1, prop.name, prop.value, prop.priority); + + prop.updateComputed(); + } + + // Store disabled properties in the disabled store. + let disabled = this.elementStyle.store.disabled; + if (disabledProps.length > 0) { + disabled.set(this.style, disabledProps); + } else { + disabled.delete(this.style); + } + + return modifications.apply().then(() => { + let cssProps = {}; + // Note that even though StyleRuleActors normally provide parsed + // declarations already, _applyPropertiesNoAuthored is only used when + // connected to older backend that do not provide them. So parse here. + for (let cssProp of parseDeclarations(this.cssProperties.isKnown, + this.style.authoredText)) { + cssProps[cssProp.name] = cssProp; + } + + for (let textProp of this.textProps) { + if (!textProp.enabled) { + continue; + } + let cssProp = cssProps[textProp.name]; + + if (!cssProp) { + cssProp = { + name: textProp.name, + value: "", + priority: "" + }; + } + + textProp.priority = cssProp.priority; + } + }); + }, + + /** + * A helper for applyProperties that applies properties in the "as + * authored" case; that is, when the StyleRuleActor supports + * setRuleText. + */ + _applyPropertiesAuthored: function (modifications) { + return modifications.apply().then(() => { + // The rewriting may have required some other property values to + // change, e.g., to insert some needed terminators. Update the + // relevant properties here. + for (let index in modifications.changedDeclarations) { + let newValue = modifications.changedDeclarations[index]; + this.textProps[index].noticeNewValue(newValue); + } + // Recompute and redisplay the computed properties. + for (let prop of this.textProps) { + if (!prop.invisible && prop.enabled) { + prop.updateComputed(); + prop.updateEditor(); + } + } + }); + }, + + /** + * Reapply all the properties in this rule, and update their + * computed styles. Will re-mark overridden properties. Sets the + * |_applyingModifications| property to a promise which will resolve + * when the edit has completed. + * + * @param {Function} modifier a function that takes a RuleModificationList + * (or RuleRewriter) as an argument and that modifies it + * to apply the desired edit + * @return {Promise} a promise which will resolve when the edit + * is complete + */ + applyProperties: function (modifier) { + // If there is already a pending modification, we have to wait + // until it settles before applying the next modification. + let resultPromise = + promise.resolve(this._applyingModifications).then(() => { + let modifications = this.style.startModifyingProperties( + this.cssProperties); + modifier(modifications); + if (this.style.canSetRuleText) { + return this._applyPropertiesAuthored(modifications); + } + return this._applyPropertiesNoAuthored(modifications); + }).then(() => { + this.elementStyle.markOverriddenAll(); + + if (resultPromise === this._applyingModifications) { + this._applyingModifications = null; + this.elementStyle._changed(); + } + }).catch(promiseWarn); + + this._applyingModifications = resultPromise; + return resultPromise; + }, + + /** + * Renames a property. + * + * @param {TextProperty} property + * The property to rename. + * @param {String} name + * The new property name (such as "background" or "border-top"). + */ + setPropertyName: function (property, name) { + if (name === property.name) { + return; + } + + let oldName = property.name; + property.name = name; + let index = this.textProps.indexOf(property); + this.applyProperties((modifications) => { + modifications.renameProperty(index, oldName, name); + }); + }, + + /** + * Sets the value and priority of a property, then reapply all properties. + * + * @param {TextProperty} property + * The property to manipulate. + * @param {String} value + * The property's value (not including priority). + * @param {String} priority + * The property's priority (either "important" or an empty string). + */ + setPropertyValue: function (property, value, priority) { + if (value === property.value && priority === property.priority) { + return; + } + + property.value = value; + property.priority = priority; + + let index = this.textProps.indexOf(property); + this.applyProperties((modifications) => { + modifications.setProperty(index, property.name, value, priority); + }); + }, + + /** + * Just sets the value and priority of a property, in order to preview its + * effect on the content document. + * + * @param {TextProperty} property + * The property which value will be previewed + * @param {String} value + * The value to be used for the preview + * @param {String} priority + * The property's priority (either "important" or an empty string). + */ + previewPropertyValue: function (property, value, priority) { + let modifications = this.style.startModifyingProperties(this.cssProperties); + modifications.setProperty(this.textProps.indexOf(property), + property.name, value, priority); + modifications.apply().then(() => { + // Ensure dispatching a ruleview-changed event + // also for previews + this.elementStyle._changed(); + }); + }, + + /** + * Disables or enables given TextProperty. + * + * @param {TextProperty} property + * The property to enable/disable + * @param {Boolean} value + */ + setPropertyEnabled: function (property, value) { + if (property.enabled === !!value) { + return; + } + property.enabled = !!value; + let index = this.textProps.indexOf(property); + this.applyProperties((modifications) => { + modifications.setPropertyEnabled(index, property.name, property.enabled); + }); + }, + + /** + * Remove a given TextProperty from the rule and update the rule + * accordingly. + * + * @param {TextProperty} property + * The property to be removed + */ + removeProperty: function (property) { + let index = this.textProps.indexOf(property); + this.textProps.splice(index, 1); + // Need to re-apply properties in case removing this TextProperty + // exposes another one. + this.applyProperties((modifications) => { + modifications.removeProperty(index, property.name); + }); + }, + + /** + * Get the list of TextProperties from the style. Needs + * to parse the style's authoredText. + */ + _getTextProperties: function () { + let textProps = []; + let store = this.elementStyle.store; + + // Starting with FF49, StyleRuleActors provide parsed declarations. + let props = this.style.declarations; + if (!props.length) { + props = parseDeclarations(this.cssProperties.isKnown, + this.style.authoredText, true); + } + + for (let prop of props) { + let name = prop.name; + // If the authored text has an invalid property, it will show up + // as nameless. Skip these as we don't currently have a good + // way to display them. + if (!name) { + continue; + } + // In an inherited rule, we only show inherited properties. + // However, we must keep all properties in order for rule + // rewriting to work properly. So, compute the "invisible" + // property here. + let invisible = this.inherited && !this.cssProperties.isInherited(name); + let value = store.userProperties.getProperty(this.style, name, + prop.value); + let textProp = new TextProperty(this, name, value, prop.priority, + !("commentOffsets" in prop), + invisible); + textProps.push(textProp); + } + + return textProps; + }, + + /** + * Return the list of disabled properties from the store for this rule. + */ + _getDisabledProperties: function () { + let store = this.elementStyle.store; + + // Include properties from the disabled property store, if any. + let disabledProps = store.disabled.get(this.style); + if (!disabledProps) { + return []; + } + + let textProps = []; + + for (let prop of disabledProps) { + let value = store.userProperties.getProperty(this.style, prop.name, + prop.value); + let textProp = new TextProperty(this, prop.name, value, prop.priority); + textProp.enabled = false; + textProps.push(textProp); + } + + return textProps; + }, + + /** + * Reread the current state of the rules and rebuild text + * properties as needed. + */ + refresh: function (options) { + this.matchedSelectors = options.matchedSelectors || []; + let newTextProps = this._getTextProperties(); + + // Update current properties for each property present on the style. + // This will mark any touched properties with _visited so we + // can detect properties that weren't touched (because they were + // removed from the style). + // Also keep track of properties that didn't exist in the current set + // of properties. + let brandNewProps = []; + for (let newProp of newTextProps) { + if (!this._updateTextProperty(newProp)) { + brandNewProps.push(newProp); + } + } + + // Refresh editors and disabled state for all the properties that + // were updated. + for (let prop of this.textProps) { + // Properties that weren't touched during the update + // process must no longer exist on the node. Mark them disabled. + if (!prop._visited) { + prop.enabled = false; + prop.updateEditor(); + } else { + delete prop._visited; + } + } + + // Add brand new properties. + this.textProps = this.textProps.concat(brandNewProps); + + // Refresh the editor if one already exists. + if (this.editor) { + this.editor.populate(); + } + }, + + /** + * Update the current TextProperties that match a given property + * from the authoredText. Will choose one existing TextProperty to update + * with the new property's value, and will disable all others. + * + * When choosing the best match to reuse, properties will be chosen + * by assigning a rank and choosing the highest-ranked property: + * Name, value, and priority match, enabled. (6) + * Name, value, and priority match, disabled. (5) + * Name and value match, enabled. (4) + * Name and value match, disabled. (3) + * Name matches, enabled. (2) + * Name matches, disabled. (1) + * + * If no existing properties match the property, nothing happens. + * + * @param {TextProperty} newProp + * The current version of the property, as parsed from the + * authoredText in Rule._getTextProperties(). + * @return {Boolean} true if a property was updated, false if no properties + * were updated. + */ + _updateTextProperty: function (newProp) { + let match = { rank: 0, prop: null }; + + for (let prop of this.textProps) { + if (prop.name !== newProp.name) { + continue; + } + + // Mark this property visited. + prop._visited = true; + + // Start at rank 1 for matching name. + let rank = 1; + + // Value and Priority matches add 2 to the rank. + // Being enabled adds 1. This ranks better matches higher, + // with priority breaking ties. + if (prop.value === newProp.value) { + rank += 2; + if (prop.priority === newProp.priority) { + rank += 2; + } + } + + if (prop.enabled) { + rank += 1; + } + + if (rank > match.rank) { + if (match.prop) { + // We outrank a previous match, disable it. + match.prop.enabled = false; + match.prop.updateEditor(); + } + match.rank = rank; + match.prop = prop; + } else if (rank) { + // A previous match outranks us, disable ourself. + prop.enabled = false; + prop.updateEditor(); + } + } + + // If we found a match, update its value with the new text property + // value. + if (match.prop) { + match.prop.set(newProp); + return true; + } + + return false; + }, + + /** + * Jump between editable properties in the UI. If the focus direction is + * forward, begin editing the next property name if available or focus the + * new property editor otherwise. If the focus direction is backward, + * begin editing the previous property value or focus the selector editor if + * this is the first element in the property list. + * + * @param {TextProperty} textProperty + * The text property that will be left to focus on a sibling. + * @param {Number} direction + * The move focus direction number. + */ + editClosestTextProperty: function (textProperty, direction) { + let index = this.textProps.indexOf(textProperty); + + if (direction === Services.focus.MOVEFOCUS_FORWARD) { + for (++index; index < this.textProps.length; ++index) { + if (!this.textProps[index].invisible) { + break; + } + } + if (index === this.textProps.length) { + textProperty.rule.editor.closeBrace.click(); + } else { + this.textProps[index].editor.nameSpan.click(); + } + } else if (direction === Services.focus.MOVEFOCUS_BACKWARD) { + for (--index; index >= 0; --index) { + if (!this.textProps[index].invisible) { + break; + } + } + if (index < 0) { + textProperty.editor.ruleEditor.selectorText.click(); + } else { + this.textProps[index].editor.valueSpan.click(); + } + } + }, + + /** + * Return a string representation of the rule. + */ + stringifyRule: function () { + let selectorText = this.selectorText; + let cssText = ""; + let terminator = Services.appinfo.OS === "WINNT" ? "\r\n" : "\n"; + + for (let textProp of this.textProps) { + if (!textProp.invisible) { + cssText += "\t" + textProp.stringifyProperty() + terminator; + } + } + + return selectorText + " {" + terminator + cssText + "}"; + }, + + /** + * See whether this rule has any non-invisible properties. + * @return {Boolean} true if there is any visible property, or false + * if all properties are invisible + */ + hasAnyVisibleProperties: function () { + for (let prop of this.textProps) { + if (!prop.invisible) { + return true; + } + } + return false; + } +}; + +exports.Rule = Rule; diff --git a/devtools/client/inspector/rules/models/text-property.js b/devtools/client/inspector/rules/models/text-property.js new file mode 100644 index 000000000..3bbe6e91d --- /dev/null +++ b/devtools/client/inspector/rules/models/text-property.js @@ -0,0 +1,215 @@ +/* -*- 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 {escapeCSSComment} = require("devtools/shared/css/parsing-utils"); +const {getCssProperties} = require("devtools/shared/fronts/css-properties"); + +/** + * TextProperty is responsible for the following: + * Manages a single property from the authoredText attribute of the + * relevant declaration. + * Maintains a list of computed properties that come from this + * property declaration. + * Changes to the TextProperty are sent to its related Rule for + * application. + * + * @param {Rule} rule + * The rule this TextProperty came from. + * @param {String} name + * The text property name (such as "background" or "border-top"). + * @param {String} value + * The property's value (not including priority). + * @param {String} priority + * The property's priority (either "important" or an empty string). + * @param {Boolean} enabled + * Whether the property is enabled. + * @param {Boolean} invisible + * Whether the property is invisible. An invisible property + * does not show up in the UI; these are needed so that the + * index of a property in Rule.textProps is the same as the index + * coming from parseDeclarations. + */ +function TextProperty(rule, name, value, priority, enabled = true, + invisible = false) { + this.rule = rule; + this.name = name; + this.value = value; + this.priority = priority; + this.enabled = !!enabled; + this.invisible = invisible; + this.panelDoc = this.rule.elementStyle.ruleView.inspector.panelDoc; + + const toolbox = this.rule.elementStyle.ruleView.inspector.toolbox; + this.cssProperties = getCssProperties(toolbox); + + this.updateComputed(); +} + +TextProperty.prototype = { + /** + * Update the editor associated with this text property, + * if any. + */ + updateEditor: function () { + if (this.editor) { + this.editor.update(); + } + }, + + /** + * Update the list of computed properties for this text property. + */ + updateComputed: function () { + if (!this.name) { + return; + } + + // This is a bit funky. To get the list of computed properties + // for this text property, we'll set the property on a dummy element + // and see what the computed style looks like. + let dummyElement = this.rule.elementStyle.ruleView.dummyElement; + let dummyStyle = dummyElement.style; + dummyStyle.cssText = ""; + dummyStyle.setProperty(this.name, this.value, this.priority); + + this.computed = []; + + // Manually get all the properties that are set when setting a value on + // this.name and check the computed style on dummyElement for each one. + // If we just read dummyStyle, it would skip properties when value === "". + let subProps = this.cssProperties.getSubproperties(this.name); + + for (let prop of subProps) { + this.computed.push({ + textProp: this, + name: prop, + value: dummyStyle.getPropertyValue(prop), + priority: dummyStyle.getPropertyPriority(prop), + }); + } + }, + + /** + * Set all the values from another TextProperty instance into + * this TextProperty instance. + * + * @param {TextProperty} prop + * The other TextProperty instance. + */ + set: function (prop) { + let changed = false; + for (let item of ["name", "value", "priority", "enabled"]) { + if (this[item] !== prop[item]) { + this[item] = prop[item]; + changed = true; + } + } + + if (changed) { + this.updateEditor(); + } + }, + + setValue: function (value, priority, force = false) { + let store = this.rule.elementStyle.store; + + if (this.editor && value !== this.editor.committed.value || force) { + store.userProperties.setProperty(this.rule.style, this.name, value); + } + + this.rule.setPropertyValue(this, value, priority); + this.updateEditor(); + }, + + /** + * Called when the property's value has been updated externally, and + * the property and editor should update. + */ + noticeNewValue: function (value) { + if (value !== this.value) { + this.value = value; + this.updateEditor(); + } + }, + + setName: function (name) { + let store = this.rule.elementStyle.store; + + if (name !== this.name) { + store.userProperties.setProperty(this.rule.style, name, + this.editor.committed.value); + } + + this.rule.setPropertyName(this, name); + this.updateEditor(); + }, + + setEnabled: function (value) { + this.rule.setPropertyEnabled(this, value); + this.updateEditor(); + }, + + remove: function () { + this.rule.removeProperty(this); + }, + + /** + * Return a string representation of the rule property. + */ + stringifyProperty: function () { + // Get the displayed property value + let declaration = this.name + ": " + this.editor.valueSpan.textContent + + ";"; + + // Comment out property declarations that are not enabled + if (!this.enabled) { + declaration = "/* " + escapeCSSComment(declaration) + " */"; + } + + return declaration; + }, + + /** + * See whether this property's name is known. + * + * @return {Boolean} true if the property name is known, false otherwise. + */ + isKnownProperty: function () { + return this.cssProperties.isKnown(this.name); + }, + + /** + * Validate this property. Does it make sense for this value to be assigned + * to this property name? + * + * @return {Boolean} true if the property value is valid, false otherwise. + */ + isValid: function () { + // Starting with FF49, StyleRuleActors provide a list of parsed + // declarations, with data about their validity, but if we don't have this, + // compute validity locally (which might not be correct, but better than + // nothing). + if (!this.rule.domRule.declarations) { + return this.cssProperties.isValidOnClient(this.name, this.value, this.panelDoc); + } + + let selfIndex = this.rule.textProps.indexOf(this); + + // When adding a new property in the rule-view, the TextProperty object is + // created right away before the rule gets updated on the server, so we're + // not going to find the corresponding declaration object yet. Default to + // true. + if (!this.rule.domRule.declarations[selfIndex]) { + return true; + } + + return this.rule.domRule.declarations[selfIndex].isValid; + } +}; + +exports.TextProperty = TextProperty; diff --git a/devtools/client/inspector/rules/moz.build b/devtools/client/inspector/rules/moz.build new file mode 100644 index 000000000..e826c1414 --- /dev/null +++ b/devtools/client/inspector/rules/moz.build @@ -0,0 +1,16 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIRS += [ + 'models', + 'views', +] + +DevToolsModules( + 'rules.js', +) + +BROWSER_CHROME_MANIFESTS += ['test/browser.ini'] diff --git a/devtools/client/inspector/rules/rules.js b/devtools/client/inspector/rules/rules.js new file mode 100644 index 000000000..8c5ec7617 --- /dev/null +++ b/devtools/client/inspector/rules/rules.js @@ -0,0 +1,1673 @@ +/* -*- 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 promise = require("promise"); +const Services = require("Services"); +const {Task} = require("devtools/shared/task"); +const {Tools} = require("devtools/client/definitions"); +const {l10n} = require("devtools/shared/inspector/css-logic"); +const {ELEMENT_STYLE} = require("devtools/shared/specs/styles"); +const {OutputParser} = require("devtools/client/shared/output-parser"); +const {PrefObserver, PREF_ORIG_SOURCES} = require("devtools/client/styleeditor/utils"); +const {ElementStyle} = require("devtools/client/inspector/rules/models/element-style"); +const {Rule} = require("devtools/client/inspector/rules/models/rule"); +const {RuleEditor} = require("devtools/client/inspector/rules/views/rule-editor"); +const {gDevTools} = require("devtools/client/framework/devtools"); +const {getCssProperties} = require("devtools/shared/fronts/css-properties"); +const HighlightersOverlay = require("devtools/client/inspector/shared/highlighters-overlay"); +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 StyleInspectorMenu = require("devtools/client/inspector/shared/style-inspector-menu"); +const TooltipsOverlay = require("devtools/client/inspector/shared/tooltips-overlay"); +const {createChild, promiseWarn, throttle} = require("devtools/client/inspector/shared/utils"); +const EventEmitter = require("devtools/shared/event-emitter"); +const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts"); +const clipboardHelper = require("devtools/shared/platform/clipboard"); +const {AutocompletePopup} = require("devtools/client/shared/autocomplete-popup"); + +const HTML_NS = "http://www.w3.org/1999/xhtml"; +const PREF_UA_STYLES = "devtools.inspector.showUserAgentStyles"; +const PREF_DEFAULT_COLOR_UNIT = "devtools.defaultColorUnit"; +const PREF_ENABLE_MDN_DOCS_TOOLTIP = + "devtools.inspector.mdnDocsTooltip.enabled"; +const FILTER_CHANGED_TIMEOUT = 150; + +// This is used to parse user input when filtering. +const FILTER_PROP_RE = /\s*([^:\s]*)\s*:\s*(.*?)\s*;?$/; +// This is used to parse the filter search value to see if the filter +// should be strict or not +const FILTER_STRICT_RE = /\s*`(.*?)`\s*$/; + +/** + * Our model looks like this: + * + * ElementStyle: + * Responsible for keeping track of which properties are overridden. + * Maintains a list of Rule objects that apply to the element. + * Rule: + * Manages a single style declaration or rule. + * Responsible for applying changes to the properties in a rule. + * Maintains a list of TextProperty objects. + * TextProperty: + * Manages a single property from the authoredText attribute of the + * relevant declaration. + * Maintains a list of computed properties that come from this + * property declaration. + * Changes to the TextProperty are sent to its related Rule for + * application. + * + * View hierarchy mostly follows the model hierarchy. + * + * CssRuleView: + * Owns an ElementStyle and creates a list of RuleEditors for its + * Rules. + * RuleEditor: + * Owns a Rule object and creates a list of TextPropertyEditors + * for its TextProperties. + * Manages creation of new text properties. + * TextPropertyEditor: + * Owns a TextProperty object. + * Manages changes to the TextProperty. + * Can be expanded to display computed properties. + * Can mark a property disabled or enabled. + */ + +/** + * CssRuleView is a view of the style rules and declarations that + * apply to a given element. After construction, the 'element' + * property will be available with the user interface. + * + * @param {Inspector} inspector + * Inspector toolbox panel + * @param {Document} document + * The document that will contain the rule view. + * @param {Object} store + * The CSS rule view can use this object to store metadata + * that might outlast the rule view, particularly the current + * set of disabled properties. + * @param {PageStyleFront} pageStyle + * The PageStyleFront for communicating with the remote server. + */ +function CssRuleView(inspector, document, store, pageStyle) { + this.inspector = inspector; + this.styleDocument = document; + this.styleWindow = this.styleDocument.defaultView; + this.store = store || {}; + this.pageStyle = pageStyle; + + // Allow tests to override throttling behavior, as this can cause intermittents. + this.throttle = throttle; + + this.cssProperties = getCssProperties(inspector.toolbox); + + this._outputParser = new OutputParser(document, this.cssProperties); + + this._onAddRule = this._onAddRule.bind(this); + this._onContextMenu = this._onContextMenu.bind(this); + this._onCopy = this._onCopy.bind(this); + this._onFilterStyles = this._onFilterStyles.bind(this); + this._onClearSearch = this._onClearSearch.bind(this); + this._onTogglePseudoClassPanel = this._onTogglePseudoClassPanel.bind(this); + this._onTogglePseudoClass = this._onTogglePseudoClass.bind(this); + + let doc = this.styleDocument; + this.element = doc.getElementById("ruleview-container-focusable"); + this.addRuleButton = doc.getElementById("ruleview-add-rule-button"); + this.searchField = doc.getElementById("ruleview-searchbox"); + this.searchClearButton = doc.getElementById("ruleview-searchinput-clear"); + this.pseudoClassPanel = doc.getElementById("pseudo-class-panel"); + this.pseudoClassToggle = doc.getElementById("pseudo-class-panel-toggle"); + this.hoverCheckbox = doc.getElementById("pseudo-hover-toggle"); + this.activeCheckbox = doc.getElementById("pseudo-active-toggle"); + this.focusCheckbox = doc.getElementById("pseudo-focus-toggle"); + + this.searchClearButton.hidden = true; + + this.shortcuts = new KeyShortcuts({ window: this.styleWindow }); + this._onShortcut = this._onShortcut.bind(this); + this.shortcuts.on("Escape", this._onShortcut); + this.shortcuts.on("Return", this._onShortcut); + this.shortcuts.on("Space", this._onShortcut); + this.shortcuts.on("CmdOrCtrl+F", this._onShortcut); + this.element.addEventListener("copy", this._onCopy); + this.element.addEventListener("contextmenu", this._onContextMenu); + this.addRuleButton.addEventListener("click", this._onAddRule); + this.searchField.addEventListener("input", this._onFilterStyles); + this.searchField.addEventListener("contextmenu", this.inspector.onTextBoxContextMenu); + this.searchClearButton.addEventListener("click", this._onClearSearch); + this.pseudoClassToggle.addEventListener("click", + this._onTogglePseudoClassPanel); + this.hoverCheckbox.addEventListener("click", this._onTogglePseudoClass); + this.activeCheckbox.addEventListener("click", this._onTogglePseudoClass); + this.focusCheckbox.addEventListener("click", this._onTogglePseudoClass); + + this._handlePrefChange = this._handlePrefChange.bind(this); + this._onSourcePrefChanged = this._onSourcePrefChanged.bind(this); + + this._prefObserver = new PrefObserver("devtools."); + this._prefObserver.on(PREF_ORIG_SOURCES, this._onSourcePrefChanged); + this._prefObserver.on(PREF_UA_STYLES, this._handlePrefChange); + this._prefObserver.on(PREF_DEFAULT_COLOR_UNIT, this._handlePrefChange); + this._prefObserver.on(PREF_ENABLE_MDN_DOCS_TOOLTIP, this._handlePrefChange); + + this.showUserAgentStyles = Services.prefs.getBoolPref(PREF_UA_STYLES); + this.enableMdnDocsTooltip = + Services.prefs.getBoolPref(PREF_ENABLE_MDN_DOCS_TOOLTIP); + + // The popup will be attached to the toolbox document. + this.popup = new AutocompletePopup(inspector._toolbox.doc, { + autoSelect: true, + theme: "auto" + }); + + this._showEmpty(); + + this._contextmenu = new StyleInspectorMenu(this, { isRuleView: true }); + + // Add the tooltips and highlighters to the view + this.tooltips = new TooltipsOverlay(this); + this.tooltips.addToView(); + this.highlighters = new HighlightersOverlay(this); + this.highlighters.addToView(); + + EventEmitter.decorate(this); +} + +CssRuleView.prototype = { + // The element that we're inspecting. + _viewedElement: null, + + // Used for cancelling timeouts in the style filter. + _filterChangedTimeout: null, + + // Empty, unconnected element of the same type as this node, used + // to figure out how shorthand properties will be parsed. + _dummyElement: null, + + // Get the dummy elemenet. + get dummyElement() { + return this._dummyElement; + }, + + // Get the filter search value. + get searchValue() { + return this.searchField.value.toLowerCase(); + }, + + /** + * Get an instance of SelectorHighlighter (used to highlight nodes that match + * selectors in the rule-view). A new instance is only created the first time + * this function is called. The same instance will then be returned. + * + * @return {Promise} Resolves to the instance of the highlighter. + */ + getSelectorHighlighter: Task.async(function* () { + let utils = this.inspector.toolbox.highlighterUtils; + if (!utils.supportsCustomHighlighters()) { + return null; + } + + if (this.selectorHighlighter) { + return this.selectorHighlighter; + } + + try { + let h = yield utils.getHighlighterByType("SelectorHighlighter"); + this.selectorHighlighter = h; + return h; + } catch (e) { + // The SelectorHighlighter type could not be created in the + // current target. It could be an older server, or a XUL page. + return null; + } + }), + + /** + * Highlight/unhighlight all the nodes that match a given set of selectors + * inside the document of the current selected node. + * Only one selector can be highlighted at a time, so calling the method a + * second time with a different selector will first unhighlight the previously + * highlighted nodes. + * Calling the method a second time with the same selector will just + * unhighlight the highlighted nodes. + * + * @param {DOMNode} selectorIcon + * The icon that was clicked to toggle the selector. The + * class 'highlighted' will be added when the selector is + * highlighted. + * @param {String} selector + * The selector used to find nodes in the page. + */ + toggleSelectorHighlighter: function (selectorIcon, selector) { + if (this.lastSelectorIcon) { + this.lastSelectorIcon.classList.remove("highlighted"); + } + selectorIcon.classList.remove("highlighted"); + + this.unhighlightSelector().then(() => { + if (selector !== this.highlighters.selectorHighlighterShown) { + this.highlighters.selectorHighlighterShown = selector; + selectorIcon.classList.add("highlighted"); + this.lastSelectorIcon = selectorIcon; + this.highlightSelector(selector).then(() => { + this.emit("ruleview-selectorhighlighter-toggled", true); + }, e => console.error(e)); + } else { + this.highlighters.selectorHighlighterShown = null; + this.emit("ruleview-selectorhighlighter-toggled", false); + } + }, e => console.error(e)); + }, + + highlightSelector: Task.async(function* (selector) { + let node = this.inspector.selection.nodeFront; + + let highlighter = yield this.getSelectorHighlighter(); + if (!highlighter) { + return; + } + + yield highlighter.show(node, { + hideInfoBar: true, + hideGuides: true, + selector + }); + }), + + unhighlightSelector: Task.async(function* () { + let highlighter = yield this.getSelectorHighlighter(); + if (!highlighter) { + return; + } + + yield highlighter.hide(); + }), + + /** + * Get the type of a given node in the rule-view + * + * @param {DOMNode} node + * The node which we want information about + * @return {Object} The type information object contains the following props: + * - type {String} One of the VIEW_NODE_XXX_TYPE const in + * client/inspector/shared/node-types + * - value {Object} Depends on the type of the node + * returns null of the node isn't anything we care about + */ + getNodeInfo: function (node) { + if (!node) { + return null; + } + + let type, value; + let classes = node.classList; + let prop = getParentTextProperty(node); + + if (classes.contains("ruleview-propertyname") && prop) { + type = VIEW_NODE_PROPERTY_TYPE; + value = { + property: node.textContent, + value: getPropertyNameAndValue(node).value, + enabled: prop.enabled, + overridden: prop.overridden, + pseudoElement: prop.rule.pseudoElement, + sheetHref: prop.rule.domRule.href, + textProperty: prop + }; + } else if (classes.contains("ruleview-propertyvalue") && prop) { + type = VIEW_NODE_VALUE_TYPE; + value = { + property: getPropertyNameAndValue(node).name, + value: node.textContent, + enabled: prop.enabled, + overridden: prop.overridden, + pseudoElement: prop.rule.pseudoElement, + sheetHref: prop.rule.domRule.href, + textProperty: prop + }; + } else if (classes.contains("theme-link") && + !classes.contains("ruleview-rule-source") && prop) { + type = VIEW_NODE_IMAGE_URL_TYPE; + value = { + property: getPropertyNameAndValue(node).name, + value: node.parentNode.textContent, + url: node.href, + enabled: prop.enabled, + overridden: prop.overridden, + pseudoElement: prop.rule.pseudoElement, + sheetHref: prop.rule.domRule.href, + textProperty: prop + }; + } else if (classes.contains("ruleview-selector-unmatched") || + classes.contains("ruleview-selector-matched") || + classes.contains("ruleview-selectorcontainer") || + classes.contains("ruleview-selector") || + classes.contains("ruleview-selector-attribute") || + classes.contains("ruleview-selector-pseudo-class") || + classes.contains("ruleview-selector-pseudo-class-lock")) { + type = VIEW_NODE_SELECTOR_TYPE; + value = this._getRuleEditorForNode(node).selectorText.textContent; + } else if (classes.contains("ruleview-rule-source") || + classes.contains("ruleview-rule-source-label")) { + type = VIEW_NODE_LOCATION_TYPE; + let rule = this._getRuleEditorForNode(node).rule; + value = (rule.sheet && rule.sheet.href) ? rule.sheet.href : rule.title; + } else { + return null; + } + + return {type, value}; + }, + + /** + * Retrieve the RuleEditor instance that should be stored on + * the offset parent of the node + */ + _getRuleEditorForNode: function (node) { + if (!node.offsetParent) { + // some nodes don't have an offsetParent, but their parentNode does + node = node.parentNode; + } + return node.offsetParent._ruleEditor; + }, + + /** + * Context menu handler. + */ + _onContextMenu: function (event) { + this._contextmenu.show(event); + }, + + /** + * Callback for copy event. Copy the selected text. + * + * @param {Event} event + * copy event object. + */ + _onCopy: function (event) { + if (event) { + this.copySelection(event.target); + event.preventDefault(); + } + }, + + /** + * Copy the current selection. The current target is necessary + * if the selection is inside an input or a textarea + * + * @param {DOMNode} target + * DOMNode target of the copy action + */ + copySelection: function (target) { + try { + let text = ""; + + let nodeName = target && target.nodeName; + if (nodeName === "input" || nodeName == "textarea") { + let start = Math.min(target.selectionStart, target.selectionEnd); + let end = Math.max(target.selectionStart, target.selectionEnd); + let count = end - start; + text = target.value.substr(start, count); + } else { + text = this.styleWindow.getSelection().toString(); + + // Remove any double newlines. + text = text.replace(/(\r?\n)\r?\n/g, "$1"); + } + + clipboardHelper.copyString(text); + } catch (e) { + console.error(e); + } + }, + + /** + * A helper for _onAddRule that handles the case where the actor + * does not support as-authored styles. + */ + _onAddNewRuleNonAuthored: function () { + let elementStyle = this._elementStyle; + let element = elementStyle.element; + let rules = elementStyle.rules; + let pseudoClasses = element.pseudoClassLocks; + + this.pageStyle.addNewRule(element, pseudoClasses).then(options => { + let newRule = new Rule(elementStyle, options); + rules.push(newRule); + let editor = new RuleEditor(this, newRule); + newRule.editor = editor; + + // Insert the new rule editor after the inline element rule + if (rules.length <= 1) { + this.element.appendChild(editor.element); + } else { + for (let rule of rules) { + if (rule.domRule.type === ELEMENT_STYLE) { + let referenceElement = rule.editor.element.nextSibling; + this.element.insertBefore(editor.element, referenceElement); + break; + } + } + } + + // Focus and make the new rule's selector editable + editor.selectorText.click(); + elementStyle._changed(); + }); + }, + + /** + * Add a new rule to the current element. + */ + _onAddRule: function () { + let elementStyle = this._elementStyle; + let element = elementStyle.element; + let client = this.inspector.target.client; + let pseudoClasses = element.pseudoClassLocks; + + if (!client.traits.addNewRule) { + return; + } + + if (!this.pageStyle.supportsAuthoredStyles) { + // We're talking to an old server. + this._onAddNewRuleNonAuthored(); + return; + } + + // Adding a new rule with authored styles will cause the actor to + // emit an event, which will in turn cause the rule view to be + // updated. So, we wait for this update and for the rule creation + // request to complete, and then focus the new rule's selector. + let eventPromise = this.once("ruleview-refreshed"); + let newRulePromise = this.pageStyle.addNewRule(element, pseudoClasses); + promise.all([eventPromise, newRulePromise]).then((values) => { + let options = values[1]; + // Be sure the reference the correct |rules| here. + for (let rule of this._elementStyle.rules) { + if (options.rule === rule.domRule) { + rule.editor.selectorText.click(); + elementStyle._changed(); + break; + } + } + }); + }, + + /** + * Disables add rule button when needed + */ + refreshAddRuleButtonState: function () { + let shouldBeDisabled = !this._viewedElement || + !this.inspector.selection.isElementNode() || + this.inspector.selection.isAnonymousNode(); + this.addRuleButton.disabled = shouldBeDisabled; + }, + + setPageStyle: function (pageStyle) { + this.pageStyle = pageStyle; + }, + + /** + * Return {Boolean} true if the rule view currently has an input + * editor visible. + */ + get isEditing() { + return this.tooltips.isEditing || + this.element.querySelectorAll(".styleinspector-propertyeditor") + .length > 0; + }, + + _handlePrefChange: function (pref) { + if (pref === PREF_UA_STYLES) { + this.showUserAgentStyles = Services.prefs.getBoolPref(pref); + } + + // Reselect the currently selected element + let refreshOnPrefs = [PREF_UA_STYLES, PREF_DEFAULT_COLOR_UNIT]; + if (refreshOnPrefs.indexOf(pref) > -1) { + this.selectElement(this._viewedElement, true); + } + }, + + /** + * Update source links when pref for showing original sources changes + */ + _onSourcePrefChanged: function () { + if (this._elementStyle && this._elementStyle.rules) { + for (let rule of this._elementStyle.rules) { + if (rule.editor) { + rule.editor.updateSourceLink(); + } + } + this.inspector.emit("rule-view-sourcelinks-updated"); + } + }, + + /** + * Set the filter style search value. + * @param {String} value + * The search value. + */ + setFilterStyles: function (value = "") { + this.searchField.value = value; + this.searchField.focus(); + this._onFilterStyles(); + }, + + /** + * Called when the user enters a search term in the filter style search box. + */ + _onFilterStyles: function () { + if (this._filterChangedTimeout) { + clearTimeout(this._filterChangedTimeout); + } + + let filterTimeout = (this.searchValue.length > 0) ? + FILTER_CHANGED_TIMEOUT : 0; + this.searchClearButton.hidden = this.searchValue.length === 0; + + this._filterChangedTimeout = setTimeout(() => { + if (this.searchField.value.length > 0) { + this.searchField.setAttribute("filled", true); + } else { + this.searchField.removeAttribute("filled"); + } + + this.searchData = { + searchPropertyMatch: FILTER_PROP_RE.exec(this.searchValue), + searchPropertyName: this.searchValue, + searchPropertyValue: this.searchValue, + strictSearchValue: "", + strictSearchPropertyName: false, + strictSearchPropertyValue: false, + strictSearchAllValues: false + }; + + if (this.searchData.searchPropertyMatch) { + // Parse search value as a single property line and extract the + // property name and value. If the parsed property name or value is + // contained in backquotes (`), extract the value within the backquotes + // and set the corresponding strict search for the property to true. + if (FILTER_STRICT_RE.test(this.searchData.searchPropertyMatch[1])) { + this.searchData.strictSearchPropertyName = true; + this.searchData.searchPropertyName = + FILTER_STRICT_RE.exec(this.searchData.searchPropertyMatch[1])[1]; + } else { + this.searchData.searchPropertyName = + this.searchData.searchPropertyMatch[1]; + } + + if (FILTER_STRICT_RE.test(this.searchData.searchPropertyMatch[2])) { + this.searchData.strictSearchPropertyValue = true; + this.searchData.searchPropertyValue = + FILTER_STRICT_RE.exec(this.searchData.searchPropertyMatch[2])[1]; + } else { + this.searchData.searchPropertyValue = + this.searchData.searchPropertyMatch[2]; + } + + // Strict search for stylesheets will match the property line regex. + // Extract the search value within the backquotes to be used + // in the strict search for stylesheets in _highlightStyleSheet. + if (FILTER_STRICT_RE.test(this.searchValue)) { + this.searchData.strictSearchValue = + FILTER_STRICT_RE.exec(this.searchValue)[1]; + } + } else if (FILTER_STRICT_RE.test(this.searchValue)) { + // If the search value does not correspond to a property line and + // is contained in backquotes, extract the search value within the + // backquotes and set the flag to perform a strict search for all + // the values (selector, stylesheet, property and computed values). + let searchValue = FILTER_STRICT_RE.exec(this.searchValue)[1]; + this.searchData.strictSearchAllValues = true; + this.searchData.searchPropertyName = searchValue; + this.searchData.searchPropertyValue = searchValue; + this.searchData.strictSearchValue = searchValue; + } + + this._clearHighlight(this.element); + this._clearRules(); + this._createEditors(); + + this.inspector.emit("ruleview-filtered"); + + this._filterChangeTimeout = null; + }, filterTimeout); + }, + + /** + * Called when the user clicks on the clear button in the filter style search + * box. Returns true if the search box is cleared and false otherwise. + */ + _onClearSearch: function () { + if (this.searchField.value) { + this.setFilterStyles(""); + return true; + } + + return false; + }, + + destroy: function () { + this.isDestroyed = true; + this.clear(); + + this._dummyElement = null; + + this._prefObserver.off(PREF_ORIG_SOURCES, this._onSourcePrefChanged); + this._prefObserver.off(PREF_UA_STYLES, this._handlePrefChange); + this._prefObserver.off(PREF_DEFAULT_COLOR_UNIT, this._handlePrefChange); + this._prefObserver.destroy(); + + this._outputParser = null; + + // Remove context menu + if (this._contextmenu) { + this._contextmenu.destroy(); + this._contextmenu = null; + } + + this.tooltips.destroy(); + this.highlighters.destroy(); + + // Remove bound listeners + this.shortcuts.destroy(); + this.element.removeEventListener("copy", this._onCopy); + this.element.removeEventListener("contextmenu", this._onContextMenu); + this.addRuleButton.removeEventListener("click", this._onAddRule); + this.searchField.removeEventListener("input", this._onFilterStyles); + this.searchField.removeEventListener("contextmenu", + this.inspector.onTextBoxContextMenu); + this.searchClearButton.removeEventListener("click", this._onClearSearch); + this.pseudoClassToggle.removeEventListener("click", + this._onTogglePseudoClassPanel); + this.hoverCheckbox.removeEventListener("click", this._onTogglePseudoClass); + this.activeCheckbox.removeEventListener("click", this._onTogglePseudoClass); + this.focusCheckbox.removeEventListener("click", this._onTogglePseudoClass); + + this.searchField = null; + this.searchClearButton = null; + this.pseudoClassPanel = null; + this.pseudoClassToggle = null; + this.hoverCheckbox = null; + this.activeCheckbox = null; + this.focusCheckbox = null; + + this.inspector = null; + this.styleDocument = null; + this.styleWindow = null; + + if (this.element.parentNode) { + this.element.parentNode.removeChild(this.element); + } + + if (this._elementStyle) { + this._elementStyle.destroy(); + } + + this.popup.destroy(); + }, + + /** + * Mark the view as selecting an element, disabling all interaction, and + * visually clearing the view after a few milliseconds to avoid confusion + * about which element's styles the rule view shows. + */ + _startSelectingElement: function () { + this.element.classList.add("non-interactive"); + }, + + /** + * Mark the view as no longer selecting an element, re-enabling interaction. + */ + _stopSelectingElement: function () { + this.element.classList.remove("non-interactive"); + }, + + /** + * Update the view with a new selected element. + * + * @param {NodeActor} element + * The node whose style rules we'll inspect. + * @param {Boolean} allowRefresh + * Update the view even if the element is the same as last time. + */ + selectElement: function (element, allowRefresh = false) { + let refresh = (this._viewedElement === element); + if (refresh && !allowRefresh) { + return promise.resolve(undefined); + } + + if (this.popup.isOpen) { + this.popup.hidePopup(); + } + + this.clear(false); + this._viewedElement = element; + + this.clearPseudoClassPanel(); + this.refreshAddRuleButtonState(); + + if (!this._viewedElement) { + this._stopSelectingElement(); + this._clearRules(); + this._showEmpty(); + this.refreshPseudoClassPanel(); + return promise.resolve(undefined); + } + + // To figure out how shorthand properties are interpreted by the + // engine, we will set properties on a dummy element and observe + // how their .style attribute reflects them as computed values. + let dummyElementPromise = promise.resolve(this.styleDocument).then(document => { + // ::before and ::after do not have a namespaceURI + let namespaceURI = this.element.namespaceURI || + document.documentElement.namespaceURI; + this._dummyElement = document.createElementNS(namespaceURI, + this.element.tagName); + }).then(null, promiseWarn); + + let elementStyle = new ElementStyle(element, this, this.store, + this.pageStyle, this.showUserAgentStyles); + this._elementStyle = elementStyle; + + this._startSelectingElement(); + + return dummyElementPromise.then(() => { + if (this._elementStyle === elementStyle) { + return this._populate(); + } + return undefined; + }).then(() => { + if (this._elementStyle === elementStyle) { + if (!refresh) { + this.element.scrollTop = 0; + } + this._stopSelectingElement(); + this._elementStyle.onChanged = () => { + this._changed(); + }; + } + }).then(null, e => { + if (this._elementStyle === elementStyle) { + this._stopSelectingElement(); + this._clearRules(); + } + console.error(e); + }); + }, + + /** + * Update the rules for the currently highlighted element. + */ + refreshPanel: function () { + // Ignore refreshes during editing or when no element is selected. + if (this.isEditing || !this._elementStyle) { + return promise.resolve(undefined); + } + + // Repopulate the element style once the current modifications are done. + let promises = []; + for (let rule of this._elementStyle.rules) { + if (rule._applyingModifications) { + promises.push(rule._applyingModifications); + } + } + + return promise.all(promises).then(() => { + return this._populate(); + }); + }, + + /** + * Clear the pseudo class options panel by removing the checked and disabled + * attributes for each checkbox. + */ + clearPseudoClassPanel: function () { + this.hoverCheckbox.checked = this.hoverCheckbox.disabled = false; + this.activeCheckbox.checked = this.activeCheckbox.disabled = false; + this.focusCheckbox.checked = this.focusCheckbox.disabled = false; + }, + + /** + * Update the pseudo class options for the currently highlighted element. + */ + refreshPseudoClassPanel: function () { + if (!this._elementStyle || !this.inspector.selection.isElementNode()) { + this.hoverCheckbox.disabled = true; + this.activeCheckbox.disabled = true; + this.focusCheckbox.disabled = true; + return; + } + + for (let pseudoClassLock of this._elementStyle.element.pseudoClassLocks) { + switch (pseudoClassLock) { + case ":hover": { + this.hoverCheckbox.checked = true; + break; + } + case ":active": { + this.activeCheckbox.checked = true; + break; + } + case ":focus": { + this.focusCheckbox.checked = true; + break; + } + } + } + }, + + _populate: function () { + let elementStyle = this._elementStyle; + return this._elementStyle.populate().then(() => { + if (this._elementStyle !== elementStyle || this.isDestroyed) { + return null; + } + + this._clearRules(); + let onEditorsReady = this._createEditors(); + this.refreshPseudoClassPanel(); + + // Notify anyone that cares that we refreshed. + return onEditorsReady.then(() => { + this.emit("ruleview-refreshed"); + }, e => console.error(e)); + }).then(null, promiseWarn); + }, + + /** + * Show the user that the rule view has no node selected. + */ + _showEmpty: function () { + if (this.styleDocument.getElementById("ruleview-no-results")) { + return; + } + + createChild(this.element, "div", { + id: "ruleview-no-results", + textContent: l10n("rule.empty") + }); + }, + + /** + * Clear the rules. + */ + _clearRules: function () { + this.element.innerHTML = ""; + }, + + /** + * Clear the rule view. + */ + clear: function (clearDom = true) { + this.lastSelectorIcon = null; + + if (clearDom) { + this._clearRules(); + } + this._viewedElement = null; + + if (this._elementStyle) { + this._elementStyle.destroy(); + this._elementStyle = null; + } + }, + + /** + * Called when the user has made changes to the ElementStyle. + * Emits an event that clients can listen to. + */ + _changed: function () { + this.emit("ruleview-changed"); + }, + + /** + * Text for header that shows above rules for this element + */ + get selectedElementLabel() { + if (this._selectedElementLabel) { + return this._selectedElementLabel; + } + this._selectedElementLabel = l10n("rule.selectedElement"); + return this._selectedElementLabel; + }, + + /** + * Text for header that shows above rules for pseudo elements + */ + get pseudoElementLabel() { + if (this._pseudoElementLabel) { + return this._pseudoElementLabel; + } + this._pseudoElementLabel = l10n("rule.pseudoElement"); + return this._pseudoElementLabel; + }, + + get showPseudoElements() { + if (this._showPseudoElements === undefined) { + this._showPseudoElements = + Services.prefs.getBoolPref("devtools.inspector.show_pseudo_elements"); + } + return this._showPseudoElements; + }, + + /** + * Creates an expandable container in the rule view + * + * @param {String} label + * The label for the container header + * @param {Boolean} isPseudo + * Whether or not the container will hold pseudo element rules + * @return {DOMNode} The container element + */ + createExpandableContainer: function (label, isPseudo = false) { + let header = this.styleDocument.createElementNS(HTML_NS, "div"); + header.className = this._getRuleViewHeaderClassName(true); + header.textContent = label; + + let twisty = this.styleDocument.createElementNS(HTML_NS, "span"); + twisty.className = "ruleview-expander theme-twisty"; + twisty.setAttribute("open", "true"); + + header.insertBefore(twisty, header.firstChild); + this.element.appendChild(header); + + let container = this.styleDocument.createElementNS(HTML_NS, "div"); + container.classList.add("ruleview-expandable-container"); + container.hidden = false; + this.element.appendChild(container); + + header.addEventListener("dblclick", () => { + this._toggleContainerVisibility(twisty, container, isPseudo, + !this.showPseudoElements); + }, false); + + twisty.addEventListener("click", () => { + this._toggleContainerVisibility(twisty, container, isPseudo, + !this.showPseudoElements); + }, false); + + if (isPseudo) { + this._toggleContainerVisibility(twisty, container, isPseudo, + this.showPseudoElements); + } + + return container; + }, + + /** + * Toggle the visibility of an expandable container + * + * @param {DOMNode} twisty + * Clickable toggle DOM Node + * @param {DOMNode} container + * Expandable container DOM Node + * @param {Boolean} isPseudo + * Whether or not the container will hold pseudo element rules + * @param {Boolean} showPseudo + * Whether or not pseudo element rules should be displayed + */ + _toggleContainerVisibility: function (twisty, container, isPseudo, + showPseudo) { + let isOpen = twisty.getAttribute("open"); + + if (isPseudo) { + this._showPseudoElements = !!showPseudo; + + Services.prefs.setBoolPref("devtools.inspector.show_pseudo_elements", + this.showPseudoElements); + + container.hidden = !this.showPseudoElements; + isOpen = !this.showPseudoElements; + } else { + container.hidden = !container.hidden; + } + + if (isOpen) { + twisty.removeAttribute("open"); + } else { + twisty.setAttribute("open", "true"); + } + }, + + _getRuleViewHeaderClassName: function (isPseudo) { + let baseClassName = "theme-gutter ruleview-header"; + return isPseudo ? baseClassName + " ruleview-expandable-header" : + baseClassName; + }, + + /** + * Creates editor UI for each of the rules in _elementStyle. + */ + _createEditors: function () { + // Run through the current list of rules, attaching + // their editors in order. Create editors if needed. + let lastInheritedSource = ""; + let lastKeyframes = null; + let seenPseudoElement = false; + let seenNormalElement = false; + let seenSearchTerm = false; + let container = null; + + if (!this._elementStyle.rules) { + return promise.resolve(); + } + + let editorReadyPromises = []; + for (let rule of this._elementStyle.rules) { + if (rule.domRule.system) { + continue; + } + + // Initialize rule editor + if (!rule.editor) { + rule.editor = new RuleEditor(this, rule); + editorReadyPromises.push(rule.editor.once("source-link-updated")); + } + + // Filter the rules and highlight any matches if there is a search input + if (this.searchValue && this.searchData) { + if (this.highlightRule(rule)) { + seenSearchTerm = true; + } else if (rule.domRule.type !== ELEMENT_STYLE) { + continue; + } + } + + // Only print header for this element if there are pseudo elements + if (seenPseudoElement && !seenNormalElement && !rule.pseudoElement) { + seenNormalElement = true; + let div = this.styleDocument.createElementNS(HTML_NS, "div"); + div.className = this._getRuleViewHeaderClassName(); + div.textContent = this.selectedElementLabel; + this.element.appendChild(div); + } + + let inheritedSource = rule.inheritedSource; + if (inheritedSource && inheritedSource !== lastInheritedSource) { + let div = this.styleDocument.createElementNS(HTML_NS, "div"); + div.className = this._getRuleViewHeaderClassName(); + div.textContent = inheritedSource; + lastInheritedSource = inheritedSource; + this.element.appendChild(div); + } + + if (!seenPseudoElement && rule.pseudoElement) { + seenPseudoElement = true; + container = this.createExpandableContainer(this.pseudoElementLabel, + true); + } + + let keyframes = rule.keyframes; + if (keyframes && keyframes !== lastKeyframes) { + lastKeyframes = keyframes; + container = this.createExpandableContainer(rule.keyframesName); + } + + if (container && (rule.pseudoElement || keyframes)) { + container.appendChild(rule.editor.element); + } else { + this.element.appendChild(rule.editor.element); + } + } + + if (this.searchValue && !seenSearchTerm) { + this.searchField.classList.add("devtools-style-searchbox-no-match"); + } else { + this.searchField.classList.remove("devtools-style-searchbox-no-match"); + } + + return promise.all(editorReadyPromises); + }, + + /** + * Highlight rules that matches the filter search value and returns a + * boolean indicating whether or not rules were highlighted. + * + * @param {Rule} rule + * The rule object we're highlighting if its rule selectors or + * property values match the search value. + * @return {Boolean} true if the rule was highlighted, false otherwise. + */ + highlightRule: function (rule) { + let isRuleSelectorHighlighted = this._highlightRuleSelector(rule); + let isStyleSheetHighlighted = this._highlightStyleSheet(rule); + let isHighlighted = isRuleSelectorHighlighted || isStyleSheetHighlighted; + + // Highlight search matches in the rule properties + for (let textProp of rule.textProps) { + if (!textProp.invisible && this._highlightProperty(textProp.editor)) { + isHighlighted = true; + } + } + + return isHighlighted; + }, + + /** + * Highlights the rule selector that matches the filter search value and + * returns a boolean indicating whether or not the selector was highlighted. + * + * @param {Rule} rule + * The Rule object. + * @return {Boolean} true if the rule selector was highlighted, + * false otherwise. + */ + _highlightRuleSelector: function (rule) { + let isSelectorHighlighted = false; + + let selectorNodes = [...rule.editor.selectorText.childNodes]; + if (rule.domRule.type === CSSRule.KEYFRAME_RULE) { + selectorNodes = [rule.editor.selectorText]; + } else if (rule.domRule.type === ELEMENT_STYLE) { + selectorNodes = []; + } + + // Highlight search matches in the rule selectors + for (let selectorNode of selectorNodes) { + let selector = selectorNode.textContent.toLowerCase(); + if ((this.searchData.strictSearchAllValues && + selector === this.searchData.strictSearchValue) || + (!this.searchData.strictSearchAllValues && + selector.includes(this.searchValue))) { + selectorNode.classList.add("ruleview-highlight"); + isSelectorHighlighted = true; + } + } + + return isSelectorHighlighted; + }, + + /** + * Highlights the stylesheet source that matches the filter search value and + * returns a boolean indicating whether or not the stylesheet source was + * highlighted. + * + * @return {Boolean} true if the stylesheet source was highlighted, false + * otherwise. + */ + _highlightStyleSheet: function (rule) { + let styleSheetSource = rule.title.toLowerCase(); + let isStyleSheetHighlighted = this.searchData.strictSearchValue ? + styleSheetSource === this.searchData.strictSearchValue : + styleSheetSource.includes(this.searchValue); + + if (isStyleSheetHighlighted) { + rule.editor.source.classList.add("ruleview-highlight"); + } + + return isStyleSheetHighlighted; + }, + + /** + * Highlights the rule properties and computed properties that match the + * filter search value and returns a boolean indicating whether or not the + * property or computed property was highlighted. + * + * @param {TextPropertyEditor} editor + * The rule property TextPropertyEditor object. + * @return {Boolean} true if the property or computed property was + * highlighted, false otherwise. + */ + _highlightProperty: function (editor) { + let isPropertyHighlighted = this._highlightRuleProperty(editor); + let isComputedHighlighted = this._highlightComputedProperty(editor); + + // Expand the computed list if a computed property is highlighted and the + // property rule is not highlighted + if (!isPropertyHighlighted && isComputedHighlighted && + !editor.computed.hasAttribute("user-open")) { + editor.expandForFilter(); + } + + return isPropertyHighlighted || isComputedHighlighted; + }, + + /** + * Called when TextPropertyEditor is updated and updates the rule property + * highlight. + * + * @param {TextPropertyEditor} editor + * The rule property TextPropertyEditor object. + */ + _updatePropertyHighlight: function (editor) { + if (!this.searchValue || !this.searchData) { + return; + } + + this._clearHighlight(editor.element); + + if (this._highlightProperty(editor)) { + this.searchField.classList.remove("devtools-style-searchbox-no-match"); + } + }, + + /** + * Highlights the rule property that matches the filter search value + * and returns a boolean indicating whether or not the property was + * highlighted. + * + * @param {TextPropertyEditor} editor + * The rule property TextPropertyEditor object. + * @return {Boolean} true if the rule property was highlighted, + * false otherwise. + */ + _highlightRuleProperty: function (editor) { + // Get the actual property value displayed in the rule view + let propertyName = editor.prop.name.toLowerCase(); + let propertyValue = editor.valueSpan.textContent.toLowerCase(); + + return this._highlightMatches(editor.container, propertyName, + propertyValue); + }, + + /** + * Highlights the computed property that matches the filter search value and + * returns a boolean indicating whether or not the computed property was + * highlighted. + * + * @param {TextPropertyEditor} editor + * The rule property TextPropertyEditor object. + * @return {Boolean} true if the computed property was highlighted, false + * otherwise. + */ + _highlightComputedProperty: function (editor) { + let isComputedHighlighted = false; + + // Highlight search matches in the computed list of properties + editor._populateComputed(); + for (let computed of editor.prop.computed) { + if (computed.element) { + // Get the actual property value displayed in the computed list + let computedName = computed.name.toLowerCase(); + let computedValue = computed.parsedValue.toLowerCase(); + + isComputedHighlighted = this._highlightMatches(computed.element, + computedName, computedValue) ? true : isComputedHighlighted; + } + } + + return isComputedHighlighted; + }, + + /** + * Helper function for highlightRules that carries out highlighting the given + * element if the search terms match the property, and returns a boolean + * indicating whether or not the search terms match. + * + * @param {DOMNode} element + * The node to highlight if search terms match + * @param {String} propertyName + * The property name of a rule + * @param {String} propertyValue + * The property value of a rule + * @return {Boolean} true if the given search terms match the property, false + * otherwise. + */ + _highlightMatches: function (element, propertyName, propertyValue) { + let { + searchPropertyName, + searchPropertyValue, + searchPropertyMatch, + strictSearchPropertyName, + strictSearchPropertyValue, + strictSearchAllValues, + } = this.searchData; + let matches = false; + + // If the inputted search value matches a property line like + // `font-family: arial`, then check to make sure the name and value match. + // Otherwise, just compare the inputted search string directly against the + // name and value of the rule property. + let hasNameAndValue = searchPropertyMatch && + searchPropertyName && + searchPropertyValue; + let isMatch = (value, query, isStrict) => { + return isStrict ? value === query : query && value.includes(query); + }; + + if (hasNameAndValue) { + matches = + isMatch(propertyName, searchPropertyName, strictSearchPropertyName) && + isMatch(propertyValue, searchPropertyValue, strictSearchPropertyValue); + } else { + matches = + isMatch(propertyName, searchPropertyName, + strictSearchPropertyName || strictSearchAllValues) || + isMatch(propertyValue, searchPropertyValue, + strictSearchPropertyValue || strictSearchAllValues); + } + + if (matches) { + element.classList.add("ruleview-highlight"); + } + + return matches; + }, + + /** + * Clear all search filter highlights in the panel, and close the computed + * list if toggled opened + */ + _clearHighlight: function (element) { + for (let el of element.querySelectorAll(".ruleview-highlight")) { + el.classList.remove("ruleview-highlight"); + } + + for (let computed of element.querySelectorAll( + ".ruleview-computedlist[filter-open]")) { + computed.parentNode._textPropertyEditor.collapseForFilter(); + } + }, + + /** + * Called when the pseudo class panel button is clicked and toggles + * the display of the pseudo class panel. + */ + _onTogglePseudoClassPanel: function () { + if (this.pseudoClassPanel.hidden) { + this.pseudoClassToggle.setAttribute("checked", "true"); + this.hoverCheckbox.setAttribute("tabindex", "0"); + this.activeCheckbox.setAttribute("tabindex", "0"); + this.focusCheckbox.setAttribute("tabindex", "0"); + } else { + this.pseudoClassToggle.removeAttribute("checked"); + this.hoverCheckbox.setAttribute("tabindex", "-1"); + this.activeCheckbox.setAttribute("tabindex", "-1"); + this.focusCheckbox.setAttribute("tabindex", "-1"); + } + + this.pseudoClassPanel.hidden = !this.pseudoClassPanel.hidden; + }, + + /** + * Called when a pseudo class checkbox is clicked and toggles + * the pseudo class for the current selected element. + */ + _onTogglePseudoClass: function (event) { + let target = event.currentTarget; + this.inspector.togglePseudoClass(target.value); + }, + + /** + * Handle the keypress event in the rule view. + */ + _onShortcut: function (name, event) { + if (!event.target.closest("#sidebar-panel-ruleview")) { + return; + } + + if (name === "CmdOrCtrl+F") { + this.searchField.focus(); + event.preventDefault(); + } else if ((name === "Return" || name === "Space") && + this.element.classList.contains("non-interactive")) { + event.preventDefault(); + } else if (name === "Escape" && + event.target === this.searchField && + this._onClearSearch()) { + // Handle the search box's keypress event. If the escape key is pressed, + // clear the search box field. + event.preventDefault(); + event.stopPropagation(); + } + } + +}; + +/** + * Helper functions + */ + +/** + * Walk up the DOM from a given node until a parent property holder is found. + * For elements inside the computed property list, the non-computed parent + * property holder will be returned + * + * @param {DOMNode} node + * The node to start from + * @return {DOMNode} The parent property holder node, or null if not found + */ +function getParentTextPropertyHolder(node) { + while (true) { + if (!node || !node.classList) { + return null; + } + if (node.classList.contains("ruleview-property")) { + return node; + } + node = node.parentNode; + } +} + +/** + * For any given node, find the TextProperty it is in if any + * @param {DOMNode} node + * The node to start from + * @return {TextProperty} + */ +function getParentTextProperty(node) { + let parent = getParentTextPropertyHolder(node); + if (!parent) { + return null; + } + + let propValue = parent.querySelector(".ruleview-propertyvalue"); + if (!propValue) { + return null; + } + + return propValue.textProperty; +} + +/** + * Walker up the DOM from a given node until a parent property holder is found, + * and return the textContent for the name and value nodes. + * Stops at the first property found, so if node is inside the computed property + * list, the computed property will be returned + * + * @param {DOMNode} node + * The node to start from + * @return {Object} {name, value} + */ +function getPropertyNameAndValue(node) { + while (true) { + if (!node || !node.classList) { + return null; + } + // Check first for ruleview-computed since it's the deepest + if (node.classList.contains("ruleview-computed") || + node.classList.contains("ruleview-property")) { + return { + name: node.querySelector(".ruleview-propertyname").textContent, + value: node.querySelector(".ruleview-propertyvalue").textContent + }; + } + node = node.parentNode; + } +} + +function RuleViewTool(inspector, window) { + this.inspector = inspector; + this.document = window.document; + + this.view = new CssRuleView(this.inspector, this.document); + + this.clearUserProperties = this.clearUserProperties.bind(this); + this.refresh = this.refresh.bind(this); + this.onLinkClicked = this.onLinkClicked.bind(this); + this.onMutations = this.onMutations.bind(this); + this.onPanelSelected = this.onPanelSelected.bind(this); + this.onPropertyChanged = this.onPropertyChanged.bind(this); + this.onResized = this.onResized.bind(this); + this.onSelected = this.onSelected.bind(this); + this.onViewRefreshed = this.onViewRefreshed.bind(this); + + this.view.on("ruleview-changed", this.onPropertyChanged); + this.view.on("ruleview-refreshed", this.onViewRefreshed); + this.view.on("ruleview-linked-clicked", this.onLinkClicked); + + this.inspector.selection.on("detached-front", this.onSelected); + this.inspector.selection.on("new-node-front", this.onSelected); + this.inspector.selection.on("pseudoclass", this.refresh); + this.inspector.target.on("navigate", this.clearUserProperties); + this.inspector.sidebar.on("ruleview-selected", this.onPanelSelected); + this.inspector.pageStyle.on("stylesheet-updated", this.refresh); + this.inspector.walker.on("mutations", this.onMutations); + this.inspector.walker.on("resize", this.onResized); + + this.onSelected(); +} + +RuleViewTool.prototype = { + isSidebarActive: function () { + if (!this.view) { + return false; + } + return this.inspector.sidebar.getCurrentTabID() == "ruleview"; + }, + + onSelected: function (event) { + // Ignore the event if the view has been destroyed, or if it's inactive. + // But only if the current selection isn't null. If it's been set to null, + // let the update go through as this is needed to empty the view on + // navigation. + if (!this.view) { + return; + } + + let isInactive = !this.isSidebarActive() && + this.inspector.selection.nodeFront; + if (isInactive) { + return; + } + + this.view.setPageStyle(this.inspector.pageStyle); + + if (!this.inspector.selection.isConnected() || + !this.inspector.selection.isElementNode()) { + this.view.selectElement(null); + return; + } + + if (!event || event == "new-node-front") { + let done = this.inspector.updating("rule-view"); + this.view.selectElement(this.inspector.selection.nodeFront) + .then(done, done); + } + }, + + refresh: function () { + if (this.isSidebarActive()) { + this.view.refreshPanel(); + } + }, + + clearUserProperties: function () { + if (this.view && this.view.store && this.view.store.userProperties) { + this.view.store.userProperties.clear(); + } + }, + + onPanelSelected: function () { + if (this.inspector.selection.nodeFront === this.view._viewedElement) { + this.refresh(); + } else { + this.onSelected(); + } + }, + + onLinkClicked: function (e, rule) { + let sheet = rule.parentStyleSheet; + + // Chrome stylesheets are not listed in the style editor, so show + // these sheets in the view source window instead. + if (!sheet || sheet.isSystem) { + let href = rule.nodeHref || rule.href; + let toolbox = gDevTools.getToolbox(this.inspector.target); + toolbox.viewSource(href, rule.line); + return; + } + + let location = promise.resolve(rule.location); + if (Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) { + location = rule.getOriginalLocation(); + } + location.then(({ source, href, line, column }) => { + let target = this.inspector.target; + if (Tools.styleEditor.isTargetSupported(target)) { + gDevTools.showToolbox(target, "styleeditor").then(function (toolbox) { + let url = source || href; + toolbox.getCurrentPanel().selectStyleSheet(url, line, column); + }); + } + return; + }); + }, + + onPropertyChanged: function () { + this.inspector.markDirty(); + }, + + onViewRefreshed: function () { + this.inspector.emit("rule-view-refreshed"); + }, + + /** + * When markup mutations occur, if an attribute of the selected node changes, + * we need to refresh the view as that might change the node's styles. + */ + onMutations: function (mutations) { + for (let {type, target} of mutations) { + if (target === this.inspector.selection.nodeFront && + type === "attributes") { + this.refresh(); + break; + } + } + }, + + /** + * When the window gets resized, this may cause media-queries to match, and + * therefore, different styles may apply. + */ + onResized: function () { + this.refresh(); + }, + + destroy: function () { + this.inspector.walker.off("mutations", this.onMutations); + this.inspector.walker.off("resize", this.onResized); + this.inspector.selection.off("detached-front", this.onSelected); + this.inspector.selection.off("pseudoclass", this.refresh); + this.inspector.selection.off("new-node-front", this.onSelected); + this.inspector.target.off("navigate", this.clearUserProperties); + this.inspector.sidebar.off("ruleview-selected", this.onPanelSelected); + if (this.inspector.pageStyle) { + this.inspector.pageStyle.off("stylesheet-updated", this.refresh); + } + + this.view.off("ruleview-linked-clicked", this.onLinkClicked); + this.view.off("ruleview-changed", this.onPropertyChanged); + this.view.off("ruleview-refreshed", this.onViewRefreshed); + + this.view.destroy(); + + this.view = this.document = this.inspector = null; + } +}; + +exports.CssRuleView = CssRuleView; +exports.RuleViewTool = RuleViewTool; diff --git a/devtools/client/inspector/rules/test/.eslintrc.js b/devtools/client/inspector/rules/test/.eslintrc.js new file mode 100644 index 000000000..698ae9181 --- /dev/null +++ b/devtools/client/inspector/rules/test/.eslintrc.js @@ -0,0 +1,6 @@ +"use strict"; + +module.exports = { + // Extend from the shared list of defined globals for mochitests. + "extends": "../../../../.eslintrc.mochitests.js" +}; diff --git a/devtools/client/inspector/rules/test/browser.ini b/devtools/client/inspector/rules/test/browser.ini new file mode 100644 index 000000000..2c11219fb --- /dev/null +++ b/devtools/client/inspector/rules/test/browser.ini @@ -0,0 +1,221 @@ +[DEFAULT] +tags = devtools +subsuite = devtools +support-files = + doc_author-sheet.html + doc_blob_stylesheet.html + doc_content_stylesheet.html + doc_content_stylesheet_imported.css + doc_content_stylesheet_imported2.css + doc_content_stylesheet_linked.css + doc_content_stylesheet_script.css + doc_copystyles.css + doc_copystyles.html + doc_cssom.html + doc_custom.html + doc_filter.html + doc_frame_script.js + doc_inline_sourcemap.html + doc_invalid_sourcemap.css + doc_invalid_sourcemap.html + doc_keyframeanimation.css + doc_keyframeanimation.html + doc_keyframeLineNumbers.html + doc_media_queries.html + doc_pseudoelement.html + doc_ruleLineNumbers.html + doc_sourcemaps.css + doc_sourcemaps.css.map + doc_sourcemaps.html + doc_sourcemaps.scss + doc_style_editor_link.css + doc_test_image.png + doc_urls_clickable.css + doc_urls_clickable.html + head.js + !/devtools/client/commandline/test/helpers.js + !/devtools/client/framework/test/shared-head.js + !/devtools/client/inspector/test/head.js + !/devtools/client/inspector/test/shared-head.js + !/devtools/client/shared/test/test-actor.js + !/devtools/client/shared/test/test-actor-registry.js + +[browser_rules_add-property-and-reselect.js] +[browser_rules_add-property-cancel_01.js] +[browser_rules_add-property-cancel_02.js] +[browser_rules_add-property-cancel_03.js] +[browser_rules_add-property-commented.js] +[browser_rules_add-property_01.js] +[browser_rules_add-property_02.js] +[browser_rules_add-property-svg.js] +[browser_rules_add-rule-and-property.js] +[browser_rules_add-rule-button-state.js] +[browser_rules_add-rule-edit-selector.js] +[browser_rules_add-rule-iframes.js] +[browser_rules_add-rule-namespace-elements.js] +[browser_rules_add-rule-pseudo-class.js] +[browser_rules_add-rule-then-property-edit-selector.js] +[browser_rules_add-rule-with-menu.js] +[browser_rules_add-rule.js] +[browser_rules_authored.js] +[browser_rules_authored_color.js] +[browser_rules_authored_override.js] +[browser_rules_blob_stylesheet.js] +[browser_rules_colorpicker-and-image-tooltip_01.js] +[browser_rules_colorpicker-and-image-tooltip_02.js] +[browser_rules_colorpicker-appears-on-swatch-click.js] +[browser_rules_colorpicker-commit-on-ENTER.js] +[browser_rules_colorpicker-edit-gradient.js] +[browser_rules_colorpicker-hides-on-tooltip.js] +[browser_rules_colorpicker-multiple-changes.js] +[browser_rules_colorpicker-release-outside-frame.js] +[browser_rules_colorpicker-revert-on-ESC.js] +[browser_rules_colorpicker-swatch-displayed.js] +[browser_rules_colorUnit.js] +[browser_rules_completion-existing-property_01.js] +[browser_rules_completion-existing-property_02.js] +[browser_rules_completion-new-property_01.js] +[browser_rules_completion-new-property_02.js] +[browser_rules_completion-new-property_03.js] +[browser_rules_completion-new-property_04.js] +[browser_rules_completion-new-property_multiline.js] +[browser_rules_computed-lists_01.js] +[browser_rules_computed-lists_02.js] +[browser_rules_completion-popup-hidden-after-navigation.js] +[browser_rules_content_01.js] +[browser_rules_content_02.js] +skip-if = e10s && debug # Bug 1250058 - Docshell leak on debug e10s +[browser_rules_context-menu-show-mdn-docs-01.js] +[browser_rules_context-menu-show-mdn-docs-02.js] +[browser_rules_context-menu-show-mdn-docs-03.js] +[browser_rules_copy_styles.js] +subsuite = clipboard +[browser_rules_cssom.js] +[browser_rules_cubicbezier-appears-on-swatch-click.js] +[browser_rules_cubicbezier-commit-on-ENTER.js] +[browser_rules_cubicbezier-revert-on-ESC.js] +[browser_rules_custom.js] +[browser_rules_cycle-angle.js] +[browser_rules_cycle-color.js] +[browser_rules_edit-display-grid-property.js] +[browser_rules_edit-property-cancel.js] +[browser_rules_edit-property-click.js] +[browser_rules_edit-property-commit.js] +[browser_rules_edit-property-computed.js] +[browser_rules_edit-property-increments.js] +[browser_rules_edit-property-order.js] +[browser_rules_edit-property-remove_01.js] +[browser_rules_edit-property-remove_02.js] +[browser_rules_edit-property-remove_03.js] +[browser_rules_edit-property_01.js] +[browser_rules_edit-property_02.js] +[browser_rules_edit-property_03.js] +[browser_rules_edit-property_04.js] +[browser_rules_edit-property_05.js] +[browser_rules_edit-property_06.js] +[browser_rules_edit-property_07.js] +[browser_rules_edit-property_08.js] +[browser_rules_edit-property_09.js] +[browser_rules_edit-selector-click.js] +[browser_rules_edit-selector-click-on-scrollbar.js] +skip-if = os == "mac" # Bug 1245996 : click on scrollbar not working on OSX +[browser_rules_edit-selector-commit.js] +[browser_rules_edit-selector_01.js] +[browser_rules_edit-selector_02.js] +[browser_rules_edit-selector_03.js] +[browser_rules_edit-selector_04.js] +[browser_rules_edit-selector_05.js] +[browser_rules_edit-selector_06.js] +[browser_rules_edit-selector_07.js] +[browser_rules_edit-selector_08.js] +[browser_rules_edit-selector_09.js] +[browser_rules_edit-selector_10.js] +[browser_rules_edit-selector_11.js] +[browser_rules_edit-value-after-name_01.js] +[browser_rules_edit-value-after-name_02.js] +[browser_rules_edit-value-after-name_03.js] +[browser_rules_edit-value-after-name_04.js] +[browser_rules_editable-field-focus_01.js] +[browser_rules_editable-field-focus_02.js] +[browser_rules_eyedropper.js] +[browser_rules_filtereditor-appears-on-swatch-click.js] +[browser_rules_filtereditor-commit-on-ENTER.js] +[browser_rules_filtereditor-revert-on-ESC.js] +skip-if = (os == "win" && debug) # bug 963492: win. +[browser_rules_grid-highlighter-on-navigate.js] +[browser_rules_grid-highlighter-on-reload.js] +[browser_rules_grid-toggle_01.js] +[browser_rules_grid-toggle_02.js] +[browser_rules_grid-toggle_03.js] +[browser_rules_guessIndentation.js] +[browser_rules_inherited-properties_01.js] +[browser_rules_inherited-properties_02.js] +[browser_rules_inherited-properties_03.js] +[browser_rules_inline-source-map.js] +[browser_rules_invalid.js] +[browser_rules_invalid-source-map.js] +[browser_rules_keybindings.js] +[browser_rules_keyframes-rule_01.js] +[browser_rules_keyframes-rule_02.js] +[browser_rules_keyframeLineNumbers.js] +[browser_rules_lineNumbers.js] +[browser_rules_livepreview.js] +[browser_rules_mark_overridden_01.js] +[browser_rules_mark_overridden_02.js] +[browser_rules_mark_overridden_03.js] +[browser_rules_mark_overridden_04.js] +[browser_rules_mark_overridden_05.js] +[browser_rules_mark_overridden_06.js] +[browser_rules_mark_overridden_07.js] +[browser_rules_mathml-element.js] +[browser_rules_media-queries.js] +[browser_rules_multiple-properties-duplicates.js] +[browser_rules_multiple-properties-priority.js] +[browser_rules_multiple-properties-unfinished_01.js] +[browser_rules_multiple-properties-unfinished_02.js] +[browser_rules_multiple_properties_01.js] +[browser_rules_multiple_properties_02.js] +[browser_rules_original-source-link.js] +[browser_rules_pseudo-element_01.js] +[browser_rules_pseudo-element_02.js] +[browser_rules_pseudo_lock_options.js] +[browser_rules_refresh-no-flicker.js] +[browser_rules_refresh-on-attribute-change_01.js] +[browser_rules_refresh-on-attribute-change_02.js] +[browser_rules_refresh-on-style-change.js] +[browser_rules_search-filter-computed-list_01.js] +[browser_rules_search-filter-computed-list_02.js] +[browser_rules_search-filter-computed-list_03.js] +[browser_rules_search-filter-computed-list_04.js] +[browser_rules_search-filter-computed-list_expander.js] +[browser_rules_search-filter-overridden-property.js] +[browser_rules_search-filter_01.js] +[browser_rules_search-filter_02.js] +[browser_rules_search-filter_03.js] +[browser_rules_search-filter_04.js] +[browser_rules_search-filter_05.js] +[browser_rules_search-filter_06.js] +[browser_rules_search-filter_07.js] +[browser_rules_search-filter_08.js] +[browser_rules_search-filter_09.js] +[browser_rules_search-filter_10.js] +[browser_rules_search-filter_context-menu.js] +subsuite = clipboard +[browser_rules_search-filter_escape-keypress.js] +[browser_rules_select-and-copy-styles.js] +subsuite = clipboard +[browser_rules_selector-highlighter-on-navigate.js] +[browser_rules_selector-highlighter_01.js] +[browser_rules_selector-highlighter_02.js] +[browser_rules_selector-highlighter_03.js] +[browser_rules_selector-highlighter_04.js] +[browser_rules_selector_highlight.js] +[browser_rules_strict-search-filter-computed-list_01.js] +[browser_rules_strict-search-filter_01.js] +[browser_rules_strict-search-filter_02.js] +[browser_rules_strict-search-filter_03.js] +[browser_rules_style-editor-link.js] +[browser_rules_urls-clickable.js] +[browser_rules_user-agent-styles.js] +[browser_rules_user-agent-styles-uneditable.js] +[browser_rules_user-property-reset.js] diff --git a/devtools/client/inspector/rules/test/browser_rules_add-property-and-reselect.js b/devtools/client/inspector/rules/test/browser_rules_add-property-and-reselect.js new file mode 100644 index 000000000..492739abe --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_add-property-and-reselect.js @@ -0,0 +1,44 @@ +/* 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"; + +// Tests that adding properties to rules work and reselecting the element still +// show them. + +const TEST_URI = URL_ROOT + "doc_content_stylesheet.html"; + +add_task(function* () { + yield addTab(TEST_URI); + let {inspector, view} = yield openRuleView(); + yield selectNode("#target", inspector); + + info("Setting a font-weight property on all rules"); + yield setPropertyOnAllRules(view); + + info("Reselecting the element"); + yield selectNode("body", inspector); + yield selectNode("#target", inspector); + + checkPropertyOnAllRules(view); +}); + +function* setPropertyOnAllRules(view) { + // Wait for the properties to be properly created on the backend and for the + // view to be updated. + let onRefreshed = view.once("ruleview-refreshed"); + for (let rule of view._elementStyle.rules) { + rule.editor.addProperty("font-weight", "bold", "", true); + } + yield onRefreshed; +} + +function checkPropertyOnAllRules(view) { + for (let rule of view._elementStyle.rules) { + let lastRule = rule.textProps[rule.textProps.length - 1]; + + is(lastRule.name, "font-weight", "Last rule name is font-weight"); + is(lastRule.value, "bold", "Last rule value is bold"); + } +} diff --git a/devtools/client/inspector/rules/test/browser_rules_add-property-cancel_01.js b/devtools/client/inspector/rules/test/browser_rules_add-property-cancel_01.js new file mode 100644 index 000000000..78b3a4c91 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_add-property-cancel_01.js @@ -0,0 +1,44 @@ +/* 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"; + +// Tests adding a new property and escapes the new empty property name editor. + +const TEST_URI = ` + <style type="text/css"> + #testid { + background-color: blue; + } + .testclass { + background-color: green; + } + </style> + <div id='testid' class='testclass'>Styled Node</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + + let elementRuleEditor = getRuleViewRuleEditor(view, 0); + let editor = yield focusNewRuleViewProperty(elementRuleEditor); + is(inplaceEditor(elementRuleEditor.newPropSpan), editor, + "The new property editor got focused"); + + info("Escape the new property editor"); + let onBlur = once(editor.input, "blur"); + EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow); + yield onBlur; + + info("Checking the state of cancelling a new property name editor"); + is(elementRuleEditor.rule.textProps.length, 0, + "Should have cancelled creating a new text property."); + ok(!elementRuleEditor.propertyList.hasChildNodes(), + "Should not have any properties."); + + is(view.styleDocument.activeElement, view.styleDocument.body, + "Correct element has focus"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_add-property-cancel_02.js b/devtools/client/inspector/rules/test/browser_rules_add-property-cancel_02.js new file mode 100644 index 000000000..7f4d1564c --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_add-property-cancel_02.js @@ -0,0 +1,34 @@ +/* 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"; + +// Tests adding a new property and escapes the new empty property value editor. + +const TEST_URI = ` + <style type='text/css'> + #testid { + background-color: blue; + } + </style> + <div id='testid'>Styled Node</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + + info("Test creating a new property and escaping"); + yield addProperty(view, 1, "color", "red", "VK_ESCAPE", false); + + is(view.styleDocument.activeElement, view.styleDocument.body, + "Correct element has focus"); + + let elementRuleEditor = getRuleViewRuleEditor(view, 1); + is(elementRuleEditor.rule.textProps.length, 1, + "Removed the new text property."); + is(elementRuleEditor.propertyList.children.length, 1, + "Removed the property editor."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_add-property-cancel_03.js b/devtools/client/inspector/rules/test/browser_rules_add-property-cancel_03.js new file mode 100644 index 000000000..4f8b42009 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_add-property-cancel_03.js @@ -0,0 +1,43 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests adding a new property and escapes the property name editor with a +// value. + +const TEST_URI = ` + <style type='text/css'> + div { + background-color: blue; + } + </style> + <div>Test node</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("div", inspector); + + // Add a property to the element's style declaration, add some text, + // then press escape. + + let elementRuleEditor = getRuleViewRuleEditor(view, 1); + let editor = yield focusNewRuleViewProperty(elementRuleEditor); + + is(inplaceEditor(elementRuleEditor.newPropSpan), editor, + "Next focused editor should be the new property editor."); + + EventUtils.sendString("background", view.styleWindow); + + let onBlur = once(editor.input, "blur"); + EventUtils.synthesizeKey("VK_ESCAPE", {}); + yield onBlur; + + is(elementRuleEditor.rule.textProps.length, 1, + "Should have canceled creating a new text property."); + is(view.styleDocument.activeElement, view.styleDocument.body, + "Correct element has focus"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_add-property-commented.js b/devtools/client/inspector/rules/test/browser_rules_add-property-commented.js new file mode 100644 index 000000000..eacf5db5a --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_add-property-commented.js @@ -0,0 +1,47 @@ +/* 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 commented properties can be added and are disabled. + +const TEST_URI = "<div id='testid'></div>"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + yield testCreateNewSetOfCommentedAndUncommentedProperties(view); +}); + +function* testCreateNewSetOfCommentedAndUncommentedProperties(view) { + info("Test creating a new set of commented and uncommented properties"); + + info("Focusing a new property name in the rule-view"); + let ruleEditor = getRuleViewRuleEditor(view, 0); + let editor = yield focusEditableField(view, ruleEditor.closeBrace); + is(inplaceEditor(ruleEditor.newPropSpan), editor, + "The new property editor has focus"); + + info( + "Entering a commented property/value pair into the property name editor"); + let input = editor.input; + input.value = `color: blue; + /* background-color: yellow; */ + width: 200px; + height: 100px; + /* padding-bottom: 1px; */`; + + info("Pressing return to commit and focus the new value field"); + let onModifications = view.once("ruleview-changed"); + EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow); + yield onModifications; + + let textProps = ruleEditor.rule.textProps; + ok(textProps[0].enabled, "The 'color' property is enabled."); + ok(!textProps[1].enabled, "The 'background-color' property is disabled."); + ok(textProps[2].enabled, "The 'width' property is enabled."); + ok(textProps[3].enabled, "The 'height' property is enabled."); + ok(!textProps[4].enabled, "The 'padding-bottom' property is disabled."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_add-property-svg.js b/devtools/client/inspector/rules/test/browser_rules_add-property-svg.js new file mode 100644 index 000000000..a53421db3 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_add-property-svg.js @@ -0,0 +1,22 @@ +/* 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"; + +// Tests editing SVG styles using the rules view. + +var TEST_URL = "chrome://global/skin/icons/warning.svg"; +var TEST_SELECTOR = "path"; + +add_task(function* () { + yield addTab(TEST_URL); + let {inspector, view} = yield openRuleView(); + yield selectNode(TEST_SELECTOR, inspector); + + info("Test creating a new property"); + yield addProperty(view, 0, "fill", "red"); + + is((yield getComputedStyleProperty(TEST_SELECTOR, null, "fill")), + "rgb(255, 0, 0)", "The fill was changed to red"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_add-property_01.js b/devtools/client/inspector/rules/test/browser_rules_add-property_01.js new file mode 100644 index 000000000..1d7068d54 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_add-property_01.js @@ -0,0 +1,32 @@ +/* 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 adding an invalid property. + +const TEST_URI = ` + <style type='text/css'> + #testid { + background-color: blue; + } + .testclass { + background-color: green; + } + </style> + <div id='testid' class='testclass'>Styled Node</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + + info("Test creating a new property"); + let textProp = yield addProperty(view, 0, "background-color", "#XYZ"); + + is(textProp.value, "#XYZ", "Text prop should have been changed."); + is(textProp.overridden, true, "Property should be overridden"); + is(textProp.editor.isValid(), false, "#XYZ should not be a valid entry"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_add-property_02.js b/devtools/client/inspector/rules/test/browser_rules_add-property_02.js new file mode 100644 index 000000000..6f6bef0f7 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_add-property_02.js @@ -0,0 +1,65 @@ +/* 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 adding a valid property to a CSS rule, and navigating through the fields +// by pressing ENTER. + +const TEST_URI = ` + <style type="text/css"> + #testid { + color: blue; + } + </style> + <div id='testid'>Styled Node</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + + info("Focus the new property name field"); + let ruleEditor = getRuleViewRuleEditor(view, 1); + let editor = yield focusNewRuleViewProperty(ruleEditor); + let input = editor.input; + + is(inplaceEditor(ruleEditor.newPropSpan), editor, + "Next focused editor should be the new property editor."); + ok(input.selectionStart === 0 && input.selectionEnd === input.value.length, + "Editor contents are selected."); + + // Try clicking on the editor's input again, shouldn't cause trouble + // (see bug 761665). + EventUtils.synthesizeMouse(input, 1, 1, {}, view.styleWindow); + input.select(); + + info("Entering the property name"); + editor.input.value = "background-color"; + + info("Pressing RETURN and waiting for the value field focus"); + let onNameAdded = view.once("ruleview-changed"); + EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow); + + yield onNameAdded; + + editor = inplaceEditor(view.styleDocument.activeElement); + + is(ruleEditor.rule.textProps.length, 2, + "Should have created a new text property."); + is(ruleEditor.propertyList.children.length, 2, + "Should have created a property editor."); + let textProp = ruleEditor.rule.textProps[1]; + is(editor, inplaceEditor(textProp.editor.valueSpan), + "Should be editing the value span now."); + + info("Entering the property value"); + let onValueAdded = view.once("ruleview-changed"); + editor.input.value = "purple"; + EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow); + yield onValueAdded; + + is(textProp.value, "purple", "Text prop should have been changed."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_add-rule-and-property.js b/devtools/client/inspector/rules/test/browser_rules_add-rule-and-property.js new file mode 100644 index 000000000..1cf04a275 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_add-rule-and-property.js @@ -0,0 +1,30 @@ +/* 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"; + +// Tests adding a new rule and a new property in this rule. + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8,<div id='testid'>Styled Node</div>"); + let {inspector, view} = yield openRuleView(); + + info("Selecting the test node"); + yield selectNode("#testid", inspector); + + info("Adding a new rule for this node and blurring the new selector field"); + yield addNewRuleAndDismissEditor(inspector, view, "#testid", 1); + + info("Adding a new property for this rule"); + let ruleEditor = getRuleViewRuleEditor(view, 1); + + let onRuleViewChanged = view.once("ruleview-changed"); + ruleEditor.addProperty("font-weight", "bold", "", true); + yield onRuleViewChanged; + + let textProps = ruleEditor.rule.textProps; + let prop = textProps[textProps.length - 1]; + is(prop.name, "font-weight", "The last property name is font-weight"); + is(prop.value, "bold", "The last property value is bold"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_add-rule-button-state.js b/devtools/client/inspector/rules/test/browser_rules_add-rule-button-state.js new file mode 100644 index 000000000..1441213b3 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_add-rule-button-state.js @@ -0,0 +1,51 @@ +/* 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"; + +// Tests if the `Add rule` button disables itself properly for non-element nodes +// and anonymous element. + +const TEST_URI = ` + <style type="text/css"> + #pseudo::before { + content: "before"; + } + </style> + <div id="pseudo"></div> + <div id="testid">Test Node</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield testDisabledButton(inspector, view); +}); + +function* testDisabledButton(inspector, view) { + let node = "#testid"; + + info("Selecting a real element"); + yield selectNode(node, inspector); + ok(!view.addRuleButton.disabled, "Add rule button should be enabled"); + + info("Select a null element"); + yield view.selectElement(null); + ok(view.addRuleButton.disabled, "Add rule button should be disabled"); + + info("Selecting a real element"); + yield selectNode(node, inspector); + ok(!view.addRuleButton.disabled, "Add rule button should be enabled"); + + info("Selecting a pseudo element"); + let pseudo = yield getNodeFront("#pseudo", inspector); + let children = yield inspector.walker.children(pseudo); + let before = children.nodes[0]; + yield selectNode(before, inspector); + ok(view.addRuleButton.disabled, "Add rule button should be disabled"); + + info("Selecting a real element"); + yield selectNode(node, inspector); + ok(!view.addRuleButton.disabled, "Add rule button should be enabled"); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_add-rule-edit-selector.js b/devtools/client/inspector/rules/test/browser_rules_add-rule-edit-selector.js new file mode 100644 index 000000000..b59f317a5 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_add-rule-edit-selector.js @@ -0,0 +1,55 @@ +/* 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"; + +// Tests the behaviour of adding a new rule to the rule view and editing +// its selector. + +const TEST_URI = ` + <style type="text/css"> + #testid { + text-align: center; + } + </style> + <div id="testid">Styled Node</div> + <span>This is a span</span> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + + yield addNewRule(inspector, view); + yield testEditSelector(view, "span"); + + info("Selecting the modified element with the new rule"); + yield selectNode("span", inspector); + yield checkModifiedElement(view, "span"); +}); + +function* testEditSelector(view, name) { + info("Test editing existing selector field"); + let idRuleEditor = getRuleViewRuleEditor(view, 1); + let editor = idRuleEditor.selectorText.ownerDocument.activeElement; + + info("Entering a new selector name and committing"); + editor.value = name; + + info("Waiting for rule view to update"); + let onRuleViewChanged = once(view, "ruleview-changed"); + + info("Entering the commit key"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewChanged; + + is(view._elementStyle.rules.length, 3, "Should have 3 rules."); + ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists."); +} + +function* checkModifiedElement(view, name) { + is(view._elementStyle.rules.length, 2, "Should have 2 rules."); + ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_add-rule-iframes.js b/devtools/client/inspector/rules/test/browser_rules_add-rule-iframes.js new file mode 100644 index 000000000..7b0ba7812 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_add-rule-iframes.js @@ -0,0 +1,57 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests adding a rule on elements nested in iframes. + +const TEST_URI = + `<div>outer</div> + <iframe id="frame1" src="data:text/html;charset=utf-8,<div>inner1</div>"> + </iframe> + <iframe id="frame2" src="data:text/html;charset=utf-8,<div>inner2</div>"> + </iframe>`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("div", inspector); + yield addNewRuleAndDismissEditor(inspector, view, "div", 1); + yield addNewProperty(view, 1, "color", "red"); + + let innerFrameDiv1 = yield getNodeFrontInFrame("div", "#frame1", inspector); + yield selectNode(innerFrameDiv1, inspector); + yield addNewRuleAndDismissEditor(inspector, view, "div", 1); + yield addNewProperty(view, 1, "color", "blue"); + + let innerFrameDiv2 = yield getNodeFrontInFrame("div", "#frame2", inspector); + yield selectNode(innerFrameDiv2, inspector); + yield addNewRuleAndDismissEditor(inspector, view, "div", 1); + yield addNewProperty(view, 1, "color", "green"); +}); + +/** + * Add a new property in the rule at the provided index in the rule view. + * + * @param {RuleView} view + * @param {Number} index + * The index of the rule in which we should add a new property. + * @param {String} name + * The name of the new property. + * @param {String} value + * The value of the new property. + */ +function* addNewProperty(view, index, name, value) { + let idRuleEditor = getRuleViewRuleEditor(view, index); + info(`Adding new property "${name}: ${value};"`); + + let onRuleViewChanged = view.once("ruleview-changed"); + idRuleEditor.addProperty(name, value, "", true); + yield onRuleViewChanged; + + let textProps = idRuleEditor.rule.textProps; + let lastProperty = textProps[textProps.length - 1]; + is(lastProperty.name, name, "Last property has the expected name"); + is(lastProperty.value, value, "Last property has the expected value"); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_add-rule-namespace-elements.js b/devtools/client/inspector/rules/test/browser_rules_add-rule-namespace-elements.js new file mode 100644 index 000000000..98e34e69f --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_add-rule-namespace-elements.js @@ -0,0 +1,41 @@ +/* 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"; + +// Tests the behaviour of adding a new rule using the add rule button +// on namespaced elements. + +const XHTML = ` + <!DOCTYPE html> + <html xmlns="http://www.w3.org/1999/xhtml" + xmlns:svg="http://www.w3.org/2000/svg"> + <body> + <svg:svg width="100" height="100"> + <svg:clipPath> + <svg:rect x="0" y="0" width="10" height="5"></svg:rect> + </svg:clipPath> + <svg:circle cx="0" cy="0" r="5"></svg:circle> + </svg:svg> + </body> + </html> +`; +const TEST_URI = "data:application/xhtml+xml;charset=utf-8," + encodeURI(XHTML); + +const TEST_DATA = [ + { node: "clipPath", expected: "clipPath" }, + { node: "rect", expected: "rect" }, + { node: "circle", expected: "circle" } +]; + +add_task(function* () { + yield addTab(TEST_URI); + let {inspector, view} = yield openRuleView(); + + for (let data of TEST_DATA) { + let {node, expected} = data; + yield selectNode(node, inspector); + yield addNewRuleAndDismissEditor(inspector, view, expected, 1); + } +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_add-rule-pseudo-class.js b/devtools/client/inspector/rules/test/browser_rules_add-rule-pseudo-class.js new file mode 100644 index 000000000..39f773c13 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_add-rule-pseudo-class.js @@ -0,0 +1,82 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests adding a rule with pseudo class locks on. + +const TEST_URI = "<p id='element'>Test element</p>"; + +const EXPECTED_SELECTOR = "#element"; +const TEST_DATA = [ + [], + [":hover"], + [":hover", ":active"], + [":hover", ":active", ":focus"], + [":active"], + [":active", ":focus"], + [":focus"] +]; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#element", inspector); + + for (let data of TEST_DATA) { + yield runTestData(inspector, view, data); + } +}); + +function* runTestData(inspector, view, pseudoClasses) { + yield setPseudoLocks(inspector, view, pseudoClasses); + + let expected = EXPECTED_SELECTOR + pseudoClasses.join(""); + yield addNewRuleAndDismissEditor(inspector, view, expected, 1); + + yield resetPseudoLocks(inspector, view); +} + +function* setPseudoLocks(inspector, view, pseudoClasses) { + if (pseudoClasses.length == 0) { + return; + } + + for (let pseudoClass of pseudoClasses) { + switch (pseudoClass) { + case ":hover": + view.hoverCheckbox.click(); + yield inspector.once("rule-view-refreshed"); + break; + case ":active": + view.activeCheckbox.click(); + yield inspector.once("rule-view-refreshed"); + break; + case ":focus": + view.focusCheckbox.click(); + yield inspector.once("rule-view-refreshed"); + break; + } + } +} + +function* resetPseudoLocks(inspector, view) { + if (!view.hoverCheckbox.checked && + !view.activeCheckbox.checked && + !view.focusCheckbox.checked) { + return; + } + if (view.hoverCheckbox.checked) { + view.hoverCheckbox.click(); + yield inspector.once("rule-view-refreshed"); + } + if (view.activeCheckbox.checked) { + view.activeCheckbox.click(); + yield inspector.once("rule-view-refreshed"); + } + if (view.focusCheckbox.checked) { + view.focusCheckbox.click(); + yield inspector.once("rule-view-refreshed"); + } +} diff --git a/devtools/client/inspector/rules/test/browser_rules_add-rule-then-property-edit-selector.js b/devtools/client/inspector/rules/test/browser_rules_add-rule-then-property-edit-selector.js new file mode 100644 index 000000000..294eb67e4 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_add-rule-then-property-edit-selector.js @@ -0,0 +1,80 @@ +/* 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"; + +// Tests the behaviour of adding a new rule to the rule view, adding a new +// property and editing the selector. + +const TEST_URI = ` + <style type="text/css"> + #testid { + text-align: center; + } + </style> + <div id="testid">Styled Node</div> + <span>This is a span</span> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + + yield addNewRuleAndDismissEditor(inspector, view, "#testid", 1); + + info("Adding a new property to the new rule"); + yield testAddingProperty(view, 1); + + info("Editing existing selector field"); + yield testEditSelector(view, "span"); + + info("Selecting the modified element"); + yield selectNode("span", inspector); + + info("Check new rule and property exist in the modified element"); + yield checkModifiedElement(view, "span", 1); +}); + +function* testAddingProperty(view, index) { + let ruleEditor = getRuleViewRuleEditor(view, index); + ruleEditor.addProperty("font-weight", "bold", "", true); + let textProps = ruleEditor.rule.textProps; + let lastRule = textProps[textProps.length - 1]; + is(lastRule.name, "font-weight", "Last rule name is font-weight"); + is(lastRule.value, "bold", "Last rule value is bold"); +} + +function* testEditSelector(view, name) { + let idRuleEditor = getRuleViewRuleEditor(view, 1); + + info("Focusing an existing selector name in the rule-view"); + let editor = yield focusEditableField(view, idRuleEditor.selectorText); + + is(inplaceEditor(idRuleEditor.selectorText), editor, + "The selector editor got focused"); + + info("Entering a new selector name: " + name); + editor.input.value = name; + + info("Waiting for rule view to update"); + let onRuleViewChanged = once(view, "ruleview-changed"); + + info("Entering the commit key"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewChanged; + + is(view._elementStyle.rules.length, 3, "Should have 3 rules."); +} + +function* checkModifiedElement(view, name, index) { + is(view._elementStyle.rules.length, 2, "Should have 2 rules."); + ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists."); + + let idRuleEditor = getRuleViewRuleEditor(view, index); + let textProps = idRuleEditor.rule.textProps; + let lastRule = textProps[textProps.length - 1]; + is(lastRule.name, "font-weight", "Last rule name is font-weight"); + is(lastRule.value, "bold", "Last rule value is bold"); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_add-rule-with-menu.js b/devtools/client/inspector/rules/test/browser_rules_add-rule-with-menu.js new file mode 100644 index 000000000..976fc9643 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_add-rule-with-menu.js @@ -0,0 +1,42 @@ +/* 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"; + +// Tests the a new CSS rule can be added using the context menu. + +const TEST_URI = '<div id="testid">Test Node</div>'; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + + yield selectNode("#testid", inspector); + yield addNewRuleFromContextMenu(inspector, view); + yield testNewRule(view); +}); + +function* addNewRuleFromContextMenu(inspector, view) { + info("Waiting for context menu to be shown"); + + let allMenuItems = openStyleContextMenuAndGetAllItems(view, view.element); + let menuitemAddRule = allMenuItems.find(item => item.label === + STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.addNewRule")); + + ok(menuitemAddRule.visible, "Add rule is visible"); + + info("Adding the new rule and expecting a ruleview-changed event"); + let onRuleViewChanged = view.once("ruleview-changed"); + menuitemAddRule.click(); + yield onRuleViewChanged; +} + +function* testNewRule(view) { + let ruleEditor = getRuleViewRuleEditor(view, 1); + let editor = ruleEditor.selectorText.ownerDocument.activeElement; + is(editor.value, "#testid", "Selector editor value is as expected"); + + info("Escaping from the selector field the change"); + EventUtils.synthesizeKey("VK_ESCAPE", {}); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_add-rule.js b/devtools/client/inspector/rules/test/browser_rules_add-rule.js new file mode 100644 index 000000000..296105c85 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_add-rule.js @@ -0,0 +1,47 @@ +/* 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"; + +// Tests adding a new rule using the add rule button. + +const TEST_URI = ` + <style type="text/css"> + .testclass { + text-align: center; + } + </style> + <div id="testid" class="testclass">Styled Node</div> + <span class="testclass2">This is a span</span> + <span class="class1 class2">Multiple classes</span> + <span class="class3 class4">Multiple classes</span> + <p>Empty<p> + <h1 class="asd@@@@a!!!!:::@asd">Invalid characters in class</h1> + <h2 id="asd@@@a!!2a">Invalid characters in id</h2> + <svg viewBox="0 0 10 10"> + <circle cx="5" cy="5" r="5" fill="blue"></circle> + </svg> +`; + +const TEST_DATA = [ + { node: "#testid", expected: "#testid" }, + { node: ".testclass2", expected: ".testclass2" }, + { node: ".class1.class2", expected: ".class1.class2" }, + { node: ".class3.class4", expected: ".class3.class4" }, + { node: "p", expected: "p" }, + { node: "h1", expected: ".asd\\@\\@\\@\\@a\\!\\!\\!\\!\\:\\:\\:\\@asd" }, + { node: "h2", expected: "#asd\\@\\@\\@a\\!\\!2a" }, + { node: "circle", expected: "circle" } +]; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + + for (let data of TEST_DATA) { + let {node, expected} = data; + yield selectNode(node, inspector); + yield addNewRuleAndDismissEditor(inspector, view, expected, 1); + } +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_authored.js b/devtools/client/inspector/rules/test/browser_rules_authored.js new file mode 100644 index 000000000..cb0dd1186 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_authored.js @@ -0,0 +1,49 @@ +/* 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 as-authored styles. + +function* createTestContent(style) { + let html = `<style type="text/css"> + ${style} + </style> + <div id="testid" class="testclass">Styled Node</div>`; + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(html)); + + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + return view; +} + +add_task(function* () { + let view = yield createTestContent("#testid {" + + // Invalid property. + " something: random;" + + // Invalid value. + " color: orang;" + + // Override. + " background-color: blue;" + + " background-color: #f0c;" + + "} "); + + let elementStyle = view._elementStyle; + + let expected = [ + {name: "something", overridden: true}, + {name: "color", overridden: true}, + {name: "background-color", overridden: true}, + {name: "background-color", overridden: false} + ]; + + let rule = elementStyle.rules[1]; + + for (let i = 0; i < expected.length; ++i) { + let prop = rule.textProps[i]; + is(prop.name, expected[i].name, "test name for prop " + i); + is(prop.overridden, expected[i].overridden, + "test overridden for prop " + i); + } +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_authored_color.js b/devtools/client/inspector/rules/test/browser_rules_authored_color.js new file mode 100644 index 000000000..4c5cab206 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_authored_color.js @@ -0,0 +1,67 @@ +/* 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 as-authored color styles. + +/** + * Array of test color objects: + * {String} name: name of the used & expected color format. + * {String} id: id of the element that will be created to test this color. + * {String} color: initial value of the color property applied to the test element. + * {String} result: expected value of the color property after edition. + */ +const colors = [ + {name: "hex", id: "test1", color: "#f0c", result: "#0f0"}, + {name: "rgb", id: "test2", color: "rgb(0,128,250)", result: "rgb(0, 255, 0)"}, + // Test case preservation. + {name: "hex", id: "test3", color: "#F0C", result: "#0F0"}, +]; + +add_task(function* () { + Services.prefs.setCharPref("devtools.defaultColorUnit", "authored"); + + let html = ""; + for (let {color, id} of colors) { + html += `<div id="${id}" style="color: ${color}">Styled Node</div>`; + } + + let tab = yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(html)); + + let {inspector, view} = yield openRuleView(); + + for (let color of colors) { + let cPicker = view.tooltips.colorPicker; + let selector = "#" + color.id; + yield selectNode(selector, inspector); + + let swatch = getRuleViewProperty(view, "element", "color").valueSpan + .querySelector(".ruleview-colorswatch"); + let onColorPickerReady = cPicker.once("ready"); + swatch.click(); + yield onColorPickerReady; + + yield simulateColorPickerChange(view, cPicker, [0, 255, 0, 1], { + selector, + name: "color", + value: "rgb(0, 255, 0)" + }); + + let spectrum = cPicker.spectrum; + let onHidden = cPicker.tooltip.once("hidden"); + // Validating the color change ends up updating the rule view twice + let onRuleViewChanged = waitForNEvents(view, "ruleview-changed", 2); + focusAndSendKey(spectrum.element.ownerDocument.defaultView, "RETURN"); + yield onHidden; + yield onRuleViewChanged; + + is(getRuleViewPropertyValue(view, "element", "color"), color.result, + "changing the color preserved the unit for " + color.name); + } + + let target = TargetFactory.forTab(tab); + yield gDevTools.closeToolbox(target); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_authored_override.js b/devtools/client/inspector/rules/test/browser_rules_authored_override.js new file mode 100644 index 000000000..7305e5712 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_authored_override.js @@ -0,0 +1,53 @@ +/* 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 as-authored styles. + +function* createTestContent(style) { + let html = `<style type="text/css"> + ${style} + </style> + <div id="testid" class="testclass">Styled Node</div>`; + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(html)); + + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + return view; +} + +add_task(function* () { + let gradientText1 = "(orange, blue);"; + let gradientText2 = "(pink, teal);"; + + let view = + yield createTestContent("#testid {" + + " background-image: linear-gradient" + + gradientText1 + + " background-image: -ms-linear-gradient" + + gradientText2 + + " background-image: linear-gradient" + + gradientText2 + + "} "); + + let elementStyle = view._elementStyle; + let rule = elementStyle.rules[1]; + + // Initially the last property should be active. + for (let i = 0; i < 3; ++i) { + let prop = rule.textProps[i]; + is(prop.name, "background-image", "check the property name"); + is(prop.overridden, i !== 2, "check overridden for " + i); + } + + yield togglePropStatus(view, rule.textProps[2]); + + // Now the first property should be active. + for (let i = 0; i < 3; ++i) { + let prop = rule.textProps[i]; + is(prop.overridden || !prop.enabled, i !== 0, + "post-change check overridden for " + i); + } +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_blob_stylesheet.js b/devtools/client/inspector/rules/test/browser_rules_blob_stylesheet.js new file mode 100644 index 000000000..adc8eb2ee --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_blob_stylesheet.js @@ -0,0 +1,20 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the rule-view content is correct for stylesheet generated +// with createObjectURL(cssBlob) +const TEST_URL = URL_ROOT + "doc_blob_stylesheet.html"; + +add_task(function* () { + yield addTab(TEST_URL); + let {inspector, view} = yield openRuleView(); + + yield selectNode("h1", inspector); + is(view.element.querySelectorAll("#noResults").length, 0, + "The no-results element is not displayed"); + + is(view.element.querySelectorAll(".ruleview-rule").length, 2, + "There are 2 displayed rules"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_colorUnit.js b/devtools/client/inspector/rules/test/browser_rules_colorUnit.js new file mode 100644 index 000000000..138f68365 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_colorUnit.js @@ -0,0 +1,65 @@ +/* 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 color selection respects the user pref. + +const TEST_URI = ` + <style type='text/css'> + #testid { + color: blue; + } + </style> + <div id='testid' class='testclass'>Styled Node</div> +`; + +add_task(function* () { + let TESTS = [ + {name: "hex", result: "#0f0"}, + {name: "rgb", result: "rgb(0, 255, 0)"} + ]; + + for (let {name, result} of TESTS) { + info("starting test for " + name); + Services.prefs.setCharPref("devtools.defaultColorUnit", name); + + let tab = yield addTab("data:text/html;charset=utf-8," + + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + + yield selectNode("#testid", inspector); + yield basicTest(view, name, result); + + let target = TargetFactory.forTab(tab); + yield gDevTools.closeToolbox(target); + gBrowser.removeCurrentTab(); + } +}); + +function* basicTest(view, name, result) { + let cPicker = view.tooltips.colorPicker; + let swatch = getRuleViewProperty(view, "#testid", "color").valueSpan + .querySelector(".ruleview-colorswatch"); + let onColorPickerReady = cPicker.once("ready"); + swatch.click(); + yield onColorPickerReady; + + yield simulateColorPickerChange(view, cPicker, [0, 255, 0, 1], { + selector: "#testid", + name: "color", + value: "rgb(0, 255, 0)" + }); + + let spectrum = cPicker.spectrum; + let onHidden = cPicker.tooltip.once("hidden"); + // Validating the color change ends up updating the rule view twice + let onRuleViewChanged = waitForNEvents(view, "ruleview-changed", 2); + focusAndSendKey(spectrum.element.ownerDocument.defaultView, "RETURN"); + yield onHidden; + yield onRuleViewChanged; + + is(getRuleViewPropertyValue(view, "#testid", "color"), result, + "changing the color used the " + name + " unit"); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_colorpicker-and-image-tooltip_01.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-and-image-tooltip_01.js new file mode 100644 index 000000000..a8d2fd5f1 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-and-image-tooltip_01.js @@ -0,0 +1,63 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that after a color change, the image preview tooltip in the same +// property is displayed and positioned correctly. +// See bug 979292 + +const TEST_URI = ` + <style type="text/css"> + body { + background: url("chrome://global/skin/icons/warning-64.png"), linear-gradient(white, #F06 400px); + } + </style> + Testing the color picker tooltip! +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {view} = yield openRuleView(); + let value = getRuleViewProperty(view, "body", "background").valueSpan; + let swatch = value.querySelectorAll(".ruleview-colorswatch")[0]; + let url = value.querySelector(".theme-link"); + yield testImageTooltipAfterColorChange(swatch, url, view); +}); + +function* testImageTooltipAfterColorChange(swatch, url, ruleView) { + info("First, verify that the image preview tooltip works"); + let anchor = yield isHoverTooltipTarget(ruleView.tooltips.previewTooltip, + url); + ok(anchor, "The image preview tooltip is shown on the url span"); + is(anchor, url, "The anchor returned by the showOnHover callback is correct"); + + info("Open the color picker tooltip and change the color"); + let picker = ruleView.tooltips.colorPicker; + let onColorPickerReady = picker.once("ready"); + swatch.click(); + yield onColorPickerReady; + + yield simulateColorPickerChange(ruleView, picker, [0, 0, 0, 1], { + selector: "body", + name: "background-image", + value: 'url("chrome://global/skin/icons/warning-64.png"), linear-gradient(rgb(0, 0, 0), rgb(255, 0, 102) 400px)' + }); + + let spectrum = picker.spectrum; + let onHidden = picker.tooltip.once("hidden"); + let onModifications = ruleView.once("ruleview-changed"); + focusAndSendKey(spectrum.element.ownerDocument.defaultView, "RETURN"); + yield onHidden; + yield onModifications; + + info("Verify again that the image preview tooltip works"); + // After a color change, the property is re-populated, we need to get the new + // dom node + url = getRuleViewProperty(ruleView, "body", "background").valueSpan + .querySelector(".theme-link"); + anchor = yield isHoverTooltipTarget(ruleView.tooltips.previewTooltip, url); + ok(anchor, "The image preview tooltip is shown on the url span"); + is(anchor, url, "The anchor returned by the showOnHover callback is correct"); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_colorpicker-and-image-tooltip_02.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-and-image-tooltip_02.js new file mode 100644 index 000000000..743ad5180 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-and-image-tooltip_02.js @@ -0,0 +1,66 @@ +/* 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"; + +// Tests that after a color change, opening another tooltip, like the image +// preview doesn't revert the color change in the rule view. +// This used to happen when the activeSwatch wasn't reset when the colorpicker +// would hide. +// See bug 979292 + +const TEST_URI = ` + <style type="text/css"> + body { + background: red url("chrome://global/skin/icons/warning-64.png") + no-repeat center center; + } + </style> + Testing the color picker tooltip! +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {view} = yield openRuleView(); + yield testColorChangeIsntRevertedWhenOtherTooltipIsShown(view); +}); + +function* testColorChangeIsntRevertedWhenOtherTooltipIsShown(ruleView) { + let swatch = getRuleViewProperty(ruleView, "body", "background").valueSpan + .querySelector(".ruleview-colorswatch"); + + info("Open the color picker tooltip and change the color"); + let picker = ruleView.tooltips.colorPicker; + let onColorPickerReady = picker.once("ready"); + swatch.click(); + yield onColorPickerReady; + + yield simulateColorPickerChange(ruleView, picker, [0, 0, 0, 1], { + selector: "body", + name: "background-color", + value: "rgb(0, 0, 0)" + }); + + let spectrum = picker.spectrum; + + let onModifications = waitForNEvents(ruleView, "ruleview-changed", 2); + let onHidden = picker.tooltip.once("hidden"); + focusAndSendKey(spectrum.element.ownerDocument.defaultView, "RETURN"); + yield onHidden; + yield onModifications; + + info("Open the image preview tooltip"); + let value = getRuleViewProperty(ruleView, "body", "background").valueSpan; + let url = value.querySelector(".theme-link"); + let onShown = ruleView.tooltips.previewTooltip.once("shown"); + let anchor = yield isHoverTooltipTarget(ruleView.tooltips.previewTooltip, url); + ruleView.tooltips.previewTooltip.show(anchor); + yield onShown; + + info("Image tooltip is shown, verify that the swatch is still correct"); + swatch = value.querySelector(".ruleview-colorswatch"); + is(swatch.style.backgroundColor, "black", + "The swatch's color is correct"); + is(swatch.nextSibling.textContent, "black", "The color name is correct"); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_colorpicker-appears-on-swatch-click.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-appears-on-swatch-click.js new file mode 100644 index 000000000..383ffed6c --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-appears-on-swatch-click.js @@ -0,0 +1,51 @@ +/* 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"; + +// Tests that color pickers appear when clicking on color swatches. + +const TEST_URI = ` + <style type="text/css"> + body { + color: red; + background-color: #ededed; + background-image: url(chrome://global/skin/icons/warning-64.png); + border: 2em solid rgba(120, 120, 120, .5); + } + </style> + Testing the color picker tooltip! +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {view} = yield openRuleView(); + + let propertiesToTest = ["color", "background-color", "border"]; + + for (let property of propertiesToTest) { + info("Testing that the colorpicker appears on swatch click"); + let value = getRuleViewProperty(view, "body", property).valueSpan; + let swatch = value.querySelector(".ruleview-colorswatch"); + yield testColorPickerAppearsOnColorSwatchClick(view, swatch); + } +}); + +function* testColorPickerAppearsOnColorSwatchClick(view, swatch) { + let cPicker = view.tooltips.colorPicker; + ok(cPicker, "The rule-view has the expected colorPicker property"); + + let cPickerPanel = cPicker.tooltip.panel; + ok(cPickerPanel, "The XUL panel for the color picker exists"); + + let onColorPickerReady = cPicker.once("ready"); + swatch.click(); + yield onColorPickerReady; + + ok(true, "The color picker was shown on click of the color swatch"); + ok(!inplaceEditor(swatch.parentNode), + "The inplace editor wasn't shown as a result of the color swatch click"); + + yield hideTooltipAndWaitForRuleViewChanged(cPicker, view); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_colorpicker-commit-on-ENTER.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-commit-on-ENTER.js new file mode 100644 index 000000000..129e8f245 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-commit-on-ENTER.js @@ -0,0 +1,61 @@ +/* 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"; + +// Tests that a color change in the color picker is committed when ENTER is +// pressed. + +const TEST_URI = ` + <style type="text/css"> + body { + border: 2em solid rgba(120, 120, 120, .5); + } + </style> + Testing the color picker tooltip! +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {view} = yield openRuleView(); + + let swatch = getRuleViewProperty(view, "body", "border").valueSpan + .querySelector(".ruleview-colorswatch"); + yield testPressingEnterCommitsChanges(swatch, view); +}); + +function* testPressingEnterCommitsChanges(swatch, ruleView) { + let cPicker = ruleView.tooltips.colorPicker; + + let onColorPickerReady = cPicker.once("ready"); + swatch.click(); + yield onColorPickerReady; + + yield simulateColorPickerChange(ruleView, cPicker, [0, 255, 0, .5], { + selector: "body", + name: "border-left-color", + value: "rgba(0, 255, 0, 0.5)" + }); + + is(swatch.style.backgroundColor, "rgba(0, 255, 0, 0.5)", + "The color swatch's background was updated"); + is(getRuleViewProperty(ruleView, "body", "border").valueSpan.textContent, + "2em solid rgba(0, 255, 0, 0.5)", + "The text of the border css property was updated"); + + let onModified = ruleView.once("ruleview-changed"); + let spectrum = cPicker.spectrum; + let onHidden = cPicker.tooltip.once("hidden"); + focusAndSendKey(spectrum.element.ownerDocument.defaultView, "RETURN"); + yield onHidden; + yield onModified; + + is((yield getComputedStyleProperty("body", null, "border-left-color")), + "rgba(0, 255, 0, 0.5)", "The element's border was kept after RETURN"); + is(swatch.style.backgroundColor, "rgba(0, 255, 0, 0.5)", + "The color swatch's background was kept after RETURN"); + is(getRuleViewProperty(ruleView, "body", "border").valueSpan.textContent, + "2em solid rgba(0, 255, 0, 0.5)", + "The text of the border css property was kept after RETURN"); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_colorpicker-edit-gradient.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-edit-gradient.js new file mode 100644 index 000000000..71ceb14c3 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-edit-gradient.js @@ -0,0 +1,77 @@ +/* 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"; + +// Tests that changing a color in a gradient css declaration using the tooltip +// color picker works. + +const TEST_URI = ` + <style type="text/css"> + body { + background-image: linear-gradient(to left, #f06 25%, #333 95%, #000 100%); + } + </style> + Updating a gradient declaration with the color picker tooltip +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {view} = yield openRuleView(); + + info("Testing that the colors in gradient properties are parsed correctly"); + testColorParsing(view); + + info("Testing that changing one of the colors of a gradient property works"); + yield testPickingNewColor(view); +}); + +function testColorParsing(view) { + let ruleEl = getRuleViewProperty(view, "body", "background-image"); + ok(ruleEl, "The background-image gradient declaration was found"); + + let swatchEls = ruleEl.valueSpan.querySelectorAll(".ruleview-colorswatch"); + ok(swatchEls, "The color swatch elements were found"); + is(swatchEls.length, 3, "There are 3 color swatches"); + + let colorEls = ruleEl.valueSpan.querySelectorAll(".ruleview-color"); + ok(colorEls, "The color elements were found"); + is(colorEls.length, 3, "There are 3 color values"); + + let colors = ["#f06", "#333", "#000"]; + for (let i = 0; i < colors.length; i++) { + is(colorEls[i].textContent, colors[i], "The right color value was found"); + } +} + +function* testPickingNewColor(view) { + // Grab the first color swatch and color in the gradient + let ruleEl = getRuleViewProperty(view, "body", "background-image"); + let swatchEl = ruleEl.valueSpan.querySelector(".ruleview-colorswatch"); + let colorEl = ruleEl.valueSpan.querySelector(".ruleview-color"); + + info("Get the color picker tooltip and clicking on the swatch to show it"); + let cPicker = view.tooltips.colorPicker; + let onColorPickerReady = cPicker.once("ready"); + swatchEl.click(); + yield onColorPickerReady; + + let change = { + selector: "body", + name: "background-image", + value: "linear-gradient(to left, rgb(1, 1, 1) 25%, " + + "rgb(51, 51, 51) 95%, rgb(0, 0, 0) 100%)" + }; + yield simulateColorPickerChange(view, cPicker, [1, 1, 1, 1], change); + + is(swatchEl.style.backgroundColor, "rgb(1, 1, 1)", + "The color swatch's background was updated"); + is(colorEl.textContent, "#010101", "The color text was updated"); + is((yield getComputedStyleProperty("body", null, "background-image")), + "linear-gradient(to left, rgb(1, 1, 1) 25%, rgb(51, 51, 51) 95%, " + + "rgb(0, 0, 0) 100%)", + "The gradient has been updated correctly"); + + yield hideTooltipAndWaitForRuleViewChanged(cPicker, view); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_colorpicker-hides-on-tooltip.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-hides-on-tooltip.js new file mode 100644 index 000000000..b50c63605 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-hides-on-tooltip.js @@ -0,0 +1,46 @@ +/* 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"; + +// Tests that the color picker tooltip hides when an image tooltip appears. + +const TEST_URI = ` + <style type="text/css"> + body { + color: red; + background-color: #ededed; + background-image: url(chrome://global/skin/icons/warning-64.png); + border: 2em solid rgba(120, 120, 120, .5); + } + </style> + Testing the color picker tooltip! +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {view} = yield openRuleView(); + + let swatch = getRuleViewProperty(view, "body", "color").valueSpan + .querySelector(".ruleview-colorswatch"); + + let bgImageSpan = getRuleViewProperty(view, "body", "background-image").valueSpan; + let uriSpan = bgImageSpan.querySelector(".theme-link"); + + let colorPicker = view.tooltips.colorPicker; + info("Showing the color picker tooltip by clicking on the color swatch"); + let onColorPickerReady = colorPicker.once("ready"); + swatch.click(); + yield onColorPickerReady; + + info("Now showing the image preview tooltip to hide the color picker"); + let onHidden = colorPicker.tooltip.once("hidden"); + // Hiding the color picker refreshes the value. + let onRuleViewChanged = view.once("ruleview-changed"); + yield assertHoverTooltipOn(view.tooltips.previewTooltip, uriSpan); + yield onHidden; + yield onRuleViewChanged; + + ok(true, "The color picker closed when the image preview tooltip appeared"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_colorpicker-multiple-changes.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-multiple-changes.js new file mode 100644 index 000000000..06fab72d6 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-multiple-changes.js @@ -0,0 +1,124 @@ +/* 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"; + +// Tests that the color in the colorpicker tooltip can be changed several times. +// without causing error in various cases: +// - simple single-color property (color) +// - color and image property (background-image) +// - overridden property +// See bug 979292 and bug 980225 + +const TEST_URI = ` + <style type="text/css"> + body { + color: green; + background: red url("chrome://global/skin/icons/warning-64.png") + no-repeat center center; + } + p { + color: blue; + } + </style> + <p>Testing the color picker tooltip!</p> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + + yield testSimpleMultipleColorChanges(inspector, view); + yield testComplexMultipleColorChanges(inspector, view); + yield testOverriddenMultipleColorChanges(inspector, view); +}); + +function* testSimpleMultipleColorChanges(inspector, ruleView) { + yield selectNode("p", inspector); + + info("Getting the <p> tag's color property"); + let swatch = getRuleViewProperty(ruleView, "p", "color").valueSpan + .querySelector(".ruleview-colorswatch"); + + info("Opening the color picker"); + let picker = ruleView.tooltips.colorPicker; + let onColorPickerReady = picker.once("ready"); + swatch.click(); + yield onColorPickerReady; + + info("Changing the color several times"); + let colors = [ + {rgba: [0, 0, 0, 1], computed: "rgb(0, 0, 0)"}, + {rgba: [100, 100, 100, 1], computed: "rgb(100, 100, 100)"}, + {rgba: [200, 200, 200, 1], computed: "rgb(200, 200, 200)"} + ]; + for (let {rgba, computed} of colors) { + yield simulateColorPickerChange(ruleView, picker, rgba, { + selector: "p", + name: "color", + value: computed + }); + } +} + +function* testComplexMultipleColorChanges(inspector, ruleView) { + yield selectNode("body", inspector); + + info("Getting the <body> tag's color property"); + let swatch = getRuleViewProperty(ruleView, "body", "background").valueSpan + .querySelector(".ruleview-colorswatch"); + + info("Opening the color picker"); + let picker = ruleView.tooltips.colorPicker; + let onColorPickerReady = picker.once("ready"); + swatch.click(); + yield onColorPickerReady; + + info("Changing the color several times"); + let colors = [ + {rgba: [0, 0, 0, 1], computed: "rgb(0, 0, 0)"}, + {rgba: [100, 100, 100, 1], computed: "rgb(100, 100, 100)"}, + {rgba: [200, 200, 200, 1], computed: "rgb(200, 200, 200)"} + ]; + for (let {rgba, computed} of colors) { + yield simulateColorPickerChange(ruleView, picker, rgba, { + selector: "body", + name: "background-color", + value: computed + }); + } + + info("Closing the color picker"); + yield hideTooltipAndWaitForRuleViewChanged(picker, ruleView); +} + +function* testOverriddenMultipleColorChanges(inspector, ruleView) { + yield selectNode("p", inspector); + + info("Getting the <body> tag's color property"); + let swatch = getRuleViewProperty(ruleView, "body", "color").valueSpan + .querySelector(".ruleview-colorswatch"); + + info("Opening the color picker"); + let picker = ruleView.tooltips.colorPicker; + let onColorPickerReady = picker.once("ready"); + swatch.click(); + yield onColorPickerReady; + + info("Changing the color several times"); + let colors = [ + {rgba: [0, 0, 0, 1], computed: "rgb(0, 0, 0)"}, + {rgba: [100, 100, 100, 1], computed: "rgb(100, 100, 100)"}, + {rgba: [200, 200, 200, 1], computed: "rgb(200, 200, 200)"} + ]; + for (let {rgba, computed} of colors) { + yield simulateColorPickerChange(ruleView, picker, rgba, { + selector: "body", + name: "color", + value: computed + }); + is((yield getComputedStyleProperty("p", null, "color")), + "rgb(200, 200, 200)", "The color of the P tag is still correct"); + } +} diff --git a/devtools/client/inspector/rules/test/browser_rules_colorpicker-release-outside-frame.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-release-outside-frame.js new file mode 100644 index 000000000..ef6ca02b1 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-release-outside-frame.js @@ -0,0 +1,67 @@ +/* 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"; + +// Tests that color pickers stops following the pointer if the pointer is +// released outside the tooltip frame (bug 1160720). + +const TEST_URI = "<body style='color: red'>Test page for bug 1160720"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {view} = yield openRuleView(); + + let cSwatch = getRuleViewProperty(view, "element", "color").valueSpan + .querySelector(".ruleview-colorswatch"); + + let picker = yield openColorPickerForSwatch(cSwatch, view); + let spectrum = picker.spectrum; + let change = spectrum.once("changed"); + + info("Pressing mouse down over color picker."); + let onRuleViewChanged = view.once("ruleview-changed"); + EventUtils.synthesizeMouseAtCenter(spectrum.dragger, { + type: "mousedown", + }, spectrum.dragger.ownerDocument.defaultView); + yield onRuleViewChanged; + + let value = yield change; + info(`Color changed to ${value} on mousedown.`); + + // If the mousemove below fails to detect that the button is no longer pressed + // the spectrum will update and emit changed event synchronously after calling + // synthesizeMouse so this handler is executed before the test ends. + spectrum.once("changed", (event, newValue) => { + is(newValue, value, "Value changed on mousemove without a button pressed."); + }); + + // Releasing the button pressed by mousedown above on top of a different frame + // does not make sense in this test as EventUtils doesn't preserve the context + // i.e. the buttons that were pressed down between events. + + info("Moving mouse over color picker without any buttons pressed."); + + EventUtils.synthesizeMouse(spectrum.dragger, 10, 10, { + // -1 = no buttons are pressed down + button: -1, + type: "mousemove", + }, spectrum.dragger.ownerDocument.defaultView); +}); + +function* openColorPickerForSwatch(swatch, view) { + let cPicker = view.tooltips.colorPicker; + ok(cPicker, "The rule-view has the expected colorPicker property"); + + let cPickerPanel = cPicker.tooltip.panel; + ok(cPickerPanel, "The XUL panel for the color picker exists"); + + let onColorPickerReady = cPicker.once("ready"); + swatch.click(); + yield onColorPickerReady; + + ok(true, "The color picker was shown on click of the color swatch"); + + return cPicker; +} diff --git a/devtools/client/inspector/rules/test/browser_rules_colorpicker-revert-on-ESC.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-revert-on-ESC.js new file mode 100644 index 000000000..e244d429c --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-revert-on-ESC.js @@ -0,0 +1,109 @@ +/* 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"; + +// Tests that a color change in the color picker is reverted when ESC is +// pressed. + +const TEST_URI = ` + <style type="text/css"> + body { + background-color: #EDEDED; + } + </style> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {view} = yield openRuleView(); + yield testPressingEscapeRevertsChanges(view); + yield testPressingEscapeRevertsChangesAndDisables(view); +}); + +function* testPressingEscapeRevertsChanges(view) { + let {swatch, propEditor, cPicker} = yield openColorPickerAndSelectColor(view, + 1, 0, [0, 0, 0, 1], { + selector: "body", + name: "background-color", + value: "rgb(0, 0, 0)" + }); + + is(swatch.style.backgroundColor, "rgb(0, 0, 0)", + "The color swatch's background was updated"); + is(propEditor.valueSpan.textContent, "#000", + "The text of the background-color css property was updated"); + + let spectrum = cPicker.spectrum; + + info("Pressing ESCAPE to close the tooltip"); + let onHidden = cPicker.tooltip.once("hidden"); + let onModifications = view.once("ruleview-changed"); + EventUtils.sendKey("ESCAPE", spectrum.element.ownerDocument.defaultView); + yield onHidden; + yield onModifications; + + yield waitForComputedStyleProperty("body", null, "background-color", + "rgb(237, 237, 237)"); + is(propEditor.valueSpan.textContent, "#EDEDED", + "Got expected property value."); +} + +function* testPressingEscapeRevertsChangesAndDisables(view) { + let ruleEditor = getRuleViewRuleEditor(view, 1); + + info("Disabling background-color property"); + let textProp = ruleEditor.rule.textProps[0]; + yield togglePropStatus(view, textProp); + + ok(textProp.editor.element.classList.contains("ruleview-overridden"), + "property is overridden."); + is(textProp.editor.enable.style.visibility, "visible", + "property enable checkbox is visible."); + ok(!textProp.editor.enable.getAttribute("checked"), + "property enable checkbox is not checked."); + ok(!textProp.editor.prop.enabled, + "background-color property is disabled."); + let newValue = yield getRulePropertyValue("background-color"); + is(newValue, "", "background-color should have been unset."); + + let {cPicker} = yield openColorPickerAndSelectColor(view, + 1, 0, [0, 0, 0, 1]); + + ok(!textProp.editor.element.classList.contains("ruleview-overridden"), + "property overridden is not displayed."); + is(textProp.editor.enable.style.visibility, "hidden", + "property enable checkbox is hidden."); + + let spectrum = cPicker.spectrum; + + info("Pressing ESCAPE to close the tooltip"); + let onHidden = cPicker.tooltip.once("hidden"); + let onModifications = view.once("ruleview-changed"); + EventUtils.sendKey("ESCAPE", spectrum.element.ownerDocument.defaultView); + yield onHidden; + yield onModifications; + + ok(textProp.editor.element.classList.contains("ruleview-overridden"), + "property is overridden."); + is(textProp.editor.enable.style.visibility, "visible", + "property enable checkbox is visible."); + ok(!textProp.editor.enable.getAttribute("checked"), + "property enable checkbox is not checked."); + ok(!textProp.editor.prop.enabled, + "background-color property is disabled."); + newValue = yield getRulePropertyValue("background-color"); + is(newValue, "", "background-color should have been unset."); + is(textProp.editor.valueSpan.textContent, "#EDEDED", + "Got expected property value."); +} + +function* getRulePropertyValue(name) { + let propValue = yield executeInContent("Test:GetRulePropertyValue", { + styleSheetIndex: 0, + ruleIndex: 0, + name: name + }); + return propValue; +} diff --git a/devtools/client/inspector/rules/test/browser_rules_colorpicker-swatch-displayed.js b/devtools/client/inspector/rules/test/browser_rules_colorpicker-swatch-displayed.js new file mode 100644 index 000000000..b06ff37df --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-swatch-displayed.js @@ -0,0 +1,73 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that color swatches are displayed next to colors in the rule-view. + +const TEST_URI = ` + <style type="text/css"> + body { + color: red; + background-color: #ededed; + background-image: url(chrome://global/skin/icons/warning-64.png); + border: 2em solid rgba(120, 120, 120, .5); + } + * { + color: blue; + background: linear-gradient( + to right, + #f00, + #f008, + #00ff00, + #00ff0080, + rgb(31,170,217), + rgba(31,170,217,.5), + hsl(5, 5%, 5%), + hsla(5, 5%, 5%, 0.25), + #F00, + #F008, + #00FF00, + #00FF0080, + RGB(31,170,217), + RGBA(31,170,217,.5), + HSL(5, 5%, 5%), + HSLA(5, 5%, 5%, 0.25)); + box-shadow: inset 0 0 2px 20px red, inset 0 0 2px 40px blue; + } + </style> + Testing the color picker tooltip! +`; + +// Tests that properties in the rule-view contain color swatches. +// Each entry in the test array should contain: +// { +// selector: the rule-view selector to look for the property in +// propertyName: the property to test +// nb: the number of color swatches this property should have +// } +const TESTS = [ + {selector: "body", propertyName: "color", nb: 1}, + {selector: "body", propertyName: "background-color", nb: 1}, + {selector: "body", propertyName: "border", nb: 1}, + {selector: "*", propertyName: "color", nb: 1}, + {selector: "*", propertyName: "background", nb: 16}, + {selector: "*", propertyName: "box-shadow", nb: 2}, +]; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {view} = yield openRuleView(); + + for (let {selector, propertyName, nb} of TESTS) { + info("Looking for color swatches in property " + propertyName + + " in selector " + selector); + + let prop = getRuleViewProperty(view, selector, propertyName).valueSpan; + let swatches = prop.querySelectorAll(".ruleview-colorswatch"); + + ok(swatches.length, "Swatches found in the property"); + is(swatches.length, nb, "Correct number of swatches found in the property"); + } +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_completion-existing-property_01.js b/devtools/client/inspector/rules/test/browser_rules_completion-existing-property_01.js new file mode 100644 index 000000000..566bae259 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_completion-existing-property_01.js @@ -0,0 +1,139 @@ +/* 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"; + +// Tests that CSS property names are autocompleted and cycled correctly when +// editing an existing property in the rule view. + +// format : +// [ +// what key to press, +// expected input box value after keypress, +// is the popup open, +// is a suggestion selected in the popup, +// ] + +const OPEN = true, SELECTED = true; +var testData = [ + ["VK_RIGHT", "font", !OPEN, !SELECTED], + ["-", "font-size", OPEN, SELECTED], + ["f", "font-family", OPEN, SELECTED], + ["VK_BACK_SPACE", "font-f", !OPEN, !SELECTED], + ["VK_BACK_SPACE", "font-", !OPEN, !SELECTED], + ["VK_BACK_SPACE", "font", !OPEN, !SELECTED], + ["VK_BACK_SPACE", "fon", !OPEN, !SELECTED], + ["VK_BACK_SPACE", "fo", !OPEN, !SELECTED], + ["VK_BACK_SPACE", "f", !OPEN, !SELECTED], + ["VK_BACK_SPACE", "", !OPEN, !SELECTED], + ["d", "display", OPEN, SELECTED], + ["VK_DOWN", "dominant-baseline", OPEN, SELECTED], + ["VK_DOWN", "direction", OPEN, SELECTED], + ["VK_DOWN", "display", OPEN, SELECTED], + ["VK_UP", "direction", OPEN, SELECTED], + ["VK_UP", "dominant-baseline", OPEN, SELECTED], + ["VK_UP", "display", OPEN, SELECTED], + ["VK_BACK_SPACE", "d", !OPEN, !SELECTED], + ["i", "display", OPEN, SELECTED], + ["s", "display", !OPEN, !SELECTED], + ["VK_BACK_SPACE", "dis", !OPEN, !SELECTED], + ["VK_BACK_SPACE", "di", !OPEN, !SELECTED], + ["VK_BACK_SPACE", "d", !OPEN, !SELECTED], + ["VK_BACK_SPACE", "", !OPEN, !SELECTED], + ["VK_HOME", "", !OPEN, !SELECTED], + ["VK_END", "", !OPEN, !SELECTED], + ["VK_PAGE_UP", "", !OPEN, !SELECTED], + ["VK_PAGE_DOWN", "", !OPEN, !SELECTED], + ["d", "display", OPEN, SELECTED], + ["VK_HOME", "display", !OPEN, !SELECTED], + ["VK_END", "display", !OPEN, !SELECTED], + // Press right key to ensure caret move to end of the input on Mac OS since + // Mac OS doesn't move caret after pressing HOME / END. + ["VK_RIGHT", "display", !OPEN, !SELECTED], + ["VK_BACK_SPACE", "displa", !OPEN, !SELECTED], + ["VK_BACK_SPACE", "displ", !OPEN, !SELECTED], + ["VK_BACK_SPACE", "disp", !OPEN, !SELECTED], + ["VK_BACK_SPACE", "dis", !OPEN, !SELECTED], + ["VK_BACK_SPACE", "di", !OPEN, !SELECTED], + ["VK_BACK_SPACE", "d", !OPEN, !SELECTED], + ["VK_BACK_SPACE", "", !OPEN, !SELECTED], + ["f", "font-size", OPEN, SELECTED], + ["i", "filter", OPEN, SELECTED], + ["VK_LEFT", "filter", !OPEN, !SELECTED], + ["VK_LEFT", "filter", !OPEN, !SELECTED], + ["i", "fiilter", !OPEN, !SELECTED], + ["VK_ESCAPE", null, !OPEN, !SELECTED], +]; + +const TEST_URI = "<h1 style='font: 24px serif'>Header</h1>"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {toolbox, inspector, view, testActor} = yield openRuleView(); + + info("Test autocompletion after 1st page load"); + yield runAutocompletionTest(toolbox, inspector, view); + + info("Test autocompletion after page navigation"); + yield reloadPage(inspector, testActor); + yield runAutocompletionTest(toolbox, inspector, view); +}); + +function* runAutocompletionTest(toolbox, inspector, view) { + info("Selecting the test node"); + yield selectNode("h1", inspector); + + info("Focusing the css property editable field"); + let propertyName = view.styleDocument.querySelectorAll(".ruleview-propertyname")[0]; + let editor = yield focusEditableField(view, propertyName); + + info("Starting to test for css property completion"); + for (let i = 0; i < testData.length; i++) { + yield testCompletion(testData[i], editor, view); + } +} + +function* testCompletion([key, completion, open, selected], + editor, view) { + info("Pressing key " + key); + info("Expecting " + completion); + info("Is popup opened: " + open); + info("Is item selected: " + selected); + + // Listening for the right event that will tell us when the key has been + // entered and processed. + let onSuggest; + if (/(left|right|back_space|escape|home|end|page_up|page_down)/ig.test(key)) { + info("Adding event listener for " + + "left|right|back_space|escape|home|end|page_up|page_down keys"); + onSuggest = once(editor.input, "keypress"); + } else { + info("Waiting for after-suggest event on the editor"); + onSuggest = editor.once("after-suggest"); + } + + // Also listening for popup opened/closed events if needed. + let popupEvent = open ? "popup-opened" : "popup-closed"; + let onPopupEvent = editor.popup.isOpen !== open ? once(editor.popup, popupEvent) : null; + + info("Synthesizing key " + key); + EventUtils.synthesizeKey(key, {}, view.styleWindow); + + // Flush the throttle for the preview text. + view.throttle.flush(); + + yield onSuggest; + yield onPopupEvent; + + info("Checking the state"); + if (completion !== null) { + is(editor.input.value, completion, "Correct value is autocompleted"); + } + if (!open) { + ok(!(editor.popup && editor.popup.isOpen), "Popup is closed"); + } else { + ok(editor.popup.isOpen, "Popup is open"); + is(editor.popup.selectedIndex !== -1, selected, "An item is selected"); + } +} diff --git a/devtools/client/inspector/rules/test/browser_rules_completion-existing-property_02.js b/devtools/client/inspector/rules/test/browser_rules_completion-existing-property_02.js new file mode 100644 index 000000000..fde8f5d12 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_completion-existing-property_02.js @@ -0,0 +1,123 @@ +/* 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"; + +// Tests that CSS property names and values are autocompleted and cycled +// correctly when editing existing properties in the rule view. + +// format : +// [ +// what key to press, +// modifers, +// expected input box value after keypress, +// is the popup open, +// is a suggestion selected in the popup, +// expect ruleview-changed, +// ] + +const OPEN = true, SELECTED = true, CHANGE = true; +var testData = [ + ["b", {}, "beige", OPEN, SELECTED, CHANGE], + ["l", {}, "black", OPEN, SELECTED, CHANGE], + ["VK_DOWN", {}, "blanchedalmond", OPEN, SELECTED, CHANGE], + ["VK_DOWN", {}, "blue", OPEN, SELECTED, CHANGE], + ["VK_RIGHT", {}, "blue", !OPEN, !SELECTED, !CHANGE], + [" ", {}, "blue aliceblue", OPEN, SELECTED, CHANGE], + ["!", {}, "blue !important", !OPEN, !SELECTED, CHANGE], + ["VK_BACK_SPACE", {}, "blue !", !OPEN, !SELECTED, CHANGE], + ["VK_BACK_SPACE", {}, "blue ", !OPEN, !SELECTED, CHANGE], + ["VK_BACK_SPACE", {}, "blue", !OPEN, !SELECTED, CHANGE], + ["VK_TAB", {shiftKey: true}, "color", !OPEN, !SELECTED, CHANGE], + ["VK_BACK_SPACE", {}, "", !OPEN, !SELECTED, !CHANGE], + ["d", {}, "display", OPEN, SELECTED, !CHANGE], + ["VK_TAB", {}, "blue", !OPEN, !SELECTED, CHANGE], + ["n", {}, "none", !OPEN, !SELECTED, CHANGE], + ["VK_RETURN", {}, null, !OPEN, !SELECTED, CHANGE] +]; + +const TEST_URI = "<h1 style='color: red'>Header</h1>"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {toolbox, inspector, view, testActor} = yield openRuleView(); + + info("Test autocompletion after 1st page load"); + yield runAutocompletionTest(toolbox, inspector, view); + + info("Test autocompletion after page navigation"); + yield reloadPage(inspector, testActor); + yield runAutocompletionTest(toolbox, inspector, view); +}); + +function* runAutocompletionTest(toolbox, inspector, view) { + info("Selecting the test node"); + yield selectNode("h1", inspector); + + let rule = getRuleViewRuleEditor(view, 0).rule; + let prop = rule.textProps[0]; + + info("Focusing the css property editable value"); + let editor = yield focusEditableField(view, prop.editor.valueSpan); + + info("Starting to test for css property completion"); + for (let i = 0; i < testData.length; i++) { + // Re-define the editor at each iteration, because the focus may have moved + // from property to value and back + editor = inplaceEditor(view.styleDocument.activeElement); + yield testCompletion(testData[i], editor, view); + } +} + +function* testCompletion([key, modifiers, completion, open, selected, change], + editor, view) { + info("Pressing key " + key); + info("Expecting " + completion); + info("Is popup opened: " + open); + info("Is item selected: " + selected); + + let onDone; + if (change) { + // If the key triggers a ruleview-changed, wait for that event, it will + // always be the last to be triggered and tells us when the preview has + // been done. + onDone = view.once("ruleview-changed"); + } else { + // Otherwise, expect an after-suggest event (except if the popup gets + // closed). + onDone = key !== "VK_RIGHT" && key !== "VK_BACK_SPACE" + ? editor.once("after-suggest") + : null; + } + + info("Synthesizing key " + key + ", modifiers: " + Object.keys(modifiers)); + + // Also listening for popup opened/closed events if needed. + let popupEvent = open ? "popup-opened" : "popup-closed"; + let onPopupEvent = editor.popup.isOpen !== open ? once(editor.popup, popupEvent) : null; + + EventUtils.synthesizeKey(key, modifiers, view.styleWindow); + + // Flush the throttle for the preview text. + view.throttle.flush(); + + yield onDone; + yield onPopupEvent; + + // The key might have been a TAB or shift-TAB, in which case the editor will + // be a new one + editor = inplaceEditor(view.styleDocument.activeElement); + + info("Checking the state"); + if (completion !== null) { + is(editor.input.value, completion, "Correct value is autocompleted"); + } + + if (!open) { + ok(!(editor.popup && editor.popup.isOpen), "Popup is closed"); + } else { + ok(editor.popup.isOpen, "Popup is open"); + is(editor.popup.selectedIndex !== -1, selected, "An item is selected"); + } +} diff --git a/devtools/client/inspector/rules/test/browser_rules_completion-new-property_01.js b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_01.js new file mode 100644 index 000000000..86ff9ca03 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_01.js @@ -0,0 +1,102 @@ +/* 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"; + +// Tests that CSS property names are autocompleted and cycled correctly when +// creating a new property in the rule view. + +// format : +// [ +// what key to press, +// expected input box value after keypress, +// is the popup open, +// is a suggestion selected in the popup, +// ] +const OPEN = true, SELECTED = true; +var testData = [ + ["d", "display", OPEN, SELECTED], + ["VK_DOWN", "dominant-baseline", OPEN, SELECTED], + ["VK_DOWN", "direction", OPEN, SELECTED], + ["VK_DOWN", "display", OPEN, SELECTED], + ["VK_UP", "direction", OPEN, SELECTED], + ["VK_UP", "dominant-baseline", OPEN, SELECTED], + ["VK_UP", "display", OPEN, SELECTED], + ["VK_BACK_SPACE", "d", !OPEN, !SELECTED], + ["i", "display", OPEN, SELECTED], + ["s", "display", !OPEN, !SELECTED], + ["VK_BACK_SPACE", "dis", !OPEN, !SELECTED], + ["VK_BACK_SPACE", "di", !OPEN, !SELECTED], + ["VK_BACK_SPACE", "d", !OPEN, !SELECTED], + ["VK_BACK_SPACE", "", !OPEN, !SELECTED], + ["f", "font-size", OPEN, SELECTED], + ["i", "filter", OPEN, SELECTED], + ["VK_ESCAPE", null, !OPEN, !SELECTED], +]; + +const TEST_URI = "<h1 style='border: 1px solid red'>Header</h1>"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {toolbox, inspector, view, testActor} = yield openRuleView(); + + info("Test autocompletion after 1st page load"); + yield runAutocompletionTest(toolbox, inspector, view); + + info("Test autocompletion after page navigation"); + yield reloadPage(inspector, testActor); + yield runAutocompletionTest(toolbox, inspector, view); +}); + +function* runAutocompletionTest(toolbox, inspector, view) { + info("Selecting the test node"); + yield selectNode("h1", inspector); + + info("Focusing the css property editable field"); + let ruleEditor = getRuleViewRuleEditor(view, 0); + let editor = yield focusNewRuleViewProperty(ruleEditor); + + info("Starting to test for css property completion"); + for (let i = 0; i < testData.length; i++) { + yield testCompletion(testData[i], editor, view); + } +} + +function* testCompletion([key, completion, open, isSelected], editor, view) { + info("Pressing key " + key); + info("Expecting " + completion); + info("Is popup opened: " + open); + info("Is item selected: " + isSelected); + + let onSuggest; + + if (/(right|back_space|escape)/ig.test(key)) { + info("Adding event listener for right|back_space|escape keys"); + onSuggest = once(editor.input, "keypress"); + } else { + info("Waiting for after-suggest event on the editor"); + onSuggest = editor.once("after-suggest"); + } + + // Also listening for popup opened/closed events if needed. + let popupEvent = open ? "popup-opened" : "popup-closed"; + let onPopupEvent = editor.popup.isOpen !== open ? once(editor.popup, popupEvent) : null; + + info("Synthesizing key " + key); + EventUtils.synthesizeKey(key, {}, view.styleWindow); + + yield onSuggest; + yield onPopupEvent; + + info("Checking the state"); + if (completion !== null) { + is(editor.input.value, completion, "Correct value is autocompleted"); + } + if (!open) { + ok(!(editor.popup && editor.popup.isOpen), "Popup is closed"); + } else { + ok(editor.popup.isOpen, "Popup is open"); + is(editor.popup.selectedIndex !== -1, isSelected, "An item is selected"); + } +} diff --git a/devtools/client/inspector/rules/test/browser_rules_completion-new-property_02.js b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_02.js new file mode 100644 index 000000000..d89e5129d --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_02.js @@ -0,0 +1,129 @@ +/* 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"; + +// Tests that CSS property names and values are autocompleted and cycled +// correctly when editing new properties in the rule view. + +// format : +// [ +// what key to press, +// modifers, +// expected input box value after keypress, +// is the popup open, +// is a suggestion selected in the popup, +// expect ruleview-changed, +// ] + +const OPEN = true, SELECTED = true, CHANGE = true; +const testData = [ + ["d", {}, "display", OPEN, SELECTED, !CHANGE], + ["VK_TAB", {}, "", OPEN, !SELECTED, CHANGE], + ["VK_DOWN", {}, "block", OPEN, SELECTED, CHANGE], + ["n", {}, "none", !OPEN, !SELECTED, CHANGE], + ["VK_TAB", {shiftKey: true}, "display", !OPEN, !SELECTED, CHANGE], + ["VK_BACK_SPACE", {}, "", !OPEN, !SELECTED, !CHANGE], + ["o", {}, "overflow", OPEN, SELECTED, !CHANGE], + ["u", {}, "outline", OPEN, SELECTED, !CHANGE], + ["VK_DOWN", {}, "outline-color", OPEN, SELECTED, !CHANGE], + ["VK_TAB", {}, "none", !OPEN, !SELECTED, CHANGE], + ["r", {}, "rebeccapurple", OPEN, SELECTED, CHANGE], + ["VK_DOWN", {}, "red", OPEN, SELECTED, CHANGE], + ["VK_DOWN", {}, "rgb", OPEN, SELECTED, CHANGE], + ["VK_DOWN", {}, "rgba", OPEN, SELECTED, CHANGE], + ["VK_DOWN", {}, "rosybrown", OPEN, SELECTED, CHANGE], + ["VK_DOWN", {}, "royalblue", OPEN, SELECTED, CHANGE], + ["VK_RIGHT", {}, "royalblue", !OPEN, !SELECTED, !CHANGE], + [" ", {}, "royalblue aliceblue", OPEN, SELECTED, CHANGE], + ["!", {}, "royalblue !important", !OPEN, !SELECTED, CHANGE], + ["VK_ESCAPE", {}, null, !OPEN, !SELECTED, CHANGE] +]; + +const TEST_URI = ` + <style type="text/css"> + h1 { + border: 1px solid red; + } + </style> + <h1>Test element</h1> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {toolbox, inspector, view, testActor} = yield openRuleView(); + + info("Test autocompletion after 1st page load"); + yield runAutocompletionTest(toolbox, inspector, view); + + info("Test autocompletion after page navigation"); + yield reloadPage(inspector, testActor); + yield runAutocompletionTest(toolbox, inspector, view); +}); + +function* runAutocompletionTest(toolbox, inspector, view) { + info("Selecting the test node"); + yield selectNode("h1", inspector); + + info("Focusing a new css property editable property"); + let ruleEditor = getRuleViewRuleEditor(view, 1); + let editor = yield focusNewRuleViewProperty(ruleEditor); + + info("Starting to test for css property completion"); + for (let i = 0; i < testData.length; i++) { + // Re-define the editor at each iteration, because the focus may have moved + // from property to value and back + editor = inplaceEditor(view.styleDocument.activeElement); + yield testCompletion(testData[i], editor, view); + } +} + +function* testCompletion([key, modifiers, completion, open, selected, change], + editor, view) { + info("Pressing key " + key); + info("Expecting " + completion); + info("Is popup opened: " + open); + info("Is item selected: " + selected); + + let onDone; + if (change) { + // If the key triggers a ruleview-changed, wait for that event, it will + // always be the last to be triggered and tells us when the preview has + // been done. + onDone = view.once("ruleview-changed"); + } else { + // Otherwise, expect an after-suggest event (except if the popup gets + // closed). + onDone = key !== "VK_RIGHT" && key !== "VK_BACK_SPACE" + ? editor.once("after-suggest") + : null; + } + + // Also listening for popup opened/closed events if needed. + let popupEvent = open ? "popup-opened" : "popup-closed"; + let onPopupEvent = editor.popup.isOpen !== open ? once(editor.popup, popupEvent) : null; + + info("Synthesizing key " + key + ", modifiers: " + Object.keys(modifiers)); + EventUtils.synthesizeKey(key, modifiers, view.styleWindow); + + // Flush the throttle for the preview text. + view.throttle.flush(); + + yield onDone; + yield onPopupEvent; + + info("Checking the state"); + if (completion !== null) { + // The key might have been a TAB or shift-TAB, in which case the editor will + // be a new one + editor = inplaceEditor(view.styleDocument.activeElement); + is(editor.input.value, completion, "Correct value is autocompleted"); + } + if (!open) { + ok(!(editor.popup && editor.popup.isOpen), "Popup is closed"); + } else { + ok(editor.popup.isOpen, "Popup is open"); + is(editor.popup.selectedIndex !== -1, selected, "An item is selected"); + } +} diff --git a/devtools/client/inspector/rules/test/browser_rules_completion-new-property_03.js b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_03.js new file mode 100644 index 000000000..a5072429c --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_03.js @@ -0,0 +1,47 @@ +/* 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"; + +// Regression test for a case where completing gave the wrong answer. +// See bug 1179318. + +const TEST_URI = "<h1 style='color: red'>Header</h1>"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {toolbox, inspector, view} = yield openRuleView(); + + info("Test autocompletion for background-color"); + yield runAutocompletionTest(toolbox, inspector, view); +}); + +function* runAutocompletionTest(toolbox, inspector, view) { + info("Selecting the test node"); + yield selectNode("h1", inspector); + + info("Focusing the new property editable field"); + let ruleEditor = getRuleViewRuleEditor(view, 0); + let editor = yield focusNewRuleViewProperty(ruleEditor); + + info("Sending \"background\" to the editable field"); + for (let key of "background") { + let onSuggest = editor.once("after-suggest"); + EventUtils.synthesizeKey(key, {}, view.styleWindow); + yield onSuggest; + } + + const itemIndex = 4; + + let bgcItem = editor.popup.getItemAtIndex(itemIndex); + is(bgcItem.label, "background-color", + "check the expected completion element"); + + editor.popup.selectedIndex = itemIndex; + + let node = editor.popup._list.childNodes[itemIndex]; + EventUtils.synthesizeMouseAtCenter(node, {}, editor.popup._window); + + is(editor.input.value, "background-color", "Correct value is autocompleted"); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_completion-new-property_04.js b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_04.js new file mode 100644 index 000000000..e19794e1b --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_04.js @@ -0,0 +1,73 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that a new property editor supports the following flow: +// - type first character of property name +// - select an autocomplete suggestion !!with a mouse click!! +// - press RETURN to move to the property value +// - blur the input to commit + +const TEST_URI = "<style>.title {color: red;}</style>" + + "<h1 class=title>Header</h1>"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let { inspector, view} = yield openRuleView(); + + info("Selecting the test node"); + yield selectNode("h1", inspector); + + info("Focusing the new property editable field"); + let ruleEditor = getRuleViewRuleEditor(view, 1); + let editor = yield focusNewRuleViewProperty(ruleEditor); + + info("Sending \"background\" to the editable field."); + for (let key of "background") { + let onSuggest = editor.once("after-suggest"); + EventUtils.synthesizeKey(key, {}, view.styleWindow); + yield onSuggest; + } + + const itemIndex = 4; + let bgcItem = editor.popup.getItemAtIndex(itemIndex); + is(bgcItem.label, "background-color", + "Check the expected completion element is background-color."); + editor.popup.selectedIndex = itemIndex; + + info("Select the background-color suggestion with a mouse click."); + let onSuggest = editor.once("after-suggest"); + let node = editor.popup.elements.get(bgcItem); + EventUtils.synthesizeMouseAtCenter(node, {}, editor.popup._window); + + yield onSuggest; + is(editor.input.value, "background-color", "Correct value is autocompleted"); + + info("Press RETURN to move the focus to a property value editor."); + let onModifications = view.once("ruleview-changed"); + EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow); + + yield onModifications; + + // Getting the new value editor after focus + editor = inplaceEditor(view.styleDocument.activeElement); + let textProp = ruleEditor.rule.textProps[1]; + + is(ruleEditor.rule.textProps.length, 2, + "Created a new text property."); + is(ruleEditor.propertyList.children.length, 2, + "Created a property editor."); + is(editor, inplaceEditor(textProp.editor.valueSpan), + "Editing the value span now."); + + info("Entering a value and blurring the field to expect a rule change"); + editor.input.value = "#F00"; + + onModifications = view.once("ruleview-changed"); + editor.input.blur(); + yield onModifications; + + is(textProp.value, "#F00", "Text prop should have been changed."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_completion-new-property_multiline.js b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_multiline.js new file mode 100644 index 000000000..ec939eafc --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_completion-new-property_multiline.js @@ -0,0 +1,131 @@ +/* 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 behaviour of the CSS autocomplete for CSS value displayed on +// multiple lines. Expected behavior is: +// - UP/DOWN should navigate in the input and not increment/decrement numbers +// - typing a new value should still trigger the autocomplete +// - UP/DOWN when the autocomplete popup is displayed should cycle through +// suggestions + +const LONG_CSS_VALUE = + "transparent linear-gradient(0deg, blue 0%, white 5%, red 10%, blue 15%, " + + "white 20%, red 25%, blue 30%, white 35%, red 40%, blue 45%, white 50%, " + + "red 55%, blue 60%, white 65%, red 70%, blue 75%, white 80%, red 85%, " + + "blue 90%, white 95% ) repeat scroll 0% 0%"; + +const EXPECTED_CSS_VALUE = LONG_CSS_VALUE.replace("95%", "95%, red"); + +const TEST_URI = + `<style> + .title { + background: ${LONG_CSS_VALUE}; + } + </style> + <h1 class=title>Header</h1>`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let { inspector, view} = yield openRuleView(); + + info("Selecting the test node"); + yield selectNode("h1", inspector); + + info("Focusing the property editable field"); + let rule = getRuleViewRuleEditor(view, 1).rule; + let prop = rule.textProps[0]; + + // Calculate offsets to click in the middle of the first box quad. + let rect = prop.editor.valueSpan.getBoundingClientRect(); + let firstQuad = prop.editor.valueSpan.getBoxQuads()[0]; + // For a multiline value, the first quad left edge is not aligned with the + // bounding rect left edge. The offsets expected by focusEditableField are + // relative to the bouding rectangle, so we need to translate the x-offset. + let x = firstQuad.bounds.left - rect.left + firstQuad.bounds.width / 2; + // The first quad top edge is aligned with the bounding top edge, no + // translation needed here. + let y = firstQuad.bounds.height / 2; + + info("Focusing the css property editable value"); + let editor = yield focusEditableField(view, prop.editor.valueSpan, x, y); + + info("Moving the caret next to a number"); + let pos = editor.input.value.indexOf("0deg") + 1; + editor.input.setSelectionRange(pos, pos); + is(editor.input.value[editor.input.selectionStart - 1], "0", + "Input caret is after a 0"); + + info("Check that UP/DOWN navigates in the input, even when next to a number"); + EventUtils.synthesizeKey("VK_DOWN", {}, view.styleWindow); + ok(editor.input.selectionStart !== pos, "Input caret moved"); + is(editor.input.value, LONG_CSS_VALUE, "Input value was not decremented."); + + info("Move the caret to the end of the gradient definition."); + pos = editor.input.value.indexOf("95%") + 3; + editor.input.setSelectionRange(pos, pos); + + info("Sending \", re\" to the editable field."); + for (let key of ", re") { + yield synthesizeKeyForAutocomplete(key, editor, view.styleWindow); + } + + info("Check the autocomplete can still be displayed."); + ok(editor.popup && editor.popup.isOpen, "Autocomplete popup is displayed."); + is(editor.popup.selectedIndex, 0, + "Autocomplete has an item selected by default"); + + let item = editor.popup.getItemAtIndex(editor.popup.selectedIndex); + is(item.label, "rebeccapurple", + "Check autocomplete displays expected value."); + + info("Check autocomplete suggestions can be cycled using UP/DOWN arrows."); + + yield synthesizeKeyForAutocomplete("VK_DOWN", editor, view.styleWindow); + ok(editor.popup.selectedIndex, 1, "Using DOWN cycles autocomplete values."); + yield synthesizeKeyForAutocomplete("VK_DOWN", editor, view.styleWindow); + ok(editor.popup.selectedIndex, 2, "Using DOWN cycles autocomplete values."); + yield synthesizeKeyForAutocomplete("VK_UP", editor, view.styleWindow); + is(editor.popup.selectedIndex, 1, "Using UP cycles autocomplete values."); + item = editor.popup.getItemAtIndex(editor.popup.selectedIndex); + is(item.label, "red", "Check autocomplete displays expected value."); + + info("Select the background-color suggestion with a mouse click."); + let onRuleviewChanged = view.once("ruleview-changed"); + let onSuggest = editor.once("after-suggest"); + + let node = editor.popup._list.childNodes[editor.popup.selectedIndex]; + EventUtils.synthesizeMouseAtCenter(node, {}, editor.popup._window); + + view.throttle.flush(); + yield onSuggest; + yield onRuleviewChanged; + + is(editor.input.value, EXPECTED_CSS_VALUE, + "Input value correctly autocompleted"); + + info("Press ESCAPE to leave the input."); + onRuleviewChanged = view.once("ruleview-changed"); + EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow); + yield onRuleviewChanged; +}); + +/** + * Send the provided key to the currently focused input of the provided window. + * Wait for the editor to emit "after-suggest" to make sure the autocompletion + * process is finished. + * + * @param {String} key + * The key to send to the input. + * @param {InplaceEditor} editor + * The inplace editor which owns the focused input. + * @param {Window} win + * Window in which the key event will be dispatched. + */ +function* synthesizeKeyForAutocomplete(key, editor, win) { + let onSuggest = editor.once("after-suggest"); + EventUtils.synthesizeKey(key, {}, win); + yield onSuggest; +} diff --git a/devtools/client/inspector/rules/test/browser_rules_completion-popup-hidden-after-navigation.js b/devtools/client/inspector/rules/test/browser_rules_completion-popup-hidden-after-navigation.js new file mode 100644 index 000000000..84f119606 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_completion-popup-hidden-after-navigation.js @@ -0,0 +1,41 @@ +/* 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"; + +// Tests that the ruleview autocomplete popup is hidden after page navigation. + +const TEST_URI = "<h1 style='font: 24px serif'></h1>"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view, testActor} = yield openRuleView(); + + info("Test autocompletion popup is hidden after page navigation"); + + info("Selecting the test node"); + yield selectNode("h1", inspector); + + info("Focusing the css property editable field"); + let propertyName = view.styleDocument + .querySelectorAll(".ruleview-propertyname")[0]; + let editor = yield focusEditableField(view, propertyName); + + info("Pressing key VK_DOWN"); + let onSuggest = once(editor.input, "keypress"); + let onPopupOpened = once(editor.popup, "popup-opened"); + + EventUtils.synthesizeKey("VK_DOWN", {}, view.styleWindow); + + info("Waiting for autocomplete popup to be displayed"); + yield onSuggest; + yield onPopupOpened; + + ok(view.popup && view.popup.isOpen, "Popup should be opened"); + + info("Reloading the page"); + yield reloadPage(inspector, testActor); + + ok(!(view.popup && view.popup.isOpen), "Popup should be closed"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_computed-lists_01.js b/devtools/client/inspector/rules/test/browser_rules_computed-lists_01.js new file mode 100644 index 000000000..5acebd562 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_computed-lists_01.js @@ -0,0 +1,47 @@ +/* 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"; + +// Tests that the rule view shows expanders for properties with computed lists. + +var TEST_URI = ` + <style type="text/css"> + #testid { + margin: 4px; + top: 0px; + } + </style> + <h1 id="testid">Styled Node</h1> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + yield testExpandersShown(inspector, view); +}); + +function* testExpandersShown(inspector, view) { + let rule = getRuleViewRuleEditor(view, 1).rule; + + info("Check that the correct rules are visible"); + is(rule.selectorText, "#testid", "Second rule is #testid."); + is(rule.textProps[0].name, "margin", "First property is margin."); + is(rule.textProps[1].name, "top", "Second property is top."); + + info("Check that the expanders are shown correctly"); + is(rule.textProps[0].editor.expander.style.visibility, "visible", + "margin expander is visible."); + is(rule.textProps[1].editor.expander.style.visibility, "hidden", + "top expander is hidden."); + ok(!rule.textProps[0].editor.expander.hasAttribute("open"), + "margin computed list is closed."); + ok(!rule.textProps[1].editor.expander.hasAttribute("open"), + "top computed list is closed."); + ok(!rule.textProps[0].editor.computed.hasChildNodes(), + "margin computed list is empty before opening."); + ok(!rule.textProps[1].editor.computed.hasChildNodes(), + "top computed list is empty."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_computed-lists_02.js b/devtools/client/inspector/rules/test/browser_rules_computed-lists_02.js new file mode 100644 index 000000000..d6dc82d5f --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_computed-lists_02.js @@ -0,0 +1,74 @@ +/* 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"; + +// Tests that the rule view computed lists can be expanded/collapsed, +// and contain the right subproperties. + +var TEST_URI = ` + <style type="text/css"> + #testid { + margin: 0px 1px 2px 3px; + top: 0px; + } + </style> + <h1 id="testid">Styled Node</h1> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + yield testComputedList(inspector, view); +}); + +function* testComputedList(inspector, view) { + let rule = getRuleViewRuleEditor(view, 1).rule; + let propEditor = rule.textProps[0].editor; + let expander = propEditor.expander; + + ok(!expander.hasAttribute("open"), "margin computed list is closed"); + + info("Opening the computed list of margin property"); + expander.click(); + ok(expander.hasAttribute("open"), "margin computed list is open"); + + let computed = propEditor.prop.computed; + let computedDom = propEditor.computed; + let propNames = [ + "margin-top", + "margin-right", + "margin-bottom", + "margin-left" + ]; + + is(computed.length, propNames.length, "There should be 4 computed values"); + is(computedDom.children.length, propNames.length, + "There should be 4 nodes in the DOM"); + + propNames.forEach((propName, i) => { + let propValue = i + "px"; + is(computed[i].name, propName, + "Computed property #" + i + " has name " + propName); + is(computed[i].value, propValue, + "Computed property #" + i + " has value " + propValue); + is(computedDom.querySelectorAll(".ruleview-propertyname")[i].textContent, + propName, + "Computed property #" + i + " in DOM has correct name"); + is(computedDom.querySelectorAll(".ruleview-propertyvalue")[i].textContent, + propValue, + "Computed property #" + i + " in DOM has correct value"); + }); + + info("Closing the computed list of margin property"); + expander.click(); + ok(!expander.hasAttribute("open"), "margin computed list is closed"); + + info("Opening the computed list of margin property"); + expander.click(); + ok(expander.hasAttribute("open"), "margin computed list is open"); + is(computed.length, propNames.length, "Still 4 computed values"); + is(computedDom.children.length, propNames.length, "Still 4 nodes in the DOM"); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_content_01.js b/devtools/client/inspector/rules/test/browser_rules_content_01.js new file mode 100644 index 000000000..8695d9b8d --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_content_01.js @@ -0,0 +1,51 @@ +/* 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 rule-view content is correct + +const TEST_URI = ` + <style type="text/css"> + @media screen and (min-width: 10px) { + #testid { + background-color: blue; + } + } + .testclass, .unmatched { + background-color: green; + } + </style> + <div id="testid" class="testclass">Styled Node</div> + <div id="testid2">Styled Node</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + + yield selectNode("#testid", inspector); + is(view.element.querySelectorAll("#ruleview-no-results").length, 0, + "After a highlight, no longer has a no-results element."); + + yield clearCurrentNodeSelection(inspector); + is(view.element.querySelectorAll("#ruleview-no-results").length, 1, + "After highlighting null, has a no-results element again."); + + yield selectNode("#testid", inspector); + + let linkText = getRuleViewLinkTextByIndex(view, 1); + is(linkText, "inline:3 @screen and (min-width: 10px)", + "link text at index 1 contains media query text."); + + linkText = getRuleViewLinkTextByIndex(view, 2); + is(linkText, "inline:7", + "link text at index 2 contains no media query text."); + + let selector = getRuleViewRuleEditor(view, 2).selectorText; + is(selector.querySelector(".ruleview-selector-matched").textContent, + ".testclass", ".textclass should be matched."); + is(selector.querySelector(".ruleview-selector-unmatched").textContent, + ".unmatched", ".unmatched should not be matched."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_content_02.js b/devtools/client/inspector/rules/test/browser_rules_content_02.js new file mode 100644 index 000000000..253f374b4 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_content_02.js @@ -0,0 +1,60 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* globals getTestActorWithoutToolbox */ +"use strict"; + +// Test the rule-view content when the inspector gets opened via the page +// ctx-menu "inspect element" + +const CONTENT = ` + <body style="color:red;"> + <div style="color:blue;"> + <p style="color:green;"> + <span style="color:yellow;">test element</span> + </p> + </div> + </body> +`; + +add_task(function* () { + let tab = yield addTab("data:text/html;charset=utf-8," + CONTENT); + + let testActor = yield getTestActorWithoutToolbox(tab); + let inspector = yield clickOnInspectMenuItem(testActor, "span"); + + checkRuleViewContent(inspector.ruleview.view); +}); + +function checkRuleViewContent({styleDocument}) { + info("Making sure the rule-view contains the expected content"); + + let headers = [...styleDocument.querySelectorAll(".ruleview-header")]; + is(headers.length, 3, "There are 3 headers for inherited rules"); + + is(headers[0].textContent, + STYLE_INSPECTOR_L10N.getFormatStr("rule.inheritedFrom", "p"), + "The first header is correct"); + is(headers[1].textContent, + STYLE_INSPECTOR_L10N.getFormatStr("rule.inheritedFrom", "div"), + "The second header is correct"); + is(headers[2].textContent, + STYLE_INSPECTOR_L10N.getFormatStr("rule.inheritedFrom", "body"), + "The third header is correct"); + + let rules = styleDocument.querySelectorAll(".ruleview-rule"); + is(rules.length, 4, "There are 4 rules in the view"); + + for (let rule of rules) { + let selector = rule.querySelector(".ruleview-selectorcontainer"); + is(selector.textContent, STYLE_INSPECTOR_L10N.getStr("rule.sourceElement"), + "The rule's selector is correct"); + + let propertyNames = [...rule.querySelectorAll(".ruleview-propertyname")]; + is(propertyNames.length, 1, "There's only one property name, as expected"); + + let propertyValues = [...rule.querySelectorAll(".ruleview-propertyvalue")]; + is(propertyValues.length, 1, "There's only one property value, as expected"); + } +} + diff --git a/devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-01.js b/devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-01.js new file mode 100644 index 000000000..b81bb8013 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-01.js @@ -0,0 +1,96 @@ +/* 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/ */ + +/** + * This file tests the code that integrates the Style Inspector's rule view + * with the MDN docs tooltip. + * + * If you display the context click on a property name in the rule view, you + * should see a menu item "Show MDN Docs". If you click that item, the MDN + * docs tooltip should be shown, containing docs from MDN for that property. + * + * This file tests that the context menu item is shown when it should be + * shown and hidden when it should be hidden. + */ + +"use strict"; + +/** + * The test document tries to confuse the context menu + * code by having a tag called "padding" and a property + * value called "margin". + */ +const TEST_URI = ` + <html> + <head> + <style> + padding {font-family: margin;} + </style> + </head> + + <body> + <padding>MDN tooltip testing</padding> + </body> + </html> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("padding", inspector); + yield testMdnContextMenuItemVisibility(view); +}); + +/** + * Tests that the MDN context menu item is shown when it should be, + * and hidden when it should be. + * - iterate through every node in the rule view + * - set that node as popupNode (the node that the context menu + * is shown for) + * - update the context menu's state + * - test that the MDN context menu item is hidden, or not, + * depending on popupNode + */ +function* testMdnContextMenuItemVisibility(view) { + info("Test that MDN context menu item is shown only when it should be."); + + let root = rootElement(view); + for (let node of iterateNodes(root)) { + info("Setting " + node + " as popupNode"); + info("Creating context menu with " + node + " as popupNode"); + let allMenuItems = openStyleContextMenuAndGetAllItems(view, node); + let menuitemShowMdnDocs = allMenuItems.find(item => item.label === + STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.showMdnDocs")); + + let isVisible = menuitemShowMdnDocs.visible; + let shouldBeVisible = isPropertyNameNode(node); + let message = shouldBeVisible ? "shown" : "hidden"; + is(isVisible, shouldBeVisible, + "The MDN context menu item is " + message + " ; content : " + + node.textContent + " ; type : " + node.nodeType); + } +} + +/** + * Check if a node is a property name. + */ +function isPropertyNameNode(node) { + return node.textContent === "font-family"; +} + +/** + * A generator that iterates recursively through 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 rule view. + */ +var rootElement = view => (view.element) ? view.element : view.styleDocument; diff --git a/devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-02.js b/devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-02.js new file mode 100644 index 000000000..e0d08d28a --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-02.js @@ -0,0 +1,61 @@ +/* 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/ */ + +/** + * This file tests the code that integrates the Style Inspector's rule view + * with the MDN docs tooltip. + * + * If you display the context click on a property name in the rule view, you + * should see a menu item "Show MDN Docs". If you click that item, the MDN + * docs tooltip should be shown, containing docs from MDN for that property. + * + * This file tests that: + * - clicking the context menu item shows the tooltip + * - the tooltip content matches the property name for which the context menu was opened + */ + +"use strict"; + +const {setBaseCssDocsUrl} = + require("devtools/client/shared/widgets/MdnDocsWidget"); + +const PROPERTYNAME = "color"; + +const TEST_DOC = ` + <html> + <body> + <div style="color: red"> + Test "Show MDN Docs" context menu option + </div> + </body> + </html> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf8," + encodeURIComponent(TEST_DOC)); + let {inspector, view} = yield openRuleView(); + yield selectNode("div", inspector); + + setBaseCssDocsUrl(URL_ROOT); + + info("Setting the popupNode for the MDN docs tooltip"); + + let {nameSpan} = getRuleViewProperty(view, "element", PROPERTYNAME); + + let allMenuItems = openStyleContextMenuAndGetAllItems(view, nameSpan.firstChild); + let menuitemShowMdnDocs = allMenuItems.find(item => item.label === + STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.showMdnDocs")); + + let cssDocs = view.tooltips.cssDocs; + + info("Showing the MDN docs tooltip"); + let onShown = cssDocs.tooltip.once("shown"); + menuitemShowMdnDocs.click(); + yield onShown; + ok(true, "The MDN docs tooltip was shown"); + + info("Quick check that the tooltip contents are set"); + let h1 = cssDocs.tooltip.container.querySelector(".mdn-property-name"); + is(h1.textContent, PROPERTYNAME, "The MDN docs tooltip h1 is correct"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-03.js b/devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-03.js new file mode 100644 index 000000000..d1089fcf6 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-03.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/ */ + +/** + * This file tests the "devtools.inspector.mdnDocsTooltip.enabled" preference, + * that we use to enable/disable the MDN tooltip in the Inspector. + * + * The desired behavior is: + * - if the preference is true, show the "Show MDN Docs" context menu item + * - if the preference is false, don't show the item + * - listen for changes to the pref, so we can show/hide the item dynamically + */ + +"use strict"; + +const { PrefObserver } = require("devtools/client/styleeditor/utils"); +const PREF_ENABLE_MDN_DOCS_TOOLTIP = + "devtools.inspector.mdnDocsTooltip.enabled"; +const PROPERTY_NAME_CLASS = "ruleview-propertyname"; + +const TEST_DOC = ` + <html> + <body> + <div style="color: red"> + Test the pref to enable/disable the "Show MDN Docs" context menu option + </div> + </body> + </html> +`; + +add_task(function* () { + info("Ensure the pref is true to begin with"); + let initial = Services.prefs.getBoolPref(PREF_ENABLE_MDN_DOCS_TOOLTIP); + if (initial != true) { + setBooleanPref(PREF_ENABLE_MDN_DOCS_TOOLTIP, true); + } + + yield addTab("data:text/html;charset=utf8," + encodeURIComponent(TEST_DOC)); + + let {inspector, view} = yield openRuleView(); + yield selectNode("div", inspector); + yield testMdnContextMenuItemVisibility(view, true); + + yield setBooleanPref(PREF_ENABLE_MDN_DOCS_TOOLTIP, false); + yield testMdnContextMenuItemVisibility(view, false); + + info("Close the Inspector"); + let target = TargetFactory.forTab(gBrowser.selectedTab); + yield gDevTools.closeToolbox(target); + + ({inspector, view} = yield openRuleView()); + yield selectNode("div", inspector); + yield testMdnContextMenuItemVisibility(view, false); + + yield setBooleanPref(PREF_ENABLE_MDN_DOCS_TOOLTIP, true); + yield testMdnContextMenuItemVisibility(view, true); + + info("Ensure the pref is reset to its initial value"); + let eventual = Services.prefs.getBoolPref(PREF_ENABLE_MDN_DOCS_TOOLTIP); + if (eventual != initial) { + setBooleanPref(PREF_ENABLE_MDN_DOCS_TOOLTIP, initial); + } +}); + +/** + * Set a boolean pref, and wait for the pref observer to + * trigger, so that code listening for the pref change + * has had a chance to update itself. + * + * @param pref {string} Name of the pref to change + * @param state {boolean} Desired value of the pref. + * + * Note that if the pref already has the value in `state`, + * then the prefObserver will not trigger. So you should only + * call this function if you know the pref's current value is + * not `state`. + */ +function* setBooleanPref(pref, state) { + let oncePrefChanged = defer(); + let prefObserver = new PrefObserver("devtools."); + prefObserver.on(pref, oncePrefChanged.resolve); + + info("Set the pref " + pref + " to: " + state); + Services.prefs.setBoolPref(pref, state); + + info("Wait for prefObserver to call back so the UI can update"); + yield oncePrefChanged.promise; + prefObserver.off(pref, oncePrefChanged.resolve); +} + +/** + * Test whether the MDN tooltip context menu item is visible when it should be. + * + * @param view The rule view + * @param shouldBeVisible {boolean} Whether we expect the context + * menu item to be visible or not. + */ +function* testMdnContextMenuItemVisibility(view, shouldBeVisible) { + let message = shouldBeVisible ? "shown" : "hidden"; + info("Test that MDN context menu item is " + message); + + info("Set a CSS property name as popupNode"); + let root = rootElement(view); + let node = root.querySelector("." + PROPERTY_NAME_CLASS).firstChild; + let allMenuItems = openStyleContextMenuAndGetAllItems(view, node); + let menuitemShowMdnDocs = allMenuItems.find(item => item.label === + STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.showMdnDocs")); + + let isVisible = menuitemShowMdnDocs.visible; + is(isVisible, shouldBeVisible, + "The MDN context menu item is " + message); +} + +/** + * Returns the root element for the rule view. + */ +var rootElement = view => (view.element) ? view.element : view.styleDocument; diff --git a/devtools/client/inspector/rules/test/browser_rules_copy_styles.js b/devtools/client/inspector/rules/test/browser_rules_copy_styles.js new file mode 100644 index 000000000..a6f991a60 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_copy_styles.js @@ -0,0 +1,307 @@ +/* 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"; + +/** + * Tests the behaviour of the copy styles context menu items in the rule + * view. + */ + +const osString = Services.appinfo.OS; + +const TEST_URI = URL_ROOT + "doc_copystyles.html"; + +add_task(function* () { + yield addTab(TEST_URI); + let { inspector, view } = yield openRuleView(); + yield selectNode("#testid", inspector); + + let ruleEditor = getRuleViewRuleEditor(view, 1); + + let data = [ + { + desc: "Test Copy Property Name", + node: ruleEditor.rule.textProps[0].editor.nameSpan, + menuItemLabel: "styleinspector.contextmenu.copyPropertyName", + expectedPattern: "color", + visible: { + copyLocation: false, + copyPropertyDeclaration: true, + copyPropertyName: true, + copyPropertyValue: false, + copySelector: false, + copyRule: true + } + }, + { + desc: "Test Copy Property Value", + node: ruleEditor.rule.textProps[2].editor.valueSpan, + menuItemLabel: "styleinspector.contextmenu.copyPropertyValue", + expectedPattern: "12px", + visible: { + copyLocation: false, + copyPropertyDeclaration: true, + copyPropertyName: false, + copyPropertyValue: true, + copySelector: false, + copyRule: true + } + }, + { + desc: "Test Copy Property Value with Priority", + node: ruleEditor.rule.textProps[3].editor.valueSpan, + menuItemLabel: "styleinspector.contextmenu.copyPropertyValue", + expectedPattern: "#00F !important", + visible: { + copyLocation: false, + copyPropertyDeclaration: true, + copyPropertyName: false, + copyPropertyValue: true, + copySelector: false, + copyRule: true + } + }, + { + desc: "Test Copy Property Declaration", + node: ruleEditor.rule.textProps[2].editor.nameSpan, + menuItemLabel: "styleinspector.contextmenu.copyPropertyDeclaration", + expectedPattern: "font-size: 12px;", + visible: { + copyLocation: false, + copyPropertyDeclaration: true, + copyPropertyName: true, + copyPropertyValue: false, + copySelector: false, + copyRule: true + } + }, + { + desc: "Test Copy Property Declaration with Priority", + node: ruleEditor.rule.textProps[3].editor.nameSpan, + menuItemLabel: "styleinspector.contextmenu.copyPropertyDeclaration", + expectedPattern: "border-color: #00F !important;", + visible: { + copyLocation: false, + copyPropertyDeclaration: true, + copyPropertyName: true, + copyPropertyValue: false, + copySelector: false, + copyRule: true + } + }, + { + desc: "Test Copy Rule", + node: ruleEditor.rule.textProps[2].editor.nameSpan, + menuItemLabel: "styleinspector.contextmenu.copyRule", + expectedPattern: "#testid {[\\r\\n]+" + + "\tcolor: #F00;[\\r\\n]+" + + "\tbackground-color: #00F;[\\r\\n]+" + + "\tfont-size: 12px;[\\r\\n]+" + + "\tborder-color: #00F !important;[\\r\\n]+" + + "\t--var: \"\\*/\";[\\r\\n]+" + + "}", + visible: { + copyLocation: false, + copyPropertyDeclaration: true, + copyPropertyName: true, + copyPropertyValue: false, + copySelector: false, + copyRule: true + } + }, + { + desc: "Test Copy Selector", + node: ruleEditor.selectorText, + menuItemLabel: "styleinspector.contextmenu.copySelector", + expectedPattern: "html, body, #testid", + visible: { + copyLocation: false, + copyPropertyDeclaration: false, + copyPropertyName: false, + copyPropertyValue: false, + copySelector: true, + copyRule: true + } + }, + { + desc: "Test Copy Location", + node: ruleEditor.source, + menuItemLabel: "styleinspector.contextmenu.copyLocation", + expectedPattern: "http://example.com/browser/devtools/client/" + + "inspector/rules/test/doc_copystyles.css", + visible: { + copyLocation: true, + copyPropertyDeclaration: false, + copyPropertyName: false, + copyPropertyValue: false, + copySelector: false, + copyRule: true + } + }, + { + setup: function* () { + yield disableProperty(view, 0); + }, + desc: "Test Copy Rule with Disabled Property", + node: ruleEditor.rule.textProps[2].editor.nameSpan, + menuItemLabel: "styleinspector.contextmenu.copyRule", + expectedPattern: "#testid {[\\r\\n]+" + + "\t\/\\* color: #F00; \\*\/[\\r\\n]+" + + "\tbackground-color: #00F;[\\r\\n]+" + + "\tfont-size: 12px;[\\r\\n]+" + + "\tborder-color: #00F !important;[\\r\\n]+" + + "\t--var: \"\\*/\";[\\r\\n]+" + + "}", + visible: { + copyLocation: false, + copyPropertyDeclaration: true, + copyPropertyName: true, + copyPropertyValue: false, + copySelector: false, + copyRule: true + } + }, + { + setup: function* () { + yield disableProperty(view, 4); + }, + desc: "Test Copy Rule with Disabled Property with Comment", + node: ruleEditor.rule.textProps[2].editor.nameSpan, + menuItemLabel: "styleinspector.contextmenu.copyRule", + expectedPattern: "#testid {[\\r\\n]+" + + "\t\/\\* color: #F00; \\*\/[\\r\\n]+" + + "\tbackground-color: #00F;[\\r\\n]+" + + "\tfont-size: 12px;[\\r\\n]+" + + "\tborder-color: #00F !important;[\\r\\n]+" + + "\t/\\* --var: \"\\*\\\\\/\"; \\*\/[\\r\\n]+" + + "}", + visible: { + copyLocation: false, + copyPropertyDeclaration: true, + copyPropertyName: true, + copyPropertyValue: false, + copySelector: false, + copyRule: true + } + }, + { + desc: "Test Copy Property Declaration with Disabled Property", + node: ruleEditor.rule.textProps[0].editor.nameSpan, + menuItemLabel: "styleinspector.contextmenu.copyPropertyDeclaration", + expectedPattern: "\/\\* color: #F00; \\*\/", + visible: { + copyLocation: false, + copyPropertyDeclaration: true, + copyPropertyName: true, + copyPropertyValue: false, + copySelector: false, + copyRule: true + } + }, + ]; + + for (let { setup, desc, node, menuItemLabel, expectedPattern, visible } of data) { + if (setup) { + yield setup(); + } + + info(desc); + yield checkCopyStyle(view, node, menuItemLabel, expectedPattern, visible); + } +}); + +function* checkCopyStyle(view, node, menuItemLabel, expectedPattern, visible) { + let allMenuItems = openStyleContextMenuAndGetAllItems(view, node); + let menuItem = allMenuItems.find(item => + item.label === STYLE_INSPECTOR_L10N.getStr(menuItemLabel)); + let menuitemCopy = allMenuItems.find(item => item.label === + STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copy")); + let menuitemCopyLocation = allMenuItems.find(item => item.label === + STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyLocation")); + let menuitemCopyPropertyDeclaration = allMenuItems.find(item => item.label === + STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyPropertyDeclaration")); + let menuitemCopyPropertyName = allMenuItems.find(item => item.label === + STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyPropertyName")); + let menuitemCopyPropertyValue = allMenuItems.find(item => item.label === + STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyPropertyValue")); + let menuitemCopySelector = allMenuItems.find(item => item.label === + STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copySelector")); + let menuitemCopyRule = allMenuItems.find(item => item.label === + STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyRule")); + + ok(menuitemCopy.disabled, + "Copy disabled is as expected: true"); + ok(menuitemCopy.visible, + "Copy visible is as expected: true"); + + is(menuitemCopyLocation.visible, + visible.copyLocation, + "Copy Location visible attribute is as expected: " + + visible.copyLocation); + + is(menuitemCopyPropertyDeclaration.visible, + visible.copyPropertyDeclaration, + "Copy Property Declaration visible attribute is as expected: " + + visible.copyPropertyDeclaration); + + is(menuitemCopyPropertyName.visible, + visible.copyPropertyName, + "Copy Property Name visible attribute is as expected: " + + visible.copyPropertyName); + + is(menuitemCopyPropertyValue.visible, + visible.copyPropertyValue, + "Copy Property Value visible attribute is as expected: " + + visible.copyPropertyValue); + + is(menuitemCopySelector.visible, + visible.copySelector, + "Copy Selector visible attribute is as expected: " + + visible.copySelector); + + is(menuitemCopyRule.visible, + visible.copyRule, + "Copy Rule visible attribute is as expected: " + + visible.copyRule); + + try { + yield waitForClipboardPromise(() => menuItem.click(), + () => checkClipboardData(expectedPattern)); + } catch (e) { + failedClipboard(expectedPattern); + } +} + +function* disableProperty(view, index) { + let ruleEditor = getRuleViewRuleEditor(view, 1); + let textProp = ruleEditor.rule.textProps[index]; + yield togglePropStatus(view, textProp); +} + +function checkClipboardData(expectedPattern) { + let actual = SpecialPowers.getClipboardData("text/unicode"); + let expectedRegExp = new RegExp(expectedPattern, "g"); + return expectedRegExp.test(actual); +} + +function failedClipboard(expectedPattern) { + // Format expected text for comparison + let terminator = osString == "WINNT" ? "\r\n" : "\n"; + expectedPattern = expectedPattern.replace(/\[\\r\\n\][+*]/g, terminator); + expectedPattern = expectedPattern.replace(/\\\(/g, "("); + expectedPattern = expectedPattern.replace(/\\\)/g, ")"); + + let actual = SpecialPowers.getClipboardData("text/unicode"); + + // Trim the right hand side of our strings. This is because expectedPattern + // accounts for windows sometimes adding a newline to our copied data. + expectedPattern = expectedPattern.trimRight(); + actual = actual.trimRight(); + + ok(false, "Clipboard text does not match expected " + + "results (escaped for accurate comparison):\n"); + info("Actual: " + escape(actual)); + info("Expected: " + escape(expectedPattern)); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_css-docs-tooltip_closes-on-escape.js b/devtools/client/inspector/rules/test/browser_rules_css-docs-tooltip_closes-on-escape.js new file mode 100644 index 000000000..f386f45b4 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_css-docs-tooltip_closes-on-escape.js @@ -0,0 +1,51 @@ +/* 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/ */ + +/** + * Test that the CssDocs tooltip of the ruleview can be closed when pressing the Escape + * key. + */ + +"use strict"; + +const {setBaseCssDocsUrl} = + require("devtools/client/shared/widgets/MdnDocsWidget"); + +const PROPERTYNAME = "color"; + +const TEST_URI = ` + <html> + <body> + <div style="color: red"> + Test "Show MDN Docs" closes on escape + </div> + </body> + </html> +`; + +/** + * Test that the tooltip is hidden when we press Escape + */ +add_task(function* () { + yield addTab("data:text/html;charset=utf8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("div", inspector); + + setBaseCssDocsUrl(URL_ROOT); + + info("Retrieve a valid anchor for the CssDocs tooltip"); + let {nameSpan} = getRuleViewProperty(view, "element", PROPERTYNAME); + + info("Showing the MDN docs tooltip"); + let onShown = view.tooltips.cssDocs.tooltip.once("shown"); + view.tooltips.cssDocs.show(nameSpan, PROPERTYNAME); + yield onShown; + ok(true, "The MDN docs tooltip was shown"); + + info("Simulate pressing the 'Escape' key"); + let onHidden = view.tooltips.cssDocs.tooltip.once("hidden"); + EventUtils.sendKey("escape"); + yield onHidden; + ok(true, "The MDN docs tooltip was hidden on pressing 'escape'"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_cssom.js b/devtools/client/inspector/rules/test/browser_rules_cssom.js new file mode 100644 index 000000000..d20e85192 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_cssom.js @@ -0,0 +1,22 @@ +/* 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 to ensure that CSSOM doesn't make the rule view blow up. +// https://bugzilla.mozilla.org/show_bug.cgi?id=1224121 + +const TEST_URI = URL_ROOT + "doc_cssom.html"; + +add_task(function* () { + yield addTab(TEST_URI); + let {inspector, view} = yield openRuleView(); + yield selectNode("#target", inspector); + + let elementStyle = view._elementStyle; + let rule = elementStyle.rules[1]; + + is(rule.textProps.length, 1, "rule should have one property"); + is(rule.textProps[0].name, "color", "the property should be 'color'"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_cubicbezier-appears-on-swatch-click.js b/devtools/client/inspector/rules/test/browser_rules_cubicbezier-appears-on-swatch-click.js new file mode 100644 index 000000000..18099894b --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_cubicbezier-appears-on-swatch-click.js @@ -0,0 +1,70 @@ +/* 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"; + +// Tests that cubic-bezier pickers appear when clicking on cubic-bezier +// swatches. + +const TEST_URI = ` + <style type="text/css"> + div { + animation: move 3s linear; + transition: top 4s cubic-bezier(.1, 1.45, 1, -1.2); + } + .test { + animation-timing-function: ease-in-out; + transition-timing-function: ease-out; + } + </style> + <div class="test">Testing the cubic-bezier tooltip!</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("div", inspector); + + let swatches = []; + swatches.push( + getRuleViewProperty(view, "div", "animation").valueSpan + .querySelector(".ruleview-bezierswatch") + ); + swatches.push( + getRuleViewProperty(view, "div", "transition").valueSpan + .querySelector(".ruleview-bezierswatch") + ); + swatches.push( + getRuleViewProperty(view, ".test", "animation-timing-function").valueSpan + .querySelector(".ruleview-bezierswatch") + ); + swatches.push( + getRuleViewProperty(view, ".test", "transition-timing-function").valueSpan + .querySelector(".ruleview-bezierswatch") + ); + + for (let swatch of swatches) { + info("Testing that the cubic-bezier appears on cubicswatch click"); + yield testAppears(view, swatch); + } +}); + +function* testAppears(view, swatch) { + ok(swatch, "The cubic-swatch exists"); + + let bezier = view.tooltips.cubicBezier; + ok(bezier, "The rule-view has the expected cubicBezier property"); + + let bezierPanel = bezier.tooltip.panel; + ok(bezierPanel, "The XUL panel for the cubic-bezier tooltip exists"); + + let onBezierWidgetReady = bezier.once("ready"); + swatch.click(); + yield onBezierWidgetReady; + + ok(true, "The cubic-bezier tooltip was shown on click of the cibuc swatch"); + ok(!inplaceEditor(swatch.parentNode), + "The inplace editor wasn't shown as a result of the cibuc swatch click"); + yield hideTooltipAndWaitForRuleViewChanged(bezier, view); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_cubicbezier-commit-on-ENTER.js b/devtools/client/inspector/rules/test/browser_rules_cubicbezier-commit-on-ENTER.js new file mode 100644 index 000000000..5dc43d1c9 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_cubicbezier-commit-on-ENTER.js @@ -0,0 +1,66 @@ +/* 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"; + +// Tests that a curve change in the cubic-bezier tooltip is committed when ENTER +// is pressed. + +const TEST_URI = ` + <style type="text/css"> + body { + transition: top 2s linear; + } + </style> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {view} = yield openRuleView(); + + info("Getting the bezier swatch element"); + let swatch = getRuleViewProperty(view, "body", "transition").valueSpan + .querySelector(".ruleview-bezierswatch"); + + yield testPressingEnterCommitsChanges(swatch, view); +}); + +function* testPressingEnterCommitsChanges(swatch, ruleView) { + let bezierTooltip = ruleView.tooltips.cubicBezier; + + info("Showing the tooltip"); + let onBezierWidgetReady = bezierTooltip.once("ready"); + swatch.click(); + yield onBezierWidgetReady; + + let widget = yield bezierTooltip.widget; + info("Simulating a change of curve in the widget"); + widget.coordinates = [0.1, 2, 0.9, -1]; + let expected = "cubic-bezier(0.1, 2, 0.9, -1)"; + + yield waitForSuccess(function* () { + let func = yield getComputedStyleProperty("body", null, + "transition-timing-function"); + return func === expected; + }, "Waiting for the change to be previewed on the element"); + + ok(getRuleViewProperty(ruleView, "body", "transition").valueSpan.textContent + .indexOf("cubic-bezier(") !== -1, + "The text of the timing-function was updated"); + + info("Sending RETURN key within the tooltip document"); + // Pressing RETURN ends up doing 2 rule-view updates, one for the preview and + // one for the commit when the tooltip closes. + let onRuleViewChanged = waitForNEvents(ruleView, "ruleview-changed", 2); + focusAndSendKey(widget.parent.ownerDocument.defaultView, "RETURN"); + yield onRuleViewChanged; + + let style = yield getComputedStyleProperty("body", null, + "transition-timing-function"); + is(style, expected, "The element's timing-function was kept after RETURN"); + + let ruleViewStyle = getRuleViewProperty(ruleView, "body", "transition") + .valueSpan.textContent.indexOf("cubic-bezier(") !== -1; + ok(ruleViewStyle, "The text of the timing-function was kept after RETURN"); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_cubicbezier-revert-on-ESC.js b/devtools/client/inspector/rules/test/browser_rules_cubicbezier-revert-on-ESC.js new file mode 100644 index 000000000..826d8a5aa --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_cubicbezier-revert-on-ESC.js @@ -0,0 +1,100 @@ +/* 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"; + +// Tests that changes made to the cubic-bezier timing-function in the +// cubic-bezier tooltip are reverted when ESC is pressed. + +const TEST_URI = ` + <style type='text/css'> + body { + animation-timing-function: linear; + } + </style> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {view} = yield openRuleView(); + yield testPressingEscapeRevertsChanges(view); + yield testPressingEscapeRevertsChangesAndDisables(view); +}); + +function* testPressingEscapeRevertsChanges(view) { + let {propEditor} = yield openCubicBezierAndChangeCoords(view, 1, 0, + [0.1, 2, 0.9, -1], { + selector: "body", + name: "animation-timing-function", + value: "cubic-bezier(0.1, 2, 0.9, -1)" + }); + + is(propEditor.valueSpan.textContent, "cubic-bezier(.1,2,.9,-1)", + "Got expected property value."); + + yield escapeTooltip(view); + + yield waitForComputedStyleProperty("body", null, "animation-timing-function", + "linear"); + is(propEditor.valueSpan.textContent, "linear", + "Got expected property value."); +} + +function* testPressingEscapeRevertsChangesAndDisables(view) { + let ruleEditor = getRuleViewRuleEditor(view, 1); + let textProp = ruleEditor.rule.textProps[0]; + let propEditor = textProp.editor; + + info("Disabling animation-timing-function property"); + yield togglePropStatus(view, textProp); + + ok(propEditor.element.classList.contains("ruleview-overridden"), + "property is overridden."); + is(propEditor.enable.style.visibility, "visible", + "property enable checkbox is visible."); + ok(!propEditor.enable.getAttribute("checked"), + "property enable checkbox is not checked."); + ok(!propEditor.prop.enabled, + "animation-timing-function property is disabled."); + let newValue = yield getRulePropertyValue("animation-timing-function"); + is(newValue, "", "animation-timing-function should have been unset."); + + yield openCubicBezierAndChangeCoords(view, 1, 0, [0.1, 2, 0.9, -1]); + + yield escapeTooltip(view); + + ok(propEditor.element.classList.contains("ruleview-overridden"), + "property is overridden."); + is(propEditor.enable.style.visibility, "visible", + "property enable checkbox is visible."); + ok(!propEditor.enable.getAttribute("checked"), + "property enable checkbox is not checked."); + ok(!propEditor.prop.enabled, + "animation-timing-function property is disabled."); + newValue = yield getRulePropertyValue("animation-timing-function"); + is(newValue, "", "animation-timing-function should have been unset."); + is(propEditor.valueSpan.textContent, "linear", + "Got expected property value."); +} + +function* getRulePropertyValue(name) { + let propValue = yield executeInContent("Test:GetRulePropertyValue", { + styleSheetIndex: 0, + ruleIndex: 0, + name: name + }); + return propValue; +} + +function* escapeTooltip(view) { + info("Pressing ESCAPE to close the tooltip"); + + let bezierTooltip = view.tooltips.cubicBezier; + let widget = yield bezierTooltip.widget; + let onHidden = bezierTooltip.tooltip.once("hidden"); + let onModifications = view.once("ruleview-changed"); + focusAndSendKey(widget.parent.ownerDocument.defaultView, "ESCAPE"); + yield onHidden; + yield onModifications; +} diff --git a/devtools/client/inspector/rules/test/browser_rules_custom.js b/devtools/client/inspector/rules/test/browser_rules_custom.js new file mode 100644 index 000000000..7c941af6f --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_custom.js @@ -0,0 +1,72 @@ +/* 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"; + +const TEST_URI = URL_ROOT + "doc_custom.html"; + +// Tests the display of custom declarations in the rule-view. + +add_task(function* () { + yield addTab(TEST_URI); + let {inspector, view} = yield openRuleView(); + + yield simpleCustomOverride(inspector, view); + yield importantCustomOverride(inspector, view); + yield disableCustomOverride(inspector, view); +}); + +function* simpleCustomOverride(inspector, view) { + yield selectNode("#testidSimple", inspector); + + let idRule = getRuleViewRuleEditor(view, 1).rule; + let idRuleProp = idRule.textProps[0]; + + is(idRuleProp.name, "--background-color", + "First ID prop should be --background-color"); + ok(!idRuleProp.overridden, "ID prop should not be overridden."); + + let classRule = getRuleViewRuleEditor(view, 2).rule; + let classRuleProp = classRule.textProps[0]; + + is(classRuleProp.name, "--background-color", + "First class prop should be --background-color"); + ok(classRuleProp.overridden, "Class property should be overridden."); + + // Override --background-color by changing the element style. + let elementProp = yield addProperty(view, 0, "--background-color", "purple"); + + is(classRuleProp.name, "--background-color", + "First element prop should now be --background-color"); + ok(!elementProp.overridden, + "Element style property should not be overridden"); + ok(idRuleProp.overridden, "ID property should be overridden"); + ok(classRuleProp.overridden, "Class property should be overridden"); +} + +function* importantCustomOverride(inspector, view) { + yield selectNode("#testidImportant", inspector); + + let idRule = getRuleViewRuleEditor(view, 1).rule; + let idRuleProp = idRule.textProps[0]; + ok(idRuleProp.overridden, "Not-important rule should be overridden."); + + let classRule = getRuleViewRuleEditor(view, 2).rule; + let classRuleProp = classRule.textProps[0]; + ok(!classRuleProp.overridden, "Important rule should not be overridden."); +} + +function* disableCustomOverride(inspector, view) { + yield selectNode("#testidDisable", inspector); + + let idRule = getRuleViewRuleEditor(view, 1).rule; + let idRuleProp = idRule.textProps[0]; + + yield togglePropStatus(view, idRuleProp); + + let classRule = getRuleViewRuleEditor(view, 2).rule; + let classRuleProp = classRule.textProps[0]; + ok(!classRuleProp.overridden, + "Class prop should not be overridden after id prop was disabled."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_cycle-angle.js b/devtools/client/inspector/rules/test/browser_rules_cycle-angle.js new file mode 100644 index 000000000..fa135f937 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_cycle-angle.js @@ -0,0 +1,93 @@ +/* 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 cycling angle units in the rule view. + +const TEST_URI = ` + <style type="text/css"> + body { + image-orientation: 1turn; + } + div { + image-orientation: 180deg; + } + </style> + <body><div>Test</div>cycling angle units in the rule view!</body> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + let container = getRuleViewProperty( + view, "body", "image-orientation").valueSpan; + yield checkAngleCycling(container, view); + yield checkAngleCyclingPersist(inspector, view); +}); + +function* checkAngleCycling(container, view) { + let valueNode = container.querySelector(".ruleview-angle"); + let win = view.styleWindow; + + // turn + is(valueNode.textContent, "1turn", "Angle displayed as a turn value."); + + let tests = [{ + value: "360deg", + comment: "Angle displayed as a degree value." + }, { + value: `${Math.round(Math.PI * 2 * 10000) / 10000}rad`, + comment: "Angle displayed as a radian value." + }, { + value: "400grad", + comment: "Angle displayed as a gradian value." + }, { + value: "1turn", + comment: "Angle displayed as a turn value again." + }]; + + for (let test of tests) { + yield checkSwatchShiftClick(container, win, test.value, test.comment); + } +} + +function* checkAngleCyclingPersist(inspector, view) { + yield selectNode("div", inspector); + let container = getRuleViewProperty( + view, "div", "image-orientation").valueSpan; + let valueNode = container.querySelector(".ruleview-angle"); + let win = view.styleWindow; + + is(valueNode.textContent, "180deg", "Angle displayed as a degree value."); + + yield checkSwatchShiftClick(container, win, + `${Math.round(Math.PI * 10000) / 10000}rad`, + "Angle displayed as a radian value."); + + // Select the body and reselect the div to see + // if the new angle unit persisted + yield selectNode("body", inspector); + yield selectNode("div", inspector); + + // We have to query for the container and the swatch because + // they've been re-generated + container = getRuleViewProperty(view, "div", "image-orientation").valueSpan; + valueNode = container.querySelector(".ruleview-angle"); + is(valueNode.textContent, `${Math.round(Math.PI * 10000) / 10000}rad`, + "Angle still displayed as a radian value."); +} + +function* checkSwatchShiftClick(container, win, expectedValue, comment) { + let swatch = container.querySelector(".ruleview-angleswatch"); + let valueNode = container.querySelector(".ruleview-angle"); + + let onUnitChange = swatch.once("unit-change"); + EventUtils.synthesizeMouseAtCenter(swatch, { + type: "mousedown", + shiftKey: true + }, win); + yield onUnitChange; + is(valueNode.textContent, expectedValue, comment); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_cycle-color.js b/devtools/client/inspector/rules/test/browser_rules_cycle-color.js new file mode 100644 index 000000000..e31ffa133 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_cycle-color.js @@ -0,0 +1,120 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test cycling color types in the rule view. + +const TEST_URI = ` + <style type="text/css"> + body { + color: #f00; + } + span { + color: blue; + border-color: #ff000080; + } + </style> + <body><span>Test</span> cycling color types in the rule view!</body> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + let container = getRuleViewProperty(view, "body", "color").valueSpan; + yield checkColorCycling(container, view); + yield checkAlphaColorCycling(inspector, view); + yield checkColorCyclingPersist(inspector, view); +}); + +function* checkColorCycling(container, view) { + let valueNode = container.querySelector(".ruleview-color"); + let win = view.styleWindow; + + // Hex + is(valueNode.textContent, "#f00", "Color displayed as a hex value."); + + let tests = [{ + value: "hsl(0, 100%, 50%)", + comment: "Color displayed as an HSL value." + }, { + value: "rgb(255, 0, 0)", + comment: "Color displayed as an RGB value." + }, { + value: "red", + comment: "Color displayed as a color name." + }, { + value: "#f00", + comment: "Color displayed as an authored value." + }, { + value: "hsl(0, 100%, 50%)", + comment: "Color displayed as an HSL value again." + }]; + + for (let test of tests) { + yield checkSwatchShiftClick(container, win, test.value, test.comment); + } +} + +function* checkAlphaColorCycling(inspector, view) { + yield selectNode("span", inspector); + let container = getRuleViewProperty(view, "span", "border-color").valueSpan; + let valueNode = container.querySelector(".ruleview-color"); + let win = view.styleWindow; + + is(valueNode.textContent, "#ff000080", + "Color displayed as an alpha hex value."); + + let tests = [{ + value: "hsla(0, 100%, 50%, 0.5)", + comment: "Color displayed as an HSLa value." + }, { + value: "rgba(255, 0, 0, 0.5)", + comment: "Color displayed as an RGBa value." + }, { + value: "#ff000080", + comment: "Color displayed as an alpha hex value again." + }]; + + for (let test of tests) { + yield checkSwatchShiftClick(container, win, test.value, test.comment); + } +} + +function* checkColorCyclingPersist(inspector, view) { + yield selectNode("span", inspector); + let container = getRuleViewProperty(view, "span", "color").valueSpan; + let valueNode = container.querySelector(".ruleview-color"); + let win = view.styleWindow; + + is(valueNode.textContent, "blue", "Color displayed as a color name."); + + yield checkSwatchShiftClick(container, win, "#00f", + "Color displayed as a hex value."); + + // Select the body and reselect the span to see + // if the new color unit persisted + yield selectNode("body", inspector); + yield selectNode("span", inspector); + + // We have to query for the container and the swatch because + // they've been re-generated + container = getRuleViewProperty(view, "span", "color").valueSpan; + valueNode = container.querySelector(".ruleview-color"); + is(valueNode.textContent, "#00f", + "Color is still displayed as a hex value."); +} + +function* checkSwatchShiftClick(container, win, expectedValue, comment) { + let swatch = container.querySelector(".ruleview-colorswatch"); + let valueNode = container.querySelector(".ruleview-color"); + + let onUnitChange = swatch.once("unit-change"); + EventUtils.synthesizeMouseAtCenter(swatch, { + type: "mousedown", + shiftKey: true + }, win); + yield onUnitChange; + is(valueNode.textContent, expectedValue, comment); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-display-grid-property.js b/devtools/client/inspector/rules/test/browser_rules_edit-display-grid-property.js new file mode 100644 index 000000000..18522b527 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-display-grid-property.js @@ -0,0 +1,49 @@ +/* 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 toggling the grid highlighter in the rule view and modifying the 'display: grid' +// declaration. + +const TEST_URI = ` + <style type='text/css'> + #grid { + display: grid; + } + </style> + <div id="grid"> + <div id="cell1">cell1</div> + <div id="cell2">cell2</div> + </div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + let highlighters = view.highlighters; + + yield selectNode("#grid", inspector); + let container = getRuleViewProperty(view, "#grid", "display").valueSpan; + let gridToggle = container.querySelector(".ruleview-grid"); + + info("Toggling ON the CSS grid highlighter from the rule-view."); + let onHighlighterShown = highlighters.once("highlighter-shown"); + gridToggle.click(); + yield onHighlighterShown; + + info("Edit the 'grid' property value to 'block'."); + let editor = yield focusEditableField(view, container); + let onHighlighterHidden = highlighters.once("highlighter-hidden"); + let onDone = view.once("ruleview-changed"); + editor.input.value = "block;"; + EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow); + yield onHighlighterHidden; + yield onDone; + + info("Check the grid highlighter and grid toggle button are hidden."); + gridToggle = container.querySelector(".ruleview-grid"); + ok(!gridToggle, "Grid highlighter toggle is not visible."); + ok(!highlighters.gridHighlighterShown, "No CSS grid highlighter is shown."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property-cancel.js b/devtools/client/inspector/rules/test/browser_rules_edit-property-cancel.js new file mode 100644 index 000000000..af1a6fbc0 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-cancel.js @@ -0,0 +1,46 @@ +/* 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"; + +// Tests editing a property name or value and escaping will revert the +// changes and restore the original value. + +const TEST_URI = ` + <style type='text/css'> + #testid { + background-color: #00F; + } + </style> + <div id='testid'>Styled Node</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + + let ruleEditor = getRuleViewRuleEditor(view, 1); + let propEditor = ruleEditor.rule.textProps[0].editor; + + yield focusEditableField(view, propEditor.nameSpan); + yield sendKeysAndWaitForFocus(view, ruleEditor.element, + ["DELETE", "ESCAPE"]); + + is(propEditor.nameSpan.textContent, "background-color", + "'background-color' property name is correctly set."); + is((yield getComputedStyleProperty("#testid", null, "background-color")), + "rgb(0, 0, 255)", "#00F background color is set."); + + yield focusEditableField(view, propEditor.valueSpan); + let onValueDeleted = view.once("ruleview-changed"); + yield sendKeysAndWaitForFocus(view, ruleEditor.element, + ["DELETE", "ESCAPE"]); + yield onValueDeleted; + + is(propEditor.valueSpan.textContent, "#00F", + "'#00F' property value is correctly set."); + is((yield getComputedStyleProperty("#testid", null, "background-color")), + "rgb(0, 0, 255)", "#00F background color is set."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property-click.js b/devtools/client/inspector/rules/test/browser_rules_edit-property-click.js new file mode 100644 index 000000000..08a5ee786 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-click.js @@ -0,0 +1,61 @@ +/* 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"; + +// Tests that the property name and value editors can be triggered when +// clicking on the property-name, the property-value, the colon or semicolon. + +const TEST_URI = ` + <style type='text/css'> + #testid { + margin: 0; + } + </style> + <div id='testid'>Styled Node</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + yield testEditPropertyAndCancel(inspector, view); +}); + +function* testEditPropertyAndCancel(inspector, view) { + let ruleEditor = getRuleViewRuleEditor(view, 1); + let propEditor = ruleEditor.rule.textProps[0].editor; + + info("Test editor is created when clicking on property name"); + yield focusEditableField(view, propEditor.nameSpan); + ok(propEditor.nameSpan.inplaceEditor, "Editor created for property name"); + yield sendKeysAndWaitForFocus(view, ruleEditor.element, ["ESCAPE"]); + + info("Test editor is created when clicking on ':' next to property name"); + let nameRect = propEditor.nameSpan.getBoundingClientRect(); + yield focusEditableField(view, propEditor.nameSpan, nameRect.width + 1); + ok(propEditor.nameSpan.inplaceEditor, "Editor created for property name"); + yield sendKeysAndWaitForFocus(view, ruleEditor.element, ["ESCAPE"]); + + info("Test editor is created when clicking on property value"); + yield focusEditableField(view, propEditor.valueSpan); + ok(propEditor.valueSpan.inplaceEditor, "Editor created for property value"); + // When cancelling a value edition, the text-property-editor will trigger + // a modification to make sure the property is back to its original value + // => need to wait on "ruleview-changed" to avoid unhandled promises + let onRuleviewChanged = view.once("ruleview-changed"); + yield sendKeysAndWaitForFocus(view, ruleEditor.element, ["ESCAPE"]); + yield onRuleviewChanged; + + info("Test editor is created when clicking on ';' next to property value"); + let valueRect = propEditor.valueSpan.getBoundingClientRect(); + yield focusEditableField(view, propEditor.valueSpan, valueRect.width + 1); + ok(propEditor.valueSpan.inplaceEditor, "Editor created for property value"); + // When cancelling a value edition, the text-property-editor will trigger + // a modification to make sure the property is back to its original value + // => need to wait on "ruleview-changed" to avoid unhandled promises + onRuleviewChanged = view.once("ruleview-changed"); + yield sendKeysAndWaitForFocus(view, ruleEditor.element, ["ESCAPE"]); + yield onRuleviewChanged; +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property-commit.js b/devtools/client/inspector/rules/test/browser_rules_edit-property-commit.js new file mode 100644 index 000000000..8e16601c7 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-commit.js @@ -0,0 +1,92 @@ +/* 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 original value is correctly displayed when ESCaping out of the +// inplace editor in the style inspector. + +const TEST_URI = ` + <style type='text/css'> + #testid { + color: #00F; + } + </style> + <div id='testid'>Styled Node</div> +`; + +// Test data format +// { +// value: what char sequence to type, +// commitKey: what key to type to "commit" the change, +// modifiers: commitKey modifiers, +// expected: what value is expected as a result +// } +const testData = [ + { + value: "red", + commitKey: "VK_ESCAPE", + modifiers: {}, + expected: "#00F" + }, + { + value: "red", + commitKey: "VK_RETURN", + modifiers: {}, + expected: "red" + }, + { + value: "invalid", + commitKey: "VK_RETURN", + modifiers: {}, + expected: "invalid" + }, + { + value: "blue", + commitKey: "VK_TAB", modifiers: {shiftKey: true}, + expected: "blue" + } +]; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + + for (let data of testData) { + yield runTestData(view, data); + } +}); + +function* runTestData(view, {value, commitKey, modifiers, expected}) { + let idRuleEditor = getRuleViewRuleEditor(view, 1); + let propEditor = idRuleEditor.rule.textProps[0].editor; + + info("Focusing the inplace editor field"); + + let editor = yield focusEditableField(view, propEditor.valueSpan); + is(inplaceEditor(propEditor.valueSpan), editor, + "Focused editor should be the value span."); + + info("Entering test data " + value); + let onRuleViewChanged = view.once("ruleview-changed"); + EventUtils.sendString(value, view.styleWindow); + view.throttle.flush(); + yield onRuleViewChanged; + + info("Entering the commit key " + commitKey + " " + modifiers); + onRuleViewChanged = view.once("ruleview-changed"); + let onBlur = once(editor.input, "blur"); + EventUtils.synthesizeKey(commitKey, modifiers); + yield onBlur; + yield onRuleViewChanged; + + if (commitKey === "VK_ESCAPE") { + is(propEditor.valueSpan.textContent, expected, + "Value is as expected: " + expected); + } else { + is(propEditor.valueSpan.textContent, expected, + "Value is as expected: " + expected); + } +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property-computed.js b/devtools/client/inspector/rules/test/browser_rules_edit-property-computed.js new file mode 100644 index 000000000..ee0a1fa74 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-computed.js @@ -0,0 +1,89 @@ +/* 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 computed values of a style (the shorthand expansion) are +// properly updated after the style is changed. + +const TEST_URI = ` + <style type="text/css"> + #testid { + padding: 10px; + } + </style> + <div id="testid">Styled Node</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + yield editAndCheck(view); +}); + +function* editAndCheck(view) { + let idRuleEditor = getRuleViewRuleEditor(view, 1); + let prop = idRuleEditor.rule.textProps[0]; + let propEditor = prop.editor; + let newPaddingValue = "20px"; + + info("Focusing the inplace editor field"); + let editor = yield focusEditableField(view, propEditor.valueSpan); + is(inplaceEditor(propEditor.valueSpan), editor, + "Focused editor should be the value span."); + + let onPropertyChange = waitForComputedStyleProperty("#testid", null, + "padding-top", newPaddingValue); + let onRefreshAfterPreview = once(view, "ruleview-changed"); + + info("Entering a new value"); + EventUtils.sendString(newPaddingValue, view.styleWindow); + + info("Waiting for the throttled previewValue to apply the " + + "changes to document"); + + view.throttle.flush(); + yield onPropertyChange; + + info("Waiting for ruleview-refreshed after previewValue was applied."); + yield onRefreshAfterPreview; + + let onBlur = once(editor.input, "blur"); + + info("Entering the commit key and finishing edit"); + EventUtils.synthesizeKey("VK_RETURN", {}); + + info("Waiting for blur on the field"); + yield onBlur; + + info("Waiting for the style changes to be applied"); + yield once(view, "ruleview-changed"); + + let computed = prop.computed; + let propNames = [ + "padding-top", + "padding-right", + "padding-bottom", + "padding-left" + ]; + + is(computed.length, propNames.length, "There should be 4 computed values"); + propNames.forEach((propName, i) => { + is(computed[i].name, propName, + "Computed property #" + i + " has name " + propName); + is(computed[i].value, newPaddingValue, + "Computed value of " + propName + " is as expected"); + }); + + propEditor.expander.click(); + let computedDom = propEditor.computed; + is(computedDom.children.length, propNames.length, + "There should be 4 nodes in the DOM"); + propNames.forEach((propName, i) => { + is(computedDom.getElementsByClassName("ruleview-propertyvalue")[i] + .textContent, newPaddingValue, + "Computed value of " + propName + " in DOM is as expected"); + }); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property-increments.js b/devtools/client/inspector/rules/test/browser_rules_edit-property-increments.js new file mode 100644 index 000000000..ca63cedcc --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-increments.js @@ -0,0 +1,280 @@ +/* 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 increasing/decreasing values in rule view using +// arrow keys works correctly. + +// Bug 1275446 - This test happen to hit the default timeout on linux32 +requestLongerTimeout(2); + +const TEST_URI = ` + <style> + #test { + margin-top: 0px; + padding-top: 0px; + color: #000000; + background-color: #000000; + background: none; + transition: initial; + z-index: 0; + } + </style> + <div id="test"></div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + + let {inspector, view} = yield openRuleView(); + yield selectNode("#test", inspector); + + yield testMarginIncrements(view); + yield testVariousUnitIncrements(view); + yield testHexIncrements(view); + yield testAlphaHexIncrements(view); + yield testRgbIncrements(view); + yield testShorthandIncrements(view); + yield testOddCases(view); + yield testZeroValueIncrements(view); +}); + +function* testMarginIncrements(view) { + info("Testing keyboard increments on the margin property"); + + let idRuleEditor = getRuleViewRuleEditor(view, 1); + let marginPropEditor = idRuleEditor.rule.textProps[0].editor; + + yield runIncrementTest(marginPropEditor, view, { + 1: {alt: true, start: "0px", end: "0.1px", selectAll: true}, + 2: {start: "0px", end: "1px", selectAll: true}, + 3: {shift: true, start: "0px", end: "10px", selectAll: true}, + 4: {down: true, alt: true, start: "0.1px", end: "0px", selectAll: true}, + 5: {down: true, start: "0px", end: "-1px", selectAll: true}, + 6: {down: true, shift: true, start: "0px", end: "-10px", selectAll: true}, + 7: {pageUp: true, shift: true, start: "0px", end: "100px", selectAll: true}, + 8: {pageDown: true, shift: true, start: "0px", end: "-100px", + selectAll: true}, + 9: {start: "0", end: "1px", selectAll: true}, + 10: {down: true, start: "0", end: "-1px", selectAll: true}, + }); +} + +function* testVariousUnitIncrements(view) { + info("Testing keyboard increments on values with various units"); + + let idRuleEditor = getRuleViewRuleEditor(view, 1); + let paddingPropEditor = idRuleEditor.rule.textProps[1].editor; + + yield runIncrementTest(paddingPropEditor, view, { + 1: {start: "0px", end: "1px", selectAll: true}, + 2: {start: "0pt", end: "1pt", selectAll: true}, + 3: {start: "0pc", end: "1pc", selectAll: true}, + 4: {start: "0em", end: "1em", selectAll: true}, + 5: {start: "0%", end: "1%", selectAll: true}, + 6: {start: "0in", end: "1in", selectAll: true}, + 7: {start: "0cm", end: "1cm", selectAll: true}, + 8: {start: "0mm", end: "1mm", selectAll: true}, + 9: {start: "0ex", end: "1ex", selectAll: true}, + 10: {start: "0", end: "1px", selectAll: true}, + 11: {down: true, start: "0", end: "-1px", selectAll: true}, + }); +} + +function* testHexIncrements(view) { + info("Testing keyboard increments with hex colors"); + + let idRuleEditor = getRuleViewRuleEditor(view, 1); + let hexColorPropEditor = idRuleEditor.rule.textProps[2].editor; + + yield runIncrementTest(hexColorPropEditor, view, { + 1: {start: "#CCCCCC", end: "#CDCDCD", selectAll: true}, + 2: {shift: true, start: "#CCCCCC", end: "#DCDCDC", selectAll: true}, + 3: {start: "#CCCCCC", end: "#CDCCCC", selection: [1, 3]}, + 4: {shift: true, start: "#CCCCCC", end: "#DCCCCC", selection: [1, 3]}, + 5: {start: "#FFFFFF", end: "#FFFFFF", selectAll: true}, + 6: {down: true, shift: true, start: "#000000", end: "#000000", + selectAll: true} + }); +} + +function* testAlphaHexIncrements(view) { + info("Testing keyboard increments with alpha hex colors"); + + let idRuleEditor = getRuleViewRuleEditor(view, 1); + let hexColorPropEditor = idRuleEditor.rule.textProps[2].editor; + + yield runIncrementTest(hexColorPropEditor, view, { + 1: {start: "#CCCCCCAA", end: "#CDCDCDAB", selectAll: true}, + 2: {shift: true, start: "#CCCCCCAA", end: "#DCDCDCBA", selectAll: true}, + 3: {start: "#CCCCCCAA", end: "#CDCCCCAA", selection: [1, 3]}, + 4: {shift: true, start: "#CCCCCCAA", end: "#DCCCCCAA", selection: [1, 3]}, + 5: {start: "#FFFFFFFF", end: "#FFFFFFFF", selectAll: true}, + 6: {down: true, shift: true, start: "#00000000", end: "#00000000", + selectAll: true} + }); +} + +function* testRgbIncrements(view) { + info("Testing keyboard increments with rgb colors"); + + let idRuleEditor = getRuleViewRuleEditor(view, 1); + let rgbColorPropEditor = idRuleEditor.rule.textProps[3].editor; + + yield runIncrementTest(rgbColorPropEditor, view, { + 1: {start: "rgb(0,0,0)", end: "rgb(0,1,0)", selection: [6, 7]}, + 2: {shift: true, start: "rgb(0,0,0)", end: "rgb(0,10,0)", + selection: [6, 7]}, + 3: {start: "rgb(0,255,0)", end: "rgb(0,255,0)", selection: [6, 9]}, + 4: {shift: true, start: "rgb(0,250,0)", end: "rgb(0,255,0)", + selection: [6, 9]}, + 5: {down: true, start: "rgb(0,0,0)", end: "rgb(0,0,0)", selection: [6, 7]}, + 6: {down: true, shift: true, start: "rgb(0,5,0)", end: "rgb(0,0,0)", + selection: [6, 7]} + }); +} + +function* testShorthandIncrements(view) { + info("Testing keyboard increments within shorthand values"); + + let idRuleEditor = getRuleViewRuleEditor(view, 1); + let paddingPropEditor = idRuleEditor.rule.textProps[1].editor; + + yield runIncrementTest(paddingPropEditor, view, { + 1: {start: "0px 0px 0px 0px", end: "0px 1px 0px 0px", selection: [4, 7]}, + 2: {shift: true, start: "0px 0px 0px 0px", end: "0px 10px 0px 0px", + selection: [4, 7]}, + 3: {start: "0px 0px 0px 0px", end: "1px 0px 0px 0px", selectAll: true}, + 4: {shift: true, start: "0px 0px 0px 0px", end: "10px 0px 0px 0px", + selectAll: true}, + 5: {down: true, start: "0px 0px 0px 0px", end: "0px 0px -1px 0px", + selection: [8, 11]}, + 6: {down: true, shift: true, start: "0px 0px 0px 0px", + end: "-10px 0px 0px 0px", selectAll: true}, + 7: {up: true, start: "0.1em .1em 0em 0em", end: "0.1em 1.1em 0em 0em", + selection: [6, 9]}, + 8: {up: true, alt: true, start: "0.1em .9em 0em 0em", + end: "0.1em 1em 0em 0em", selection: [6, 9]}, + 9: {up: true, shift: true, start: "0.2em .2em 0em 0em", + end: "0.2em 10.2em 0em 0em", selection: [6, 9]} + }); +} + +function* testOddCases(view) { + info("Testing some more odd cases"); + + let idRuleEditor = getRuleViewRuleEditor(view, 1); + let marginPropEditor = idRuleEditor.rule.textProps[0].editor; + + yield runIncrementTest(marginPropEditor, view, { + 1: {start: "98.7%", end: "99.7%", selection: [3, 3]}, + 2: {alt: true, start: "98.7%", end: "98.8%", selection: [3, 3]}, + 3: {start: "0", end: "1px"}, + 4: {down: true, start: "0", end: "-1px"}, + 5: {start: "'a=-1'", end: "'a=0'", selection: [4, 4]}, + 6: {start: "0 -1px", end: "0 0px", selection: [2, 2]}, + 7: {start: "url(-1)", end: "url(-1)", selection: [4, 4]}, + 8: {start: "url('test1.1.png')", end: "url('test1.2.png')", + selection: [11, 11]}, + 9: {start: "url('test1.png')", end: "url('test2.png')", selection: [9, 9]}, + 10: {shift: true, start: "url('test1.1.png')", end: "url('test11.1.png')", + selection: [9, 9]}, + 11: {down: true, start: "url('test-1.png')", end: "url('test-2.png')", + selection: [9, 11]}, + 12: {start: "url('test1.1.png')", end: "url('test1.2.png')", + selection: [11, 12]}, + 13: {down: true, alt: true, start: "url('test-0.png')", + end: "url('test--0.1.png')", selection: [10, 11]}, + 14: {alt: true, start: "url('test--0.1.png')", end: "url('test-0.png')", + selection: [10, 14]} + }); +} + +function* testZeroValueIncrements(view) { + info("Testing a valid unit is added when incrementing from 0"); + + let idRuleEditor = getRuleViewRuleEditor(view, 1); + let backgroundPropEditor = idRuleEditor.rule.textProps[4].editor; + yield runIncrementTest(backgroundPropEditor, view, { + 1: { start: "url(test-0.png) no-repeat 0 0", + end: "url(test-0.png) no-repeat 1px 0", selection: [26, 26] }, + 2: { start: "url(test-0.png) no-repeat 0 0", + end: "url(test-0.png) no-repeat 0 1px", selection: [28, 28] }, + 3: { start: "url(test-0.png) no-repeat center/0", + end: "url(test-0.png) no-repeat center/1px", selection: [34, 34] }, + 4: { start: "url(test-0.png) no-repeat 0 0", + end: "url(test-1.png) no-repeat 0 0", selection: [10, 10] }, + 5: { start: "linear-gradient(0, red 0, blue 0)", + end: "linear-gradient(1deg, red 0, blue 0)", selection: [17, 17] }, + 6: { start: "linear-gradient(1deg, red 0, blue 0)", + end: "linear-gradient(1deg, red 1px, blue 0)", selection: [27, 27] }, + 7: { start: "linear-gradient(1deg, red 0, blue 0)", + end: "linear-gradient(1deg, red 0, blue 1px)", selection: [35, 35] }, + }); + + let transitionPropEditor = idRuleEditor.rule.textProps[5].editor; + yield runIncrementTest(transitionPropEditor, view, { + 1: { start: "all 0 ease-out", end: "all 1s ease-out", selection: [5, 5] }, + 2: { start: "margin 4s, color 0", + end: "margin 4s, color 1s", selection: [18, 18] }, + }); + + let zIndexPropEditor = idRuleEditor.rule.textProps[6].editor; + yield runIncrementTest(zIndexPropEditor, view, { + 1: {start: "0", end: "1", selection: [1, 1]}, + }); +} + +function* runIncrementTest(propertyEditor, view, tests) { + let editor = yield focusEditableField(view, propertyEditor.valueSpan); + + for (let test in tests) { + yield testIncrement(editor, tests[test], view, propertyEditor); + } + + // Blur the field to put back the UI in its initial state (and avoid pending + // requests when the test ends). + let onRuleViewChanged = view.once("ruleview-changed"); + EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow); + view.throttle.flush(); + yield onRuleViewChanged; +} + +function* testIncrement(editor, options, view) { + editor.input.value = options.start; + let input = editor.input; + + if (options.selectAll) { + input.select(); + } else if (options.selection) { + input.setSelectionRange(options.selection[0], options.selection[1]); + } + + is(input.value, options.start, "Value initialized at " + options.start); + + let onRuleViewChanged = view.once("ruleview-changed"); + let onKeyUp = once(input, "keyup"); + + let key; + key = options.down ? "VK_DOWN" : "VK_UP"; + if (options.pageDown) { + key = "VK_PAGE_DOWN"; + } else if (options.pageUp) { + key = "VK_PAGE_UP"; + } + + EventUtils.synthesizeKey(key, {altKey: options.alt, shiftKey: options.shift}, + view.styleWindow); + + yield onKeyUp; + + // Only expect a change if the value actually changed! + if (options.start !== options.end) { + view.throttle.flush(); + yield onRuleViewChanged; + } + + is(input.value, options.end, "Value changed to " + options.end); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property-order.js b/devtools/client/inspector/rules/test/browser_rules_edit-property-order.js new file mode 100644 index 000000000..b4a86c194 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-order.js @@ -0,0 +1,89 @@ +/* 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 properties orders and overrides in the rule-view. + +const TEST_URI = "<style>#testid {}</style><div id='testid'>Styled Node</div>"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + + let elementStyle = view._elementStyle; + let elementRule = elementStyle.rules[1]; + + info("Checking rules insertion order and checking the applied style"); + let firstProp = yield addProperty(view, 1, "background-color", "green"); + let secondProp = yield addProperty(view, 1, "background-color", "blue"); + + is(elementRule.textProps[0], firstProp, + "Rules should be in addition order."); + is(elementRule.textProps[1], secondProp, + "Rules should be in addition order."); + + // rgb(0, 0, 255) = blue + is((yield getValue("#testid", "background-color")), "rgb(0, 0, 255)", + "Second property should have been used."); + + info("Removing the second property and checking the applied style again"); + yield removeProperty(view, secondProp); + // rgb(0, 128, 0) = green + is((yield getValue("#testid", "background-color")), "rgb(0, 128, 0)", + "After deleting second property, first should be used."); + + info("Creating a new second property and checking that the insertion order " + + "is still the same"); + + secondProp = yield addProperty(view, 1, "background-color", "blue"); + + is((yield getValue("#testid", "background-color")), "rgb(0, 0, 255)", + "New property should be used."); + is(elementRule.textProps[0], firstProp, + "Rules shouldn't have switched places."); + is(elementRule.textProps[1], secondProp, + "Rules shouldn't have switched places."); + + info("Disabling the second property and checking the applied style"); + yield togglePropStatus(view, secondProp); + + is((yield getValue("#testid", "background-color")), "rgb(0, 128, 0)", + "After disabling second property, first value should be used"); + + info("Disabling the first property too and checking the applied style"); + yield togglePropStatus(view, firstProp); + + is((yield getValue("#testid", "background-color")), "transparent", + "After disabling both properties, value should be empty."); + + info("Re-enabling the second propertyt and checking the applied style"); + yield togglePropStatus(view, secondProp); + + is((yield getValue("#testid", "background-color")), "rgb(0, 0, 255)", + "Value should be set correctly after re-enabling"); + + info("Re-enabling the first property and checking the insertion order " + + "is still respected"); + yield togglePropStatus(view, firstProp); + + is((yield getValue("#testid", "background-color")), "rgb(0, 0, 255)", + "Re-enabling an earlier property shouldn't make it override " + + "a later property."); + is(elementRule.textProps[0], firstProp, + "Rules shouldn't have switched places."); + is(elementRule.textProps[1], secondProp, + "Rules shouldn't have switched places."); + info("Modifying the first property and checking the applied style"); + yield setProperty(view, firstProp, "purple"); + + is((yield getValue("#testid", "background-color")), "rgb(0, 0, 255)", + "Modifying an earlier property shouldn't override a later property."); +}); + +function* getValue(selector, propName) { + let value = yield getComputedStyleProperty(selector, null, propName); + return value; +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property-remove_01.js b/devtools/client/inspector/rules/test/browser_rules_edit-property-remove_01.js new file mode 100644 index 000000000..0aed2f5c8 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-remove_01.js @@ -0,0 +1,67 @@ +/* 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"; + +// Tests removing a property by clearing the property name and pressing the +// return key, and checks if the focus is moved to the appropriate editable +// field. + +const TEST_URI = ` + <style type='text/css'> + #testid { + background-color: #00F; + color: #00F; + } + </style> + <div id='testid'>Styled Node</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + + info("Getting the first property in the #testid rule"); + let rule = getRuleViewRuleEditor(view, 1).rule; + let prop = rule.textProps[0]; + + info("Deleting the name of that property to remove the property"); + yield removeProperty(view, prop, false); + + let newValue = yield executeInContent("Test:GetRulePropertyValue", { + styleSheetIndex: 0, + ruleIndex: 0, + name: "background-color" + }); + is(newValue, "", "background-color should have been unset."); + + info("Getting the new first property in the rule"); + prop = rule.textProps[0]; + + let editor = inplaceEditor(view.styleDocument.activeElement); + is(inplaceEditor(prop.editor.nameSpan), editor, + "Focus should have moved to the next property name"); + + info("Deleting the name of that property to remove the property"); + view.styleDocument.activeElement.blur(); + yield removeProperty(view, prop, false); + + newValue = yield executeInContent("Test:GetRulePropertyValue", { + styleSheetIndex: 0, + ruleIndex: 0, + name: "color" + }); + is(newValue, "", "color should have been unset."); + + editor = inplaceEditor(view.styleDocument.activeElement); + is(inplaceEditor(rule.editor.newPropSpan), editor, + "Focus should have moved to the new property span"); + is(rule.textProps.length, 0, + "All properties should have been removed."); + is(rule.editor.propertyList.children.length, 1, + "Should have the new property span."); + + view.styleDocument.activeElement.blur(); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property-remove_02.js b/devtools/client/inspector/rules/test/browser_rules_edit-property-remove_02.js new file mode 100644 index 000000000..5690e7c2d --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-remove_02.js @@ -0,0 +1,67 @@ +/* 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"; + +// Tests removing a property by clearing the property value and pressing the +// return key, and checks if the focus is moved to the appropriate editable +// field. + +const TEST_URI = ` + <style type='text/css'> + #testid { + background-color: #00F; + color: #00F; + } + </style> + <div id='testid'>Styled Node</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + + info("Getting the first property in the rule"); + let rule = getRuleViewRuleEditor(view, 1).rule; + let prop = rule.textProps[0]; + + info("Clearing the property value"); + yield setProperty(view, prop, null, false); + + let newValue = yield executeInContent("Test:GetRulePropertyValue", { + styleSheetIndex: 0, + ruleIndex: 0, + name: "background-color" + }); + is(newValue, "", "background-color should have been unset."); + + info("Getting the new first property in the rule"); + prop = rule.textProps[0]; + + let editor = inplaceEditor(view.styleDocument.activeElement); + is(inplaceEditor(prop.editor.nameSpan), editor, + "Focus should have moved to the next property name"); + view.styleDocument.activeElement.blur(); + + info("Clearing the property value"); + yield setProperty(view, prop, null, false); + + newValue = yield executeInContent("Test:GetRulePropertyValue", { + styleSheetIndex: 0, + ruleIndex: 0, + name: "color" + }); + is(newValue, "", "color should have been unset."); + + editor = inplaceEditor(view.styleDocument.activeElement); + is(inplaceEditor(rule.editor.newPropSpan), editor, + "Focus should have moved to the new property span"); + is(rule.textProps.length, 0, + "All properties should have been removed."); + is(rule.editor.propertyList.children.length, 1, + "Should have the new property span."); + + view.styleDocument.activeElement.blur(); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property-remove_03.js b/devtools/client/inspector/rules/test/browser_rules_edit-property-remove_03.js new file mode 100644 index 000000000..21a1063c2 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-property-remove_03.js @@ -0,0 +1,83 @@ +/* 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"; + +// Tests removing a property by clearing the property name and pressing shift +// and tab keys, and checks if the focus is moved to the appropriate editable +// field. + +const TEST_URI = ` + <style type='text/css'> + #testid { + background-color: #00F; + color: #00F; + } + </style> + <div id='testid'>Styled Node</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + + info("Getting the second property in the rule"); + let rule = getRuleViewRuleEditor(view, 1).rule; + let prop = rule.textProps[1]; + + info("Clearing the property value and pressing shift-tab"); + let editor = yield focusEditableField(view, prop.editor.valueSpan); + let onValueDone = view.once("ruleview-changed"); + editor.input.value = ""; + EventUtils.synthesizeKey("VK_TAB", {shiftKey: true}, view.styleWindow); + yield onValueDone; + + let newValue = yield executeInContent("Test:GetRulePropertyValue", { + styleSheetIndex: 0, + ruleIndex: 0, + name: "color" + }); + is(newValue, "", "color should have been unset."); + is(prop.editor.valueSpan.textContent, "", + "'' property value is correctly set."); + + info("Pressing shift-tab again to focus the previous property value"); + let onValueFocused = view.once("ruleview-changed"); + EventUtils.synthesizeKey("VK_TAB", {shiftKey: true}, view.styleWindow); + yield onValueFocused; + + info("Getting the first property in the rule"); + prop = rule.textProps[0]; + + editor = inplaceEditor(view.styleDocument.activeElement); + is(inplaceEditor(prop.editor.valueSpan), editor, + "Focus should have moved to the previous property value"); + + info("Pressing shift-tab again to focus the property name"); + let onNameFocused = view.once("ruleview-changed"); + EventUtils.synthesizeKey("VK_TAB", {shiftKey: true}, view.styleWindow); + yield onNameFocused; + + info("Removing the name and pressing shift-tab to focus the selector"); + let onNameDeleted = view.once("ruleview-changed"); + EventUtils.synthesizeKey("VK_DELETE", {}, view.styleWindow); + EventUtils.synthesizeKey("VK_TAB", {shiftKey: true}, view.styleWindow); + yield onNameDeleted; + + newValue = yield executeInContent("Test:GetRulePropertyValue", { + styleSheetIndex: 0, + ruleIndex: 0, + name: "background-color" + }); + is(newValue, "", "background-color should have been unset."); + + editor = inplaceEditor(view.styleDocument.activeElement); + is(inplaceEditor(rule.editor.selectorText), editor, + "Focus should have moved to the selector text."); + is(rule.textProps.length, 0, + "All properties should have been removed."); + ok(!rule.editor.propertyList.hasChildNodes(), + "Should not have any properties."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property_01.js b/devtools/client/inspector/rules/test/browser_rules_edit-property_01.js new file mode 100644 index 000000000..6f4c49e20 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_01.js @@ -0,0 +1,93 @@ +/* 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"; + +// Testing adding new properties via the inplace-editors in the rule +// view. +// FIXME: some of the inplace-editor focus/blur/commit/revert stuff +// should be factored out in head.js + +const TEST_URI = ` + <style type="text/css"> + #testid { + color: red; + background-color: blue; + } + .testclass, .unmatched { + background-color: green; + } + </style> + <div id="testid" class="testclass">Styled Node</div> + <div id="testid2">Styled Node</div> +`; + +var BACKGROUND_IMAGE_URL = 'url("' + URL_ROOT + 'doc_test_image.png")'; + +var TEST_DATA = [ + { name: "border-color", value: "red", isValid: true }, + { name: "background-image", value: BACKGROUND_IMAGE_URL, isValid: true }, + { name: "border", value: "solid 1px foo", isValid: false }, +]; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + + let rule = getRuleViewRuleEditor(view, 1).rule; + for (let {name, value, isValid} of TEST_DATA) { + yield testEditProperty(view, rule, name, value, isValid); + } +}); + +function* testEditProperty(view, rule, name, value, isValid) { + info("Test editing existing property name/value fields"); + + let doc = rule.editor.doc; + let prop = rule.textProps[0]; + + info("Focusing an existing property name in the rule-view"); + let editor = yield focusEditableField(view, prop.editor.nameSpan, 32, 1); + + is(inplaceEditor(prop.editor.nameSpan), editor, + "The property name editor got focused"); + let input = editor.input; + + info("Entering a new property name, including : to commit and " + + "focus the value"); + let onValueFocus = once(rule.editor.element, "focus", true); + let onNameDone = view.once("ruleview-changed"); + EventUtils.sendString(name + ":", doc.defaultView); + yield onValueFocus; + yield onNameDone; + + // Getting the value editor after focus + editor = inplaceEditor(doc.activeElement); + input = editor.input; + is(inplaceEditor(prop.editor.valueSpan), editor, "Focus moved to the value."); + + info("Entering a new value, including ; to commit and blur the value"); + let onValueDone = view.once("ruleview-changed"); + let onBlur = once(input, "blur"); + EventUtils.sendString(value + ";", doc.defaultView); + yield onBlur; + yield onValueDone; + + is(prop.editor.isValid(), isValid, + value + " is " + isValid ? "valid" : "invalid"); + + info("Checking that the style property was changed on the content page"); + let propValue = yield executeInContent("Test:GetRulePropertyValue", { + styleSheetIndex: 0, + ruleIndex: 0, + name + }); + + if (isValid) { + is(propValue, value, name + " should have been set."); + } else { + isnot(propValue, value, name + " shouldn't have been set."); + } +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property_02.js b/devtools/client/inspector/rules/test/browser_rules_edit-property_02.js new file mode 100644 index 000000000..7e6315236 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_02.js @@ -0,0 +1,133 @@ +/* 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 several types of rule-view property edition + +const TEST_URI = ` + <style type="text/css"> + #testid { + background-color: blue; + } + .testclass, .unmatched { + background-color: green; + } + </style> + <div id="testid" class="testclass">Styled Node</div> + <div id="testid2">Styled Node</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + + yield testEditProperty(inspector, view); + yield testDisableProperty(inspector, view); + yield testPropertyStillMarkedDirty(inspector, view); +}); + +function* testEditProperty(inspector, ruleView) { + let idRule = getRuleViewRuleEditor(ruleView, 1).rule; + let prop = idRule.textProps[0]; + + let editor = yield focusEditableField(ruleView, prop.editor.nameSpan); + let input = editor.input; + is(inplaceEditor(prop.editor.nameSpan), editor, + "Next focused editor should be the name editor."); + + ok(input.selectionStart === 0 && input.selectionEnd === input.value.length, + "Editor contents are selected."); + + // Try clicking on the editor's input again, shouldn't cause trouble + // (see bug 761665). + EventUtils.synthesizeMouse(input, 1, 1, {}, ruleView.styleWindow); + input.select(); + + info("Entering property name \"border-color\" followed by a colon to " + + "focus the value"); + let onNameDone = ruleView.once("ruleview-changed"); + let onFocus = once(idRule.editor.element, "focus", true); + EventUtils.sendString("border-color:", ruleView.styleWindow); + yield onFocus; + yield onNameDone; + + info("Verifying that the focused field is the valueSpan"); + editor = inplaceEditor(ruleView.styleDocument.activeElement); + input = editor.input; + is(inplaceEditor(prop.editor.valueSpan), editor, + "Focus should have moved to the value."); + ok(input.selectionStart === 0 && input.selectionEnd === input.value.length, + "Editor contents are selected."); + + info("Entering a value following by a semi-colon to commit it"); + let onBlur = once(editor.input, "blur"); + // Use sendChar() to pass each character as a string so that we can test + // prop.editor.warning.hidden after each character. + for (let ch of "red;") { + let onPreviewDone = ruleView.once("ruleview-changed"); + EventUtils.sendChar(ch, ruleView.styleWindow); + ruleView.throttle.flush(); + yield onPreviewDone; + is(prop.editor.warning.hidden, true, + "warning triangle is hidden or shown as appropriate"); + } + yield onBlur; + + let newValue = yield executeInContent("Test:GetRulePropertyValue", { + styleSheetIndex: 0, + ruleIndex: 0, + name: "border-color" + }); + is(newValue, "red", "border-color should have been set."); + + ruleView.styleDocument.activeElement.blur(); + yield addProperty(ruleView, 1, "color", "red", ";"); + + let props = ruleView.element.querySelectorAll(".ruleview-property"); + for (let i = 0; i < props.length; i++) { + is(props[i].hasAttribute("dirty"), i <= 1, + "props[" + i + "] marked dirty as appropriate"); + } +} + +function* testDisableProperty(inspector, ruleView) { + let idRule = getRuleViewRuleEditor(ruleView, 1).rule; + let prop = idRule.textProps[0]; + + info("Disabling a property"); + yield togglePropStatus(ruleView, prop); + + let newValue = yield executeInContent("Test:GetRulePropertyValue", { + styleSheetIndex: 0, + ruleIndex: 0, + name: "border-color" + }); + is(newValue, "", "Border-color should have been unset."); + + info("Enabling the property again"); + yield togglePropStatus(ruleView, prop); + + newValue = yield executeInContent("Test:GetRulePropertyValue", { + styleSheetIndex: 0, + ruleIndex: 0, + name: "border-color" + }); + is(newValue, "red", "Border-color should have been reset."); +} + +function* testPropertyStillMarkedDirty(inspector, ruleView) { + // Select an unstyled node. + yield selectNode("#testid2", inspector); + + // Select the original node again. + yield selectNode("#testid", inspector); + + let props = ruleView.element.querySelectorAll(".ruleview-property"); + for (let i = 0; i < props.length; i++) { + is(props[i].hasAttribute("dirty"), i <= 1, + "props[" + i + "] marked dirty as appropriate"); + } +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property_03.js b/devtools/client/inspector/rules/test/browser_rules_edit-property_03.js new file mode 100644 index 000000000..a5771b41e --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_03.js @@ -0,0 +1,50 @@ +/* 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"; + +// Tests that emptying out an existing value removes the property and +// doesn't cause any other issues. See also Bug 1150780. + +const TEST_URI = ` + <style type="text/css"> + #testid { + color: red; + background-color: blue; + font-size: 12px; + } + .testclass, .unmatched { + background-color: green; + } + </style> + <div id="testid" class="testclass">Styled Node</div> + <div id="testid2">Styled Node</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + + let ruleEditor = getRuleViewRuleEditor(view, 1); + let propEditor = ruleEditor.rule.textProps[1].editor; + + yield focusEditableField(view, propEditor.valueSpan); + + info("Deleting all the text out of a value field"); + let onRuleViewChanged = view.once("ruleview-changed"); + yield sendKeysAndWaitForFocus(view, ruleEditor.element, + ["DELETE", "RETURN"]); + yield onRuleViewChanged; + + info("Pressing enter a couple times to cycle through editors"); + yield sendKeysAndWaitForFocus(view, ruleEditor.element, ["RETURN"]); + onRuleViewChanged = view.once("ruleview-changed"); + yield sendKeysAndWaitForFocus(view, ruleEditor.element, ["RETURN"]); + yield onRuleViewChanged; + + isnot(ruleEditor.rule.textProps[1].editor.nameSpan.style.display, "none", + "The name span is visible"); + is(ruleEditor.rule.textProps.length, 2, "Correct number of props"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property_04.js b/devtools/client/inspector/rules/test/browser_rules_edit-property_04.js new file mode 100644 index 000000000..7460db4cd --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_04.js @@ -0,0 +1,85 @@ +/* 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"; + +// Tests that a disabled property remains disabled when the escaping out of +// the property editor. + +const TEST_URI = ` + <style type='text/css'> + #testid { + background-color: blue; + } + </style> + <div id='testid'>Styled Node</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + + let rule = getRuleViewRuleEditor(view, 1).rule; + let prop = rule.textProps[0]; + + info("Disabling a property"); + yield togglePropStatus(view, prop); + + let newValue = yield executeInContent("Test:GetRulePropertyValue", { + styleSheetIndex: 0, + ruleIndex: 0, + name: "background-color" + }); + is(newValue, "", "background-color should have been unset."); + + yield testEditDisableProperty(view, rule, prop, "name", "VK_ESCAPE"); + yield testEditDisableProperty(view, rule, prop, "value", "VK_ESCAPE"); + yield testEditDisableProperty(view, rule, prop, "value", "VK_TAB"); + yield testEditDisableProperty(view, rule, prop, "value", "VK_RETURN"); +}); + +function* testEditDisableProperty(view, rule, prop, fieldType, commitKey) { + let field = fieldType === "name" ? prop.editor.nameSpan + : prop.editor.valueSpan; + + let editor = yield focusEditableField(view, field); + + ok(!prop.editor.element.classList.contains("ruleview-overridden"), + "property is not overridden."); + is(prop.editor.enable.style.visibility, "hidden", + "property enable checkbox is hidden."); + + let newValue = yield executeInContent("Test:GetRulePropertyValue", { + styleSheetIndex: 0, + ruleIndex: 0, + name: "background-color" + }); + is(newValue, "", "background-color should remain unset."); + + let onChangeDone; + if (fieldType === "value") { + onChangeDone = view.once("ruleview-changed"); + } + + let onBlur = once(editor.input, "blur"); + EventUtils.synthesizeKey(commitKey, {}, view.styleWindow); + yield onBlur; + yield onChangeDone; + + ok(!prop.enabled, "property is disabled."); + ok(prop.editor.element.classList.contains("ruleview-overridden"), + "property is overridden."); + is(prop.editor.enable.style.visibility, "visible", + "property enable checkbox is visible."); + ok(!prop.editor.enable.getAttribute("checked"), + "property enable checkbox is not checked."); + + newValue = yield executeInContent("Test:GetRulePropertyValue", { + styleSheetIndex: 0, + ruleIndex: 0, + name: "background-color" + }); + is(newValue, "", "background-color should remain unset."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property_05.js b/devtools/client/inspector/rules/test/browser_rules_edit-property_05.js new file mode 100644 index 000000000..3d37c81d5 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_05.js @@ -0,0 +1,77 @@ +/* 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"; + +// Tests that a disabled property is re-enabled if the property name or value is +// modified + +const TEST_URI = ` + <style type='text/css'> + #testid { + background-color: blue; + } + </style> + <div id='testid'>Styled Node</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + + let rule = getRuleViewRuleEditor(view, 1).rule; + let prop = rule.textProps[0]; + + info("Disabling background-color property"); + yield togglePropStatus(view, prop); + + let newValue = yield getRulePropertyValue("background-color"); + is(newValue, "", "background-color should have been unset."); + + info("Entering a new property name, including : to commit and " + + "focus the value"); + + yield focusEditableField(view, prop.editor.nameSpan); + let onNameDone = view.once("ruleview-changed"); + EventUtils.sendString("border-color:", view.styleWindow); + yield onNameDone; + + info("Escape editing the property value"); + let onValueDone = view.once("ruleview-changed"); + EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow); + yield onValueDone; + + newValue = yield getRulePropertyValue("border-color"); + is(newValue, "blue", "border-color should have been set."); + + ok(prop.enabled, "border-color property is enabled."); + ok(!prop.editor.element.classList.contains("ruleview-overridden"), + "border-color is not overridden"); + + info("Disabling border-color property"); + yield togglePropStatus(view, prop); + + newValue = yield getRulePropertyValue("border-color"); + is(newValue, "", "border-color should have been unset."); + + info("Enter a new property value for the border-color property"); + yield setProperty(view, prop, "red"); + + newValue = yield getRulePropertyValue("border-color"); + is(newValue, "red", "new border-color should have been set."); + + ok(prop.enabled, "border-color property is enabled."); + ok(!prop.editor.element.classList.contains("ruleview-overridden"), + "border-color is not overridden"); +}); + +function* getRulePropertyValue(name) { + let propValue = yield executeInContent("Test:GetRulePropertyValue", { + styleSheetIndex: 0, + ruleIndex: 0, + name: name + }); + return propValue; +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property_06.js b/devtools/client/inspector/rules/test/browser_rules_edit-property_06.js new file mode 100644 index 000000000..95211f1d0 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_06.js @@ -0,0 +1,52 @@ +/* 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"; + +// Tests that editing a property's priority is behaving correctly, and disabling +// and editing the property will re-enable the property. + +const TEST_URI = ` + <style type='text/css'> + body { + background-color: green !important; + } + body { + background-color: red; + } + </style> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("body", inspector); + + let rule = getRuleViewRuleEditor(view, 1).rule; + let prop = rule.textProps[0]; + + is((yield getComputedStyleProperty("body", null, "background-color")), + "rgb(0, 128, 0)", "green background color is set."); + + yield setProperty(view, prop, "red !important"); + + is(prop.editor.valueSpan.textContent, "red !important", + "'red !important' property value is correctly set."); + is((yield getComputedStyleProperty("body", null, "background-color")), + "rgb(255, 0, 0)", "red background color is set."); + + info("Disabling red background color property"); + yield togglePropStatus(view, prop); + + is((yield getComputedStyleProperty("body", null, "background-color")), + "rgb(0, 128, 0)", "green background color is set."); + + yield setProperty(view, prop, "red"); + + is(prop.editor.valueSpan.textContent, "red", + "'red' property value is correctly set."); + ok(prop.enabled, "red background-color property is enabled."); + is((yield getComputedStyleProperty("body", null, "background-color")), + "rgb(0, 128, 0)", "green background color is set."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property_07.js b/devtools/client/inspector/rules/test/browser_rules_edit-property_07.js new file mode 100644 index 000000000..40314819f --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_07.js @@ -0,0 +1,50 @@ +/* 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"; + +// Tests that adding multiple values will enable the property even if the +// property does not change, and that the extra values are added correctly. + +const TEST_URI = ` + <style type='text/css'> + #testid { + background-color: #f00; + } + </style> + <div id='testid'>Styled Node</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + + let rule = getRuleViewRuleEditor(view, 1).rule; + let prop = rule.textProps[0]; + + info("Disabling red background color property"); + yield togglePropStatus(view, prop); + ok(!prop.enabled, "red background-color property is disabled."); + + let editor = yield focusEditableField(view, prop.editor.valueSpan); + let onDone = view.once("ruleview-changed"); + editor.input.value = "red; color: red;"; + EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow); + yield onDone; + + is(prop.editor.valueSpan.textContent, "red", + "'red' property value is correctly set."); + ok(prop.enabled, "red background-color property is enabled."); + is((yield getComputedStyleProperty("#testid", null, "background-color")), + "rgb(255, 0, 0)", "red background color is set."); + + let propEditor = rule.textProps[1].editor; + is(propEditor.nameSpan.textContent, "color", + "new 'color' property name is correctly set."); + is(propEditor.valueSpan.textContent, "red", + "new 'red' property value is correctly set."); + is((yield getComputedStyleProperty("#testid", null, "color")), + "rgb(255, 0, 0)", "red color is set."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property_08.js b/devtools/client/inspector/rules/test/browser_rules_edit-property_08.js new file mode 100644 index 000000000..1becd40d9 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_08.js @@ -0,0 +1,57 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that renaming a property works. + +const TEST_URI = ` + <style type="text/css"> + #testid { + color: #FFF; + } + </style> + <div style='color: red' id='testid'>Styled Node</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + + info("Get the color property editor"); + let ruleEditor = getRuleViewRuleEditor(view, 0); + let propEditor = ruleEditor.rule.textProps[0].editor; + is(ruleEditor.rule.textProps[0].name, "color"); + + info("Focus the property name field"); + yield focusEditableField(ruleEditor.ruleView, propEditor.nameSpan, 32, 1); + + info("Rename the property to background-color"); + // Expect 3 events: the value editor being focused, the ruleview-changed event + // which signals that the new value has been previewed (fires once when the + // value gets focused), and the markupmutation event since we're modifying an + // inline style. + let onValueFocus = once(ruleEditor.element, "focus", true); + let onRuleViewChanged = ruleEditor.ruleView.once("ruleview-changed"); + let onMutation = inspector.once("markupmutation"); + EventUtils.sendString("background-color:", ruleEditor.doc.defaultView); + yield onValueFocus; + yield onRuleViewChanged; + yield onMutation; + + is(ruleEditor.rule.textProps[0].name, "background-color"); + yield waitForComputedStyleProperty("#testid", null, "background-color", + "rgb(255, 0, 0)"); + + is((yield getComputedStyleProperty("#testid", null, "color")), + "rgb(255, 255, 255)", "color is white"); + + // The value field is still focused. Blur it now and wait for the + // ruleview-changed event to avoid pending requests. + onRuleViewChanged = view.once("ruleview-changed"); + EventUtils.synthesizeKey("VK_ESCAPE", {}); + yield onRuleViewChanged; +}); + diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-property_09.js b/devtools/client/inspector/rules/test/browser_rules_edit-property_09.js new file mode 100644 index 000000000..51f714021 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-property_09.js @@ -0,0 +1,69 @@ +/* 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 a newProperty editor is only created if no other editor was +// previously displayed. + +const TEST_URI = ` + <style type='text/css'> + #testid { + background-color: blue; + } + </style> + <div id='testid'>Styled Node</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + yield testClickOnEmptyAreaToCloseEditor(inspector, view); +}); + +function synthesizeMouseOnEmptyArea(ruleEditor, view) { + // any text property editor will do + let propEditor = ruleEditor.rule.textProps[0].editor; + let valueContainer = propEditor.valueContainer; + let valueRect = valueContainer.getBoundingClientRect(); + // click right next to the ";" at the end of valueContainer + EventUtils.synthesizeMouse(valueContainer, valueRect.width + 1, 1, {}, + view.styleWindow); +} + +function* testClickOnEmptyAreaToCloseEditor(inspector, view) { + // Start at the beginning: start to add a rule to the element's style + // declaration, add some text, then press escape. + let ruleEditor = getRuleViewRuleEditor(view, 1); + let propEditor = ruleEditor.rule.textProps[0].editor; + + info("Create a property value editor"); + let editor = yield focusEditableField(view, propEditor.valueSpan); + ok(editor.input, "The inplace-editor field is ready"); + + info("Close the property value editor by clicking on an empty area " + + "in the rule editor"); + let onRuleViewChanged = view.once("ruleview-changed"); + let onBlur = once(editor.input, "blur"); + synthesizeMouseOnEmptyArea(ruleEditor, view); + yield onBlur; + yield onRuleViewChanged; + ok(!view.isEditing, "No inplace editor should be displayed in the ruleview"); + + info("Create new newProperty editor by clicking again on the empty area"); + let onFocus = once(ruleEditor.element, "focus", true); + synthesizeMouseOnEmptyArea(ruleEditor, view); + yield onFocus; + editor = inplaceEditor(ruleEditor.element.ownerDocument.activeElement); + is(inplaceEditor(ruleEditor.newPropSpan), editor, + "New property editor was created"); + + info("Close the newProperty editor by clicking again on the empty area"); + onBlur = once(editor.input, "blur"); + synthesizeMouseOnEmptyArea(ruleEditor, view); + yield onBlur; + + ok(!view.isEditing, "No inplace editor should be displayed in the ruleview"); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector-click-on-scrollbar.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector-click-on-scrollbar.js new file mode 100644 index 000000000..1846df60d --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector-click-on-scrollbar.js @@ -0,0 +1,88 @@ +/* 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"; + +// Testing ruleview inplace-editor is not blurred when clicking on the ruleview +// container scrollbar. + +const TEST_URI = ` + <style type="text/css"> + div.testclass { + color: black; + } + .a { + color: #aaa; + } + .b { + color: #bbb; + } + .c { + color: #ccc; + } + .d { + color: #ddd; + } + .e { + color: #eee; + } + .f { + color: #fff; + } + </style> + <div class="testclass a b c d e f">Styled Node</div> +`; + +add_task(function* () { + info("Toolbox height should be small enough to force scrollbars to appear"); + yield new Promise(done => { + let options = {"set": [ + ["devtools.toolbox.footer.height", 200], + ]}; + SpecialPowers.pushPrefEnv(options, done); + }); + + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode(".testclass", inspector); + + info("Check we have an overflow on the ruleview container."); + let container = view.element; + let hasScrollbar = container.offsetHeight < container.scrollHeight; + ok(hasScrollbar, "The rule view container should have a vertical scrollbar."); + + info("Focusing an existing selector name in the rule-view."); + let ruleEditor = getRuleViewRuleEditor(view, 1); + let editor = yield focusEditableField(view, ruleEditor.selectorText); + is(inplaceEditor(ruleEditor.selectorText), editor, + "The selector editor is focused."); + + info("Click on the scrollbar element."); + yield clickOnRuleviewScrollbar(view); + + is(editor.input, view.styleDocument.activeElement, + "The editor input should still be focused."); + + info("Check a new value can still be committed in the editable field"); + let newValue = ".testclass.a.b.c.d.e.f"; + let onRuleViewChanged = once(view, "ruleview-changed"); + + info("Enter new value and commit."); + editor.input.value = newValue; + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewChanged; + ok(getRuleViewRule(view, newValue), "Rule with '" + newValue + " 'exists."); +}); + +function* clickOnRuleviewScrollbar(view) { + let container = view.element.parentNode; + let onScroll = once(container, "scroll"); + let rect = container.getBoundingClientRect(); + // click 5 pixels before the bottom-right corner should hit the scrollbar + EventUtils.synthesizeMouse(container, rect.width - 5, rect.height - 5, + {}, view.styleWindow); + yield onScroll; + + ok(true, "The rule view container scrolled after clicking on the scrollbar."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector-click.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector-click.js new file mode 100644 index 000000000..7a3b6d467 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector-click.js @@ -0,0 +1,63 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Testing selector inplace-editor remains available and focused after clicking +// in its input. + +const TEST_URI = ` + <style type="text/css"> + .testclass { + text-align: center; + } + </style> + <div class="testclass">Styled Node</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode(".testclass", inspector); + yield testClickOnSelectorEditorInput(view); +}); + +function* testClickOnSelectorEditorInput(view) { + info("Test clicking inside the selector editor input"); + + let ruleEditor = getRuleViewRuleEditor(view, 1); + + info("Focusing an existing selector name in the rule-view"); + let editor = yield focusEditableField(view, ruleEditor.selectorText); + let editorInput = editor.input; + is(inplaceEditor(ruleEditor.selectorText), editor, + "The selector editor got focused"); + + info("Click inside the editor input"); + let onClick = once(editorInput, "click"); + EventUtils.synthesizeMouse(editor.input, 2, 1, {}, view.styleWindow); + yield onClick; + is(editor.input, view.styleDocument.activeElement, + "The editor input should still be focused"); + ok(!ruleEditor.newPropSpan, "No newProperty editor was created"); + + info("Doubleclick inside the editor input"); + let onDoubleClick = once(editorInput, "dblclick"); + EventUtils.synthesizeMouse(editor.input, 2, 1, { clickCount: 2 }, + view.styleWindow); + yield onDoubleClick; + is(editor.input, view.styleDocument.activeElement, + "The editor input should still be focused"); + ok(!ruleEditor.newPropSpan, "No newProperty editor was created"); + + info("Click outside the editor input"); + let onBlur = once(editorInput, "blur"); + let rect = editorInput.getBoundingClientRect(); + EventUtils.synthesizeMouse(editorInput, rect.width + 5, rect.height / 2, {}, + view.styleWindow); + yield onBlur; + + isnot(editorInput, view.styleDocument.activeElement, + "The editor input should no longer be focused"); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector-commit.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector-commit.js new file mode 100644 index 000000000..f7058371f --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector-commit.js @@ -0,0 +1,117 @@ +/* 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 selector value is correctly displayed when committing the inplace editor +// with ENTER, ESC, SHIFT+TAB and TAB + +const TEST_URI = ` + <style type='text/css'> + #testid1 { + text-align: center; + } + #testid2 { + text-align: center; + } + #testid3 { + } + </style> + <div id='testid1'>Styled Node</div> + <div id='testid2'>Styled Node</div> + <div id='testid3'>Styled Node</div> +`; + +const TEST_DATA = [ + { + node: "#testid1", + value: ".testclass", + commitKey: "VK_ESCAPE", + modifiers: {}, + expected: "#testid1", + + }, + { + node: "#testid1", + value: ".testclass1", + commitKey: "VK_RETURN", + modifiers: {}, + expected: ".testclass1" + }, + { + node: "#testid2", + value: ".testclass2", + commitKey: "VK_TAB", + modifiers: {}, + expected: ".testclass2" + }, + { + node: "#testid3", + value: ".testclass3", + commitKey: "VK_TAB", + modifiers: {shiftKey: true}, + expected: ".testclass3" + } +]; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let { inspector, view } = yield openRuleView(); + + for (let data of TEST_DATA) { + yield runTestData(inspector, view, data); + } +}); + +function* runTestData(inspector, view, data) { + let {node, value, commitKey, modifiers, expected} = data; + + info("Updating " + node + " to " + value + " and committing with " + + commitKey + ". Expecting: " + expected); + + info("Selecting the test element"); + yield selectNode(node, inspector); + + let idRuleEditor = getRuleViewRuleEditor(view, 1); + + info("Focusing an existing selector name in the rule-view"); + let editor = yield focusEditableField(view, idRuleEditor.selectorText); + is(inplaceEditor(idRuleEditor.selectorText), editor, + "The selector editor got focused"); + + info("Enter the new selector value: " + value); + editor.input.value = value; + + info("Entering the commit key " + commitKey + " " + modifiers); + EventUtils.synthesizeKey(commitKey, modifiers); + + let activeElement = view.styleDocument.activeElement; + + if (commitKey === "VK_ESCAPE") { + is(idRuleEditor.rule.selectorText, expected, + "Value is as expected: " + expected); + is(idRuleEditor.isEditing, false, "Selector is not being edited."); + is(idRuleEditor.selectorText, activeElement, + "Focus is on selector span."); + return; + } + + yield once(view, "ruleview-changed"); + + ok(getRuleViewRule(view, expected), + "Rule with " + expected + " selector exists."); + + if (modifiers.shiftKey) { + idRuleEditor = getRuleViewRuleEditor(view, 0); + } + + let rule = idRuleEditor.rule; + if (rule.textProps.length > 0) { + is(inplaceEditor(rule.textProps[0].editor.nameSpan).input, activeElement, + "Focus is on the first property name span."); + } else { + is(inplaceEditor(idRuleEditor.newPropSpan).input, activeElement, + "Focus is on the new property span."); + } +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector_01.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector_01.js new file mode 100644 index 000000000..af228094b --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_01.js @@ -0,0 +1,62 @@ +/* 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"; + +// Testing selector inplace-editor behaviors in the rule-view + +const TEST_URI = ` + <style type="text/css"> + .testclass { + text-align: center; + } + </style> + <div id="testid" class="testclass">Styled Node</div> + <span>This is a span</span> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + + info("Selecting the test element"); + yield selectNode("#testid", inspector); + yield testEditSelector(view, "span"); + + info("Selecting the modified element with the new rule"); + yield selectNode("span", inspector); + yield checkModifiedElement(view, "span"); +}); + +function* testEditSelector(view, name) { + info("Test editing existing selector fields"); + + let idRuleEditor = getRuleViewRuleEditor(view, 1); + + info("Focusing an existing selector name in the rule-view"); + let editor = yield focusEditableField(view, idRuleEditor.selectorText); + + is(inplaceEditor(idRuleEditor.selectorText), editor, + "The selector editor got focused"); + + info("Entering a new selector name and committing"); + editor.input.value = name; + + info("Waiting for rule view to update"); + let onRuleViewChanged = once(view, "ruleview-changed"); + + info("Entering the commit key"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewChanged; + + is(view._elementStyle.rules.length, 2, "Should have 2 rules."); + ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists."); + ok(getRuleViewRuleEditor(view, 1).element.getAttribute("unmatched"), + "Rule with " + name + " does not match the current element."); +} + +function* checkModifiedElement(view, name) { + is(view._elementStyle.rules.length, 2, "Should have 2 rules."); + ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector_02.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector_02.js new file mode 100644 index 000000000..503f91efa --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_02.js @@ -0,0 +1,88 @@ +/* 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"; + +// Testing selector inplace-editor behaviors in the rule-view with pseudo +// classes. + +const TEST_URI = ` + <style type="text/css"> + .testclass { + text-align: center; + } + #testid3::first-letter { + text-decoration: "italic" + } + </style> + <div id="testid">Styled Node</div> + <span class="testclass">This is a span</span> + <div class="testclass2">A</div> + <div id="testid3">B</div> +`; + +const PSEUDO_PREF = "devtools.inspector.show_pseudo_elements"; + +add_task(function* () { + // Expand the pseudo-elements section by default. + Services.prefs.setBoolPref(PSEUDO_PREF, true); + + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + + info("Selecting the test element"); + yield selectNode(".testclass", inspector); + yield testEditSelector(view, "div:nth-child(1)"); + + info("Selecting the modified element"); + yield selectNode("#testid", inspector); + yield checkModifiedElement(view, "div:nth-child(1)"); + + info("Selecting the test element"); + yield selectNode("#testid3", inspector); + yield testEditSelector(view, ".testclass2::first-letter"); + + info("Selecting the modified element"); + yield selectNode(".testclass2", inspector); + yield checkModifiedElement(view, ".testclass2::first-letter"); + + // Reset the pseudo-elements section pref to its default value. + Services.prefs.clearUserPref(PSEUDO_PREF); +}); + +function* testEditSelector(view, name) { + info("Test editing existing selector fields"); + + let idRuleEditor = getRuleViewRuleEditor(view, 1) || + getRuleViewRuleEditor(view, 1, 0); + + info("Focusing an existing selector name in the rule-view"); + let editor = yield focusEditableField(view, idRuleEditor.selectorText); + + is(inplaceEditor(idRuleEditor.selectorText), editor, + "The selector editor got focused"); + + info("Entering a new selector name: " + name); + editor.input.value = name; + + info("Waiting for rule view to update"); + let onRuleViewChanged = once(view, "ruleview-changed"); + + info("Entering the commit key"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewChanged; + + is(view._elementStyle.rules.length, 2, "Should have 2 rule."); + ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists."); + + let newRuleEditor = getRuleViewRuleEditor(view, 1) || + getRuleViewRuleEditor(view, 1, 0); + ok(newRuleEditor.element.getAttribute("unmatched"), + "Rule with " + name + " does not match the current element."); +} + +function* checkModifiedElement(view, name) { + is(view._elementStyle.rules.length, 2, "Should have 2 rules."); + ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector_03.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector_03.js new file mode 100644 index 000000000..c6834f6ee --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_03.js @@ -0,0 +1,48 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Testing selector inplace-editor behaviors in the rule-view with invalid +// selectors + +const TEST_URI = ` + <style type="text/css"> + .testclass { + text-align: center; + } + </style> + <div class="testclass">Styled Node</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode(".testclass", inspector); + yield testEditSelector(view, "asd@:::!"); +}); + +function* testEditSelector(view, name) { + info("Test editing existing selector fields"); + + let ruleEditor = getRuleViewRuleEditor(view, 1); + + info("Focusing an existing selector name in the rule-view"); + let editor = yield focusEditableField(view, ruleEditor.selectorText); + + is(inplaceEditor(ruleEditor.selectorText), editor, + "The selector editor got focused"); + + info("Entering a new selector name and committing"); + editor.input.value = name; + let onRuleViewChanged = once(view, "ruleview-invalid-selector"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewChanged; + + is(view._elementStyle.rules.length, 2, "Should have 2 rules."); + is(getRuleViewRule(view, name), undefined, + "Rule with " + name + " selector should not exist."); + ok(getRuleViewRule(view, ".testclass"), + "Rule with .testclass selector exists."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector_04.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector_04.js new file mode 100644 index 000000000..09b6ad841 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_04.js @@ -0,0 +1,69 @@ +/* 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"; + +// Tests that the selector highlighter is removed when modifying a selector and +// the selector highlighter works for the newly added unmatched rule. + +const TEST_URI = ` + <style type="text/css"> + p { + background: red; + } + </style> + <p>Test the selector highlighter</p> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("p", inspector); + + ok(!view.selectorHighlighter, + "No selectorhighlighter exist in the rule-view"); + + yield testSelectorHighlight(view, "p"); + yield testEditSelector(view, "body"); + yield testSelectorHighlight(view, "body"); +}); + +function* testSelectorHighlight(view, name) { + info("Test creating selector highlighter"); + + info("Clicking on a selector icon"); + let icon = getRuleViewSelectorHighlighterIcon(view, name); + + let onToggled = view.once("ruleview-selectorhighlighter-toggled"); + EventUtils.synthesizeMouseAtCenter(icon, {}, view.styleWindow); + let isVisible = yield onToggled; + + ok(view.selectorHighlighter, "The selectorhighlighter instance was created"); + ok(isVisible, "The toggle event says the highlighter is visible"); +} + +function* testEditSelector(view, name) { + info("Test editing existing selector fields"); + + let ruleEditor = getRuleViewRuleEditor(view, 1); + + info("Focusing an existing selector name in the rule-view"); + let editor = yield focusEditableField(view, ruleEditor.selectorText); + + is(inplaceEditor(ruleEditor.selectorText), editor, + "The selector editor got focused"); + + info("Waiting for rule view to update"); + let onToggled = view.once("ruleview-selectorhighlighter-toggled"); + + info("Entering a new selector name and committing"); + editor.input.value = name; + EventUtils.synthesizeKey("VK_RETURN", {}); + + let isVisible = yield onToggled; + + ok(!view.highlighters.selectorHighlighterShown, + "The selectorHighlighterShown instance was removed"); + ok(!isVisible, "The toggle event says the highlighter is not visible"); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector_05.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector_05.js new file mode 100644 index 000000000..cd996b4b0 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_05.js @@ -0,0 +1,78 @@ +/* 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"; + +// Tests that adding a new property of an unmatched rule works properly. + +const TEST_URI = ` + <style type="text/css"> + #testid { + } + .testclass { + background-color: white; + } + </style> + <div id="testid">Styled Node</div> + <span class="testclass">This is a span</span> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + + info("Selecting the test element"); + yield selectNode("#testid", inspector); + yield testEditSelector(view, "span"); + yield testAddProperty(view); + + info("Selecting the modified element with the new rule"); + yield selectNode("span", inspector); + yield checkModifiedElement(view, "span"); +}); + +function* testEditSelector(view, name) { + info("Test editing existing selector fields"); + + let ruleEditor = getRuleViewRuleEditor(view, 1); + + info("Focusing an existing selector name in the rule-view"); + let editor = yield focusEditableField(view, ruleEditor.selectorText); + + is(inplaceEditor(ruleEditor.selectorText), editor, + "The selector editor got focused"); + + info("Entering a new selector name and committing"); + editor.input.value = name; + + info("Waiting for rule view to update"); + let onRuleViewChanged = once(view, "ruleview-changed"); + + info("Entering the commit key"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewChanged; + + is(view._elementStyle.rules.length, 2, "Should have 2 rules."); + ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists."); + ok(getRuleViewRuleEditor(view, 1).element.getAttribute("unmatched"), + "Rule with " + name + " does not match the current element."); + + // Escape the new property editor after editing the selector + let onBlur = once(view.styleDocument.activeElement, "blur"); + EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow); + yield onBlur; +} + +function* checkModifiedElement(view, name) { + is(view._elementStyle.rules.length, 3, "Should have 3 rules."); + ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists."); +} + +function* testAddProperty(view) { + info("Test creating a new property"); + let textProp = yield addProperty(view, 1, "text-align", "center"); + + is(textProp.value, "center", "Text prop should have been changed."); + ok(!textProp.overridden, "Property should not be overridden"); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector_06.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector_06.js new file mode 100644 index 000000000..7d782a309 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_06.js @@ -0,0 +1,76 @@ +/* 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"; + +// Testing selector inplace-editor behaviors in the rule-view with unmatched +// selectors + +const TEST_URI = ` + <style type="text/css"> + .testclass { + text-align: center; + } + div { + } + </style> + <div class="testclass">Styled Node</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode(".testclass", inspector); + yield testEditClassSelector(view); + yield testEditDivSelector(view); +}); + +function* testEditClassSelector(view) { + let ruleEditor = getRuleViewRuleEditor(view, 1); + let editor = yield focusEditableField(view, ruleEditor.selectorText); + + editor.input.value = "body"; + let onRuleViewChanged = once(view, "ruleview-changed"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewChanged; + + // Get the new rule editor that replaced the original + ruleEditor = getRuleViewRuleEditor(view, 1); + let propEditor = ruleEditor.rule.textProps[0].editor; + + info("Check that the correct rules are visible"); + is(view._elementStyle.rules.length, 3, "Should have 3 rules."); + ok(ruleEditor.element.getAttribute("unmatched"), "Rule editor is unmatched."); + is(getRuleViewRule(view, ".testclass"), undefined, + "Rule with .testclass selector should not exist."); + ok(getRuleViewRule(view, "body"), + "Rule with body selector exists."); + is(inplaceEditor(propEditor.nameSpan), + inplaceEditor(view.styleDocument.activeElement), + "Focus should have moved to the property name."); +} + +function* testEditDivSelector(view) { + let ruleEditor = getRuleViewRuleEditor(view, 2); + let editor = yield focusEditableField(view, ruleEditor.selectorText); + + editor.input.value = "asdf"; + let onRuleViewChanged = once(view, "ruleview-changed"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewChanged; + + // Get the new rule editor that replaced the original + ruleEditor = getRuleViewRuleEditor(view, 2); + + info("Check that the correct rules are visible"); + is(view._elementStyle.rules.length, 3, "Should have 3 rules."); + ok(ruleEditor.element.getAttribute("unmatched"), "Rule editor is unmatched."); + is(getRuleViewRule(view, "div"), undefined, + "Rule with div selector should not exist."); + ok(getRuleViewRule(view, "asdf"), + "Rule with asdf selector exists."); + is(inplaceEditor(ruleEditor.newPropSpan), + inplaceEditor(view.styleDocument.activeElement), + "Focus should have moved to the property name."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector_07.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector_07.js new file mode 100644 index 000000000..81c7aad72 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_07.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that the rule view overridden search filter does not appear for an +// unmatched rule. + +const TEST_URI = ` + <style type="text/css"> + div { + height: 0px; + } + #testid { + height: 1px; + } + .testclass { + height: 10px; + } + </style> + <div id="testid">Styled Node</div> + <span class="testclass">This is a span</span> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + + yield selectNode("#testid", inspector); + yield testEditSelector(view, "span"); +}); + +function* testEditSelector(view, name) { + info("Test editing existing selector fields"); + + let ruleEditor = getRuleViewRuleEditor(view, 1); + + info("Focusing an existing selector name in the rule-view"); + let editor = yield focusEditableField(view, ruleEditor.selectorText); + + is(inplaceEditor(ruleEditor.selectorText), editor, + "The selector editor got focused"); + + info("Entering a new selector name and committing"); + editor.input.value = name; + + info("Entering the commit key"); + let onRuleViewChanged = once(view, "ruleview-changed"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewChanged; + + // Get the new rule editor that replaced the original + ruleEditor = getRuleViewRuleEditor(view, 1); + let rule = ruleEditor.rule; + let textPropEditor = rule.textProps[0].editor; + + is(view._elementStyle.rules.length, 3, "Should have 3 rules."); + ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists."); + ok(ruleEditor.element.getAttribute("unmatched"), + "Rule with " + name + " does not match the current element."); + ok(textPropEditor.filterProperty.hidden, "Overridden search is hidden."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector_08.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector_08.js new file mode 100644 index 000000000..33382e0de --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_08.js @@ -0,0 +1,71 @@ +/* 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 reverting a selector edit does the right thing. +// Bug 1241046. + +const TEST_URI = ` + <style type="text/css"> + span { + color: chartreuse; + } + </style> + <span> + <div id="testid" class="testclass">Styled Node</div> + </span> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + + info("Selecting the test element"); + yield selectNode("#testid", inspector); + + let idRuleEditor = getRuleViewRuleEditor(view, 2); + + info("Focusing an existing selector name in the rule-view"); + let editor = yield focusEditableField(view, idRuleEditor.selectorText); + + is(inplaceEditor(idRuleEditor.selectorText), editor, + "The selector editor got focused"); + + info("Entering a new selector name and committing"); + editor.input.value = "pre"; + + info("Waiting for rule view to update"); + let onRuleViewChanged = once(view, "ruleview-changed"); + + info("Entering the commit key"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewChanged; + + info("Re-focusing the selector name in the rule-view"); + idRuleEditor = getRuleViewRuleEditor(view, 2); + editor = yield focusEditableField(view, idRuleEditor.selectorText); + + is(view._elementStyle.rules.length, 2, "Should have 2 rules."); + ok(getRuleViewRule(view, "pre"), "Rule with pre selector exists."); + is(getRuleViewRuleEditor(view, 2).element.getAttribute("unmatched"), + "true", + "Rule with pre does not match the current element."); + + // Now change it back. + info("Re-entering original selector name and committing"); + editor.input.value = "span"; + + info("Waiting for rule view to update"); + onRuleViewChanged = once(view, "ruleview-changed"); + + info("Entering the commit key"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewChanged; + + is(view._elementStyle.rules.length, 2, "Should have 2 rules."); + ok(getRuleViewRule(view, "span"), "Rule with span selector exists."); + is(getRuleViewRuleEditor(view, 2).element.getAttribute("unmatched"), + "false", "Rule with span matches the current element."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector_09.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector_09.js new file mode 100644 index 000000000..a18ddc5ef --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_09.js @@ -0,0 +1,110 @@ +/* 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"; + +// Tests that editing a selector to an unmatched rule does set up the correct +// property on the rule, and that settings property in said rule does not +// lead to overriding properties from matched rules. +// Test that having a rule with both matched and unmatched selectors does work +// correctly. + +const TEST_URI = ` + <style type="text/css"> + #testid { + color: black; + } + .testclass { + background-color: white; + } + </style> + <div id="testid">Styled Node</div> + <span class="testclass">This is a span</span> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + + yield selectNode("#testid", inspector); + yield testEditSelector(view, "span"); + yield testAddImportantProperty(view); + yield testAddMatchedRule(view, "span, div"); +}); + +function* testEditSelector(view, name) { + info("Test editing existing selector fields"); + + let ruleEditor = getRuleViewRuleEditor(view, 1); + + info("Focusing an existing selector name in the rule-view"); + let editor = yield focusEditableField(view, ruleEditor.selectorText); + + is(inplaceEditor(ruleEditor.selectorText), editor, + "The selector editor got focused"); + + info("Entering a new selector name and committing"); + editor.input.value = name; + + info("Waiting for rule view to update"); + let onRuleViewChanged = once(view, "ruleview-changed"); + + info("Entering the commit key"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewChanged; + + ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists."); + ok(getRuleViewRuleEditor(view, 1).element.getAttribute("unmatched"), + "Rule with " + name + " does not match the current element."); + + // Escape the new property editor after editing the selector + let onBlur = once(view.styleDocument.activeElement, "blur"); + EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow); + yield onBlur; +} + +function* testAddImportantProperty(view) { + info("Test creating a new property with !important"); + let textProp = yield addProperty(view, 1, "color", "red !important"); + + is(textProp.value, "red", "Text prop should have been changed."); + is(textProp.priority, "important", + "Text prop has an \"important\" priority."); + ok(!textProp.overridden, "Property should not be overridden"); + + let ruleEditor = getRuleViewRuleEditor(view, 1); + let prop = ruleEditor.rule.textProps[0]; + ok(!prop.overridden, + "Existing property on matched rule should not be overridden"); +} + +function* testAddMatchedRule(view, name) { + info("Test adding a matching selector"); + + let ruleEditor = getRuleViewRuleEditor(view, 1); + + info("Focusing an existing selector name in the rule-view"); + let editor = yield focusEditableField(view, ruleEditor.selectorText); + + is(inplaceEditor(ruleEditor.selectorText), editor, + "The selector editor got focused"); + + info("Entering a new selector name and committing"); + editor.input.value = name; + + info("Waiting for rule view to update"); + let onRuleViewChanged = once(view, "ruleview-changed"); + + info("Entering the commit key"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewChanged; + + is(getRuleViewRuleEditor(view, 1).element.getAttribute("unmatched"), "false", + "Rule with " + name + " does match the current element."); + + // Escape the new property editor after editing the selector + let onBlur = once(view.styleDocument.activeElement, "blur"); + EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow); + yield onBlur; +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector_10.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector_10.js new file mode 100644 index 000000000..d878dd516 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_10.js @@ -0,0 +1,64 @@ +/* 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"; + +// Regression test for bug 1293616: make sure that editing a selector +// keeps the rule in the proper position. + +const TEST_URI = ` + <style type="text/css"> + #testid span, #testid p { + background: aqua; + } + span { + background: fuchsia; + } + </style> + <div id="testid"> + <span class="pickme"> + Styled Node + </span> + </div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode(".pickme", inspector); + yield testEditSelector(view); +}); + +function* testEditSelector(view) { + let ruleEditor = getRuleViewRuleEditor(view, 1); + let editor = yield focusEditableField(view, ruleEditor.selectorText); + + editor.input.value = "#testid span"; + let onRuleViewChanged = once(view, "ruleview-changed"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewChanged; + + // Escape the new property editor after editing the selector + let onBlur = once(view.styleDocument.activeElement, "blur"); + EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow); + yield onBlur; + + // Get the new rule editor that replaced the original + ruleEditor = getRuleViewRuleEditor(view, 1); + + info("Check that the correct rules are visible"); + is(view._elementStyle.rules.length, 3, "Should have 3 rules."); + is(ruleEditor.element.getAttribute("unmatched"), "false", "Rule editor is matched."); + + let props = ruleEditor.rule.textProps; + is(props.length, 1, "Rule has correct number of properties"); + is(props[0].name, "background", "Found background property"); + ok(!props[0].overridden, "Background property is not overridden"); + + ruleEditor = getRuleViewRuleEditor(view, 2); + props = ruleEditor.rule.textProps; + is(props.length, 1, "Rule has correct number of properties"); + is(props[0].name, "background", "Found background property"); + ok(props[0].overridden, "Background property is overridden"); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-selector_11.js b/devtools/client/inspector/rules/test/browser_rules_edit-selector_11.js new file mode 100644 index 000000000..9a1bdc8fa --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_11.js @@ -0,0 +1,69 @@ +/* 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"; + +// Regression test for bug 1293616, where editing a selector should +// change the relative priority of the rule. + +const TEST_URI = ` + <style type="text/css"> + #testid { + background: aqua; + } + .pickme { + background: seagreen; + } + span { + background: fuchsia; + } + </style> + <div> + <span id="testid" class="pickme"> + Styled Node + </span> + </div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode(".pickme", inspector); + yield testEditSelector(view); +}); + +function* testEditSelector(view) { + let ruleEditor = getRuleViewRuleEditor(view, 1); + let editor = yield focusEditableField(view, ruleEditor.selectorText); + + editor.input.value = ".pickme"; + let onRuleViewChanged = once(view, "ruleview-changed"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewChanged; + + // Escape the new property editor after editing the selector + let onBlur = once(view.styleDocument.activeElement, "blur"); + EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow); + yield onBlur; + + // Get the new rule editor that replaced the original + ruleEditor = getRuleViewRuleEditor(view, 1); + + info("Check that the correct rules are visible"); + is(view._elementStyle.rules.length, 4, "Should have 4 rules."); + is(ruleEditor.element.getAttribute("unmatched"), "false", "Rule editor is matched."); + + let props = ruleEditor.rule.textProps; + is(props.length, 1, "Rule has correct number of properties"); + is(props[0].name, "background", "Found background property"); + is(props[0].value, "aqua", "Background property is aqua"); + ok(props[0].overridden, "Background property is overridden"); + + ruleEditor = getRuleViewRuleEditor(view, 2); + props = ruleEditor.rule.textProps; + is(props.length, 1, "Rule has correct number of properties"); + is(props[0].name, "background", "Found background property"); + is(props[0].value, "seagreen", "Background property is seagreen"); + ok(!props[0].overridden, "Background property is not overridden"); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_01.js b/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_01.js new file mode 100644 index 000000000..dbf59cba9 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_01.js @@ -0,0 +1,107 @@ +/* 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"; + +// Tests that clicking on swatch-preceeded value while editing the property name +// will result in editing the property value. Also tests that the value span is updated +// only if the property name has changed. See also Bug 1248274. + +const TEST_URI = ` + <style type="text/css"> + #testid { + color: red; + } + </style> + <div id="testid">Styled Node</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + + yield selectNode("#testid", inspector); + + let ruleEditor = getRuleViewRuleEditor(view, 1); + let propEditor = ruleEditor.rule.textProps[0].editor; + + yield testColorValueSpanClickWithoutNameChange(propEditor, view); + yield testColorValueSpanClickAfterNameChange(propEditor, view); +}); + +function* testColorValueSpanClickWithoutNameChange(propEditor, view) { + info("Test click on color span while focusing property name editor"); + let colorSpan = propEditor.valueSpan.querySelector(".ruleview-color"); + + info("Focus the color name span"); + yield focusEditableField(view, propEditor.nameSpan); + let editor = inplaceEditor(propEditor.doc.activeElement); + + // We add a click event to make sure the color span won't be cleared + // on nameSpan blur (which would lead to the click event not being triggered) + let onColorSpanClick = once(colorSpan, "click"); + + // The property-value-updated is emitted when the valueSpan markup is being + // re-populated, which should not be the case when not modifying the property name + let onPropertyValueUpdated = function () { + ok(false, "The \"property-value-updated\" should not be emitted"); + }; + view.on("property-value-updated", onPropertyValueUpdated); + + info("blur propEditor.nameSpan by clicking on the color span"); + EventUtils.synthesizeMouse(colorSpan, 1, 1, {}, propEditor.doc.defaultView); + + info("wait for the click event on the color span"); + yield onColorSpanClick; + ok(true, "Expected click event was emitted"); + + editor = inplaceEditor(propEditor.doc.activeElement); + is(inplaceEditor(propEditor.valueSpan), editor, + "The property value editor got focused"); + + // We remove this listener in order to not cause unwanted conflict in the next test + view.off("property-value-updated", onPropertyValueUpdated); + + info("blur valueSpan editor to trigger ruleview-changed event and prevent " + + "having pending request"); + let onRuleViewChanged = view.once("ruleview-changed"); + editor.input.blur(); + yield onRuleViewChanged; +} + +function* testColorValueSpanClickAfterNameChange(propEditor, view) { + info("Test click on color span after property name change"); + let colorSpan = propEditor.valueSpan.querySelector(".ruleview-color"); + + info("Focus the color name span"); + yield focusEditableField(view, propEditor.nameSpan); + let editor = inplaceEditor(propEditor.doc.activeElement); + + info("Modify the property to border-color to trigger the " + + "property-value-updated event"); + editor.input.value = "border-color"; + + let onRuleViewChanged = view.once("ruleview-changed"); + let onPropertyValueUpdate = view.once("property-value-updated"); + + info("blur propEditor.nameSpan by clicking on the color span"); + EventUtils.synthesizeMouse(colorSpan, 1, 1, {}, propEditor.doc.defaultView); + + info("wait for ruleview-changed event to be triggered to prevent pending requests"); + yield onRuleViewChanged; + + info("wait for the property value to be updated"); + yield onPropertyValueUpdate; + ok(true, "Expected \"property-value-updated\" event was emitted"); + + editor = inplaceEditor(propEditor.doc.activeElement); + is(inplaceEditor(propEditor.valueSpan), editor, + "The property value editor got focused"); + + info("blur valueSpan editor to trigger ruleview-changed event and prevent " + + "having pending request"); + onRuleViewChanged = view.once("ruleview-changed"); + editor.input.blur(); + yield onRuleViewChanged; +} diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_02.js b/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_02.js new file mode 100644 index 000000000..372ed7477 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_02.js @@ -0,0 +1,65 @@ +/* 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"; + +// Tests that hitting shift + click on color swatch while editing the property +// name will only change the color unit and not lead to edit the property value. +// See also Bug 1248274. + +const TEST_URI = ` + <style type="text/css"> + #testid { + color: red; + background: linear-gradient( + 90deg, + rgb(183,222,237), + rgb(33,180,226), + rgb(31,170,217), + rgba(200,170,140,0.5)); + } + </style> + <div id="testid">Styled Node</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + + info("Test shift + click on color swatch while editing property name"); + + yield selectNode("#testid", inspector); + let ruleEditor = getRuleViewRuleEditor(view, 1); + let propEditor = ruleEditor.rule.textProps[1].editor; + let swatchSpan = propEditor.valueSpan.querySelectorAll(".ruleview-colorswatch")[2]; + + info("Focus the background name span"); + yield focusEditableField(view, propEditor.nameSpan); + let editor = inplaceEditor(propEditor.doc.activeElement); + + info("Modify the property to background-image to trigger the " + + "property-value-updated event"); + editor.input.value = "background-image"; + + let onPropertyValueUpdate = view.once("property-value-updated"); + let onSwatchUnitChange = swatchSpan.once("unit-change"); + let onRuleViewChanged = view.once("ruleview-changed"); + + info("blur propEditor.nameSpan by clicking on the color swatch"); + EventUtils.synthesizeMouseAtCenter(swatchSpan, {shiftKey: true}, + propEditor.doc.defaultView); + + info("wait for ruleview-changed event to be triggered to prevent pending requests"); + yield onRuleViewChanged; + + info("wait for the color unit to change"); + yield onSwatchUnitChange; + ok(true, "the color unit was changed"); + + info("wait for the property value to be updated"); + yield onPropertyValueUpdate; + + ok(!inplaceEditor(propEditor.valueSpan), "The inplace editor wasn't shown " + + "as a result of the color swatch shift + click"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_03.js b/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_03.js new file mode 100644 index 000000000..041a45a3e --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_03.js @@ -0,0 +1,69 @@ +/* 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"; + +// Tests that clicking on color swatch while editing the property name +// will show the color tooltip with the correct value. See also Bug 1248274. + +const TEST_URI = ` + <style type="text/css"> + #testid { + color: red; + background: linear-gradient( + 90deg, + rgb(183,222,237), + rgb(33,180,226), + rgb(31,170,217), + rgba(200,170,140,0.5)); + } + </style> + <div id="testid">Styled Node</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + + info("Test click on color swatch while editing property name"); + + yield selectNode("#testid", inspector); + let ruleEditor = getRuleViewRuleEditor(view, 1); + let propEditor = ruleEditor.rule.textProps[1].editor; + let swatchSpan = propEditor.valueSpan.querySelectorAll( + ".ruleview-colorswatch")[3]; + let colorPicker = view.tooltips.colorPicker; + + info("Focus the background name span"); + yield focusEditableField(view, propEditor.nameSpan); + let editor = inplaceEditor(propEditor.doc.activeElement); + + info("Modify the background property to background-image to trigger the " + + "property-value-updated event"); + editor.input.value = "background-image"; + + let onRuleViewChanged = view.once("ruleview-changed"); + let onPropertyValueUpdate = view.once("property-value-updated"); + let onReady = colorPicker.once("ready"); + + info("blur propEditor.nameSpan by clicking on the color swatch"); + EventUtils.synthesizeMouseAtCenter(swatchSpan, {}, + propEditor.doc.defaultView); + + info("wait for ruleview-changed event to be triggered to prevent pending requests"); + yield onRuleViewChanged; + + info("wait for the property value to be updated"); + yield onPropertyValueUpdate; + + info("wait for the color picker to be shown"); + yield onReady; + + ok(true, "The color picker was shown on click of the color swatch"); + ok(!inplaceEditor(propEditor.valueSpan), + "The inplace editor wasn't shown as a result of the color swatch click"); + + let spectrum = colorPicker.spectrum; + is(spectrum.rgb, "200,170,140,0.5", "The correct color picker was shown"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_04.js b/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_04.js new file mode 100644 index 000000000..fa4d8e6e2 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_04.js @@ -0,0 +1,62 @@ +/* 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"; + +// Tests that clicking on a property's value URL while editing the property name +// will open the link in a new tab. See also Bug 1248274. + +const TEST_URI = ` + <style type="text/css"> + #testid { + background: url("chrome://global/skin/icons/warning-64.png"), linear-gradient(white, #F06 400px); + } + </style> + <div id="testid">Styled Node</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + + info("Test click on background-image url while editing property name"); + + yield selectNode("#testid", inspector); + let ruleEditor = getRuleViewRuleEditor(view, 1); + let propEditor = ruleEditor.rule.textProps[0].editor; + let anchor = propEditor.valueSpan.querySelector(".ruleview-propertyvalue .theme-link"); + + info("Focus the background name span"); + yield focusEditableField(view, propEditor.nameSpan); + let editor = inplaceEditor(propEditor.doc.activeElement); + + info("Modify the property to background to trigger the " + + "property-value-updated event"); + editor.input.value = "background-image"; + + let onRuleViewChanged = view.once("ruleview-changed"); + let onPropertyValueUpdate = view.once("property-value-updated"); + let onTabOpened = waitForTab(); + + info("blur propEditor.nameSpan by clicking on the link"); + // The url can be wrapped across multiple lines, and so we click the lower left corner + // of the anchor to make sure to target the link. + let rect = anchor.getBoundingClientRect(); + EventUtils.synthesizeMouse(anchor, 2, rect.height - 2, {}, propEditor.doc.defaultView); + + info("wait for ruleview-changed event to be triggered to prevent pending requests"); + yield onRuleViewChanged; + + info("wait for the property value to be updated"); + yield onPropertyValueUpdate; + + info("wait for the image to be open in a new tab"); + let tab = yield onTabOpened; + ok(true, "A new tab opened"); + + is(tab.linkedBrowser.currentURI.spec, anchor.href, + "The URL for the new tab is correct"); + + gBrowser.removeTab(tab); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_editable-field-focus_01.js b/devtools/client/inspector/rules/test/browser_rules_editable-field-focus_01.js new file mode 100644 index 000000000..c9c7cd3d2 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_editable-field-focus_01.js @@ -0,0 +1,94 @@ +/* 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"; + +// Tests that the correct editable fields are focused when tabbing and entering +// through the rule view. + +const TEST_URI = ` + <style type='text/css'> + #testid { + background-color: blue; + color: red; + margin: 0; + padding: 0; + } + div { + border-color: red + } + </style> + <div id='testid'>Styled Node</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + yield testEditableFieldFocus(inspector, view, "VK_RETURN"); + yield testEditableFieldFocus(inspector, view, "VK_TAB"); +}); + +function* testEditableFieldFocus(inspector, view, commitKey) { + info("Click on the selector of the inline style ('element')"); + let ruleEditor = getRuleViewRuleEditor(view, 0); + let onFocus = once(ruleEditor.element, "focus", true); + ruleEditor.selectorText.click(); + yield onFocus; + assertEditor(view, ruleEditor.newPropSpan, + "Focus should be in the element property span"); + + info("Focus the next field with " + commitKey); + ruleEditor = getRuleViewRuleEditor(view, 1); + yield focusNextEditableField(view, ruleEditor, commitKey); + assertEditor(view, ruleEditor.selectorText, + "Focus should have moved to the next rule selector"); + + for (let i = 0; i < ruleEditor.rule.textProps.length; i++) { + let textProp = ruleEditor.rule.textProps[i]; + let propEditor = textProp.editor; + + info("Focus the next field with " + commitKey); + // Expect a ruleview-changed event if we are moving from a property value + // to the next property name (which occurs after the first iteration, as for + // i=0, the previous field is the selector). + let onRuleViewChanged = i > 0 ? view.once("ruleview-changed") : null; + yield focusNextEditableField(view, ruleEditor, commitKey); + yield onRuleViewChanged; + assertEditor(view, propEditor.nameSpan, + "Focus should have moved to the property name"); + + info("Focus the next field with " + commitKey); + yield focusNextEditableField(view, ruleEditor, commitKey); + assertEditor(view, propEditor.valueSpan, + "Focus should have moved to the property value"); + } + + // Expect a ruleview-changed event again as we're bluring a property value. + let onRuleViewChanged = view.once("ruleview-changed"); + yield focusNextEditableField(view, ruleEditor, commitKey); + yield onRuleViewChanged; + assertEditor(view, ruleEditor.newPropSpan, + "Focus should have moved to the new property span"); + + ruleEditor = getRuleViewRuleEditor(view, 2); + + yield focusNextEditableField(view, ruleEditor, commitKey); + assertEditor(view, ruleEditor.selectorText, + "Focus should have moved to the next rule selector"); + + info("Blur the selector field"); + EventUtils.synthesizeKey("VK_ESCAPE", {}); +} + +function* focusNextEditableField(view, ruleEditor, commitKey) { + let onFocus = once(ruleEditor.element, "focus", true); + EventUtils.synthesizeKey(commitKey, {}, view.styleWindow); + yield onFocus; +} + +function assertEditor(view, element, message) { + let editor = inplaceEditor(view.styleDocument.activeElement); + is(inplaceEditor(element), editor, message); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_editable-field-focus_02.js b/devtools/client/inspector/rules/test/browser_rules_editable-field-focus_02.js new file mode 100644 index 000000000..13ad221f0 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_editable-field-focus_02.js @@ -0,0 +1,84 @@ +/* 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"; + +// Tests that the correct editable fields are focused when shift tabbing +// through the rule view. + +const TEST_URI = ` + <style type='text/css'> + #testid { + background-color: blue; + color: red; + margin: 0; + padding: 0; + } + div { + border-color: red + } + </style> + <div id='testid'>Styled Node</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + yield testEditableFieldFocus(inspector, view, "VK_TAB", { shiftKey: true }); +}); + +function* testEditableFieldFocus(inspector, view, commitKey, options = {}) { + let ruleEditor = getRuleViewRuleEditor(view, 2); + let editor = yield focusEditableField(view, ruleEditor.selectorText); + is(inplaceEditor(ruleEditor.selectorText), editor, + "Focus should be in the 'div' rule selector"); + + ruleEditor = getRuleViewRuleEditor(view, 1); + + yield focusNextField(view, ruleEditor, commitKey, options); + assertEditor(view, ruleEditor.newPropSpan, + "Focus should have moved to the new property span"); + + for (let textProp of ruleEditor.rule.textProps.slice(0).reverse()) { + let propEditor = textProp.editor; + + yield focusNextField(view, ruleEditor, commitKey, options); + yield assertEditor(view, propEditor.valueSpan, + "Focus should have moved to the property value"); + + yield focusNextFieldAndExpectChange(view, ruleEditor, commitKey, options); + yield assertEditor(view, propEditor.nameSpan, + "Focus should have moved to the property name"); + } + + ruleEditor = getRuleViewRuleEditor(view, 1); + + yield focusNextField(view, ruleEditor, commitKey, options); + yield assertEditor(view, ruleEditor.selectorText, + "Focus should have moved to the '#testid' rule selector"); + + ruleEditor = getRuleViewRuleEditor(view, 0); + + yield focusNextField(view, ruleEditor, commitKey, options); + assertEditor(view, ruleEditor.newPropSpan, + "Focus should have moved to the new property span"); +} + +function* focusNextFieldAndExpectChange(view, ruleEditor, commitKey, options) { + let onRuleViewChanged = view.once("ruleview-changed"); + yield focusNextField(view, ruleEditor, commitKey, options); + yield onRuleViewChanged; +} + +function* focusNextField(view, ruleEditor, commitKey, options) { + let onFocus = once(ruleEditor.element, "focus", true); + EventUtils.synthesizeKey(commitKey, options, view.styleWindow); + yield onFocus; +} + +function* assertEditor(view, element, message) { + let editor = inplaceEditor(view.styleDocument.activeElement); + is(inplaceEditor(element), editor, message); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_eyedropper.js b/devtools/client/inspector/rules/test/browser_rules_eyedropper.js new file mode 100644 index 000000000..0762066e3 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_eyedropper.js @@ -0,0 +1,123 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test opening the eyedropper from the color picker. Pressing escape to close it, and +// clicking the page to select a color. + +const TEST_URI = ` + <style type="text/css"> + body { + background-color: white; + padding: 0px + } + + #div1 { + background-color: #ff5; + width: 20px; + height: 20px; + } + + #div2 { + margin-left: 20px; + width: 20px; + height: 20px; + background-color: #f09; + } + </style> + <body><div id="div1"></div><div id="div2"></div></body> +`; + +// #f09 +const ORIGINAL_COLOR = "rgb(255, 0, 153)"; +// #ff5 +const EXPECTED_COLOR = "rgb(255, 255, 85)"; + +add_task(function* () { + info("Add the test tab, open the rule-view and select the test node"); + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {testActor, inspector, view} = yield openRuleView(); + yield selectNode("#div2", inspector); + + info("Get the background-color property from the rule-view"); + let property = getRuleViewProperty(view, "#div2", "background-color"); + let swatch = property.valueSpan.querySelector(".ruleview-colorswatch"); + ok(swatch, "Color swatch is displayed for the bg-color property"); + + info("Open the eyedropper from the colorpicker tooltip"); + yield openEyedropper(view, swatch); + + let tooltip = view.tooltips.colorPicker.tooltip; + ok(!tooltip.isVisible(), "color picker tooltip is closed after opening eyedropper"); + + info("Test that pressing escape dismisses the eyedropper"); + yield testESC(swatch, inspector, testActor); + + info("Open the eyedropper again"); + yield openEyedropper(view, swatch); + + info("Test that a color can be selected with the eyedropper"); + yield testSelect(view, swatch, inspector, testActor); + + let onHidden = tooltip.once("hidden"); + tooltip.hide(); + yield onHidden; + ok(!tooltip.isVisible(), "color picker tooltip is closed"); + + yield waitForTick(); +}); + +function* testESC(swatch, inspector, testActor) { + info("Press escape"); + let onCanceled = new Promise(resolve => { + inspector.inspector.once("color-pick-canceled", resolve); + }); + yield testActor.synthesizeKey({key: "VK_ESCAPE", options: {}}); + yield onCanceled; + + let color = swatch.style.backgroundColor; + is(color, ORIGINAL_COLOR, "swatch didn't change after pressing ESC"); +} + +function* testSelect(view, swatch, inspector, testActor) { + info("Click at x:10px y:10px"); + let onPicked = new Promise(resolve => { + inspector.inspector.once("color-picked", resolve); + }); + // The change to the content is done async after rule view change + let onRuleViewChanged = view.once("ruleview-changed"); + + yield testActor.synthesizeMouse({selector: "html", x: 10, y: 10, + options: {type: "mousemove"}}); + yield testActor.synthesizeMouse({selector: "html", x: 10, y: 10, + options: {type: "mousedown"}}); + yield testActor.synthesizeMouse({selector: "html", x: 10, y: 10, + options: {type: "mouseup"}}); + + yield onPicked; + yield onRuleViewChanged; + + let color = swatch.style.backgroundColor; + is(color, EXPECTED_COLOR, "swatch changed colors"); + + is((yield getComputedStyleProperty("div", null, "background-color")), + EXPECTED_COLOR, + "div's color set to body color after dropper"); +} + +function* openEyedropper(view, swatch) { + let tooltip = view.tooltips.colorPicker.tooltip; + + info("Click on the swatch"); + let onColorPickerReady = view.tooltips.colorPicker.once("ready"); + swatch.click(); + yield onColorPickerReady; + + let dropperButton = tooltip.doc.querySelector("#eyedropper-button"); + + info("Click on the eyedropper icon"); + let onOpened = tooltip.once("eyedropper-opened"); + dropperButton.click(); + yield onOpened; +} diff --git a/devtools/client/inspector/rules/test/browser_rules_filtereditor-appears-on-swatch-click.js b/devtools/client/inspector/rules/test/browser_rules_filtereditor-appears-on-swatch-click.js new file mode 100644 index 000000000..21eeebb36 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_filtereditor-appears-on-swatch-click.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests the that Filter Editor Tooltip opens by clicking on filter swatches + +const TEST_URL = URL_ROOT + "doc_filter.html"; + +add_task(function* () { + yield addTab(TEST_URL); + + let {view} = yield openRuleView(); + + info("Getting the filter swatch element"); + let swatch = getRuleViewProperty(view, "body", "filter").valueSpan + .querySelector(".ruleview-filterswatch"); + + let filterTooltip = view.tooltips.filterEditor; + // Clicking on a cssfilter swatch sets the current filter value in the tooltip + // which, in turn, makes the FilterWidget emit an "updated" event that causes + // the rule-view to refresh. So we must wait for the ruleview-changed event. + let onRuleViewChanged = view.once("ruleview-changed"); + swatch.click(); + yield onRuleViewChanged; + + ok(true, "The shown event was emitted after clicking on swatch"); + ok(!inplaceEditor(swatch.parentNode), + "The inplace editor wasn't shown as a result of the filter swatch click"); + + yield hideTooltipAndWaitForRuleViewChanged(filterTooltip, view); + + yield waitForTick(); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_filtereditor-commit-on-ENTER.js b/devtools/client/inspector/rules/test/browser_rules_filtereditor-commit-on-ENTER.js new file mode 100644 index 000000000..127a20843 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_filtereditor-commit-on-ENTER.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests the Filter Editor Tooltip committing changes on ENTER + +const TEST_URL = URL_ROOT + "doc_filter.html"; + +add_task(function* () { + yield addTab(TEST_URL); + let {view} = yield openRuleView(); + + info("Get the filter swatch element"); + let swatch = getRuleViewProperty(view, "body", "filter").valueSpan + .querySelector(".ruleview-filterswatch"); + + info("Click on the filter swatch element"); + // Clicking on a cssfilter swatch sets the current filter value in the tooltip + // which, in turn, makes the FilterWidget emit an "updated" event that causes + // the rule-view to refresh. So we must wait for the ruleview-changed event. + let onRuleViewChanged = view.once("ruleview-changed"); + swatch.click(); + yield onRuleViewChanged; + + info("Get the cssfilter widget instance"); + let filterTooltip = view.tooltips.filterEditor; + let widget = filterTooltip.widget; + + info("Set a new value in the cssfilter widget"); + onRuleViewChanged = view.once("ruleview-changed"); + widget.setCssValue("blur(2px)"); + yield waitForComputedStyleProperty("body", null, "filter", "blur(2px)"); + yield onRuleViewChanged; + ok(true, "Changes previewed on the element"); + + info("Press RETURN to commit changes"); + // Pressing return in the cssfilter tooltip triggeres 2 ruleview-changed + onRuleViewChanged = waitForNEvents(view, "ruleview-changed", 2); + EventUtils.sendKey("RETURN", widget.styleWindow); + yield onRuleViewChanged; + + is((yield getComputedStyleProperty("body", null, "filter")), "blur(2px)", + "The elemenet's filter was kept after RETURN"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_filtereditor-revert-on-ESC.js b/devtools/client/inspector/rules/test/browser_rules_filtereditor-revert-on-ESC.js new file mode 100644 index 000000000..0302f40a9 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_filtereditor-revert-on-ESC.js @@ -0,0 +1,118 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that changes made to the Filter Editor Tooltip are reverted when +// ESC is pressed + +const TEST_URL = URL_ROOT + "doc_filter.html"; + +add_task(function* () { + yield addTab(TEST_URL); + let {view} = yield openRuleView(); + yield testPressingEscapeRevertsChanges(view); + yield testPressingEscapeRevertsChangesAndDisables(view); +}); + +function* testPressingEscapeRevertsChanges(view) { + let ruleEditor = getRuleViewRuleEditor(view, 1); + let propEditor = ruleEditor.rule.textProps[0].editor; + let swatch = propEditor.valueSpan.querySelector(".ruleview-filterswatch"); + + yield clickOnFilterSwatch(swatch, view); + yield setValueInFilterWidget("blur(2px)", view); + + yield waitForComputedStyleProperty("body", null, "filter", "blur(2px)"); + is(propEditor.valueSpan.textContent, "blur(2px)", + "Got expected property value."); + + yield pressEscapeToCloseTooltip(view); + + yield waitForComputedStyleProperty("body", null, "filter", + "blur(2px) contrast(2)"); + is(propEditor.valueSpan.textContent, "blur(2px) contrast(2)", + "Got expected property value."); +} + +function* testPressingEscapeRevertsChangesAndDisables(view) { + let ruleEditor = getRuleViewRuleEditor(view, 1); + let propEditor = ruleEditor.rule.textProps[0].editor; + + info("Disabling filter property"); + let onRuleViewChanged = view.once("ruleview-changed"); + propEditor.enable.click(); + yield onRuleViewChanged; + + ok(propEditor.element.classList.contains("ruleview-overridden"), + "property is overridden."); + is(propEditor.enable.style.visibility, "visible", + "property enable checkbox is visible."); + ok(!propEditor.enable.getAttribute("checked"), + "property enable checkbox is not checked."); + ok(!propEditor.prop.enabled, + "filter property is disabled."); + let newValue = yield getRulePropertyValue("filter"); + is(newValue, "", "filter should have been unset."); + + let swatch = propEditor.valueSpan.querySelector(".ruleview-filterswatch"); + yield clickOnFilterSwatch(swatch, view); + + ok(!propEditor.element.classList.contains("ruleview-overridden"), + "property overridden is not displayed."); + is(propEditor.enable.style.visibility, "hidden", + "property enable checkbox is hidden."); + + yield setValueInFilterWidget("blur(2px)", view); + yield pressEscapeToCloseTooltip(view); + + ok(propEditor.element.classList.contains("ruleview-overridden"), + "property is overridden."); + is(propEditor.enable.style.visibility, "visible", + "property enable checkbox is visible."); + ok(!propEditor.enable.getAttribute("checked"), + "property enable checkbox is not checked."); + ok(!propEditor.prop.enabled, "filter property is disabled."); + newValue = yield getRulePropertyValue("filter"); + is(newValue, "", "filter should have been unset."); + is(propEditor.valueSpan.textContent, "blur(2px) contrast(2)", + "Got expected property value."); +} + +function* getRulePropertyValue(name) { + let propValue = yield executeInContent("Test:GetRulePropertyValue", { + styleSheetIndex: 0, + ruleIndex: 0, + name: name + }); + return propValue; +} + +function* clickOnFilterSwatch(swatch, view) { + info("Clicking on a css filter swatch to open the tooltip"); + + // Clicking on a cssfilter swatch sets the current filter value in the tooltip + // which, in turn, makes the FilterWidget emit an "updated" event that causes + // the rule-view to refresh. So we must wait for the ruleview-changed event. + let onRuleViewChanged = view.once("ruleview-changed"); + swatch.click(); + yield onRuleViewChanged; +} + +function* setValueInFilterWidget(value, view) { + info("Setting the CSS filter value in the tooltip"); + + let filterTooltip = view.tooltips.filterEditor; + let onRuleViewChanged = view.once("ruleview-changed"); + filterTooltip.widget.setCssValue(value); + yield onRuleViewChanged; +} + +function* pressEscapeToCloseTooltip(view) { + info("Pressing ESCAPE to close the tooltip"); + + let filterTooltip = view.tooltips.filterEditor; + let onRuleViewChanged = view.once("ruleview-changed"); + EventUtils.sendKey("ESCAPE", filterTooltip.widget.styleWindow); + yield onRuleViewChanged; +} diff --git a/devtools/client/inspector/rules/test/browser_rules_grid-highlighter-on-navigate.js b/devtools/client/inspector/rules/test/browser_rules_grid-highlighter-on-navigate.js new file mode 100644 index 000000000..617eb00da --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_grid-highlighter-on-navigate.js @@ -0,0 +1,41 @@ +/* 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 grid highlighter is hidden on page navigation. + +const TEST_URI = ` + <style type='text/css'> + #grid { + display: grid; + } + </style> + <div id="grid"> + <div id="cell1">cell1</div> + <div id="cell2">cell2</div> + </div> +`; + +const TEST_URI_2 = "data:text/html,<html><body>test</body></html>"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + let highlighters = view.highlighters; + + yield selectNode("#grid", inspector); + let container = getRuleViewProperty(view, "#grid", "display").valueSpan; + let gridToggle = container.querySelector(".ruleview-grid"); + + info("Toggling ON the CSS grid highlighter from the rule-view."); + let onHighlighterShown = highlighters.once("highlighter-shown"); + gridToggle.click(); + yield onHighlighterShown; + + ok(highlighters.gridHighlighterShown, "CSS grid highlighter is shown."); + + yield navigateTo(inspector, TEST_URI_2); + ok(!highlighters.gridHighlighterShown, "CSS grid highlighter is hidden."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_grid-highlighter-on-reload.js b/devtools/client/inspector/rules/test/browser_rules_grid-highlighter-on-reload.js new file mode 100644 index 000000000..a6780a94a --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_grid-highlighter-on-reload.js @@ -0,0 +1,53 @@ +/* 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 a grid highlighter showing grid gaps can be displayed after reloading the +// page (Bug 1342051). + +const TEST_URI = ` + <style type='text/css'> + #grid { + display: grid; + grid-gap: 10px; + } + </style> + <div id="grid"> + <div id="cell1">cell1</div> + <div id="cell2">cell2</div> + </div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + + info("Check that the grid highlighter can be displayed"); + yield checkGridHighlighter(); + + info("Close the toolbox before reloading the tab"); + let target = TargetFactory.forTab(gBrowser.selectedTab); + yield gDevTools.closeToolbox(target); + + yield refreshTab(gBrowser.selectedTab); + + info("Check that the grid highlighter can be displayed after reloading the page"); + yield checkGridHighlighter(); +}); + +function* checkGridHighlighter() { + let {inspector, view} = yield openRuleView(); + let {highlighters} = view; + + yield selectNode("#grid", inspector); + let container = getRuleViewProperty(view, "#grid", "display").valueSpan; + let gridToggle = container.querySelector(".ruleview-grid"); + + info("Toggling ON the CSS grid highlighter from the rule-view."); + let onHighlighterShown = highlighters.once("highlighter-shown"); + gridToggle.click(); + yield onHighlighterShown; + + ok(highlighters.gridHighlighterShown, "CSS grid highlighter is shown."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_grid-toggle_01.js b/devtools/client/inspector/rules/test/browser_rules_grid-toggle_01.js new file mode 100644 index 000000000..04534522b --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_grid-toggle_01.js @@ -0,0 +1,64 @@ +/* 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 toggling the grid highlighter in the rule view and the display of the +// grid highlighter. + +const TEST_URI = ` + <style type='text/css'> + #grid { + display: grid; + } + </style> + <div id="grid"> + <div id="cell1">cell1</div> + <div id="cell2">cell2</div> + </div> +`; + +const HIGHLIGHTER_TYPE = "CssGridHighlighter"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + let highlighters = view.highlighters; + + yield selectNode("#grid", inspector); + let container = getRuleViewProperty(view, "#grid", "display").valueSpan; + let gridToggle = container.querySelector(".ruleview-grid"); + + info("Checking the initial state of the CSS grid toggle in the rule-view."); + ok(gridToggle, "Grid highlighter toggle is visible."); + ok(!gridToggle.classList.contains("active"), + "Grid highlighter toggle button is not active."); + ok(!highlighters.highlighters[HIGHLIGHTER_TYPE], + "No CSS grid highlighter exists in the rule-view."); + ok(!highlighters.gridHighlighterShown, "No CSS grid highlighter is shown."); + + info("Toggling ON the CSS grid highlighter from the rule-view."); + let onHighlighterShown = highlighters.once("highlighter-shown"); + gridToggle.click(); + yield onHighlighterShown; + + info("Checking the CSS grid highlighter is created and toggle button is active in " + + "the rule-view."); + ok(gridToggle.classList.contains("active"), + "Grid highlighter toggle is active."); + ok(highlighters.highlighters[HIGHLIGHTER_TYPE], + "CSS grid highlighter created in the rule-view."); + ok(highlighters.gridHighlighterShown, "CSS grid highlighter is shown."); + + info("Toggling OFF the CSS grid highlighter from the rule-view."); + let onHighlighterHidden = highlighters.once("highlighter-hidden"); + gridToggle.click(); + yield onHighlighterHidden; + + info("Checking the CSS grid highlighter is not shown and toggle button is not active " + + "in the rule-view."); + ok(!gridToggle.classList.contains("active"), + "Grid highlighter toggle button is not active."); + ok(!highlighters.gridHighlighterShown, "No CSS grid highlighter is shown."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_grid-toggle_02.js b/devtools/client/inspector/rules/test/browser_rules_grid-toggle_02.js new file mode 100644 index 000000000..5c339e892 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_grid-toggle_02.js @@ -0,0 +1,73 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test toggling the grid highlighter in the rule view from an overridden 'display: grid' +// declaration. + +const TEST_URI = ` + <style type='text/css'> + #grid { + display: grid; + } + div, ul { + display: grid; + } + </style> + <ul id="grid"> + <li id="cell1">cell1</li> + <li id="cell2">cell2</li> + </ul> +`; + +const HIGHLIGHTER_TYPE = "CssGridHighlighter"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + let highlighters = view.highlighters; + + yield selectNode("#grid", inspector); + let container = getRuleViewProperty(view, "#grid", "display").valueSpan; + let gridToggle = container.querySelector(".ruleview-grid"); + let overriddenContainer = getRuleViewProperty(view, "div, ul", "display").valueSpan; + let overriddenGridToggle = overriddenContainer.querySelector(".ruleview-grid"); + + info("Checking the initial state of the CSS grid toggle in the rule-view."); + ok(gridToggle && overriddenGridToggle, "Grid highlighter toggles are visible."); + ok(!gridToggle.classList.contains("active") && + !overriddenGridToggle.classList.contains("active"), + "Grid highlighter toggle buttons are not active."); + ok(!highlighters.highlighters[HIGHLIGHTER_TYPE], + "No CSS grid highlighter exists in the rule-view."); + ok(!highlighters.gridHighlighterShown, "No CSS grid highlighter is shown."); + + info("Toggling ON the CSS grid highlighter from the overridden rule in the rule-view."); + let onHighlighterShown = highlighters.once("highlighter-shown"); + overriddenGridToggle.click(); + yield onHighlighterShown; + + info("Checking the CSS grid highlighter is created and toggle buttons are active in " + + "the rule-view."); + ok(gridToggle.classList.contains("active") && + overriddenGridToggle.classList.contains("active"), + "Grid highlighter toggle is active."); + ok(highlighters.highlighters[HIGHLIGHTER_TYPE], + "CSS grid highlighter created in the rule-view."); + ok(highlighters.gridHighlighterShown, "CSS grid highlighter is shown."); + + info("Toggling off the CSS grid highlighter from the normal grid declaration in the " + + "rule-view."); + let onHighlighterHidden = highlighters.once("highlighter-hidden"); + gridToggle.click(); + yield onHighlighterHidden; + + info("Checking the CSS grid highlighter is not shown and toggle buttons are not " + + "active in the rule-view."); + ok(!gridToggle.classList.contains("active") && + !overriddenGridToggle.classList.contains("active"), + "Grid highlighter toggle buttons are not active."); + ok(!highlighters.gridHighlighterShown, "No CSS grid highlighter is shown."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_grid-toggle_03.js b/devtools/client/inspector/rules/test/browser_rules_grid-toggle_03.js new file mode 100644 index 000000000..a908d6a97 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_grid-toggle_03.js @@ -0,0 +1,96 @@ +/* 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 toggling the grid highlighter in the rule view with multiple grids in the page. + +const TEST_URI = ` + <style type='text/css'> + .grid { + display: grid; + } + </style> + <div id="grid1" class="grid"> + <div class="cell1">cell1</div> + <div class="cell2">cell2</div> + </div> + <div id="grid2" class="grid"> + <div class="cell1">cell1</div> + <div class="cell2">cell2</div> + </div> +`; + +const HIGHLIGHTER_TYPE = "CssGridHighlighter"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + let highlighters = view.highlighters; + + info("Selecting the first grid container."); + yield selectNode("#grid1", inspector); + let container = getRuleViewProperty(view, ".grid", "display").valueSpan; + let gridToggle = container.querySelector(".ruleview-grid"); + + info("Checking the state of the CSS grid toggle for the first grid container in the " + + "rule-view."); + ok(gridToggle, "Grid highlighter toggle is visible."); + ok(!gridToggle.classList.contains("active"), + "Grid highlighter toggle button is not active."); + ok(!highlighters.highlighters[HIGHLIGHTER_TYPE], + "No CSS grid highlighter exists in the rule-view."); + ok(!highlighters.gridHighlighterShown, "No CSS grid highlighter is shown."); + + info("Toggling ON the CSS grid highlighter for the first grid container from the " + + "rule-view."); + let onHighlighterShown = highlighters.once("highlighter-shown"); + gridToggle.click(); + yield onHighlighterShown; + + info("Checking the CSS grid highlighter is created and toggle button is active in " + + "the rule-view."); + ok(gridToggle.classList.contains("active"), + "Grid highlighter toggle is active."); + ok(highlighters.highlighters[HIGHLIGHTER_TYPE], + "CSS grid highlighter created in the rule-view."); + ok(highlighters.gridHighlighterShown, "CSS grid highlighter is shown."); + + info("Selecting the second grid container."); + yield selectNode("#grid2", inspector); + let firstGridHighterShown = highlighters.gridHighlighterShown; + container = getRuleViewProperty(view, ".grid", "display").valueSpan; + gridToggle = container.querySelector(".ruleview-grid"); + + info("Checking the state of the CSS grid toggle for the second grid container in the " + + "rule-view."); + ok(gridToggle, "Grid highlighter toggle is visible."); + ok(!gridToggle.classList.contains("active"), + "Grid highlighter toggle button is not active."); + ok(highlighters.gridHighlighterShown, "CSS grid highlighter is still shown."); + + info("Toggling ON the CSS grid highlighter for the second grid container from the " + + "rule-view."); + onHighlighterShown = highlighters.once("highlighter-shown"); + gridToggle.click(); + yield onHighlighterShown; + + info("Checking the CSS grid highlighter is created for the second grid container and " + + "toggle button is active in the rule-view."); + ok(gridToggle.classList.contains("active"), + "Grid highlighter toggle is active."); + ok(highlighters.gridHighlighterShown != firstGridHighterShown, + "Grid highlighter for the second grid container is shown."); + + info("Selecting the first grid container."); + yield selectNode("#grid1", inspector); + container = getRuleViewProperty(view, ".grid", "display").valueSpan; + gridToggle = container.querySelector(".ruleview-grid"); + + info("Checking the state of the CSS grid toggle for the first grid container in the " + + "rule-view."); + ok(gridToggle, "Grid highlighter toggle is visible."); + ok(!gridToggle.classList.contains("active"), + "Grid highlighter toggle button is not active."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_guessIndentation.js b/devtools/client/inspector/rules/test/browser_rules_guessIndentation.js new file mode 100644 index 000000000..ba2a1d7fb --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_guessIndentation.js @@ -0,0 +1,47 @@ +/* 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"; + +// Tests that we can guess indentation from a style sheet, not just a +// rule. + +// Use a weird indentation depth to avoid accidental success. +const TEST_URI = ` + <style type='text/css'> +div { + background-color: blue; +} + +* { +} +</style> + <div id='testid' class='testclass'>Styled Node</div> +`; + +const expectedText = ` +div { + background-color: blue; +} + +* { + color: chartreuse; +} +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {toolbox, inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + + info("Add a new property in the rule-view"); + yield addProperty(view, 2, "color", "chartreuse"); + + info("Switch to the style-editor"); + let { UI } = yield toolbox.selectTool("styleeditor"); + + let styleEditor = yield UI.editors[0].getSourceEditor(); + let text = styleEditor.sourceEditor.getText(); + is(text, expectedText, "style inspector changes are synced"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_inherited-properties_01.js b/devtools/client/inspector/rules/test/browser_rules_inherited-properties_01.js new file mode 100644 index 000000000..d1f6d7f45 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_inherited-properties_01.js @@ -0,0 +1,47 @@ +/* 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 that inherited properties appear for a nested element in the +// rule view. + +const TEST_URI = ` + <style type="text/css"> + #test2 { + background-color: green; + color: purple; + } + </style> + <div id="test2"><div id="test1">Styled Node</div></div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#test1", inspector); + yield simpleInherit(inspector, view); +}); + +function* simpleInherit(inspector, view) { + let elementStyle = view._elementStyle; + is(elementStyle.rules.length, 2, "Should have 2 rules."); + + let elementRule = elementStyle.rules[0]; + ok(!elementRule.inherited, + "Element style attribute should not consider itself inherited."); + + let inheritRule = elementStyle.rules[1]; + is(inheritRule.selectorText, "#test2", + "Inherited rule should be the one that includes inheritable properties."); + ok(!!inheritRule.inherited, "Rule should consider itself inherited."); + is(inheritRule.textProps.length, 2, + "Rule should have two styles"); + let bgcProp = inheritRule.textProps[0]; + is(bgcProp.name, "background-color", + "background-color property should exist"); + ok(bgcProp.invisible, "background-color property should be invisible"); + let inheritProp = inheritRule.textProps[1]; + is(inheritProp.name, "color", "color should have been inherited."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_inherited-properties_02.js b/devtools/client/inspector/rules/test/browser_rules_inherited-properties_02.js new file mode 100644 index 000000000..db9662eee --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_inherited-properties_02.js @@ -0,0 +1,34 @@ +/* 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 that no inherited properties appear when the property does not apply +// to the nested element. + +const TEST_URI = ` + <style type="text/css"> + #test2 { + background-color: green; + } + </style> + <div id="test2"><div id="test1">Styled Node</div></div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#test1", inspector); + yield emptyInherit(inspector, view); +}); + +function* emptyInherit(inspector, view) { + // No inheritable styles, this rule shouldn't show up. + let elementStyle = view._elementStyle; + is(elementStyle.rules.length, 1, "Should have 1 rule."); + + let elementRule = elementStyle.rules[0]; + ok(!elementRule.inherited, + "Element style attribute should not consider itself inherited."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_inherited-properties_03.js b/devtools/client/inspector/rules/test/browser_rules_inherited-properties_03.js new file mode 100644 index 000000000..d6075f6f4 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_inherited-properties_03.js @@ -0,0 +1,40 @@ +/* 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 that inline inherited properties appear in the nested element. + +var {ELEMENT_STYLE} = require("devtools/shared/specs/styles"); + +const TEST_URI = ` + <div id="test2" style="color: red"> + <div id="test1">Styled Node</div> + </div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#test1", inspector); + yield elementStyleInherit(inspector, view); +}); + +function* elementStyleInherit(inspector, view) { + let elementStyle = view._elementStyle; + is(elementStyle.rules.length, 2, "Should have 2 rules."); + + let elementRule = elementStyle.rules[0]; + ok(!elementRule.inherited, + "Element style attribute should not consider itself inherited."); + + let inheritRule = elementStyle.rules[1]; + is(inheritRule.domRule.type, ELEMENT_STYLE, + "Inherited rule should be an element style, not a rule."); + ok(!!inheritRule.inherited, "Rule should consider itself inherited."); + is(inheritRule.textProps.length, 1, + "Should only display one inherited style"); + let inheritProp = inheritRule.textProps[0]; + is(inheritProp.name, "color", "color should have been inherited."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_inline-source-map.js b/devtools/client/inspector/rules/test/browser_rules_inline-source-map.js new file mode 100644 index 000000000..05109d8c6 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_inline-source-map.js @@ -0,0 +1,26 @@ +/* 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 when a source map comment appears in an inline stylesheet, the +// rule-view still appears correctly. +// Bug 1255787. + +const TESTCASE_URI = URL_ROOT + "doc_inline_sourcemap.html"; +const PREF = "devtools.styleeditor.source-maps-enabled"; + +add_task(function* () { + Services.prefs.setBoolPref(PREF, true); + + yield addTab(TESTCASE_URI); + let {inspector, view} = yield openRuleView(); + + yield selectNode("div", inspector); + + let ruleEl = getRuleViewRule(view, "div"); + ok(ruleEl, "The 'div' rule exists in the rule-view"); + + Services.prefs.clearUserPref(PREF); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_invalid-source-map.js b/devtools/client/inspector/rules/test/browser_rules_invalid-source-map.js new file mode 100644 index 000000000..825f48a96 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_invalid-source-map.js @@ -0,0 +1,44 @@ +/* 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 when a source map is missing/invalid, the rule view still loads +// correctly. + +const TESTCASE_URI = URL_ROOT + "doc_invalid_sourcemap.html"; +const PREF = "devtools.styleeditor.source-maps-enabled"; +const CSS_LOC = "doc_invalid_sourcemap.css:1"; + +add_task(function* () { + Services.prefs.setBoolPref(PREF, true); + + yield addTab(TESTCASE_URI); + let {inspector, view} = yield openRuleView(); + + yield selectNode("div", inspector); + + let ruleEl = getRuleViewRule(view, "div"); + ok(ruleEl, "The 'div' rule exists in the rule-view"); + + let prop = getRuleViewProperty(view, "div", "color"); + ok(prop, "The 'color' property exists in this rule"); + + let value = getRuleViewPropertyValue(view, "div", "color"); + is(value, "gold", "The 'color' property has the right value"); + + yield verifyLinkText(view, CSS_LOC); + + Services.prefs.clearUserPref(PREF); +}); + +function verifyLinkText(view, text) { + info("Verifying that the rule-view stylesheet link is " + text); + let label = getRuleViewLinkByIndex(view, 1) + .querySelector(".ruleview-rule-source-label"); + return waitForSuccess( + () => label.textContent == text, + "Link text changed to display correct location: " + text + ); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_invalid.js b/devtools/client/inspector/rules/test/browser_rules_invalid.js new file mode 100644 index 000000000..e664f68ac --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_invalid.js @@ -0,0 +1,33 @@ +/* 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 an invalid property still lets us display the rule view +// Bug 1235603. + +const TEST_URI = ` + <style> + div { + background: #fff; + font-family: sans-serif; + url(display-table.min.htc); + } + </style> + <body> + <div id="testid" class="testclass">Styled Node</div> + </body> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + + is(view._elementStyle.rules.length, 2, "Should have 2 rules."); + // Have to actually get the rule in order to ensure that the + // elements were created. + ok(getRuleViewRule(view, "div"), "Rule with div selector exists"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_keybindings.js b/devtools/client/inspector/rules/test/browser_rules_keybindings.js new file mode 100644 index 000000000..84fdeff85 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_keybindings.js @@ -0,0 +1,49 @@ +/* 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 focus doesn't leave the style editor when adding a property +// (bug 719916) + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8,<h1>Some header text</h1>"); + let {inspector, view} = yield openRuleView(); + yield selectNode("h1", inspector); + + info("Getting the ruleclose brace element"); + let brace = view.styleDocument.querySelector(".ruleview-ruleclose"); + + info("Focus the new property editable field to create a color property"); + let ruleEditor = getRuleViewRuleEditor(view, 0); + let editor = yield focusNewRuleViewProperty(ruleEditor); + editor.input.value = "color"; + + info("Typing ENTER to focus the next field: property value"); + let onFocus = once(brace.parentNode, "focus", true); + let onRuleViewChanged = view.once("ruleview-changed"); + + EventUtils.sendKey("return"); + + yield onFocus; + yield onRuleViewChanged; + ok(true, "The value field was focused"); + + info("Entering a property value"); + editor = getCurrentInplaceEditor(view); + editor.input.value = "green"; + + info("Typing ENTER again should focus a new property name"); + onFocus = once(brace.parentNode, "focus", true); + onRuleViewChanged = view.once("ruleview-changed"); + EventUtils.sendKey("return"); + yield onFocus; + yield onRuleViewChanged; + ok(true, "The new property name field was focused"); + getCurrentInplaceEditor(view).input.blur(); +}); + +function getCurrentInplaceEditor(view) { + return inplaceEditor(view.styleDocument.activeElement); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_keyframeLineNumbers.js b/devtools/client/inspector/rules/test/browser_rules_keyframeLineNumbers.js new file mode 100644 index 000000000..ebbde08ac --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_keyframeLineNumbers.js @@ -0,0 +1,25 @@ +/* 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 editing a rule will update the line numbers of subsequent +// rules in the rule view. + +const TESTCASE_URI = URL_ROOT + "doc_keyframeLineNumbers.html"; + +add_task(function* () { + yield addTab(TESTCASE_URI); + let { inspector, view } = yield openRuleView(); + yield selectNode("#outer", inspector); + + info("Insert a new property, which will affect the line numbers"); + yield addProperty(view, 1, "font-size", "72px"); + + yield selectNode("#inner", inspector); + + let value = getRuleViewLinkTextByIndex(view, 3); + // Note that this is relative to the <style>. + is(value.slice(-3), ":27", "rule line number is 27"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_keyframes-rule_01.js b/devtools/client/inspector/rules/test/browser_rules_keyframes-rule_01.js new file mode 100644 index 000000000..8d4b436c5 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_keyframes-rule_01.js @@ -0,0 +1,106 @@ +/* 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 keyframe rules and gutters are displayed correctly in the +// rule view. + +const TEST_URI = URL_ROOT + "doc_keyframeanimation.html"; + +add_task(function* () { + yield addTab(TEST_URI); + let {inspector, view} = yield openRuleView(); + yield testPacman(inspector, view); + yield testBoxy(inspector, view); + yield testMoxy(inspector, view); +}); + +function* testPacman(inspector, view) { + info("Test content and gutter in the keyframes rule of #pacman"); + + yield assertKeyframeRules("#pacman", inspector, view, { + elementRulesNb: 2, + keyframeRulesNb: 2, + keyframesRules: ["pacman", "pacman"], + keyframeRules: ["100%", "100%"] + }); + + assertGutters(view, { + guttersNbs: 2, + gutterHeading: ["Keyframes pacman", "Keyframes pacman"] + }); +} + +function* testBoxy(inspector, view) { + info("Test content and gutter in the keyframes rule of #boxy"); + + yield assertKeyframeRules("#boxy", inspector, view, { + elementRulesNb: 3, + keyframeRulesNb: 3, + keyframesRules: ["boxy", "boxy", "boxy"], + keyframeRules: ["10%", "20%", "100%"] + }); + + assertGutters(view, { + guttersNbs: 1, + gutterHeading: ["Keyframes boxy"] + }); +} + +function* testMoxy(inspector, view) { + info("Test content and gutter in the keyframes rule of #moxy"); + + yield assertKeyframeRules("#moxy", inspector, view, { + elementRulesNb: 3, + keyframeRulesNb: 4, + keyframesRules: ["boxy", "boxy", "boxy", "moxy"], + keyframeRules: ["10%", "20%", "100%", "100%"] + }); + + assertGutters(view, { + guttersNbs: 2, + gutterHeading: ["Keyframes boxy", "Keyframes moxy"] + }); +} + +function* assertKeyframeRules(selector, inspector, view, expected) { + yield selectNode(selector, inspector); + let elementStyle = view._elementStyle; + + let rules = { + elementRules: elementStyle.rules.filter(rule => !rule.keyframes), + keyframeRules: elementStyle.rules.filter(rule => rule.keyframes) + }; + + is(rules.elementRules.length, expected.elementRulesNb, selector + + " has the correct number of non keyframe element rules"); + is(rules.keyframeRules.length, expected.keyframeRulesNb, selector + + " has the correct number of keyframe rules"); + + let i = 0; + for (let keyframeRule of rules.keyframeRules) { + ok(keyframeRule.keyframes.name == expected.keyframesRules[i], + keyframeRule.keyframes.name + " has the correct keyframes name"); + ok(keyframeRule.domRule.keyText == expected.keyframeRules[i], + keyframeRule.domRule.keyText + " selector heading is correct"); + i++; + } +} + +function assertGutters(view, expected) { + let gutters = view.element.querySelectorAll(".theme-gutter"); + + is(gutters.length, expected.guttersNbs, + "There are " + gutters.length + " gutter headings"); + + let i = 0; + for (let gutter of gutters) { + is(gutter.textContent, expected.gutterHeading[i], + "Correct " + gutter.textContent + " gutter headings"); + i++; + } + + return gutters; +} diff --git a/devtools/client/inspector/rules/test/browser_rules_keyframes-rule_02.js b/devtools/client/inspector/rules/test/browser_rules_keyframes-rule_02.js new file mode 100644 index 000000000..b7652ecaa --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_keyframes-rule_02.js @@ -0,0 +1,92 @@ +/* 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 verifies the content of the keyframes rule and property changes +// to keyframe rules. + +const TEST_URI = URL_ROOT + "doc_keyframeanimation.html"; + +add_task(function* () { + yield addTab(TEST_URI); + let {inspector, view} = yield openRuleView(); + yield testPacman(inspector, view); + yield testBoxy(inspector, view); +}); + +function* testPacman(inspector, view) { + info("Test content in the keyframes rule of #pacman"); + + let rules = yield getKeyframeRules("#pacman", inspector, view); + + info("Test text properties for Keyframes #pacman"); + + is(convertTextPropsToString(rules.keyframeRules[0].textProps), + "left: 750px", + "Keyframe pacman (100%) property is correct" + ); + + // Dynamic changes test disabled because of Bug 1050940 + // If this part of the test is ever enabled again, it should be changed to + // use addProperty (in head.js) and stop using _applyingModifications + + // info("Test dynamic changes to keyframe rule for #pacman"); + + // let defaultView = element.ownerDocument.defaultView; + // let ruleEditor = view.element.children[5].childNodes[0]._ruleEditor; + // ruleEditor.addProperty("opacity", "0", true); + + // yield ruleEditor._applyingModifications; + // yield once(element, "animationend"); + + // is + // ( + // convertTextPropsToString(rules.keyframeRules[1].textProps), + // "left: 750px; opacity: 0", + // "Keyframe pacman (100%) property is correct" + // ); + + // is(defaultView.getComputedStyle(element).getPropertyValue("opacity"), "0", + // "Added opacity property should have been used."); +} + +function* testBoxy(inspector, view) { + info("Test content in the keyframes rule of #boxy"); + + let rules = yield getKeyframeRules("#boxy", inspector, view); + + info("Test text properties for Keyframes #boxy"); + + is(convertTextPropsToString(rules.keyframeRules[0].textProps), + "background-color: blue", + "Keyframe boxy (10%) property is correct" + ); + + is(convertTextPropsToString(rules.keyframeRules[1].textProps), + "background-color: green", + "Keyframe boxy (20%) property is correct" + ); + + is(convertTextPropsToString(rules.keyframeRules[2].textProps), + "opacity: 0", + "Keyframe boxy (100%) property is correct" + ); +} + +function convertTextPropsToString(textProps) { + return textProps.map(t => t.name + ": " + t.value).join("; "); +} + +function* getKeyframeRules(selector, inspector, view) { + yield selectNode(selector, inspector); + let elementStyle = view._elementStyle; + + let rules = { + elementRules: elementStyle.rules.filter(rule => !rule.keyframes), + keyframeRules: elementStyle.rules.filter(rule => rule.keyframes) + }; + + return rules; +} diff --git a/devtools/client/inspector/rules/test/browser_rules_lineNumbers.js b/devtools/client/inspector/rules/test/browser_rules_lineNumbers.js new file mode 100644 index 000000000..3b09209f5 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_lineNumbers.js @@ -0,0 +1,29 @@ +/* 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 editing a rule will update the line numbers of subsequent +// rules in the rule view. + +const TESTCASE_URI = URL_ROOT + "doc_ruleLineNumbers.html"; + +add_task(function* () { + yield addTab(TESTCASE_URI); + let { inspector, view } = yield openRuleView(); + yield selectNode("#testid", inspector); + + let bodyRuleEditor = getRuleViewRuleEditor(view, 3); + let value = getRuleViewLinkTextByIndex(view, 2); + // Note that this is relative to the <style>. + is(value.slice(-2), ":6", "initial rule line number is 6"); + + let onLocationChanged = once(bodyRuleEditor.rule.domRule, "location-changed"); + yield addProperty(view, 1, "font-size", "23px"); + yield onLocationChanged; + + let newBodyTitle = getRuleViewLinkTextByIndex(view, 2); + // Note that this is relative to the <style>. + is(newBodyTitle.slice(-2), ":7", "updated rule line number is 7"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_livepreview.js b/devtools/client/inspector/rules/test/browser_rules_livepreview.js new file mode 100644 index 000000000..1f1302a70 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_livepreview.js @@ -0,0 +1,72 @@ +/* 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 changes are previewed when editing a property value. + +const TEST_URI = ` + <style type="text/css"> + #testid { + display:block; + } + </style> + <div id="testid">Styled Node</div><span>inline element</span> +`; + +// Format +// { +// value : what to type in the field +// expected : expected computed style on the targeted element +// } +const TEST_DATA = [ + {value: "inline", expected: "inline"}, + {value: "inline-block", expected: "inline-block"}, + + // Invalid property values should not apply, and should fall back to default + {value: "red", expected: "block"}, + {value: "something", expected: "block"}, + + {escape: true, value: "inline", expected: "block"} +]; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + + for (let data of TEST_DATA) { + yield testLivePreviewData(data, view, "#testid"); + } +}); + +function* testLivePreviewData(data, ruleView, selector) { + let rule = getRuleViewRuleEditor(ruleView, 1).rule; + let propEditor = rule.textProps[0].editor; + + info("Focusing the property value inplace-editor"); + let editor = yield focusEditableField(ruleView, propEditor.valueSpan); + is(inplaceEditor(propEditor.valueSpan), editor, + "The focused editor is the value"); + + info("Entering value in the editor: " + data.value); + let onPreviewDone = ruleView.once("ruleview-changed"); + EventUtils.sendString(data.value, ruleView.styleWindow); + ruleView.throttle.flush(); + yield onPreviewDone; + + let onValueDone = ruleView.once("ruleview-changed"); + if (data.escape) { + EventUtils.synthesizeKey("VK_ESCAPE", {}); + } else { + EventUtils.synthesizeKey("VK_RETURN", {}); + } + yield onValueDone; + + // While the editor is still focused in, the display should have + // changed already + is((yield getComputedStyleProperty(selector, null, "display")), + data.expected, + "Element should be previewed as " + data.expected); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_mark_overridden_01.js b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_01.js new file mode 100644 index 000000000..ab10fadfe --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_01.js @@ -0,0 +1,56 @@ +/* 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"; + +// Tests that the rule view marks overridden rules correctly based on the +// specificity of the rule. + +const TEST_URI = ` + <style type='text/css'> + #testid { + background-color: blue; + } + .testclass { + background-color: green; + } + </style> + <div id='testid' class='testclass'>Styled Node</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + + let idRule = getRuleViewRuleEditor(view, 1).rule; + let idProp = idRule.textProps[0]; + is(idProp.name, "background-color", + "First ID property should be background-color"); + is(idProp.value, "blue", "First ID property value should be blue"); + ok(!idProp.overridden, "ID prop should not be overridden."); + ok(!idProp.editor.element.classList.contains("ruleview-overridden"), + "ID property editor should not have ruleview-overridden class"); + + let classRule = getRuleViewRuleEditor(view, 2).rule; + let classProp = classRule.textProps[0]; + is(classProp.name, "background-color", + "First class prop should be background-color"); + is(classProp.value, "green", "First class property value should be green"); + ok(classProp.overridden, "Class property should be overridden."); + ok(classProp.editor.element.classList.contains("ruleview-overridden"), + "Class property editor should have ruleview-overridden class"); + + // Override background-color by changing the element style. + let elementProp = yield addProperty(view, 0, "background-color", "purple"); + + ok(!elementProp.overridden, + "Element style property should not be overridden"); + ok(idProp.overridden, "ID property should be overridden"); + ok(idProp.editor.element.classList.contains("ruleview-overridden"), + "ID property editor should have ruleview-overridden class"); + ok(classProp.overridden, "Class property should be overridden"); + ok(classProp.editor.element.classList.contains("ruleview-overridden"), + "Class property editor should have ruleview-overridden class"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_mark_overridden_02.js b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_02.js new file mode 100644 index 000000000..c71fc7211 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_02.js @@ -0,0 +1,45 @@ +/* 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"; + +// Tests that the rule view marks overridden rules correctly for short hand +// properties and the computed list properties + +const TEST_URI = ` + <style type='text/css'> + #testid { + margin-left: 1px; + } + .testclass { + margin: 2px; + } + </style> + <div id='testid' class='testclass'>Styled Node</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + yield testMarkOverridden(inspector, view); +}); + +function* testMarkOverridden(inspector, view) { + let elementStyle = view._elementStyle; + + let classRule = elementStyle.rules[2]; + let classProp = classRule.textProps[0]; + ok(!classProp.overridden, + "Class prop shouldn't be overridden, some props are still being used."); + + for (let computed of classProp.computed) { + if (computed.name.indexOf("margin-left") == 0) { + ok(computed.overridden, "margin-left props should be overridden."); + } else { + ok(!computed.overridden, + "Non-margin-left props should not be overridden."); + } + } +} diff --git a/devtools/client/inspector/rules/test/browser_rules_mark_overridden_03.js b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_03.js new file mode 100644 index 000000000..b99bab8b4 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_03.js @@ -0,0 +1,41 @@ +/* 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"; + +// Tests that the rule view marks overridden rules correctly based on the +// priority for the rule + +const TEST_URI = ` + <style type='text/css'> + #testid { + background-color: blue; + } + .testclass { + background-color: green !important; + } + </style> + <div id='testid' class='testclass'>Styled Node</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + + let idRule = getRuleViewRuleEditor(view, 1).rule; + let idProp = idRule.textProps[0]; + ok(idProp.overridden, "Not-important rule should be overridden."); + + let classRule = getRuleViewRuleEditor(view, 2).rule; + let classProp = classRule.textProps[0]; + ok(!classProp.overridden, "Important rule should not be overridden."); + + ok(idProp.overridden, "ID property should be overridden."); + + // FIXME: re-enable these 2 assertions when bug 1247737 is fixed. + // let elementProp = yield addProperty(view, 0, "background-color", "purple"); + // ok(!elementProp.overridden, "New important prop should not be overriden."); + // ok(classProp.overridden, "Class property should be overridden."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_mark_overridden_04.js b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_04.js new file mode 100644 index 000000000..fbce1ebf4 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_04.js @@ -0,0 +1,36 @@ +/* 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"; + +// Tests that the rule view marks overridden rules correctly if a property gets +// disabled + +const TEST_URI = ` + <style type='text/css'> + #testid { + background-color: blue; + } + .testclass { + background-color: green; + } + </style> + <div id='testid' class='testclass'>Styled Node</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + + let idRule = getRuleViewRuleEditor(view, 1).rule; + let idProp = idRule.textProps[0]; + + yield togglePropStatus(view, idProp); + + let classRule = getRuleViewRuleEditor(view, 2).rule; + let classProp = classRule.textProps[0]; + ok(!classProp.overridden, + "Class prop should not be overridden after id prop was disabled."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_mark_overridden_05.js b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_05.js new file mode 100644 index 000000000..11ecd72ff --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_05.js @@ -0,0 +1,33 @@ +/* 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"; + +// Tests that the rule view marks overridden rules correctly based on the +// order of the property. + +const TEST_URI = ` + <style type='text/css'> + #testid { + background-color: green; + } + </style> + <div id='testid' class='testclass'>Styled Node</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + + let rule = getRuleViewRuleEditor(view, 1).rule; + + yield addProperty(view, 1, "background-color", "red"); + + let firstProp = rule.textProps[0]; + let secondProp = rule.textProps[1]; + + ok(firstProp.overridden, "First property should be overridden."); + ok(!secondProp.overridden, "Second property should not be overridden."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_mark_overridden_06.js b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_06.js new file mode 100644 index 000000000..c2e71fe49 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_06.js @@ -0,0 +1,60 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that the rule view marks overridden rules correctly after +// editing the selector. + +const TEST_URI = ` + <style type='text/css'> + div { + background-color: blue; + background-color: chartreuse; + } + </style> + <div id='testid' class='testclass'>Styled Node</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + yield testMarkOverridden(inspector, view); +}); + +function* testMarkOverridden(inspector, view) { + let elementStyle = view._elementStyle; + let rule = elementStyle.rules[1]; + checkProperties(rule); + + let ruleEditor = getRuleViewRuleEditor(view, 1); + info("Focusing an existing selector name in the rule-view"); + let editor = yield focusEditableField(view, ruleEditor.selectorText); + + info("Entering a new selector name and committing"); + editor.input.value = "div[class]"; + + let onRuleViewChanged = once(view, "ruleview-changed"); + info("Entering the commit key"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewChanged; + + view.searchField.focus(); + checkProperties(rule); +} + +// A helper to perform a repeated set of checks. +function checkProperties(rule) { + let prop = rule.textProps[0]; + is(prop.name, "background-color", + "First property should be background-color"); + is(prop.value, "blue", "First property value should be blue"); + ok(prop.overridden, "prop should be overridden."); + prop = rule.textProps[1]; + is(prop.name, "background-color", + "Second property should be background-color"); + is(prop.value, "chartreuse", "First property value should be chartreuse"); + ok(!prop.overridden, "prop should not be overridden."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_mark_overridden_07.js b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_07.js new file mode 100644 index 000000000..9480ddd47 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_mark_overridden_07.js @@ -0,0 +1,72 @@ +/* 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"; + +// Tests that the rule view marks overridden rules correctly based on the +// specificity of the rule. + +const TEST_URI = ` + <style type='text/css'> + #testid { + margin-left: 23px; + } + + div { + margin-right: 23px; + margin-left: 1px !important; + } + + body { + margin-right: 1px !important; + font-size: 79px; + } + + span { + font-size: 12px; + } + </style> + <body> + <span> + <div id='testid' class='testclass'>Styled Node</div> + </span> + </body> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + yield testMarkOverridden(inspector, view); +}); + +function* testMarkOverridden(inspector, view) { + let elementStyle = view._elementStyle; + + let RESULTS = [ + // We skip the first element + [], + [{name: "margin-left", value: "23px", overridden: true}], + [{name: "margin-right", value: "23px", overridden: false}, + {name: "margin-left", value: "1px", overridden: false}], + [{name: "font-size", value: "12px", overridden: false}], + [{name: "margin-right", value: "1px", overridden: true}, + {name: "font-size", value: "79px", overridden: true}] + ]; + + for (let i = 1; i < RESULTS.length; ++i) { + let idRule = elementStyle.rules[i]; + + for (let propIndex in RESULTS[i]) { + let expected = RESULTS[i][propIndex]; + let prop = idRule.textProps[propIndex]; + + info("Checking rule " + i + ", property " + propIndex); + + is(prop.name, expected.name, "check property name"); + is(prop.value, expected.value, "check property value"); + is(prop.overridden, expected.overridden, "check property overridden"); + } + } +} diff --git a/devtools/client/inspector/rules/test/browser_rules_mathml-element.js b/devtools/client/inspector/rules/test/browser_rules_mathml-element.js new file mode 100644 index 000000000..f8a1e8572 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_mathml-element.js @@ -0,0 +1,53 @@ +/* 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"; + +// Tests that the rule-view displays correctly on MathML elements. + +const TEST_URI = ` + <div> + <math xmlns=\http://www.w3.org/1998/Math/MathML\> + <mfrac> + <msubsup> + <mi>a</mi> + <mi>i</mi> + <mi>j</mi> + </msubsup> + <msub> + <mi>x</mi> + <mn>0</mn> + </msub> + </mfrac> + </math> + </div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + + info("Select the DIV node and verify the rule-view shows rules"); + yield selectNode("div", inspector); + ok(view.element.querySelectorAll(".ruleview-rule").length, + "The rule-view shows rules for the div element"); + + info("Select various MathML nodes and verify the rule-view is empty"); + yield selectNode("math", inspector); + ok(!view.element.querySelectorAll(".ruleview-rule").length, + "The rule-view is empty for the math element"); + + yield selectNode("msubsup", inspector); + ok(!view.element.querySelectorAll(".ruleview-rule").length, + "The rule-view is empty for the msubsup element"); + + yield selectNode("mn", inspector); + ok(!view.element.querySelectorAll(".ruleview-rule").length, + "The rule-view is empty for the mn element"); + + info("Select again the DIV node and verify the rule-view shows rules"); + yield selectNode("div", inspector); + ok(view.element.querySelectorAll(".ruleview-rule").length, + "The rule-view shows rules for the div element"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_media-queries.js b/devtools/client/inspector/rules/test/browser_rules_media-queries.js new file mode 100644 index 000000000..57ab19163 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_media-queries.js @@ -0,0 +1,26 @@ +/* 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"; + +// Tests that we correctly display appropriate media query titles in the +// rule view. + +const TEST_URI = URL_ROOT + "doc_media_queries.html"; + +add_task(function* () { + yield addTab(TEST_URI); + let {inspector, view} = yield openRuleView(); + yield selectNode("div", inspector); + + let elementStyle = view._elementStyle; + + let inline = STYLE_INSPECTOR_L10N.getStr("rule.sourceInline"); + + is(elementStyle.rules.length, 3, "Should have 3 rules."); + is(elementStyle.rules[0].title, inline, "check rule 0 title"); + is(elementStyle.rules[1].title, inline + + ":9 @media screen and (min-width: 1px)", "check rule 1 title"); + is(elementStyle.rules[2].title, inline + ":2", "check rule 2 title"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_multiple-properties-duplicates.js b/devtools/client/inspector/rules/test/browser_rules_multiple-properties-duplicates.js new file mode 100644 index 000000000..c820dd73f --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_multiple-properties-duplicates.js @@ -0,0 +1,68 @@ +/* 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 rule-view behaves correctly when entering mutliple and/or +// unfinished properties/values in inplace-editors + +const TEST_URI = "<div>Test Element</div>"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("div", inspector); + + let ruleEditor = getRuleViewRuleEditor(view, 0); + // Note that we wait for a markup mutation here because this new rule will end + // up creating a style attribute on the node shown in the markup-view. + // (we also wait for the rule-view to refresh). + let onMutation = inspector.once("markupmutation"); + let onRuleViewChanged = view.once("ruleview-changed"); + yield createNewRuleViewProperty(ruleEditor, + "color:red;color:orange;color:yellow;color:green;color:blue;color:indigo;" + + "color:violet;"); + yield onMutation; + yield onRuleViewChanged; + + is(ruleEditor.rule.textProps.length, 7, + "Should have created new text properties."); + is(ruleEditor.propertyList.children.length, 8, + "Should have created new property editors."); + + is(ruleEditor.rule.textProps[0].name, "color", + "Should have correct property name"); + is(ruleEditor.rule.textProps[0].value, "red", + "Should have correct property value"); + + is(ruleEditor.rule.textProps[1].name, "color", + "Should have correct property name"); + is(ruleEditor.rule.textProps[1].value, "orange", + "Should have correct property value"); + + is(ruleEditor.rule.textProps[2].name, "color", + "Should have correct property name"); + is(ruleEditor.rule.textProps[2].value, "yellow", + "Should have correct property value"); + + is(ruleEditor.rule.textProps[3].name, "color", + "Should have correct property name"); + is(ruleEditor.rule.textProps[3].value, "green", + "Should have correct property value"); + + is(ruleEditor.rule.textProps[4].name, "color", + "Should have correct property name"); + is(ruleEditor.rule.textProps[4].value, "blue", + "Should have correct property value"); + + is(ruleEditor.rule.textProps[5].name, "color", + "Should have correct property name"); + is(ruleEditor.rule.textProps[5].value, "indigo", + "Should have correct property value"); + + is(ruleEditor.rule.textProps[6].name, "color", + "Should have correct property name"); + is(ruleEditor.rule.textProps[6].value, "violet", + "Should have correct property value"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_multiple-properties-priority.js b/devtools/client/inspector/rules/test/browser_rules_multiple-properties-priority.js new file mode 100644 index 000000000..f7d98b768 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_multiple-properties-priority.js @@ -0,0 +1,47 @@ +/* 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 rule-view behaves correctly when entering mutliple and/or +// unfinished properties/values in inplace-editors. + +const TEST_URI = "<div>Test Element</div>"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("div", inspector); + + let ruleEditor = getRuleViewRuleEditor(view, 0); + // Note that we wait for a markup mutation here because this new rule will end + // up creating a style attribute on the node shown in the markup-view. + // (we also wait for the rule-view to refresh). + let onMutation = inspector.once("markupmutation"); + let onRuleViewChanged = view.once("ruleview-changed"); + yield createNewRuleViewProperty(ruleEditor, + "color:red;width:100px;height: 100px;"); + yield onMutation; + yield onRuleViewChanged; + + is(ruleEditor.rule.textProps.length, 3, + "Should have created new text properties."); + is(ruleEditor.propertyList.children.length, 4, + "Should have created new property editors."); + + is(ruleEditor.rule.textProps[0].name, "color", + "Should have correct property name"); + is(ruleEditor.rule.textProps[0].value, "red", + "Should have correct property value"); + + is(ruleEditor.rule.textProps[1].name, "width", + "Should have correct property name"); + is(ruleEditor.rule.textProps[1].value, "100px", + "Should have correct property value"); + + is(ruleEditor.rule.textProps[2].name, "height", + "Should have correct property name"); + is(ruleEditor.rule.textProps[2].value, "100px", + "Should have correct property value"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_multiple-properties-unfinished_01.js b/devtools/client/inspector/rules/test/browser_rules_multiple-properties-unfinished_01.js new file mode 100644 index 000000000..deaf16029 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_multiple-properties-unfinished_01.js @@ -0,0 +1,62 @@ +/* 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 rule-view behaves correctly when entering multiple and/or +// unfinished properties/values in inplace-editors + +const TEST_URI = "<div>Test Element</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 testCreateNewMultiUnfinished(inspector, view); +}); + +function* testCreateNewMultiUnfinished(inspector, view) { + let ruleEditor = getRuleViewRuleEditor(view, 0); + let onMutation = inspector.once("markupmutation"); + let onRuleViewChanged = view.once("ruleview-changed"); + yield createNewRuleViewProperty(ruleEditor, + "color:blue;background : orange ; text-align:center; border-color: "); + yield onMutation; + yield onRuleViewChanged; + + is(ruleEditor.rule.textProps.length, 4, + "Should have created new text properties."); + is(ruleEditor.propertyList.children.length, 4, + "Should have created property editors."); + + EventUtils.sendString("red", view.styleWindow); + onRuleViewChanged = view.once("ruleview-changed"); + EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow); + yield onRuleViewChanged; + + is(ruleEditor.rule.textProps.length, 4, + "Should have the same number of text properties."); + is(ruleEditor.propertyList.children.length, 5, + "Should have added the changed value editor."); + + is(ruleEditor.rule.textProps[0].name, "color", + "Should have correct property name"); + is(ruleEditor.rule.textProps[0].value, "blue", + "Should have correct property value"); + + is(ruleEditor.rule.textProps[1].name, "background", + "Should have correct property name"); + is(ruleEditor.rule.textProps[1].value, "orange", + "Should have correct property value"); + + is(ruleEditor.rule.textProps[2].name, "text-align", + "Should have correct property name"); + is(ruleEditor.rule.textProps[2].value, "center", + "Should have correct property value"); + + is(ruleEditor.rule.textProps[3].name, "border-color", + "Should have correct property name"); + is(ruleEditor.rule.textProps[3].value, "red", + "Should have correct property value"); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_multiple-properties-unfinished_02.js b/devtools/client/inspector/rules/test/browser_rules_multiple-properties-unfinished_02.js new file mode 100644 index 000000000..dd1360b96 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_multiple-properties-unfinished_02.js @@ -0,0 +1,71 @@ +/* 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 rule-view behaves correctly when entering mutliple and/or +// unfinished properties/values in inplace-editors + +const TEST_URI = "<div>Test Element</div>"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + + // Turn off throttling, which can cause intermittents. Throttling is used by + // the TextPropertyEditor. + view.throttle = () => {}; + + yield selectNode("div", inspector); + + let ruleEditor = getRuleViewRuleEditor(view, 0); + // Note that we wait for a markup mutation here because this new rule will end + // up creating a style attribute on the node shown in the markup-view. + // (we also wait for the rule-view to refresh). + let onMutation = inspector.once("markupmutation"); + let onRuleViewChanged = view.once("ruleview-changed"); + yield createNewRuleViewProperty(ruleEditor, "width: 100px; heig"); + yield onMutation; + yield onRuleViewChanged; + + is(ruleEditor.rule.textProps.length, 2, + "Should have created a new text property."); + is(ruleEditor.propertyList.children.length, 2, + "Should have created a property editor."); + + // Value is focused, lets add multiple rules here and make sure they get added + onMutation = inspector.once("markupmutation"); + onRuleViewChanged = view.once("ruleview-changed"); + let valueEditor = ruleEditor.propertyList.children[1] + .querySelector(".styleinspector-propertyeditor"); + valueEditor.value = "10px;background:orangered;color: black;"; + EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow); + yield onMutation; + yield onRuleViewChanged; + + is(ruleEditor.rule.textProps.length, 4, + "Should have added the changed value."); + is(ruleEditor.propertyList.children.length, 5, + "Should have added the changed value editor."); + + is(ruleEditor.rule.textProps[0].name, "width", + "Should have correct property name"); + is(ruleEditor.rule.textProps[0].value, "100px", + "Should have correct property value"); + + is(ruleEditor.rule.textProps[1].name, "heig", + "Should have correct property name"); + is(ruleEditor.rule.textProps[1].value, "10px", + "Should have correct property value"); + + is(ruleEditor.rule.textProps[2].name, "background", + "Should have correct property name"); + is(ruleEditor.rule.textProps[2].value, "orangered", + "Should have correct property value"); + + is(ruleEditor.rule.textProps[3].name, "color", + "Should have correct property name"); + is(ruleEditor.rule.textProps[3].value, "black", + "Should have correct property value"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_multiple_properties_01.js b/devtools/client/inspector/rules/test/browser_rules_multiple_properties_01.js new file mode 100644 index 000000000..2801df652 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_multiple_properties_01.js @@ -0,0 +1,53 @@ +/* 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 rule-view behaves correctly when entering mutliple and/or +// unfinished properties/values in inplace-editors. + +const TEST_URI = "<div>Test Element</div>"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("div", inspector); + + let ruleEditor = getRuleViewRuleEditor(view, 0); + // Note that we wait for a markup mutation here because this new rule will end + // up creating a style attribute on the node shown in the markup-view. + // (we also wait for the rule-view to refresh). + let onMutation = inspector.once("markupmutation"); + let onRuleViewChanged = view.once("ruleview-changed"); + yield createNewRuleViewProperty(ruleEditor, + "color:blue;background : orange ; text-align:center; " + + "border-color: green;"); + yield onMutation; + yield onRuleViewChanged; + + is(ruleEditor.rule.textProps.length, 4, + "Should have created a new text property."); + is(ruleEditor.propertyList.children.length, 5, + "Should have created a new property editor."); + + is(ruleEditor.rule.textProps[0].name, "color", + "Should have correct property name"); + is(ruleEditor.rule.textProps[0].value, "blue", + "Should have correct property value"); + + is(ruleEditor.rule.textProps[1].name, "background", + "Should have correct property name"); + is(ruleEditor.rule.textProps[1].value, "orange", + "Should have correct property value"); + + is(ruleEditor.rule.textProps[2].name, "text-align", + "Should have correct property name"); + is(ruleEditor.rule.textProps[2].value, "center", + "Should have correct property value"); + + is(ruleEditor.rule.textProps[3].name, "border-color", + "Should have correct property name"); + is(ruleEditor.rule.textProps[3].value, "green", + "Should have correct property value"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_multiple_properties_02.js b/devtools/client/inspector/rules/test/browser_rules_multiple_properties_02.js new file mode 100644 index 000000000..ce6f1909f --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_multiple_properties_02.js @@ -0,0 +1,54 @@ +/* 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 rule-view behaves correctly when entering mutliple and/or +// unfinished properties/values in inplace-editors + +const TEST_URI = "<div>Test Element</div>"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("div", inspector); + + let ruleEditor = getRuleViewRuleEditor(view, 0); + let onDone = view.once("ruleview-changed"); + yield createNewRuleViewProperty(ruleEditor, "width:"); + yield onDone; + + is(ruleEditor.rule.textProps.length, 1, + "Should have created a new text property."); + is(ruleEditor.propertyList.children.length, 1, + "Should have created a property editor."); + + // Value is focused, lets add multiple rules here and make sure they get added + onDone = view.once("ruleview-changed"); + let onMutation = inspector.once("markupmutation"); + let input = view.styleDocument.activeElement; + input.value = "height: 10px;color:blue"; + EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow); + yield onMutation; + yield onDone; + + is(ruleEditor.rule.textProps.length, 2, + "Should have added the changed value."); + is(ruleEditor.propertyList.children.length, 3, + "Should have added the changed value editor."); + + EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow); + is(ruleEditor.propertyList.children.length, 2, + "Should have removed the value editor."); + + is(ruleEditor.rule.textProps[0].name, "width", + "Should have correct property name"); + is(ruleEditor.rule.textProps[0].value, "height: 10px", + "Should have correct property value"); + + is(ruleEditor.rule.textProps[1].name, "color", + "Should have correct property name"); + is(ruleEditor.rule.textProps[1].value, "blue", + "Should have correct property value"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_original-source-link.js b/devtools/client/inspector/rules/test/browser_rules_original-source-link.js new file mode 100644 index 000000000..09dad9a86 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_original-source-link.js @@ -0,0 +1,85 @@ +/* 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 stylesheet links in the rule view are correct when source maps +// are involved. + +const TESTCASE_URI = URL_ROOT + "doc_sourcemaps.html"; +const PREF = "devtools.styleeditor.source-maps-enabled"; +const SCSS_LOC = "doc_sourcemaps.scss:4"; +const CSS_LOC = "doc_sourcemaps.css:1"; + +add_task(function* () { + info("Setting the " + PREF + " pref to true"); + Services.prefs.setBoolPref(PREF, true); + + yield addTab(TESTCASE_URI); + let {toolbox, inspector, view} = yield openRuleView(); + + info("Selecting the test node"); + yield selectNode("div", inspector); + + yield verifyLinkText(SCSS_LOC, view); + + info("Setting the " + PREF + " pref to false"); + Services.prefs.setBoolPref(PREF, false); + yield verifyLinkText(CSS_LOC, view); + + info("Setting the " + PREF + " pref to true again"); + Services.prefs.setBoolPref(PREF, true); + + yield testClickingLink(toolbox, view); + yield checkDisplayedStylesheet(toolbox); + + info("Clearing the " + PREF + " pref"); + Services.prefs.clearUserPref(PREF); +}); + +function* testClickingLink(toolbox, view) { + info("Listening for switch to the style editor"); + let onStyleEditorReady = toolbox.once("styleeditor-ready"); + + info("Finding the stylesheet link and clicking it"); + let link = getRuleViewLinkByIndex(view, 1); + link.scrollIntoView(); + link.click(); + yield onStyleEditorReady; +} + +function checkDisplayedStylesheet(toolbox) { + let def = defer(); + + let panel = toolbox.getCurrentPanel(); + panel.UI.on("editor-selected", (event, editor) => { + // The style editor selects the first sheet at first load before + // selecting the desired sheet. + if (editor.styleSheet.href.endsWith("scss")) { + info("Original source editor selected"); + editor.getSourceEditor().then(editorSelected) + .then(def.resolve, def.reject); + } + }); + + return def.promise; +} + +function editorSelected(editor) { + let href = editor.styleSheet.href; + ok(href.endsWith("doc_sourcemaps.scss"), + "selected stylesheet is correct one"); + + let {line} = editor.sourceEditor.getCursor(); + is(line, 3, "cursor is at correct line number in original source"); +} + +function verifyLinkText(text, view) { + info("Verifying that the rule-view stylesheet link is " + text); + let label = getRuleViewLinkByIndex(view, 1) + .querySelector(".ruleview-rule-source-label"); + return waitForSuccess(function* () { + return label.textContent == text; + }, "Link text changed to display correct location: " + text); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_pseudo-element_01.js b/devtools/client/inspector/rules/test/browser_rules_pseudo-element_01.js new file mode 100644 index 000000000..e98b5437c --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_pseudo-element_01.js @@ -0,0 +1,260 @@ +/* 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 pseudoelements are displayed correctly in the rule view + +const TEST_URI = URL_ROOT + "doc_pseudoelement.html"; +const PSEUDO_PREF = "devtools.inspector.show_pseudo_elements"; + +add_task(function* () { + yield pushPref(PSEUDO_PREF, true); + + yield addTab(TEST_URI); + let {inspector, view} = yield openRuleView(); + + yield testTopLeft(inspector, view); + yield testTopRight(inspector, view); + yield testBottomRight(inspector, view); + yield testBottomLeft(inspector, view); + yield testParagraph(inspector, view); + yield testBody(inspector, view); +}); + +function* testTopLeft(inspector, view) { + let id = "#topleft"; + let rules = yield assertPseudoElementRulesNumbers(id, + inspector, view, { + elementRulesNb: 4, + firstLineRulesNb: 2, + firstLetterRulesNb: 1, + selectionRulesNb: 0, + afterRulesNb: 1, + beforeRulesNb: 2 + } + ); + + let gutters = assertGutters(view); + + info("Make sure that clicking on the twisty hides pseudo elements"); + let expander = gutters[0].querySelector(".ruleview-expander"); + ok(!view.element.children[1].hidden, "Pseudo Elements are expanded"); + + expander.click(); + ok(view.element.children[1].hidden, + "Pseudo Elements are collapsed by twisty"); + + expander.click(); + ok(!view.element.children[1].hidden, "Pseudo Elements are expanded again"); + + info("Make sure that dblclicking on the header container also toggles " + + "the pseudo elements"); + EventUtils.synthesizeMouseAtCenter(gutters[0], {clickCount: 2}, + view.styleWindow); + ok(view.element.children[1].hidden, + "Pseudo Elements are collapsed by dblclicking"); + + let elementRuleView = getRuleViewRuleEditor(view, 3); + + let elementFirstLineRule = rules.firstLineRules[0]; + let elementFirstLineRuleView = + [...view.element.children[1].children].filter(e => { + return e._ruleEditor && e._ruleEditor.rule === elementFirstLineRule; + })[0]._ruleEditor; + + is(convertTextPropsToString(elementFirstLineRule.textProps), + "color: orange", + "TopLeft firstLine properties are correct"); + + let onAdded = view.once("ruleview-changed"); + let firstProp = elementFirstLineRuleView.addProperty("background-color", + "rgb(0, 255, 0)", "", true); + yield onAdded; + + onAdded = view.once("ruleview-changed"); + let secondProp = elementFirstLineRuleView.addProperty("font-style", + "italic", "", true); + yield onAdded; + + is(firstProp, + elementFirstLineRule.textProps[elementFirstLineRule.textProps.length - 2], + "First added property is on back of array"); + is(secondProp, + elementFirstLineRule.textProps[elementFirstLineRule.textProps.length - 1], + "Second added property is on back of array"); + + is((yield getComputedStyleProperty(id, ":first-line", "background-color")), + "rgb(0, 255, 0)", "Added property should have been used."); + is((yield getComputedStyleProperty(id, ":first-line", "font-style")), + "italic", "Added property should have been used."); + is((yield getComputedStyleProperty(id, null, "text-decoration")), + "none", "Added property should not apply to element"); + + yield togglePropStatus(view, firstProp); + + is((yield getComputedStyleProperty(id, ":first-line", "background-color")), + "rgb(255, 0, 0)", "Disabled property should now have been used."); + is((yield getComputedStyleProperty(id, null, "background-color")), + "rgb(221, 221, 221)", "Added property should not apply to element"); + + yield togglePropStatus(view, firstProp); + + is((yield getComputedStyleProperty(id, ":first-line", "background-color")), + "rgb(0, 255, 0)", "Added property should have been used."); + is((yield getComputedStyleProperty(id, null, "text-decoration")), + "none", "Added property should not apply to element"); + + onAdded = view.once("ruleview-changed"); + firstProp = elementRuleView.addProperty("background-color", + "rgb(0, 0, 255)", "", true); + yield onAdded; + + is((yield getComputedStyleProperty(id, null, "background-color")), + "rgb(0, 0, 255)", "Added property should have been used."); + is((yield getComputedStyleProperty(id, ":first-line", "background-color")), + "rgb(0, 255, 0)", "Added prop does not apply to pseudo"); +} + +function* testTopRight(inspector, view) { + yield assertPseudoElementRulesNumbers("#topright", inspector, view, { + elementRulesNb: 4, + firstLineRulesNb: 1, + firstLetterRulesNb: 1, + selectionRulesNb: 0, + beforeRulesNb: 2, + afterRulesNb: 1 + }); + + let gutters = assertGutters(view); + + let expander = gutters[0].querySelector(".ruleview-expander"); + ok(!view.element.firstChild.classList.contains("show-expandable-container"), + "Pseudo Elements remain collapsed after switching element"); + + expander.scrollIntoView(); + expander.click(); + ok(!view.element.children[1].hidden, + "Pseudo Elements are shown again after clicking twisty"); +} + +function* testBottomRight(inspector, view) { + yield assertPseudoElementRulesNumbers("#bottomright", inspector, view, { + elementRulesNb: 4, + firstLineRulesNb: 1, + firstLetterRulesNb: 1, + selectionRulesNb: 0, + beforeRulesNb: 3, + afterRulesNb: 1 + }); +} + +function* testBottomLeft(inspector, view) { + yield assertPseudoElementRulesNumbers("#bottomleft", inspector, view, { + elementRulesNb: 4, + firstLineRulesNb: 1, + firstLetterRulesNb: 1, + selectionRulesNb: 0, + beforeRulesNb: 2, + afterRulesNb: 1 + }); +} + +function* testParagraph(inspector, view) { + let rules = + yield assertPseudoElementRulesNumbers("#bottomleft p", inspector, view, { + elementRulesNb: 3, + firstLineRulesNb: 1, + firstLetterRulesNb: 1, + selectionRulesNb: 1, + beforeRulesNb: 0, + afterRulesNb: 0 + }); + + assertGutters(view); + + let elementFirstLineRule = rules.firstLineRules[0]; + is(convertTextPropsToString(elementFirstLineRule.textProps), + "background: blue", + "Paragraph first-line properties are correct"); + + let elementFirstLetterRule = rules.firstLetterRules[0]; + is(convertTextPropsToString(elementFirstLetterRule.textProps), + "color: red; font-size: 130%", + "Paragraph first-letter properties are correct"); + + let elementSelectionRule = rules.selectionRules[0]; + is(convertTextPropsToString(elementSelectionRule.textProps), + "color: white; background: black", + "Paragraph first-letter properties are correct"); +} + +function* testBody(inspector, view) { + yield testNode("body", inspector, view); + + let gutters = getGutters(view); + is(gutters.length, 0, "There are no gutter headings"); +} + +function convertTextPropsToString(textProps) { + return textProps.map(t => t.name + ": " + t.value).join("; "); +} + +function* testNode(selector, inspector, view) { + yield selectNode(selector, inspector); + let elementStyle = view._elementStyle; + return elementStyle; +} + +function* assertPseudoElementRulesNumbers(selector, inspector, view, ruleNbs) { + let elementStyle = yield testNode(selector, inspector, view); + + let rules = { + elementRules: elementStyle.rules.filter(rule => !rule.pseudoElement), + firstLineRules: elementStyle.rules.filter(rule => + rule.pseudoElement === ":first-line"), + firstLetterRules: elementStyle.rules.filter(rule => + rule.pseudoElement === ":first-letter"), + selectionRules: elementStyle.rules.filter(rule => + rule.pseudoElement === ":-moz-selection"), + beforeRules: elementStyle.rules.filter(rule => + rule.pseudoElement === ":before"), + afterRules: elementStyle.rules.filter(rule => + rule.pseudoElement === ":after"), + }; + + is(rules.elementRules.length, ruleNbs.elementRulesNb, + selector + " has the correct number of non pseudo element rules"); + is(rules.firstLineRules.length, ruleNbs.firstLineRulesNb, + selector + " has the correct number of :first-line rules"); + is(rules.firstLetterRules.length, ruleNbs.firstLetterRulesNb, + selector + " has the correct number of :first-letter rules"); + is(rules.selectionRules.length, ruleNbs.selectionRulesNb, + selector + " has the correct number of :selection rules"); + is(rules.beforeRules.length, ruleNbs.beforeRulesNb, + selector + " has the correct number of :before rules"); + is(rules.afterRules.length, ruleNbs.afterRulesNb, + selector + " has the correct number of :after rules"); + + return rules; +} + +function getGutters(view) { + return view.element.querySelectorAll(".theme-gutter"); +} + +function assertGutters(view) { + let gutters = getGutters(view); + + is(gutters.length, 3, + "There are 3 gutter headings"); + is(gutters[0].textContent, "Pseudo-elements", + "Gutter heading is correct"); + is(gutters[1].textContent, "This Element", + "Gutter heading is correct"); + is(gutters[2].textContent, "Inherited from body", + "Gutter heading is correct"); + + return gutters; +} diff --git a/devtools/client/inspector/rules/test/browser_rules_pseudo-element_02.js b/devtools/client/inspector/rules/test/browser_rules_pseudo-element_02.js new file mode 100644 index 000000000..f69c328db --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_pseudo-element_02.js @@ -0,0 +1,29 @@ +/* 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 pseudoelements are displayed correctly in the markup view. + +const TEST_URI = URL_ROOT + "doc_pseudoelement.html"; + +add_task(function* () { + yield addTab(TEST_URI); + let {inspector} = yield openRuleView(); + + let node = yield getNodeFront("#topleft", inspector); + let children = yield inspector.markup.walker.children(node); + + is(children.nodes.length, 3, "Element has correct number of children"); + + let beforeElement = children.nodes[0]; + is(beforeElement.tagName, "_moz_generated_content_before", + "tag name is correct"); + yield selectNode(beforeElement, inspector); + + let afterElement = children.nodes[children.nodes.length - 1]; + is(afterElement.tagName, "_moz_generated_content_after", + "tag name is correct"); + yield selectNode(afterElement, inspector); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_pseudo_lock_options.js b/devtools/client/inspector/rules/test/browser_rules_pseudo_lock_options.js new file mode 100644 index 000000000..d795ba5f3 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_pseudo_lock_options.js @@ -0,0 +1,131 @@ +/* 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"; + +// Tests that the rule view pseudo lock options work properly. + +const TEST_URI = ` + <style type='text/css'> + div { + color: red; + } + div:hover { + color: blue; + } + div:active { + color: yellow; + } + div:focus { + color: green; + } + </style> + <div>test 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 assertPseudoPanelClosed(view); + + info("Toggle the pseudo class panel open"); + view.pseudoClassToggle.click(); + yield assertPseudoPanelOpened(view); + + info("Toggle each pseudo lock and check that the pseudo lock is added"); + yield togglePseudoClass(inspector, view.hoverCheckbox); + yield assertPseudoAdded(inspector, view, ":hover", 3, 1); + yield togglePseudoClass(inspector, view.hoverCheckbox); + yield assertPseudoRemoved(inspector, view, 2); + + yield togglePseudoClass(inspector, view.activeCheckbox); + yield assertPseudoAdded(inspector, view, ":active", 3, 1); + yield togglePseudoClass(inspector, view.activeCheckbox); + yield assertPseudoRemoved(inspector, view, 2); + + yield togglePseudoClass(inspector, view.focusCheckbox); + yield assertPseudoAdded(inspector, view, ":focus", 3, 1); + yield togglePseudoClass(inspector, view.focusCheckbox); + yield assertPseudoRemoved(inspector, view, 2); + + info("Toggle all pseudo lock and check that the pseudo lock is added"); + yield togglePseudoClass(inspector, view.hoverCheckbox); + yield togglePseudoClass(inspector, view.activeCheckbox); + yield togglePseudoClass(inspector, view.focusCheckbox); + yield assertPseudoAdded(inspector, view, ":focus", 5, 1); + yield assertPseudoAdded(inspector, view, ":active", 5, 2); + yield assertPseudoAdded(inspector, view, ":hover", 5, 3); + yield togglePseudoClass(inspector, view.hoverCheckbox); + yield togglePseudoClass(inspector, view.activeCheckbox); + yield togglePseudoClass(inspector, view.focusCheckbox); + yield assertPseudoRemoved(inspector, view, 2); + + info("Select a null element"); + yield view.selectElement(null); + ok(!view.hoverCheckbox.checked && view.hoverCheckbox.disabled, + ":hover checkbox is unchecked and disabled"); + ok(!view.activeCheckbox.checked && view.activeCheckbox.disabled, + ":active checkbox is unchecked and disabled"); + ok(!view.focusCheckbox.checked && view.focusCheckbox.disabled, + ":focus checkbox is unchecked and disabled"); + + info("Toggle the pseudo class panel close"); + view.pseudoClassToggle.click(); + yield assertPseudoPanelClosed(view); +}); + +function* togglePseudoClass(inspector, pseudoClassOption) { + info("Toggle the pseudoclass, wait for it to be applied"); + let onRefresh = inspector.once("rule-view-refreshed"); + pseudoClassOption.click(); + yield onRefresh; +} + +function* assertPseudoAdded(inspector, view, pseudoClass, numRules, + childIndex) { + info("Check that the ruleview contains the pseudo-class rule"); + is(view.element.children.length, numRules, + "Should have " + numRules + " rules."); + is(getRuleViewRuleEditor(view, childIndex).rule.selectorText, + "div" + pseudoClass, "rule view is showing " + pseudoClass + " rule"); +} + +function* assertPseudoRemoved(inspector, view, numRules) { + info("Check that the ruleview no longer contains the pseudo-class rule"); + is(view.element.children.length, numRules, + "Should have " + numRules + " rules."); + is(getRuleViewRuleEditor(view, 1).rule.selectorText, "div", + "Second rule is div"); +} + +function* assertPseudoPanelOpened(view) { + info("Check the opened state of the pseudo class panel"); + + ok(!view.pseudoClassPanel.hidden, "Pseudo Class Panel Opened"); + ok(!view.hoverCheckbox.disabled, ":hover checkbox is not disabled"); + ok(!view.activeCheckbox.disabled, ":active checkbox is not disabled"); + ok(!view.focusCheckbox.disabled, ":focus checkbox is not disabled"); + + is(view.hoverCheckbox.getAttribute("tabindex"), "0", + ":hover checkbox has a tabindex of 0"); + is(view.activeCheckbox.getAttribute("tabindex"), "0", + ":active checkbox has a tabindex of 0"); + is(view.focusCheckbox.getAttribute("tabindex"), "0", + ":focus checkbox has a tabindex of 0"); +} + +function* assertPseudoPanelClosed(view) { + info("Check the closed state of the pseudo clas panel"); + + ok(view.pseudoClassPanel.hidden, "Pseudo Class Panel Hidden"); + + is(view.hoverCheckbox.getAttribute("tabindex"), "-1", + ":hover checkbox has a tabindex of -1"); + is(view.activeCheckbox.getAttribute("tabindex"), "-1", + ":active checkbox has a tabindex of -1"); + is(view.focusCheckbox.getAttribute("tabindex"), "-1", + ":focus checkbox has a tabindex of -1"); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_refresh-no-flicker.js b/devtools/client/inspector/rules/test/browser_rules_refresh-no-flicker.js new file mode 100644 index 000000000..25ea3d972 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_refresh-no-flicker.js @@ -0,0 +1,39 @@ +/* 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 rule view does not go blank while selecting a new node. + +const TESTCASE_URI = "data:text/html;charset=utf-8," + + "<div id=\"testdiv\" style=\"font-size:10px;\">" + + "Test div!</div>"; + +add_task(function* () { + yield addTab(TESTCASE_URI); + + info("Opening the rule view and selecting the test node"); + let {inspector, view} = yield openRuleView(); + let testdiv = yield getNodeFront("#testdiv", inspector); + yield selectNode(testdiv, inspector); + + let htmlBefore = view.element.innerHTML; + ok(htmlBefore.indexOf("font-size") > -1, + "The rule view should contain a font-size property."); + + // Do the selectNode call manually, because otherwise it's hard to guarantee + // that we can make the below checks at a reasonable time. + info("refreshing the node"); + let p = view.selectElement(testdiv, true); + is(view.element.innerHTML, htmlBefore, + "The rule view is unchanged during selection."); + ok(view.element.classList.contains("non-interactive"), + "The rule view is marked non-interactive."); + yield p; + + info("node refreshed"); + ok(!view.element.classList.contains("non-interactive"), + "The rule view is marked interactive again."); +}); + diff --git a/devtools/client/inspector/rules/test/browser_rules_refresh-on-attribute-change_01.js b/devtools/client/inspector/rules/test/browser_rules_refresh-on-attribute-change_01.js new file mode 100644 index 000000000..381a6bda2 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_refresh-on-attribute-change_01.js @@ -0,0 +1,61 @@ +/* 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 changing the current element's attributes refreshes the rule-view + +const TEST_URI = ` + <style type="text/css"> + #testid { + background-color: blue; + } + .testclass { + background-color: green; + } + </style> + <div id="testid" class="testclass" style="margin-top: 1px; padding-top: 5px;"> + Styled Node + </div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view, testActor} = yield openRuleView(); + yield selectNode("#testid", inspector); + + info("Checking that the rule-view has the element, #testid and " + + ".testclass selectors"); + checkRuleViewContent(view, ["element", "#testid", ".testclass"]); + + info("Changing the node's ID attribute and waiting for the " + + "rule-view refresh"); + let ruleViewRefreshed = inspector.once("rule-view-refreshed"); + yield testActor.setAttribute("#testid", "id", "differentid"); + yield ruleViewRefreshed; + + info("Checking that the rule-view doesn't have the #testid selector anymore"); + checkRuleViewContent(view, ["element", ".testclass"]); + + info("Reverting the ID attribute change"); + ruleViewRefreshed = inspector.once("rule-view-refreshed"); + yield testActor.setAttribute("#differentid", "id", "testid"); + yield ruleViewRefreshed; + + info("Checking that the rule-view has all the selectors again"); + checkRuleViewContent(view, ["element", "#testid", ".testclass"]); +}); + +function checkRuleViewContent(view, expectedSelectors) { + let selectors = view.styleDocument + .querySelectorAll(".ruleview-selectorcontainer"); + + is(selectors.length, expectedSelectors.length, + expectedSelectors.length + " selectors are displayed"); + + for (let i = 0; i < expectedSelectors.length; i++) { + is(selectors[i].textContent.indexOf(expectedSelectors[i]), 0, + "Selector " + (i + 1) + " is " + expectedSelectors[i]); + } +} diff --git a/devtools/client/inspector/rules/test/browser_rules_refresh-on-attribute-change_02.js b/devtools/client/inspector/rules/test/browser_rules_refresh-on-attribute-change_02.js new file mode 100644 index 000000000..6ee385faa --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_refresh-on-attribute-change_02.js @@ -0,0 +1,153 @@ +/* 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 changing the current element's style attribute refreshes the +// rule-view + +const TEST_URI = ` + <div id="testid" class="testclass" style="margin-top: 1px; padding-top: 5px;"> + Styled Node + </div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view, testActor} = yield openRuleView(); + yield selectNode("#testid", inspector); + + yield testPropertyChanges(inspector, view); + yield testPropertyChange0(inspector, view, "#testid", testActor); + yield testPropertyChange1(inspector, view, "#testid", testActor); + yield testPropertyChange2(inspector, view, "#testid", testActor); + yield testPropertyChange3(inspector, view, "#testid", testActor); + yield testPropertyChange4(inspector, view, "#testid", testActor); + yield testPropertyChange5(inspector, view, "#testid", testActor); + yield testPropertyChange6(inspector, view, "#testid", testActor); +}); + +function* testPropertyChanges(inspector, ruleView) { + info("Adding a second margin-top value in the element selector"); + let ruleEditor = ruleView._elementStyle.rules[0].editor; + let onRefreshed = inspector.once("rule-view-refreshed"); + ruleEditor.addProperty("margin-top", "5px", "", true); + yield onRefreshed; + + let rule = ruleView._elementStyle.rules[0]; + validateTextProp(rule.textProps[0], false, "margin-top", "1px", + "Original margin property active"); +} + +function* testPropertyChange0(inspector, ruleView, selector, testActor) { + yield changeElementStyle(selector, "margin-top: 1px; padding-top: 5px", + inspector, testActor); + + let rule = ruleView._elementStyle.rules[0]; + is(rule.editor.element.querySelectorAll(".ruleview-property").length, 3, + "Correct number of properties"); + validateTextProp(rule.textProps[0], true, "margin-top", "1px", + "First margin property re-enabled"); + validateTextProp(rule.textProps[2], false, "margin-top", "5px", + "Second margin property disabled"); +} + +function* testPropertyChange1(inspector, ruleView, selector, testActor) { + info("Now set it back to 5px, the 5px value should be re-enabled."); + yield changeElementStyle(selector, "margin-top: 5px; padding-top: 5px;", + inspector, testActor); + + let rule = ruleView._elementStyle.rules[0]; + is(rule.editor.element.querySelectorAll(".ruleview-property").length, 3, + "Correct number of properties"); + validateTextProp(rule.textProps[0], false, "margin-top", "1px", + "First margin property re-enabled"); + validateTextProp(rule.textProps[2], true, "margin-top", "5px", + "Second margin property disabled"); +} + +function* testPropertyChange2(inspector, ruleView, selector, testActor) { + info("Set the margin property to a value that doesn't exist in the editor."); + info("Should reuse the currently-enabled element (the second one.)"); + yield changeElementStyle(selector, "margin-top: 15px; padding-top: 5px;", + inspector, testActor); + + let rule = ruleView._elementStyle.rules[0]; + is(rule.editor.element.querySelectorAll(".ruleview-property").length, 3, + "Correct number of properties"); + validateTextProp(rule.textProps[0], false, "margin-top", "1px", + "First margin property re-enabled"); + validateTextProp(rule.textProps[2], true, "margin-top", "15px", + "Second margin property disabled"); +} + +function* testPropertyChange3(inspector, ruleView, selector, testActor) { + info("Remove the padding-top attribute. Should disable the padding " + + "property but not remove it."); + yield changeElementStyle(selector, "margin-top: 5px;", inspector, testActor); + + let rule = ruleView._elementStyle.rules[0]; + is(rule.editor.element.querySelectorAll(".ruleview-property").length, 3, + "Correct number of properties"); + validateTextProp(rule.textProps[1], false, "padding-top", "5px", + "Padding property disabled"); +} + +function* testPropertyChange4(inspector, ruleView, selector, testActor) { + info("Put the padding-top attribute back in, should re-enable the " + + "padding property."); + yield changeElementStyle(selector, "margin-top: 5px; padding-top: 25px", + inspector, testActor); + + let rule = ruleView._elementStyle.rules[0]; + is(rule.editor.element.querySelectorAll(".ruleview-property").length, 3, + "Correct number of properties"); + validateTextProp(rule.textProps[1], true, "padding-top", "25px", + "Padding property enabled"); +} + +function* testPropertyChange5(inspector, ruleView, selector, testActor) { + info("Add an entirely new property"); + yield changeElementStyle(selector, + "margin-top: 5px; padding-top: 25px; padding-left: 20px;", + inspector, testActor); + + let rule = ruleView._elementStyle.rules[0]; + is(rule.editor.element.querySelectorAll(".ruleview-property").length, 4, + "Added a property"); + validateTextProp(rule.textProps[3], true, "padding-left", "20px", + "Padding property enabled"); +} + +function* testPropertyChange6(inspector, ruleView, selector, testActor) { + info("Add an entirely new property again"); + yield changeElementStyle(selector, "background: red " + + "url(\"chrome://branding/content/about-logo.png\") repeat scroll 0% 0%", + inspector, testActor); + + let rule = ruleView._elementStyle.rules[0]; + is(rule.editor.element.querySelectorAll(".ruleview-property").length, 5, + "Added a property"); + validateTextProp(rule.textProps[4], true, "background", + "red url(\"chrome://branding/content/about-logo.png\") repeat scroll 0% 0%", + "shortcut property correctly set"); +} + +function* changeElementStyle(selector, style, inspector, testActor) { + let onRefreshed = inspector.once("rule-view-refreshed"); + yield testActor.setAttribute(selector, "style", style); + yield onRefreshed; +} + +function validateTextProp(prop, enabled, name, value, desc) { + is(prop.enabled, enabled, desc + ": enabled."); + is(prop.name, name, desc + ": name."); + is(prop.value, value, desc + ": value."); + + is(prop.editor.enable.hasAttribute("checked"), enabled, + desc + ": enabled checkbox."); + is(prop.editor.nameSpan.textContent, name, desc + ": name span."); + is(prop.editor.valueSpan.textContent, + value, desc + ": value span."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_refresh-on-style-change.js b/devtools/client/inspector/rules/test/browser_rules_refresh-on-style-change.js new file mode 100644 index 000000000..81ff9d4d5 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_refresh-on-style-change.js @@ -0,0 +1,38 @@ +/* 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 rule view refreshes when the current node has its style +// changed + +const TEST_URI = "<div id='testdiv' style='font-size: 10px;''>Test div!</div>"; + +add_task(function* () { + Services.prefs.setCharPref("devtools.defaultColorUnit", "name"); + + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view, testActor} = yield openRuleView(); + yield selectNode("#testdiv", inspector); + + let fontSize = getRuleViewPropertyValue(view, "element", "font-size"); + is(fontSize, "10px", "The rule view shows the right font-size"); + + info("Changing the node's style and waiting for the update"); + let onUpdated = inspector.once("rule-view-refreshed"); + yield testActor.setAttribute("#testdiv", "style", + "font-size: 3em; color: lightgoldenrodyellow; " + + "text-align: right; text-transform: uppercase"); + yield onUpdated; + + let textAlign = getRuleViewPropertyValue(view, "element", "text-align"); + is(textAlign, "right", "The rule view shows the new text align."); + let color = getRuleViewPropertyValue(view, "element", "color"); + is(color, "lightgoldenrodyellow", "The rule view shows the new color."); + fontSize = getRuleViewPropertyValue(view, "element", "font-size"); + is(fontSize, "3em", "The rule view shows the new font size."); + let textTransform = getRuleViewPropertyValue(view, "element", + "text-transform"); + is(textTransform, "uppercase", "The rule view shows the new text transform."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_01.js b/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_01.js new file mode 100644 index 000000000..f4c47bba0 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_01.js @@ -0,0 +1,156 @@ +/* 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"; + +// Tests that the rule view search filter and clear button works properly in +// the computed list. + +const TEST_URI = ` + <style type="text/css"> + #testid { + margin: 4px 0px; + } + .testclass { + background-color: red; + } + </style> + <h1 id="testid" class="testclass">Styled Node</h1> +`; + +const TEST_DATA = [ + { + desc: "Tests that the search filter works properly in the computed list " + + "for property names", + search: "margin", + isExpanderOpen: false, + isFilterOpen: false, + isMarginHighlighted: true, + isMarginTopHighlighted: true, + isMarginRightHighlighted: true, + isMarginBottomHighlighted: true, + isMarginLeftHighlighted: true + }, + { + desc: "Tests that the search filter works properly in the computed list " + + "for property values", + search: "0px", + isExpanderOpen: false, + isFilterOpen: false, + isMarginHighlighted: true, + isMarginTopHighlighted: false, + isMarginRightHighlighted: true, + isMarginBottomHighlighted: false, + isMarginLeftHighlighted: true + }, + { + desc: "Tests that the search filter works properly in the computed list " + + "for property line input", + search: "margin-top:4px", + isExpanderOpen: true, + isFilterOpen: true, + isMarginHighlighted: false, + isMarginTopHighlighted: true, + isMarginRightHighlighted: false, + isMarginBottomHighlighted: false, + isMarginLeftHighlighted: false + }, + { + desc: "Tests that the search filter works properly in the computed list " + + "for parsed name", + search: "margin-top:", + isExpanderOpen: true, + isFilterOpen: true, + isMarginHighlighted: false, + isMarginTopHighlighted: true, + isMarginRightHighlighted: false, + isMarginBottomHighlighted: false, + isMarginLeftHighlighted: false + }, + { + desc: "Tests that the search filter works properly in the computed list " + + "for parsed property value", + search: ":4px", + isExpanderOpen: false, + isFilterOpen: false, + isMarginHighlighted: true, + isMarginTopHighlighted: true, + isMarginRightHighlighted: false, + isMarginBottomHighlighted: true, + isMarginLeftHighlighted: false + } +]; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + yield testAddTextInFilter(inspector, view); +}); + +function* testAddTextInFilter(inspector, view) { + for (let data of TEST_DATA) { + info(data.desc); + yield setSearchFilter(view, data.search); + yield checkRules(view, data); + yield clearSearchAndCheckRules(view); + } +} + +function* checkRules(view, data) { + info("Check that the correct rules are visible"); + is(view.element.children.length, 2, "Should have 2 rules."); + is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element", + "First rule is inline element."); + + let rule = getRuleViewRuleEditor(view, 1).rule; + let textPropEditor = rule.textProps[0].editor; + let computed = textPropEditor.computed; + + is(rule.selectorText, "#testid", "Second rule is #testid."); + is(!!textPropEditor.expander.getAttribute("open"), data.isExpanderOpen, + "Got correct expander state."); + is(computed.hasAttribute("filter-open"), data.isFilterOpen, + "Got correct expanded state for margin computed list."); + is(textPropEditor.container.classList.contains("ruleview-highlight"), + data.isMarginHighlighted, + "Got correct highlight for margin text property."); + + is(computed.children[0].classList.contains("ruleview-highlight"), + data.isMarginTopHighlighted, + "Got correct highlight for margin-top computed property."); + is(computed.children[1].classList.contains("ruleview-highlight"), + data.isMarginRightHighlighted, + "Got correct highlight for margin-right computed property."); + is(computed.children[2].classList.contains("ruleview-highlight"), + data.isMarginBottomHighlighted, + "Got correct highlight for margin-bottom computed property."); + is(computed.children[3].classList.contains("ruleview-highlight"), + data.isMarginLeftHighlighted, + "Got correct highlight for margin-left computed property."); +} + +function* clearSearchAndCheckRules(view) { + let win = view.styleWindow; + let searchField = view.searchField; + let searchClearButton = view.searchClearButton; + + let rule = getRuleViewRuleEditor(view, 1).rule; + let textPropEditor = rule.textProps[0].editor; + let computed = textPropEditor.computed; + + info("Clearing the search filter"); + EventUtils.synthesizeMouseAtCenter(searchClearButton, {}, win); + yield view.inspector.once("ruleview-filtered"); + + info("Check the search filter is cleared and no rules are highlighted"); + is(view.element.children.length, 3, "Should have 3 rules."); + ok(!searchField.value, "Search filter is cleared"); + ok(!view.styleDocument.querySelectorAll(".ruleview-highlight").length, + "No rules are higlighted"); + + ok(!textPropEditor.expander.getAttribute("open"), "Expander is closed."); + ok(!computed.hasAttribute("filter-open"), + "margin computed list is closed."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_02.js b/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_02.js new file mode 100644 index 000000000..911f09ff3 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_02.js @@ -0,0 +1,93 @@ +/* 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"; + +// Tests that the rule view search filter works properly in the computed list +// when modifying the existing search filter value + +const SEARCH = "margin-"; + +const TEST_URI = ` + <style type="text/css"> + #testid { + margin: 4px 0px; + } + .testclass { + background-color: red; + } + </style> + <h1 id="testid" class="testclass">Styled Node</h1> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + yield testAddTextInFilter(inspector, view); + yield testRemoveTextInFilter(inspector, view); +}); + +function* testAddTextInFilter(inspector, view) { + yield setSearchFilter(view, SEARCH); + + info("Check that the correct rules are visible"); + is(view.element.children.length, 2, "Should have 2 rules."); + is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element", + "First rule is inline element."); + + let rule = getRuleViewRuleEditor(view, 1).rule; + let ruleEditor = rule.textProps[0].editor; + let computed = ruleEditor.computed; + + is(rule.selectorText, "#testid", "Second rule is #testid."); + ok(ruleEditor.expander.getAttribute("open"), "Expander is open."); + ok(!ruleEditor.container.classList.contains("ruleview-highlight"), + "margin text property is not highlighted."); + ok(computed.hasAttribute("filter-open"), "margin computed list is open."); + + ok(computed.children[0].classList.contains("ruleview-highlight"), + "margin-top computed property is correctly highlighted."); + ok(computed.children[1].classList.contains("ruleview-highlight"), + "margin-right computed property is correctly highlighted."); + ok(computed.children[2].classList.contains("ruleview-highlight"), + "margin-bottom computed property is correctly highlighted."); + ok(computed.children[3].classList.contains("ruleview-highlight"), + "margin-left computed property is correctly highlighted."); +} + +function* testRemoveTextInFilter(inspector, view) { + info("Press backspace and set filter text to \"margin\""); + + let win = view.styleWindow; + let searchField = view.searchField; + + searchField.focus(); + EventUtils.synthesizeKey("VK_BACK_SPACE", {}, win); + yield inspector.once("ruleview-filtered"); + + info("Check that the correct rules are visible"); + is(view.element.children.length, 2, "Should have 2 rules."); + is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element", + "First rule is inline element."); + + let rule = getRuleViewRuleEditor(view, 1).rule; + let ruleEditor = rule.textProps[0].editor; + let computed = ruleEditor.computed; + + is(rule.selectorText, "#testid", "Second rule is #testid."); + ok(!ruleEditor.expander.getAttribute("open"), "Expander is closed."); + ok(ruleEditor.container.classList.contains("ruleview-highlight"), + "margin text property is correctly highlighted."); + ok(!computed.hasAttribute("filter-open"), "margin computed list is closed."); + + ok(computed.children[0].classList.contains("ruleview-highlight"), + "margin-top computed property is correctly highlighted."); + ok(computed.children[1].classList.contains("ruleview-highlight"), + "margin-right computed property is correctly highlighted."); + ok(computed.children[2].classList.contains("ruleview-highlight"), + "margin-bottom computed property is correctly highlighted."); + ok(computed.children[3].classList.contains("ruleview-highlight"), + "margin-left computed property is correctly highlighted."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_03.js b/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_03.js new file mode 100644 index 000000000..1d8063419 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_03.js @@ -0,0 +1,49 @@ +/* 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"; + +// Tests that the rule view search filter works properly in the computed list +// for color values. + +// The color format here is chosen to match the default returned by +// CssColor.toString. +const SEARCH = "background-color: rgb(243, 243, 243)"; + +const TEST_URI = ` + <style type="text/css"> + .testclass { + background: rgb(243, 243, 243) none repeat scroll 0% 0%; + } + </style> + <div class="testclass">Styled Node</h1> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode(".testclass", inspector); + yield testAddTextInFilter(inspector, view); +}); + +function* testAddTextInFilter(inspector, view) { + yield setSearchFilter(view, SEARCH); + + info("Check that the correct rules are visible"); + is(view.element.children.length, 2, "Should have 2 rules."); + is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element", + "First rule is inline element."); + + let rule = getRuleViewRuleEditor(view, 1).rule; + let ruleEditor = rule.textProps[0].editor; + let computed = ruleEditor.computed; + + is(rule.selectorText, ".testclass", "Second rule is .testclass."); + ok(ruleEditor.expander.getAttribute("open"), "Expander is open."); + ok(!ruleEditor.container.classList.contains("ruleview-highlight"), + "background property is not highlighted."); + ok(computed.hasAttribute("filter-open"), "background computed list is open."); + ok(computed.children[0].classList.contains("ruleview-highlight"), + "background-color computed property is highlighted."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_04.js b/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_04.js new file mode 100644 index 000000000..05b8b01eb --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_04.js @@ -0,0 +1,63 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that the rule view search filter works properly in the computed list +// for newly modified property values. + +const SEARCH = "0px"; + +const TEST_URI = ` + <style type='text/css'> + #testid { + margin: 4px; + top: 0px; + } + </style> + <h1 id='testid'>Styled Node</h1> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + yield testModifyPropertyValueFilter(inspector, view); +}); + +function* testModifyPropertyValueFilter(inspector, view) { + yield setSearchFilter(view, SEARCH); + + let rule = getRuleViewRuleEditor(view, 1).rule; + let propEditor = rule.textProps[0].editor; + let computed = propEditor.computed; + let editor = yield focusEditableField(view, propEditor.valueSpan); + + info("Check that the correct rules are visible"); + is(rule.selectorText, "#testid", "Second rule is #testid."); + ok(!propEditor.container.classList.contains("ruleview-highlight"), + "margin text property is not highlighted."); + ok(rule.textProps[1].editor.container.classList + .contains("ruleview-highlight"), + "top text property is correctly highlighted."); + + let onBlur = once(editor.input, "blur"); + let onModification = view.once("ruleview-changed"); + EventUtils.sendString("4px 0px", view.styleWindow); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onBlur; + yield onModification; + + ok(propEditor.container.classList.contains("ruleview-highlight"), + "margin text property is correctly highlighted."); + ok(!computed.hasAttribute("filter-open"), "margin computed list is closed."); + ok(!computed.children[0].classList.contains("ruleview-highlight"), + "margin-top computed property is not highlighted."); + ok(computed.children[1].classList.contains("ruleview-highlight"), + "margin-right computed property is correctly highlighted."); + ok(!computed.children[2].classList.contains("ruleview-highlight"), + "margin-bottom computed property is not highlighted."); + ok(computed.children[3].classList.contains("ruleview-highlight"), + "margin-left computed property is correctly highlighted."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_expander.js b/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_expander.js new file mode 100644 index 000000000..c8b1e0869 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_search-filter-computed-list_expander.js @@ -0,0 +1,92 @@ +/* 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"; + +// Tests that the expanded computed list for a property remains open after +// clearing the rule view search filter. + +const SEARCH = "0px"; + +const TEST_URI = ` + <style type="text/css"> + #testid { + margin: 4px 0px; + } + .testclass { + background-color: red; + } + </style> + <h1 id="testid" class="testclass">Styled Node</h1> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + yield testOpenExpanderAndAddTextInFilter(inspector, view); + yield testClearSearchFilter(inspector, view); +}); + +function* testOpenExpanderAndAddTextInFilter(inspector, view) { + let rule = getRuleViewRuleEditor(view, 1).rule; + let ruleEditor = rule.textProps[0].editor; + let computed = ruleEditor.computed; + + info("Opening the computed list of margin property"); + ruleEditor.expander.click(); + + yield setSearchFilter(view, SEARCH); + + info("Check that the correct rules are visible"); + is(view.element.children.length, 2, "Should have 2 rules."); + is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element", + "First rule is inline element."); + + is(rule.selectorText, "#testid", "Second rule is #testid."); + ok(ruleEditor.expander.getAttribute("open"), "Expander is open."); + ok(ruleEditor.container.classList.contains("ruleview-highlight"), + "margin text property is correctly highlighted."); + ok(!computed.hasAttribute("filter-open"), + "margin computed list does not contain filter-open class."); + ok(computed.hasAttribute("user-open"), + "margin computed list contains user-open attribute."); + + ok(!computed.children[0].classList.contains("ruleview-highlight"), + "margin-top computed property is not highlighted."); + ok(computed.children[1].classList.contains("ruleview-highlight"), + "margin-right computed property is correctly highlighted."); + ok(!computed.children[2].classList.contains("ruleview-highlight"), + "margin-bottom computed property is not highlighted."); + ok(computed.children[3].classList.contains("ruleview-highlight"), + "margin-left computed property is correctly highlighted."); +} + +function* testClearSearchFilter(inspector, view) { + info("Clearing the search filter"); + + let searchField = view.searchField; + let searchClearButton = view.searchClearButton; + let onRuleViewFiltered = inspector.once("ruleview-filtered"); + + EventUtils.synthesizeMouseAtCenter(searchClearButton, {}, + view.styleWindow); + + yield onRuleViewFiltered; + + info("Check the search filter is cleared and no rules are highlighted"); + is(view.element.children.length, 3, "Should have 3 rules."); + ok(!searchField.value, "Search filter is cleared"); + ok(!view.styleDocument.querySelectorAll(".ruleview-highlight").length, + "No rules are higlighted"); + + let ruleEditor = getRuleViewRuleEditor(view, 1).rule.textProps[0].editor; + let computed = ruleEditor.computed; + + ok(ruleEditor.expander.getAttribute("open"), "Expander is open."); + ok(!computed.hasAttribute("filter-open"), + "margin computed list does not contain filter-open class."); + ok(computed.hasAttribute("user-open"), + "margin computed list contains user-open attribute."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter-overridden-property.js b/devtools/client/inspector/rules/test/browser_rules_search-filter-overridden-property.js new file mode 100644 index 000000000..3e634b76e --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_search-filter-overridden-property.js @@ -0,0 +1,74 @@ +/* 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"; + +// Tests that the rule view overriden search filter works properly for +// overridden properties. + +const TEST_URI = ` + <style type='text/css'> + #testid { + width: 100%; + } + h1 { + width: 50%; + } + </style> + <h1 id='testid' class='testclass'>Styled Node</h1> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + yield testFilterOverriddenProperty(inspector, view); +}); + +function* testFilterOverriddenProperty(inspector, ruleView) { + info("Check that the correct rules are visible"); + is(ruleView.element.children.length, 3, "Should have 3 rules."); + + let rule = getRuleViewRuleEditor(ruleView, 1).rule; + let textPropEditor = rule.textProps[0].editor; + is(rule.selectorText, "#testid", "Second rule is #testid."); + ok(!textPropEditor.element.classList.contains("ruleview-overridden"), + "width property is not overridden."); + ok(textPropEditor.filterProperty.hidden, + "Overridden search button is hidden."); + + rule = getRuleViewRuleEditor(ruleView, 2).rule; + textPropEditor = rule.textProps[0].editor; + is(rule.selectorText, "h1", "Third rule is h1."); + ok(textPropEditor.element.classList.contains("ruleview-overridden"), + "width property is overridden."); + ok(!textPropEditor.filterProperty.hidden, + "Overridden search button is not hidden."); + + let searchField = ruleView.searchField; + let onRuleViewFiltered = inspector.once("ruleview-filtered"); + + info("Click the overridden search"); + textPropEditor.filterProperty.click(); + yield onRuleViewFiltered; + + info("Check that the overridden search is applied"); + is(searchField.value, "`width`", "The search field value is width."); + + rule = getRuleViewRuleEditor(ruleView, 1).rule; + textPropEditor = rule.textProps[0].editor; + is(rule.selectorText, "#testid", "Second rule is #testid."); + ok(textPropEditor.container.classList.contains("ruleview-highlight"), + "width property is correctly highlighted."); + + rule = getRuleViewRuleEditor(ruleView, 2).rule; + textPropEditor = rule.textProps[0].editor; + is(rule.selectorText, "h1", "Third rule is h1."); + ok(textPropEditor.container.classList.contains("ruleview-highlight"), + "width property is correctly highlighted."); + ok(textPropEditor.element.classList.contains("ruleview-overridden"), + "width property is overridden."); + ok(!textPropEditor.filterProperty.hidden, + "Overridden search button is not hidden."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_01.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_01.js new file mode 100644 index 000000000..4dd1c951d --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_01.js @@ -0,0 +1,91 @@ +/* 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"; + +// Tests that the rule view search filter and clear button works properly. + +const TEST_URI = ` + <style type="text/css"> + #testid, h1 { + background-color: #00F !important; + } + .testclass { + width: 100%; + } + </style> + <h1 id="testid" class="testclass">Styled Node</h1> +`; + +const TEST_DATA = [ + { + desc: "Tests that the search filter works properly for property names", + search: "color" + }, + { + desc: "Tests that the search filter works properly for property values", + search: "00F" + }, + { + desc: "Tests that the search filter works properly for property line input", + search: "background-color:#00F" + }, + { + desc: "Tests that the search filter works properly for parsed property " + + "names", + search: "background:" + }, + { + desc: "Tests that the search filter works properly for parsed property " + + "values", + search: ":00F" + }, +]; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + yield testAddTextInFilter(inspector, view); +}); + +function* testAddTextInFilter(inspector, view) { + for (let data of TEST_DATA) { + info(data.desc); + yield setSearchFilter(view, data.search); + yield checkRules(view); + yield clearSearchAndCheckRules(view); + } +} + +function* checkRules(view) { + info("Check that the correct rules are visible"); + is(view.element.children.length, 2, "Should have 2 rules."); + is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element", + "First rule is inline element."); + + let rule = getRuleViewRuleEditor(view, 1).rule; + + is(rule.selectorText, "#testid, h1", "Second rule is #testid, h1."); + ok(rule.textProps[0].editor.container.classList + .contains("ruleview-highlight"), + "background-color text property is correctly highlighted."); +} + +function* clearSearchAndCheckRules(view) { + let doc = view.styleDocument; + let win = view.styleWindow; + let searchField = view.searchField; + let searchClearButton = view.searchClearButton; + + info("Clearing the search filter"); + EventUtils.synthesizeMouseAtCenter(searchClearButton, {}, win); + yield view.inspector.once("ruleview-filtered"); + + info("Check the search filter is cleared and no rules are highlighted"); + is(view.element.children.length, 3, "Should have 3 rules."); + ok(!searchField.value, "Search filter is cleared."); + ok(!doc.querySelectorAll(".ruleview-highlight").length, + "No rules are higlighted."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_02.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_02.js new file mode 100644 index 000000000..c23e7be62 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_02.js @@ -0,0 +1,32 @@ +/* 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"; + +// Tests that the rule view search filter works properly for keyframe rule +// selectors. + +const SEARCH = "20%"; +const TEST_URI = URL_ROOT + "doc_keyframeanimation.html"; + +add_task(function* () { + yield addTab(TEST_URI); + let {inspector, view} = yield openRuleView(); + yield selectNode("#boxy", inspector); + yield testAddTextInFilter(inspector, view); +}); + +function* testAddTextInFilter(inspector, view) { + yield setSearchFilter(view, SEARCH); + + info("Check that the correct rules are visible"); + is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element", + "First rule is inline element."); + + let ruleEditor = getRuleViewRuleEditor(view, 2, 0); + + is(ruleEditor.rule.domRule.keyText, "20%", "Second rule is 20%."); + ok(ruleEditor.selectorText.classList.contains("ruleview-highlight"), + "20% selector is highlighted."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_03.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_03.js new file mode 100644 index 000000000..89280f0eb --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_03.js @@ -0,0 +1,39 @@ +/* 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"; + +// Tests that the rule view search filter works properly for inline styles. + +const SEARCH = "color"; + +const TEST_URI = ` + <style type="text/css"> + #testid { + width: 100%; + } + </style> + <div id="testid" style="background-color:aliceblue">Styled Node</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + yield testAddTextInFilter(inspector, view); +}); + +function* testAddTextInFilter(inspector, view) { + yield setSearchFilter(view, SEARCH); + + info("Check that the correct rules are visible"); + is(view.element.children.length, 1, "Should have 1 rule."); + + let rule = getRuleViewRuleEditor(view, 0).rule; + + is(rule.selectorText, "element", "First rule is inline element."); + ok(rule.textProps[0].editor.container.classList + .contains("ruleview-highlight"), + "background-color text property is correctly highlighted."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_04.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_04.js new file mode 100644 index 000000000..5804d74ac --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_04.js @@ -0,0 +1,76 @@ +/* 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"; + +// Tests that the rule view search filter works properly when modifying the +// existing search filter value. + +const SEARCH = "00F"; + +const TEST_URI = ` + <style type="text/css"> + #testid { + background-color: #00F; + } + .testclass { + width: 100%; + } + </style> + <div id="testid" class="testclass">Styled Node</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + yield testAddTextInFilter(inspector, view); + yield testRemoveTextInFilter(inspector, view); +}); + +function* testAddTextInFilter(inspector, view) { + yield setSearchFilter(view, SEARCH); + + info("Check that the correct rules are visible"); + is(view.element.children.length, 2, "Should have 2 rules."); + is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element", + "First rule is inline element."); + + let rule = getRuleViewRuleEditor(view, 1).rule; + + is(rule.selectorText, "#testid", "Second rule is #testid."); + ok(rule.textProps[0].editor.container.classList + .contains("ruleview-highlight"), + "background-color text property is correctly highlighted."); +} + +function* testRemoveTextInFilter(inspector, view) { + info("Press backspace and set filter text to \"00\""); + + let win = view.styleWindow; + let searchField = view.searchField; + + searchField.focus(); + EventUtils.synthesizeKey("VK_BACK_SPACE", {}, win); + yield inspector.once("ruleview-filtered"); + + info("Check that the correct rules are visible"); + is(view.element.children.length, 3, "Should have 3 rules."); + is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element", + "First rule is inline element."); + + let rule = getRuleViewRuleEditor(view, 1).rule; + + is(rule.selectorText, "#testid", "Second rule is #testid."); + ok(rule.textProps[0].editor.container.classList + .contains("ruleview-highlight"), + "background-color text property is correctly highlighted."); + + rule = getRuleViewRuleEditor(view, 2).rule; + + is(rule.selectorText, ".testclass", "Second rule is .testclass."); + ok(rule.textProps[0].editor.container.classList + .contains("ruleview-highlight"), + "width text property is correctly highlighted."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_05.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_05.js new file mode 100644 index 000000000..9388dd47e --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_05.js @@ -0,0 +1,33 @@ +/* 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"; + +// Tests that the rule view search filter works properly for stylesheet source. + +const SEARCH = "doc_urls_clickable.css"; +const TEST_URI = URL_ROOT + "doc_urls_clickable.html"; + +add_task(function* () { + yield addTab(TEST_URI); + let {inspector, view} = yield openRuleView(); + yield selectNode(".relative1", inspector); + yield testAddTextInFilter(inspector, view); +}); + +function* testAddTextInFilter(inspector, view) { + yield setSearchFilter(view, SEARCH); + + info("Check that the correct rules are visible"); + is(view.element.children.length, 2, "Should have 2 rules."); + is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element", + "First rule is inline element."); + + let rule = getRuleViewRuleEditor(view, 1).rule; + let source = rule.textProps[0].editor.ruleEditor.source; + + is(rule.selectorText, ".relative1", "Second rule is .relative1."); + ok(source.classList.contains("ruleview-highlight"), + "stylesheet source is correctly highlighted."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_06.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_06.js new file mode 100644 index 000000000..67b02ab73 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_06.js @@ -0,0 +1,27 @@ +/* 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"; + +// Tests that the rule view search filter does not highlight the source with +// input that could be parsed as a property line. + +const SEARCH = "doc_urls_clickable.css: url"; +const TEST_URI = URL_ROOT + "doc_urls_clickable.html"; + +add_task(function* () { + yield addTab(TEST_URI); + let {inspector, view} = yield openRuleView(); + yield selectNode(".relative1", inspector); + yield testAddTextInFilter(inspector, view); +}); + +function* testAddTextInFilter(inspector, view) { + yield setSearchFilter(view, SEARCH); + + info("Check that the correct rules are visible"); + is(view.element.children.length, 1, "Should have 1 rules."); + is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element", + "First rule is inline element."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_07.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_07.js new file mode 100644 index 000000000..16b047d8d --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_07.js @@ -0,0 +1,62 @@ +/* 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"; + +// Tests that the rule view search filter works properly for newly modified +// property name. + +const SEARCH = "e"; + +const TEST_URI = ` + <style type='text/css'> + #testid { + width: 100%; + height: 50%; + } + </style> + <h1 id='testid'>Styled Node</h1> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + + info("Enter the test value in the search filter"); + yield setSearchFilter(view, SEARCH); + + info("Focus the width property name"); + let ruleEditor = getRuleViewRuleEditor(view, 1); + let rule = ruleEditor.rule; + let propEditor = rule.textProps[0].editor; + yield focusEditableField(view, propEditor.nameSpan); + + info("Check that the correct rules are visible"); + is(view.element.children.length, 2, "Should have 2 rules."); + is(rule.selectorText, "#testid", "Second rule is #testid."); + ok(!propEditor.container.classList.contains("ruleview-highlight"), + "width text property is not highlighted."); + ok(rule.textProps[1].editor.container.classList + .contains("ruleview-highlight"), + "height text property is correctly highlighted."); + + info("Change the width property to margin-left"); + EventUtils.sendString("margin-left", view.styleWindow); + + info("Submit the change"); + let onRuleViewChanged = view.once("ruleview-changed"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewChanged; + + ok(propEditor.container.classList.contains("ruleview-highlight"), + "margin-left text property is correctly highlighted."); + + // After pressing return on the property name, the value has been focused + // automatically. Blur it now and wait for the rule-view to refresh to avoid + // pending requests. + onRuleViewChanged = view.once("ruleview-changed"); + EventUtils.synthesizeKey("VK_ESCAPE", {}); + yield onRuleViewChanged; +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_08.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_08.js new file mode 100644 index 000000000..1a3c0de59 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_08.js @@ -0,0 +1,53 @@ +/* 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"; + +// Tests that the rule view search filter works properly for newly modified +// property value. + +const SEARCH = "100%"; + +const TEST_URI = ` + <style type='text/css'> + #testid { + width: 100%; + height: 50%; + } + </style> + <h1 id='testid'>Styled Node</h1> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + + info("Enter the test value in the search filter"); + yield setSearchFilter(view, SEARCH); + + info("Focus the height property value"); + let ruleEditor = getRuleViewRuleEditor(view, 1); + let rule = ruleEditor.rule; + let propEditor = rule.textProps[1].editor; + yield focusEditableField(view, propEditor.valueSpan); + + info("Check that the correct rules are visible"); + is(view.element.children.length, 2, "Should have 2 rules."); + is(rule.selectorText, "#testid", "Second rule is #testid."); + ok(rule.textProps[0].editor.container.classList + .contains("ruleview-highlight"), + "width text property is correctly highlighted."); + ok(!propEditor.container.classList.contains("ruleview-highlight"), + "height text property is not highlighted."); + + info("Change the height property value to 100%"); + let onRuleViewChanged = view.once("ruleview-changed"); + EventUtils.sendString("100%", view.styleWindow); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewChanged; + + ok(propEditor.container.classList.contains("ruleview-highlight"), + "height text property is correctly highlighted."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_09.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_09.js new file mode 100644 index 000000000..620e5d336 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_09.js @@ -0,0 +1,73 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that the rule view search filter works properly for newly added +// property. + +const SEARCH = "100%"; + +const TEST_URI = ` + <style type='text/css'> + #testid { + width: 100%; + height: 50%; + } + </style> + <h1 id='testid'>Styled Node</h1> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + + info("Enter the test value in the search filter"); + yield setSearchFilter(view, SEARCH); + + info("Start entering a new property in the rule"); + let ruleEditor = getRuleViewRuleEditor(view, 1); + let rule = ruleEditor.rule; + let editor = yield focusNewRuleViewProperty(ruleEditor); + + info("Check that the correct rules are visible"); + is(view.element.children.length, 2, "Should have 2 rules."); + is(rule.selectorText, "#testid", "Second rule is #testid."); + ok(rule.textProps[0].editor.container.classList + .contains("ruleview-highlight"), + "width text property is correctly highlighted."); + ok(!rule.textProps[1].editor.container.classList + .contains("ruleview-highlight"), + "height text property is not highlighted."); + + info("Test creating a new property"); + + info("Entering margin-left in the property name editor"); + // Changing the value doesn't cause a rule-view refresh, no need to wait for + // ruleview-changed here. + editor.input.value = "margin-left"; + + info("Pressing return to commit and focus the new value field"); + let onRuleViewChanged = view.once("ruleview-changed"); + EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow); + yield onRuleViewChanged; + + // Getting the new value editor after focus + editor = inplaceEditor(view.styleDocument.activeElement); + let propEditor = ruleEditor.rule.textProps[2].editor; + + info("Entering a value and bluring the field to expect a rule change"); + onRuleViewChanged = view.once("ruleview-changed"); + editor.input.value = "100%"; + view.throttle.flush(); + yield onRuleViewChanged; + + onRuleViewChanged = view.once("ruleview-changed"); + editor.input.blur(); + yield onRuleViewChanged; + + ok(propEditor.container.classList.contains("ruleview-highlight"), + "margin-left text property is correctly highlighted."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_10.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_10.js new file mode 100644 index 000000000..ac336591d --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_10.js @@ -0,0 +1,84 @@ +/* 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"; + +// Tests that the rule view search filter works properly for rule selectors. + +const TEST_URI = ` + <style type="text/css"> + html, body, div { + background-color: #00F; + } + #testid { + width: 100%; + } + </style> + <div id="testid" class="testclass">Styled Node</div> +`; + +const TEST_DATA = [ + { + desc: "Tests that the search filter works properly for a single rule " + + "selector", + search: "#test", + selectorText: "#testid", + index: 0 + }, + { + desc: "Tests that the search filter works properly for multiple rule " + + "selectors", + search: "body", + selectorText: "html, body, div", + index: 2 + } +]; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + yield testAddTextInFilter(inspector, view); +}); + +function* testAddTextInFilter(inspector, view) { + for (let data of TEST_DATA) { + info(data.desc); + yield setSearchFilter(view, data.search); + yield checkRules(view, data); + yield clearSearchAndCheckRules(view); + } +} + +function* checkRules(view, data) { + info("Check that the correct rules are visible"); + is(view.element.children.length, 2, "Should have 2 rules."); + is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element", + "First rule is inline element."); + + let ruleEditor = getRuleViewRuleEditor(view, 1); + + is(ruleEditor.rule.selectorText, data.selectorText, + "Second rule is " + data.selectorText + "."); + ok(ruleEditor.selectorText.children[data.index].classList + .contains("ruleview-highlight"), + data.selectorText + " selector is highlighted."); +} + +function* clearSearchAndCheckRules(view) { + let doc = view.styleDocument; + let win = view.styleWindow; + let searchField = view.searchField; + let searchClearButton = view.searchClearButton; + + info("Clearing the search filter"); + EventUtils.synthesizeMouseAtCenter(searchClearButton, {}, win); + yield view.inspector.once("ruleview-filtered"); + + info("Check the search filter is cleared and no rules are highlighted"); + is(view.element.children.length, 3, "Should have 3 rules."); + ok(!searchField.value, "Search filter is cleared."); + ok(!doc.querySelectorAll(".ruleview-highlight").length, + "No rules are higlighted."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_context-menu.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_context-menu.js new file mode 100644 index 000000000..349f1b9b3 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_context-menu.js @@ -0,0 +1,83 @@ +/* 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 rule view search filter context menu works properly. + +const TEST_INPUT = "h1"; +const TEST_URI = "<h1>test filter context menu</h1>"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {toolbox, inspector, view} = yield openRuleView(); + yield selectNode("h1", inspector); + + let win = view.styleWindow; + let searchField = view.searchField; + let searchContextMenu = toolbox.textBoxContextMenuPopup; + ok(searchContextMenu, + "The search filter context menu is loaded in the rule view"); + + let cmdUndo = searchContextMenu.querySelector("[command=cmd_undo]"); + let cmdDelete = searchContextMenu.querySelector("[command=cmd_delete]"); + let cmdSelectAll = searchContextMenu.querySelector("[command=cmd_selectAll]"); + let cmdCut = searchContextMenu.querySelector("[command=cmd_cut]"); + let cmdCopy = searchContextMenu.querySelector("[command=cmd_copy]"); + let cmdPaste = searchContextMenu.querySelector("[command=cmd_paste]"); + + info("Opening context menu"); + + emptyClipboard(); + + let onFocus = once(searchField, "focus"); + searchField.focus(); + yield onFocus; + + let onContextMenuPopup = once(searchContextMenu, "popupshowing"); + EventUtils.synthesizeMouse(searchField, 2, 2, + {type: "contextmenu", button: 2}, win); + yield onContextMenuPopup; + + is(cmdUndo.getAttribute("disabled"), "true", "cmdUndo is disabled"); + is(cmdDelete.getAttribute("disabled"), "true", "cmdDelete is disabled"); + is(cmdSelectAll.getAttribute("disabled"), "true", "cmdSelectAll is disabled"); + + // Cut/Copy items are enabled in context menu even if there + // is no selection. See also Bug 1303033 + is(cmdCut.getAttribute("disabled"), "", "cmdCut is enabled"); + is(cmdCopy.getAttribute("disabled"), "", "cmdCopy is enabled"); + + if (isWindows()) { + // emptyClipboard only works on Windows (666254), assert paste only for this OS. + is(cmdPaste.getAttribute("disabled"), "true", "cmdPaste is disabled"); + } + + info("Closing context menu"); + let onContextMenuHidden = once(searchContextMenu, "popuphidden"); + searchContextMenu.hidePopup(); + yield onContextMenuHidden; + + info("Copy text in search field using the context menu"); + searchField.value = TEST_INPUT; + searchField.select(); + EventUtils.synthesizeMouse(searchField, 2, 2, + {type: "contextmenu", button: 2}, win); + yield onContextMenuPopup; + yield waitForClipboardPromise(() => cmdCopy.click(), TEST_INPUT); + searchContextMenu.hidePopup(); + yield onContextMenuHidden; + + info("Reopen context menu and check command properties"); + EventUtils.synthesizeMouse(searchField, 2, 2, + {type: "contextmenu", button: 2}, win); + yield onContextMenuPopup; + + is(cmdUndo.getAttribute("disabled"), "", "cmdUndo is enabled"); + is(cmdDelete.getAttribute("disabled"), "", "cmdDelete is enabled"); + is(cmdSelectAll.getAttribute("disabled"), "", "cmdSelectAll is enabled"); + is(cmdCut.getAttribute("disabled"), "", "cmdCut is enabled"); + is(cmdCopy.getAttribute("disabled"), "", "cmdCopy is enabled"); + is(cmdPaste.getAttribute("disabled"), "", "cmdPaste is enabled"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_escape-keypress.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_escape-keypress.js new file mode 100644 index 000000000..21848dce8 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_escape-keypress.js @@ -0,0 +1,65 @@ +/* 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"; + +// Tests that the rule view search filter escape keypress will clear the search +// field. + +const SEARCH = "00F"; + +const TEST_URI = ` + <style type="text/css"> + #testid { + background-color: #00F; + } + .testclass { + width: 100%; + } + </style> + <div id="testid" class="testclass">Styled Node</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + yield testAddTextInFilter(inspector, view); + yield testEscapeKeypress(inspector, view); +}); + +function* testAddTextInFilter(inspector, view) { + yield setSearchFilter(view, SEARCH); + + info("Check that the correct rules are visible"); + is(view.element.children.length, 2, "Should have 2 rules."); + is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element", + "First rule is inline element."); + + let rule = getRuleViewRuleEditor(view, 1).rule; + + is(rule.selectorText, "#testid", "Second rule is #testid."); + ok(rule.textProps[0].editor.container.classList + .contains("ruleview-highlight"), + "background-color text property is correctly highlighted."); +} + +function* testEscapeKeypress(inspector, view) { + info("Pressing the escape key on search filter"); + + let doc = view.styleDocument; + let win = view.styleWindow; + let searchField = view.searchField; + let onRuleViewFiltered = inspector.once("ruleview-filtered"); + + searchField.focus(); + EventUtils.synthesizeKey("VK_ESCAPE", {}, win); + yield onRuleViewFiltered; + + info("Check the search filter is cleared and no rules are highlighted"); + is(view.element.children.length, 3, "Should have 3 rules."); + ok(!searchField.value, "Search filter is cleared"); + ok(!doc.querySelectorAll(".ruleview-highlight").length, + "No rules are higlighted"); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_select-and-copy-styles.js b/devtools/client/inspector/rules/test/browser_rules_select-and-copy-styles.js new file mode 100644 index 000000000..b3f4ef364 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_select-and-copy-styles.js @@ -0,0 +1,171 @@ +/* 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"; + +// Tests that properties can be selected and copied from the rule view + +const osString = Services.appinfo.OS; + +const TEST_URI = ` + <style type="text/css"> + html { + color: #000000; + } + span { + font-variant: small-caps; color: #000000; + } + .nomatches { + color: #ff0000; + } + </style> + <div id="first" style="margin: 10em; + font-size: 14pt; font-family: helvetica, sans-serif; color: #AAA"> + <h1>Some header text</h1> + <p id="salutation" style="font-size: 12pt">hi.</p> + <p id="body" style="font-size: 12pt">I am a test-case. This text exists + solely to provide some things to <span style="color: yellow"> + highlight</span> and <span style="font-weight: bold">count</span> + style list-items in the box at right. If you are reading this, + you should go do something else instead. Maybe read a book. Or better + yet, write some test-cases for another bit of code. + <span style="font-style: italic">some text</span></p> + <p id="closing">more text</p> + <p>even more text</p> + </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 checkCopySelection(view); + yield checkSelectAll(view); + yield checkCopyEditorValue(view); +}); + +function* checkCopySelection(view) { + info("Testing selection copy"); + + let contentDoc = view.styleDocument; + let win = view.styleWindow; + let prop = contentDoc.querySelector(".ruleview-property"); + let values = contentDoc.querySelectorAll(".ruleview-propertyvaluecontainer"); + + let range = contentDoc.createRange(); + range.setStart(prop, 0); + range.setEnd(values[4], 2); + win.getSelection().addRange(range); + info("Checking that _Copy() returns the correct clipboard value"); + + let expectedPattern = " margin: 10em;[\\r\\n]+" + + " font-size: 14pt;[\\r\\n]+" + + " font-family: helvetica, sans-serif;[\\r\\n]+" + + " color: #AAA;[\\r\\n]+" + + "}[\\r\\n]+" + + "html {[\\r\\n]+" + + " color: #000000;[\\r\\n]*"; + + let allMenuItems = openStyleContextMenuAndGetAllItems(view, prop); + let menuitemCopy = allMenuItems.find(item => item.label === + STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copy")); + + ok(menuitemCopy.visible, + "Copy menu item is displayed as expected"); + + try { + yield waitForClipboardPromise(() => menuitemCopy.click(), + () => checkClipboardData(expectedPattern)); + } catch (e) { + failedClipboard(expectedPattern); + } +} + +function* checkSelectAll(view) { + info("Testing select-all copy"); + + let contentDoc = view.styleDocument; + let prop = contentDoc.querySelector(".ruleview-property"); + + info("Checking that _SelectAll() then copy returns the correct " + + "clipboard value"); + view._contextmenu._onSelectAll(); + let expectedPattern = "element {[\\r\\n]+" + + " margin: 10em;[\\r\\n]+" + + " font-size: 14pt;[\\r\\n]+" + + " font-family: helvetica, sans-serif;[\\r\\n]+" + + " color: #AAA;[\\r\\n]+" + + "}[\\r\\n]+" + + "html {[\\r\\n]+" + + " color: #000000;[\\r\\n]+" + + "}[\\r\\n]*"; + + let allMenuItems = openStyleContextMenuAndGetAllItems(view, prop); + let menuitemCopy = allMenuItems.find(item => item.label === + STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copy")); + + ok(menuitemCopy.visible, + "Copy menu item is displayed as expected"); + + try { + yield waitForClipboardPromise(() => menuitemCopy.click(), + () => checkClipboardData(expectedPattern)); + } catch (e) { + failedClipboard(expectedPattern); + } +} + +function* checkCopyEditorValue(view) { + info("Testing CSS property editor value copy"); + + let ruleEditor = getRuleViewRuleEditor(view, 0); + let propEditor = ruleEditor.rule.textProps[0].editor; + + let editor = yield focusEditableField(view, propEditor.valueSpan); + + info("Checking that copying a css property value editor returns the correct" + + " clipboard value"); + + let expectedPattern = "10em"; + + let allMenuItems = openStyleContextMenuAndGetAllItems(view, editor.input); + let menuitemCopy = allMenuItems.find(item => item.label === + STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copy")); + + ok(menuitemCopy.visible, + "Copy menu item is displayed as expected"); + + try { + yield waitForClipboardPromise(() => menuitemCopy.click(), + () => checkClipboardData(expectedPattern)); + } catch (e) { + failedClipboard(expectedPattern); + } +} + +function checkClipboardData(expectedPattern) { + let actual = SpecialPowers.getClipboardData("text/unicode"); + let expectedRegExp = new RegExp(expectedPattern, "g"); + return expectedRegExp.test(actual); +} + +function failedClipboard(expectedPattern) { + // Format expected text for comparison + let terminator = osString == "WINNT" ? "\r\n" : "\n"; + expectedPattern = expectedPattern.replace(/\[\\r\\n\][+*]/g, terminator); + expectedPattern = expectedPattern.replace(/\\\(/g, "("); + expectedPattern = expectedPattern.replace(/\\\)/g, ")"); + + let actual = SpecialPowers.getClipboardData("text/unicode"); + + // Trim the right hand side of our strings. This is because expectedPattern + // accounts for windows sometimes adding a newline to our copied data. + expectedPattern = expectedPattern.trimRight(); + actual = actual.trimRight(); + + dump("TEST-UNEXPECTED-FAIL | Clipboard text does not match expected ... " + + "results (escaped for accurate comparison):\n"); + info("Actual: " + escape(actual)); + info("Expected: " + escape(expectedPattern)); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_selector-highlighter-on-navigate.js b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter-on-navigate.js new file mode 100644 index 000000000..54e25c399 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter-on-navigate.js @@ -0,0 +1,38 @@ +/* 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 selector highlighter is hidden on page navigation. + +const TEST_URI = ` + <style type="text/css"> + body, p, td { + background: red; + } + </style> + Test the selector highlighter +`; + +const TEST_URI_2 = "data:text/html,<html><body>test</body></html>"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + let highlighters = view.highlighters; + + info("Clicking on a selector icon"); + let icon = getRuleViewSelectorHighlighterIcon(view, "body, p, td"); + + let onToggled = view.once("ruleview-selectorhighlighter-toggled"); + EventUtils.synthesizeMouseAtCenter(icon, {}, view.styleWindow); + let isVisible = yield onToggled; + + ok(highlighters.selectorHighlighterShown, "The selectorHighlighterShown is set."); + ok(view.selectorHighlighter, "The selectorhighlighter instance was created"); + ok(isVisible, "The toggle event says the highlighter is visible"); + + yield navigateTo(inspector, TEST_URI_2); + ok(!highlighters.selectorHighlighterShown, "The selectorHighlighterShown is unset."); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_01.js b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_01.js new file mode 100644 index 000000000..4c8853e02 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_01.js @@ -0,0 +1,35 @@ +/* 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 selector highlighter is created when clicking on a selector +// icon in the rule view. + +const TEST_URI = ` + <style type="text/css"> + body, p, td { + background: red; + } + </style> + Test the selector highlighter +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {view} = yield openRuleView(); + + ok(!view.selectorHighlighter, + "No selectorhighlighter exist in the rule-view"); + + info("Clicking on a selector icon"); + let icon = getRuleViewSelectorHighlighterIcon(view, "body, p, td"); + + let onToggled = view.once("ruleview-selectorhighlighter-toggled"); + EventUtils.synthesizeMouseAtCenter(icon, {}, view.styleWindow); + let isVisible = yield onToggled; + + ok(view.selectorHighlighter, "The selectorhighlighter instance was created"); + ok(isVisible, "The toggle event says the highlighter is visible"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_02.js b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_02.js new file mode 100644 index 000000000..33f73e587 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_02.js @@ -0,0 +1,78 @@ +/* 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 selector highlighter is shown when clicking on a selector icon +// in the rule-view + +// 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"> + body { + background: red; + } + p { + color: white; + } + </style> + <p>Testing the selector highlighter</p> +`; + +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, + options: null, + show: function (nodeFront, options) { + this.nodeFront = nodeFront; + this.options = options; + this.isShown = true; + }, + hide: function () { + this.nodeFront = null; + this.options = null; + this.isShown = false; + } + }; + + // Inject the mock highlighter in the rule-view + view.selectorHighlighter = HighlighterFront; + + let icon = getRuleViewSelectorHighlighterIcon(view, "body"); + + info("Checking that the HighlighterFront's show/hide methods are called"); + + info("Clicking once on the body selector highlighter icon"); + yield clickSelectorIcon(icon, view); + ok(HighlighterFront.isShown, "The highlighter is shown"); + + info("Clicking once again on the body selector highlighter icon"); + yield clickSelectorIcon(icon, view); + ok(!HighlighterFront.isShown, "The highlighter is hidden"); + + info("Checking that the right NodeFront reference and options are passed"); + yield selectNode("p", inspector); + icon = getRuleViewSelectorHighlighterIcon(view, "p"); + + yield clickSelectorIcon(icon, view); + is(HighlighterFront.nodeFront.tagName, "P", + "The right NodeFront is passed to the highlighter (1)"); + is(HighlighterFront.options.selector, "p", + "The right selector option is passed to the highlighter (1)"); + + yield selectNode("body", inspector); + icon = getRuleViewSelectorHighlighterIcon(view, "body"); + yield clickSelectorIcon(icon, view); + is(HighlighterFront.nodeFront.tagName, "BODY", + "The right NodeFront is passed to the highlighter (2)"); + is(HighlighterFront.options.selector, "body", + "The right selector option is passed to the highlighter (2)"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_03.js b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_03.js new file mode 100644 index 000000000..1ffbac012 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_03.js @@ -0,0 +1,78 @@ +/* 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 selector highlighter toggling mechanism works correctly. + +// 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"> + div {text-decoration: underline;} + .node-1 {color: red;} + .node-2 {color: green;} + </style> + <div class="node-1">Node 1</div> + <div class="node-2">Node 2</div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + + // Mock the highlighter front. + let HighlighterFront = { + isShown: false, + show: function () { + this.isShown = true; + }, + hide: function () { + this.isShown = false; + } + }; + + // Inject the mock highlighter in the rule-view + view.selectorHighlighter = HighlighterFront; + + info("Select .node-1 and click on the .node-1 selector icon"); + yield selectNode(".node-1", inspector); + let icon = getRuleViewSelectorHighlighterIcon(view, ".node-1"); + yield clickSelectorIcon(icon, view); + ok(HighlighterFront.isShown, "The highlighter is shown"); + + info("With .node-1 still selected, click again on the .node-1 selector icon"); + yield clickSelectorIcon(icon, view); + ok(!HighlighterFront.isShown, "The highlighter is now hidden"); + + info("With .node-1 still selected, click on the div selector icon"); + icon = getRuleViewSelectorHighlighterIcon(view, "div"); + yield clickSelectorIcon(icon, view); + ok(HighlighterFront.isShown, "The highlighter is shown again"); + + info("With .node-1 still selected, click again on the .node-1 selector icon"); + icon = getRuleViewSelectorHighlighterIcon(view, ".node-1"); + yield clickSelectorIcon(icon, view); + ok(HighlighterFront.isShown, + "The highlighter is shown again since the clicked selector was different"); + + info("Selecting .node-2"); + yield selectNode(".node-2", inspector); + ok(HighlighterFront.isShown, + "The highlighter is still shown after selection"); + + info("With .node-2 selected, click on the div selector icon"); + icon = getRuleViewSelectorHighlighterIcon(view, "div"); + yield clickSelectorIcon(icon, view); + ok(HighlighterFront.isShown, + "The highlighter is shown still since the selected was different"); + + info("Switching back to .node-1 and clicking on the div selector"); + yield selectNode(".node-1", inspector); + icon = getRuleViewSelectorHighlighterIcon(view, "div"); + yield clickSelectorIcon(icon, view); + ok(!HighlighterFront.isShown, + "The highlighter is hidden now that the same selector was clicked"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_04.js b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_04.js new file mode 100644 index 000000000..b770f8127 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter_04.js @@ -0,0 +1,53 @@ +/* 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 selector highlighter is shown when clicking on a selector icon +// for the 'element {}' rule + +// Note that in this test, we mock the highlighter front, merely testing the +// behavior of the style-inspector UI for now + +const TEST_URI = ` +<p>Testing the selector highlighter for the 'element {}' rule</p> +`; + +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, + options: null, + show: function (nodeFront, options) { + this.nodeFront = nodeFront; + this.options = options; + this.isShown = true; + }, + hide: function () { + this.nodeFront = null; + this.options = null; + this.isShown = false; + } + }; + // Inject the mock highlighter in the rule-view + view.selectorHighlighter = HighlighterFront; + + info("Checking that the right NodeFront reference and options are passed"); + yield selectNode("p", inspector); + let icon = getRuleViewSelectorHighlighterIcon(view, "element"); + + yield clickSelectorIcon(icon, view); + is(HighlighterFront.nodeFront.tagName, "P", + "The right NodeFront is passed to the highlighter (1)"); + is(HighlighterFront.options.selector, "body > p:nth-child(1)", + "The right selector option is passed to the highlighter (1)"); + ok(HighlighterFront.isShown, "The toggle event says the highlighter is visible"); + + yield clickSelectorIcon(icon, view); + ok(!HighlighterFront.isShown, "The toggle event says the highlighter is not visible"); +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_selector_highlight.js b/devtools/client/inspector/rules/test/browser_rules_selector_highlight.js new file mode 100644 index 000000000..91422d57a --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_selector_highlight.js @@ -0,0 +1,144 @@ +/* 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"; + +// Tests that the rule view selector text is highlighted correctly according +// to the components of the selector. + +const TEST_URI = [ + "<style type='text/css'>", + " h1 {}", + " h1#testid {}", + " h1 + p {}", + " div[hidden=\"true\"] {}", + " div[title=\"test\"][checked=true] {}", + " p:empty {}", + " p:lang(en) {}", + " .testclass:active {}", + " .testclass:focus {}", + " .testclass:hover {}", + "</style>", + "<h1>Styled Node</h1>", + "<p>Paragraph</p>", + "<h1 id=\"testid\">Styled Node</h1>", + "<div hidden=\"true\"></div>", + "<div title=\"test\" checked=\"true\"></div>", + "<p></p>", + "<p lang=\"en\">Paragraph<p>", + "<div class=\"testclass\">Styled Node</div>" +].join("\n"); + +const SELECTOR_ATTRIBUTE = "ruleview-selector-attribute"; +const SELECTOR_ELEMENT = "ruleview-selector"; +const SELECTOR_PSEUDO_CLASS = "ruleview-selector-pseudo-class"; +const SELECTOR_PSEUDO_CLASS_LOCK = "ruleview-selector-pseudo-class-lock"; + +const TEST_DATA = [ + { + node: "h1", + expected: [ + { value: "h1", class: SELECTOR_ELEMENT } + ] + }, + { + node: "h1 + p", + expected: [ + { value: "h1 + p", class: SELECTOR_ELEMENT } + ] + }, + { + node: "h1#testid", + expected: [ + { value: "h1#testid", class: SELECTOR_ELEMENT } + ] + }, + { + node: "div[hidden='true']", + expected: [ + { value: "div", class: SELECTOR_ELEMENT }, + { value: "[hidden=\"true\"]", class: SELECTOR_ATTRIBUTE } + ] + }, + { + node: "div[title=\"test\"][checked=\"true\"]", + expected: [ + { value: "div", class: SELECTOR_ELEMENT }, + { value: "[title=\"test\"]", class: SELECTOR_ATTRIBUTE }, + { value: "[checked=\"true\"]", class: SELECTOR_ATTRIBUTE } + ] + }, + { + node: "p:empty", + expected: [ + { value: "p", class: SELECTOR_ELEMENT }, + { value: ":empty", class: SELECTOR_PSEUDO_CLASS } + ] + }, + { + node: "p:lang(en)", + expected: [ + { value: "p", class: SELECTOR_ELEMENT }, + { value: ":lang(en)", class: SELECTOR_PSEUDO_CLASS } + ] + }, + { + node: ".testclass", + pseudoClass: ":active", + expected: [ + { value: ".testclass", class: SELECTOR_ELEMENT }, + { value: ":active", class: SELECTOR_PSEUDO_CLASS_LOCK } + ] + }, + { + node: ".testclass", + pseudoClass: ":focus", + expected: [ + { value: ".testclass", class: SELECTOR_ELEMENT }, + { value: ":focus", class: SELECTOR_PSEUDO_CLASS_LOCK } + ] + }, + { + node: ".testclass", + pseudoClass: ":hover", + expected: [ + { value: ".testclass", class: SELECTOR_ELEMENT }, + { value: ":hover", class: SELECTOR_PSEUDO_CLASS_LOCK } + ] + }, +]; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + + for (let {node, pseudoClass, expected} of TEST_DATA) { + yield selectNode(node, inspector); + + if (pseudoClass) { + let onRefresh = inspector.once("rule-view-refreshed"); + inspector.togglePseudoClass(pseudoClass); + yield onRefresh; + } + + let selectorContainer = + getRuleViewRuleEditor(view, 1).selectorText.firstChild; + + if (selectorContainer.children.length === expected.length) { + for (let i = 0; i < expected.length; i++) { + is(expected[i].value, selectorContainer.children[i].textContent, + "Got expected selector value: " + expected[i].value + " == " + + selectorContainer.children[i].textContent); + is(expected[i].class, selectorContainer.children[i].className, + "Got expected class name: " + expected[i].class + " == " + + selectorContainer.children[i].className); + } + } else { + for (let selector of selectorContainer.children) { + info("Actual selector components: { value: " + selector.textContent + + ", class: " + selector.className + " }\n"); + } + } + } +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_strict-search-filter-computed-list_01.js b/devtools/client/inspector/rules/test/browser_rules_strict-search-filter-computed-list_01.js new file mode 100644 index 000000000..dea9fff32 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_strict-search-filter-computed-list_01.js @@ -0,0 +1,182 @@ +/* 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"; + +// Tests that the rule view strict search filter and clear button works properly +// in the computed list + +const TEST_URI = ` + <style type="text/css"> + #testid { + margin: 4px 0px 10px 44px; + } + .testclass { + background-color: red; + } + </style> + <h1 id="testid" class="testclass">Styled Node</h1> +`; + +const TEST_DATA = [ + { + desc: "Tests that the strict search filter works properly in the " + + "computed list for property names", + search: "`margin-left`", + isExpanderOpen: true, + isFilterOpen: true, + isMarginHighlighted: false, + isMarginTopHighlighted: false, + isMarginRightHighlighted: false, + isMarginBottomHighlighted: false, + isMarginLeftHighlighted: true + }, + { + desc: "Tests that the strict search filter works properly in the " + + "computed list for property values", + search: "`0px`", + isExpanderOpen: true, + isFilterOpen: true, + isMarginHighlighted: false, + isMarginTopHighlighted: false, + isMarginRightHighlighted: true, + isMarginBottomHighlighted: false, + isMarginLeftHighlighted: false + }, + { + desc: "Tests that the strict search filter works properly in the " + + "computed list for parsed property names", + search: "`margin-left`:", + isExpanderOpen: true, + isFilterOpen: true, + isMarginHighlighted: false, + isMarginTopHighlighted: false, + isMarginRightHighlighted: false, + isMarginBottomHighlighted: false, + isMarginLeftHighlighted: true + }, + { + desc: "Tests that the strict search filter works properly in the " + + "computed list for parsed property values", + search: ":`4px`", + isExpanderOpen: true, + isFilterOpen: true, + isMarginHighlighted: false, + isMarginTopHighlighted: true, + isMarginRightHighlighted: false, + isMarginBottomHighlighted: false, + isMarginLeftHighlighted: false + }, + { + desc: "Tests that the strict search filter works properly in the " + + "computed list for property line input", + search: "`margin-top`:`4px`", + isExpanderOpen: true, + isFilterOpen: true, + isMarginHighlighted: false, + isMarginTopHighlighted: true, + isMarginRightHighlighted: false, + isMarginBottomHighlighted: false, + isMarginLeftHighlighted: false + }, + { + desc: "Tests that the strict search filter works properly in the " + + "computed list for a parsed strict property name and non-strict " + + "property value", + search: "`margin-top`:4px", + isExpanderOpen: true, + isFilterOpen: true, + isMarginHighlighted: false, + isMarginTopHighlighted: true, + isMarginRightHighlighted: false, + isMarginBottomHighlighted: false, + isMarginLeftHighlighted: false + }, + { + desc: "Tests that the strict search filter works properly in the " + + "computed list for a parsed strict property value and non-strict " + + "property name", + search: "i:`4px`", + isExpanderOpen: true, + isFilterOpen: true, + isMarginHighlighted: false, + isMarginTopHighlighted: true, + isMarginRightHighlighted: false, + isMarginBottomHighlighted: false, + isMarginLeftHighlighted: false + }, +]; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + yield testAddTextInFilter(inspector, view); +}); + +function* testAddTextInFilter(inspector, view) { + for (let data of TEST_DATA) { + info(data.desc); + yield setSearchFilter(view, data.search); + yield checkRules(view, data); + yield clearSearchAndCheckRules(view); + } +} + +function* checkRules(view, data) { + info("Check that the correct rules are visible"); + is(view.element.children.length, 2, "Should have 2 rules."); + is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element", + "First rule is inline element."); + + let rule = getRuleViewRuleEditor(view, 1).rule; + let textPropEditor = rule.textProps[0].editor; + let computed = textPropEditor.computed; + + is(rule.selectorText, "#testid", "Second rule is #testid."); + is(!!textPropEditor.expander.getAttribute("open"), data.isExpanderOpen, + "Got correct expander state."); + is(computed.hasAttribute("filter-open"), data.isFilterOpen, + "Got correct expanded state for margin computed list."); + is(textPropEditor.container.classList.contains("ruleview-highlight"), + data.isMarginHighlighted, + "Got correct highlight for margin text property."); + + is(computed.children[0].classList.contains("ruleview-highlight"), + data.isMarginTopHighlighted, + "Got correct highlight for margin-top computed property."); + is(computed.children[1].classList.contains("ruleview-highlight"), + data.isMarginRightHighlighted, + "Got correct highlight for margin-right computed property."); + is(computed.children[2].classList.contains("ruleview-highlight"), + data.isMarginBottomHighlighted, + "Got correct highlight for margin-bottom computed property."); + is(computed.children[3].classList.contains("ruleview-highlight"), + data.isMarginLeftHighlighted, + "Got correct highlight for margin-left computed property."); +} + +function* clearSearchAndCheckRules(view) { + let win = view.styleWindow; + let searchField = view.searchField; + let searchClearButton = view.searchClearButton; + + let rule = getRuleViewRuleEditor(view, 1).rule; + let textPropEditor = rule.textProps[0].editor; + let computed = textPropEditor.computed; + + info("Clearing the search filter"); + EventUtils.synthesizeMouseAtCenter(searchClearButton, {}, win); + yield view.inspector.once("ruleview-filtered"); + + info("Check the search filter is cleared and no rules are highlighted"); + is(view.element.children.length, 3, "Should have 3 rules."); + ok(!searchField.value, "Search filter is cleared"); + ok(!view.styleDocument.querySelectorAll(".ruleview-highlight").length, + "No rules are higlighted"); + + ok(!textPropEditor.expander.getAttribute("open"), "Expander is closed."); + ok(!computed.hasAttribute("filter-open"), + "margin computed list is closed."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_strict-search-filter_01.js b/devtools/client/inspector/rules/test/browser_rules_strict-search-filter_01.js new file mode 100644 index 000000000..50948e174 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_strict-search-filter_01.js @@ -0,0 +1,130 @@ +/* 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"; + +// Tests that the rule view strict search filter works properly for property +// names. + +const TEST_URI = ` + <style type="text/css"> + #testid { + width: 2%; + color: red; + } + .testclass { + width: 22%; + background-color: #00F; + } + </style> + <h1 id="testid" class="testclass">Styled Node</h1> +`; + +const TEST_DATA = [ + { + desc: "Tests that the strict search filter works properly for property " + + "names", + search: "`color`", + ruleCount: 2, + propertyIndex: 1 + }, + { + desc: "Tests that the strict search filter works properly for property " + + "values", + search: "`2%`", + ruleCount: 2, + propertyIndex: 0 + }, + { + desc: "Tests that the strict search filter works properly for parsed " + + "property names", + search: "`color`:", + ruleCount: 2, + propertyIndex: 1 + }, + { + desc: "Tests that the strict search filter works properly for parsed " + + "property values", + search: ":`2%`", + ruleCount: 2, + propertyIndex: 0 + }, + { + desc: "Tests that the strict search filter works properly for property " + + "line input", + search: "`width`:`2%`", + ruleCount: 2, + propertyIndex: 0 + }, + { + desc: "Tests that the search filter works properly for a parsed strict " + + "property name and non-strict property value.", + search: "`width`:2%", + ruleCount: 3, + propertyIndex: 0 + }, + { + desc: "Tests that the search filter works properly for a parsed strict " + + "property value and non-strict property name.", + search: "i:`2%`", + ruleCount: 2, + propertyIndex: 0 + } +]; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + yield testAddTextInFilter(inspector, view); +}); + +function* testAddTextInFilter(inspector, view) { + for (let data of TEST_DATA) { + info(data.desc); + yield setSearchFilter(view, data.search); + yield checkRules(view, data); + yield clearSearchAndCheckRules(view); + } +} + +function* checkRules(view, data) { + info("Check that the correct rules are visible"); + is(view.element.children.length, data.ruleCount, + "Should have " + data.ruleCount + " rules."); + is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element", + "First rule is inline element."); + + let rule = getRuleViewRuleEditor(view, 1).rule; + + is(rule.selectorText, "#testid", "Second rule is #testid."); + ok(rule.textProps[data.propertyIndex].editor.container.classList + .contains("ruleview-highlight"), + "Text property is correctly highlighted."); + + if (data.ruleCount > 2) { + rule = getRuleViewRuleEditor(view, 2).rule; + is(rule.selectorText, ".testclass", "Third rule is .testclass."); + ok(rule.textProps[data.propertyIndex].editor.container.classList + .contains("ruleview-highlight"), + "Text property is correctly highlighted."); + } +} + +function* clearSearchAndCheckRules(view) { + let doc = view.styleDocument; + let win = view.styleWindow; + let searchField = view.searchField; + let searchClearButton = view.searchClearButton; + + info("Clearing the search filter"); + EventUtils.synthesizeMouseAtCenter(searchClearButton, {}, win); + yield view.inspector.once("ruleview-filtered"); + + info("Check the search filter is cleared and no rules are highlighted"); + is(view.element.children.length, 3, "Should have 3 rules."); + ok(!searchField.value, "Search filter is cleared."); + ok(!doc.querySelectorAll(".ruleview-highlight").length, + "No rules are higlighted."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_strict-search-filter_02.js b/devtools/client/inspector/rules/test/browser_rules_strict-search-filter_02.js new file mode 100644 index 000000000..0c76f0518 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_strict-search-filter_02.js @@ -0,0 +1,34 @@ +/* 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"; + +// Tests that the rule view strict search filter works properly for stylesheet +// source. + +const SEARCH = "`doc_urls_clickable.css:1`"; +const TEST_URI = URL_ROOT + "doc_urls_clickable.html"; + +add_task(function* () { + yield addTab(TEST_URI); + let {inspector, view} = yield openRuleView(); + yield selectNode(".relative1", inspector); + yield testAddTextInFilter(inspector, view); +}); + +function* testAddTextInFilter(inspector, view) { + yield setSearchFilter(view, SEARCH); + + info("Check that the correct rules are visible"); + is(view.element.children.length, 2, "Should have 2 rules."); + is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element", + "First rule is inline element."); + + let rule = getRuleViewRuleEditor(view, 1).rule; + let source = rule.textProps[0].editor.ruleEditor.source; + + is(rule.selectorText, ".relative1", "Second rule is .relative1."); + ok(source.classList.contains("ruleview-highlight"), + "stylesheet source is correctly highlighted."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_strict-search-filter_03.js b/devtools/client/inspector/rules/test/browser_rules_strict-search-filter_03.js new file mode 100644 index 000000000..0326b0e9c --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_strict-search-filter_03.js @@ -0,0 +1,44 @@ +/* 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"; + +// Tests that the rule view strict search filter works properly for selector +// values. + +const SEARCH = "`.testclass`"; + +const TEST_URI = ` + <style type="text/css"> + .testclass1 { + background-color: #00F; + } + .testclass { + color: red; + } + </style> + <h1 id="testid" class="testclass testclass1">Styled Node</h1> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + yield testAddTextInFilter(inspector, view); +}); + +function* testAddTextInFilter(inspector, view) { + yield setSearchFilter(view, SEARCH); + + info("Check that the correct rules are visible"); + is(view.element.children.length, 2, "Should have 2 rules."); + is(getRuleViewRuleEditor(view, 0).rule.selectorText, "element", + "First rule is inline element."); + + let ruleEditor = getRuleViewRuleEditor(view, 1); + + is(ruleEditor.rule.selectorText, ".testclass", "Second rule is .testclass."); + ok(ruleEditor.selectorText.children[0].classList + .contains("ruleview-highlight"), ".testclass selector is highlighted."); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_style-editor-link.js b/devtools/client/inspector/rules/test/browser_rules_style-editor-link.js new file mode 100644 index 000000000..927deb8ce --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_style-editor-link.js @@ -0,0 +1,203 @@ +/* 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"; + +// FIXME: Whitelisting this test. +// As part of bug 1077403, the leaking uncaught rejection should be fixed. +thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Unknown sheet source"); + +// Test the links from the rule-view to the styleeditor + +const STYLESHEET_URL = "data:text/css," + encodeURIComponent( + ["#first {", + "color: blue", + "}"].join("\n")); + +const EXTERNAL_STYLESHEET_FILE_NAME = "doc_style_editor_link.css"; +const EXTERNAL_STYLESHEET_URL = URL_ROOT + EXTERNAL_STYLESHEET_FILE_NAME; + +const DOCUMENT_URL = "data:text/html;charset=utf-8," + encodeURIComponent(` + <html> + <head> + <title>Rule view style editor link test</title> + <style type="text/css"> + html { color: #000000; } + div { font-variant: small-caps; color: #000000; } + .nomatches {color: #ff0000;}</style> <div id="first" style="margin: 10em; + font-size: 14pt; font-family: helvetica, sans-serif; color: #AAA"> + </style> + <style> + div { font-weight: bold; } + </style> + <link rel="stylesheet" type="text/css" href="${STYLESHEET_URL}"> + <link rel="stylesheet" type="text/css" href="${EXTERNAL_STYLESHEET_URL}"> + </head> + <body> + <h1>Some header text</h1> + <p id="salutation" style="font-size: 12pt">hi.</p> + <p id="body" style="font-size: 12pt">I am a test-case. This text exists + solely to provide some things to + <span style="color: yellow" class="highlight"> + highlight</span> and <span style="font-weight: bold">count</span> + style list-items in the box at right. If you are reading this, + you should go do something else instead. Maybe read a book. Or better + yet, write some test-cases for another bit of code. + <span style="font-style: italic">some text</span></p> + <p id="closing">more text</p> + <p>even more text</p> + </div> + </body> + </html> +`); + +add_task(function* () { + yield addTab(DOCUMENT_URL); + let {toolbox, inspector, view, testActor} = yield openRuleView(); + yield selectNode("div", inspector); + + yield testInlineStyle(view); + yield testFirstInlineStyleSheet(view, toolbox, testActor); + yield testSecondInlineStyleSheet(view, toolbox, testActor); + yield testExternalStyleSheet(view, toolbox, testActor); + yield testDisabledStyleEditor(view, toolbox); +}); + +function* testInlineStyle(view) { + info("Testing inline style"); + + let onTab = waitForTab(); + info("Clicking on the first link in the rule-view"); + clickLinkByIndex(view, 0); + + let tab = yield onTab; + + let tabURI = tab.linkedBrowser.documentURI.spec; + ok(tabURI.startsWith("view-source:"), "View source tab is open"); + info("Closing tab"); + gBrowser.removeTab(tab); +} + +function* testFirstInlineStyleSheet(view, toolbox, testActor) { + info("Testing inline stylesheet"); + + info("Listening for toolbox switch to the styleeditor"); + let onSwitch = waitForStyleEditor(toolbox); + + info("Clicking an inline stylesheet"); + clickLinkByIndex(view, 4); + let editor = yield onSwitch; + + ok(true, "Switched to the style-editor panel in the toolbox"); + + yield validateStyleEditorSheet(editor, 0, testActor); +} + +function* testSecondInlineStyleSheet(view, toolbox, testActor) { + info("Testing second inline stylesheet"); + + info("Waiting for the stylesheet editor to be selected"); + let panel = toolbox.getCurrentPanel(); + let onSelected = panel.UI.once("editor-selected"); + + info("Switching back to the inspector panel in the toolbox"); + yield toolbox.selectTool("inspector"); + + info("Clicking on second inline stylesheet link"); + testRuleViewLinkLabel(view); + clickLinkByIndex(view, 3); + let editor = yield onSelected; + + is(toolbox.currentToolId, "styleeditor", + "The style editor is selected again"); + yield validateStyleEditorSheet(editor, 1, testActor); +} + +function* testExternalStyleSheet(view, toolbox, testActor) { + info("Testing external stylesheet"); + + info("Waiting for the stylesheet editor to be selected"); + let panel = toolbox.getCurrentPanel(); + let onSelected = panel.UI.once("editor-selected"); + + info("Switching back to the inspector panel in the toolbox"); + yield toolbox.selectTool("inspector"); + + info("Clicking on an external stylesheet link"); + testRuleViewLinkLabel(view); + clickLinkByIndex(view, 1); + let editor = yield onSelected; + + is(toolbox.currentToolId, "styleeditor", + "The style editor is selected again"); + yield validateStyleEditorSheet(editor, 2, testActor); +} + +function* validateStyleEditorSheet(editor, expectedSheetIndex, testActor) { + info("validating style editor stylesheet"); + is(editor.styleSheet.styleSheetIndex, expectedSheetIndex, + "loaded stylesheet index matches document stylesheet"); + + let href = editor.styleSheet.href || editor.styleSheet.nodeHref; + + let expectedHref = yield testActor.eval( + `content.document.styleSheets[${expectedSheetIndex}].href || + content.document.location.href`); + + is(href, expectedHref, "loaded stylesheet href matches document stylesheet"); +} + +function* testDisabledStyleEditor(view, toolbox) { + info("Testing with the style editor disabled"); + + info("Switching to the inspector panel in the toolbox"); + yield toolbox.selectTool("inspector"); + + info("Disabling the style editor"); + Services.prefs.setBoolPref("devtools.styleeditor.enabled", false); + gDevTools.emit("tool-unregistered", "styleeditor"); + + info("Clicking on a link"); + testUnselectableRuleViewLink(view, 1); + clickLinkByIndex(view, 1); + + is(toolbox.currentToolId, "inspector", "The click should have no effect"); + + info("Enabling the style editor"); + Services.prefs.setBoolPref("devtools.styleeditor.enabled", true); + gDevTools.emit("tool-registered", "styleeditor"); + + info("Clicking on a link"); + let onStyleEditorSelected = toolbox.once("styleeditor-selected"); + clickLinkByIndex(view, 1); + yield onStyleEditorSelected; + is(toolbox.currentToolId, "styleeditor", "Style Editor should be selected"); + + Services.prefs.clearUserPref("devtools.styleeditor.enabled"); +} + +function testRuleViewLinkLabel(view) { + let link = getRuleViewLinkByIndex(view, 2); + let labelElem = link.querySelector(".ruleview-rule-source-label"); + let value = labelElem.textContent; + let tooltipText = labelElem.getAttribute("title"); + + is(value, EXTERNAL_STYLESHEET_FILE_NAME + ":1", + "rule view stylesheet display value matches filename and line number"); + is(tooltipText, EXTERNAL_STYLESHEET_URL + ":1", + "rule view stylesheet tooltip text matches the full URI path"); +} + +function testUnselectableRuleViewLink(view, index) { + let link = getRuleViewLinkByIndex(view, index); + let unselectable = link.hasAttribute("unselectable"); + + ok(unselectable, "Rule view is unselectable"); +} + +function clickLinkByIndex(view, index) { + let link = getRuleViewLinkByIndex(view, index); + link.scrollIntoView(); + link.click(); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_urls-clickable.js b/devtools/client/inspector/rules/test/browser_rules_urls-clickable.js new file mode 100644 index 000000000..fb1211e3c --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_urls-clickable.js @@ -0,0 +1,70 @@ +/* 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"; + +// Tests to make sure that URLs are clickable in the rule view + +const TEST_URI = URL_ROOT + "doc_urls_clickable.html"; +const TEST_IMAGE = URL_ROOT + "doc_test_image.png"; +const BASE_64_URL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAA" + + "FCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAA" + + "BJRU5ErkJggg=="; + +add_task(function* () { + yield addTab(TEST_URI); + let {inspector, view} = yield openRuleView(); + yield selectNodes(inspector, view); +}); + +function* selectNodes(inspector, ruleView) { + let relative1 = ".relative1"; + let relative2 = ".relative2"; + let absolute = ".absolute"; + let inline = ".inline"; + let base64 = ".base64"; + let noimage = ".noimage"; + let inlineresolved = ".inline-resolved"; + + yield selectNode(relative1, inspector); + let relativeLink = ruleView.styleDocument + .querySelector(".ruleview-propertyvaluecontainer a"); + ok(relativeLink, "Link exists for relative1 node"); + is(relativeLink.getAttribute("href"), TEST_IMAGE, "href matches"); + + yield selectNode(relative2, inspector); + relativeLink = ruleView.styleDocument + .querySelector(".ruleview-propertyvaluecontainer a"); + ok(relativeLink, "Link exists for relative2 node"); + is(relativeLink.getAttribute("href"), TEST_IMAGE, "href matches"); + + yield selectNode(absolute, inspector); + let absoluteLink = ruleView.styleDocument + .querySelector(".ruleview-propertyvaluecontainer a"); + ok(absoluteLink, "Link exists for absolute node"); + is(absoluteLink.getAttribute("href"), TEST_IMAGE, "href matches"); + + yield selectNode(inline, inspector); + let inlineLink = ruleView.styleDocument + .querySelector(".ruleview-propertyvaluecontainer a"); + ok(inlineLink, "Link exists for inline node"); + is(inlineLink.getAttribute("href"), TEST_IMAGE, "href matches"); + + yield selectNode(base64, inspector); + let base64Link = ruleView.styleDocument + .querySelector(".ruleview-propertyvaluecontainer a"); + ok(base64Link, "Link exists for base64 node"); + is(base64Link.getAttribute("href"), BASE_64_URL, "href matches"); + + yield selectNode(inlineresolved, inspector); + let inlineResolvedLink = ruleView.styleDocument + .querySelector(".ruleview-propertyvaluecontainer a"); + ok(inlineResolvedLink, "Link exists for style tag node"); + is(inlineResolvedLink.getAttribute("href"), TEST_IMAGE, "href matches"); + + yield selectNode(noimage, inspector); + let noimageLink = ruleView.styleDocument + .querySelector(".ruleview-propertyvaluecontainer a"); + ok(!noimageLink, "There is no link for the node with no background image"); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_user-agent-styles-uneditable.js b/devtools/client/inspector/rules/test/browser_rules_user-agent-styles-uneditable.js new file mode 100644 index 000000000..e1bafff9b --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_user-agent-styles-uneditable.js @@ -0,0 +1,58 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that user agent styles are never editable via +// the UI + +const TEST_URI = ` + <blockquote type=cite> + <pre _moz_quote=true> + inspect <a href='foo' style='color:orange'>user agent</a> styles + </pre> + </blockquote> +`; + +var PREF_UA_STYLES = "devtools.inspector.showUserAgentStyles"; + +add_task(function* () { + info("Starting the test with the pref set to true before toolbox is opened"); + Services.prefs.setBoolPref(PREF_UA_STYLES, true); + + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openRuleView(); + + yield userAgentStylesUneditable(inspector, view); + + info("Resetting " + PREF_UA_STYLES); + Services.prefs.clearUserPref(PREF_UA_STYLES); +}); + +function* userAgentStylesUneditable(inspector, view) { + info("Making sure that UI is not editable for user agent styles"); + + yield selectNode("a", inspector); + let uaRules = view._elementStyle.rules.filter(rule=>!rule.editor.isEditable); + + for (let rule of uaRules) { + ok(rule.editor.element.hasAttribute("uneditable"), + "UA rules have uneditable attribute"); + + let firstProp = rule.textProps.filter(p => !p.invisible)[0]; + + ok(!firstProp.editor.nameSpan._editable, + "nameSpan is not editable"); + ok(!firstProp.editor.valueSpan._editable, + "valueSpan is not editable"); + ok(!rule.editor.closeBrace._editable, "closeBrace is not editable"); + + let colorswatch = rule.editor.element + .querySelector(".ruleview-colorswatch"); + if (colorswatch) { + ok(!view.tooltips.colorPicker.swatches.has(colorswatch), + "The swatch is not editable"); + } + } +} diff --git a/devtools/client/inspector/rules/test/browser_rules_user-agent-styles.js b/devtools/client/inspector/rules/test/browser_rules_user-agent-styles.js new file mode 100644 index 000000000..6852e3c03 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_user-agent-styles.js @@ -0,0 +1,183 @@ +/* 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 that user agent styles are inspectable via rule view if +// it is preffed on. + +var PREF_UA_STYLES = "devtools.inspector.showUserAgentStyles"; +const { PrefObserver } = require("devtools/client/styleeditor/utils"); + +const TEST_URI = URL_ROOT + "doc_author-sheet.html"; + +const TEST_DATA = [ + { + selector: "blockquote", + numUserRules: 1, + numUARules: 0 + }, + { + selector: "pre", + numUserRules: 1, + numUARules: 0 + }, + { + selector: "input[type=range]", + numUserRules: 1, + numUARules: 0 + }, + { + selector: "input[type=number]", + numUserRules: 1, + numUARules: 0 + }, + { + selector: "input[type=color]", + numUserRules: 1, + numUARules: 0 + }, + { + selector: "input[type=text]", + numUserRules: 1, + numUARules: 0 + }, + { + selector: "progress", + numUserRules: 1, + numUARules: 0 + }, + // Note that some tests below assume that the "a" selector is the + // last test in TEST_DATA. + { + selector: "a", + numUserRules: 3, + numUARules: 0 + } +]; + +add_task(function* () { + requestLongerTimeout(4); + + info("Starting the test with the pref set to true before toolbox is opened"); + yield setUserAgentStylesPref(true); + + yield addTab(TEST_URI); + let {inspector, view} = yield openRuleView(); + + info("Making sure that UA styles are visible on initial load"); + yield userAgentStylesVisible(inspector, view); + + info("Making sure that setting the pref to false hides UA styles"); + yield setUserAgentStylesPref(false); + yield userAgentStylesNotVisible(inspector, view); + + info("Making sure that resetting the pref to true shows UA styles again"); + yield setUserAgentStylesPref(true); + yield userAgentStylesVisible(inspector, view); + + info("Resetting " + PREF_UA_STYLES); + Services.prefs.clearUserPref(PREF_UA_STYLES); +}); + +function* setUserAgentStylesPref(val) { + info("Setting the pref " + PREF_UA_STYLES + " to: " + val); + + // Reset the pref and wait for PrefObserver to callback so UI + // has a chance to get updated. + let oncePrefChanged = defer(); + let prefObserver = new PrefObserver("devtools."); + prefObserver.on(PREF_UA_STYLES, oncePrefChanged.resolve); + Services.prefs.setBoolPref(PREF_UA_STYLES, val); + yield oncePrefChanged.promise; + prefObserver.off(PREF_UA_STYLES, oncePrefChanged.resolve); +} + +function* userAgentStylesVisible(inspector, view) { + info("Making sure that user agent styles are currently visible"); + + let userRules; + let uaRules; + + for (let data of TEST_DATA) { + yield selectNode(data.selector, inspector); + yield compareAppliedStylesWithUI(inspector, view, "ua"); + + userRules = view._elementStyle.rules.filter(rule=>rule.editor.isEditable); + uaRules = view._elementStyle.rules.filter(rule=>!rule.editor.isEditable); + is(userRules.length, data.numUserRules, "Correct number of user rules"); + ok(uaRules.length > data.numUARules, "Has UA rules"); + } + + ok(userRules.some(rule => rule.matchedSelectors.length === 1), + "There is an inline style for element in user styles"); + + // These tests rely on the "a" selector being the last test in + // TEST_DATA. + ok(uaRules.some(rule => { + return rule.matchedSelectors.indexOf(":any-link") !== -1; + }), "There is a rule for :any-link"); + ok(uaRules.some(rule => { + return rule.matchedSelectors.indexOf("*|*:link") !== -1; + }), "There is a rule for *|*:link"); + ok(uaRules.some(rule => { + return rule.matchedSelectors.length === 1; + }), "Inline styles for ua styles"); +} + +function* userAgentStylesNotVisible(inspector, view) { + info("Making sure that user agent styles are not currently visible"); + + let userRules; + let uaRules; + + for (let data of TEST_DATA) { + yield selectNode(data.selector, inspector); + yield compareAppliedStylesWithUI(inspector, view); + + userRules = view._elementStyle.rules.filter(rule=>rule.editor.isEditable); + uaRules = view._elementStyle.rules.filter(rule=>!rule.editor.isEditable); + is(userRules.length, data.numUserRules, "Correct number of user rules"); + is(uaRules.length, data.numUARules, "No UA rules"); + } +} + +function* compareAppliedStylesWithUI(inspector, view, filter) { + info("Making sure that UI is consistent with pageStyle.getApplied"); + + let entries = yield inspector.pageStyle.getApplied( + inspector.selection.nodeFront, + { + inherited: true, + matchedSelectors: true, + filter: filter + } + ); + + // We may see multiple entries that map to a given rule; filter the + // duplicates here to match what the UI does. + let entryMap = new Map(); + for (let entry of entries) { + entryMap.set(entry.rule, entry); + } + entries = [...entryMap.values()]; + + let elementStyle = view._elementStyle; + is(elementStyle.rules.length, entries.length, + "Should have correct number of rules (" + entries.length + ")"); + + entries = entries.sort((a, b) => { + return (a.pseudoElement || "z") > (b.pseudoElement || "z"); + }); + + entries.forEach((entry, i) => { + let elementStyleRule = elementStyle.rules[i]; + is(elementStyleRule.inherited, entry.inherited, + "Same inherited (" + entry.inherited + ")"); + is(elementStyleRule.isSystem, entry.isSystem, + "Same isSystem (" + entry.isSystem + ")"); + is(elementStyleRule.editor.isEditable, !entry.isSystem, + "Editor isEditable opposite of UA (" + entry.isSystem + ")"); + }); +} diff --git a/devtools/client/inspector/rules/test/browser_rules_user-property-reset.js b/devtools/client/inspector/rules/test/browser_rules_user-property-reset.js new file mode 100644 index 000000000..62b1d927c --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_user-property-reset.js @@ -0,0 +1,90 @@ +/* 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 user set style properties can be changed from the markup-view and +// don't survive page reload + +const TEST_URI = ` + <p id='id1' style='width:200px;'>element 1</p> + <p id='id2' style='width:100px;'>element 2</p> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view, testActor} = yield openRuleView(); + + yield selectNode("#id1", inspector); + yield modifyRuleViewWidth("300px", view, inspector); + yield assertRuleAndMarkupViewWidth("id1", "300px", view, inspector); + + yield selectNode("#id2", inspector); + yield assertRuleAndMarkupViewWidth("id2", "100px", view, inspector); + yield modifyRuleViewWidth("50px", view, inspector); + yield assertRuleAndMarkupViewWidth("id2", "50px", view, inspector); + + yield reloadPage(inspector, testActor); + + yield selectNode("#id1", inspector); + yield assertRuleAndMarkupViewWidth("id1", "200px", view, inspector); + yield selectNode("#id2", inspector); + yield assertRuleAndMarkupViewWidth("id2", "100px", view, inspector); +}); + +function getStyleRule(ruleView) { + return ruleView.styleDocument.querySelector(".ruleview-rule"); +} + +function* modifyRuleViewWidth(value, ruleView, inspector) { + info("Getting the property value element"); + let valueSpan = getStyleRule(ruleView) + .querySelector(".ruleview-propertyvalue"); + + info("Focusing the property value to set it to edit mode"); + let editor = yield focusEditableField(ruleView, valueSpan.parentNode); + + ok(editor.input, "The inplace-editor field is ready"); + info("Setting the new value"); + editor.input.value = value; + + info("Pressing return and waiting for the field to blur and for the " + + "markup-view to show the mutation"); + let onBlur = once(editor.input, "blur", true); + let onStyleChanged = waitForStyleModification(inspector); + EventUtils.sendKey("return"); + yield onBlur; + yield onStyleChanged; + + info("Escaping out of the new property field that has been created after " + + "the value was edited"); + let onNewFieldBlur = once(ruleView.styleDocument.activeElement, "blur", true); + EventUtils.sendKey("escape"); + yield onNewFieldBlur; +} + +function* getContainerStyleAttrValue(id, {walker, markup}) { + let front = yield walker.querySelector(walker.rootNode, "#" + id); + let container = markup.getContainer(front); + + let attrIndex = 0; + for (let attrName of container.elt.querySelectorAll(".attr-name")) { + if (attrName.textContent === "style") { + return container.elt.querySelectorAll(".attr-value")[attrIndex]; + } + attrIndex++; + } + return undefined; +} + +function* assertRuleAndMarkupViewWidth(id, value, ruleView, inspector) { + let valueSpan = getStyleRule(ruleView) + .querySelector(".ruleview-propertyvalue"); + is(valueSpan.textContent, value, + "Rule-view style width is " + value + " as expected"); + + let attr = yield getContainerStyleAttrValue(id, inspector); + is(attr.textContent.replace(/\s/g, ""), + "width:" + value + ";", "Markup-view style attribute width is " + value); +} diff --git a/devtools/client/inspector/rules/test/doc_author-sheet.html b/devtools/client/inspector/rules/test/doc_author-sheet.html new file mode 100644 index 000000000..f8c2eadd5 --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_author-sheet.html @@ -0,0 +1,39 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> +<head> + <title>authored sheet test</title> + + <style> + pre a { + color: orange; + } + </style> + + <script> + "use strict"; + var gIOService = SpecialPowers.Cc["@mozilla.org/network/io-service;1"] + .getService(SpecialPowers.Ci.nsIIOService); + + var style = "data:text/css,a { 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> + <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> diff --git a/devtools/client/inspector/rules/test/doc_blob_stylesheet.html b/devtools/client/inspector/rules/test/doc_blob_stylesheet.html new file mode 100644 index 000000000..c9973993b --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_blob_stylesheet.html @@ -0,0 +1,39 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> +</html> +<html> +<head> + <meta charset="utf-8"> + <title>Blob stylesheet sourcemap</title> +</head> +<body> +<h1>Test</h1> +<script> +"use strict"; + +var cssContent = `body { + background-color: black; +} +body > h1 { + color: white; +} +` + +"/*# sourceMappingURL=data:application/json;base64,ewoidmVyc2lvbiI6IDMsCiJtYX" + +"BwaW5ncyI6ICJBQUFBLElBQUs7RUFDSCxnQkFBZ0IsRUFBRSxLQUFLOztBQUN2QixTQUFPO0VBQ0" + +"wsS0FBSyxFQUFFLEtBQUsiLAoic291cmNlcyI6IFsidGVzdC5zY3NzIl0sCiJzb3VyY2VzQ29udG" + +"VudCI6IFsiYm9keSB7XG4gIGJhY2tncm91bmQtY29sb3I6IGJsYWNrO1xuICAmID4gaDEge1xuIC" + +"AgIGNvbG9yOiB3aGl0ZTsgIFxuICB9XG59XG4iXSwKIm5hbWVzIjogW10sCiJmaWxlIjogInRlc3" + +"QuY3NzIgp9Cg== */"; +var cssBlob = new Blob([cssContent], {type: "text/css"}); +var url = URL.createObjectURL(cssBlob); + +var head = document.querySelector("head"); +var link = document.createElement("link"); +link.rel = "stylesheet"; +link.type = "text/css"; +link.href = url; +head.appendChild(link); +</script> +</body> +</html> diff --git a/devtools/client/inspector/rules/test/doc_content_stylesheet.html b/devtools/client/inspector/rules/test/doc_content_stylesheet.html new file mode 100644 index 000000000..3ea65f606 --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_content_stylesheet.html @@ -0,0 +1,35 @@ +<html> +<head> + <title>test</title> + + <link href="./doc_content_stylesheet_linked.css" rel="stylesheet" type="text/css"> + + <script> + /* eslint no-unused-vars: [2, {"vars": "local"}] */ + "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> diff --git a/devtools/client/inspector/rules/test/doc_content_stylesheet_imported.css b/devtools/client/inspector/rules/test/doc_content_stylesheet_imported.css new file mode 100644 index 000000000..ea1a3d986 --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_content_stylesheet_imported.css @@ -0,0 +1,5 @@ +@import url("./doc_content_stylesheet_imported2.css"); + +#target { + text-decoration: underline; +} diff --git a/devtools/client/inspector/rules/test/doc_content_stylesheet_imported2.css b/devtools/client/inspector/rules/test/doc_content_stylesheet_imported2.css new file mode 100644 index 000000000..77c73299e --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_content_stylesheet_imported2.css @@ -0,0 +1,3 @@ +#target { + text-decoration: underline; +} diff --git a/devtools/client/inspector/rules/test/doc_content_stylesheet_linked.css b/devtools/client/inspector/rules/test/doc_content_stylesheet_linked.css new file mode 100644 index 000000000..712ba78fb --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_content_stylesheet_linked.css @@ -0,0 +1,3 @@ +table { + border-collapse: collapse; +} diff --git a/devtools/client/inspector/rules/test/doc_content_stylesheet_script.css b/devtools/client/inspector/rules/test/doc_content_stylesheet_script.css new file mode 100644 index 000000000..5aa5e2c6c --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_content_stylesheet_script.css @@ -0,0 +1,5 @@ +@import url("./doc_content_stylesheet_imported.css"); + +table { + opacity: 1; +} diff --git a/devtools/client/inspector/rules/test/doc_copystyles.css b/devtools/client/inspector/rules/test/doc_copystyles.css new file mode 100644 index 000000000..83f0c87b1 --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_copystyles.css @@ -0,0 +1,11 @@ +/* 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/. */ + +html, body, #testid { + color: #F00; + background-color: #00F; + font-size: 12px; + border-color: #00F !important; + --var: "*/"; +} diff --git a/devtools/client/inspector/rules/test/doc_copystyles.html b/devtools/client/inspector/rules/test/doc_copystyles.html new file mode 100644 index 000000000..da1b4c0b3 --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_copystyles.html @@ -0,0 +1,11 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> + <head> + <title>Test case for copying stylesheet in rule-view</title> + <link rel="stylesheet" type="text/css" href="doc_copystyles.css"/> + </head> + <body> + <div id='testid'>Styled Node</div> + </body> +</html> diff --git a/devtools/client/inspector/rules/test/doc_cssom.html b/devtools/client/inspector/rules/test/doc_cssom.html new file mode 100644 index 000000000..28de66d7d --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_cssom.html @@ -0,0 +1,22 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> +<head> + <title>CSSOM test</title> + + <script> + "use strict"; + window.onload = function () { + let x = document.styleSheets[0]; + x.insertRule("div { color: seagreen; }", 1); + }; + </script> + + <style> + span { } + </style> +</head> +<body> + <div id="target"> the ocean </div> +</body> +</html> diff --git a/devtools/client/inspector/rules/test/doc_custom.html b/devtools/client/inspector/rules/test/doc_custom.html new file mode 100644 index 000000000..09bf501d5 --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_custom.html @@ -0,0 +1,33 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> + <head> + <style> + #testidSimple { + --background-color: blue; + } + .testclassSimple { + --background-color: green; + } + + .testclassImportant { + --background-color: green !important; + } + #testidImportant { + --background-color: blue; + } + + #testidDisable { + --background-color: blue; + } + .testclassDisable { + --background-color: green; + } + </style> + </head> + <body> + <div id="testidSimple" class="testclassSimple">Styled Node</div> + <div id="testidImportant" class="testclassImportant">Styled Node</div> + <div id="testidDisable" class="testclassDisable">Styled Node</div> + </body> +</html> diff --git a/devtools/client/inspector/rules/test/doc_filter.html b/devtools/client/inspector/rules/test/doc_filter.html new file mode 100644 index 000000000..cb2df9feb --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_filter.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<!-- 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/. --> +<html> +<head> + <title>Bug 1055181 - CSS Filter Editor Widget</title> + <style> + body { + filter: blur(2px) contrast(2); + } + </style> +</head> diff --git a/devtools/client/inspector/rules/test/doc_frame_script.js b/devtools/client/inspector/rules/test/doc_frame_script.js new file mode 100644 index 000000000..88da043f1 --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_frame_script.js @@ -0,0 +1,113 @@ +/* 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 element = content.document.querySelector(selector); + let value = content.document.defaultView.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. + * @return a promise that resolves when the function returned true or rejects + * if the timeout is reached + */ +function waitForSuccess(validatorFn) { + let def = defer(); + + function wait(fn) { + if (fn()) { + def.resolve(); + } else { + setTimeout(() => wait(fn), 200); + } + } + wait(validatorFn); + + return def.promise; +} diff --git a/devtools/client/inspector/rules/test/doc_inline_sourcemap.html b/devtools/client/inspector/rules/test/doc_inline_sourcemap.html new file mode 100644 index 000000000..cb107d424 --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_inline_sourcemap.html @@ -0,0 +1,18 @@ +<!doctype html> +<html> +<head> + <title>CSS source maps in inline stylesheets</title> +</head> +<body> + <div>CSS source maps in inline stylesheets</div> + <style> +div { + color: #ff0066; } + +span { + background-color: #EEE; } + +/*# sourceMappingURL=doc_sourcemaps.css.map */ + </style> +</body> +</html> diff --git a/devtools/client/inspector/rules/test/doc_invalid_sourcemap.css b/devtools/client/inspector/rules/test/doc_invalid_sourcemap.css new file mode 100644 index 000000000..ff96a6b54 --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_invalid_sourcemap.css @@ -0,0 +1,3 @@ +div { color: gold; }
+
+/*# sourceMappingURL=this-source-map-does-not-exist.css.map */
\ No newline at end of file diff --git a/devtools/client/inspector/rules/test/doc_invalid_sourcemap.html b/devtools/client/inspector/rules/test/doc_invalid_sourcemap.html new file mode 100644 index 000000000..2e6422bec --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_invalid_sourcemap.html @@ -0,0 +1,11 @@ +<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Invalid source map</title>
+ <link rel="stylesheet" type="text/css" href="doc_invalid_sourcemap.css">
+</head>
+<body>
+ <div>invalid source map</div>
+</body>
+</html>
diff --git a/devtools/client/inspector/rules/test/doc_keyframeLineNumbers.html b/devtools/client/inspector/rules/test/doc_keyframeLineNumbers.html new file mode 100644 index 000000000..8fce04584 --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_keyframeLineNumbers.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>keyframe line numbers test</title> + <style type="text/css"> +div { + animation-duration: 1s; + animation-iteration-count: infinite; + animation-direction: alternate; + animation-name: CC; +} + +span { + animation-duration: 3s; + animation-iteration-count: infinite; + animation-direction: alternate; + animation-name: DD; +} + +@keyframes CC { + from { + background: #ffffff; + } + to { + background: #f0c; + } +} + +@keyframes DD { + from { + background: seagreen; + } + to { + background: chartreuse; + } +} + </style> +</head> +<body> + <div id="outer"> + <span id="inner">lizards</div> + </div> +</body> +</html> diff --git a/devtools/client/inspector/rules/test/doc_keyframeanimation.css b/devtools/client/inspector/rules/test/doc_keyframeanimation.css new file mode 100644 index 000000000..64582ed35 --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_keyframeanimation.css @@ -0,0 +1,84 @@ +/* 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/. */ + +.box { + height: 50px; + width: 50px; +} + +.circle { + width: 20px; + height: 20px; + border-radius: 10px; + background-color: #FFCB01; +} + +#pacman { + width: 0px; + height: 0px; + border-right: 60px solid transparent; + border-top: 60px solid #FFCB01; + border-left: 60px solid #FFCB01; + border-bottom: 60px solid #FFCB01; + border-top-left-radius: 60px; + border-bottom-left-radius: 60px; + border-top-right-radius: 60px; + border-bottom-right-radius: 60px; + top: 120px; + left: 150px; + position: absolute; + animation-name: pacman; + animation-fill-mode: forwards; + animation-timing-function: linear; + animation-duration: 15s; +} + +#boxy { + top: 170px; + left: 450px; + position: absolute; + animation: 4s linear 0s normal none infinite boxy; +} + + +#moxy { + animation-name: moxy, boxy; + animation-delay: 3.5s; + animation-duration: 2s; + top: 170px; + left: 650px; + position: absolute; +} + +@-moz-keyframes pacman { + 100% { + left: 750px; + } +} + +@keyframes pacman { + 100% { + left: 750px; + } +} + +@keyframes boxy { + 10% { + background-color: blue; + } + + 20% { + background-color: green; + } + + 100% { + opacity: 0; + } +} + +@keyframes moxy { + to { + opacity: 0; + } +} diff --git a/devtools/client/inspector/rules/test/doc_keyframeanimation.html b/devtools/client/inspector/rules/test/doc_keyframeanimation.html new file mode 100644 index 000000000..4e02c32f0 --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_keyframeanimation.html @@ -0,0 +1,13 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> + <head> + <title>test case for keyframes rule in rule-view</title> + <link rel="stylesheet" type="text/css" href="doc_keyframeanimation.css"/> + </head> + <body> + <div id="pacman"></div> + <div id="boxy" class="circle"></div> + <div id="moxy" class="circle"></div> + </body> +</html> diff --git a/devtools/client/inspector/rules/test/doc_media_queries.html b/devtools/client/inspector/rules/test/doc_media_queries.html new file mode 100644 index 000000000..1adb8bc7a --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_media_queries.html @@ -0,0 +1,24 @@ +<html> +<head> + <title>test</title> + <script type="application/javascript;version=1.7"> + + </script> + <style> + div { + width: 1000px; + height: 100px; + background-color: #f00; + } + + @media screen and (min-width: 1px) { + div { + width: 200px; + } + } + </style> +</head> +<body> +<div></div> +</body> +</html> diff --git a/devtools/client/inspector/rules/test/doc_pseudoelement.html b/devtools/client/inspector/rules/test/doc_pseudoelement.html new file mode 100644 index 000000000..6145d4bf1 --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_pseudoelement.html @@ -0,0 +1,131 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> + <head> + <style> + +body { + color: #333; +} + +.box { + float:left; + width: 128px; + height: 128px; + background: #ddd; + padding: 32px; + margin: 32px; + position:relative; +} + +.box:first-line { + color: orange; + background: red; +} + +.box:first-letter { + color: green; +} + +* { + cursor: default; +} + +nothing { + cursor: pointer; +} + +p::-moz-selection { + color: white; + background: black; +} +p::selection { + color: white; + background: black; +} + +p:first-line { + background: blue; +} +p:first-letter { + color: red; + font-size: 130%; +} + +.box:before { + background: green; + content: " "; + position: absolute; + height:32px; + width:32px; +} + +.box:after { + background: red; + content: " "; + position: absolute; + border-radius: 50%; + height:32px; + width:32px; + top: 50%; + left: 50%; + margin-top: -16px; + margin-left: -16px; +} + +.topleft:before { + top:0; + left:0; +} + +.topleft:first-line { + color: orange; +} +.topleft::selection { + color: orange; +} + +.topright:before { + top:0; + right:0; +} + +.bottomright:before { + bottom:10px; + right:10px; + color: red; +} + +.bottomright:before { + bottom:0; + right:0; +} + +.bottomleft:before { + bottom:0; + left:0; +} + + </style> + </head> + <body> + <h1>ruleview pseudoelement($("test"));</h1> + + <div id="topleft" class="box topleft"> + <p>Top Left<br />Position</p> + </div> + + <div id="topright" class="box topright"> + <p>Top Right<br />Position</p> + </div> + + <div id="bottomright" class="box bottomright"> + <p>Bottom Right<br />Position</p> + </div> + + <div id="bottomleft" class="box bottomleft"> + <p>Bottom Left<br />Position</p> + </div> + + </body> +</html> diff --git a/devtools/client/inspector/rules/test/doc_ruleLineNumbers.html b/devtools/client/inspector/rules/test/doc_ruleLineNumbers.html new file mode 100644 index 000000000..5a157f384 --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_ruleLineNumbers.html @@ -0,0 +1,19 @@ +<!doctype html> +<html> +<head> + <meta charset="utf-8"> + <title>simple testcase</title> + <style type="text/css"> + #testid { + background-color: seagreen; + } + + body { + color: chartreuse; + } + </style> +</head> +<body> + <div id="testid">simple testcase</div> +</body> +</html> diff --git a/devtools/client/inspector/rules/test/doc_sourcemaps.css b/devtools/client/inspector/rules/test/doc_sourcemaps.css new file mode 100644 index 000000000..a9b437a40 --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_sourcemaps.css @@ -0,0 +1,7 @@ +div { + color: #ff0066; } + +span { + background-color: #EEE; } + +/*# sourceMappingURL=doc_sourcemaps.css.map */
\ No newline at end of file diff --git a/devtools/client/inspector/rules/test/doc_sourcemaps.css.map b/devtools/client/inspector/rules/test/doc_sourcemaps.css.map new file mode 100644 index 000000000..0f7486fd9 --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_sourcemaps.css.map @@ -0,0 +1,7 @@ +{ +"version": 3, +"mappings": "AAGA,GAAI;EACF,KAAK,EAHU,OAAI;;AAMrB,IAAK;EACH,gBAAgB,EAAE,IAAI", +"sources": ["doc_sourcemaps.scss"], +"names": [], +"file": "doc_sourcemaps.css" +} diff --git a/devtools/client/inspector/rules/test/doc_sourcemaps.html b/devtools/client/inspector/rules/test/doc_sourcemaps.html new file mode 100644 index 000000000..0014e55fe --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_sourcemaps.html @@ -0,0 +1,11 @@ +<!doctype html> +<html> +<head> + <title>testcase for testing CSS source maps</title> + <link rel="stylesheet" type="text/css" href="simple.css"/> + <link rel="stylesheet" type="text/css" href="doc_sourcemaps.css"/> +</head> +<body> + <div>source maps <span>testcase</span></div> +</body> +</html> diff --git a/devtools/client/inspector/rules/test/doc_sourcemaps.scss b/devtools/client/inspector/rules/test/doc_sourcemaps.scss new file mode 100644 index 000000000..0ff6c471b --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_sourcemaps.scss @@ -0,0 +1,10 @@ + +$paulrougetpink: #f06; + +div { + color: $paulrougetpink; +} + +span { + background-color: #EEE; +}
\ No newline at end of file diff --git a/devtools/client/inspector/rules/test/doc_style_editor_link.css b/devtools/client/inspector/rules/test/doc_style_editor_link.css new file mode 100644 index 000000000..e49e1f587 --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_style_editor_link.css @@ -0,0 +1,3 @@ +div { + opacity: 1; +}
\ No newline at end of file diff --git a/devtools/client/inspector/rules/test/doc_test_image.png b/devtools/client/inspector/rules/test/doc_test_image.png Binary files differnew file mode 100644 index 000000000..769c63634 --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_test_image.png diff --git a/devtools/client/inspector/rules/test/doc_urls_clickable.css b/devtools/client/inspector/rules/test/doc_urls_clickable.css new file mode 100644 index 000000000..04315b2c3 --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_urls_clickable.css @@ -0,0 +1,9 @@ +.relative1 { + background-image: url(./doc_test_image.png); +} +.absolute { + background: url("http://example.com/browser/devtools/client/inspector/rules/test/doc_test_image.png"); +} +.base64 { + background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=='); +} diff --git a/devtools/client/inspector/rules/test/doc_urls_clickable.html b/devtools/client/inspector/rules/test/doc_urls_clickable.html new file mode 100644 index 000000000..b0265a703 --- /dev/null +++ b/devtools/client/inspector/rules/test/doc_urls_clickable.html @@ -0,0 +1,30 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> + <head> + + <link href="./doc_urls_clickable.css" rel="stylesheet" type="text/css"> + + <style> + .relative2 { + background-image: url(doc_test_image.png); + } + </style> + </head> + <body> + + <div class="relative1">Background image #1 with relative path (loaded from external css)</div> + + <div class="relative2">Background image #2 with relative path (loaded from style tag)</div> + + <div class="absolute">Background image with absolute path (loaded from external css)</div> + + <div class="base64">Background image with base64 url (loaded from external css)</div> + + <div class="inline" style="background: url(doc_test_image.png);">Background image with relative path (loaded from style attribute)</div> + + <div class="inline-resolved" style="background-image: url(./doc_test_image.png)">Background image with resolved relative path (loaded from style attribute)</div> + + <div class="noimage">No background image :(</div> + </body> +</html> diff --git a/devtools/client/inspector/rules/test/head.js b/devtools/client/inspector/rules/test/head.js new file mode 100644 index 000000000..5e5ede09b --- /dev/null +++ b/devtools/client/inspector/rules/test/head.js @@ -0,0 +1,840 @@ +/* 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); + +registerCleanupFunction(() => { + Services.prefs.clearUserPref("devtools.defaultColorUnit"); +}); + +var {getInplaceEditorForSpan: inplaceEditor} = + require("devtools/client/shared/inplace-editor"); + +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"); + +registerCleanupFunction(() => { + Services.prefs.clearUserPref("devtools.defaultColorUnit"); +}); + +/** + * 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}); +} + +/** + * Get an element's inline style property value. + * @param {TestActor} testActor + * @param {String} selector + * The selector used to obtain the element. + * @param {String} name + * name of the property. + */ +function getStyle(testActor, selector, propName) { + return testActor.eval(` + content.document.querySelector("${selector}") + .style.getPropertyValue("${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; +}); + +/** + * When a tooltip is closed, this ends up "commiting" the value changed within + * the tooltip (e.g. the color in case of a colorpicker) which, in turn, ends up + * setting the value of the corresponding css property in the rule-view. + * Use this function to close the tooltip and make sure the test waits for the + * ruleview-changed event. + * @param {SwatchBasedEditorTooltip} editorTooltip + * @param {CSSRuleView} view + */ +function* hideTooltipAndWaitForRuleViewChanged(editorTooltip, view) { + let onModified = view.once("ruleview-changed"); + let onHidden = editorTooltip.tooltip.once("hidden"); + editorTooltip.hide(); + yield onModified; + yield onHidden; +} + +/** + * Polls a given generator function waiting for it to return true. + * + * @param {Function} validatorFn + * A validator generator 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 + */ +var waitForSuccess = Task.async(function* (validatorFn, desc = "untitled") { + let i = 0; + while (true) { + info("Checking: " + desc); + if (yield validatorFn()) { + ok(true, "Success: " + desc); + break; + } + i++; + if (i > 10) { + ok(false, "Failure: " + desc); + break; + } + yield new Promise(r => setTimeout(r, 200)); + } +}); + +/** + * 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. + * Note that this function assumes that the colorpicker popup is already open + * and it won't close it after having selected the new color. + * + * @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: + * - {String} selector The selector to 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 onComputedStyleChanged; + if (expectedChange) { + let {selector, name, value} = expectedChange; + onComputedStyleChanged = waitForComputedStyleProperty(selector, null, name, value); + } + let onRuleViewChanged = ruleView.once("ruleview-changed"); + info("Getting the spectrum colorpicker object"); + let spectrum = 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 onComputedStyleChanged; + } +}); + +/** + * Open the color picker popup for a given property in a given rule and + * simulate a color change. Optionally wait for a given element in the page to + * have its style changed as a result. + * + * @param {RuleView} view + * The related rule view instance + * @param {Number} ruleIndex + * Which rule to target in the rule view + * @param {Number} propIndex + * Which property to target in the rule + * @param {Array} newRgba + * The new color to be set [r, g, b, a] + * @param {Object} expectedChange + * Optional object that needs the following props: + * - {String} selector The selector to 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 openColorPickerAndSelectColor = Task.async(function* (view, ruleIndex, + propIndex, newRgba, expectedChange) { + let ruleEditor = getRuleViewRuleEditor(view, ruleIndex); + let propEditor = ruleEditor.rule.textProps[propIndex].editor; + let swatch = propEditor.valueSpan.querySelector(".ruleview-colorswatch"); + let cPicker = view.tooltips.colorPicker; + + info("Opening the colorpicker by clicking the color swatch"); + let onColorPickerReady = cPicker.once("ready"); + swatch.click(); + yield onColorPickerReady; + + yield simulateColorPickerChange(view, cPicker, newRgba, expectedChange); + + return {propEditor, swatch, cPicker}; +}); + +/** + * Open the cubicbezier popup for a given property in a given rule and + * simulate a curve change. Optionally wait for a given element in the page to + * have its style changed as a result. + * + * @param {RuleView} view + * The related rule view instance + * @param {Number} ruleIndex + * Which rule to target in the rule view + * @param {Number} propIndex + * Which property to target in the rule + * @param {Array} coords + * The new coordinates to be used, e.g. [0.1, 2, 0.9, -1] + * @param {Object} expectedChange + * Optional object that needs the following props: + * - {String} selector The selector to 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 openCubicBezierAndChangeCoords = Task.async(function* (view, ruleIndex, + propIndex, coords, expectedChange) { + let ruleEditor = getRuleViewRuleEditor(view, ruleIndex); + let propEditor = ruleEditor.rule.textProps[propIndex].editor; + let swatch = propEditor.valueSpan.querySelector(".ruleview-bezierswatch"); + let bezierTooltip = view.tooltips.cubicBezier; + + info("Opening the cubicBezier by clicking the swatch"); + let onBezierWidgetReady = bezierTooltip.once("ready"); + swatch.click(); + yield onBezierWidgetReady; + + let widget = yield bezierTooltip.widget; + + info("Simulating a change of curve in the widget"); + let onRuleViewChanged = view.once("ruleview-changed"); + widget.coordinates = coords; + yield onRuleViewChanged; + + if (expectedChange) { + info("Waiting for the style to be applied on the page"); + let {selector, name, value} = expectedChange; + yield waitForComputedStyleProperty(selector, null, name, value); + } + + return {propEditor, swatch, bezierTooltip}; +}); + +/** + * 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; +} + +/** + * Simulate adding a new property in an existing rule in the rule-view. + * + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {Number} ruleIndex + * The index of the rule to use. Note that if ruleIndex is 0, you might + * want to also listen to markupmutation events in your test since + * that's going to change the style attribute of the selected node. + * @param {String} name + * The name for the new property + * @param {String} value + * The value for the new property + * @param {String} commitValueWith + * Which key should be used to commit the new value. VK_RETURN is used by + * default, but tests might want to use another key to test cancelling + * for exemple. + * @param {Boolean} blurNewProperty + * After the new value has been added, a new property would have been + * focused. This parameter is true by default, and that causes the new + * property to be blurred. Set to false if you don't want this. + * @return {TextProperty} The instance of the TextProperty that was added + */ +var addProperty = Task.async(function* (view, ruleIndex, name, value, + commitValueWith = "VK_RETURN", + blurNewProperty = true) { + info("Adding new property " + name + ":" + value + " to rule " + ruleIndex); + + let ruleEditor = getRuleViewRuleEditor(view, ruleIndex); + let editor = yield focusNewRuleViewProperty(ruleEditor); + let numOfProps = ruleEditor.rule.textProps.length; + + info("Adding name " + name); + editor.input.value = name; + let onNameAdded = view.once("ruleview-changed"); + EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow); + yield onNameAdded; + + // Focus has moved to the value inplace-editor automatically. + editor = inplaceEditor(view.styleDocument.activeElement); + let textProps = ruleEditor.rule.textProps; + let textProp = textProps[textProps.length - 1]; + + is(ruleEditor.rule.textProps.length, numOfProps + 1, + "A new test property was added"); + is(editor, inplaceEditor(textProp.editor.valueSpan), + "The inplace editor appeared for the value"); + + info("Adding value " + value); + // Setting the input value schedules a preview to be shown in 10ms which + // triggers a ruleview-changed event (see bug 1209295). + let onPreview = view.once("ruleview-changed"); + editor.input.value = value; + view.throttle.flush(); + yield onPreview; + + let onValueAdded = view.once("ruleview-changed"); + EventUtils.synthesizeKey(commitValueWith, {}, view.styleWindow); + yield onValueAdded; + + if (blurNewProperty) { + view.styleDocument.activeElement.blur(); + } + + return textProp; +}); + +/** + * Simulate changing the value of a property in a rule in the rule-view. + * + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {TextProperty} textProp + * The instance of the TextProperty to be changed + * @param {String} value + * The new value to be used. If null is passed, then the value will be + * deleted + * @param {Boolean} blurNewProperty + * After the value has been changed, a new property would have been + * focused. This parameter is true by default, and that causes the new + * property to be blurred. Set to false if you don't want this. + */ +var setProperty = Task.async(function* (view, textProp, value, + blurNewProperty = true) { + yield focusEditableField(view, textProp.editor.valueSpan); + + let onPreview = view.once("ruleview-changed"); + if (value === null) { + EventUtils.synthesizeKey("VK_DELETE", {}, view.styleWindow); + } else { + EventUtils.sendString(value, view.styleWindow); + } + view.throttle.flush(); + yield onPreview; + + let onValueDone = view.once("ruleview-changed"); + EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow); + yield onValueDone; + + if (blurNewProperty) { + view.styleDocument.activeElement.blur(); + } +}); + +/** + * Simulate removing a property from an existing rule in the rule-view. + * + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {TextProperty} textProp + * The instance of the TextProperty to be removed + * @param {Boolean} blurNewProperty + * After the property has been removed, a new property would have been + * focused. This parameter is true by default, and that causes the new + * property to be blurred. Set to false if you don't want this. + */ +var removeProperty = Task.async(function* (view, textProp, + blurNewProperty = true) { + yield focusEditableField(view, textProp.editor.nameSpan); + + let onModifications = view.once("ruleview-changed"); + info("Deleting the property name now"); + EventUtils.synthesizeKey("VK_DELETE", {}, view.styleWindow); + EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow); + yield onModifications; + + if (blurNewProperty) { + view.styleDocument.activeElement.blur(); + } +}); + +/** + * Simulate clicking the enable/disable checkbox next to a property in a rule. + * + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {TextProperty} textProp + * The instance of the TextProperty to be enabled/disabled + */ +var togglePropStatus = Task.async(function* (view, textProp) { + let onRuleViewRefreshed = view.once("ruleview-changed"); + textProp.editor.enable.click(); + yield onRuleViewRefreshed; +}); + +/** + * 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"); + + // Use bottom alignment to avoid scrolling out of the parent element area. + ruleEditor.closeBrace.scrollIntoView(false); + 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"); +}); + +/** + * Reload the current page and wait for the inspector to be initialized after + * the navigation + * + * @param {InspectorPanel} inspector + * The instance of InspectorPanel currently loaded in the toolbox + * @param {TestActor} testActor + * The current instance of the TestActor + */ +function* reloadPage(inspector, testActor) { + let onNewRoot = inspector.once("new-root"); + yield testActor.reload(); + yield onNewRoot; + yield inspector.markup._waitForChildren(); +} + +/** + * Create a new rule by clicking on the "add rule" button. + * This will leave the selector inplace-editor active. + * + * @param {InspectorPanel} inspector + * The instance of InspectorPanel currently loaded in the toolbox + * @param {CssRuleView} view + * The instance of the rule-view panel + * @return a promise that resolves after the rule has been added + */ +function* addNewRule(inspector, view) { + info("Adding the new rule using the button"); + view.addRuleButton.click(); + + info("Waiting for rule view to change"); + yield view.once("ruleview-changed"); +} + +/** + * Create a new rule by clicking on the "add rule" button, dismiss the editor field and + * verify that the selector is correct. + * + * @param {InspectorPanel} inspector + * The instance of InspectorPanel currently loaded in the toolbox + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {String} expectedSelector + * The value we expect the selector to have + * @param {Number} expectedIndex + * The index we expect the rule to have in the rule-view + * @return a promise that resolves after the rule has been added + */ +function* addNewRuleAndDismissEditor(inspector, view, expectedSelector, expectedIndex) { + yield addNewRule(inspector, view); + + info("Getting the new rule at index " + expectedIndex); + let ruleEditor = getRuleViewRuleEditor(view, expectedIndex); + let editor = ruleEditor.selectorText.ownerDocument.activeElement; + is(editor.value, expectedSelector, + "The editor for the new selector has the correct value: " + expectedSelector); + + info("Pressing escape to leave the editor"); + EventUtils.synthesizeKey("VK_ESCAPE", {}); + + is(ruleEditor.selectorText.textContent, expectedSelector, + "The new selector has the correct text: " + expectedSelector); +} + +/** + * Simulate a sequence of non-character keys (return, escape, tab) and wait for + * a given element to receive the focus. + * + * @param {CssRuleView} view + * The instance of the rule-view panel + * @param {DOMNode} element + * The element that should be focused + * @param {Array} keys + * Array of non-character keys, the part that comes after "DOM_VK_" eg. + * "RETURN", "ESCAPE" + * @return a promise that resolves after the element received the focus + */ +function* sendKeysAndWaitForFocus(view, element, keys) { + let onFocus = once(element, "focus", true); + for (let key of keys) { + EventUtils.sendKey(key, view.styleWindow); + } + yield onFocus; +} + +/** + * 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; +} + +/** + * Wait for a markupmutation event on the inspector that is for a style modification. + * @param {InspectorPanel} inspector + * @return {Promise} + */ +function waitForStyleModification(inspector) { + return new Promise(function (resolve) { + function checkForStyleModification(name, mutations) { + for (let mutation of mutations) { + if (mutation.type === "attributes" && mutation.attributeName === "style") { + inspector.off("markupmutation", checkForStyleModification); + resolve(); + return; + } + } + } + inspector.on("markupmutation", checkForStyleModification); + }); +} + +/** + * Click on the selector icon + * @param {DOMNode} icon + * @param {CSSRuleView} view + */ +function* clickSelectorIcon(icon, view) { + let onToggled = view.once("ruleview-selectorhighlighter-toggled"); + EventUtils.synthesizeMouseAtCenter(icon, {}, view.styleWindow); + yield onToggled; +} + +/** + * Make sure window is properly focused before sending a key event. + * @param {Window} win + * @param {Event} key + */ +function focusAndSendKey(win, key) { + win.document.documentElement.focus(); + EventUtils.sendKey(key, win); +} diff --git a/devtools/client/inspector/rules/views/moz.build b/devtools/client/inspector/rules/views/moz.build new file mode 100644 index 000000000..ac0a24d76 --- /dev/null +++ b/devtools/client/inspector/rules/views/moz.build @@ -0,0 +1,8 @@ +# 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( + 'rule-editor.js', + 'text-property-editor.js', +) diff --git a/devtools/client/inspector/rules/views/rule-editor.js b/devtools/client/inspector/rules/views/rule-editor.js new file mode 100644 index 000000000..2587bf19c --- /dev/null +++ b/devtools/client/inspector/rules/views/rule-editor.js @@ -0,0 +1,620 @@ +/* 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 {l10n} = require("devtools/shared/inspector/css-logic"); +const {ELEMENT_STYLE} = require("devtools/shared/specs/styles"); +const {PREF_ORIG_SOURCES} = require("devtools/client/styleeditor/utils"); +const {Rule} = require("devtools/client/inspector/rules/models/rule"); +const {InplaceEditor, editableField, editableItem} = + require("devtools/client/shared/inplace-editor"); +const {TextPropertyEditor} = + require("devtools/client/inspector/rules/views/text-property-editor"); +const { + createChild, + blurOnMultipleProperties, + promiseWarn +} = require("devtools/client/inspector/shared/utils"); +const { + parseDeclarations, + parsePseudoClassesAndAttributes, + SELECTOR_ATTRIBUTE, + SELECTOR_ELEMENT, + SELECTOR_PSEUDO_CLASS +} = require("devtools/shared/css/parsing-utils"); +const promise = require("promise"); +const Services = require("Services"); +const EventEmitter = require("devtools/shared/event-emitter"); +const {Task} = require("devtools/shared/task"); + +const STYLE_INSPECTOR_PROPERTIES = "devtools/shared/locales/styleinspector.properties"; +const {LocalizationHelper} = require("devtools/shared/l10n"); +const STYLE_INSPECTOR_L10N = new LocalizationHelper(STYLE_INSPECTOR_PROPERTIES); + +/** + * RuleEditor is responsible for the following: + * Owns a Rule object and creates a list of TextPropertyEditors + * for its TextProperties. + * Manages creation of new text properties. + * + * One step of a RuleEditor's instantiation is figuring out what's the original + * source link to the parent stylesheet (in case of source maps). This step is + * asynchronous and is triggered as soon as the RuleEditor is instantiated (see + * updateSourceLink). If you need to know when the RuleEditor is done with this, + * you need to listen to the source-link-updated event. + * + * @param {CssRuleView} ruleView + * The CssRuleView containg the document holding this rule editor. + * @param {Rule} rule + * The Rule object we're editing. + */ +function RuleEditor(ruleView, rule) { + EventEmitter.decorate(this); + + this.ruleView = ruleView; + this.doc = this.ruleView.styleDocument; + this.toolbox = this.ruleView.inspector.toolbox; + this.rule = rule; + + this.isEditable = !rule.isSystem; + // Flag that blocks updates of the selector and properties when it is + // being edited + this.isEditing = false; + + this._onNewProperty = this._onNewProperty.bind(this); + this._newPropertyDestroy = this._newPropertyDestroy.bind(this); + this._onSelectorDone = this._onSelectorDone.bind(this); + this._locationChanged = this._locationChanged.bind(this); + this.updateSourceLink = this.updateSourceLink.bind(this); + + this.rule.domRule.on("location-changed", this._locationChanged); + this.toolbox.on("tool-registered", this.updateSourceLink); + this.toolbox.on("tool-unregistered", this.updateSourceLink); + + this._create(); +} + +RuleEditor.prototype = { + destroy: function () { + this.rule.domRule.off("location-changed"); + this.toolbox.off("tool-registered", this.updateSourceLink); + this.toolbox.off("tool-unregistered", this.updateSourceLink); + }, + + get isSelectorEditable() { + let trait = this.isEditable && + this.ruleView.inspector.target.client.traits.selectorEditable && + this.rule.domRule.type !== ELEMENT_STYLE && + this.rule.domRule.type !== CSSRule.KEYFRAME_RULE; + + // Do not allow editing anonymousselectors until we can + // detect mutations on pseudo elements in Bug 1034110. + return trait && !this.rule.elementStyle.element.isAnonymous; + }, + + _create: function () { + this.element = this.doc.createElement("div"); + this.element.className = "ruleview-rule theme-separator"; + this.element.setAttribute("uneditable", !this.isEditable); + this.element.setAttribute("unmatched", this.rule.isUnmatched); + this.element._ruleEditor = this; + + // Give a relative position for the inplace editor's measurement + // span to be placed absolutely against. + this.element.style.position = "relative"; + + // Add the source link. + this.source = createChild(this.element, "div", { + class: "ruleview-rule-source theme-link" + }); + this.source.addEventListener("click", function () { + if (this.source.hasAttribute("unselectable")) { + return; + } + let rule = this.rule.domRule; + this.ruleView.emit("ruleview-linked-clicked", rule); + }.bind(this)); + let sourceLabel = this.doc.createElement("span"); + sourceLabel.classList.add("ruleview-rule-source-label"); + this.source.appendChild(sourceLabel); + + this.updateSourceLink(); + + let code = createChild(this.element, "div", { + class: "ruleview-code" + }); + + let header = createChild(code, "div", {}); + + this.selectorText = createChild(header, "span", { + class: "ruleview-selectorcontainer theme-fg-color3", + tabindex: this.isSelectorEditable ? "0" : "-1", + }); + + if (this.isSelectorEditable) { + this.selectorText.addEventListener("click", event => { + // Clicks within the selector shouldn't propagate any further. + event.stopPropagation(); + }, false); + + editableField({ + element: this.selectorText, + done: this._onSelectorDone, + cssProperties: this.rule.cssProperties, + contextMenu: this.ruleView.inspector.onTextBoxContextMenu + }); + } + + if (this.rule.domRule.type !== CSSRule.KEYFRAME_RULE) { + let selector = this.rule.domRule.selectors + ? this.rule.domRule.selectors.join(", ") + : this.ruleView.inspector.selectionCssSelector; + + let selectorHighlighter = createChild(header, "span", { + class: "ruleview-selectorhighlighter" + + (this.ruleView.highlighters.selectorHighlighterShown === selector ? + " highlighted" : ""), + title: l10n("rule.selectorHighlighter.tooltip") + }); + selectorHighlighter.addEventListener("click", () => { + this.ruleView.toggleSelectorHighlighter(selectorHighlighter, selector); + }); + } + + this.openBrace = createChild(header, "span", { + class: "ruleview-ruleopen", + textContent: " {" + }); + + this.propertyList = createChild(code, "ul", { + class: "ruleview-propertylist" + }); + + this.populate(); + + this.closeBrace = createChild(code, "div", { + class: "ruleview-ruleclose", + tabindex: this.isEditable ? "0" : "-1", + textContent: "}" + }); + + if (this.isEditable) { + // A newProperty editor should only be created when no editor was + // previously displayed. Since the editors are cleared on blur, + // check this.ruleview.isEditing on mousedown + this._ruleViewIsEditing = false; + + code.addEventListener("mousedown", () => { + this._ruleViewIsEditing = this.ruleView.isEditing; + }); + + code.addEventListener("click", () => { + let selection = this.doc.defaultView.getSelection(); + if (selection.isCollapsed && !this._ruleViewIsEditing) { + this.newProperty(); + } + // Cleanup the _ruleViewIsEditing flag + this._ruleViewIsEditing = false; + }, false); + + this.element.addEventListener("mousedown", () => { + this.doc.defaultView.focus(); + }, false); + + // Create a property editor when the close brace is clicked. + editableItem({ element: this.closeBrace }, () => { + this.newProperty(); + }); + } + }, + + /** + * Event handler called when a property changes on the + * StyleRuleActor. + */ + _locationChanged: function () { + this.updateSourceLink(); + }, + + updateSourceLink: function () { + let sourceLabel = this.element.querySelector(".ruleview-rule-source-label"); + let title = this.rule.title; + let sourceHref = (this.rule.sheet && this.rule.sheet.href) ? + this.rule.sheet.href : title; + let sourceLine = this.rule.ruleLine > 0 ? ":" + this.rule.ruleLine : ""; + + sourceLabel.setAttribute("title", sourceHref + sourceLine); + + if (this.toolbox.isToolRegistered("styleeditor")) { + this.source.removeAttribute("unselectable"); + } else { + this.source.setAttribute("unselectable", true); + } + + if (this.rule.isSystem) { + let uaLabel = STYLE_INSPECTOR_L10N.getStr("rule.userAgentStyles"); + sourceLabel.textContent = uaLabel + " " + title; + + // Special case about:PreferenceStyleSheet, as it is generated on the + // fly and the URI is not registered with the about: handler. + // https://bugzilla.mozilla.org/show_bug.cgi?id=935803#c37 + if (sourceHref === "about:PreferenceStyleSheet") { + this.source.setAttribute("unselectable", "true"); + sourceLabel.textContent = uaLabel; + sourceLabel.removeAttribute("title"); + } + } else { + sourceLabel.textContent = title; + if (this.rule.ruleLine === -1 && this.rule.domRule.parentStyleSheet) { + this.source.setAttribute("unselectable", "true"); + } + } + + let showOrig = Services.prefs.getBoolPref(PREF_ORIG_SOURCES); + if (showOrig && !this.rule.isSystem && + this.rule.domRule.type !== ELEMENT_STYLE) { + // Only get the original source link if the right pref is set, if the rule + // isn't a system rule and if it isn't an inline rule. + this.rule.getOriginalSourceStrings().then((strings) => { + sourceLabel.textContent = strings.short; + sourceLabel.setAttribute("title", strings.full); + }, e => console.error(e)).then(() => { + this.emit("source-link-updated"); + }); + } else { + // If we're not getting the original source link, then we can emit the + // event immediately (but still asynchronously to give consumers a chance + // to register it after having instantiated the RuleEditor). + promise.resolve().then(() => { + this.emit("source-link-updated"); + }); + } + }, + + /** + * Update the rule editor with the contents of the rule. + */ + populate: function () { + // Clear out existing viewers. + while (this.selectorText.hasChildNodes()) { + this.selectorText.removeChild(this.selectorText.lastChild); + } + + // If selector text comes from a css rule, highlight selectors that + // actually match. For custom selector text (such as for the 'element' + // style, just show the text directly. + if (this.rule.domRule.type === ELEMENT_STYLE) { + this.selectorText.textContent = this.rule.selectorText; + } else if (this.rule.domRule.type === CSSRule.KEYFRAME_RULE) { + this.selectorText.textContent = this.rule.domRule.keyText; + } else { + this.rule.domRule.selectors.forEach((selector, i) => { + if (i !== 0) { + createChild(this.selectorText, "span", { + class: "ruleview-selector-separator", + textContent: ", " + }); + } + + let containerClass = + (this.rule.matchedSelectors.indexOf(selector) > -1) ? + "ruleview-selector-matched" : "ruleview-selector-unmatched"; + let selectorContainer = createChild(this.selectorText, "span", { + class: containerClass + }); + + let parsedSelector = parsePseudoClassesAndAttributes(selector); + + for (let selectorText of parsedSelector) { + let selectorClass = ""; + + switch (selectorText.type) { + case SELECTOR_ATTRIBUTE: + selectorClass = "ruleview-selector-attribute"; + break; + case SELECTOR_ELEMENT: + selectorClass = "ruleview-selector"; + break; + case SELECTOR_PSEUDO_CLASS: + selectorClass = [":active", ":focus", ":hover"].some( + pseudo => selectorText.value === pseudo) ? + "ruleview-selector-pseudo-class-lock" : + "ruleview-selector-pseudo-class"; + break; + default: + break; + } + + createChild(selectorContainer, "span", { + textContent: selectorText.value, + class: selectorClass + }); + } + }); + } + + for (let prop of this.rule.textProps) { + if (!prop.editor && !prop.invisible) { + let editor = new TextPropertyEditor(this, prop); + this.propertyList.appendChild(editor.element); + } + } + }, + + /** + * Programatically add a new property to the rule. + * + * @param {String} name + * Property name. + * @param {String} value + * Property value. + * @param {String} priority + * Property priority. + * @param {Boolean} enabled + * True if the property should be enabled. + * @param {TextProperty} siblingProp + * Optional, property next to which the new property will be added. + * @return {TextProperty} + * The new property + */ + addProperty: function (name, value, priority, enabled, siblingProp) { + let prop = this.rule.createProperty(name, value, priority, enabled, + siblingProp); + let index = this.rule.textProps.indexOf(prop); + let editor = new TextPropertyEditor(this, prop); + + // Insert this node before the DOM node that is currently at its new index + // in the property list. There is currently one less node in the DOM than + // in the property list, so this causes it to appear after siblingProp. + // If there is no node at its index, as is the case where this is the last + // node being inserted, then this behaves as appendChild. + this.propertyList.insertBefore(editor.element, + this.propertyList.children[index]); + + return prop; + }, + + /** + * Programatically add a list of new properties to the rule. Focus the UI + * to the proper location after adding (either focus the value on the + * last property if it is empty, or create a new property and focus it). + * + * @param {Array} properties + * Array of properties, which are objects with this signature: + * { + * name: {string}, + * value: {string}, + * priority: {string} + * } + * @param {TextProperty} siblingProp + * Optional, the property next to which all new props should be added. + */ + addProperties: function (properties, siblingProp) { + if (!properties || !properties.length) { + return; + } + + let lastProp = siblingProp; + for (let p of properties) { + let isCommented = Boolean(p.commentOffsets); + let enabled = !isCommented; + lastProp = this.addProperty(p.name, p.value, p.priority, enabled, + lastProp); + } + + // Either focus on the last value if incomplete, or start a new one. + if (lastProp && lastProp.value.trim() === "") { + lastProp.editor.valueSpan.click(); + } else { + this.newProperty(); + } + }, + + /** + * Create a text input for a property name. If a non-empty property + * name is given, we'll create a real TextProperty and add it to the + * rule. + */ + newProperty: function () { + // If we're already creating a new property, ignore this. + if (!this.closeBrace.hasAttribute("tabindex")) { + return; + } + + // While we're editing a new property, it doesn't make sense to + // start a second new property editor, so disable focusing the + // close brace for now. + this.closeBrace.removeAttribute("tabindex"); + + this.newPropItem = createChild(this.propertyList, "li", { + class: "ruleview-property ruleview-newproperty", + }); + + this.newPropSpan = createChild(this.newPropItem, "span", { + class: "ruleview-propertyname", + tabindex: "0" + }); + + this.multipleAddedProperties = null; + + this.editor = new InplaceEditor({ + element: this.newPropSpan, + done: this._onNewProperty, + destroy: this._newPropertyDestroy, + advanceChars: ":", + contentType: InplaceEditor.CONTENT_TYPES.CSS_PROPERTY, + popup: this.ruleView.popup, + cssProperties: this.rule.cssProperties, + contextMenu: this.ruleView.inspector.onTextBoxContextMenu + }); + + // Auto-close the input if multiple rules get pasted into new property. + this.editor.input.addEventListener("paste", + blurOnMultipleProperties(this.rule.cssProperties), false); + }, + + /** + * Called when the new property input has been dismissed. + * + * @param {String} value + * The value in the editor. + * @param {Boolean} commit + * True if the value should be committed. + */ + _onNewProperty: function (value, commit) { + if (!value || !commit) { + return; + } + + // parseDeclarations allows for name-less declarations, but in the present + // case, we're creating a new declaration, it doesn't make sense to accept + // these entries + this.multipleAddedProperties = + parseDeclarations(this.rule.cssProperties.isKnown, value, true) + .filter(d => d.name); + + // Blur the editor field now and deal with adding declarations later when + // the field gets destroyed (see _newPropertyDestroy) + this.editor.input.blur(); + }, + + /** + * Called when the new property editor is destroyed. + * This is where the properties (type TextProperty) are actually being + * added, since we want to wait until after the inplace editor `destroy` + * event has been fired to keep consistent UI state. + */ + _newPropertyDestroy: function () { + // We're done, make the close brace focusable again. + this.closeBrace.setAttribute("tabindex", "0"); + + this.propertyList.removeChild(this.newPropItem); + delete this.newPropItem; + delete this.newPropSpan; + + // If properties were added, we want to focus the proper element. + // If the last new property has no value, focus the value on it. + // Otherwise, start a new property and focus that field. + if (this.multipleAddedProperties && this.multipleAddedProperties.length) { + this.addProperties(this.multipleAddedProperties); + } + }, + + /** + * Called when the selector's inplace editor is closed. + * Ignores the change if the user pressed escape, otherwise + * commits it. + * + * @param {String} value + * The value contained in the editor. + * @param {Boolean} commit + * True if the change should be applied. + * @param {Number} direction + * The move focus direction number. + */ + _onSelectorDone: Task.async(function* (value, commit, direction) { + if (!commit || this.isEditing || value === "" || + value === this.rule.selectorText) { + return; + } + + let ruleView = this.ruleView; + let elementStyle = ruleView._elementStyle; + let element = elementStyle.element; + let supportsUnmatchedRules = + this.rule.domRule.supportsModifySelectorUnmatched; + + this.isEditing = true; + + try { + let response = yield this.rule.domRule.modifySelector(element, value); + + if (!supportsUnmatchedRules) { + this.isEditing = false; + + if (response) { + this.ruleView.refreshPanel(); + } + return; + } + + // We recompute the list of applied styles, because editing a + // selector might cause this rule's position to change. + let applied = yield elementStyle.pageStyle.getApplied(element, { + inherited: true, + matchedSelectors: true, + filter: elementStyle.showUserAgentStyles ? "ua" : undefined + }); + + this.isEditing = false; + + let {ruleProps, isMatching} = response; + if (!ruleProps) { + // Notify for changes, even when nothing changes, + // just to allow tests being able to track end of this request. + ruleView.emit("ruleview-invalid-selector"); + return; + } + + ruleProps.isUnmatched = !isMatching; + let newRule = new Rule(elementStyle, ruleProps); + let editor = new RuleEditor(ruleView, newRule); + let rules = elementStyle.rules; + + let newRuleIndex = applied.findIndex((r) => r.rule == ruleProps.rule); + let oldIndex = rules.indexOf(this.rule); + + // If the selector no longer matches, then we leave the rule in + // the same relative position. + if (newRuleIndex === -1) { + newRuleIndex = oldIndex; + } + + // Remove the old rule and insert the new rule. + rules.splice(oldIndex, 1); + rules.splice(newRuleIndex, 0, newRule); + elementStyle._changed(); + elementStyle.markOverriddenAll(); + + // We install the new editor in place of the old -- you might + // think we would replicate the list-modification logic above, + // but that is complicated due to the way the UI installs + // pseudo-element rules and the like. + this.element.parentNode.replaceChild(editor.element, this.element); + + // Remove highlight for modified selector + if (ruleView.highlighters.selectorHighlighterShown) { + ruleView.toggleSelectorHighlighter(ruleView.lastSelectorIcon, + ruleView.highlighters.selectorHighlighterShown); + } + + editor._moveSelectorFocus(direction); + } catch (err) { + this.isEditing = false; + promiseWarn(err); + } + }), + + /** + * Handle moving the focus change after a tab or return keypress in the + * selector inplace editor. + * + * @param {Number} direction + * The move focus direction number. + */ + _moveSelectorFocus: function (direction) { + if (!direction || direction === Services.focus.MOVEFOCUS_BACKWARD) { + return; + } + + if (this.rule.textProps.length > 0) { + this.rule.textProps[0].editor.nameSpan.click(); + } else { + this.propertyList.click(); + } + } +}; + +exports.RuleEditor = RuleEditor; diff --git a/devtools/client/inspector/rules/views/text-property-editor.js b/devtools/client/inspector/rules/views/text-property-editor.js new file mode 100644 index 000000000..d3015f931 --- /dev/null +++ b/devtools/client/inspector/rules/views/text-property-editor.js @@ -0,0 +1,880 @@ +/* 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 {l10n} = require("devtools/shared/inspector/css-logic"); +const {getCssProperties} = require("devtools/shared/fronts/css-properties"); +const {InplaceEditor, editableField} = + require("devtools/client/shared/inplace-editor"); +const { + createChild, + appendText, + advanceValidate, + blurOnMultipleProperties +} = require("devtools/client/inspector/shared/utils"); +const { + parseDeclarations, + parseSingleValue, +} = require("devtools/shared/css/parsing-utils"); +const Services = require("Services"); + +const HTML_NS = "http://www.w3.org/1999/xhtml"; + +const SHARED_SWATCH_CLASS = "ruleview-swatch"; +const COLOR_SWATCH_CLASS = "ruleview-colorswatch"; +const BEZIER_SWATCH_CLASS = "ruleview-bezierswatch"; +const FILTER_SWATCH_CLASS = "ruleview-filterswatch"; +const ANGLE_SWATCH_CLASS = "ruleview-angleswatch"; + +/* + * An actionable element is an element which on click triggers a specific action + * (e.g. shows a color tooltip, opens a link, …). + */ +const ACTIONABLE_ELEMENTS_SELECTORS = [ + `.${COLOR_SWATCH_CLASS}`, + `.${BEZIER_SWATCH_CLASS}`, + `.${FILTER_SWATCH_CLASS}`, + `.${ANGLE_SWATCH_CLASS}`, + "a" +]; + +/** + * TextPropertyEditor is responsible for the following: + * Owns a TextProperty object. + * Manages changes to the TextProperty. + * Can be expanded to display computed properties. + * Can mark a property disabled or enabled. + * + * @param {RuleEditor} ruleEditor + * The rule editor that owns this TextPropertyEditor. + * @param {TextProperty} property + * The text property to edit. + */ +function TextPropertyEditor(ruleEditor, property) { + this.ruleEditor = ruleEditor; + this.ruleView = this.ruleEditor.ruleView; + this.doc = this.ruleEditor.doc; + this.popup = this.ruleView.popup; + this.prop = property; + this.prop.editor = this; + this.browserWindow = this.doc.defaultView.top; + this._populatedComputed = false; + this._hasPendingClick = false; + this._clickedElementOptions = null; + + const toolbox = this.ruleView.inspector.toolbox; + this.cssProperties = getCssProperties(toolbox); + + this._onEnableClicked = this._onEnableClicked.bind(this); + this._onExpandClicked = this._onExpandClicked.bind(this); + this._onStartEditing = this._onStartEditing.bind(this); + this._onNameDone = this._onNameDone.bind(this); + this._onValueDone = this._onValueDone.bind(this); + this._onSwatchCommit = this._onSwatchCommit.bind(this); + this._onSwatchPreview = this._onSwatchPreview.bind(this); + this._onSwatchRevert = this._onSwatchRevert.bind(this); + this._onValidate = this.ruleView.throttle(this._previewValue, 10, this); + this.update = this.update.bind(this); + this.updatePropertyState = this.updatePropertyState.bind(this); + + this._create(); + this.update(); +} + +TextPropertyEditor.prototype = { + /** + * Boolean indicating if the name or value is being currently edited. + */ + get editing() { + return !!(this.nameSpan.inplaceEditor || this.valueSpan.inplaceEditor || + this.ruleView.tooltips.isEditing) || this.popup.isOpen; + }, + + /** + * Get the rule to the current text property + */ + get rule() { + return this.prop.rule; + }, + + /** + * Create the property editor's DOM. + */ + _create: function () { + this.element = this.doc.createElementNS(HTML_NS, "li"); + this.element.classList.add("ruleview-property"); + this.element._textPropertyEditor = this; + + this.container = createChild(this.element, "div", { + class: "ruleview-propertycontainer" + }); + + // The enable checkbox will disable or enable the rule. + this.enable = createChild(this.container, "div", { + class: "ruleview-enableproperty theme-checkbox", + tabindex: "-1" + }); + + // Click to expand the computed properties of the text property. + this.expander = createChild(this.container, "span", { + class: "ruleview-expander theme-twisty" + }); + this.expander.addEventListener("click", this._onExpandClicked, true); + + this.nameContainer = createChild(this.container, "span", { + class: "ruleview-namecontainer" + }); + + // Property name, editable when focused. Property name + // is committed when the editor is unfocused. + this.nameSpan = createChild(this.nameContainer, "span", { + class: "ruleview-propertyname theme-fg-color5", + tabindex: this.ruleEditor.isEditable ? "0" : "-1", + }); + + appendText(this.nameContainer, ": "); + + // Create a span that will hold the property and semicolon. + // Use this span to create a slightly larger click target + // for the value. + this.valueContainer = createChild(this.container, "span", { + class: "ruleview-propertyvaluecontainer" + }); + + // Property value, editable when focused. Changes to the + // property value are applied as they are typed, and reverted + // if the user presses escape. + this.valueSpan = createChild(this.valueContainer, "span", { + class: "ruleview-propertyvalue theme-fg-color1", + tabindex: this.ruleEditor.isEditable ? "0" : "-1", + }); + + // Storing the TextProperty on the elements for easy access + // (for instance by the tooltip) + this.valueSpan.textProperty = this.prop; + this.nameSpan.textProperty = this.prop; + + // If the value is a color property we need to put it through the parser + // so that colors can be coerced into the default color type. This prevents + // us from thinking that when colors are coerced they have been changed by + // the user. + let outputParser = this.ruleView._outputParser; + let frag = outputParser.parseCssProperty(this.prop.name, this.prop.value); + let parsedValue = frag.textContent; + + // Save the initial value as the last committed value, + // for restoring after pressing escape. + this.committed = { name: this.prop.name, + value: parsedValue, + priority: this.prop.priority }; + + appendText(this.valueContainer, ";"); + + this.warning = createChild(this.container, "div", { + class: "ruleview-warning", + hidden: "", + title: l10n("rule.warning.title"), + }); + + // Filter button that filters for the current property name and is + // displayed when the property is overridden by another rule. + this.filterProperty = createChild(this.container, "div", { + class: "ruleview-overridden-rule-filter", + hidden: "", + title: l10n("rule.filterProperty.title"), + }); + + this.filterProperty.addEventListener("click", event => { + this.ruleEditor.ruleView.setFilterStyles("`" + this.prop.name + "`"); + event.stopPropagation(); + }, false); + + // Holds the viewers for the computed properties. + // will be populated in |_updateComputed|. + this.computed = createChild(this.element, "ul", { + class: "ruleview-computedlist", + }); + + // Only bind event handlers if the rule is editable. + if (this.ruleEditor.isEditable) { + this.enable.addEventListener("click", this._onEnableClicked, true); + + this.nameContainer.addEventListener("click", (event) => { + // Clicks within the name shouldn't propagate any further. + event.stopPropagation(); + + // Forward clicks on nameContainer to the editable nameSpan + if (event.target === this.nameContainer) { + this.nameSpan.click(); + } + }, false); + + editableField({ + start: this._onStartEditing, + element: this.nameSpan, + done: this._onNameDone, + destroy: this.updatePropertyState, + advanceChars: ":", + contentType: InplaceEditor.CONTENT_TYPES.CSS_PROPERTY, + popup: this.popup, + cssProperties: this.cssProperties, + contextMenu: this.ruleView.inspector.onTextBoxContextMenu + }); + + // Auto blur name field on multiple CSS rules get pasted in. + this.nameContainer.addEventListener("paste", + blurOnMultipleProperties(this.cssProperties), false); + + this.valueContainer.addEventListener("click", (event) => { + // Clicks within the value shouldn't propagate any further. + event.stopPropagation(); + + // Forward clicks on valueContainer to the editable valueSpan + if (event.target === this.valueContainer) { + this.valueSpan.click(); + } + }, false); + + // The mousedown event could trigger a blur event on nameContainer, which + // will trigger a call to the update function. The update function clears + // valueSpan's markup. Thus the regular click event does not bubble up, and + // listener's callbacks are not called. + // So we need to remember where the user clicks in order to re-trigger the click + // after the valueSpan's markup is re-populated. We only need to track this for + // valueSpan's child elements, because direct click on valueSpan will always + // trigger a click event. + this.valueSpan.addEventListener("mousedown", (event) => { + let clickedEl = event.target; + if (clickedEl === this.valueSpan) { + return; + } + this._hasPendingClick = true; + + let matchedSelector = ACTIONABLE_ELEMENTS_SELECTORS.find( + (selector) => clickedEl.matches(selector)); + if (matchedSelector) { + let similarElements = [...this.valueSpan.querySelectorAll(matchedSelector)]; + this._clickedElementOptions = { + selector: matchedSelector, + index: similarElements.indexOf(clickedEl) + }; + } + }, false); + + this.valueSpan.addEventListener("mouseup", (event) => { + this._clickedElementOptions = null; + this._hasPendingClick = false; + }, false); + + this.valueSpan.addEventListener("click", (event) => { + let target = event.target; + + if (target.nodeName === "a") { + event.stopPropagation(); + event.preventDefault(); + this.browserWindow.openUILinkIn(target.href, "tab"); + } + }, false); + + editableField({ + start: this._onStartEditing, + element: this.valueSpan, + done: this._onValueDone, + destroy: this.update, + validate: this._onValidate, + advanceChars: advanceValidate, + contentType: InplaceEditor.CONTENT_TYPES.CSS_VALUE, + property: this.prop, + popup: this.popup, + multiline: true, + maxWidth: () => this.container.getBoundingClientRect().width, + cssProperties: this.cssProperties, + contextMenu: this.ruleView.inspector.onTextBoxContextMenu + }); + } + }, + + /** + * Get the path from which to resolve requests for this + * rule's stylesheet. + * + * @return {String} the stylesheet's href. + */ + get sheetHref() { + let domRule = this.rule.domRule; + if (domRule) { + return domRule.href || domRule.nodeHref; + } + return undefined; + }, + + /** + * Populate the span based on changes to the TextProperty. + */ + update: function () { + if (this.ruleView.isDestroyed) { + return; + } + + this.updatePropertyState(); + + let name = this.prop.name; + this.nameSpan.textContent = name; + + // Combine the property's value and priority into one string for + // the value. + let store = this.rule.elementStyle.store; + let val = store.userProperties.getProperty(this.rule.style, name, + this.prop.value); + if (this.prop.priority) { + val += " !" + this.prop.priority; + } + + let propDirty = store.userProperties.contains(this.rule.style, name); + + if (propDirty) { + this.element.setAttribute("dirty", ""); + } else { + this.element.removeAttribute("dirty"); + } + + let outputParser = this.ruleView._outputParser; + let parserOptions = { + angleClass: "ruleview-angle", + angleSwatchClass: SHARED_SWATCH_CLASS + " " + ANGLE_SWATCH_CLASS, + bezierClass: "ruleview-bezier", + bezierSwatchClass: SHARED_SWATCH_CLASS + " " + BEZIER_SWATCH_CLASS, + colorClass: "ruleview-color", + colorSwatchClass: SHARED_SWATCH_CLASS + " " + COLOR_SWATCH_CLASS, + filterClass: "ruleview-filter", + filterSwatchClass: SHARED_SWATCH_CLASS + " " + FILTER_SWATCH_CLASS, + gridClass: "ruleview-grid", + defaultColorType: !propDirty, + urlClass: "theme-link", + baseURI: this.sheetHref + }; + let frag = outputParser.parseCssProperty(name, val, parserOptions); + this.valueSpan.innerHTML = ""; + this.valueSpan.appendChild(frag); + + this.ruleView.emit("property-value-updated", this.valueSpan); + + // Attach the color picker tooltip to the color swatches + this._colorSwatchSpans = + this.valueSpan.querySelectorAll("." + COLOR_SWATCH_CLASS); + if (this.ruleEditor.isEditable) { + for (let span of this._colorSwatchSpans) { + // Adding this swatch to the list of swatches our colorpicker + // knows about + this.ruleView.tooltips.colorPicker.addSwatch(span, { + onShow: this._onStartEditing, + onPreview: this._onSwatchPreview, + onCommit: this._onSwatchCommit, + onRevert: this._onSwatchRevert + }); + span.on("unit-change", this._onSwatchCommit); + let title = l10n("rule.colorSwatch.tooltip"); + span.setAttribute("title", title); + } + } + + // Attach the cubic-bezier tooltip to the bezier swatches + this._bezierSwatchSpans = + this.valueSpan.querySelectorAll("." + BEZIER_SWATCH_CLASS); + if (this.ruleEditor.isEditable) { + for (let span of this._bezierSwatchSpans) { + // Adding this swatch to the list of swatches our colorpicker + // knows about + this.ruleView.tooltips.cubicBezier.addSwatch(span, { + onShow: this._onStartEditing, + onPreview: this._onSwatchPreview, + onCommit: this._onSwatchCommit, + onRevert: this._onSwatchRevert + }); + let title = l10n("rule.bezierSwatch.tooltip"); + span.setAttribute("title", title); + } + } + + // Attach the filter editor tooltip to the filter swatch + let span = this.valueSpan.querySelector("." + FILTER_SWATCH_CLASS); + if (this.ruleEditor.isEditable) { + if (span) { + parserOptions.filterSwatch = true; + + this.ruleView.tooltips.filterEditor.addSwatch(span, { + onShow: this._onStartEditing, + onPreview: this._onSwatchPreview, + onCommit: this._onSwatchCommit, + onRevert: this._onSwatchRevert + }, outputParser, parserOptions); + let title = l10n("rule.filterSwatch.tooltip"); + span.setAttribute("title", title); + } + } + + this.angleSwatchSpans = + this.valueSpan.querySelectorAll("." + ANGLE_SWATCH_CLASS); + if (this.ruleEditor.isEditable) { + for (let angleSpan of this.angleSwatchSpans) { + angleSpan.on("unit-change", this._onSwatchCommit); + let title = l10n("rule.angleSwatch.tooltip"); + angleSpan.setAttribute("title", title); + } + } + + let gridToggle = this.valueSpan.querySelector(".ruleview-grid"); + if (gridToggle) { + gridToggle.setAttribute("title", l10n("rule.gridToggle.tooltip")); + if (this.ruleView.highlighters.gridHighlighterShown === + this.ruleView.inspector.selection.nodeFront) { + gridToggle.classList.add("active"); + } + } + + // Now that we have updated the property's value, we might have a pending + // click on the value container. If we do, we have to trigger a click event + // on the right element. + if (this._hasPendingClick) { + this._hasPendingClick = false; + let elToClick; + + if (this._clickedElementOptions !== null) { + let {selector, index} = this._clickedElementOptions; + elToClick = this.valueSpan.querySelectorAll(selector)[index]; + + this._clickedElementOptions = null; + } + + if (!elToClick) { + elToClick = this.valueSpan; + } + elToClick.click(); + } + + // Populate the computed styles. + this._updateComputed(); + + // Update the rule property highlight. + this.ruleView._updatePropertyHighlight(this); + }, + + _onStartEditing: function () { + this.element.classList.remove("ruleview-overridden"); + this.filterProperty.hidden = true; + this.enable.style.visibility = "hidden"; + }, + + /** + * Update the visibility of the enable checkbox, the warning indicator and + * the filter property, as well as the overriden state of the property. + */ + updatePropertyState: function () { + if (this.prop.enabled) { + this.enable.style.removeProperty("visibility"); + this.enable.setAttribute("checked", ""); + } else { + this.enable.style.visibility = "visible"; + this.enable.removeAttribute("checked"); + } + + this.warning.hidden = this.editing || this.isValid(); + this.filterProperty.hidden = this.editing || + !this.isValid() || + !this.prop.overridden || + this.ruleEditor.rule.isUnmatched; + + if (!this.editing && + (this.prop.overridden || !this.prop.enabled || + !this.prop.isKnownProperty())) { + this.element.classList.add("ruleview-overridden"); + } else { + this.element.classList.remove("ruleview-overridden"); + } + }, + + /** + * Update the indicator for computed styles. The computed styles themselves + * are populated on demand, when they become visible. + */ + _updateComputed: function () { + this.computed.innerHTML = ""; + + let showExpander = this.prop.computed.some(c => c.name !== this.prop.name); + this.expander.style.visibility = showExpander ? "visible" : "hidden"; + + this._populatedComputed = false; + if (this.expander.hasAttribute("open")) { + this._populateComputed(); + } + }, + + /** + * Populate the list of computed styles. + */ + _populateComputed: function () { + if (this._populatedComputed) { + return; + } + this._populatedComputed = true; + + for (let computed of this.prop.computed) { + // Don't bother to duplicate information already + // shown in the text property. + if (computed.name === this.prop.name) { + continue; + } + + let li = createChild(this.computed, "li", { + class: "ruleview-computed" + }); + + if (computed.overridden) { + li.classList.add("ruleview-overridden"); + } + + createChild(li, "span", { + class: "ruleview-propertyname theme-fg-color5", + textContent: computed.name + }); + appendText(li, ": "); + + let outputParser = this.ruleView._outputParser; + let frag = outputParser.parseCssProperty( + computed.name, computed.value, { + colorSwatchClass: "ruleview-swatch ruleview-colorswatch", + urlClass: "theme-link", + baseURI: this.sheetHref + } + ); + + // Store the computed property value that was parsed for output + computed.parsedValue = frag.textContent; + + createChild(li, "span", { + class: "ruleview-propertyvalue theme-fg-color1", + child: frag + }); + + appendText(li, ";"); + + // Store the computed style element for easy access when highlighting + // styles + computed.element = li; + } + }, + + /** + * Handles clicks on the disabled property. + */ + _onEnableClicked: function (event) { + let checked = this.enable.hasAttribute("checked"); + if (checked) { + this.enable.removeAttribute("checked"); + } else { + this.enable.setAttribute("checked", ""); + } + this.prop.setEnabled(!checked); + event.stopPropagation(); + }, + + /** + * Handles clicks on the computed property expander. If the computed list is + * open due to user expanding or style filtering, collapse the computed list + * and close the expander. Otherwise, add user-open attribute which is used to + * expand the computed list and tracks whether or not the computed list is + * expanded by manually by the user. + */ + _onExpandClicked: function (event) { + if (this.computed.hasAttribute("filter-open") || + this.computed.hasAttribute("user-open")) { + this.expander.removeAttribute("open"); + this.computed.removeAttribute("filter-open"); + this.computed.removeAttribute("user-open"); + } else { + this.expander.setAttribute("open", "true"); + this.computed.setAttribute("user-open", ""); + this._populateComputed(); + } + + event.stopPropagation(); + }, + + /** + * Expands the computed list when a computed property is matched by the style + * filtering. The filter-open attribute is used to track whether or not the + * computed list was toggled opened by the filter. + */ + expandForFilter: function () { + if (!this.computed.hasAttribute("user-open")) { + this.expander.setAttribute("open", "true"); + this.computed.setAttribute("filter-open", ""); + this._populateComputed(); + } + }, + + /** + * Collapses the computed list that was expanded by style filtering. + */ + collapseForFilter: function () { + this.computed.removeAttribute("filter-open"); + + if (!this.computed.hasAttribute("user-open")) { + this.expander.removeAttribute("open"); + } + }, + + /** + * Called when the property name's inplace editor is closed. + * Ignores the change if the user pressed escape, otherwise + * commits it. + * + * @param {String} value + * The value contained in the editor. + * @param {Boolean} commit + * True if the change should be applied. + * @param {Number} direction + * The move focus direction number. + */ + _onNameDone: function (value, commit, direction) { + let isNameUnchanged = (!commit && !this.ruleEditor.isEditing) || + this.committed.name === value; + if (this.prop.value && isNameUnchanged) { + return; + } + + // Remove a property if the name is empty + if (!value.trim()) { + this.remove(direction); + return; + } + + // Remove a property if the property value is empty and the property + // value is not about to be focused + if (!this.prop.value && + direction !== Services.focus.MOVEFOCUS_FORWARD) { + this.remove(direction); + return; + } + + // Adding multiple rules inside of name field overwrites the current + // property with the first, then adds any more onto the property list. + let properties = parseDeclarations(this.cssProperties.isKnown, value); + + if (properties.length) { + this.prop.setName(properties[0].name); + this.committed.name = this.prop.name; + + if (!this.prop.enabled) { + this.prop.setEnabled(true); + } + + if (properties.length > 1) { + this.prop.setValue(properties[0].value, properties[0].priority); + this.ruleEditor.addProperties(properties.slice(1), this.prop); + } + } + }, + + /** + * Remove property from style and the editors from DOM. + * Begin editing next or previous available property given the focus + * direction. + * + * @param {Number} direction + * The move focus direction number. + */ + remove: function (direction) { + if (this._colorSwatchSpans && this._colorSwatchSpans.length) { + for (let span of this._colorSwatchSpans) { + this.ruleView.tooltips.colorPicker.removeSwatch(span); + span.off("unit-change", this._onSwatchCommit); + } + } + + if (this.angleSwatchSpans && this.angleSwatchSpans.length) { + for (let span of this.angleSwatchSpans) { + span.off("unit-change", this._onSwatchCommit); + } + } + + this.element.parentNode.removeChild(this.element); + this.ruleEditor.rule.editClosestTextProperty(this.prop, direction); + this.nameSpan.textProperty = null; + this.valueSpan.textProperty = null; + this.prop.remove(); + }, + + /** + * Called when a value editor closes. If the user pressed escape, + * revert to the value this property had before editing. + * + * @param {String} value + * The value contained in the editor. + * @param {Boolean} commit + * True if the change should be applied. + * @param {Number} direction + * The move focus direction number. + */ + _onValueDone: function (value = "", commit, direction) { + let parsedProperties = this._getValueAndExtraProperties(value); + let val = parseSingleValue(this.cssProperties.isKnown, + parsedProperties.firstValue); + let isValueUnchanged = (!commit && !this.ruleEditor.isEditing) || + !parsedProperties.propertiesToAdd.length && + this.committed.value === val.value && + this.committed.priority === val.priority; + // If the value is not empty and unchanged, revert the property back to + // its original value and enabled or disabled state + if (value.trim() && isValueUnchanged) { + this.ruleEditor.rule.previewPropertyValue(this.prop, val.value, + val.priority); + this.rule.setPropertyEnabled(this.prop, this.prop.enabled); + return; + } + + if (this.isDisplayGrid()) { + this.ruleView.highlighters._hideGridHighlighter(); + } + + // First, set this property value (common case, only modified a property) + this.prop.setValue(val.value, val.priority); + + if (!this.prop.enabled) { + this.prop.setEnabled(true); + } + + this.committed.value = this.prop.value; + this.committed.priority = this.prop.priority; + + // If needed, add any new properties after this.prop. + this.ruleEditor.addProperties(parsedProperties.propertiesToAdd, this.prop); + + // If the input value is empty and the focus is moving forward to the next + // editable field, then remove the whole property. + // A timeout is used here to accurately check the state, since the inplace + // editor `done` and `destroy` events fire before the next editor + // is focused. + if (!value.trim() && direction !== Services.focus.MOVEFOCUS_BACKWARD) { + setTimeout(() => { + if (!this.editing) { + this.remove(direction); + } + }, 0); + } + }, + + /** + * Called when the swatch editor wants to commit a value change. + */ + _onSwatchCommit: function () { + this._onValueDone(this.valueSpan.textContent, true); + this.update(); + }, + + /** + * Called when the swatch editor wants to preview a value change. + */ + _onSwatchPreview: function () { + this._previewValue(this.valueSpan.textContent); + }, + + /** + * Called when the swatch editor closes from an ESC. Revert to the original + * value of this property before editing. + */ + _onSwatchRevert: function () { + this._previewValue(this.prop.value, true); + this.update(); + }, + + /** + * Parse a value string and break it into pieces, starting with the + * first value, and into an array of additional properties (if any). + * + * Example: Calling with "red; width: 100px" would return + * { firstValue: "red", propertiesToAdd: [{ name: "width", value: "100px" }] } + * + * @param {String} value + * The string to parse + * @return {Object} An object with the following properties: + * firstValue: A string containing a simple value, like + * "red" or "100px!important" + * propertiesToAdd: An array with additional properties, following the + * parseDeclarations format of {name,value,priority} + */ + _getValueAndExtraProperties: function (value) { + // The inplace editor will prevent manual typing of multiple properties, + // but we need to deal with the case during a paste event. + // Adding multiple properties inside of value editor sets value with the + // first, then adds any more onto the property list (below this property). + let firstValue = value; + let propertiesToAdd = []; + + let properties = parseDeclarations(this.cssProperties.isKnown, value); + + // Check to see if the input string can be parsed as multiple properties + if (properties.length) { + // Get the first property value (if any), and any remaining + // properties (if any) + if (!properties[0].name && properties[0].value) { + firstValue = properties[0].value; + propertiesToAdd = properties.slice(1); + } else if (properties[0].name && properties[0].value) { + // In some cases, the value could be a property:value pair + // itself. Join them as one value string and append + // potentially following properties + firstValue = properties[0].name + ": " + properties[0].value; + propertiesToAdd = properties.slice(1); + } + } + + return { + propertiesToAdd: propertiesToAdd, + firstValue: firstValue + }; + }, + + /** + * Live preview this property, without committing changes. + * + * @param {String} value + * The value to set the current property to. + * @param {Boolean} reverting + * True if we're reverting the previously previewed value + */ + _previewValue: function (value, reverting = false) { + // Since function call is throttled, we need to make sure we are still + // editing, and any selector modifications have been completed + if (!reverting && (!this.editing || this.ruleEditor.isEditing)) { + return; + } + + let val = parseSingleValue(this.cssProperties.isKnown, value); + this.ruleEditor.rule.previewPropertyValue(this.prop, val.value, + val.priority); + }, + + /** + * Validate this property. Does it make sense for this value to be assigned + * to this property name? This does not apply the property value + * + * @return {Boolean} true if the property value is valid, false otherwise. + */ + isValid: function () { + return this.prop.isValid(); + }, + + /** + * Returns true if the property is a `display: grid` declaration. + * + * @return {Boolean} true if the property is a `display: grid` declaration. + */ + isDisplayGrid: function () { + return this.prop.name === "display" && this.prop.value === "grid"; + } +}; + +exports.TextPropertyEditor = TextPropertyEditor; |