diff options
Diffstat (limited to 'toolkit/devtools/styleinspector')
129 files changed, 15343 insertions, 0 deletions
diff --git a/toolkit/devtools/styleinspector/computed-view.js b/toolkit/devtools/styleinspector/computed-view.js new file mode 100644 index 000000000..5e1cd3ffb --- /dev/null +++ b/toolkit/devtools/styleinspector/computed-view.js @@ -0,0 +1,1493 @@ +/* -*- 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/. */ + +const {Cc, Ci, Cu} = require("chrome"); + +const ToolDefinitions = require("main").Tools; +const {CssLogic} = require("devtools/styleinspector/css-logic"); +const {ELEMENT_STYLE} = require("devtools/server/actors/styles"); +const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); +const {EventEmitter} = require("devtools/toolkit/event-emitter"); +const {OutputParser} = require("devtools/output-parser"); +const {PrefObserver, PREF_ORIG_SOURCES} = require("devtools/styleeditor/utils"); +const {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {}); +const overlays = require("devtools/styleinspector/style-inspector-overlays"); + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/devtools/Templater.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", + "resource://gre/modules/PluralForm.jsm"); + +const FILTER_CHANGED_TIMEOUT = 300; +const HTML_NS = "http://www.w3.org/1999/xhtml"; +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + +/** + * Helper for long-running processes that should yield occasionally to + * the mainloop. + * + * @param {Window} aWin + * Timeouts will be set on this window when appropriate. + * @param {Generator} aGenerator + * Will iterate this generator. + * @param {object} aOptions + * Options for the update process: + * onItem {function} Will be called with the value of each iteration. + * onBatch {function} Will be called after each batch of iterations, + * before yielding to the main loop. + * onDone {function} Will be called when iteration is complete. + * onCancel {function} Will be called if the process is canceled. + * threshold {int} How long to process before yielding, in ms. + * + * @constructor + */ +function UpdateProcess(aWin, aGenerator, aOptions) +{ + this.win = aWin; + this.iter = _Iterator(aGenerator); + this.onItem = aOptions.onItem || function() {}; + this.onBatch = aOptions.onBatch || function () {}; + this.onDone = aOptions.onDone || function() {}; + this.onCancel = aOptions.onCancel || function() {}; + this.threshold = aOptions.threshold || 45; + + this.canceled = false; +} + +UpdateProcess.prototype = { + /** + * Schedule a new batch on the main loop. + */ + schedule: function UP_schedule() + { + if (this.canceled) { + return; + } + this._timeout = this.win.setTimeout(this._timeoutHandler.bind(this), 0); + }, + + /** + * Cancel the running process. onItem will not be called again, + * and onCancel will be called. + */ + cancel: function UP_cancel() + { + if (this._timeout) { + this.win.clearTimeout(this._timeout); + this._timeout = 0; + } + this.canceled = true; + this.onCancel(); + }, + + _timeoutHandler: function UP_timeoutHandler() { + this._timeout = null; + try { + this._runBatch(); + this.schedule(); + } catch(e) { + if (e instanceof StopIteration) { + this.onBatch(); + this.onDone(); + return; + } + console.error(e); + throw e; + } + }, + + _runBatch: function Y_runBatch() + { + let time = Date.now(); + while(!this.canceled) { + // Continue until iter.next() throws... + let next = this.iter.next(); + this.onItem(next[1]); + if ((Date.now() - time) > this.threshold) { + this.onBatch(); + return; + } + } + } +}; + +/** + * CssHtmlTree is a panel that manages the display of a table sorted by style. + * There should be one instance of CssHtmlTree per style display (of which there + * will generally only be one). + * + * @params {StyleInspector} aStyleInspector The owner of this CssHtmlTree + * @param {PageStyleFront} aPageStyle + * Front for the page style actor that will be providing + * the style information. + * + * @constructor + */ +function CssHtmlTree(aStyleInspector, aPageStyle) +{ + this.styleWindow = aStyleInspector.doc.defaultView; + this.styleDocument = aStyleInspector.doc; + this.styleInspector = aStyleInspector; + this.inspector = this.styleInspector.inspector; + this.pageStyle = aPageStyle; + this.propertyViews = []; + + this._outputParser = new OutputParser(); + + let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"]. + getService(Ci.nsIXULChromeRegistry); + this.getRTLAttr = chromeReg.isLocaleRTL("global") ? "rtl" : "ltr"; + + // Create bound methods. + this.focusWindow = this.focusWindow.bind(this); + this._onContextMenu = this._onContextMenu.bind(this); + this._contextMenuUpdate = this._contextMenuUpdate.bind(this); + this._onSelectAll = this._onSelectAll.bind(this); + this._onClick = this._onClick.bind(this); + this._onCopy = this._onCopy.bind(this); + this._onCopyColor = this._onCopyColor.bind(this); + + this.styleDocument.addEventListener("copy", this._onCopy); + this.styleDocument.addEventListener("mousedown", this.focusWindow); + this.styleDocument.addEventListener("contextmenu", this._onContextMenu); + + // Nodes used in templating + this.root = this.styleDocument.getElementById("root"); + this.templateRoot = this.styleDocument.getElementById("templateRoot"); + this.element = this.styleDocument.getElementById("propertyContainer"); + + // Listen for click events + this.element.addEventListener("click", this._onClick, false); + + // No results text. + this.noResults = this.styleDocument.getElementById("noResults"); + + // Refresh panel when color unit changed. + this._handlePrefChange = this._handlePrefChange.bind(this); + gDevTools.on("pref-changed", this._handlePrefChange); + + // Refresh panel when pref for showing original sources changes + this._updateSourceLinks = this._updateSourceLinks.bind(this); + this._prefObserver = new PrefObserver("devtools."); + this._prefObserver.on(PREF_ORIG_SOURCES, this._updateSourceLinks); + + CssHtmlTree.processTemplate(this.templateRoot, this.root, this); + + // The element that we're inspecting, and the document that it comes from. + this.viewedElement = null; + + this._buildContextMenu(); + this.createStyleViews(); + + // Add the tooltips and highlightersoverlay + this.tooltips = new overlays.TooltipsOverlay(this); + this.tooltips.addToView(); + this.highlighters = new overlays.HighlightersOverlay(this); + this.highlighters.addToView(); +} + +/** + * Memoized lookup of a l10n string from a string bundle. + * @param {string} aName The key to lookup. + * @returns A localized version of the given key. + */ +CssHtmlTree.l10n = function CssHtmlTree_l10n(aName) +{ + try { + return CssHtmlTree._strings.GetStringFromName(aName); + } catch (ex) { + Services.console.logStringMessage("Error reading '" + aName + "'"); + throw new Error("l10n error with " + aName); + } +}; + +/** + * Clone the given template node, and process it by resolving ${} references + * in the template. + * + * @param {nsIDOMElement} aTemplate the template note to use. + * @param {nsIDOMElement} aDestination the destination node where the + * processed nodes will be displayed. + * @param {object} aData the data to pass to the template. + * @param {Boolean} aPreserveDestination If true then the template will be + * appended to aDestination's content else aDestination.innerHTML will be + * cleared before the template is appended. + */ +CssHtmlTree.processTemplate = function CssHtmlTree_processTemplate(aTemplate, + aDestination, aData, aPreserveDestination) +{ + if (!aPreserveDestination) { + aDestination.innerHTML = ""; + } + + // All the templater does is to populate a given DOM tree with the given + // values, so we need to clone the template first. + let duplicated = aTemplate.cloneNode(true); + + // See https://github.com/mozilla/domtemplate/blob/master/README.md + // for docs on the template() function + template(duplicated, aData, { allowEval: true }); + while (duplicated.firstChild) { + aDestination.appendChild(duplicated.firstChild); + } +}; + +XPCOMUtils.defineLazyGetter(CssHtmlTree, "_strings", function() Services.strings + .createBundle("chrome://global/locale/devtools/styleinspector.properties")); + +XPCOMUtils.defineLazyGetter(this, "clipboardHelper", function() { + return Cc["@mozilla.org/widget/clipboardhelper;1"]. + getService(Ci.nsIClipboardHelper); +}); + +CssHtmlTree.prototype = { + // Cache the list of properties that match the selected element. + _matchedProperties: null, + + // Used for cancelling timeouts in the style filter. + _filterChangedTimeout: null, + + // The search filter + searchField: null, + + // Reference to the "Include browser styles" checkbox. + includeBrowserStylesCheckbox: null, + + // Holds the ID of the panelRefresh timeout. + _panelRefreshTimeout: null, + + // Toggle for zebra striping + _darkStripe: true, + + // Number of visible properties + numVisibleProperties: 0, + + setPageStyle: function(pageStyle) { + this.pageStyle = pageStyle; + }, + + get includeBrowserStyles() + { + return this.includeBrowserStylesCheckbox.checked; + }, + + _handlePrefChange: function(event, data) { + if (this._computed && (data.pref == "devtools.defaultColorUnit" || + data.pref == PREF_ORIG_SOURCES)) { + this.refreshPanel(); + } + }, + + /** + * Update the view with a new selected element. + * The CssHtmlTree panel will show the style information for the given element. + * @param {NodeFront} aElement The highlighted node to get styles for. + * @returns a promise that will be resolved when highlighting is complete. + */ + selectElement: function(aElement) { + if (!aElement) { + this.viewedElement = null; + this.noResults.hidden = false; + + if (this._refreshProcess) { + this._refreshProcess.cancel(); + } + // Hiding all properties + for (let propView of this.propertyViews) { + propView.refresh(); + } + return promise.resolve(undefined); + } + + if (aElement === this.viewedElement) { + return promise.resolve(undefined); + } + + this.viewedElement = aElement; + this.refreshSourceFilter(); + + return this.refreshPanel(); + }, + + /** + * Get the type of a given node in the computed-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 + * style-inspector-overlays + * - 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 classes = node.classList; + + // Check if the node isn't a selector first since this doesn't require + // walking the DOM + if (classes.contains("matched") || + classes.contains("bestmatch") || + classes.contains("parentmatch")) { + let selectorText = ""; + for (let child of node.childNodes) { + if (child.nodeType === node.TEXT_NODE) { + selectorText += child.textContent; + } + } + return { + type: overlays.VIEW_NODE_SELECTOR_TYPE, + value: selectorText.trim() + } + } + + // Walk up the nodes to find out where node is + let propertyView; + let propertyContent; + let parent = node; + while (parent.parentNode) { + if (parent.classList.contains("property-view")) { + propertyView = parent; + break; + } + if (parent.classList.contains("property-content")) { + propertyContent = parent; + break; + } + parent = parent.parentNode; + } + if (!propertyView && !propertyContent) { + return null; + } + + let value, type; + + // Get the property and value for a node that's a property name or value + let isHref = classes.contains("theme-link") && !classes.contains("link"); + if (propertyView && (classes.contains("property-name") || + classes.contains("property-value") || + isHref)) { + value = { + property: parent.querySelector(".property-name").textContent, + value: parent.querySelector(".property-value").textContent + }; + } + if (propertyContent && (classes.contains("other-property-value") || + isHref)) { + let view = propertyContent.previousSibling; + value = { + property: view.querySelector(".property-name").textContent, + value: node.textContent + }; + } + + // Get the type + if (classes.contains("property-name")) { + type = overlays.VIEW_NODE_PROPERTY_TYPE; + } else if (classes.contains("property-value") || + classes.contains("other-property-value")) { + type = overlays.VIEW_NODE_VALUE_TYPE; + } else if (isHref) { + type = overlays.VIEW_NODE_IMAGE_URL_TYPE; + value.url = node.href; + } else { + return null; + } + + return {type, value}; + }, + + _createPropertyViews: function() + { + if (this._createViewsPromise) { + return this._createViewsPromise; + } + + let deferred = promise.defer(); + this._createViewsPromise = deferred.promise; + + this.refreshSourceFilter(); + this.numVisibleProperties = 0; + let fragment = this.styleDocument.createDocumentFragment(); + + this._createViewsProcess = new UpdateProcess(this.styleWindow, CssHtmlTree.propertyNames, { + onItem: (aPropertyName) => { + // Per-item callback. + let propView = new PropertyView(this, aPropertyName); + fragment.appendChild(propView.buildMain()); + fragment.appendChild(propView.buildSelectorContainer()); + + if (propView.visible) { + this.numVisibleProperties++; + } + this.propertyViews.push(propView); + }, + onCancel: () => { + deferred.reject("_createPropertyViews cancelled"); + }, + onDone: () => { + // Completed callback. + this.element.appendChild(fragment); + this.noResults.hidden = this.numVisibleProperties > 0; + deferred.resolve(undefined); + } + }); + + this._createViewsProcess.schedule(); + return deferred.promise; + }, + + /** + * Refresh the panel content. + */ + refreshPanel: function CssHtmlTree_refreshPanel() + { + if (!this.viewedElement) { + return promise.resolve(); + } + + // Capture the current viewed element to return from the promise handler + // early if it changed + let viewedElement = this.viewedElement; + + return promise.all([ + this._createPropertyViews(), + this.pageStyle.getComputed(this.viewedElement, { + filter: this._sourceFilter, + onlyMatched: !this.includeBrowserStyles, + markMatched: true + }) + ]).then(([createViews, computed]) => { + if (viewedElement !== this.viewedElement) { + return; + } + + this._matchedProperties = new Set; + for (let name in computed) { + if (computed[name].matched) { + this._matchedProperties.add(name); + } + } + this._computed = computed; + + if (this._refreshProcess) { + this._refreshProcess.cancel(); + } + + this.noResults.hidden = true; + + // Reset visible property count + this.numVisibleProperties = 0; + + // Reset zebra striping. + this._darkStripe = true; + + let deferred = promise.defer(); + this._refreshProcess = new UpdateProcess(this.styleWindow, this.propertyViews, { + onItem: (aPropView) => { + aPropView.refresh(); + }, + onDone: () => { + this._refreshProcess = null; + this.noResults.hidden = this.numVisibleProperties > 0; + this.inspector.emit("computed-view-refreshed"); + deferred.resolve(undefined); + } + }); + this._refreshProcess.schedule(); + return deferred.promise; + }).then(null, (err) => console.error(err)); + }, + + /** + * Called when the user enters a search term. + * + * @param {Event} aEvent the DOM Event object. + */ + filterChanged: function CssHtmlTree_filterChanged(aEvent) + { + let win = this.styleWindow; + + if (this._filterChangedTimeout) { + win.clearTimeout(this._filterChangedTimeout); + } + + this._filterChangedTimeout = win.setTimeout(() => { + this.refreshPanel(); + this._filterChangeTimeout = null; + }, FILTER_CHANGED_TIMEOUT); + }, + + /** + * The change event handler for the includeBrowserStyles checkbox. + * + * @param {Event} aEvent the DOM Event object. + */ + includeBrowserStylesChanged: + function CssHtmltree_includeBrowserStylesChanged(aEvent) + { + this.refreshSourceFilter(); + this.refreshPanel(); + }, + + /** + * When includeBrowserStyles.checked is false we only display properties that + * have matched selectors and have been included by the document or one of the + * document's stylesheets. If .checked is false we display all properties + * including those that come from UA stylesheets. + */ + refreshSourceFilter: function CssHtmlTree_setSourceFilter() + { + this._matchedProperties = null; + this._sourceFilter = this.includeBrowserStyles ? + CssLogic.FILTER.UA : + CssLogic.FILTER.USER; + }, + + _updateSourceLinks: function CssHtmlTree__updateSourceLinks() + { + for (let propView of this.propertyViews) { + propView.updateSourceLinks(); + } + this.inspector.emit("computed-view-sourcelinks-updated"); + }, + + /** + * The CSS as displayed by the UI. + */ + createStyleViews: function CssHtmlTree_createStyleViews() + { + if (CssHtmlTree.propertyNames) { + return; + } + + CssHtmlTree.propertyNames = []; + + // Here we build and cache a list of css properties supported by the browser + // We could use any element but let's use the main document's root element + let styles = this.styleWindow.getComputedStyle(this.styleDocument.documentElement); + let mozProps = []; + for (let i = 0, numStyles = styles.length; i < numStyles; i++) { + let prop = styles.item(i); + if (prop.startsWith("--")) { + // Skip any CSS variables used inside of browser CSS files + continue; + } else if (prop.startsWith("-")) { + mozProps.push(prop); + } else { + CssHtmlTree.propertyNames.push(prop); + } + } + + CssHtmlTree.propertyNames.sort(); + CssHtmlTree.propertyNames.push.apply(CssHtmlTree.propertyNames, + mozProps.sort()); + + this._createPropertyViews().then(null, e => { + if (!this.styleInspector) { + console.warn("The creation of property views was cancelled because the " + + "computed-view was destroyed before it was done creating views"); + } else { + console.error(e); + } + }); + }, + + /** + * Get a set of properties that have matched selectors. + * + * @return {Set} If a property name is in the set, it has matching selectors. + */ + get matchedProperties() + { + return this._matchedProperties || new Set; + }, + + /** + * Focus the window on mousedown. + * + * @param aEvent The event object + */ + focusWindow: function(aEvent) + { + let win = this.styleDocument.defaultView; + win.focus(); + }, + + /** + * Create a context menu. + */ + _buildContextMenu: function() + { + let doc = this.styleDocument.defaultView.parent.document; + + this._contextmenu = this.styleDocument.createElementNS(XUL_NS, "menupopup"); + this._contextmenu.addEventListener("popupshowing", this._contextMenuUpdate); + this._contextmenu.id = "computed-view-context-menu"; + + // Select All + this.menuitemSelectAll = createMenuItem(this._contextmenu, { + label: "computedView.contextmenu.selectAll", + accesskey: "computedView.contextmenu.selectAll.accessKey", + command: this._onSelectAll + }); + + // Copy + this.menuitemCopy = createMenuItem(this._contextmenu, { + label: "computedView.contextmenu.copy", + accesskey: "computedView.contextmenu.copy.accessKey", + command: this._onCopy + }); + + // Copy color + this.menuitemCopyColor = createMenuItem(this._contextmenu, { + label: "ruleView.contextmenu.copyColor", + accesskey: "ruleView.contextmenu.copyColor.accessKey", + command: this._onCopyColor + }); + + // Show Original Sources + this.menuitemSources= createMenuItem(this._contextmenu, { + label: "ruleView.contextmenu.showOrigSources", + accesskey: "ruleView.contextmenu.showOrigSources.accessKey", + command: this._onToggleOrigSources, + type: "checkbox" + }); + + let popupset = doc.documentElement.querySelector("popupset"); + if (!popupset) { + popupset = doc.createElementNS(XUL_NS, "popupset"); + doc.documentElement.appendChild(popupset); + } + popupset.appendChild(this._contextmenu); + }, + + /** + * Update the context menu. This means enabling or disabling menuitems as + * appropriate. + */ + _contextMenuUpdate: function() + { + let win = this.styleDocument.defaultView; + let disable = win.getSelection().isCollapsed; + this.menuitemCopy.disabled = disable; + + let showOrig = Services.prefs.getBoolPref(PREF_ORIG_SOURCES); + this.menuitemSources.setAttribute("checked", showOrig); + + this.menuitemCopyColor.hidden = !this._isColorPopup(); + }, + + /** + * A helper that determines if the popup was opened with a click to a color + * value and saves the color to this._colorToCopy. + * + * @return {Boolean} + * true if click on color opened the popup, false otherwise. + */ + _isColorPopup: function () { + this._colorToCopy = ""; + + let trigger = this.popupNode; + if (!trigger) { + return false; + } + + let container = (trigger.nodeType == trigger.TEXT_NODE) ? + trigger.parentElement : trigger; + + let isColorNode = el => el.dataset && "color" in el.dataset; + + while (!isColorNode(container)) { + container = container.parentNode; + if (!container) { + return false; + } + } + + this._colorToCopy = container.dataset["color"]; + return true; + }, + + /** + * Context menu handler. + */ + _onContextMenu: function(event) { + try { + this.popupNode = event.explicitOriginalTarget; + this.styleDocument.defaultView.focus(); + this._contextmenu.openPopupAtScreen(event.screenX, event.screenY, true); + } catch(e) { + console.error(e); + } + }, + + /** + * Select all text. + */ + _onSelectAll: function() + { + try { + let win = this.styleDocument.defaultView; + let selection = win.getSelection(); + + selection.selectAllChildren(this.styleDocument.documentElement); + } catch(e) { + console.error(e); + } + }, + + _onClick: function(event) { + let target = event.target; + + if (target.nodeName === "a") { + event.stopPropagation(); + event.preventDefault(); + let browserWin = this.inspector.target.tab.ownerDocument.defaultView; + browserWin.openUILinkIn(target.href, "tab"); + } + }, + + _onCopyColor: function() { + clipboardHelper.copyString(this._colorToCopy, this.styleDocument); + }, + + /** + * Copy selected text. + * + * @param event The event object + */ + _onCopy: function(event) + { + try { + let win = this.styleDocument.defaultView; + let text = win.getSelection().toString().trim(); + + // Tidy up block headings by moving CSS property names and their values onto + // the same line and inserting a colon between them. + let textArray = text.split(/[\r\n]+/); + let result = ""; + + // Parse text array to output string. + if (textArray.length > 1) { + for (let prop of textArray) { + if (CssHtmlTree.propertyNames.indexOf(prop) !== -1) { + // Property name + result += prop; + } else { + // Property value + result += ": " + prop; + if (result.length > 0) { + result += ";\n"; + } + } + } + } else { + // Short text fragment. + result = textArray[0]; + } + + clipboardHelper.copyString(result, this.styleDocument); + + if (event) { + event.preventDefault(); + } + } catch(e) { + console.error(e); + } + }, + + /** + * Toggle the original sources pref. + */ + _onToggleOrigSources: function() + { + let isEnabled = Services.prefs.getBoolPref(PREF_ORIG_SOURCES); + Services.prefs.setBoolPref(PREF_ORIG_SOURCES, !isEnabled); + }, + + /** + * Destructor for CssHtmlTree. + */ + destroy: function CssHtmlTree_destroy() + { + this.viewedElement = null; + this._outputParser = null; + + // Remove event listeners + this.includeBrowserStylesCheckbox.removeEventListener("command", + this.includeBrowserStylesChanged); + this.searchField.removeEventListener("command", this.filterChanged); + gDevTools.off("pref-changed", this._handlePrefChange); + + this._prefObserver.off(PREF_ORIG_SOURCES, this._updateSourceLinks); + this._prefObserver.destroy(); + + // Cancel tree construction + if (this._createViewsProcess) { + this._createViewsProcess.cancel(); + } + if (this._refreshProcess) { + this._refreshProcess.cancel(); + } + + this.element.removeEventListener("click", this._onClick, false); + + // Remove context menu + if (this._contextmenu) { + // Destroy the Select All menuitem. + this.menuitemCopy.removeEventListener("command", this._onCopy); + this.menuitemCopy = null; + + // Destroy the Copy menuitem. + this.menuitemSelectAll.removeEventListener("command", this._onSelectAll); + this.menuitemSelectAll = null; + + // Destroy Copy Color menuitem. + this.menuitemCopyColor.removeEventListener("command", this._onCopyColor); + this.menuitemCopyColor = null; + + // Destroy the context menu. + this._contextmenu.removeEventListener("popupshowing", this._contextMenuUpdate); + this._contextmenu.parentNode.removeChild(this._contextmenu); + this._contextmenu = null; + } + + this.popupNode = null; + + this.tooltips.destroy(); + this.highlighters.destroy(); + + // Remove bound listeners + this.styleDocument.removeEventListener("contextmenu", this._onContextMenu); + this.styleDocument.removeEventListener("copy", this._onCopy); + this.styleDocument.removeEventListener("mousedown", this.focusWindow); + + // Nodes used in templating + this.root = null; + this.element = null; + this.panel = null; + + // The document in which we display the results (csshtmltree.xul). + this.styleDocument = null; + + for (let propView of this.propertyViews) { + propView.destroy(); + } + + // The element that we're inspecting, and the document that it comes from. + this.propertyViews = null; + this.styleWindow = null; + this.styleDocument = null; + this.styleInspector = null; + } +}; + +function PropertyInfo(aTree, aName) { + this.tree = aTree; + this.name = aName; +} +PropertyInfo.prototype = { + get value() { + if (this.tree._computed) { + let value = this.tree._computed[this.name].value; + return value; + } + } +}; + +function createMenuItem(aMenu, aAttributes) +{ + let item = aMenu.ownerDocument.createElementNS(XUL_NS, "menuitem"); + + item.setAttribute("label", CssHtmlTree.l10n(aAttributes.label)); + item.setAttribute("accesskey", CssHtmlTree.l10n(aAttributes.accesskey)); + item.addEventListener("command", aAttributes.command); + + aMenu.appendChild(item); + + return item; +} + +/** + * A container to give easy access to property data from the template engine. + * + * @constructor + * @param {CssHtmlTree} aTree the CssHtmlTree instance we are working with. + * @param {string} aName the CSS property name for which this PropertyView + * instance will render the rules. + */ +function PropertyView(aTree, aName) +{ + this.tree = aTree; + this.name = aName; + this.getRTLAttr = aTree.getRTLAttr; + + this.link = "https://developer.mozilla.org/CSS/" + aName; + + this.templateMatchedSelectors = aTree.styleDocument.getElementById("templateMatchedSelectors"); + this._propertyInfo = new PropertyInfo(aTree, aName); +} + +PropertyView.prototype = { + // The parent element which contains the open attribute + element: null, + + // Property header node + propertyHeader: null, + + // Destination for property names + nameNode: null, + + // Destination for property values + valueNode: null, + + // Are matched rules expanded? + matchedExpanded: false, + + // Matched selector container + matchedSelectorsContainer: null, + + // Matched selector expando + matchedExpander: null, + + // Cache for matched selector views + _matchedSelectorViews: null, + + // The previously selected element used for the selector view caches + prevViewedElement: null, + + /** + * Get the computed style for the current property. + * + * @return {string} the computed style for the current property of the + * currently highlighted element. + */ + get value() + { + return this.propertyInfo.value; + }, + + /** + * An easy way to access the CssPropertyInfo behind this PropertyView. + */ + get propertyInfo() + { + return this._propertyInfo; + }, + + /** + * Does the property have any matched selectors? + */ + get hasMatchedSelectors() + { + return this.tree.matchedProperties.has(this.name); + }, + + /** + * Should this property be visible? + */ + get visible() + { + if (!this.tree.viewedElement) { + return false; + } + + if (!this.tree.includeBrowserStyles && !this.hasMatchedSelectors) { + return false; + } + + let searchTerm = this.tree.searchField.value.toLowerCase(); + if (searchTerm && this.name.toLowerCase().indexOf(searchTerm) == -1 && + this.value.toLowerCase().indexOf(searchTerm) == -1) { + return false; + } + + return true; + }, + + /** + * Returns the className that should be assigned to the propertyView. + * @return string + */ + get propertyHeaderClassName() + { + if (this.visible) { + let isDark = this.tree._darkStripe = !this.tree._darkStripe; + return isDark ? "property-view row-striped" : "property-view"; + } + return "property-view-hidden"; + }, + + /** + * Returns the className that should be assigned to the propertyView content + * container. + * @return string + */ + get propertyContentClassName() + { + if (this.visible) { + let isDark = this.tree._darkStripe; + return isDark ? "property-content row-striped" : "property-content"; + } + return "property-content-hidden"; + }, + + /** + * Build the markup for on computed style + * @return Element + */ + buildMain: function PropertyView_buildMain() + { + let doc = this.tree.styleDocument; + + // Build the container element + this.onMatchedToggle = this.onMatchedToggle.bind(this); + this.element = doc.createElementNS(HTML_NS, "div"); + this.element.setAttribute("class", this.propertyHeaderClassName); + this.element.addEventListener("dblclick", this.onMatchedToggle, false); + + // Make it keyboard navigable + this.element.setAttribute("tabindex", "0"); + this.onKeyDown = (aEvent) => { + let keyEvent = Ci.nsIDOMKeyEvent; + if (aEvent.keyCode == keyEvent.DOM_VK_F1) { + this.mdnLinkClick(); + } + if (aEvent.keyCode == keyEvent.DOM_VK_RETURN || + aEvent.keyCode == keyEvent.DOM_VK_SPACE) { + this.onMatchedToggle(aEvent); + } + }; + this.element.addEventListener("keydown", this.onKeyDown, false); + + // Build the twisty expand/collapse + this.matchedExpander = doc.createElementNS(HTML_NS, "div"); + this.matchedExpander.className = "expander theme-twisty"; + this.matchedExpander.addEventListener("click", this.onMatchedToggle, false); + this.element.appendChild(this.matchedExpander); + + this.focusElement = () => this.element.focus(); + + // Build the style name element + this.nameNode = doc.createElementNS(HTML_NS, "div"); + this.nameNode.setAttribute("class", "property-name theme-fg-color5"); + // Reset its tabindex attribute otherwise, if an ellipsis is applied + // it will be reachable via TABing + this.nameNode.setAttribute("tabindex", ""); + this.nameNode.textContent = this.nameNode.title = this.name; + // Make it hand over the focus to the container + this.onFocus = () => this.element.focus(); + this.nameNode.addEventListener("click", this.onFocus, false); + this.element.appendChild(this.nameNode); + + // Build the style value element + this.valueNode = doc.createElementNS(HTML_NS, "div"); + this.valueNode.setAttribute("class", "property-value theme-fg-color1"); + // Reset its tabindex attribute otherwise, if an ellipsis is applied + // it will be reachable via TABing + this.valueNode.setAttribute("tabindex", ""); + this.valueNode.setAttribute("dir", "ltr"); + // Make it hand over the focus to the container + this.valueNode.addEventListener("click", this.onFocus, false); + this.element.appendChild(this.valueNode); + + return this.element; + }, + + buildSelectorContainer: function PropertyView_buildSelectorContainer() + { + let doc = this.tree.styleDocument; + let element = doc.createElementNS(HTML_NS, "div"); + element.setAttribute("class", this.propertyContentClassName); + this.matchedSelectorsContainer = doc.createElementNS(HTML_NS, "div"); + this.matchedSelectorsContainer.setAttribute("class", "matchedselectors"); + element.appendChild(this.matchedSelectorsContainer); + + return element; + }, + + /** + * Refresh the panel's CSS property value. + */ + refresh: function PropertyView_refresh() + { + this.element.className = this.propertyHeaderClassName; + this.element.nextElementSibling.className = this.propertyContentClassName; + + if (this.prevViewedElement != this.tree.viewedElement) { + this._matchedSelectorViews = null; + this.prevViewedElement = this.tree.viewedElement; + } + + if (!this.tree.viewedElement || !this.visible) { + this.valueNode.textContent = this.valueNode.title = ""; + this.matchedSelectorsContainer.parentNode.hidden = true; + this.matchedSelectorsContainer.textContent = ""; + this.matchedExpander.removeAttribute("open"); + return; + } + + this.tree.numVisibleProperties++; + + let outputParser = this.tree._outputParser; + let frag = outputParser.parseCssProperty(this.propertyInfo.name, + this.propertyInfo.value, + { + colorSwatchClass: "computedview-colorswatch", + urlClass: "theme-link" + // No need to use baseURI here as computed URIs are never relative. + }); + this.valueNode.innerHTML = ""; + this.valueNode.appendChild(frag); + + this.refreshMatchedSelectors(); + }, + + /** + * Refresh the panel matched rules. + */ + refreshMatchedSelectors: function PropertyView_refreshMatchedSelectors() + { + let hasMatchedSelectors = this.hasMatchedSelectors; + this.matchedSelectorsContainer.parentNode.hidden = !hasMatchedSelectors; + + if (hasMatchedSelectors) { + this.matchedExpander.classList.add("expandable"); + } else { + this.matchedExpander.classList.remove("expandable"); + } + + if (this.matchedExpanded && hasMatchedSelectors) { + return this.tree.pageStyle.getMatchedSelectors(this.tree.viewedElement, this.name).then(matched => { + if (!this.matchedExpanded) { + return; + } + + this._matchedSelectorResponse = matched; + CssHtmlTree.processTemplate(this.templateMatchedSelectors, + this.matchedSelectorsContainer, this); + this.matchedExpander.setAttribute("open", ""); + this.tree.inspector.emit("computed-view-property-expanded"); + }).then(null, console.error); + } else { + this.matchedSelectorsContainer.innerHTML = ""; + this.matchedExpander.removeAttribute("open"); + this.tree.inspector.emit("computed-view-property-collapsed"); + return promise.resolve(undefined); + } + }, + + get matchedSelectors() + { + return this._matchedSelectorResponse; + }, + + /** + * Provide access to the matched SelectorViews that we are currently + * displaying. + */ + get matchedSelectorViews() + { + if (!this._matchedSelectorViews) { + this._matchedSelectorViews = []; + this._matchedSelectorResponse.forEach( + function matchedSelectorViews_convert(aSelectorInfo) { + this._matchedSelectorViews.push(new SelectorView(this.tree, aSelectorInfo)); + }, this); + } + + return this._matchedSelectorViews; + }, + + /** + * Update all the selector source links to reflect whether we're linking to + * original sources (e.g. Sass files). + */ + updateSourceLinks: function PropertyView_updateSourceLinks() + { + if (!this._matchedSelectorViews) { + return; + } + for (let view of this._matchedSelectorViews) { + view.updateSourceLink(); + } + }, + + /** + * The action when a user expands matched selectors. + * + * @param {Event} aEvent Used to determine the class name of the targets click + * event. + */ + onMatchedToggle: function PropertyView_onMatchedToggle(aEvent) + { + this.matchedExpanded = !this.matchedExpanded; + this.refreshMatchedSelectors(); + aEvent.preventDefault(); + }, + + /** + * The action when a user clicks on the MDN help link for a property. + */ + mdnLinkClick: function PropertyView_mdnLinkClick(aEvent) + { + let inspector = this.tree.inspector; + + if (inspector.target.tab) { + let browserWin = inspector.target.tab.ownerDocument.defaultView; + browserWin.openUILinkIn(this.link, "tab"); + } + aEvent.preventDefault(); + }, + + /** + * Destroy this property view, removing event listeners + */ + destroy: function PropertyView_destroy() { + this.element.removeEventListener("dblclick", this.onMatchedToggle, false); + this.element.removeEventListener("keydown", this.onKeyDown, false); + this.element = null; + + this.matchedExpander.removeEventListener("click", this.onMatchedToggle, false); + this.matchedExpander = null; + + this.nameNode.removeEventListener("click", this.onFocus, false); + this.nameNode = null; + + this.valueNode.removeEventListener("click", this.onFocus, false); + this.valueNode = null; + } +}; + +/** + * A container to give us easy access to display data from a CssRule + * @param CssHtmlTree aTree, the owning CssHtmlTree + * @param aSelectorInfo + */ +function SelectorView(aTree, aSelectorInfo) +{ + this.tree = aTree; + this.selectorInfo = aSelectorInfo; + this._cacheStatusNames(); + + this.updateSourceLink(); +} + +/** + * Decode for cssInfo.rule.status + * @see SelectorView.prototype._cacheStatusNames + * @see CssLogic.STATUS + */ +SelectorView.STATUS_NAMES = [ + // "Parent Match", "Matched", "Best Match" +]; + +SelectorView.CLASS_NAMES = [ + "parentmatch", "matched", "bestmatch" +]; + +SelectorView.prototype = { + /** + * Cache localized status names. + * + * These statuses are localized inside the styleinspector.properties string + * bundle. + * @see css-logic.js - the CssLogic.STATUS array. + * + * @return {void} + */ + _cacheStatusNames: function SelectorView_cacheStatusNames() + { + if (SelectorView.STATUS_NAMES.length) { + return; + } + + for (let status in CssLogic.STATUS) { + let i = CssLogic.STATUS[status]; + if (i > CssLogic.STATUS.UNMATCHED) { + let value = CssHtmlTree.l10n("rule.status." + status); + // Replace normal spaces with non-breaking spaces + SelectorView.STATUS_NAMES[i] = value.replace(/ /g, '\u00A0'); + } + } + }, + + /** + * A localized version of cssRule.status + */ + get statusText() + { + return SelectorView.STATUS_NAMES[this.selectorInfo.status]; + }, + + /** + * Get class name for selector depending on status + */ + get statusClass() + { + return SelectorView.CLASS_NAMES[this.selectorInfo.status - 1]; + }, + + get href() + { + if (this._href) { + return this._href; + } + let sheet = this.selectorInfo.rule.parentStyleSheet; + this._href = sheet ? sheet.href : "#"; + return this._href; + }, + + get sourceText() + { + return this.selectorInfo.sourceText; + }, + + + get value() + { + return this.selectorInfo.value; + }, + + get outputFragment() + { + // Sadly, because this fragment is added to the template by DOM Templater + // we lose any events that are attached. This means that URLs will open in a + // new window. At some point we should fix this by stopping using the + // templater. + let outputParser = this.tree._outputParser; + let frag = outputParser.parseCssProperty( + this.selectorInfo.name, + this.selectorInfo.value, { + colorSwatchClass: "computedview-colorswatch", + urlClass: "theme-link", + baseURI: this.selectorInfo.rule.href + }); + return frag; + }, + + /** + * Update the text of the source link to reflect whether we're showing + * original sources or not. + */ + updateSourceLink: function() + { + return this.updateSource().then((oldSource) => { + if (oldSource != this.source && this.tree.element) { + let selector = '[sourcelocation="' + oldSource + '"]'; + let link = this.tree.element.querySelector(selector); + if (link) { + link.textContent = this.source; + link.setAttribute("sourcelocation", this.source); + } + } + }); + }, + + /** + * Update the 'source' store based on our original sources preference. + */ + updateSource: function() + { + let rule = this.selectorInfo.rule; + this.sheet = rule.parentStyleSheet; + + if (!rule || !this.sheet) { + let oldSource = this.source; + this.source = CssLogic.l10n("rule.sourceElement"); + this.href = "#"; + return promise.resolve(oldSource); + } + + let showOrig = Services.prefs.getBoolPref(PREF_ORIG_SOURCES); + + if (showOrig && rule.type != ELEMENT_STYLE) { + let deferred = promise.defer(); + + // set as this first so we show something while we're fetching + this.source = CssLogic.shortSource(this.sheet) + ":" + rule.line; + + rule.getOriginalLocation().then(({href, line, column}) => { + let oldSource = this.source; + this.source = CssLogic.shortSource({href: href}) + ":" + line; + deferred.resolve(oldSource); + }); + + return deferred.promise; + } + + let oldSource = this.source; + this.source = CssLogic.shortSource(this.sheet) + ":" + rule.line; + return promise.resolve(oldSource); + }, + + /** + * Open the style editor if the RETURN key was pressed. + */ + maybeOpenStyleEditor: function(aEvent) + { + let keyEvent = Ci.nsIDOMKeyEvent; + if (aEvent.keyCode == keyEvent.DOM_VK_RETURN) { + this.openStyleEditor(); + } + }, + + /** + * When a css link is clicked this method is called in order to either: + * 1. Open the link in view source (for chrome stylesheets). + * 2. Open the link in the style editor. + * + * We can only view stylesheets contained in document.styleSheets inside the + * style editor. + * + * @param aEvent The click event + */ + openStyleEditor: function(aEvent) + { + let inspector = this.tree.inspector; + let rule = this.selectorInfo.rule; + + // The style editor can only display stylesheets coming from content because + // chrome stylesheets are not listed in the editor's stylesheet selector. + // + // If the stylesheet is a content stylesheet we send it to the style + // editor else we display it in the view source window. + let sheet = rule.parentStyleSheet; + if (!sheet || sheet.isSystem) { + let contentDoc = null; + if (this.tree.viewedElement.isLocal_toBeDeprecated()) { + let rawNode = this.tree.viewedElement.rawNode(); + if (rawNode) { + contentDoc = rawNode.ownerDocument; + } + } + let viewSourceUtils = inspector.viewSourceUtils; + viewSourceUtils.viewSource(rule.href, null, contentDoc, 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 = inspector.target; + if (ToolDefinitions.styleEditor.isTargetSupported(target)) { + gDevTools.showToolbox(target, "styleeditor").then(function(toolbox) { + let sheet = source || href; + toolbox.getCurrentPanel().selectStyleSheet(sheet, line, column); + }); + } + }); + } +}; + +exports.CssHtmlTree = CssHtmlTree; +exports.PropertyView = PropertyView; diff --git a/toolkit/devtools/styleinspector/computedview.xhtml b/toolkit/devtools/styleinspector/computedview.xhtml new file mode 100644 index 000000000..4ab8a9db2 --- /dev/null +++ b/toolkit/devtools/styleinspector/computedview.xhtml @@ -0,0 +1,115 @@ +<?xml version="1.0"?> +<!-- 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/. --> + +<!DOCTYPE window [ + <!ENTITY % inspectorDTD SYSTEM "chrome://browser/locale/devtools/styleinspector.dtd"> + %inspectorDTD; + <!ELEMENT loop ANY> + <!ATTLIST li foreach CDATA #IMPLIED> + <!ATTLIST div foreach CDATA #IMPLIED> + <!ATTLIST loop foreach CDATA #IMPLIED> + <!ATTLIST a target CDATA #IMPLIED> + <!ATTLIST a __pathElement CDATA #IMPLIED> + <!ATTLIST div _id CDATA #IMPLIED> + <!ATTLIST div save CDATA #IMPLIED> + <!ATTLIST table save CDATA #IMPLIED> + <!ATTLIST loop if CDATA #IMPLIED> + <!ATTLIST tr if CDATA #IMPLIED> +]> + +<html xmlns="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + class="theme-sidebar"> + + <head> + + <title>&computedViewTitle;</title> + + <link rel="stylesheet" href="chrome://global/skin/global.css" type="text/css"/> + <link rel="stylesheet" href="chrome://browser/skin/devtools/common.css" type="text/css"/> + <link rel="stylesheet" href="chrome://browser/skin/devtools/computedview.css" type="text/css"/> + + <script type="application/javascript;version=1.8" src="theme-switching.js"/> + + <script type="application/javascript;version=1.8"> + window.setPanel = function(panel, iframe) { + let {devtools} = Components.utils.import("resource://gre/modules/devtools/Loader.jsm", {}); + let inspector = devtools.require("devtools/styleinspector/style-inspector"); + this.computedview = new inspector.ComputedViewTool(panel, window, iframe); + } + window.onunload = function() { + if (this.computedview) { + this.computedview.destroy(); + } + } + </script> + </head> + + <body> + + <!-- The output from #templateProperty (below) is appended here. --> + <div id="propertyContainer" class="devtools-monospace"> + </div> + + <!-- When no properties are found the following block is displayed. --> + <div id="noResults" hidden=""> + &noPropertiesFound; + </div> + + <!-- The output from #templateRoot (below) is inserted here. --> + <div id="root" class="devtools-monospace"></div> + + <!-- + To visually debug the templates without running firefox, alter the display:none + --> + <div style="display:none;"> + <!-- + templateRoot sits at the top of the window and contains the "include default + styles" checkbox. For data it needs an instance of CssHtmlTree. + --> + <div id="templateRoot"> + <xul:hbox class="devtools-toolbar" flex="1" align="center"> + <xul:checkbox class="includebrowserstyles" + save="${includeBrowserStylesCheckbox}" + oncommand="${includeBrowserStylesChanged}" checked="false" + label="&browserStylesLabel;"/> + <xul:textbox class="devtools-searchinput" type="search" save="${searchField}" + placeholder="&userStylesSearch;" flex="1" + oncommand="${filterChanged}"/> + </xul:hbox> + </div> + + + <!-- + A templateMatchedSelectors sits inside each templateProperties showing the + list of selectors that affect that property. Each needs data like this: + { + matchedSelectorViews: ..., // from cssHtmlTree.propertyViews[name].matchedSelectorViews + } + This is a template so the parent does not need to be a table, except that + using a div as the parent causes the DOM to muck with the tr elements + --> + <div id="templateMatchedSelectors"> + <loop foreach="selector in ${matchedSelectorViews}"> + <p> + <span class="rule-link"> + <a target="_blank" class="link theme-link" + onclick="${selector.openStyleEditor}" + onkeydown="${selector.maybeOpenStyleEditor}" + title="${selector.href}" + sourcelocation="${selector.source}" + tabindex="0">${selector.source}</a> + </span> + <span dir="ltr" class="rule-text ${selector.statusClass} theme-fg-color3" title="${selector.statusText}"> + ${selector.sourceText} + <span class="other-property-value theme-fg-color1">${selector.outputFragment}</span> + </span> + </p> + </loop> + </div> + </div> + + </body> +</html> diff --git a/toolkit/devtools/styleinspector/css-parsing-utils.js b/toolkit/devtools/styleinspector/css-parsing-utils.js new file mode 100644 index 000000000..a1e670c45 --- /dev/null +++ b/toolkit/devtools/styleinspector/css-parsing-utils.js @@ -0,0 +1,154 @@ +/* -*- 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 cssTokenizer = require("devtools/sourceeditor/css-tokenizer"); + +/** + * Returns the string enclosed in quotes + */ +function quoteString(string) { + let hasDoubleQuotes = string.contains('"'); + let hasSingleQuotes = string.contains("'"); + + if (hasDoubleQuotes && !hasSingleQuotes) { + // In this case, no escaping required, just enclose in single-quotes + return "'" + string + "'"; + } + + // In all other cases, enclose in double-quotes, and escape any double-quote + // that may be in the string + return '"' + string.replace(/"/g, '\"') + '"'; +} + +/** + * Returns an array of CSS declarations given an string. + * For example, parseDeclarations("width: 1px; height: 1px") would return + * [{name:"width", value: "1px"}, {name: "height", "value": "1px"}] + * + * The input string is assumed to only contain declarations so { and } characters + * will be treated as part of either the property or value, depending where it's + * found. + * + * @param {string} inputString + * An input string of CSS + * @return {Array} an array of objects with the following signature: + * [{"name": string, "value": string, "priority": string}, ...] + */ +function parseDeclarations(inputString) { + let tokens = cssTokenizer(inputString); + + let declarations = [{name: "", value: "", priority: ""}]; + + let current = "", hasBang = false, lastProp; + for (let token of tokens) { + lastProp = declarations[declarations.length - 1]; + + if (token.tokenType === ":") { + if (!lastProp.name) { + // Set the current declaration name if there's no name yet + lastProp.name = current.trim(); + current = ""; + hasBang = false; + } else { + // Otherwise, just append ':' to the current value (declaration value + // with colons) + current += ":"; + } + } else if (token.tokenType === ";") { + lastProp.value = current.trim(); + current = ""; + hasBang = false; + declarations.push({name: "", value: "", priority: ""}); + } else { + switch(token.tokenType) { + case "IDENT": + if (token.value === "important" && hasBang) { + lastProp.priority = "important"; + hasBang = false; + } else { + if (hasBang) { + current += "!"; + } + current += token.value; + } + break; + case "WHITESPACE": + current += " "; + break; + case "DIMENSION": + current += token.repr; + break; + case "HASH": + current += "#" + token.value; + break; + case "URL": + current += "url(" + quoteString(token.value) + ")"; + break; + case "FUNCTION": + current += token.value + "("; + break; + case "(": + case ")": + current += token.tokenType; + break; + case "EOF": + break; + case "DELIM": + if (token.value === "!") { + hasBang = true; + } else { + current += token.value; + } + break; + case "STRING": + current += quoteString(token.value); + break; + case "{": + case "}": + current += token.tokenType; + break; + default: + current += token.value; + break; + } + } + } + + // Handle whatever trailing properties or values might still be there + if (current) { + if (!lastProp.name) { + // Trailing property found, e.g. p1:v1;p2:v2;p3 + lastProp.name = current.trim(); + } else { + // Trailing value found, i.e. value without an ending ; + lastProp.value += current.trim(); + } + } + + // Remove declarations that have neither a name nor a value + declarations = declarations.filter(prop => prop.name || prop.value); + + return declarations; +}; +exports.parseDeclarations = parseDeclarations; + +/** + * Expects a single CSS value to be passed as the input and parses the value + * and priority. + * + * @param {string} value The value from the text editor. + * @return {object} an object with 'value' and 'priority' properties. + */ +function parseSingleValue(value) { + let declaration = parseDeclarations("a: " + value + ";")[0]; + return { + value: declaration ? declaration.value : "", + priority: declaration ? declaration.priority : "" + }; +}; +exports.parseSingleValue = parseSingleValue; diff --git a/toolkit/devtools/styleinspector/cssruleview.xhtml b/toolkit/devtools/styleinspector/cssruleview.xhtml new file mode 100644 index 000000000..40e260d36 --- /dev/null +++ b/toolkit/devtools/styleinspector/cssruleview.xhtml @@ -0,0 +1,38 @@ +<?xml version="1.0"?> +<!-- 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/. --> + +<!DOCTYPE window [ + <!ENTITY % inspectorDTD SYSTEM "chrome://browser/locale/devtools/styleinspector.dtd"> + %inspectorDTD; +]> + + +<html xmlns="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + class="theme-sidebar"> + + <head> + <title>&ruleViewTitle;</title> + <link rel="stylesheet" href="chrome://global/skin/global.css" type="text/css"/> + <link rel="stylesheet" href="chrome://browser/skin/devtools/common.css" type="text/css"/> + <link rel="stylesheet" href="chrome://browser/content/devtools/ruleview.css" type="text/css"/> + <link rel="stylesheet" href="chrome://browser/skin/devtools/ruleview.css" type="text/css"/> + + <script type="application/javascript;version=1.8" src="theme-switching.js"/> + + <script type="application/javascript;version=1.8"> + window.setPanel = function(panel, iframe) { + let {devtools} = Components.utils.import("resource://gre/modules/devtools/Loader.jsm", {}); + let inspector = devtools.require("devtools/styleinspector/style-inspector"); + this.ruleview = new inspector.RuleViewTool(panel, window, iframe); + } + window.onunload = function() { + if (this.ruleview) { + this.ruleview.destroy(); + } + } + </script> + </head> +</html> diff --git a/toolkit/devtools/styleinspector/moz.build b/toolkit/devtools/styleinspector/moz.build index 9edb32500..93db4b302 100644 --- a/toolkit/devtools/styleinspector/moz.build +++ b/toolkit/devtools/styleinspector/moz.build @@ -4,6 +4,19 @@ # 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/. +if CONFIG['MOZ_DEVTOOLS']: + BROWSER_CHROME_MANIFESTS += ['test/browser.ini'] + XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini'] + EXTRA_JS_MODULES.devtools.styleinspector += [ 'css-logic.js' ] + +if CONFIG['MOZ_DEVTOOLS']: + EXTRA_JS_MODULES.devtools.styleinspector += [ + 'computed-view.js', + 'css-parsing-utils.js', + 'rule-view.js', + 'style-inspector-overlays.js', + 'style-inspector.js', + ]
\ No newline at end of file diff --git a/toolkit/devtools/styleinspector/rule-view.js b/toolkit/devtools/styleinspector/rule-view.js new file mode 100644 index 000000000..87e747557 --- /dev/null +++ b/toolkit/devtools/styleinspector/rule-view.js @@ -0,0 +1,3097 @@ +/* -*- 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 {Cc, Ci, Cu} = require("chrome"); +const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); +const {CssLogic} = require("devtools/styleinspector/css-logic"); +const {InplaceEditor, editableField, editableItem} = require("devtools/shared/inplace-editor"); +const {ELEMENT_STYLE, PSEUDO_ELEMENTS} = require("devtools/server/actors/styles"); +const {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {}); +const {OutputParser} = require("devtools/output-parser"); +const {PrefObserver, PREF_ORIG_SOURCES} = require("devtools/styleeditor/utils"); +const {parseSingleValue, parseDeclarations} = require("devtools/styleinspector/css-parsing-utils"); +const overlays = require("devtools/styleinspector/style-inspector-overlays"); + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +const HTML_NS = "http://www.w3.org/1999/xhtml"; +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; +const PREF_UA_STYLES = "devtools.inspector.showUserAgentStyles"; +const PREF_DEFAULT_COLOR_UNIT = "devtools.defaultColorUnit"; + +/** + * These regular expressions are adapted from firebug's css.js, and are + * used to parse CSSStyleDeclaration's cssText attribute. + */ + +// Used to split on css line separators +const CSS_LINE_RE = /(?:[^;\(]*(?:\([^\)]*?\))?[^;\(]*)*;?/g; + +// Used to parse a single property line. +const CSS_PROP_RE = /\s*([^:\s]*)\s*:\s*(.*?)\s*(?:! (important))?;?$/; + +// Used to parse an external resource from a property value +const CSS_RESOURCE_RE = /url\([\'\"]?(.*?)[\'\"]?\)/; + +const IOService = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService); + +function promiseWarn(err) { + console.error(err); + return promise.reject(err); +} + +/** + * 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. + * This function creates the document in which those dummy elements + * will be created. + */ +var gDummyPromise; +function createDummyDocument() { + if (gDummyPromise) { + return gDummyPromise; + } + const { getDocShell, create: makeFrame } = require("sdk/frame/utils"); + + let frame = makeFrame(Services.appShell.hiddenDOMWindow.document, { + nodeName: "iframe", + namespaceURI: "http://www.w3.org/1999/xhtml", + allowJavascript: false, + allowPlugins: false, + allowAuth: false + }); + let docShell = getDocShell(frame); + let eventTarget = docShell.chromeEventHandler; + docShell.createAboutBlankContentViewer(Cc["@mozilla.org/nullprincipal;1"].createInstance(Ci.nsIPrincipal)); + let window = docShell.contentViewer.DOMDocument.defaultView; + window.location = "data:text/html,<html></html>"; + let deferred = promise.defer(); + eventTarget.addEventListener("DOMContentLoaded", function handler(event) { + eventTarget.removeEventListener("DOMContentLoaded", handler, false); + deferred.resolve(window.document); + frame.remove(); + }, false); + gDummyPromise = deferred.promise; + return gDummyPromise; +} + +/** + * 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 cssText 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. + */ + +/** + * ElementStyle maintains a list of Rule objects for a given element. + * + * @param {Element} aElement + * The element whose style we are viewing. + * @param {object} aStore + * The ElementStyle can use this object to store metadata + * that might outlast the rule view, particularly the current + * set of disabled properties. + * @param {PageStyleFront} aPageStyle + * Front for the page style actor that will be providing + * the style information. + * @param {bool} aShowUserAgentStyles + * Should user agent styles be inspected? + * + * @constructor + */ +function ElementStyle(aElement, aStore, aPageStyle, aShowUserAgentStyles) { + this.element = aElement; + this.store = aStore || {}; + this.pageStyle = aPageStyle; + this.showUserAgentStyles = aShowUserAgentStyles; + this.rules = []; + + // 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(); + } +} + +// We're exporting _ElementStyle for unit tests. +exports._ElementStyle = ElementStyle; + +ElementStyle.prototype = { + // The element we're looking at. + element: null, + + // Empty, unconnected element of the same type as this node, used + // to figure out how shorthand properties will be parsed. + dummyElement: null, + + init: function() + { + // 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. + return this.dummyElementPromise = createDummyDocument().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); + document.documentElement.appendChild(this.dummyElement); + return this.dummyElement; + }).then(null, promiseWarn); + }, + + destroy: function() { + if (this.destroyed) { + return; + } + this.destroyed = true; + + this.dummyElement = null; + this.dummyElementPromise.then(dummyElement => { + dummyElement.remove(); + this.dummyElementPromise = null; + }, console.error); + }, + + /** + * 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; + } + + // Make sure the dummy element has been created before continuing... + return this.dummyElementPromise.then(() => { + if (this.populated != populated) { + // Don't care anymore. + return; + } + + // Store the current list of rules (if any) during the population + // process. They will be reused if possible. + this._refreshRules = this.rules; + + this.rules = []; + + for (let entry of entries) { + this._maybeAddRule(entry); + } + + // Mark overridden computed styles. + this.markOverriddenAll(); + + this._sortRulesForPseudoElement(); + + // We're done with the previous list of rules. + delete this._refreshRules; + + return null; + }); + }).then(null, promiseWarn); + 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} aOptions + * Options for creating the Rule, see the Rule constructor. + * + * @return {bool} true if we added the rule. + */ + _maybeAddRule: function(aOptions) { + // If we've already included this domRule (for example, when a + // common selector is inherited), ignore it. + if (aOptions.rule && + this.rules.some(function(rule) rule.domRule === aOptions.rule)) { + return false; + } + + if (aOptions.system) { + return false; + } + + let rule = null; + + // If we're refreshing and the rule previously existed, reuse the + // Rule object. + if (this._refreshRules) { + for (let r of this._refreshRules) { + if (r.matches(aOptions)) { + rule = r; + rule.refresh(aOptions); + break; + } + } + } + + // If this is a new rule, create its Rule object. + if (!rule) { + rule = new Rule(this, aOptions); + } + + // Ignore inherited rules with no properties. + if (aOptions.inherited && rule.textProps.length == 0) { + return false; + } + + this.rules.push(rule); + return true; + }, + + /** + * Calls markOverridden with all supported pseudo elements + */ + markOverriddenAll: function() { + this.markOverridden(); + for (let pseudo of PSEUDO_ELEMENTS) { + 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.pseudoElement == pseudo && !rule.keyframes) { + textProps = textProps.concat(rule.textProps.slice(0).reverse()); + } + } + + // 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]; + let overridden; + if (earlier && + computedProp.priority === "important" && + earlier.priority !== "important") { + // 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} aProp + * The text property to update. + * + * @return {bool} true if the TextProperty's overridden state (or any of its + * computed properties overridden state) changed. + */ + _updatePropertyOverridden: function(aProp) { + let overridden = true; + let dirty = false; + for each (let computedProp in aProp.computed) { + if (!computedProp.overridden) { + overridden = false; + } + dirty = computedProp._overriddenDirty || dirty; + delete computedProp._overriddenDirty; + } + + dirty = (!!aProp.overridden != overridden) || dirty; + aProp.overridden = overridden; + return dirty; + } +}; + +/** + * A single style rule or declaration. + * + * @param {ElementStyle} aElementStyle + * The ElementStyle to which this rule belongs. + * @param {object} aOptions + * 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? + * @constructor + */ +function Rule(aElementStyle, aOptions) { + this.elementStyle = aElementStyle; + this.domRule = aOptions.rule || null; + this.style = aOptions.rule; + this.matchedSelectors = aOptions.matchedSelectors || []; + this.pseudoElement = aOptions.pseudoElement || ""; + + this.isSystem = aOptions.isSystem; + this.inherited = aOptions.inherited || null; + this.keyframes = aOptions.keyframes || null; + this._modificationDepth = 0; + + if (this.domRule) { + let parentRule = this.domRule.parentRule; + if (parentRule && parentRule.type == Ci.nsIDOMCSSRule.MEDIA_RULE) { + this.mediaText = parentRule.mediaText; + } + } + + // Populate the text properties with the style's current cssText + // 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() { + if (this._title) { + return this._title; + } + this._title = CssLogic.shortSource(this.sheet); + if (this.domRule.type !== ELEMENT_STYLE && this.ruleLine > 0) { + this._title += ":" + this.ruleLine; + } + + this._title = this._title + (this.mediaText ? " @media " + this.mediaText : ""); + return this._title; + }, + + get inheritedSource() { + if (this._inheritedSource) { + return this._inheritedSource; + } + this._inheritedSource = ""; + if (this.inherited) { + let eltText = this.inherited.tagName.toLowerCase(); + if (this.inherited.id) { + eltText += "#" + this.inherited.id; + } + this._inheritedSource = + CssLogic._strings.formatStringFromName("rule.inheritedFrom", [eltText], 1); + } + return this._inheritedSource; + }, + + get keyframesName() { + if (this._keyframesName) { + return this._keyframesName; + } + this._keyframesName = ""; + if (this.keyframes) { + this._keyframesName = + CssLogic._strings.formatStringFromName("rule.keyframe", [this.keyframes.name], 1); + } + 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 : null; + }, + + /** + * 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() { + if (this._originalSourceStrings) { + return promise.resolve(this._originalSourceStrings); + } + return this.domRule.getOriginalLocation().then(({href, line}) => { + let sourceStrings = { + full: href + ":" + line, + short: CssLogic.shortSource({href: href}) + ":" + line + }; + + this._originalSourceStrings = sourceStrings; + return sourceStrings; + }, console.error); + }, + + /** + * Returns true if the rule matches the creation options + * specified. + * + * @param {object} aOptions + * Creation options. See the Rule constructor for documentation. + */ + matches: function(aOptions) { + return this.style === aOptions.rule; + }, + + /** + * Create a new TextProperty to include in the rule. + * + * @param {string} aName + * The text property name (such as "background" or "border-top"). + * @param {string} aValue + * The property's value (not including priority). + * @param {string} aPriority + * The property's priority (either "important" or an empty string). + * @param {TextProperty} aSiblingProp + * Optional, property next to which the new property will be added. + */ + createProperty: function(aName, aValue, aPriority, aSiblingProp) { + let prop = new TextProperty(this, aName, aValue, aPriority); + + if (aSiblingProp) { + let ind = this.textProps.indexOf(aSiblingProp); + this.textProps.splice(ind + 1, 0, prop); + } + else { + this.textProps.push(prop); + } + + this.applyProperties(); + return prop; + }, + + /** + * Reapply all the properties in this rule, and update their + * computed styles. Store disabled properties in the element + * style's store. Will re-mark overridden properties. + * + * @param {string} [aName] + * A text property name (such as "background" or "border-top") used + * when calling from setPropertyValue & setPropertyName to signify + * that the property should be saved in store.userProperties. + */ + applyProperties: function(aModifications, aName) { + this.elementStyle.markOverriddenAll(); + + if (!aModifications) { + aModifications = this.style.startModifyingProperties(); + } + let disabledProps = []; + let store = this.elementStyle.store; + + for (let prop of this.textProps) { + if (!prop.enabled) { + disabledProps.push({ + name: prop.name, + value: prop.value, + priority: prop.priority + }); + continue; + } + if (prop.value.trim() === "") { + continue; + } + + aModifications.setProperty(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); + } + + let promise = aModifications.apply().then(() => { + let cssProps = {}; + for (let cssProp of parseDeclarations(this.style.cssText)) { + 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; + } + + this.elementStyle.markOverriddenAll(); + + if (promise === this._applyingModifications) { + this._applyingModifications = null; + } + + this.elementStyle._changed(); + }).then(null, promiseWarn); + + this._applyingModifications = promise; + return promise; + }, + + /** + * Renames a property. + * + * @param {TextProperty} aProperty + * The property to rename. + * @param {string} aName + * The new property name (such as "background" or "border-top"). + */ + setPropertyName: function(aProperty, aName) { + if (aName === aProperty.name) { + return; + } + let modifications = this.style.startModifyingProperties(); + modifications.removeProperty(aProperty.name); + aProperty.name = aName; + this.applyProperties(modifications, aName); + }, + + /** + * Sets the value and priority of a property, then reapply all properties. + * + * @param {TextProperty} aProperty + * The property to manipulate. + * @param {string} aValue + * The property's value (not including priority). + * @param {string} aPriority + * The property's priority (either "important" or an empty string). + */ + setPropertyValue: function(aProperty, aValue, aPriority) { + if (aValue === aProperty.value && aPriority === aProperty.priority) { + return; + } + + aProperty.value = aValue; + aProperty.priority = aPriority; + this.applyProperties(null, aProperty.name); + }, + + /** + * Just sets the value and priority of a property, in order to preview its + * effect on the content document. + * + * @param {TextProperty} aProperty + * The property which value will be previewed + * @param {String} aValue + * The value to be used for the preview + * @param {String} aPriority + * The property's priority (either "important" or an empty string). + */ + previewPropertyValue: function(aProperty, aValue, aPriority) { + aProperty.value = aValue; + + let modifications = this.style.startModifyingProperties(); + modifications.setProperty(aProperty.name, aValue, aPriority); + modifications.apply(); + }, + + /** + * Disables or enables given TextProperty. + * + * @param {TextProperty} aProperty + * The property to enable/disable + * @param {Boolean} aValue + */ + setPropertyEnabled: function(aProperty, aValue) { + aProperty.enabled = !!aValue; + let modifications = this.style.startModifyingProperties(); + if (!aProperty.enabled) { + modifications.removeProperty(aProperty.name); + } + this.applyProperties(modifications); + }, + + /** + * Remove a given TextProperty from the rule and update the rule + * accordingly. + * + * @param {TextProperty} aProperty + * The property to be removed + */ + removeProperty: function(aProperty) { + this.textProps = this.textProps.filter(function(prop) prop != aProperty); + let modifications = this.style.startModifyingProperties(); + modifications.removeProperty(aProperty.name); + // Need to re-apply properties in case removing this TextProperty + // exposes another one. + this.applyProperties(modifications); + }, + + /** + * Get the list of TextProperties from the style. Needs + * to parse the style's cssText. + */ + _getTextProperties: function() { + let textProps = []; + let store = this.elementStyle.store; + let props = parseDeclarations(this.style.cssText); + for (let prop of props) { + let name = prop.name; + if (this.inherited && !domUtils.isInheritedProperty(name)) { + continue; + } + let value = store.userProperties.getProperty(this.style, name, prop.value); + let textProp = new TextProperty(this, name, value, prop.priority); + 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 each (let prop in 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(aOptions) { + this.matchedSelectors = aOptions.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 cssText. 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} aNewProp + * The current version of the property, as parsed from the + * cssText in Rule._getTextProperties(). + * + * @return {bool} true if a property was updated, false if no properties + * were updated. + */ + _updateTextProperty: function(aNewProp) { + let match = { rank: 0, prop: null }; + + for each (let prop in this.textProps) { + if (prop.name != aNewProp.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 === aNewProp.value) { + rank += 2; + if (prop.priority === aNewProp.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(aNewProp); + return true; + } + + return false; + }, + + /** + * Jump between editable properties in the UI. Will begin editing the next + * name, if possible. If this is the last element in the set, then begin + * editing the previous value. If this is the *only* element in the set, + * then settle for focusing the new property editor. + * + * @param {TextProperty} aTextProperty + * The text property that will be left to focus on a sibling. + * + */ + editClosestTextProperty: function(aTextProperty) { + let index = this.textProps.indexOf(aTextProperty); + let previous = false; + + // If this is the last element, move to the previous instead of next + if (index === this.textProps.length - 1) { + index = index - 1; + previous = true; + } + else { + index = index + 1; + } + + let nextProp = this.textProps[index]; + + // If possible, begin editing the next name or previous value. + // Otherwise, settle for focusing the new property element. + if (nextProp) { + if (previous) { + nextProp.editor.valueSpan.click(); + } else { + nextProp.editor.nameSpan.click(); + } + } else { + aTextProperty.rule.editor.closeBrace.focus(); + } + } +}; + +/** + * A single property in a rule's cssText. + * + * @param {Rule} aRule + * The rule this TextProperty came from. + * @param {string} aName + * The text property name (such as "background" or "border-top"). + * @param {string} aValue + * The property's value (not including priority). + * @param {string} aPriority + * The property's priority (either "important" or an empty string). + * + */ +function TextProperty(aRule, aName, aValue, aPriority) { + this.rule = aRule; + this.name = aName; + this.value = aValue; + this.priority = aPriority; + this.enabled = true; + 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.dummyElement; + let dummyStyle = dummyElement.style; + dummyStyle.cssText = ""; + dummyStyle.setProperty(this.name, this.value, this.priority); + + this.computed = []; + + try { + // 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 = domUtils.getSubpropertiesForCSSProperty(this.name); + + for (let prop of subProps) { + this.computed.push({ + textProp: this, + name: prop, + value: dummyStyle.getPropertyValue(prop), + priority: dummyStyle.getPropertyPriority(prop), + }); + } + } catch(e) { + // This is a partial property name, probably from cutting and pasting + // text. At this point don't check for computed properties. + } + }, + + /** + * Set all the values from another TextProperty instance into + * this TextProperty instance. + * + * @param {TextProperty} aOther + * The other TextProperty instance. + */ + set: function(aOther) { + let changed = false; + for (let item of ["name", "value", "priority", "enabled"]) { + if (this[item] != aOther[item]) { + this[item] = aOther[item]; + changed = true; + } + } + + if (changed) { + this.updateEditor(); + } + }, + + setValue: function(aValue, aPriority, force=false) { + let store = this.rule.elementStyle.store; + + if (this.editor && aValue !== this.editor.committed.value || force) { + store.userProperties.setProperty(this.rule.style, this.name, aValue); + } + + this.rule.setPropertyValue(this, aValue, aPriority); + this.updateEditor(); + }, + + setName: function(aName) { + let store = this.rule.elementStyle.store; + + if (aName !== this.name) { + store.userProperties.setProperty(this.rule.style, aName, + this.editor.committed.value); + } + + this.rule.setPropertyName(this, aName); + this.updateEditor(); + }, + + setEnabled: function(aValue) { + this.rule.setPropertyEnabled(this, aValue); + this.updateEditor(); + }, + + remove: function() { + this.rule.removeProperty(this); + } +}; + +/** + * 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} aInspector + * @param {Document} aDoc + * The document that will contain the rule view. + * @param {object} aStore + * 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} aPageStyle + * The PageStyleFront for communicating with the remote server. + * @constructor + */ +function CssRuleView(aInspector, aDoc, aStore, aPageStyle) { + this.inspector = aInspector; + this.doc = aDoc; + this.store = aStore || {}; + this.pageStyle = aPageStyle; + this.element = this.doc.createElementNS(HTML_NS, "div"); + this.element.className = "ruleview devtools-monospace"; + this.element.flex = 1; + + this._outputParser = new OutputParser(); + + this._buildContextMenu = this._buildContextMenu.bind(this); + this._contextMenuUpdate = this._contextMenuUpdate.bind(this); + this._onAddRule = this._onAddRule.bind(this); + this._onSelectAll = this._onSelectAll.bind(this); + this._onCopy = this._onCopy.bind(this); + this._onCopyColor = this._onCopyColor.bind(this); + this._onToggleOrigSources = this._onToggleOrigSources.bind(this); + + this.element.addEventListener("copy", this._onCopy); + + 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.showUserAgentStyles = Services.prefs.getBoolPref(PREF_UA_STYLES); + + let options = { + autoSelect: true, + theme: "auto" + }; + this.popup = new AutocompletePopup(aDoc.defaultView.parent.document, options); + + this._buildContextMenu(); + this._showEmpty(); + + // Add the tooltips and highlighters to the view + this.tooltips = new overlays.TooltipsOverlay(this); + this.tooltips.addToView(); + this.highlighters = new overlays.HighlightersOverlay(this); + this.highlighters.addToView(); +} + +exports.CssRuleView = CssRuleView; + +CssRuleView.prototype = { + // The element that we're inspecting. + _viewedElement: null, + + /** + * Build the context menu. + */ + _buildContextMenu: function() { + let doc = this.doc.defaultView.parent.document; + + this._contextmenu = doc.createElementNS(XUL_NS, "menupopup"); + this._contextmenu.addEventListener("popupshowing", this._contextMenuUpdate); + this._contextmenu.id = "rule-view-context-menu"; + + this.menuitemAddRule = createMenuItem(this._contextmenu, { + label: "ruleView.contextmenu.addRule", + accesskey: "ruleView.contextmenu.addRule.accessKey", + command: this._onAddRule + }); + this.menuitemSelectAll = createMenuItem(this._contextmenu, { + label: "ruleView.contextmenu.selectAll", + accesskey: "ruleView.contextmenu.selectAll.accessKey", + command: this._onSelectAll + }); + this.menuitemCopy = createMenuItem(this._contextmenu, { + label: "ruleView.contextmenu.copy", + accesskey: "ruleView.contextmenu.copy.accessKey", + command: this._onCopy + }); + this.menuitemCopyColor = createMenuItem(this._contextmenu, { + label: "ruleView.contextmenu.copyColor", + accesskey: "ruleView.contextmenu.copyColor.accessKey", + command: this._onCopyColor + }); + this.menuitemSources = createMenuItem(this._contextmenu, { + label: "ruleView.contextmenu.showOrigSources", + accesskey: "ruleView.contextmenu.showOrigSources.accessKey", + command: this._onToggleOrigSources, + type: "checkbox" + }); + + let popupset = doc.documentElement.querySelector("popupset"); + if (!popupset) { + popupset = doc.createElementNS(XUL_NS, "popupset"); + doc.documentElement.appendChild(popupset); + } + + popupset.appendChild(this._contextmenu); + }, + + /** + * Update the context menu. This means enabling or disabling menuitems as + * appropriate. + */ + _contextMenuUpdate: function() { + let win = this.doc.defaultView; + + // Copy selection. + let selection = win.getSelection(); + let copy; + + if (selection.toString()) { + // Panel text selected + copy = true; + } else if (selection.anchorNode) { + // input type="text" + let { selectionStart, selectionEnd } = this.doc.popupNode; + + if (isFinite(selectionStart) && isFinite(selectionEnd) && + selectionStart !== selectionEnd) { + copy = true; + } + } else { + // No text selected, disable copy. + copy = false; + } + + this.menuitemCopyColor.hidden = !this._isColorPopup(); + this.menuitemCopy.disabled = !copy; + + var showOrig = Services.prefs.getBoolPref(PREF_ORIG_SOURCES); + this.menuitemSources.setAttribute("checked", showOrig); + + this.menuitemAddRule.disabled = this.inspector.selection.isAnonymousNode(); + }, + + /** + * 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 + * style-inspector-overlays + * - 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 = overlays.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 + }; + } else if (classes.contains("ruleview-propertyvalue") && prop) { + type = overlays.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 + }; + } else if (classes.contains("theme-link") && prop) { + type = overlays.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 + }; + } else if (classes.contains("ruleview-selector-unmatched") || + classes.contains("ruleview-selector-matched")) { + type = overlays.VIEW_NODE_SELECTOR_TYPE; + value = node.textContent; + } else { + return null; + } + + return {type, value}; + }, + + /** + * A helper that determines if the popup was opened with a click to a color + * value and saves the color to this._colorToCopy. + * + * @return {Boolean} + * true if click on color opened the popup, false otherwise. + */ + _isColorPopup: function () { + this._colorToCopy = ""; + + let trigger = this.doc.popupNode; + if (!trigger) { + return false; + } + + let container = (trigger.nodeType == trigger.TEXT_NODE) ? + trigger.parentElement : trigger; + + let isColorNode = el => el.dataset && "color" in el.dataset; + + while (!isColorNode(container)) { + container = container.parentNode; + if (!container) { + return false; + } + } + + this._colorToCopy = container.dataset["color"]; + return true; + }, + + /** + * Select all text. + */ + _onSelectAll: function() { + let win = this.doc.defaultView; + let selection = win.getSelection(); + + selection.selectAllChildren(this.doc.documentElement); + }, + + /** + * Copy selected text from the rule view. + * + * @param {Event} event + * The event object. + */ + _onCopy: function(event) { + try { + let target = event.target; + let text; + + if (event.target.nodeName === "menuitem") { + target = this.doc.popupNode; + } + + if (target.nodeName == "input") { + 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 { + let win = this.doc.defaultView; + let selection = win.getSelection(); + + text = selection.toString(); + + // Remove any double newlines. + text = text.replace(/(\r?\n)\r?\n/g, "$1"); + + // Remove "inline" + let inline = _strings.GetStringFromName("rule.sourceInline"); + let rx = new RegExp("^" + inline + "\\r?\\n?", "g"); + text = text.replace(rx, ""); + } + + clipboardHelper.copyString(text, this.doc); + event.preventDefault(); + } catch(e) { + console.error(e); + } + }, + + /** + * Copy the most recently selected color value to clipboard. + */ + _onCopyColor: function() { + clipboardHelper.copyString(this._colorToCopy, this.styleDocument); + }, + + /** + * Toggle the original sources pref. + */ + _onToggleOrigSources: function() { + let isEnabled = Services.prefs.getBoolPref(PREF_ORIG_SOURCES); + Services.prefs.setBoolPref(PREF_ORIG_SOURCES, !isEnabled); + }, + + /** + * Add a new rule to the current element. + */ + _onAddRule: function() { + let elementStyle = this._elementStyle; + let element = elementStyle.element; + let rules = elementStyle.rules; + let client = this.inspector.toolbox._target.client; + + if (!client.traits.addNewRule) { + return; + } + + this.pageStyle.addNewRule(element).then(options => { + let newRule = new Rule(elementStyle, options); + rules.push(newRule); + let editor = new RuleEditor(this, newRule); + + // 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(); + }); + }, + + setPageStyle: function(aPageStyle) { + this.pageStyle = aPageStyle; + }, + + /** + * Return {bool} true if the rule view currently has an input editor visible. + */ + get isEditing() { + return this.element.querySelectorAll(".styleinspector-propertyeditor").length > 0 + || this.tooltips.isEditing; + }, + + _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) { + let element = this._viewedElement; + this._viewedElement = null; + this.selectElement(element); + } + }, + + _onSourcePrefChanged: function() { + if (this.menuitemSources) { + let isEnabled = Services.prefs.getBoolPref(PREF_ORIG_SOURCES); + this.menuitemSources.setAttribute("checked", isEnabled); + } + + // update text of source links if the rule-view is populated + 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"); + } + }, + + destroy: function() { + this.isDestroyed = true; + this.clear(); + + gDummyPromise = 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.element.removeEventListener("copy", this._onCopy); + this._onCopy = null; + + this._outputParser = null; + + // Remove context menu + if (this._contextmenu) { + // Destroy the Add Rule menuitem. + this.menuitemAddRule.removeEventListener("command", this._onAddRule); + this.menuitemAddRule = null; + + // Destroy the Select All menuitem. + this.menuitemSelectAll.removeEventListener("command", this._onSelectAll); + this.menuitemSelectAll = null; + + // Destroy the Copy menuitem. + this.menuitemCopy.removeEventListener("command", this._onCopy); + this.menuitemCopy = null; + + // Destroy Copy Color menuitem. + this.menuitemCopyColor.removeEventListener("command", this._onCopyColor); + this.menuitemCopyColor = null; + + this.menuitemSources.removeEventListener("command", this._onToggleOrigSources); + this.menuitemSources = null; + + // Destroy the context menu. + this._contextmenu.removeEventListener("popupshowing", this._contextMenuUpdate); + this._contextmenu.parentNode.removeChild(this._contextmenu); + this._contextmenu = null; + } + + // We manage the popupNode ourselves so we also need to destroy it. + this.doc.popupNode = null; + + this.tooltips.destroy(); + this.highlighters.destroy(); + + if (this.element.parentNode) { + this.element.parentNode.removeChild(this.element); + } + + if (this._elementStyle) { + this._elementStyle.destroy(); + } + + this.popup.destroy(); + }, + + /** + * Update the view with a new selected element. + * + * @param {NodeActor} aElement + * The node whose style rules we'll inspect. + */ + selectElement: function(aElement) { + if (this._viewedElement === aElement) { + return promise.resolve(undefined); + } + + this.clear(); + + this._viewedElement = aElement; + if (!this._viewedElement) { + this._showEmpty(); + return promise.resolve(undefined); + } + + this._elementStyle = new ElementStyle(aElement, this.store, + this.pageStyle, this.showUserAgentStyles); + + return this._elementStyle.init().then(() => { + if (this._viewedElement === aElement) { + return this._populate(); + } + }).then(() => { + if (this._viewedElement === aElement) { + this._elementStyle.onChanged = () => { + this._changed(); + }; + } + }).then(null, console.error); + }, + + /** + * 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; + } + + // 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(true); + }); + }, + + _populate: function(clearRules = false) { + let elementStyle = this._elementStyle; + return this._elementStyle.populate().then(() => { + if (this._elementStyle != elementStyle || this.isDestroyed) { + return; + } + + if (clearRules) { + this._clearRules(); + } + this._createEditors(); + + // Notify anyone that cares that we refreshed. + var evt = this.doc.createEvent("Events"); + evt.initEvent("CssRuleViewRefreshed", true, false); + this.element.dispatchEvent(evt); + return undefined; + }).then(null, promiseWarn); + }, + + /** + * Show the user that the rule view has no node selected. + */ + _showEmpty: function() { + if (this.doc.getElementById("noResults") > 0) { + return; + } + + createChild(this.element, "div", { + id: "noResults", + textContent: CssLogic.l10n("rule.empty") + }); + }, + + /** + * Clear the rules. + */ + _clearRules: function() { + while (this.element.hasChildNodes()) { + this.element.removeChild(this.element.lastChild); + } + }, + + /** + * Clear the rule view. + */ + clear: function() { + 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() { + var evt = this.doc.createEvent("Events"); + evt.initEvent("CssRuleViewChanged", true, false); + this.element.dispatchEvent(evt); + }, + + /** + * Text for header that shows above rules for this element + */ + get selectedElementLabel() { + if (this._selectedElementLabel) { + return this._selectedElementLabel; + } + this._selectedElementLabel = CssLogic.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 = CssLogic.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} aLabel 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(aLabel, isPseudo = false) { + let header = this.doc.createElementNS(HTML_NS, "div"); + header.className = this._getRuleViewHeaderClassName(true); + header.classList.add("show-expandable-container"); + header.textContent = aLabel; + + let twisty = this.doc.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.doc.createElementNS(HTML_NS, "div"); + container.classList.add("ruleview-expandable-container"); + this.element.appendChild(container); + + let toggleContainerVisibility = (isPseudo, showPseudo) => { + let isOpen = twisty.getAttribute("open"); + + if (isPseudo) { + this._showPseudoElements = !!showPseudo; + + Services.prefs.setBoolPref("devtools.inspector.show_pseudo_elements", + this.showPseudoElements); + + header.classList.toggle("show-expandable-container", + this.showPseudoElements); + + isOpen = !this.showPseudoElements; + } else { + header.classList.toggle("show-expandable-container"); + } + + if (isOpen) { + twisty.removeAttribute("open"); + } else { + twisty.setAttribute("open", "true"); + } + }; + + header.addEventListener("dblclick", () => { + toggleContainerVisibility(isPseudo, !this.showPseudoElements); + }, false); + twisty.addEventListener("click", () => { + toggleContainerVisibility(isPseudo, !this.showPseudoElements); + }, false); + + if (isPseudo) { + toggleContainerVisibility(isPseudo, this.showPseudoElements); + } + + return container; + }, + + _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 container = null; + + if (!this._elementStyle.rules) { + return; + } + + for (let rule of this._elementStyle.rules) { + if (rule.domRule.system) { + continue; + } + + // Only print header for this element if there are pseudo elements + if (seenPseudoElement && !seenNormalElement && !rule.pseudoElement) { + seenNormalElement = true; + let div = this.doc.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.doc.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 (!rule.editor) { + rule.editor = new RuleEditor(this, rule); + } + + if (container && (rule.pseudoElement || keyframes)) { + container.appendChild(rule.editor.element); + } else { + this.element.appendChild(rule.editor.element); + } + } + } +}; + +/** + * Create a RuleEditor. + * + * @param {CssRuleView} aRuleView + * The CssRuleView containg the document holding this rule editor. + * @param {Rule} aRule + * The Rule object we're editing. + * @constructor + */ +function RuleEditor(aRuleView, aRule) { + this.ruleView = aRuleView; + this.doc = this.ruleView.doc; + this.rule = aRule; + this.isEditable = !aRule.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._create(); +} + +RuleEditor.prototype = { + get isSelectorEditable() { + let toolbox = this.ruleView.inspector.toolbox; + let trait = this.isEditable && + toolbox.target.client.traits.selectorEditable && + this.rule.domRule.type !== ELEMENT_STYLE && + this.rule.domRule.type !== Ci.nsIDOMCSSRule.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.createElementNS(HTML_NS, "div"); + this.element.className = "ruleview-rule theme-separator"; + this.element.setAttribute("uneditable", !this.isEditable); + 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. + let source = createChild(this.element, "div", { + class: "ruleview-rule-source theme-link" + }); + source.addEventListener("click", function() { + if (source.hasAttribute("unselectable")) { + return; + } + let rule = this.rule.domRule; + let evt = this.doc.createEvent("CustomEvent"); + evt.initCustomEvent("CssRuleViewCSSLinkClicked", true, false, { + rule: rule, + }); + this.element.dispatchEvent(evt); + }.bind(this)); + let sourceLabel = this.doc.createElementNS(XUL_NS, "label"); + sourceLabel.setAttribute("crop", "center"); + sourceLabel.classList.add("source-link-label"); + source.appendChild(sourceLabel); + + this.updateSourceLink(); + + let code = createChild(this.element, "div", { + class: "ruleview-code" + }); + + let header = createChild(code, "div", {}); + + this.selectorContainer = createChild(header, "span", { + class: "ruleview-selectorcontainer" + }); + + this.selectorText = createChild(this.selectorContainer, "span", { + class: "ruleview-selector theme-fg-color3" + }); + + if (this.isSelectorEditable) { + this.selectorContainer.addEventListener("click", aEvent => { + // Clicks within the selector shouldn't propagate any further. + aEvent.stopPropagation(); + }, false); + + editableField({ + element: this.selectorText, + done: this._onSelectorDone, + stopOnShiftTab: true, + stopOnTab: true, + stopOnReturn: true + }); + } + + 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: "}" + }); + + this.element.addEventListener("contextmenu", event => { + try { + // In the sidebar we do not have this.doc.popupNode so we need to save + // the node ourselves. + this.doc.popupNode = event.explicitOriginalTarget; + let win = this.doc.defaultView; + win.focus(); + + this.ruleView._contextmenu.openPopupAtScreen( + event.screenX, event.screenY, true); + + } catch(e) { + console.error(e); + } + }, false); + + if (this.isEditable) { + code.addEventListener("click", () => { + let selection = this.doc.defaultView.getSelection(); + if (selection.isCollapsed) { + this.newProperty(); + } + }, false); + + this.element.addEventListener("mousedown", () => { + this.doc.defaultView.focus(); + }, false); + + // Create a property editor when the close brace is clicked. + editableItem({ element: this.closeBrace }, (aElement) => { + this.newProperty(); + }); + } + }, + + updateSourceLink: function RuleEditor_updateSourceLink() + { + let sourceLabel = this.element.querySelector(".source-link-label"); + let sourceHref = (this.rule.sheet && this.rule.sheet.href) ? + this.rule.sheet.href : this.rule.title; + let sourceLine = this.rule.ruleLine > 0 ? ":" + this.rule.ruleLine : ""; + + sourceLabel.setAttribute("tooltiptext", sourceHref + sourceLine); + + if (this.rule.isSystem) { + let uaLabel = _strings.GetStringFromName("rule.userAgentStyles"); + sourceLabel.setAttribute("value", uaLabel + " " + this.rule.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") { + sourceLabel.parentNode.setAttribute("unselectable", "true"); + sourceLabel.setAttribute("value", uaLabel); + sourceLabel.removeAttribute("tooltiptext"); + } + } else { + sourceLabel.setAttribute("value", this.rule.title); + if (this.rule.ruleLine == -1 && this.rule.domRule.parentStyleSheet) { + sourceLabel.parentNode.setAttribute("unselectable", "true"); + } + } + + let showOrig = Services.prefs.getBoolPref(PREF_ORIG_SOURCES); + if (showOrig && !this.rule.isSystem && this.rule.domRule.type != ELEMENT_STYLE) { + this.rule.getOriginalSourceStrings().then((strings) => { + sourceLabel.setAttribute("value", strings.short); + sourceLabel.setAttribute("tooltiptext", strings.full); + }, console.error); + } + }, + + /** + * 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 === Ci.nsIDOMCSSRule.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 cls; + if (this.rule.matchedSelectors.indexOf(selector) > -1) { + cls = "ruleview-selector-matched"; + } else { + cls = "ruleview-selector-unmatched"; + } + createChild(this.selectorText, "span", { + class: cls, + textContent: selector + }); + }); + } + + for (let prop of this.rule.textProps) { + if (!prop.editor) { + let editor = new TextPropertyEditor(this, prop); + this.propertyList.appendChild(editor.element); + } + } + }, + + /** + * Programatically add a new property to the rule. + * + * @param {string} aName + * Property name. + * @param {string} aValue + * Property value. + * @param {string} aPriority + * Property priority. + * @param {TextProperty} aSiblingProp + * Optional, property next to which the new property will be added. + * @return {TextProperty} + * The new property + */ + addProperty: function(aName, aValue, aPriority, aSiblingProp) { + let prop = this.rule.createProperty(aName, aValue, aPriority, aSiblingProp); + 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 aSiblingProp. + // 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} aProperties + * Array of properties, which are objects with this signature: + * { + * name: {string}, + * value: {string}, + * priority: {string} + * } + * @param {TextProperty} aSiblingProp + * Optional, the property next to which all new props should be added. + */ + addProperties: function(aProperties, aSiblingProp) { + if (!aProperties || !aProperties.length) { + return; + } + + let lastProp = aSiblingProp; + for (let p of aProperties) { + lastProp = this.addProperty(p.name, p.value, p.priority, 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 + }); + + // Auto-close the input if multiple rules get pasted into new property. + this.editor.input.addEventListener("paste", + blurOnMultipleProperties, false); + }, + + /** + * Called when the new property input has been dismissed. + * + * @param {string} aValue + * The value in the editor. + * @param {bool} aCommit + * True if the value should be committed. + */ + _onNewProperty: function(aValue, aCommit) { + if (!aValue || !aCommit) { + 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(aValue).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} aValue + * The value contained in the editor. + * @param {boolean} aCommit + * True if the change should be applied. + */ + _onSelectorDone: function(aValue, aCommit) { + if (!aCommit || this.isEditing || aValue === "" || + aValue === this.rule.selectorText) { + return; + } + + this.isEditing = true; + + this.rule.domRule.modifySelector(aValue).then(isModified => { + this.isEditing = false; + + if (isModified) { + this.ruleView.refreshPanel(); + } + }).then(null, err => { + this.isEditing = false; + promiseWarn(err); + }); + } +}; + +/** + * Create a TextPropertyEditor. + * + * @param {RuleEditor} aRuleEditor + * The rule editor that owns this TextPropertyEditor. + * @param {TextProperty} aProperty + * The text property to edit. + * @constructor + */ +function TextPropertyEditor(aRuleEditor, aProperty) { + this.ruleEditor = aRuleEditor; + this.doc = this.ruleEditor.doc; + this.popup = this.ruleEditor.ruleView.popup; + this.prop = aProperty; + this.prop.editor = this; + this.browserWindow = this.doc.defaultView.top; + this.removeOnRevert = this.prop.value === ""; + + 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._onValidate = throttle(this._previewValue, 10, this); + this.update = this.update.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.ruleEditor.ruleView.tooltips.isEditing) || this.popup.isOpen; + }, + + /** + * Create the property editor's DOM. + */ + _create: function() { + this.element = this.doc.createElementNS(HTML_NS, "li"); + this.element.classList.add("ruleview-property"); + + // The enable checkbox will disable or enable the rule. + this.enable = createChild(this.element, "div", { + class: "ruleview-enableproperty theme-checkbox", + tabindex: "-1" + }); + + // Click to expand the computed properties of the text property. + this.expander = createChild(this.element, "span", { + class: "ruleview-expander theme-twisty" + }); + this.expander.addEventListener("click", this._onExpandClicked, true); + + this.nameContainer = createChild(this.element, "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. + let propertyContainer = createChild(this.element, "span", { + class: "ruleview-propertycontainer" + }); + + + // 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(propertyContainer, "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.ruleEditor.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(propertyContainer, ";"); + + this.warning = createChild(this.element, "div", { + class: "ruleview-warning", + hidden: "", + title: CssLogic.l10n("rule.warning.title"), + }); + + // 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", (aEvent) => { + // Clicks within the name shouldn't propagate any further. + aEvent.stopPropagation(); + if (aEvent.target === propertyContainer) { + this.nameSpan.click(); + } + }, false); + + editableField({ + start: this._onStartEditing, + element: this.nameSpan, + done: this._onNameDone, + destroy: this.update, + advanceChars: ':', + contentType: InplaceEditor.CONTENT_TYPES.CSS_PROPERTY, + popup: this.popup + }); + + // Auto blur name field on multiple CSS rules get pasted in. + this.nameContainer.addEventListener("paste", + blurOnMultipleProperties, false); + + propertyContainer.addEventListener("click", (aEvent) => { + // Clicks within the value shouldn't propagate any further. + aEvent.stopPropagation(); + + if (aEvent.target === propertyContainer) { + this.valueSpan.click(); + } + }, 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: ';', + contentType: InplaceEditor.CONTENT_TYPES.CSS_VALUE, + property: this.prop, + popup: this.popup + }); + } + }, + + /** + * Get the path from which to resolve requests for this + * rule's stylesheet. + * @return {string} the stylesheet's href. + */ + get sheetHref() { + let domRule = this.prop.rule.domRule; + if (domRule) { + return domRule.href || domRule.nodeHref; + } + }, + + /** + * Get the URI from which to resolve relative requests for + * this rule's stylesheet. + * @return {nsIURI} A URI based on the the stylesheet's href. + */ + get sheetURI() { + if (this._sheetURI === undefined) { + if (this.sheetHref) { + this._sheetURI = IOService.newURI(this.sheetHref, null, null); + } else { + this._sheetURI = null; + } + } + + return this._sheetURI; + }, + + /** + * Resolve a URI based on the rule stylesheet + * @param {string} relativePath the path to resolve + * @return {string} the resolved path. + */ + resolveURI: function(relativePath) { + if (this.sheetURI) { + relativePath = this.sheetURI.resolve(relativePath); + } + return relativePath; + }, + + /** + * Check the property value to find an external resource (if any). + * @return {string} the URI in the property value, or null if there is no match. + */ + getResourceURI: function() { + let val = this.prop.value; + let uriMatch = CSS_RESOURCE_RE.exec(val); + let uri = null; + + if (uriMatch && uriMatch[1]) { + uri = uriMatch[1]; + } + + return uri; + }, + + /** + * Populate the span based on changes to the TextProperty. + */ + update: function() { + if (this.ruleEditor.ruleView.isDestroyed) { + return; + } + + 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(); + + if ((this.prop.overridden || !this.prop.enabled) && !this.editing) { + this.element.classList.add("ruleview-overridden"); + } else { + this.element.classList.remove("ruleview-overridden"); + } + + 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.prop.rule.elementStyle.store; + let val = store.userProperties.getProperty(this.prop.rule.style, name, + this.prop.value); + if (this.prop.priority) { + val += " !" + this.prop.priority; + } + + let propDirty = store.userProperties.contains(this.prop.rule.style, name); + + if (propDirty) { + this.element.setAttribute("dirty", ""); + } else { + this.element.removeAttribute("dirty"); + } + + let colorSwatchClass = "ruleview-colorswatch"; + let bezierSwatchClass = "ruleview-bezierswatch"; + + let outputParser = this.ruleEditor.ruleView._outputParser; + let frag = outputParser.parseCssProperty(name, val, { + colorSwatchClass: colorSwatchClass, + colorClass: "ruleview-color", + bezierSwatchClass: bezierSwatchClass, + bezierClass: "ruleview-bezier", + defaultColorType: !propDirty, + urlClass: "theme-link", + baseURI: this.sheetURI + }); + this.valueSpan.innerHTML = ""; + this.valueSpan.appendChild(frag); + + // Attach the color picker tooltip to the color swatches + this._colorSwatchSpans = this.valueSpan.querySelectorAll("." + colorSwatchClass); + if (this.ruleEditor.isEditable) { + for (let span of this._colorSwatchSpans) { + // Capture the original declaration value to be able to revert later + let originalValue = this.valueSpan.textContent; + // Adding this swatch to the list of swatches our colorpicker knows about + this.ruleEditor.ruleView.tooltips.colorPicker.addSwatch(span, { + onPreview: () => this._previewValue(this.valueSpan.textContent), + onCommit: () => this._applyNewValue(this.valueSpan.textContent), + onRevert: () => this._applyNewValue(originalValue, false) + }); + } + } + + // Attach the cubic-bezier tooltip to the bezier swatches + this._bezierSwatchSpans = this.valueSpan.querySelectorAll("." + bezierSwatchClass); + if (this.ruleEditor.isEditable) { + for (let span of this._bezierSwatchSpans) { + // Capture the original declaration value to be able to revert later + let originalValue = this.valueSpan.textContent; + // Adding this swatch to the list of swatches our colorpicker knows about + this.ruleEditor.ruleView.tooltips.cubicBezier.addSwatch(span, { + onPreview: () => this._previewValue(this.valueSpan.textContent), + onCommit: () => this._applyNewValue(this.valueSpan.textContent), + onRevert: () => this._applyNewValue(originalValue, false) + }); + } + } + + // Populate the computed styles. + this._updateComputed(); + }, + + _onStartEditing: function() { + this.element.classList.remove("ruleview-overridden"); + this._previewValue(this.prop.value); + }, + + /** + * Populate the list of computed styles. + */ + _updateComputed: function () { + // Clear out existing viewers. + while (this.computed.hasChildNodes()) { + this.computed.removeChild(this.computed.lastChild); + } + + let showExpander = false; + for each (let computed in this.prop.computed) { + // Don't bother to duplicate information already + // shown in the text property. + if (computed.name === this.prop.name) { + continue; + } + + showExpander = true; + + 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.ruleEditor.ruleView._outputParser; + let frag = outputParser.parseCssProperty( + computed.name, computed.value, { + colorSwatchClass: "ruleview-colorswatch", + urlClass: "theme-link", + baseURI: this.sheetURI + } + ); + + createChild(li, "span", { + class: "ruleview-propertyvalue theme-fg-color1", + child: frag + }); + + appendText(li, ";"); + } + + // Show or hide the expander as needed. + if (showExpander) { + this.expander.style.visibility = "visible"; + } else { + this.expander.style.visibility = "hidden"; + } + }, + + /** + * Handles clicks on the disabled property. + */ + _onEnableClicked: function(aEvent) { + let checked = this.enable.hasAttribute("checked"); + if (checked) { + this.enable.removeAttribute("checked"); + } else { + this.enable.setAttribute("checked", ""); + } + this.prop.setEnabled(!checked); + aEvent.stopPropagation(); + }, + + /** + * Handles clicks on the computed property expander. + */ + _onExpandClicked: function(aEvent) { + this.computed.classList.toggle("styleinspector-open"); + if (this.computed.classList.contains("styleinspector-open")) { + this.expander.setAttribute("open", "true"); + } else { + this.expander.removeAttribute("open"); + } + aEvent.stopPropagation(); + }, + + /** + * Called when the property name's inplace editor is closed. + * Ignores the change if the user pressed escape, otherwise + * commits it. + * + * @param {string} aValue + * The value contained in the editor. + * @param {boolean} aCommit + * True if the change should be applied. + */ + _onNameDone: function(aValue, aCommit) { + if (aCommit && !this.ruleEditor.isEditing) { + // Unlike the value editor, if a name is empty the entire property + // should always be removed. + if (aValue.trim() === "") { + this.remove(); + } else { + // 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(aValue); + + if (properties.length) { + this.prop.setName(properties[0].name); + 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 available property. + */ + remove: function() { + if (this._colorSwatchSpans && this._colorSwatchSpans.length) { + for (let span of this._colorSwatchSpans) { + this.ruleEditor.ruleView.tooltips.colorPicker.removeSwatch(span); + } + } + + this.element.parentNode.removeChild(this.element); + this.ruleEditor.rule.editClosestTextProperty(this.prop); + 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} aValue + * The value contained in the editor. + * @param {bool} aCommit + * True if the change should be applied. + */ + _onValueDone: function(aValue, aCommit) { + if (!aCommit && !this.ruleEditor.isEditing) { + // A new property should be removed when escape is pressed. + if (this.removeOnRevert) { + this.remove(); + } else { + this.prop.setValue(this.committed.value, this.committed.priority); + } + return; + } + + let {propertiesToAdd,firstValue} = this._getValueAndExtraProperties(aValue); + + // First, set this property value (common case, only modified a property) + let val = parseSingleValue(firstValue); + + this.prop.setValue(val.value, val.priority); + this.removeOnRevert = false; + this.committed.value = this.prop.value; + this.committed.priority = this.prop.priority; + + // If needed, add any new properties after this.prop. + this.ruleEditor.addProperties(propertiesToAdd, this.prop); + + // If the name or value is not actively being edited, and the value is + // empty, 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 (val.value.trim() === "") { + setTimeout(() => { + if (!this.editing) { + this.remove(); + } + }, 0); + } + }, + + /** + * 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} aValue + * 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(aValue) { + // 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 = aValue; + let propertiesToAdd = []; + + let properties = parseDeclarations(aValue); + + // 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); + } + // In some cases, the value could be a property:value pair itself. + // Join them as one value string and append potentially following properties + else if (properties[0].name && properties[0].value) { + firstValue = properties[0].name + ": " + properties[0].value; + propertiesToAdd = properties.slice(1); + } + } + + return { + propertiesToAdd: propertiesToAdd, + firstValue: firstValue + }; + }, + + /** + * Apply a new value. + * + * @param {String} aValue + * The value to replace. + * @param {Boolean} markChanged=true + * Set this to false if you need to prevent the property from being + * marked as changed e.g. tooltips do this when <escape> is pressed + * in order to revert the value. + */ + _applyNewValue: function(aValue, markChanged=true) { + let val = parseSingleValue(aValue); + + if (!markChanged) { + let store = this.prop.rule.elementStyle.store; + this.prop.editor.committed.value = aValue; + store.userProperties.setProperty(this.prop.rule.style, + this.prop.rule.name, aValue); + } + + this.prop.setValue(val.value, val.priority, markChanged); + this.removeOnRevert = false; + this.committed.value = this.prop.value; + this.committed.priority = this.prop.priority; + }, + + /** + * Live preview this property, without committing changes. + * @param {string} aValue The value to set the current property to. + */ + _previewValue: function(aValue) { + // Since function call is throttled, we need to make sure we are still + // editing, and any selector modifications have been completed + if (!this.editing || this.ruleEditor.isEditing) { + return; + } + + let val = parseSingleValue(aValue); + 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 {bool} true if the property value is valid, false otherwise. + */ + isValid: function() { + return domUtils.cssPropertyIsValid(this.prop.name, this.prop.value); + } +}; + +/** + * 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} aStyle + * The CSSStyleDeclaration against which the property is mapped. + * @param {string} aName + * The name of the property to get. + * @param {string} aDefault + * Default value. + * @return {string} + * The property value if it has previously been set by the user, null + * otherwise. + */ + getProperty: function(aStyle, aName, aDefault) { + let key = this.getKey(aStyle); + let entry = this.map.get(key, null); + + if (entry && aName in entry) { + return entry[aName]; + } + return aDefault; + }, + + /** + * Set a named property for a given CSSStyleDeclaration. + * + * @param {CSSStyleDeclaration} aStyle + * The CSSStyleDeclaration against which the property is to be mapped. + * @param {String} aName + * The name of the property to set. + * @param {String} aUserValue + * The value of the property to set. + */ + setProperty: function(aStyle, aName, aUserValue) { + let key = this.getKey(aStyle, aName); + let entry = this.map.get(key, null); + + if (entry) { + entry[aName] = aUserValue; + } else { + let props = {}; + props[aName] = aUserValue; + this.map.set(key, props); + } + }, + + /** + * Check whether a named property for a given CSSStyleDeclaration is stored. + * + * @param {CSSStyleDeclaration} aStyle + * The CSSStyleDeclaration against which the property would be mapped. + * @param {String} aName + * The name of the property to check. + */ + contains: function(aStyle, aName) { + let key = this.getKey(aStyle, aName); + let entry = this.map.get(key, null); + return !!entry && aName in entry; + }, + + getKey: function(aStyle, aName) { + return aStyle.actorID + ":" + aName; + }, + + clear: function() { + this.map.clear(); + } +}; + +/** + * Helper functions + */ + +/** + * Create a child element with a set of attributes. + * + * @param {Element} aParent + * The parent node. + * @param {string} aTag + * The tag name. + * @param {object} aAttributes + * A set of attributes to set on the node. + */ +function createChild(aParent, aTag, aAttributes) { + let elt = aParent.ownerDocument.createElementNS(HTML_NS, aTag); + for (let attr in aAttributes) { + if (aAttributes.hasOwnProperty(attr)) { + if (attr === "textContent") { + elt.textContent = aAttributes[attr]; + } else if(attr === "child") { + elt.appendChild(aAttributes[attr]); + } else { + elt.setAttribute(attr, aAttributes[attr]); + } + } + } + aParent.appendChild(elt); + return elt; +} + +function createMenuItem(aMenu, aAttributes) { + let item = aMenu.ownerDocument.createElementNS(XUL_NS, "menuitem"); + + item.setAttribute("label", _strings.GetStringFromName(aAttributes.label)); + item.setAttribute("accesskey", _strings.GetStringFromName(aAttributes.accesskey)); + item.addEventListener("command", aAttributes.command); + + aMenu.appendChild(item); + + return item; +} + +function setTimeout() { + let window = Services.appShell.hiddenDOMWindow; + return window.setTimeout.apply(window, arguments); +} + +function clearTimeout() { + let window = Services.appShell.hiddenDOMWindow; + return window.clearTimeout.apply(window, arguments); +} + +function throttle(func, wait, scope) { + var timer = null; + return function() { + if(timer) { + clearTimeout(timer); + } + var args = arguments; + timer = setTimeout(function() { + timer = null; + func.apply(scope, args); + }, wait); + }; +} + +/** + * Event handler that causes a blur on the target if the input has + * multiple CSS properties as the value. + */ +function blurOnMultipleProperties(e) { + setTimeout(() => { + let props = parseDeclarations(e.target.value); + if (props.length > 1) { + e.target.blur(); + } + }, 0); +} + +/** + * Append a text node to an element. + */ +function appendText(aParent, aText) { + aParent.appendChild(aParent.ownerDocument.createTextNode(aText)); +} + +/** + * 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; + } +} + +XPCOMUtils.defineLazyGetter(this, "clipboardHelper", function() { + return Cc["@mozilla.org/widget/clipboardhelper;1"]. + getService(Ci.nsIClipboardHelper); +}); + +XPCOMUtils.defineLazyGetter(this, "_strings", function() { + return Services.strings.createBundle( + "chrome://global/locale/devtools/styleinspector.properties"); +}); + +XPCOMUtils.defineLazyGetter(this, "domUtils", function() { + return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils); +}); + +loader.lazyGetter(this, "AutocompletePopup", () => require("devtools/shared/autocomplete-popup").AutocompletePopup); diff --git a/toolkit/devtools/styleinspector/ruleview.css b/toolkit/devtools/styleinspector/ruleview.css new file mode 100644 index 000000000..2f86a8839 --- /dev/null +++ b/toolkit/devtools/styleinspector/ruleview.css @@ -0,0 +1,61 @@ +/* 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/. */ + +#root { + display: -moz-box; +} + +.ruleview { + overflow: auto; + -moz-user-select: text; +} + +.ruleview-code { + direction: ltr; +} + +.ruleview-property:not(:hover) > .ruleview-enableproperty { + pointer-events: none; +} + +.ruleview-namecontainer, +.ruleview-selectorcontainer { + cursor: text; +} + +.ruleview-propertycontainer { + cursor: text; + padding-right: 15px; +} + +.ruleview-propertycontainer a { + cursor: pointer; +} + +.ruleview-computedlist:not(.styleinspector-open), +.ruleview-warning[hidden] { + display: none; +} + +.ruleview-expandable-container { + display: none; +} + +.show-expandable-container + .ruleview-expandable-container { + display: block; +} + +.ruleview .ruleview-expander { + vertical-align: middle; +} + +.ruleview-header { + vertical-align: middle; + min-height: 1.5em; + line-height: 1.5em; +} + +.ruleview-header.ruleview-expandable-header { + cursor: pointer; +} diff --git a/toolkit/devtools/styleinspector/style-inspector-overlays.js b/toolkit/devtools/styleinspector/style-inspector-overlays.js new file mode 100644 index 000000000..6b38ae8cf --- /dev/null +++ b/toolkit/devtools/styleinspector/style-inspector-overlays.js @@ -0,0 +1,405 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// The style-inspector overlays are: +// - tooltips that appear when hovering over property values +// - editor tooltips that appear when clicking color swatches, etc. +// - in-content highlighters that appear when hovering over property values +// - etc. + +const {Cc, Ci, Cu} = require("chrome"); +const { + Tooltip, + SwatchColorPickerTooltip, + SwatchCubicBezierTooltip +} = require("devtools/shared/widgets/Tooltip"); +const {CssLogic} = require("devtools/styleinspector/css-logic"); +const {Promise:promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); +Cu.import("resource://gre/modules/Task.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +const PREF_IMAGE_TOOLTIP_SIZE = "devtools.inspector.imagePreviewTooltipSize"; + +// Types of existing tooltips +const TOOLTIP_IMAGE_TYPE = "image"; +const TOOLTIP_FONTFAMILY_TYPE = "font-family"; + +// Types of existing highlighters +const HIGHLIGHTER_TRANSFORM_TYPE = "CssTransformHighlighter"; +const HIGHLIGHTER_SELECTOR_TYPE = "SelectorHighlighter"; +const HIGHLIGHTER_TYPES = [ + HIGHLIGHTER_TRANSFORM_TYPE, + HIGHLIGHTER_SELECTOR_TYPE +]; + +// Types of nodes in the rule/computed-view +const VIEW_NODE_SELECTOR_TYPE = exports.VIEW_NODE_SELECTOR_TYPE = 1; +const VIEW_NODE_PROPERTY_TYPE = exports.VIEW_NODE_PROPERTY_TYPE = 2; +const VIEW_NODE_VALUE_TYPE = exports.VIEW_NODE_VALUE_TYPE = 3; +const VIEW_NODE_IMAGE_URL_TYPE = exports.VIEW_NODE_IMAGE_URL_TYPE = 4; + +/** + * Manages all highlighters in the style-inspector. + * @param {CssRuleView|CssHtmlTree} view Either the rule-view or computed-view + * panel + */ +function HighlightersOverlay(view) { + this.view = view; + + let {CssRuleView} = require("devtools/styleinspector/rule-view"); + this.isRuleView = view instanceof CssRuleView; + + this.highlighterUtils = this.view.inspector.toolbox.highlighterUtils; + + this._onMouseMove = this._onMouseMove.bind(this); + this._onMouseLeave = this._onMouseLeave.bind(this); + + this.promises = {}; + this.highlighters = {}; + + // Only initialize the overlay if at least one of the highlighter types is + // supported + this.supportsHighlighters = this.highlighterUtils.supportsCustomHighlighters(); +} + +exports.HighlightersOverlay = HighlightersOverlay; + +HighlightersOverlay.prototype = { + /** + * Add the highlighters overlay to the view. This will start tracking mouse + * movements and display highlighters when needed + */ + addToView: function() { + if (!this.supportsHighlighters || this._isStarted || this._isDestroyed) { + return; + } + + let el = this.view.element; + el.addEventListener("mousemove", this._onMouseMove, false); + el.addEventListener("mouseleave", this._onMouseLeave, false); + + this._isStarted = true; + }, + + /** + * Remove the overlay from the current view. This will stop tracking mouse + * movement and showing highlighters + */ + removeFromView: function() { + if (!this.supportsHighlighters || !this._isStarted || this._isDestroyed) { + return; + } + + this._hideCurrent(); + + let el = this.view.element; + el.removeEventListener("mousemove", this._onMouseMove, false); + el.removeEventListener("mouseleave", this._onMouseLeave, false); + + this._isStarted = false; + }, + + _onMouseMove: function(event) { + // Bail out if the target is the same as for the last mousemove + if (event.target === this._lastHovered) { + return; + } + + // Only one highlighter can be displayed at a time, hide the currently shown + this._hideCurrent(); + + this._lastHovered = event.target; + + let nodeInfo = this.view.getNodeInfo(event.target); + if (!nodeInfo) { + return; + } + + // Choose the type of highlighter required for the hovered node + let type, options; + if (this._isRuleViewTransform(nodeInfo) || + this._isComputedViewTransform(nodeInfo)) { + type = HIGHLIGHTER_TRANSFORM_TYPE; + } else if (nodeInfo.type === VIEW_NODE_SELECTOR_TYPE) { + type = HIGHLIGHTER_SELECTOR_TYPE; + options = { + selector: nodeInfo.value, + hideInfoBar: true, + showOnly: "border", + region: "border" + }; + } + + if (type) { + this.highlighterShown = type; + let node = this.view.inspector.selection.nodeFront; + this._getHighlighter(type).then(highlighter => { + highlighter.show(node, options); + }); + } + }, + + _onMouseLeave: function(event) { + this._lastHovered = null; + this._hideCurrent(); + }, + + /** + * Is the current hovered node a css transform property value in the rule-view + * @param {Object} nodeInfo + * @return {Boolean} + */ + _isRuleViewTransform: function(nodeInfo) { + let isTransform = nodeInfo.type === VIEW_NODE_VALUE_TYPE && + nodeInfo.value.property === "transform"; + let isEnabled = nodeInfo.value.enabled && + !nodeInfo.value.overridden && + !nodeInfo.value.pseudoElement; + return this.isRuleView && isTransform && isEnabled; + }, + + /** + * Is the current hovered node a css transform property value in the + * computed-view + * @param {Object} nodeInfo + * @return {Boolean} + */ + _isComputedViewTransform: function(nodeInfo) { + let isTransform = nodeInfo.type === VIEW_NODE_VALUE_TYPE && + nodeInfo.value.property === "transform"; + return !this.isRuleView && isTransform; + }, + + /** + * Hide the currently shown highlighter + */ + _hideCurrent: function() { + if (this.highlighterShown) { + this._getHighlighter(this.highlighterShown).then(highlighter => { + // For some reason, the call to highlighter.hide doesn't always return a + // promise. This causes some tests to fail when trying to install a + // rejection handler on the result of the call. To avoid this, check + // whether the result is truthy before installing the handler. + let promise = highlighter.hide(); + if (promise) { + promise.then(null, Cu.reportError); + } + this.highlighterShown = null; + }); + } + }, + + /** + * Get a highlighter front given a type. It will only be initialized once + * @param {String} type The highlighter type. One of this.highlighters + * @return a promise that resolves to the highlighter + */ + _getHighlighter: function(type) { + let utils = this.highlighterUtils; + + if (this.promises[type]) { + return this.promises[type]; + } + + return this.promises[type] = utils.getHighlighterByType(type).then(highlighter => { + this.highlighters[type] = highlighter; + return highlighter; + }); + }, + + /** + * Destroy this overlay instance, removing it from the view and destroying + * all initialized highlighters + */ + destroy: function() { + this.removeFromView(); + + for (let type in this.highlighters) { + if (this.highlighters[type]) { + this.highlighters[type].finalize(); + this.highlighters[type] = null; + } + } + + this.promises = null; + this.view = null; + this.highlighterUtils = null; + + this._isDestroyed = true; + } +}; + +/** + * Manages all tooltips in the style-inspector. + * @param {CssRuleView|CssHtmlTree} view Either the rule-view or computed-view + * panel + */ +function TooltipsOverlay(view) { + this.view = view; + + let {CssRuleView} = require("devtools/styleinspector/rule-view"); + this.isRuleView = view instanceof CssRuleView; + + this._onNewSelection = this._onNewSelection.bind(this); + this.view.inspector.selection.on("new-node-front", this._onNewSelection); +} + +exports.TooltipsOverlay = TooltipsOverlay; + +TooltipsOverlay.prototype = { + get isEditing() { + return this.colorPicker.tooltip.isShown() || + this.colorPicker.eyedropperOpen || + this.cubicBezier.tooltip.isShown(); + }, + + /** + * Add the tooltips overlay to the view. This will start tracking mouse + * movements and display tooltips when needed + */ + addToView: function() { + if (this._isStarted || this._isDestroyed) { + return; + } + + // Image, fonts, ... preview tooltip + this.previewTooltip = new Tooltip(this.view.inspector.panelDoc); + this.previewTooltip.startTogglingOnHover(this.view.element, + this._onPreviewTooltipTargetHover.bind(this)); + + if (this.isRuleView) { + // Color picker tooltip + this.colorPicker = new SwatchColorPickerTooltip(this.view.inspector.panelDoc); + // Cubic bezier tooltip + this.cubicBezier = new SwatchCubicBezierTooltip(this.view.inspector.panelDoc); + } + + this._isStarted = true; + }, + + /** + * Remove the tooltips overlay from the view. This will stop tracking mouse + * movements and displaying tooltips + */ + removeFromView: function() { + if (!this._isStarted || this._isDestroyed) { + return; + } + + this.previewTooltip.stopTogglingOnHover(this.view.element); + this.previewTooltip.destroy(); + + if (this.colorPicker) { + this.colorPicker.destroy(); + } + + if (this.cubicBezier) { + this.cubicBezier.destroy(); + } + + this._isStarted = false; + }, + + /** + * Given a hovered node info, find out which type of tooltip should be shown, + * if any + * @param {Object} nodeInfo + * @return {String} The tooltip type to be shown, or null + */ + _getTooltipType: function({type, value:prop}) { + let tooltipType = null; + let inspector = this.view.inspector; + + // Image preview tooltip + if (type === VIEW_NODE_IMAGE_URL_TYPE && inspector.hasUrlToImageDataResolver) { + tooltipType = TOOLTIP_IMAGE_TYPE; + } + + // Font preview tooltip + if (type === VIEW_NODE_VALUE_TYPE && prop.property === "font-family") { + let value = prop.value.toLowerCase(); + if (value !== "inherit" && value !== "unset" && value !== "initial") { + tooltipType = TOOLTIP_FONTFAMILY_TYPE; + } + } + + return tooltipType; + }, + + /** + * Executed by the tooltip when the pointer hovers over an element of the view. + * Used to decide whether the tooltip should be shown or not and to actually + * put content in it. + * Checks if the hovered target is a css value we support tooltips for. + * @param {DOMNode} target The currently hovered node + */ + _onPreviewTooltipTargetHover: function(target) { + let nodeInfo = this.view.getNodeInfo(target); + if (!nodeInfo) { + // The hovered node isn't something we care about + return promise.reject(); + } + + let type = this._getTooltipType(nodeInfo); + if (!type) { + // There is no tooltip type defined for the hovered node + return promise.reject(); + } + + if (this.isRuleView && this.colorPicker.tooltip.isShown()) { + this.colorPicker.revert(); + this.colorPicker.hide(); + } + + if (this.isRuleView && this.cubicBezier.tooltip.isShown()) { + this.cubicBezier.revert(); + this.cubicBezier.hide(); + } + + let inspector = this.view.inspector; + + if (type === TOOLTIP_IMAGE_TYPE) { + let dim = Services.prefs.getIntPref(PREF_IMAGE_TOOLTIP_SIZE); + // nodeInfo contains an absolute uri + let uri = nodeInfo.value.url; + return this.previewTooltip.setRelativeImageContent(uri, + inspector.inspector, dim); + } + + if (type === TOOLTIP_FONTFAMILY_TYPE) { + return this.previewTooltip.setFontFamilyContent(nodeInfo.value.value, + inspector.selection.nodeFront); + } + }, + + _onNewSelection: function() { + if (this.previewTooltip) { + this.previewTooltip.hide(); + } + + if (this.colorPicker) { + this.colorPicker.hide(); + } + + if (this.cubicBezier) { + this.cubicBezier.hide(); + } + }, + + /** + * Destroy this overlay instance, removing it from the view + */ + destroy: function() { + this.removeFromView(); + + this.view.inspector.selection.off("new-node-front", this._onNewSelection); + this.view = null; + + this._isDestroyed = true; + } +}; diff --git a/toolkit/devtools/styleinspector/style-inspector.js b/toolkit/devtools/styleinspector/style-inspector.js new file mode 100644 index 000000000..1a63111e2 --- /dev/null +++ b/toolkit/devtools/styleinspector/style-inspector.js @@ -0,0 +1,251 @@ +/* -*- 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/. */ + +const {Cc, Cu, Ci} = require("chrome"); +const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); +const {Tools} = require("main"); +Cu.import("resource://gre/modules/Services.jsm"); +const {PREF_ORIG_SOURCES} = require("devtools/styleeditor/utils"); + +loader.lazyGetter(this, "gDevTools", () => Cu.import("resource:///modules/devtools/gDevTools.jsm", {}).gDevTools); +loader.lazyGetter(this, "RuleView", () => require("devtools/styleinspector/rule-view")); +loader.lazyGetter(this, "ComputedView", () => require("devtools/styleinspector/computed-view")); +loader.lazyGetter(this, "_strings", () => Services.strings + .createBundle("chrome://global/locale/devtools/styleinspector.properties")); + +// This module doesn't currently export any symbols directly, it only +// registers inspector tools. + +function RuleViewTool(inspector, window, iframe) { + this.inspector = inspector; + this.doc = window.document; + + this.view = new RuleView.CssRuleView(inspector, this.doc); + this.doc.documentElement.appendChild(this.view.element); + + this.onLinkClicked = this.onLinkClicked.bind(this); + this.onSelected = this.onSelected.bind(this); + this.refresh = this.refresh.bind(this); + this.clearUserProperties = this.clearUserProperties.bind(this); + this.onPropertyChanged = this.onPropertyChanged.bind(this); + this.onViewRefreshed = this.onViewRefreshed.bind(this); + this.onPanelSelected = this.onPanelSelected.bind(this); + + this.view.element.addEventListener("CssRuleViewChanged", this.onPropertyChanged); + this.view.element.addEventListener("CssRuleViewRefreshed", this.onViewRefreshed); + this.view.element.addEventListener("CssRuleViewCSSLinkClicked", this.onLinkClicked); + + this.inspector.selection.on("detached", this.onSelected); + this.inspector.selection.on("new-node-front", this.onSelected); + this.inspector.on("layout-change", this.refresh); + this.inspector.selection.on("pseudoclass", this.refresh); + this.inspector.target.on("navigate", this.clearUserProperties); + this.inspector.sidebar.on("ruleview-selected", this.onPanelSelected); + + 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(event) { + let rule = event.detail.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 contentDoc = this.inspector.selection.document; + let viewSourceUtils = this.inspector.viewSourceUtils; + let href = rule.nodeHref || rule.href; + viewSourceUtils.viewSource(href, null, contentDoc, rule.line || 0); + 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 sheet = source || href; + toolbox.getCurrentPanel().selectStyleSheet(sheet, line, column); + }); + } + return; + }) + }, + + onPropertyChanged: function() { + this.inspector.markDirty(); + }, + + onViewRefreshed: function() { + this.inspector.emit("rule-view-refreshed"); + }, + + destroy: function() { + this.inspector.off("layout-change", this.refresh); + 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); + + this.view.element.removeEventListener("CssRuleViewCSSLinkClicked", this.onLinkClicked); + this.view.element.removeEventListener("CssRuleViewChanged", this.onPropertyChanged); + this.view.element.removeEventListener("CssRuleViewRefreshed", this.onViewRefreshed); + + this.doc.documentElement.removeChild(this.view.element); + + this.view.destroy(); + + this.view = this.doc = this.inspector = null; + } +}; + +function ComputedViewTool(inspector, window, iframe) { + this.inspector = inspector; + this.doc = window.document; + + this.view = new ComputedView.CssHtmlTree(this, inspector.pageStyle); + + this.onSelected = this.onSelected.bind(this); + this.refresh = this.refresh.bind(this); + this.onPanelSelected = this.onPanelSelected.bind(this); + + this.inspector.selection.on("detached", this.onSelected); + this.inspector.selection.on("new-node-front", this.onSelected); + this.inspector.on("layout-change", this.refresh); + this.inspector.selection.on("pseudoclass", this.refresh); + this.inspector.sidebar.on("computedview-selected", this.onPanelSelected); + + this.view.selectElement(null); + + this.onSelected(); +} + +ComputedViewTool.prototype = { + isSidebarActive: function() { + if (!this.view) { + return; + } + return this.inspector.sidebar.getCurrentTabID() == "computedview"; + }, + + 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("computed-view"); + this.view.selectElement(this.inspector.selection.nodeFront).then(() => { + done(); + }); + } + }, + + refresh: function() { + if (this.isSidebarActive()) { + this.view.refreshPanel(); + } + }, + + onPanelSelected: function() { + if (this.inspector.selection.nodeFront === this.view.viewedElement) { + this.refresh(); + } else { + this.onSelected(); + } + }, + + destroy: function() { + this.inspector.off("layout-change", this.refresh); + this.inspector.sidebar.off("computedview-selected", this.refresh); + this.inspector.selection.off("pseudoclass", this.refresh); + this.inspector.selection.off("new-node-front", this.onSelected); + this.inspector.sidebar.off("computedview-selected", this.onPanelSelected); + + this.view.destroy(); + + this.view = this.cssLogic = this.cssHtmlTree = null; + this.doc = this.inspector = null; + } +}; + +exports.RuleViewTool = RuleViewTool; +exports.ComputedViewTool = ComputedViewTool; diff --git a/toolkit/devtools/styleinspector/test/browser.ini b/toolkit/devtools/styleinspector/test/browser.ini new file mode 100644 index 000000000..d551f35c4 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser.ini @@ -0,0 +1,126 @@ +[DEFAULT] +subsuite = devtools +support-files = + doc_content_stylesheet.html + doc_content_stylesheet.xul + doc_content_stylesheet_imported.css + doc_content_stylesheet_imported2.css + doc_content_stylesheet_linked.css + doc_content_stylesheet_script.css + doc_content_stylesheet_xul.css + doc_frame_script.js + doc_keyframeanimation.html + doc_keyframeanimation.css + doc_matched_selectors.html + doc_media_queries.html + doc_pseudoelement.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 + +[browser_computedview_browser-styles.js] +[browser_computedview_getNodeInfo.js] +[browser_computedview_keybindings_01.js] +[browser_computedview_keybindings_02.js] +[browser_computedview_matched-selectors-toggle.js] +[browser_computedview_matched-selectors_01.js] +[browser_computedview_matched-selectors_02.js] +[browser_computedview_media-queries.js] +[browser_computedview_no-results-placeholder.js] +[browser_computedview_original-source-link.js] +[browser_computedview_pseudo-element_01.js] +[browser_computedview_refresh-on-style-change_01.js] +[browser_computedview_search-filter.js] +[browser_computedview_select-and-copy-styles.js] +[browser_computedview_style-editor-link.js] +[browser_ruleview_add-property-and-reselect.js] +[browser_ruleview_add-property-cancel_01.js] +[browser_ruleview_add-property-cancel_02.js] +[browser_ruleview_add-property-cancel_03.js] +[browser_ruleview_add-property_01.js] +[browser_ruleview_add-property_02.js] +[browser_ruleview_add-rule_01.js] +[browser_ruleview_add-rule_02.js] +[browser_ruleview_add-rule_03.js] +[browser_ruleview_colorpicker-and-image-tooltip_01.js] +[browser_ruleview_colorpicker-and-image-tooltip_02.js] +[browser_ruleview_colorpicker-appears-on-swatch-click.js] +[browser_ruleview_colorpicker-commit-on-ENTER.js] +[browser_ruleview_colorpicker-edit-gradient.js] +[browser_ruleview_colorpicker-hides-on-tooltip.js] +[browser_ruleview_colorpicker-multiple-changes.js] +[browser_ruleview_colorpicker-revert-on-ESC.js] +[browser_ruleview_colorpicker-swatch-displayed.js] +[browser_ruleview_completion-existing-property_01.js] +[browser_ruleview_completion-existing-property_02.js] +[browser_ruleview_completion-new-property_01.js] +[browser_ruleview_completion-new-property_02.js] +[browser_ruleview_content_01.js] +[browser_ruleview_content_02.js] +skip-if = e10s # Bug 1039528: "inspect element" contextual-menu doesn't work with e10s +[browser_ruleview_cubicbezier-appears-on-swatch-click.js] +[browser_ruleview_cubicbezier-commit-on-ENTER.js] +[browser_ruleview_cubicbezier-revert-on-ESC.js] +[browser_ruleview_edit-property-commit.js] +[browser_ruleview_edit-property-increments.js] +[browser_ruleview_edit-property-order.js] +[browser_ruleview_edit-property_01.js] +[browser_ruleview_edit-property_02.js] +[browser_ruleview_edit-selector-commit.js] +[browser_ruleview_edit-selector_01.js] +[browser_ruleview_edit-selector_02.js] +[browser_ruleview_eyedropper.js] +skip-if = (os == "win" && debug) || e10s # bug 963492: win. bug 1040653: e10s. +[browser_ruleview_inherit.js] +[browser_ruleview_keybindings.js] +[browser_ruleview_keyframes-rule_01.js] +[browser_ruleview_keyframes-rule_02.js] +[browser_ruleview_livepreview.js] +[browser_ruleview_mathml-element.js] +[browser_ruleview_media-queries.js] +[browser_ruleview_multiple-properties-duplicates.js] +[browser_ruleview_multiple-properties-priority.js] +[browser_ruleview_multiple-properties-unfinished_01.js] +[browser_ruleview_multiple-properties-unfinished_02.js] +[browser_ruleview_multiple_properties_01.js] +[browser_ruleview_multiple_properties_02.js] +[browser_ruleview_original-source-link.js] +[browser_ruleview_override.js] +[browser_ruleview_pseudo-element_01.js] +[browser_ruleview_pseudo-element_02.js] +skip-if = e10s # Bug 1090340 +[browser_ruleview_refresh-on-attribute-change_01.js] +[browser_ruleview_refresh-on-attribute-change_02.js] +[browser_ruleview_refresh-on-style-change.js] +[browser_ruleview_select-and-copy-styles.js] +[browser_ruleview_selector-highlighter_01.js] +[browser_ruleview_selector-highlighter_02.js] +[browser_ruleview_style-editor-link.js] +skip-if = e10s # bug 1040670 Cannot open inline styles in viewSourceUtils +[browser_ruleview_urls-clickable.js] +[browser_ruleview_user-agent-styles.js] +[browser_ruleview_user-agent-styles-uneditable.js] +[browser_ruleview_user-property-reset.js] +[browser_styleinspector_context-menu-copy-color_01.js] +[browser_styleinspector_context-menu-copy-color_02.js] +[browser_styleinspector_csslogic-content-stylesheets.js] +[browser_styleinspector_output-parser.js] +[browser_styleinspector_refresh_when_active.js] +[browser_styleinspector_tooltip-background-image.js] +[browser_styleinspector_tooltip-closes-on-new-selection.js] +skip-if = e10s # Bug 1111546 +[browser_styleinspector_tooltip-longhand-fontfamily.js] +[browser_styleinspector_tooltip-multiple-background-images.js] +[browser_styleinspector_tooltip-shorthand-fontfamily.js] +[browser_styleinspector_tooltip-size.js] +skip-if = e10s # Bug 1111546 +[browser_styleinspector_transform-highlighter-01.js] +[browser_styleinspector_transform-highlighter-02.js] +[browser_styleinspector_transform-highlighter-03.js] +[browser_styleinspector_transform-highlighter-04.js] diff --git a/toolkit/devtools/styleinspector/test/browser_computedview_browser-styles.js b/toolkit/devtools/styleinspector/test/browser_computedview_browser-styles.js new file mode 100644 index 000000000..7bb9f82ac --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_computedview_browser-styles.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"; + +// Tests that the checkbox to include browser styles works properly. + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,default styles test"); + + info("Creating the test document"); + content.document.body.innerHTML = '<style type="text/css"> ' + + '.matches {color: #F00;}</style>' + + '<span id="matches" class="matches">Some styled text</span>' + + '</div>'; + content.document.title = "Style Inspector Default Styles Test"; + + info("Opening the computed view"); + let {toolbox, inspector, view} = yield openComputedView(); + + info("Selecting the test node"); + yield selectNode("#matches", inspector); + + info("Checking the default styles"); + is(isPropertyVisible("color", view), true, + "span #matches color property is visible"); + is(isPropertyVisible("background-color", view), false, + "span #matches background-color property is hidden"); + + info("Toggling the browser styles"); + let doc = view.styleDocument; + let checkbox = doc.querySelector(".includebrowserstyles"); + let onRefreshed = inspector.once("computed-view-refreshed"); + checkbox.click(); + yield onRefreshed; + + info("Checking the browser styles"); + is(isPropertyVisible("color", view), true, + "span color property is visible"); + is(isPropertyVisible("background-color", view), true, + "span background-color property is visible"); +}); + +function isPropertyVisible(name, view) { + info("Checking property visibility for " + name); + let propertyViews = view.propertyViews; + for each (let propView in propertyViews) { + if (propView.name == name) { + return propView.visible; + } + } + return false; +} diff --git a/toolkit/devtools/styleinspector/test/browser_computedview_getNodeInfo.js b/toolkit/devtools/styleinspector/test/browser_computedview_getNodeInfo.js new file mode 100644 index 000000000..4457ecd3a --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_computedview_getNodeInfo.js @@ -0,0 +1,177 @@ +/* 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 various output of the computed-view's getNodeInfo method. +// This method is used by the style-inspector-overlay on mouseover to decide +// which tooltip or highlighter to show when hovering over a value/name/selector +// if any. +// For instance, browser_ruleview_selector-highlighter_01.js and +// browser_ruleview_selector-highlighter_02.js test that the selector +// highlighter appear when hovering over a selector in the rule-view. +// Since the code to make this work for the computed-view is 90% the same, there +// is no need for testing it again here. +// This test however serves as a unit test for getNodeInfo. + +const { + VIEW_NODE_SELECTOR_TYPE, + VIEW_NODE_PROPERTY_TYPE, + VIEW_NODE_VALUE_TYPE, + VIEW_NODE_IMAGE_URL_TYPE +} = devtools.require("devtools/styleinspector/style-inspector-overlays"); + +const PAGE_CONTENT = [ + '<style type="text/css">', + ' body {', + ' background: red;', + ' color: white;', + ' }', + ' div {', + ' background: green;', + ' }', + ' div div {', + ' background-color: yellow;', + ' background-image: url(chrome://global/skin/icons/warning-64.png);', + ' color: red;', + ' }', + '</style>', + '<div><div id="testElement">Test element</div></div>' +].join("\n"); + +// Each item in this array must have the following properties: +// - desc {String} will be logged for information +// - getHoveredNode {Generator Function} received the computed-view instance as +// argument and must return the node to be tested +// - assertNodeInfo {Function} should check the validity of the nodeInfo +// argument it receives +const TEST_DATA = [ + { + desc: "Testing a null node", + getHoveredNode: function*() { + return null; + }, + assertNodeInfo: function(nodeInfo) { + is(nodeInfo, null); + } + }, + { + desc: "Testing a useless node", + getHoveredNode: function*(view) { + return view.element; + }, + assertNodeInfo: function(nodeInfo) { + is(nodeInfo, null); + } + }, + { + desc: "Testing a property name", + getHoveredNode: function*(view) { + return getComputedViewProperty(view, "color").nameSpan; + }, + assertNodeInfo: function(nodeInfo) { + is(nodeInfo.type, VIEW_NODE_PROPERTY_TYPE); + ok("property" in nodeInfo.value); + ok("value" in nodeInfo.value); + is(nodeInfo.value.property, "color"); + is(nodeInfo.value.value, "#F00"); + } + }, + { + desc: "Testing a property value", + getHoveredNode: function*(view) { + return getComputedViewProperty(view, "color").valueSpan; + }, + assertNodeInfo: function(nodeInfo) { + is(nodeInfo.type, VIEW_NODE_VALUE_TYPE); + ok("property" in nodeInfo.value); + ok("value" in nodeInfo.value); + is(nodeInfo.value.property, "color"); + is(nodeInfo.value.value, "#F00"); + } + }, + { + desc: "Testing an image url", + getHoveredNode: function*(view) { + let {valueSpan} = getComputedViewProperty(view, "background-image"); + return valueSpan.querySelector(".theme-link"); + }, + assertNodeInfo: function(nodeInfo) { + is(nodeInfo.type, VIEW_NODE_IMAGE_URL_TYPE); + ok("property" in nodeInfo.value); + ok("value" in nodeInfo.value); + is(nodeInfo.value.property, "background-image"); + is(nodeInfo.value.value, "url(\"chrome://global/skin/icons/warning-64.png\")"); + is(nodeInfo.value.url, "chrome://global/skin/icons/warning-64.png"); + } + }, + { + desc: "Testing a matched rule selector (bestmatch)", + getHoveredNode: function*(view) { + let content = yield getComputedViewMatchedRules(view, "background-color"); + return content.querySelector(".bestmatch"); + }, + assertNodeInfo: function(nodeInfo) { + is(nodeInfo.type, VIEW_NODE_SELECTOR_TYPE); + is(nodeInfo.value, "div div"); + } + }, + { + desc: "Testing a matched rule selector (matched)", + getHoveredNode: function*(view) { + let content = yield getComputedViewMatchedRules(view, "background-color"); + return content.querySelector(".matched"); + }, + assertNodeInfo: function(nodeInfo) { + is(nodeInfo.type, VIEW_NODE_SELECTOR_TYPE); + is(nodeInfo.value, "div"); + } + }, + { + desc: "Testing a matched rule selector (parentmatch)", + getHoveredNode: function*(view) { + let content = yield getComputedViewMatchedRules(view, "color"); + return content.querySelector(".parentmatch"); + }, + assertNodeInfo: function(nodeInfo) { + is(nodeInfo.type, VIEW_NODE_SELECTOR_TYPE); + is(nodeInfo.value, "body"); + } + }, + { + desc: "Testing a matched rule value", + getHoveredNode: function*(view) { + let content = yield getComputedViewMatchedRules(view, "color"); + return content.querySelector(".other-property-value"); + }, + assertNodeInfo: function(nodeInfo) { + is(nodeInfo.type, VIEW_NODE_VALUE_TYPE); + is(nodeInfo.value.property, "color"); + is(nodeInfo.value.value, "#F00"); + } + }, + { + desc: "Testing a matched rule stylesheet link", + getHoveredNode: function*(view) { + let content = yield getComputedViewMatchedRules(view, "color"); + return content.querySelector(".rule-link .theme-link"); + }, + assertNodeInfo: function(nodeInfo) { + is(nodeInfo, null); + } + } +]; + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8," + PAGE_CONTENT); + + let {inspector, view} = yield openComputedView(); + yield selectNode("#testElement", inspector); + + for (let {desc, getHoveredNode, assertNodeInfo} of TEST_DATA) { + info(desc); + let nodeInfo = view.getNodeInfo(yield getHoveredNode(view)); + assertNodeInfo(nodeInfo); + } +}); diff --git a/toolkit/devtools/styleinspector/test/browser_computedview_keybindings_01.js b/toolkit/devtools/styleinspector/test/browser_computedview_keybindings_01.js new file mode 100644 index 000000000..0a805bd62 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_computedview_keybindings_01.js @@ -0,0 +1,79 @@ +/* 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 computed view key bindings + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,default styles test"); + + info("Adding content to the test page"); + content.document.body.innerHTML = '<style type="text/css"> ' + + '.matches {color: #F00;}</style>' + + '<span class="matches">Some styled text</span>' + + '</div>'; + + let {toolbox, inspector, view} = yield openComputedView(); + + info("Selecting the test node"); + yield selectNode(".matches", inspector); + + let propView = getFirstVisiblePropertyView(view); + let rulesTable = propView.matchedSelectorsContainer; + let matchedExpander = propView.element; + + info("Focusing the property"); + let onMatchedExpanderFocus = once(matchedExpander, "focus", true); + EventUtils.synthesizeMouseAtCenter(matchedExpander, {}, view.styleWindow); + yield onMatchedExpanderFocus; + + yield checkToggleKeyBinding(view.styleWindow, "VK_SPACE", rulesTable, inspector); + yield checkToggleKeyBinding(view.styleWindow, "VK_RETURN", rulesTable, inspector); + yield checkHelpLinkKeybinding(view); +}); + +function getFirstVisiblePropertyView(view) { + let propView = null; + view.propertyViews.some(p => { + if (p.visible) { + propView = p; + return true; + } + return false; + }); + + return propView; +} + +function* checkToggleKeyBinding(win, key, rulesTable, inspector) { + info("Pressing " + key + " key a couple of times to check that the property gets expanded/collapsed"); + + let onExpand = inspector.once("computed-view-property-expanded"); + let onCollapse = inspector.once("computed-view-property-collapsed"); + + info("Expanding the property"); + EventUtils.synthesizeKey(key, {}, win); + yield onExpand; + isnot(rulesTable.innerHTML, "", "The property has been expanded"); + + info("Collapsing the property"); + EventUtils.synthesizeKey(key, {}, win); + yield onCollapse; + is(rulesTable.innerHTML, "", "The property has been collapsed"); +} + +function checkHelpLinkKeybinding(view) { + info("Check that MDN link is opened on \"F1\""); + let def = promise.defer(); + + let propView = getFirstVisiblePropertyView(view); + propView.mdnLinkClick = function(aEvent) { + ok(true, "Pressing F1 opened the MDN link"); + def.resolve(); + }; + + EventUtils.synthesizeKey("VK_F1", {}, view.styleWindow); + return def.promise; +} diff --git a/toolkit/devtools/styleinspector/test/browser_computedview_keybindings_02.js b/toolkit/devtools/styleinspector/test/browser_computedview_keybindings_02.js new file mode 100644 index 000000000..e7c3e9a6f --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_computedview_keybindings_02.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 the computed-view keyboard navigation + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,computed view keyboard nav test"); + + content.document.body.innerHTML = '<style type="text/css"> ' + + '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">\n' + + '<h1>Some header text</h1>\n' + + '<p id="salutation" style="font-size: 12pt">hi.</p>\n' + + '<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>\n' + + '<p id="closing">more text</p>\n' + + '<p>even more text</p>' + + '</div>'; + content.document.title = "Computed view keyboard navigation test"; + + info("Opening the computed-view"); + let {toolbox, inspector, view} = yield openComputedView(); + + info("Selecting the test node"); + yield selectNode("span", inspector); + + info("Selecting the first computed style in the list"); + let firstStyle = view.styleDocument.querySelector(".property-view"); + ok(firstStyle, "First computed style found in panel"); + firstStyle.focus(); + + info("Tab to select the 2nd style and press return"); + let onExpanded = inspector.once("computed-view-property-expanded"); + EventUtils.synthesizeKey("VK_TAB", {}); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onExpanded; + + info("Verify the 2nd style has been expanded"); + let secondStyleSelectors = view.styleDocument.querySelectorAll( + ".property-content .matchedselectors")[1]; + ok(secondStyleSelectors.childNodes.length > 0, "Matched selectors expanded"); + + info("Tab back up and test the same thing, with space"); + onExpanded = inspector.once("computed-view-property-expanded"); + EventUtils.synthesizeKey("VK_TAB", {shiftKey: true}); + EventUtils.synthesizeKey("VK_SPACE", {}); + yield onExpanded; + + info("Verify the 1st style has been expanded too"); + let firstStyleSelectors = view.styleDocument.querySelectorAll( + ".property-content .matchedselectors")[0]; + ok(firstStyleSelectors.childNodes.length > 0, "Matched selectors expanded"); +}); diff --git a/toolkit/devtools/styleinspector/test/browser_computedview_matched-selectors-toggle.js b/toolkit/devtools/styleinspector/test/browser_computedview_matched-selectors-toggle.js new file mode 100644 index 000000000..7238b315b --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_computedview_matched-selectors-toggle.js @@ -0,0 +1,108 @@ +/* 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 view properties can be expanded and collapsed with +// either the twisty or by dbl-clicking on the container + +const TEST_URL = "data:text/html;charset=utf-8," + encodeURIComponent([ + '<html>' + + '<head>' + + ' <title>Computed view toggling test</title>', + ' <style type="text/css"> ', + ' html { color: #000000; font-size: 15pt; } ', + ' h1 { color: red; } ', + ' </style>', + '</head>', + '<body>', + ' <h1>Some header text</h1>', + '</body>', + '</html>' +].join("\n")); + +add_task(function*() { + yield addTab(TEST_URL); + let {toolbox, inspector, view} = yield openComputedView(); + + info("Selecting the test node"); + yield selectNode("h1", inspector); + + yield testExpandOnTwistyClick(view, inspector); + yield testCollapseOnTwistyClick(view, inspector); + yield testExpandOnDblClick(view, inspector); + yield testCollapseOnDblClick(view, inspector); +}); + +function* testExpandOnTwistyClick({styleDocument, styleWindow}, inspector) { + info("Testing that a property expands on twisty click"); + + info("Getting twisty element"); + let twisty = styleDocument.querySelector(".expandable"); + ok(twisty, "Twisty found"); + + let onExpand = inspector.once("computed-view-property-expanded"); + info("Clicking on the twisty element"); + twisty.click(); + + yield onExpand; + + // Expanded means the matchedselectors div is not empty + let div = styleDocument.querySelector(".property-content .matchedselectors"); + ok(div.childNodes.length > 0, "Matched selectors are expanded on twisty click"); +} + +function* testCollapseOnTwistyClick({styleDocument, styleWindow}, inspector) { + info("Testing that a property collapses on twisty click"); + + info("Getting twisty element"); + let twisty = styleDocument.querySelector(".expandable"); + ok(twisty, "Twisty found"); + + let onCollapse = inspector.once("computed-view-property-collapsed"); + info("Clicking on the twisty element"); + twisty.click(); + + yield onCollapse; + + // Collapsed means the matchedselectors div is empty + let div = styleDocument.querySelector(".property-content .matchedselectors"); + ok(div.childNodes.length === 0, "Matched selectors are collapsed on twisty click"); +} + +function* testExpandOnDblClick({styleDocument, styleWindow}, inspector) { + info("Testing that a property expands on container dbl-click"); + + info("Getting computed property container"); + let container = styleDocument.querySelector(".property-view"); + ok(container, "Container found"); + + let onExpand = inspector.once("computed-view-property-expanded"); + info("Dbl-clicking on the container"); + EventUtils.synthesizeMouseAtCenter(container, {clickCount: 2}, styleWindow); + + yield onExpand; + + // Expanded means the matchedselectors div is not empty + let div = styleDocument.querySelector(".property-content .matchedselectors"); + ok(div.childNodes.length > 0, "Matched selectors are expanded on dblclick"); +} + +function* testCollapseOnDblClick({styleDocument, styleWindow}, inspector) { + info("Testing that a property collapses on container dbl-click"); + + info("Getting computed property container"); + let container = styleDocument.querySelector(".property-view"); + ok(container, "Container found"); + + let onCollapse = inspector.once("computed-view-property-collapsed"); + info("Dbl-clicking on the container"); + EventUtils.synthesizeMouseAtCenter(container, {clickCount: 2}, styleWindow); + + yield onCollapse; + + // Collapsed means the matchedselectors div is empty + let div = styleDocument.querySelector(".property-content .matchedselectors"); + ok(div.childNodes.length === 0, "Matched selectors are collapsed on dblclick"); +} diff --git a/toolkit/devtools/styleinspector/test/browser_computedview_matched-selectors_01.js b/toolkit/devtools/styleinspector/test/browser_computedview_matched-selectors_01.js new file mode 100644 index 000000000..2c4347d6a --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_computedview_matched-selectors_01.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"; + +// Checking selector counts, matched rules and titles in the computed-view + +const {PropertyView} = devtools.require("devtools/styleinspector/computed-view"); +const TEST_URI = TEST_URL_ROOT + "doc_matched_selectors.html"; + +add_task(function*() { + yield addTab(TEST_URI); + let {toolbox, inspector, view} = yield openComputedView(); + + yield selectNode("#test", inspector); + yield testMatchedSelectors(view, inspector); +}); + +function* testMatchedSelectors(view, inspector) { + info("checking selector counts, matched rules and titles"); + + let nodeFront = yield getNodeFront("#test", inspector); + is(nodeFront, view.viewedElement, "style inspector node matches the selected node"); + + let propertyView = new PropertyView(view, "color"); + propertyView.buildMain(); + propertyView.buildSelectorContainer(); + propertyView.matchedExpanded = true; + + yield propertyView.refreshMatchedSelectors(); + + let numMatchedSelectors = propertyView.matchedSelectors.length; + is(numMatchedSelectors, 6, "CssLogic returns the correct number of matched selectors for div"); + is(propertyView.hasMatchedSelectors, true, "hasMatchedSelectors returns true"); +} diff --git a/toolkit/devtools/styleinspector/test/browser_computedview_matched-selectors_02.js b/toolkit/devtools/styleinspector/test/browser_computedview_matched-selectors_02.js new file mode 100644 index 000000000..0d5193213 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_computedview_matched-selectors_02.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 for matched selector texts in the computed view + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,<div style='color:blue;'></div>"); + + info("Opening the computed view"); + let {toolbox, inspector, view} = yield openComputedView(); + + info("Selecting the test node"); + yield selectNode("div", inspector); + + info("Checking the color property view"); + let propertyView = getPropertyView(view, "color"); + ok(propertyView, "found PropertyView for color"); + is(propertyView.hasMatchedSelectors, true, "hasMatchedSelectors is true"); + + info("Expanding the matched selectors"); + propertyView.matchedExpanded = true; + yield propertyView.refreshMatchedSelectors(); + + let span = propertyView.matchedSelectorsContainer.querySelector("span.rule-text"); + ok(span, "Found the first table row"); + + let selector = propertyView.matchedSelectorViews[0]; + ok(selector, "Found the first matched selector view"); +}); + +function getPropertyView(computedView, name) { + let propertyView = null; + computedView.propertyViews.some(function(view) { + if (view.name == name) { + propertyView = view; + return true; + } + return false; + }); + return propertyView; +} diff --git a/toolkit/devtools/styleinspector/test/browser_computedview_media-queries.js b/toolkit/devtools/styleinspector/test/browser_computedview_media-queries.js new file mode 100644 index 000000000..5159ad6e8 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_computedview_media-queries.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 we correctly display appropriate media query titles in the +// property view. + +const TEST_URI = TEST_URL_ROOT + "doc_media_queries.html"; + +let {PropertyView} = devtools.require("devtools/styleinspector/computed-view"); +let {CssLogic} = devtools.require("devtools/styleinspector/css-logic"); + +add_task(function*() { + yield addTab(TEST_URI); + let {toolbox, inspector, view} = yield openComputedView(); + + info("Selecting the test element"); + yield selectNode("div", inspector); + + info("Checking property view"); + yield checkPropertyView(view); +}); + +function checkPropertyView(view) { + let propertyView = new PropertyView(view, "width"); + propertyView.buildMain(); + propertyView.buildSelectorContainer(); + propertyView.matchedExpanded = true; + + return propertyView.refreshMatchedSelectors().then(() => { + let numMatchedSelectors = propertyView.matchedSelectors.length; + + is(numMatchedSelectors, 2, + "Property view has the correct number of matched selectors for div"); + + is(propertyView.hasMatchedSelectors, true, + "hasMatchedSelectors returns true"); + }); +} diff --git a/toolkit/devtools/styleinspector/test/browser_computedview_no-results-placeholder.js b/toolkit/devtools/styleinspector/test/browser_computedview_no-results-placeholder.js new file mode 100644 index 000000000..261eba91d --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_computedview_no-results-placeholder.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 no results placeholder works properly. + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,no results placeholder test"); + + info("Creating the test document"); + content.document.body.innerHTML = '<style type="text/css"> ' + + '.matches {color: #F00;}</style>' + + '<span id="matches" class="matches">Some styled text</span>'; + content.document.title = "Tests that the no results placeholder works properly"; + + info("Opening the computed view"); + let {toolbox, inspector, view} = yield openComputedView(); + + info("Selecting the test node"); + yield selectNode("#matches", inspector); + + yield enterInvalidFilter(inspector, view); + checkNoResultsPlaceholderShown(view); + + yield clearFilterText(inspector, view); + checkNoResultsPlaceholderHidden(view); +}); + +function* enterInvalidFilter(inspector, computedView) { + let searchbar = computedView.searchField; + let searchTerm = "xxxxx"; + + info("setting filter text to \"" + searchTerm + "\""); + + let onRefreshed = inspector.once("computed-view-refreshed"); + searchbar.focus(); + for each (let c in searchTerm) { + EventUtils.synthesizeKey(c, {}, computedView.styleWindow); + } + yield onRefreshed; +} + +function checkNoResultsPlaceholderShown(computedView) { + info("Checking that the no results placeholder is shown"); + + let placeholder = computedView.noResults; + let win = computedView.styleWindow; + let display = win.getComputedStyle(placeholder).display; + is(display, "block", "placeholder is visible"); +} + +function* clearFilterText(inspector, computedView) { + info("Clearing the filter text"); + + let searchbar = computedView.searchField; + + let onRefreshed = inspector.once("computed-view-refreshed"); + searchbar.focus(); + searchbar.value = ""; + EventUtils.synthesizeKey("c", {}, computedView.styleWindow); + yield onRefreshed; +} + +function checkNoResultsPlaceholderHidden(computedView) { + info("Checking that the no results placeholder is hidden"); + + let placeholder = computedView.noResults; + let win = computedView.styleWindow; + let display = win.getComputedStyle(placeholder).display; + is(display, "none", "placeholder is hidden"); +} diff --git a/toolkit/devtools/styleinspector/test/browser_computedview_original-source-link.js b/toolkit/devtools/styleinspector/test/browser_computedview_original-source-link.js new file mode 100644 index 000000000..6ff2d5315 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_computedview_original-source-link.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"; + +// Test that the computed view shows the original source link when source maps +// are enabled + +const TESTCASE_URI = TEST_URL_ROOT_SSL + "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("Turning the pref " + PREF + " on"); + Services.prefs.setBoolPref(PREF, true); + + yield addTab(TESTCASE_URI); + let {toolbox, inspector, view} = yield openComputedView(); + + info("Select the test node"); + yield selectNode("div", inspector); + + info("Expanding the first property"); + yield expandComputedViewPropertyByIndex(view, 0); + + info("Verifying the link text"); + // Forcing a call to updateSourceLink on the SelectorView here. The + // computed-view already does it, but we have no way of waiting for it to be + // done here, so just call it again and wait for the returned promise to + // resolve. + let propertyView = getComputedViewPropertyView(view, "color"); + yield propertyView.matchedSelectorViews[0].updateSourceLink(); + verifyLinkText(view, SCSS_LOC); + + info("Toggling the pref"); + let onLinksUpdated = inspector.once("computed-view-sourcelinks-updated"); + Services.prefs.setBoolPref(PREF, false); + yield onLinksUpdated; + + info("Verifying that the link text has changed after the pref change"); + yield verifyLinkText(view, CSS_LOC); + + info("Toggling the pref again"); + onLinksUpdated = inspector.once("computed-view-sourcelinks-updated"); + Services.prefs.setBoolPref(PREF, true); + yield onLinksUpdated; + + info("Testing that clicking on the link works"); + yield testClickingLink(toolbox, view); + + info("Turning the pref " + PREF + " off"); + Services.prefs.clearUserPref(PREF); +}); + +function* testClickingLink(toolbox, view) { + let onEditor = waitForStyleEditor(toolbox, "doc_sourcemaps.scss"); + + info("Clicking the computedview stylesheet link"); + let link = getComputedViewLinkByIndex(view, 0); + link.scrollIntoView(); + link.click(); + + let editor = yield onEditor; + + let {line, col} = editor.sourceEditor.getCursor(); + is(line, 3, "cursor is at correct line number in original source"); +} + +function verifyLinkText(view, text) { + let link = getComputedViewLinkByIndex(view, 0); + is(link.textContent, text, "Linked text changed to display the correct location"); +} diff --git a/toolkit/devtools/styleinspector/test/browser_computedview_pseudo-element_01.js b/toolkit/devtools/styleinspector/test/browser_computedview_pseudo-element_01.js new file mode 100644 index 000000000..73051808f --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_computedview_pseudo-element_01.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 pseudoelements are displayed correctly in the rule view + +const TEST_URI = TEST_URL_ROOT + "doc_pseudoelement.html"; + +add_task(function*() { + yield addTab(TEST_URI); + let {toolbox, inspector, view} = yield openComputedView(); + + yield testTopLeft(inspector, view); +}); + +function* testTopLeft(inspector, view) { + let node = yield getNodeFront("#topleft", inspector.markup); + yield selectNode(node, inspector); + let float = getComputedViewPropertyValue(view, "float"); + is(float, "left", "The computed view shows the correct float"); + + let children = yield inspector.markup.walker.children(node); + is (children.nodes.length, 3, "Element has correct number of children"); + + let beforeElement = children.nodes[0]; + yield selectNode(beforeElement, inspector); + let top = getComputedViewPropertyValue(view, "top"); + is(top, "0px", "The computed view shows the correct top"); + let left = getComputedViewPropertyValue(view, "left"); + is(left, "0px", "The computed view shows the correct left"); + + let afterElement = children.nodes[children.nodes.length-1]; + yield selectNode(afterElement, inspector); + top = getComputedViewPropertyValue(view, "top"); + is(top, "50%", "The computed view shows the correct top"); + left = getComputedViewPropertyValue(view, "left"); + is(left, "50%", "The computed view shows the correct left"); +} + diff --git a/toolkit/devtools/styleinspector/test/browser_computedview_refresh-on-style-change_01.js b/toolkit/devtools/styleinspector/test/browser_computedview_refresh-on-style-change_01.js new file mode 100644 index 000000000..a550b6ba9 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_computedview_refresh-on-style-change_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 that the computed view refreshes when the current node has its style +// changed + +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 computed view and selecting the test node"); + let {toolbox, inspector, view} = yield openComputedView(); + yield selectNode("#testdiv", inspector); + + let fontSize = getComputedViewPropertyValue(view, "font-size"); + is(fontSize, "10px", "The computed view shows the right font-size"); + + info("Changing the node's style and waiting for the update"); + let onUpdated = inspector.once("computed-view-refreshed"); + getNode("#testdiv").style.cssText = "font-size: 15px; color: red;"; + yield onUpdated; + + fontSize = getComputedViewPropertyValue(view, "font-size"); + is(fontSize, "15px", "The computed view shows the updated font-size"); + let color = getComputedViewPropertyValue(view, "color"); + is(color, "#F00", "The computed view also shows the color now"); +}); diff --git a/toolkit/devtools/styleinspector/test/browser_computedview_search-filter.js b/toolkit/devtools/styleinspector/test/browser_computedview_search-filter.js new file mode 100644 index 000000000..f1447f159 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_computedview_search-filter.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 search filter works properly. + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,default styles test"); + + info("Creating the test document"); + content.document.body.innerHTML = '<style type="text/css"> ' + + '.matches {color: #F00;}</style>' + + '<span id="matches" class="matches">Some styled text</span>' + + '</div>'; + content.document.title = "Style Inspector Search Filter Test"; + + info("Opening the computed-view"); + let {toolbox, inspector, view} = yield openComputedView(); + + info("Selecting the test node"); + yield selectNode("#matches", inspector); + + yield testToggleDefaultStyles(inspector, view); + yield testAddTextInFilter(inspector, view); +}); + + +function* testToggleDefaultStyles(inspector, computedView) { + info("checking \"Browser styles\" checkbox"); + + let doc = computedView.styleDocument; + let checkbox = doc.querySelector(".includebrowserstyles"); + let onRefreshed = inspector.once("computed-view-refreshed"); + checkbox.click(); + yield onRefreshed; +} + +function* testAddTextInFilter(inspector, computedView) { + info("setting filter text to \"color\""); + + let doc = computedView.styleDocument; + let searchbar = doc.querySelector(".devtools-searchinput"); + let onRefreshed = inspector.once("computed-view-refreshed"); + searchbar.focus(); + + let win = computedView.styleWindow; + EventUtils.synthesizeKey("c", {}, win); + EventUtils.synthesizeKey("o", {}, win); + EventUtils.synthesizeKey("l", {}, win); + EventUtils.synthesizeKey("o", {}, win); + EventUtils.synthesizeKey("r", {}, win); + + yield onRefreshed; + + info("check that the correct properties are visible"); + + let propertyViews = computedView.propertyViews; + propertyViews.forEach(function(propView) { + let name = propView.name; + is(propView.visible, name.indexOf("color") > -1, + "span " + name + " property visibility check"); + }); +} diff --git a/toolkit/devtools/styleinspector/test/browser_computedview_select-and-copy-styles.js b/toolkit/devtools/styleinspector/test/browser_computedview_select-and-copy-styles.js new file mode 100644 index 000000000..356ea9de3 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_computedview_select-and-copy-styles.js @@ -0,0 +1,118 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that properties can be selected and copied from the computed view + +XPCOMUtils.defineLazyGetter(this, "osString", function() { + return Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS; +}); + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,computed view copy test"); + + info("Creating the test document"); + content.document.body.innerHTML = '<style type="text/css"> ' + + 'span { font-variant-caps: small-caps; color: #000000; } ' + + '.nomatches {color: #ff0000;}</style> <div id="first" style="margin: 10em; ' + + 'font-size: 14pt; font-family: helvetica, sans-serif; color: #AAA">\n' + + '<h1>Some header text</h1>\n' + + '<p id="salutation" style="font-size: 12pt">hi.</p>\n' + + '<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>\n' + + '<p id="closing">more text</p>\n' + + '<p>even more text</p>' + + '</div>'; + content.document.title = "Computed view context menu test"; + + info("Opening the computed view"); + let {toolbox, inspector, view} = yield openComputedView(); + + info("Selecting the test node"); + yield selectNode("span", inspector); + + yield checkCopySelection(view); + yield checkSelectAll(view); +}); + +function checkCopySelection(view) { + info("Testing selection copy"); + + let contentDocument = view.styleDocument; + let props = contentDocument.querySelectorAll(".property-view"); + ok(props, "captain, we have the property-view nodes"); + + let range = contentDocument.createRange(); + range.setStart(props[1], 0); + range.setEnd(props[3], 3); + contentDocument.defaultView.getSelection().addRange(range); + + info("Checking that cssHtmlTree.siBoundCopy() returns the correct clipboard value"); + + let expectedPattern = "font-family: helvetica,sans-serif;[\\r\\n]+" + + "font-size: 16px;[\\r\\n]+" + + "font-variant-caps: small-caps;[\\r\\n]*"; + + return waitForClipboard(() => { + fireCopyEvent(props[0]); + }, () => { + return checkClipboardData(expectedPattern); + }).then(() => {}, () => { + failedClipboard(expectedPattern); + }); +} + +function checkSelectAll(view) { + info("Testing select-all copy"); + + let contentDoc = view.styleDocument; + let prop = contentDoc.querySelector(".property-view"); + + info("Checking that _SelectAll() then copy returns the correct clipboard value"); + view._onSelectAll(); + let expectedPattern = "color: #FF0;[\\r\\n]+" + + "font-family: helvetica,sans-serif;[\\r\\n]+" + + "font-size: 16px;[\\r\\n]+" + + "font-variant-caps: small-caps;[\\r\\n]*"; + + return waitForClipboard(() => { + fireCopyEvent(prop); + }, () => { + return checkClipboardData(expectedPattern); + }).then(() => {}, () => { + 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/toolkit/devtools/styleinspector/test/browser_computedview_style-editor-link.js b/toolkit/devtools/styleinspector/test/browser_computedview_style-editor-link.js new file mode 100644 index 000000000..7f56cf233 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_computedview_style-editor-link.js @@ -0,0 +1,146 @@ +/* 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"; + +/////////////////// +// +// 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 computed view to the style editor + +const STYLESHEET_URL = "data:text/css,"+encodeURIComponent( + [".highlight {", + "color: blue", + "}"].join("\n")); + +const DOCUMENT_URL = "data:text/html;charset=utf-8,"+encodeURIComponent( + ['<html>' + + '<head>' + + '<title>Computed view style editor link test</title>', + '<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">', + '</style>', + '<style>', + 'div { color: #f06; }', + '</style>', + '<link rel="stylesheet" type="text/css" href="'+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>'].join("\n")); + +add_task(function*() { + yield addTab(DOCUMENT_URL); + + info("Opening the computed-view"); + let {toolbox, inspector, view} = yield openComputedView(); + + info("Selecting the test node"); + yield selectNode("span", inspector); + + yield testInlineStyle(view, inspector); + yield testFirstInlineStyleSheet(view, toolbox); + yield testSecondInlineStyleSheet(view, toolbox); + yield testExternalStyleSheet(view, toolbox); +}); + +function* testInlineStyle(view, inspector) { + info("Testing inline style"); + + yield expandComputedViewPropertyByIndex(view, 0); + + let onWindow = waitForWindow(); + info("Clicking on the first rule-link in the computed-view"); + clickLinkByIndex(view, 0); + + let win = yield onWindow; + + let windowType = win.document.documentElement.getAttribute("windowtype"); + is(windowType, "navigator:view-source", "View source window is open"); + info("Closing window"); + win.close(); +} + +function* testFirstInlineStyleSheet(view, toolbox) { + info("Testing inline stylesheet"); + + info("Listening for toolbox switch to the styleeditor"); + let onSwitch = waitForStyleEditor(toolbox); + + info("Clicking an inline stylesheet"); + clickLinkByIndex(view, 2); + let editor = yield onSwitch; + + ok(true, "Switched to the style-editor panel in the toolbox"); + + validateStyleEditorSheet(editor, 0); +} + +function* testSecondInlineStyleSheet(view, toolbox) { + 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"); + clickLinkByIndex(view, 4); + let editor = yield onSelected; + + is(toolbox.currentToolId, "styleeditor", "The style editor is selected again"); + validateStyleEditorSheet(editor, 1); +} + +function* testExternalStyleSheet(view, toolbox) { + 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"); + clickLinkByIndex(view, 1); + let editor = yield onSelected; + + is(toolbox.currentToolId, "styleeditor", "The style editor is selected again"); + validateStyleEditorSheet(editor, 2); +} + +function validateStyleEditorSheet(editor, expectedSheetIndex) { + info("Validating style editor stylesheet"); + let sheet = content.document.styleSheets[expectedSheetIndex]; + is(editor.styleSheet.href, sheet.href, "loaded stylesheet matches document stylesheet"); +} + +function clickLinkByIndex(view, index) { + let link = getComputedViewLinkByIndex(view, index); + link.scrollIntoView(); + link.click(); +} diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_add-property-and-reselect.js b/toolkit/devtools/styleinspector/test/browser_ruleview_add-property-and-reselect.js new file mode 100644 index 000000000..03a45e4c6 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_add-property-and-reselect.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 that adding properties to rules work and reselecting the element still +// show them + +const TEST_URI = TEST_URL_ROOT + "doc_content_stylesheet.html"; + +add_task(function*() { + yield addTab(TEST_URI); + + let target = getNode("#target"); + + let {toolbox, inspector, view} = yield openRuleView(); + yield selectNode("#target", inspector); + + info("Setting a font-weight property on all rules"); + setPropertyOnAllRules(view); + + info("Reselecting the element"); + yield selectNode("body", inspector); + yield selectNode("#target", inspector); + + checkPropertyOnAllRules(view); +}); + +function setPropertyOnAllRules(view) { + for (let rule of view._elementStyle.rules) { + rule.editor.addProperty("font-weight", "bold", ""); + } +} + +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/toolkit/devtools/styleinspector/test/browser_ruleview_add-property-cancel_01.js b/toolkit/devtools/styleinspector/test/browser_ruleview_add-property-cancel_01.js new file mode 100644 index 000000000..215c1e9c8 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_add-property-cancel_01.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"; + +// Testing various inplace-editor behaviors in the rule-view + +let TEST_URL = 'url("' + TEST_URL_ROOT + 'doc_test_image.png")'; +let PAGE_CONTENT = [ + '<style type="text/css">', + ' #testid {', + ' background-color: blue;', + ' }', + ' .testclass {', + ' background-color: green;', + ' }', + '</style>', + '<div id="testid" class="testclass">Styled Node</div>' +].join("\n"); + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,test rule view user changes"); + + info("Creating the test document"); + content.document.body.innerHTML = PAGE_CONTENT; + + info("Opening the rule-view"); + let {toolbox, inspector, view} = yield openRuleView(); + + info("Selecting the test element"); + yield selectNode("#testid", inspector); + + yield testCancelNew(view); +}); + +function* testCancelNew(view) { + info("Test adding a new rule to the element's style declaration and leaving it empty."); + + let elementRuleEditor = getRuleViewRuleEditor(view, 0); + + info("Focusing a new property name in the rule-view"); + let editor = yield focusEditableField(elementRuleEditor.closeBrace); + is(inplaceEditor(elementRuleEditor.newPropSpan), editor, "The new property editor got focused"); + + info("Bluring the editor input"); + let onBlur = once(editor.input, "blur"); + editor.input.blur(); + yield onBlur; + + info("Checking the state of canceling a new property name editor"); + ok(!elementRuleEditor.rule._applyingModifications, "Shouldn't have an outstanding request after a cancel."); + is(elementRuleEditor.rule.textProps.length, 0, "Should have canceled creating a new text property."); + ok(!elementRuleEditor.propertyList.hasChildNodes(), "Should not have any properties."); +} diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_add-property-cancel_02.js b/toolkit/devtools/styleinspector/test/browser_ruleview_add-property-cancel_02.js new file mode 100644 index 000000000..6346284c7 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_add-property-cancel_02.js @@ -0,0 +1,79 @@ +/* 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 various inplace-editor behaviors in the rule-view + +let TEST_URL = 'url("' + TEST_URL_ROOT + 'doc_test_image.png")'; +let PAGE_CONTENT = [ + '<style type="text/css">', + ' #testid {', + ' background-color: blue;', + ' }', + '</style>', + '<div id="testid">Styled Node</div>' +].join("\n"); + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,test rule view user changes"); + + info("Creating the test document"); + content.document.body.innerHTML = PAGE_CONTENT; + + info("Opening the rule-view"); + let {toolbox, inspector, view} = yield openRuleView(); + + info("Selecting the test element"); + yield selectNode("#testid", inspector); + + info("Test creating a new property and escaping"); + + let elementRuleEditor = getRuleViewRuleEditor(view, 1); + + info("Focusing a new property name in the rule-view"); + let editor = yield focusEditableField(elementRuleEditor.closeBrace); + + is(inplaceEditor(elementRuleEditor.newPropSpan), editor, "The new property editor got focused."); + let input = editor.input; + + info("Entering a value in the property name editor"); + let onModifications = elementRuleEditor.rule._applyingModifications; + input.value = "color"; + yield onModifications; + + info("Pressing return to commit and focus the new value field"); + let onValueFocus = once(elementRuleEditor.element, "focus", true); + onModifications = elementRuleEditor.rule._applyingModifications; + EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView); + yield onValueFocus; + yield onModifications; + + // Getting the new value editor after focus + editor = inplaceEditor(view.doc.activeElement); + let textProp = elementRuleEditor.rule.textProps[1]; + + is(elementRuleEditor.rule.textProps.length, 2, "Created a new text property."); + is(elementRuleEditor.propertyList.children.length, 2, "Created a property editor."); + is(editor, inplaceEditor(textProp.editor.valueSpan), "Editing the value span now."); + + info("Entering a property value"); + editor.input.value = "red"; + + info("Escaping out of the field"); + onModifications = elementRuleEditor.rule._applyingModifications; + EventUtils.synthesizeKey("VK_ESCAPE", {}, view.doc.defaultView); + yield onModifications; + + info("Checking that the previous field is focused"); + let focusedElement = inplaceEditor(elementRuleEditor.rule.textProps[0].editor.valueSpan).input; + is(focusedElement, focusedElement.ownerDocument.activeElement, "Correct element has focus"); + + onModifications = elementRuleEditor.rule._applyingModifications; + EventUtils.synthesizeKey("VK_ESCAPE", {}, view.doc.defaultView); + yield onModifications; + + is(elementRuleEditor.rule.textProps.length, 1, "Removed the new text property."); + is(elementRuleEditor.propertyList.children.length, 1, "Removed the property editor."); +}); diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_add-property-cancel_03.js b/toolkit/devtools/styleinspector/test/browser_ruleview_add-property-cancel_03.js new file mode 100644 index 000000000..aafd54b46 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_add-property-cancel_03.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 cancelling the addition of a new property in the rule-view + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,browser_ruleview_ui.js"); + let {toolbox, inspector, view} = yield openRuleView(); + + info("Creating the test document"); + let style = "" + + "#testid {" + + " background-color: blue;" + + "}" + + ".testclass, .unmatched {" + + " background-color: green;" + + "}"; + let styleNode = addStyle(content.document, style); + content.document.body.innerHTML = "<div id='testid' class='testclass'>Styled Node</div>" + + "<div id='testid2'>Styled Node</div>"; + + yield testCancelNew(inspector, view); + yield testCancelNewOnEscape(inspector, view); + yield inspector.once("inspector-updated"); +}); + +function* testCancelNew(inspector, ruleView) { + // Start at the beginning: start to add a rule to the element's style + // declaration, but leave it empty. + + let elementRuleEditor = getRuleViewRuleEditor(ruleView, 0); + let editor = yield focusEditableField(elementRuleEditor.closeBrace); + + is(inplaceEditor(elementRuleEditor.newPropSpan), editor, + "Property editor is focused"); + + let onBlur = once(editor.input, "blur"); + editor.input.blur(); + yield onBlur; + + ok(!elementRuleEditor.rule._applyingModifications, "Shouldn't have an outstanding modification request after a cancel."); + is(elementRuleEditor.rule.textProps.length, 0, "Should have canceled creating a new text property."); + ok(!elementRuleEditor.propertyList.hasChildNodes(), "Should not have any properties."); +} + +function* testCancelNewOnEscape(inspector, ruleView) { + // Start at the beginning: start to add a rule to the element's style + // declaration, add some text, then press escape. + + let elementRuleEditor = getRuleViewRuleEditor(ruleView, 0); + let editor = yield focusEditableField(elementRuleEditor.closeBrace); + + is(inplaceEditor(elementRuleEditor.newPropSpan), editor, "Next focused editor should be the new property editor."); + for (let ch of "background") { + EventUtils.sendChar(ch, ruleView.doc.defaultView); + } + + let onBlur = once(editor.input, "blur"); + EventUtils.synthesizeKey("VK_ESCAPE", {}); + yield onBlur; + + ok(!elementRuleEditor.rule._applyingModifications, "Shouldn't have an outstanding modification request after a cancel."); + is(elementRuleEditor.rule.textProps.length, 0, "Should have canceled creating a new text property."); + ok(!elementRuleEditor.propertyList.hasChildNodes(), "Should not have any properties."); +} diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_add-property_01.js b/toolkit/devtools/styleinspector/test/browser_ruleview_add-property_01.js new file mode 100644 index 000000000..a7f2a6d9a --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_add-property_01.js @@ -0,0 +1,79 @@ +/* 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 various inplace-editor behaviors in the rule-view +// FIXME: To be split in several test files, and some of the inplace-editor +// focus/blur/commit/revert stuff should be factored out in head.js + +let TEST_URL = 'url("' + TEST_URL_ROOT + 'doc_test_image.png")'; +let PAGE_CONTENT = [ + '<style type="text/css">', + ' #testid {', + ' background-color: blue;', + ' }', + ' .testclass {', + ' background-color: green;', + ' }', + '</style>', + '<div id="testid" class="testclass">Styled Node</div>' +].join("\n"); + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,test rule view user changes"); + + info("Creating the test document"); + content.document.body.innerHTML = PAGE_CONTENT; + + info("Opening the rule-view"); + let {toolbox, inspector, view} = yield openRuleView(); + + info("Selecting the test element"); + yield selectNode("#testid", inspector); + + yield testCreateNew(view); +}); + +function* testCreateNew(view) { + info("Test creating a new property"); + + let elementRuleEditor = getRuleViewRuleEditor(view, 0); + + info("Focusing a new property name in the rule-view"); + let editor = yield focusEditableField(elementRuleEditor.closeBrace); + + is(inplaceEditor(elementRuleEditor.newPropSpan), editor, "The new property editor got focused"); + let input = editor.input; + + info("Entering background-color in the property name editor"); + input.value = "background-color"; + + info("Pressing return to commit and focus the new value field"); + let onValueFocus = once(elementRuleEditor.element, "focus", true); + let onModifications = elementRuleEditor.rule._applyingModifications; + EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView); + yield onValueFocus; + yield onModifications; + + // Getting the new value editor after focus + editor = inplaceEditor(view.doc.activeElement); + let textProp = elementRuleEditor.rule.textProps[0]; + + is(elementRuleEditor.rule.textProps.length, 1, "Created a new text property."); + is(elementRuleEditor.propertyList.children.length, 1, "Created a property editor."); + is(editor, inplaceEditor(textProp.editor.valueSpan), "Editing the value span now."); + + info("Entering a value and bluring the field to expect a rule change"); + editor.input.value = "#XYZ"; + let onBlur = once(editor.input, "blur"); + onModifications = elementRuleEditor.rule._applyingModifications; + editor.input.blur(); + yield onBlur; + yield onModifications; + + is(textProp.value, "#XYZ", "Text prop should have been changed."); + is(textProp.overridden, false, "Property should not be overridden"); + is(textProp.editor.isValid(), false, "#XYZ should not be a valid entry"); +} diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_add-property_02.js b/toolkit/devtools/styleinspector/test/browser_ruleview_add-property_02.js new file mode 100644 index 000000000..a8beb0fff --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_add-property_02.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"; + +// Test all sorts of additions and updates of properties in the rule-view +// FIXME: TO BE SPLIT IN *MANY* SMALLER TESTS + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,browser_ruleview_ui.js"); + let {toolbox, inspector, view} = yield openRuleView(); + + info("Creating the test document"); + let style = "" + + "#testid {" + + " background-color: blue;" + + "}" + + ".testclass, .unmatched {" + + " background-color: green;" + + "}"; + let styleNode = addStyle(content.document, style); + content.document.body.innerHTML = "<div id='testid' class='testclass'>Styled Node</div>" + + "<div id='testid2'>Styled Node</div>"; + + yield testCreateNew(inspector, view); + yield inspector.once("inspector-updated"); +}); + +function* testCreateNew(inspector, ruleView) { + // Create a new property. + let elementRuleEditor = getRuleViewRuleEditor(ruleView, 0); + let editor = yield focusEditableField(elementRuleEditor.closeBrace); + + is(inplaceEditor(elementRuleEditor.newPropSpan), editor, + "Next focused editor should be the new property editor."); + + let input = editor.input; + + 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.doc.defaultView); + input.select(); + + info("Entering the property name"); + input.value = "background-color"; + + info("Pressing RETURN and waiting for the value field focus"); + let onFocus = once(elementRuleEditor.element, "focus", true); + EventUtils.sendKey("return", ruleView.doc.defaultView); + yield onFocus; + yield elementRuleEditor.rule._applyingModifications; + + editor = inplaceEditor(ruleView.doc.activeElement); + + is(elementRuleEditor.rule.textProps.length, 1, "Should have created a new text property."); + is(elementRuleEditor.propertyList.children.length, 1, "Should have created a property editor."); + let textProp = elementRuleEditor.rule.textProps[0]; + is(editor, inplaceEditor(textProp.editor.valueSpan), "Should be editing the value span now."); + + editor.input.value = "purple"; + let onBlur = once(editor.input, "blur"); + EventUtils.sendKey("return", ruleView.doc.defaultView); + yield onBlur; + yield elementRuleEditor.rule._applyingModifications; + + is(textProp.value, "purple", "Text prop should have been changed."); +} diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_add-rule_01.js b/toolkit/devtools/styleinspector/test/browser_ruleview_add-rule_01.js new file mode 100644 index 000000000..51885ac37 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_add-rule_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 the behaviour of adding a new rule to the rule view and the +// various inplace-editor behaviours in the new rule editor + +let PAGE_CONTENT = [ + '<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>', + '<p>Empty<p>' +].join("\n"); + +const TEST_DATA = [ + { node: "#testid", expected: "#testid" }, + { node: ".testclass2", expected: ".testclass2" }, + { node: ".class1.class2", expected: ".class1" }, + { node: "p", expected: "p" } +]; + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,test rule view add rule"); + + info("Creating the test document"); + content.document.body.innerHTML = PAGE_CONTENT; + + info("Opening the rule-view"); + let {toolbox, inspector, view} = yield openRuleView(); + + info("Iterating over the test data"); + for (let data of TEST_DATA) { + yield runTestData(inspector, view, data); + } +}); + +function* runTestData(inspector, view, data) { + let {node, expected} = data; + info("Selecting the test element"); + yield selectNode(node, inspector); + + info("Waiting for context menu to be shown"); + let onPopup = once(view._contextmenu, "popupshown"); + let win = view.doc.defaultView; + + EventUtils.synthesizeMouseAtCenter(view.element, + {button: 2, type: "contextmenu"}, win); + yield onPopup; + + ok(!view.menuitemAddRule.hidden, "Add rule is visible"); + + info("Waiting for rule view to change"); + let onRuleViewChanged = once(view.element, "CssRuleViewChanged"); + + info("Adding the new rule"); + view.menuitemAddRule.click(); + yield onRuleViewChanged; + view._contextmenu.hidePopup(); + + yield testNewRule(view, expected, 1); + + info("Resetting page content"); + content.document.body.innerHTML = PAGE_CONTENT; +} + +function* testNewRule(view, expected, index) { + let idRuleEditor = getRuleViewRuleEditor(view, index); + let editor = idRuleEditor.selectorText.ownerDocument.activeElement; + is(editor.value, expected, + "Selector editor value is as expected: " + expected); + + info("Entering the escape key"); + EventUtils.synthesizeKey("VK_ESCAPE", {}); + + is(idRuleEditor.selectorText.textContent, expected, + "Selector text value is as expected: " + expected); + + info("Adding new properties to new rule: " + expected) + idRuleEditor.addProperty("font-weight", "bold", ""); + 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/toolkit/devtools/styleinspector/test/browser_ruleview_add-rule_02.js b/toolkit/devtools/styleinspector/test/browser_ruleview_add-rule_02.js new file mode 100644 index 000000000..11db0e265 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_add-rule_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"; + +// Tests the behaviour of adding a new rule to the rule view and editing +// its selector + +let PAGE_CONTENT = [ + '<style type="text/css">', + ' #testid {', + ' text-align: center;', + ' }', + '</style>', + '<div id="testid">Styled Node</div>', + '<span>This is a span</span>' +].join("\n"); + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,test rule view add rule"); + + info("Creating the test document"); + content.document.body.innerHTML = PAGE_CONTENT; + + info("Opening the rule-view"); + let {toolbox, inspector, view} = yield openRuleView(); + + info("Selecting the test element"); + yield selectNode("#testid", inspector); + + info("Waiting for context menu to be shown"); + let onPopup = once(view._contextmenu, "popupshown"); + let win = view.doc.defaultView; + + EventUtils.synthesizeMouseAtCenter(view.element, + {button: 2, type: "contextmenu"}, win); + yield onPopup; + + ok(!view.menuitemAddRule.hidden, "Add rule is visible"); + + info("Waiting for rule view to change"); + let onRuleViewChanged = once(view.element, "CssRuleViewChanged"); + + info("Adding the new rule"); + view.menuitemAddRule.click(); + yield onRuleViewChanged; + view._contextmenu.hidePopup(); + + yield testEditSelector(view, "span"); + + info("Selecting the modified element"); + 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 refresh"); + let onRuleViewRefresh = once(view.element, "CssRuleViewRefreshed"); + + info("Entering the commit key"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewRefresh; + + is(view._elementStyle.rules.length, 2, "Should have 2 rules."); +} + +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/toolkit/devtools/styleinspector/test/browser_ruleview_add-rule_03.js b/toolkit/devtools/styleinspector/test/browser_ruleview_add-rule_03.js new file mode 100644 index 000000000..a67bd4ddb --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_add-rule_03.js @@ -0,0 +1,114 @@ +/* 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 + +let PAGE_CONTENT = [ + '<style type="text/css">', + ' #testid {', + ' text-align: center;', + ' }', + '</style>', + '<div id="testid">Styled Node</div>', + '<span>This is a span</span>' +].join("\n"); + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,test rule view add rule"); + + info("Creating the test document"); + content.document.body.innerHTML = PAGE_CONTENT; + + info("Opening the rule-view"); + let {toolbox, inspector, view} = yield openRuleView(); + + info("Selecting the test element"); + yield selectNode("#testid", inspector); + + info("Waiting for context menu to be shown"); + let onPopup = once(view._contextmenu, "popupshown"); + let win = view.doc.defaultView; + + EventUtils.synthesizeMouseAtCenter(view.element, + {button: 2, type: "contextmenu"}, win); + yield onPopup; + + ok(!view.menuitemAddRule.hidden, "Add rule is visible"); + + info("Waiting for rule view to change"); + let onRuleViewChanged = once(view.element, "CssRuleViewChanged"); + + info("Adding the new rule"); + view.menuitemAddRule.click(); + yield onRuleViewChanged; + view._contextmenu.hidePopup(); + + info("Adding new properties to the new rule"); + yield testNewRule(view, "#testid", 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* testNewRule(view, expected, index) { + let idRuleEditor = getRuleViewRuleEditor(view, index); + let editor = idRuleEditor.selectorText.ownerDocument.activeElement; + is(editor.value, expected, + "Selector editor value is as expected: " + expected); + + info("Entering the escape key"); + EventUtils.synthesizeKey("VK_ESCAPE", {}); + + is(idRuleEditor.selectorText.textContent, expected, + "Selector text value is as expected: " + expected); + + info("Adding new properties to new rule: " + expected) + idRuleEditor.addProperty("font-weight", "bold", ""); + 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"); +} + +function* testEditSelector(view, name) { + let idRuleEditor = getRuleViewRuleEditor(view, 1); + + info("Focusing an existing selector name in the rule-view"); + let editor = yield focusEditableField(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 refresh"); + let onRuleViewRefresh = once(view.element, "CssRuleViewRefreshed"); + + info("Entering the commit key"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewRefresh; + + is(view._elementStyle.rules.length, 2, "Should have 2 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/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-and-image-tooltip_01.js b/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-and-image-tooltip_01.js new file mode 100644 index 000000000..0d30f0246 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-and-image-tooltip_01.js @@ -0,0 +1,60 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that after a color change, the image preview tooltip in the same +// property is displayed and positioned correctly. +// See bug 979292 + +const PAGE_CONTENT = [ + '<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!' +].join("\n"); + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,rule view color picker tooltip test"); + content.document.body.innerHTML = PAGE_CONTENT; + let {toolbox, inspector, view} = yield openRuleView(); + + let value = getRuleViewProperty(view, "body", "background").valueSpan; + let swatch = value.querySelectorAll(".ruleview-colorswatch")[1]; + 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 onShown = picker.tooltip.once("shown"); + swatch.click(); + yield onShown; + yield simulateColorPickerChange(picker, [0, 0, 0, 1], { + element: content.document.body, + name: "backgroundImage", + value: 'url("chrome://global/skin/icons/warning-64.png"), linear-gradient(rgb(0, 0, 0), rgb(255, 0, 102) 400px)' + }); + + let spectrum = yield picker.spectrum; + let onHidden = picker.tooltip.once("hidden"); + EventUtils.sendKey("RETURN", spectrum.element.ownerDocument.defaultView); + yield onHidden; + + 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/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-and-image-tooltip_02.js b/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-and-image-tooltip_02.js new file mode 100644 index 000000000..62b51e52e --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-and-image-tooltip_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/ */ + +"use strict"; + +// Test that after a color change, opening another tooltip, like the image +// preview doesn't revert the color change in the ruleView. +// This used to happen when the activeSwatch wasn't reset when the colorpicker +// would hide. +// See bug 979292 + +const PAGE_CONTENT = [ + '<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!' +].join("\n"); + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,rule view color picker tooltip test"); + content.document.body.innerHTML = PAGE_CONTENT; + let {toolbox, inspector, 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 onShown = picker.tooltip.once("shown"); + swatch.click(); + yield onShown; + + yield simulateColorPickerChange(picker, [0, 0, 0, 1], { + element: content.document.body, + name: "backgroundColor", + value: "rgb(0, 0, 0)" + }); + let spectrum = yield picker.spectrum; + let onHidden = picker.tooltip.once("hidden"); + EventUtils.sendKey("RETURN", spectrum.element.ownerDocument.defaultView); + yield onHidden; + + info("Open the image preview tooltip"); + let value = getRuleViewProperty(ruleView, "body", "background").valueSpan; + let url = value.querySelector(".theme-link"); + 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, "rgb(0, 0, 0)", "The swatch's color is correct"); + is(swatch.nextSibling.textContent, "#000", "The color name is correct"); +} diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-appears-on-swatch-click.js b/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-appears-on-swatch-click.js new file mode 100644 index 000000000..806b35f09 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-appears-on-swatch-click.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 color pickers appear when clicking on color swatches + +const PAGE_CONTENT = [ + '<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!' +].join("\n"); + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,rule view color picker tooltip test"); + content.document.body.innerHTML = PAGE_CONTENT; + let {toolbox, inspector, view} = yield openRuleView(); + + let cSwatch = getRuleViewProperty(view, "body", "color").valueSpan + .querySelector(".ruleview-colorswatch"); + let bgSwatch = getRuleViewProperty(view, "body", "background-color").valueSpan + .querySelector(".ruleview-colorswatch"); + let bSwatch = getRuleViewProperty(view, "body", "border").valueSpan + .querySelector(".ruleview-colorswatch"); + + for (let swatch of [cSwatch, bgSwatch, bSwatch]) { + info("Testing that the colorpicker appears colorswatch click"); + 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 onShown = cPicker.tooltip.once("shown"); + swatch.click(); + yield onShown; + + 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"); + cPicker.hide(); +} diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-commit-on-ENTER.js b/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-commit-on-ENTER.js new file mode 100644 index 000000000..f866714e3 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-commit-on-ENTER.js @@ -0,0 +1,59 @@ +/* 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 color change in the color picker is committed when ENTER is pressed + +const PAGE_CONTENT = [ + '<style type="text/css">', + ' body {', + ' border: 2em solid rgba(120, 120, 120, .5);', + ' }', + '</style>', + 'Testing the color picker tooltip!' +].join("\n"); + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,rule view color picker tooltip test"); + content.document.body.innerHTML = PAGE_CONTENT; + let {toolbox, inspector, 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 onShown = cPicker.tooltip.once("shown"); + swatch.click(); + yield onShown; + + yield simulateColorPickerChange(cPicker, [0, 255, 0, .5], { + element: content.document.body, + name: "borderLeftColor", + 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 spectrum = yield cPicker.spectrum; + let onHidden = cPicker.tooltip.once("hidden"); + EventUtils.sendKey("RETURN", spectrum.element.ownerDocument.defaultView); + yield onHidden; + + is(content.getComputedStyle(content.document.body).borderLeftColor, + "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/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-edit-gradient.js b/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-edit-gradient.js new file mode 100644 index 000000000..7046369a6 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-edit-gradient.js @@ -0,0 +1,75 @@ +/* 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 a color in a gradient css declaration using the tooltip +// color picker works + +const PAGE_CONTENT = [ + '<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' +].join("\n"); + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,rule view color picker tooltip test"); + + info("Creating the test document"); + content.document.body.innerHTML = PAGE_CONTENT; + + info("Opening the rule-view") + let {toolbox, inspector, 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("Getting the color picker tooltip and clicking on the swatch to show it"); + let cPicker = view.tooltips.colorPicker; + let onShown = cPicker.tooltip.once("shown"); + swatchEl.click(); + yield onShown; + + yield simulateColorPickerChange(cPicker, [1, 1, 1, 1]); + + 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(content.getComputedStyle(content.document.body).backgroundImage, + "linear-gradient(to left, rgb(255, 0, 102) 25%, rgb(51, 51, 51) 95%, rgb(0, 0, 0) 100%)", + "The gradient has been updated correctly"); + + cPicker.hide(); +} diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-hides-on-tooltip.js b/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-hides-on-tooltip.js new file mode 100644 index 000000000..369378082 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-hides-on-tooltip.js @@ -0,0 +1,48 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the color picker tooltip hides when an image tooltip appears + +const PAGE_CONTENT = [ + '<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!' +].join("\n"); + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,rule view color picker tooltip test"); + content.document.body.innerHTML = PAGE_CONTENT; + let {toolbox, inspector, view} = yield openRuleView(); + + let swatch = getRuleViewProperty(view, "body", "color").valueSpan + .querySelector(".ruleview-colorswatch"); + + yield testColorPickerHidesWhenImageTooltipAppears(view, swatch); +}); + +function* testColorPickerHidesWhenImageTooltipAppears(view, swatch) { + let bgImageSpan = getRuleViewProperty(view, "body", "background-image").valueSpan; + let uriSpan = bgImageSpan.querySelector(".theme-link"); + let tooltip = view.tooltips.colorPicker.tooltip; + + info("Showing the color picker tooltip by clicking on the color swatch"); + let onShown = tooltip.once("shown"); + swatch.click(); + yield onShown; + + info("Now showing the image preview tooltip to hide the color picker"); + let onHidden = tooltip.once("hidden"); + yield assertHoverTooltipOn(view.tooltips.previewTooltip, uriSpan); + yield onHidden; + + ok(true, "The color picker closed when the image preview tooltip appeared"); +} diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-multiple-changes.js b/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-multiple-changes.js new file mode 100644 index 000000000..24bef27ed --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-multiple-changes.js @@ -0,0 +1,126 @@ +/* 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 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 PAGE_CONTENT = [ + '<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>' +].join("\n"); + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,rule view color picker tooltip test"); + content.document.body.innerHTML = PAGE_CONTENT; + let {toolbox, 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 onShown = picker.tooltip.once("shown"); + swatch.click(); + yield onShown; + + 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(picker, rgba, { + element: content.document.querySelector("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 onShown = picker.tooltip.once("shown"); + swatch.click(); + yield onShown; + + 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(picker, rgba, { + element: content.document.body, + name: "backgroundColor", + value: computed + }); + } + + info("Closing the color picker"); + let onHidden = picker.tooltip.once("hidden"); + picker.tooltip.hide(); + yield onHidden; +} + +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 onShown = picker.tooltip.once("shown"); + swatch.click(); + yield onShown; + + 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(picker, rgba, { + element: content.document.body, + name: "color", + value: computed + }); + is(content.getComputedStyle(content.document.querySelector("p")).color, + "rgb(200, 200, 200)", "The color of the P tag is still correct"); + } +} diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-revert-on-ESC.js b/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-revert-on-ESC.js new file mode 100644 index 000000000..f2b4e6c2a --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-revert-on-ESC.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"; + +// Test that a color change in the color picker is reverted when ESC is pressed + +const PAGE_CONTENT = [ + '<style type="text/css">', + ' body {', + ' background-color: #ededed;', + ' }', + '</style>', + 'Testing the color picker tooltip!' +].join("\n"); + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,rule view color picker tooltip test"); + content.document.body.innerHTML = PAGE_CONTENT; + let {toolbox, inspector, view} = yield openRuleView(); + + let swatch = getRuleViewProperty(view, "body", "background-color").valueSpan + .querySelector(".ruleview-colorswatch"); + yield testPressingEscapeRevertsChanges(swatch, view); +}); + +function* testPressingEscapeRevertsChanges(swatch, ruleView) { + let cPicker = ruleView.tooltips.colorPicker; + + let onShown = cPicker.tooltip.once("shown"); + swatch.click(); + yield onShown; + + yield simulateColorPickerChange(cPicker, [0, 0, 0, 1], { + element: content.document.body, + name: "backgroundColor", + value: "rgb(0, 0, 0)" + }); + + is(swatch.style.backgroundColor, "rgb(0, 0, 0)", + "The color swatch's background was updated"); + is(getRuleViewProperty(ruleView, "body", "background-color").valueSpan.textContent, + "#000", "The text of the background-color css property was updated"); + + let spectrum = yield cPicker.spectrum; + + // ESC out of the color picker + let onHidden = cPicker.tooltip.once("hidden"); + EventUtils.sendKey("ESCAPE", spectrum.element.ownerDocument.defaultView); + yield onHidden; + + yield waitForSuccess(() => { + return content.getComputedStyle(content.document.body).backgroundColor === "rgb(237, 237, 237)"; + }, "The element's background-color was reverted"); +} diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-swatch-displayed.js b/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-swatch-displayed.js new file mode 100644 index 000000000..56b94a463 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_colorpicker-swatch-displayed.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"; + +// Test that color swatches are displayed next to colors in the rule-view + +const PAGE_CONTENT = [ + '<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;', + ' box-shadow: inset 0 0 2px 20px red, inset 0 0 2px 40px blue;', + ' }', + '</style>', + 'Testing the color picker tooltip!' +].join("\n"); + +// 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: "box-shadow", nb: 2}, +]; + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,rule view color picker tooltip test"); + content.document.body.innerHTML = PAGE_CONTENT; + let {toolbox, inspector, 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/toolkit/devtools/styleinspector/test/browser_ruleview_completion-existing-property_01.js b/toolkit/devtools/styleinspector/test/browser_ruleview_completion-existing-property_01.js new file mode 100644 index 000000000..f8c7aa715 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_completion-existing-property_01.js @@ -0,0 +1,104 @@ +/* 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 CSS property names are autocompleted and cycled correctly when +// editing an existing property in the rule view + +const MAX_ENTRIES = 10; + +// format : +// [ +// what key to press, +// expected input box value after keypress, +// selectedIndex of the popup, +// total items in the popup +// ] +let testData = [ + ["VK_RIGHT", "font", -1, 0], + ["-","font-family", 0, MAX_ENTRIES], + ["f","font-family", 0, 2], + ["VK_BACK_SPACE", "font-f", -1, 0], + ["VK_BACK_SPACE", "font-", -1, 0], + ["VK_BACK_SPACE", "font", -1, 0], + ["VK_BACK_SPACE", "fon", -1, 0], + ["VK_BACK_SPACE", "fo", -1, 0], + ["VK_BACK_SPACE", "f", -1, 0], + ["VK_BACK_SPACE", "", -1, 0], + ["d", "direction", 0, 3], + ["VK_DOWN", "display", 1, 3], + ["VK_DOWN", "dominant-baseline", 2, 3], + ["VK_DOWN", "direction", 0, 3], + ["VK_DOWN", "display", 1, 3], + ["VK_UP", "direction", 0, 3], + ["VK_UP", "dominant-baseline", 2, 3], + ["VK_UP", "display", 1, 3], + ["VK_BACK_SPACE", "d", -1, 0], + ["i", "direction", 0, 2], + ["s", "display", -1, 0], + ["VK_BACK_SPACE", "dis", -1, 0], + ["VK_BACK_SPACE", "di", -1, 0], + ["VK_BACK_SPACE", "d", -1, 0], + ["VK_BACK_SPACE", "", -1, 0], + ["f", "fill", 0, MAX_ENTRIES], + ["i", "fill", 0, 4], + ["VK_LEFT", "fill", -1, 0], + ["VK_LEFT", "fill", -1, 0], + ["i", "fiill", -1, 0], + ["VK_ESCAPE", null, -1, 0], +]; + +let TEST_URL = "data:text/html;charset=utf-8,<h1 style='font: 24px serif'>Filename" + + ": browser_bug893965_css_property_completion_existing_property.js</h1>"; + +add_task(function*() { + yield addTab(TEST_URL); + let {toolbox, inspector, view} = yield openRuleView(); + + info("Selecting the test node"); + yield selectNode("h1", inspector); + + info("Focusing the css property editable field"); + let propertyName = view.doc.querySelectorAll(".ruleview-propertyname")[0]; + let editor = yield focusEditableField(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, index, total], editor, view) { + info("Pressing key " + key); + info("Expecting " + completion + ", " + index + ", " + total); + + let onSuggest; + + if (/(left|right|back_space|escape)/ig.test(key)) { + info("Adding event listener for left|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"); + } + + info("Synthesizing key " + key); + EventUtils.synthesizeKey(key, {}, view.doc.defaultView); + + yield onSuggest; + yield wait(1); // Equivalent of executeSoon + + info("Checking the state"); + if (completion != null) { + is(editor.input.value, completion, "Correct value is autocompleted"); + } + if (total == 0) { + ok(!(editor.popup && editor.popup.isOpen), "Popup is closed"); + } else { + ok(editor.popup._panel.state == "open" || editor.popup._panel.state == "showing", "Popup is open"); + is(editor.popup.getItems().length, total, "Number of suggestions match"); + is(editor.popup.selectedIndex, index, "Correct item is selected"); + } +} diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_completion-existing-property_02.js b/toolkit/devtools/styleinspector/test/browser_ruleview_completion-existing-property_02.js new file mode 100644 index 000000000..439d51c69 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_completion-existing-property_02.js @@ -0,0 +1,101 @@ +/* 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 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, +// selectedIndex of the popup, +// total items in the popup +// ] +let testData = [ + ["b", {}, "beige", 0, 8], + ["l", {}, "black", 0, 4], + ["VK_DOWN", {}, "blanchedalmond", 1, 4], + ["VK_DOWN", {}, "blue", 2, 4], + ["VK_RIGHT", {}, "blue", -1, 0], + [" ", {}, "blue !important", 0, 10], + ["!", {}, "blue !important", 0, 0], + ["VK_BACK_SPACE", {}, "blue !", -1, 0], + ["VK_BACK_SPACE", {}, "blue ", -1, 0], + ["VK_BACK_SPACE", {}, "blue", -1, 0], + ["VK_TAB", {shiftKey: true}, "color", -1, 0], + ["VK_BACK_SPACE", {}, "", -1, 0], + ["d", {}, "direction", 0, 3], + ["i", {}, "direction", 0, 2], + ["s", {}, "display", -1, 0], + ["VK_TAB", {}, "blue", -1, 0], + ["n", {}, "none", -1, 0], + ["VK_RETURN", {}, null, -1, 0] +]; + +let TEST_URL = "data:text/html;charset=utf-8,<h1 style='color: red'>Filename: " + + "browser_bug894376_css_value_completion_existing_property_value_pair.js</h1>"; + +add_task(function*() { + yield addTab(TEST_URL); + let {toolbox, inspector, view} = yield openRuleView(); + + info("Selecting the test node"); + yield selectNode("h1", inspector); + + info("Focusing the css property editable value"); + let value = view.doc.querySelectorAll(".ruleview-propertyvalue")[0]; + let editor = yield focusEditableField(value); + + 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.doc.activeElement); + yield testCompletion(testData[i], editor, view); + } +}); + +function* testCompletion([key, modifiers, completion, index, total], editor, view) { + info("Pressing key " + key); + info("Expecting " + completion + ", " + index + ", " + total); + + let onKeyPress; + + if (/tab/ig.test(key)) { + info("Waiting for the new property or value editor to get focused"); + let brace = view.doc.querySelector(".ruleview-ruleclose"); + onKeyPress = once(brace.parentNode, "focus", true); + } else if (/(right|return|back_space)/ig.test(key)) { + info("Adding event listener for right|return|back_space keys"); + onKeyPress = once(editor.input, "keypress"); + } else { + info("Waiting for after-suggest event on the editor"); + onKeyPress = editor.once("after-suggest"); + } + + info("Synthesizing key " + key + ", modifiers: " + Object.keys(modifiers)); + EventUtils.synthesizeKey(key, modifiers, view.doc.defaultView); + + yield onKeyPress; + yield wait(1); // Equivalent of executeSoon + + // The key might have been a TAB or shift-TAB, in which case the editor will + // be a new one + editor = inplaceEditor(view.doc.activeElement); + + info("Checking the state"); + if (completion != null) { + is(editor.input.value, completion, "Correct value is autocompleted"); + } + if (total == 0) { + ok(!(editor.popup && editor.popup.isOpen), "Popup is closed"); + } else { + ok(editor.popup._panel.state == "open" || editor.popup._panel.state == "showing", "Popup is open"); + is(editor.popup.getItems().length, total, "Number of suggestions match"); + is(editor.popup.selectedIndex, index, "Correct item is selected"); + } +} diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_completion-new-property_01.js b/toolkit/devtools/styleinspector/test/browser_ruleview_completion-new-property_01.js new file mode 100644 index 000000000..13b7becb9 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_completion-new-property_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"; + +// Test that CSS property names are autocompleted and cycled correctly when +// creating a new property in the ruleview + +const MAX_ENTRIES = 10; + +// format : +// [ +// what key to press, +// expected input box value after keypress, +// selectedIndex of the popup, +// total items in the popup +// ] +let testData = [ + ["d", "direction", 0, 3], + ["VK_DOWN", "display", 1, 3], + ["VK_DOWN", "dominant-baseline", 2, 3], + ["VK_DOWN", "direction", 0, 3], + ["VK_DOWN", "display", 1, 3], + ["VK_UP", "direction", 0, 3], + ["VK_UP", "dominant-baseline", 2, 3], + ["VK_UP", "display", 1, 3], + ["VK_BACK_SPACE", "d", -1, 0], + ["i", "direction", 0, 2], + ["s", "display", -1, 0], + ["VK_BACK_SPACE", "dis", -1, 0], + ["VK_BACK_SPACE", "di", -1, 0], + ["VK_BACK_SPACE", "d", -1, 0], + ["VK_BACK_SPACE", "", -1, 0], + ["f", "fill", 0, MAX_ENTRIES], + ["i", "fill", 0, 4], + ["VK_ESCAPE", null, -1, 0], +]; + +let TEST_URL = "data:text/html;charset=utf-8,<h1 style='border: 1px solid red'>Filename:" + + "browser_bug893965_css_property_completion_new_property.js</h1>"; + +add_task(function*() { + yield addTab(TEST_URL); + let {toolbox, inspector, view} = yield openRuleView(); + + info("Selecting the test node"); + yield selectNode("h1", inspector); + + info("Focusing the css property editable field"); + let brace = view.doc.querySelector(".ruleview-ruleclose"); + let editor = yield focusEditableField(brace); + + 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, index, total], editor, view) { + info("Pressing key " + key); + info("Expecting " + completion + ", " + index + ", " + total); + + 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"); + } + + info("Synthesizing key " + key); + EventUtils.synthesizeKey(key, {}, view.doc.defaultView); + + yield onSuggest; + yield wait(1); // Equivalent of executeSoon + + info("Checking the state"); + if (completion != null) { + is(editor.input.value, completion, "Correct value is autocompleted"); + } + if (total == 0) { + ok(!(editor.popup && editor.popup.isOpen), "Popup is closed"); + } else { + ok(editor.popup._panel.state == "open" || editor.popup._panel.state == "showing", "Popup is open"); + is(editor.popup.getItems().length, total, "Number of suggestions match"); + is(editor.popup.selectedIndex, index, "Correct item is selected"); + } +} diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_completion-new-property_02.js b/toolkit/devtools/styleinspector/test/browser_ruleview_completion-new-property_02.js new file mode 100644 index 000000000..f26eaec16 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_completion-new-property_02.js @@ -0,0 +1,104 @@ +/* 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 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, +// selectedIndex of the popup, +// total items in the popup +// ] +let testData = [ + ["a", {accelKey: true, ctrlKey: true}, "", -1, 0], + ["d", {}, "direction", 0, 3], + ["VK_DOWN", {}, "display", 1, 3], + ["VK_TAB", {}, "", -1, 10], + ["VK_DOWN", {}, "-moz-box", 0, 10], + ["n", {}, "none", -1, 0], + ["VK_TAB", {shiftKey: true}, "display", -1, 0], + ["VK_BACK_SPACE", {}, "", -1, 0], + ["c", {}, "caption-side", 0, 10], + ["o", {}, "color", 0, 6], + ["VK_TAB", {}, "none", -1, 0], + ["r", {}, "rebeccapurple", 0, 6], + ["VK_DOWN", {}, "red", 1, 6], + ["VK_DOWN", {}, "rgb", 2, 6], + ["VK_DOWN", {}, "rgba", 3, 6], + ["VK_DOWN", {}, "rosybrown", 4, 6], + ["VK_DOWN", {}, "royalblue", 5, 6], + ["VK_RIGHT", {}, "royalblue", -1, 0], + [" ", {}, "royalblue !important", 0, 10], + ["!", {}, "royalblue !important", 0, 0], + ["VK_ESCAPE", {}, null, -1, 0] +]; + +let TEST_URL = "data:text/html;charset=utf-8,<style>h1{border: 1px solid red}</style>" + + "<h1>Test element</h1>"; + +add_task(function*() { + yield addTab(TEST_URL); + let {toolbox, inspector, view} = yield openRuleView(); + + info("Selecting the test node"); + yield selectNode("h1", inspector); + + info("Focusing a new css property editable property"); + let brace = view.doc.querySelectorAll(".ruleview-ruleclose")[1]; + let editor = yield focusEditableField(brace); + + 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.doc.activeElement); + yield testCompletion(testData[i], editor, view); + } +}); + +function* testCompletion([key, modifiers, completion, index, total], editor, view) { + info("Pressing key " + key); + info("Expecting " + completion + ", " + index + ", " + total); + + let onKeyPress; + + if (/tab/ig.test(key)) { + info("Waiting for the new property or value editor to get focused"); + let brace = view.doc.querySelectorAll(".ruleview-ruleclose")[1]; + onKeyPress = once(brace.parentNode, "focus", true); + } else if (/(right|back_space|escape|return)/ig.test(key) || + (modifiers.accelKey || modifiers.ctrlKey)) { + info("Adding event listener for right|escape|back_space|return keys"); + onKeyPress = once(editor.input, "keypress"); + } else { + info("Waiting for after-suggest event on the editor"); + onKeyPress = editor.once("after-suggest"); + } + + info("Synthesizing key " + key + ", modifiers: " + Object.keys(modifiers)); + EventUtils.synthesizeKey(key, modifiers, view.doc.defaultView); + + yield onKeyPress; + yield wait(1); // Equivalent of executeSoon + + 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.doc.activeElement); + is(editor.input.value, completion, "Correct value is autocompleted"); + } + if (total == 0) { + ok(!(editor.popup && editor.popup.isOpen), "Popup is closed"); + } else { + ok(editor.popup._panel.state == "open" || editor.popup._panel.state == "showing", "Popup is open"); + is(editor.popup.getItems().length, total, "Number of suggestions match"); + is(editor.popup.selectedIndex, index, "Correct item is selected"); + } +} diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_content_01.js b/toolkit/devtools/styleinspector/test/browser_ruleview_content_01.js new file mode 100644 index 000000000..ff51123cf --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_content_01.js @@ -0,0 +1,43 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the rule-view content is correct + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,browser_ruleview_content.js"); + let {toolbox, inspector, view} = yield openRuleView(); + + info("Creating the test document"); + let style = "" + + "#testid {" + + " background-color: blue;" + + "}" + + ".testclass, .unmatched {" + + " background-color: green;" + + "}"; + let styleNode = addStyle(content.document, style); + content.document.body.innerHTML = "<div id='testid' class='testclass'>Styled Node</div>" + + "<div id='testid2'>Styled Node</div>"; + + yield testContentAfterNodeSelection(inspector, view); +}); + +function* testContentAfterNodeSelection(inspector, ruleView) { + yield selectNode("#testid", inspector); + is(ruleView.element.querySelectorAll("#noResults").length, 0, + "After a highlight, no longer has a no-results element."); + + yield clearCurrentNodeSelection(inspector) + is(ruleView.element.querySelectorAll("#noResults").length, 1, + "After highlighting null, has a no-results element again."); + + yield selectNode("#testid", inspector); + let classEditor = getRuleViewRuleEditor(ruleView, 2); + is(classEditor.selectorText.querySelector(".ruleview-selector-matched").textContent, + ".testclass", ".textclass should be matched."); + is(classEditor.selectorText.querySelector(".ruleview-selector-unmatched").textContent, + ".unmatched", ".unmatched should not be matched."); +} diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_content_02.js b/toolkit/devtools/styleinspector/test/browser_ruleview_content_02.js new file mode 100644 index 000000000..c5764b8e3 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_content_02.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 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>'; + +const STRINGS = Services.strings + .createBundle("chrome://global/locale/devtools/styleinspector.properties"); + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8," + CONTENT); + + info("Getting the test element"); + let element = getNode("span"); + + info("Opening the inspector using the content context-menu"); + let onInspectorReady = gDevTools.once("inspector-ready"); + + document.popupNode = element; + let contentAreaContextMenu = document.getElementById("contentAreaContextMenu"); + let contextMenu = new nsContextMenu(contentAreaContextMenu); + yield contextMenu.inspectNode(); + + // Clean up context menu: + contextMenu.hiding(); + + yield onInspectorReady; + + let target = TargetFactory.forTab(gBrowser.selectedTab); + let toolbox = gDevTools.getToolbox(target); + + info("Getting the inspector and making sure it is fully updated"); + let inspector = toolbox.getPanel("inspector"); + yield inspector.once("inspector-updated"); + + let view = inspector.sidebar.getWindowForTab("ruleview")["ruleview"].view; + + checkRuleViewContent(view); +}); + +function checkRuleViewContent({doc}) { + info("Making sure the rule-view contains the expected content"); + + let headers = [...doc.querySelectorAll(".ruleview-header")]; + is(headers.length, 3, "There are 3 headers for inherited rules"); + + is(headers[0].textContent, + STRINGS.formatStringFromName("rule.inheritedFrom", ["p"], 1), + "The first header is correct"); + is(headers[1].textContent, + STRINGS.formatStringFromName("rule.inheritedFrom", ["div"], 1), + "The second header is correct"); + is(headers[2].textContent, + STRINGS.formatStringFromName("rule.inheritedFrom", ["body"], 1), + "The third header is correct"); + + let rules = doc.querySelectorAll(".ruleview-rule"); + is(rules.length, 4, "There are 4 rules in the view"); + + for (let rule of rules) { + let selector = rule.querySelector(".ruleview-selector"); + is(selector.textContent, + STRINGS.GetStringFromName("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/toolkit/devtools/styleinspector/test/browser_ruleview_cubicbezier-appears-on-swatch-click.js b/toolkit/devtools/styleinspector/test/browser_ruleview_cubicbezier-appears-on-swatch-click.js new file mode 100644 index 000000000..1e9132b4a --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_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"; + +// Test that cubic-bezier pickers appear when clicking on cubic-bezier swatches + +const PAGE_CONTENT = [ + '<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>' +].join("\n"); + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,rule view cubic-bezier tooltip test"); + content.document.body.innerHTML = PAGE_CONTENT; + let {toolbox, 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 onShown = bezier.tooltip.once("shown"); + swatch.click(); + yield onShown; + + 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"); + bezier.hide(); +} diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_cubicbezier-commit-on-ENTER.js b/toolkit/devtools/styleinspector/test/browser_ruleview_cubicbezier-commit-on-ENTER.js new file mode 100644 index 000000000..3095cbfa2 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_cubicbezier-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"; + +// Test that a curve change in the cubic-bezier tooltip is committed when ENTER +// is pressed + +const PAGE_CONTENT = [ + '<style type="text/css">', + ' body {', + ' transition: top 2s linear;', + ' }', + '</style>' +].join("\n"); + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,rule view cubic-bezier tooltip test"); + content.document.body.innerHTML = PAGE_CONTENT; + let {toolbox, inspector, 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 onShown = bezierTooltip.tooltip.once("shown"); + swatch.click(); + yield onShown; + + 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(() => { + return content.getComputedStyle(content.document.body).transitionTimingFunction === 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"); + let onHidden = bezierTooltip.tooltip.once("hidden"); + EventUtils.sendKey("RETURN", widget.parent.ownerDocument.defaultView); + yield onHidden; + + is(content.getComputedStyle(content.document.body).transitionTimingFunction, + expected, "The element's timing-function was kept after RETURN"); + ok(getRuleViewProperty(ruleView, "body", "transition").valueSpan.textContent + .indexOf("cubic-bezier(") !== -1, + "The text of the timing-function was kept after RETURN"); +} diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_cubicbezier-revert-on-ESC.js b/toolkit/devtools/styleinspector/test/browser_ruleview_cubicbezier-revert-on-ESC.js new file mode 100644 index 000000000..998ab2021 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_cubicbezier-revert-on-ESC.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 changes made to the cubic-bezier timing-function in the cubic-bezier +// tooltip are reverted when ESC is pressed + +const PAGE_CONTENT = [ + '<style type="text/css">', + ' body {', + ' animation-timing-function: linear;', + ' }', + '</style>', +].join("\n"); + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,rule view cubic-bezier tooltip test"); + content.document.body.innerHTML = PAGE_CONTENT; + let {toolbox, inspector, view} = yield openRuleView(); + + info("Getting the bezier swatch element"); + let swatch = getRuleViewProperty(view, "body", "animation-timing-function").valueSpan + .querySelector(".ruleview-bezierswatch"); + yield testPressingEscapeRevertsChanges(swatch, view); +}); + +function* testPressingEscapeRevertsChanges(swatch, ruleView) { + let bezierTooltip = ruleView.tooltips.cubicBezier; + + let onShown = bezierTooltip.tooltip.once("shown"); + swatch.click(); + yield onShown; + + 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(() => { + return content.getComputedStyle(content.document.body).animationTimingFunction === expected; + }, "Waiting for the change to be previewed on the element"); + + info("Pressing ESCAPE to close the tooltip"); + let onHidden = bezierTooltip.tooltip.once("hidden"); + EventUtils.sendKey("ESCAPE", widget.parent.ownerDocument.defaultView); + yield onHidden; + + yield waitForSuccess(() => { + return content.getComputedStyle(content.document.body).animationTimingFunction === "cubic-bezier(0, 0, 1, 1)"; + }, "Waiting for the change to be reverted on the element"); +} diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_edit-property-commit.js b/toolkit/devtools/styleinspector/test/browser_ruleview_edit-property-commit.js new file mode 100644 index 000000000..6fbcec2a0 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_edit-property-commit.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"; + +// Test original value is correctly displayed when ESCaping out of the +// inplace editor in the style inspector. + +const originalValue = "#00F"; + +// 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: originalValue}, + {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,test escaping property change reverts back to original value"); + + info("Creating the test document"); + createDocument(); + + info("Opening the rule view"); + let {toolbox, inspector, view} = yield openRuleView(); + + info("Selecting the test node"); + yield selectNode("#testid", inspector); + + info("Iterating over the test data"); + for (let data of testData) { + yield runTestData(view, data); + } +}); + +function createDocument() { + let style = '' + + '#testid {' + + ' color: ' + originalValue + ';' + + '}'; + + let node = content.document.createElement('style'); + node.setAttribute("type", "text/css"); + node.textContent = style; + content.document.getElementsByTagName("head")[0].appendChild(node); + + content.document.body.innerHTML = '<div id="testid">Styled Node</div>'; +} + +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(propEditor.valueSpan); + is(inplaceEditor(propEditor.valueSpan), editor, "Focused editor should be the value span."); + + info("Entering test data " + value); + for (let ch of value) { + EventUtils.sendChar(ch, view.doc.defaultView); + } + + info("Waiting for focus on the field"); + let onBlur = once(editor.input, "blur"); + + info("Entering the commit key " + commitKey + " " + modifiers); + EventUtils.synthesizeKey(commitKey, modifiers); + yield onBlur; + + if (commitKey === "VK_ESCAPE") { + is(propEditor.valueSpan.textContent, expected, "Value is as expected: " + expected); + } else { + yield once(view.element, "CssRuleViewChanged"); + is(propEditor.valueSpan.textContent, expected, "Value is as expected: " + expected); + } +} diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_edit-property-increments.js b/toolkit/devtools/styleinspector/test/browser_ruleview_edit-property-increments.js new file mode 100644 index 000000000..78dc7a165 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_edit-property-increments.js @@ -0,0 +1,181 @@ +/* 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.
+
+add_task(function*() {
+ yield addTab("data:text/html;charset=utf-8,sample document for bug 722691");
+ createDocument();
+ let {toolbox, inspector, view} = yield openRuleView();
+
+ yield selectNode("#test", inspector);
+
+ yield testMarginIncrements(view);
+ yield testVariousUnitIncrements(view);
+ yield testHexIncrements(view);
+ yield testRgbIncrements(view);
+ yield testShorthandIncrements(view);
+ yield testOddCases(view);
+});
+
+function createDocument() {
+ content.document.body.innerHTML = '' +
+ '<style>' +
+ ' #test {' +
+ ' margin-top:0px;' +
+ ' padding-top: 0px;' +
+ ' color:#000000;' +
+ ' background-color: #000000;' +
+ ' }' +
+ '</style>' +
+ '<div id="test"></div>';
+}
+
+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}
+ });
+}
+
+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}
+ });
+};
+
+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* 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: "1"},
+ 4: {down: true, start: "0", end: "-1"},
+ 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* runIncrementTest(propertyEditor, view, tests) {
+ let editor = yield focusEditableField(propertyEditor.valueSpan);
+
+ for(let test in tests) {
+ yield testIncrement(editor, tests[test], view, propertyEditor);
+ }
+}
+
+function* testIncrement(editor, options, view, {ruleEditor}) {
+ 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 onModifications = ruleEditor.rule._applyingModifications;
+ let onKeyUp = once(input, "keyup");
+ let key;
+ key = options.down ? "VK_DOWN" : "VK_UP";
+ key = options.pageDown ? "VK_PAGE_DOWN" : options.pageUp ? "VK_PAGE_UP" : key;
+ EventUtils.synthesizeKey(key, {altKey: options.alt, shiftKey: options.shift},
+ view.doc.defaultView);
+ yield onKeyUp;
+ yield onModifications;
+
+ is(editor.input.value, options.end, "Value changed to " + options.end);
+}
diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_edit-property-order.js b/toolkit/devtools/styleinspector/test/browser_ruleview_edit-property-order.js new file mode 100644 index 000000000..7cfd6bcd7 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_edit-property-order.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"; + +// Checking properties orders and overrides in the rule-view + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,browser_ruleview_manipulation.js"); + let {toolbox, inspector, view} = yield openRuleView(); + + info("Creating the test document and getting the test node"); + content.document.body.innerHTML = '<div id="testid">Styled Node</div>'; + let element = getNode("#testid"); + + yield selectNode("#testid", inspector); + + let elementStyle = view._elementStyle; + let elementRule = elementStyle.rules[0]; + + info("Checking rules insertion order and checking the applied style"); + let firstProp = elementRule.createProperty("background-color", "green", ""); + let secondProp = elementRule.createProperty("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."); + yield elementRule._applyingModifications; + is(element.style.getPropertyValue("background-color"), "blue", "Second property should have been used."); + + info("Removing the second property and checking the applied style again"); + secondProp.remove(); + yield elementRule._applyingModifications; + is(element.style.getPropertyValue("background-color"), "green", "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 = elementRule.createProperty("background-color", "blue", ""); + yield elementRule._applyingModifications; + is(element.style.getPropertyValue("background-color"), "blue", "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"); + secondProp.setEnabled(false); + yield elementRule._applyingModifications; + is(element.style.getPropertyValue("background-color"), "green", "After disabling second property, first value should be used"); + + info("Disabling the first property too and checking the applied style"); + firstProp.setEnabled(false); + yield elementRule._applyingModifications; + is(element.style.getPropertyValue("background-color"), "", "After disabling both properties, value should be empty."); + + info("Re-enabling the second propertyt and checking the applied style"); + secondProp.setEnabled(true); + yield elementRule._applyingModifications; + is(element.style.getPropertyValue("background-color"), "blue", "Value should be set correctly after re-enabling"); + + info("Re-enabling the first property and checking the insertion order is still respected"); + firstProp.setEnabled(true); + yield elementRule._applyingModifications; + is(element.style.getPropertyValue("background-color"), "blue", "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"); + firstProp.setValue("purple", ""); + yield elementRule._applyingModifications; + is(element.style.getPropertyValue("background-color"), "blue", "Modifying an earlier property shouldn't override a later property."); +}); diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_edit-property_01.js b/toolkit/devtools/styleinspector/test/browser_ruleview_edit-property_01.js new file mode 100644 index 000000000..044d5f834 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_edit-property_01.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"; + +// Testing various inplace-editor behaviors in the rule-view +// FIXME: To be split in several test files, and some of the inplace-editor +// focus/blur/commit/revert stuff should be factored out in head.js + +let TEST_URL = 'url("' + TEST_URL_ROOT + 'doc_test_image.png")'; +let PAGE_CONTENT = [ + '<style type="text/css">', + ' #testid {', + ' background-color: blue;', + ' }', + ' .testclass {', + ' background-color: green;', + ' }', + '</style>', + '<div id="testid" class="testclass">Styled Node</div>' +].join("\n"); + +add_task(function*() { + let tab = yield addTab("data:text/html;charset=utf-8,test rule view user changes"); + + info("Creating the test document"); + content.document.body.innerHTML = PAGE_CONTENT; + + info("Opening the rule-view"); + let {toolbox, inspector, view} = yield openRuleView(); + + info("Selecting the test element"); + yield selectNode("#testid", inspector); + + yield testEditProperty(view, "border-color", "red", tab.linkedBrowser); + yield testEditProperty(view, "background-image", TEST_URL, tab.linkedBrowser); +}); + +function* testEditProperty(view, name, value, browser) { + info("Test editing existing property name/value fields"); + + let idRuleEditor = getRuleViewRuleEditor(view, 1); + let propEditor = idRuleEditor.rule.textProps[0].editor; + + info("Focusing an existing property name in the rule-view"); + let editor = yield focusEditableField(propEditor.nameSpan, 32, 1); + + is(inplaceEditor(propEditor.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(idRuleEditor.element, "focus", true); + let onModifications = idRuleEditor.rule._applyingModifications; + for (let ch of name + ":") { + EventUtils.sendChar(ch, view.doc.defaultView); + } + yield onValueFocus; + yield onModifications; + + // Getting the value editor after focus + editor = inplaceEditor(view.doc.activeElement); + input = editor.input; + is(inplaceEditor(propEditor.valueSpan), editor, "Focus moved to the value."); + + info("Entering a new value, including ; to commit and blur the value"); + let onBlur = once(input, "blur"); + onModifications = idRuleEditor.rule._applyingModifications; + for (let ch of value + ";") { + EventUtils.sendChar(ch, view.doc.defaultView); + } + yield onBlur; + yield onModifications; + + is(propEditor.isValid(), true, value + " is a valid entry"); + + info("Checking that the style property was changed on the content page"); + let propValue = yield executeInContent("Test:GetRulePropertyValue", { + styleSheetIndex: 0, + ruleIndex: 0, + name + }); + is(propValue, value, name + " should have been set."); +} diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_edit-property_02.js b/toolkit/devtools/styleinspector/test/browser_ruleview_edit-property_02.js new file mode 100644 index 000000000..5086873f6 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_edit-property_02.js @@ -0,0 +1,142 @@ +/* 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 + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,browser_ruleview_ui.js"); + let {toolbox, inspector, view} = yield openRuleView(); + + info("Creating the test document"); + let style = "" + + "#testid {" + + " background-color: blue;" + + "}" + + ".testclass, .unmatched {" + + " background-color: green;" + + "}"; + let styleNode = addStyle(content.document, style); + content.document.body.innerHTML = "<div id='testid' class='testclass'>Styled Node</div>" + + "<div id='testid2'>Styled Node</div>"; + + yield selectNode("#testid", inspector); + yield testEditProperty(inspector, view); + yield testDisableProperty(inspector, view); + yield testPropertyStillMarkedDirty(inspector, view); + + gBrowser.removeCurrentTab(); +}); + +function* testEditProperty(inspector, ruleView) { + let idRuleEditor = getRuleViewRuleEditor(ruleView, 1); + let propEditor = idRuleEditor.rule.textProps[0].editor; + + let editor = yield focusEditableField(propEditor.nameSpan); + let input = editor.input; + is(inplaceEditor(propEditor.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.doc.defaultView); + input.select(); + + info("Entering property name \"border-color\" followed by a colon to focus the value"); + let onFocus = once(idRuleEditor.element, "focus", true); + for (let ch of "border-color:") { + EventUtils.sendChar(ch, ruleView.doc.defaultView); + } + yield onFocus; + yield idRuleEditor.rule._applyingModifications; + + info("Verifying that the focused field is the valueSpan"); + editor = inplaceEditor(ruleView.doc.activeElement); + input = editor.input; + is(inplaceEditor(propEditor.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"); + for (let ch of "red;") { + EventUtils.sendChar(ch, ruleView.doc.defaultView); + is(propEditor.warning.hidden, true, + "warning triangle is hidden or shown as appropriate"); + } + yield onBlur; + yield idRuleEditor.rule._applyingModifications; + + let newValue = yield executeInContent("Test:GetRulePropertyValue", { + styleSheetIndex: 0, + ruleIndex: 0, + name: "border-color" + }); + is(newValue, "red", "border-color should have been set."); + + info("Entering property name \"color\" followed by a colon to focus the value"); + onFocus = once(idRuleEditor.element, "focus", true); + for (let ch of "color:") { + EventUtils.sendChar(ch, ruleView.doc.defaultView); + } + yield onFocus; + + info("Verifying that the focused field is the valueSpan"); + editor = inplaceEditor(ruleView.doc.activeElement); + + info("Entering a value following by a semi-colon to commit it"); + onBlur = once(editor.input, "blur"); + for (let ch of "red;") { + EventUtils.sendChar(ch, ruleView.doc.defaultView); + } + yield onBlur; + yield idRuleEditor.rule._applyingModifications; + + 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 idRuleEditor = getRuleViewRuleEditor(ruleView, 1); + let propEditor = idRuleEditor.rule.textProps[0].editor; + + info("Disabling a property"); + propEditor.enable.click(); + yield idRuleEditor.rule._applyingModifications; + + 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"); + propEditor.enable.click(); + yield idRuleEditor.rule._applyingModifications; + + 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/toolkit/devtools/styleinspector/test/browser_ruleview_edit-selector-commit.js b/toolkit/devtools/styleinspector/test/browser_ruleview_edit-selector-commit.js new file mode 100644 index 000000000..5d5c7516a --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_edit-selector-commit.js @@ -0,0 +1,98 @@ +/* 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 + +let PAGE_CONTENT = [ + '<style type="text/css">', + ' #testid {', + ' text-align: center;', + ' }', + '</style>', + '<div id="testid" class="testclass">Styled Node</div>', +].join("\n"); + +const TEST_DATA = [ + { + node: "#testid", + value: ".testclass", + commitKey: "VK_ESCAPE", + modifiers: {}, + expected: "#testid" + }, + { + node: "#testid", + value: ".testclass", + commitKey: "VK_RETURN", + modifiers: {}, + expected: ".testclass" + }, + { + node: "#testid", + value: ".testclass", + commitKey: "VK_TAB", + modifiers: {}, + expected: ".testclass" + }, + { + node: "#testid", + value: ".testclass", + commitKey: "VK_TAB", + modifiers: {shiftKey: true}, + expected: ".testclass" + } +]; + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,test escaping selector change reverts back to original value"); + + info("Creating the test document"); + content.document.body.innerHTML = PAGE_CONTENT; + + info("Opening the rule-view"); + let {toolbox, inspector, view} = yield openRuleView(); + + info("Iterating over the test data"); + 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(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); + + if (commitKey === "VK_ESCAPE") { + is(idRuleEditor.rule.selectorText, expected, + "Value is as expected: " + expected); + is(idRuleEditor.isEditing, false, "Selector is not being edited.") + } else { + yield once(view.element, "CssRuleViewRefreshed"); + ok(getRuleViewRule(view, expected), + "Rule with " + name + " selector exists."); + } + + info("Resetting page content"); + content.document.body.innerHTML = PAGE_CONTENT; +} diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_edit-selector_01.js b/toolkit/devtools/styleinspector/test/browser_ruleview_edit-selector_01.js new file mode 100644 index 000000000..6a8dedc0a --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_edit-selector_01.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"; + +// Testing selector inplace-editor behaviors in the rule-view + +let PAGE_CONTENT = [ + '<style type="text/css">', + ' .testclass {', + ' text-align: center;', + ' }', + '</style>', + '<div id="testid" class="testclass">Styled Node</div>', + '<span id="testid2">This is a span</span>' +].join("\n"); + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,test rule view selector changes"); + + info("Creating the test document"); + content.document.body.innerHTML = PAGE_CONTENT; + + info("Opening the rule-view"); + let {toolbox, inspector, view} = yield openRuleView(); + + info("Selecting the test element"); + yield selectNode("#testid", inspector); + yield testEditSelector(view, "span"); + + info("Selecting the modified element"); + yield selectNode("#testid2", 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(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 refresh"); + let onRuleViewRefresh = once(view.element, "CssRuleViewRefreshed"); + + info("Entering the commit key"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewRefresh; + + is(view._elementStyle.rules.length, 1, "Should have 1 rule."); + is(getRuleViewRule(view, name), undefined, + name + " selector has been removed."); +} + +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/toolkit/devtools/styleinspector/test/browser_ruleview_edit-selector_02.js b/toolkit/devtools/styleinspector/test/browser_ruleview_edit-selector_02.js new file mode 100644 index 000000000..52b96529f --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_edit-selector_02.js @@ -0,0 +1,81 @@ +/* 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. + +let PAGE_CONTENT = [ + '<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>' +].join("\n"); + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,test rule view selector changes"); + + info("Creating the test document"); + content.document.body.innerHTML = PAGE_CONTENT; + + info("Opening the rule-view"); + let {toolbox, inspector, view} = yield openRuleView(); + + info("Selecting the test element"); + yield selectNode(".testclass", inspector); + yield testEditSelector(view, "div:nth-child(2)"); + + info("Selecting the modified element"); + yield selectNode("#testid", inspector); + yield checkModifiedElement(view, "div:nth-child(2)"); + + 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"); +}); + +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(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 refresh"); + let onRuleViewRefresh = once(view.element, "CssRuleViewRefreshed"); + + info("Entering the commit key"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewRefresh; + + is(view._elementStyle.rules.length, 1, "Should have 1 rule."); + is(getRuleViewRule(view, name), undefined, + name + " selector has been removed."); +} + +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/toolkit/devtools/styleinspector/test/browser_ruleview_eyedropper.js b/toolkit/devtools/styleinspector/test/browser_ruleview_eyedropper.js new file mode 100644 index 000000000..afaa40e25 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_eyedropper.js @@ -0,0 +1,195 @@ +/* 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"; + +// So we can test collecting telemetry on the eyedropper +let oldCanRecord = Services.telemetry.canRecord; +Services.telemetry.canRecord = true; +registerCleanupFunction(function () { + Services.telemetry.canRecord = oldCanRecord; +}); +const HISTOGRAM_ID = "DEVTOOLS_PICKER_EYEDROPPER_OPENED_BOOLEAN"; +const FLAG_HISTOGRAM_ID = "DEVTOOLS_PICKER_EYEDROPPER_OPENED_PER_USER_FLAG"; +const EXPECTED_TELEMETRY = { + "DEVTOOLS_PICKER_EYEDROPPER_OPENED_BOOLEAN": 2, + "DEVTOOLS_PICKER_EYEDROPPER_OPENED_PER_USER_FLAG": 1 +} + +const PAGE_CONTENT = [ + '<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>' +].join("\n"); + +const ORIGINAL_COLOR = "rgb(255, 0, 153)"; // #f09 +const EXPECTED_COLOR = "rgb(255, 255, 85)"; // #ff5 + +// Test opening the eyedropper from the color picker. Pressing escape +// to close it, and clicking the page to select a color. + +add_task(function*() { + // clear telemetry so we can get accurate counts + clearTelemetry(); + + yield addTab("data:text/html;charset=utf-8,rule view eyedropper test"); + content.document.body.innerHTML = PAGE_CONTENT; + + let {toolbox, inspector, view} = yield openRuleView(); + yield selectNode("#div2", inspector); + + 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"); + + let dropper = yield openEyedropper(view, swatch); + + let tooltip = view.tooltips.colorPicker.tooltip; + ok(tooltip.isHidden(), + "color picker tooltip is closed after opening eyedropper"); + + yield testESC(swatch, dropper); + + dropper = yield openEyedropper(view, swatch); + + ok(dropper, "dropper opened"); + + yield testSelect(swatch, dropper); + + checkTelemetry(); +}); + +function testESC(swatch, dropper) { + let deferred = promise.defer(); + + dropper.once("destroy", () => { + let color = swatch.style.backgroundColor; + is(color, ORIGINAL_COLOR, "swatch didn't change after pressing ESC"); + + deferred.resolve(); + }); + + inspectPage(dropper, false).then(pressESC); + + return deferred.promise; +} + +function testSelect(swatch, dropper) { + let deferred = promise.defer(); + + dropper.once("destroy", () => { + let color = swatch.style.backgroundColor; + is(color, EXPECTED_COLOR, "swatch changed colors"); + + // the change to the content is done async after rule view change + executeSoon(() => { + let element = content.document.querySelector("div"); + is(content.window.getComputedStyle(element).backgroundColor, + EXPECTED_COLOR, + "div's color set to body color after dropper"); + + deferred.resolve(); + }); + }); + + inspectPage(dropper); + + return deferred.promise; +} + +function clearTelemetry() { + for (let histogramId in EXPECTED_TELEMETRY) { + let histogram = Services.telemetry.getHistogramById(histogramId); + histogram.clear(); + } +} + +function checkTelemetry() { + for (let histogramId in EXPECTED_TELEMETRY) { + let expected = EXPECTED_TELEMETRY[histogramId]; + let histogram = Services.telemetry.getHistogramById(histogramId); + let snapshot = histogram.snapshot(); + + is (snapshot.counts[1], expected, + "eyedropper telemetry value correct for " + histogramId); + } +} + +/* Helpers */ + +function openEyedropper(view, swatch) { + let deferred = promise.defer(); + + let tooltip = view.tooltips.colorPicker.tooltip; + + tooltip.once("shown", () => { + let tooltipDoc = tooltip.content.contentDocument; + let dropperButton = tooltipDoc.querySelector("#eyedropper-button"); + + tooltip.once("eyedropper-opened", (event, dropper) => { + deferred.resolve(dropper) + }); + dropperButton.click(); + }); + + swatch.click(); + return deferred.promise; +} + +function inspectPage(dropper, click=true) { + let target = document.documentElement; + let win = window; + + // get location of the content, offset from browser window + let box = gBrowser.selectedBrowser.getBoundingClientRect(); + let x = box.left + 1; + let y = box.top + 1; + + return dropperStarted(dropper).then(() => { + EventUtils.synthesizeMouse(target, x, y, { type: "mousemove" }, win); + + return dropperLoaded(dropper).then(() => { + EventUtils.synthesizeMouse(target, x + 10, y + 10, { type: "mousemove" }, win); + + if (click) { + EventUtils.synthesizeMouse(target, x + 10, y + 10, {}, win); + } + }); + }); +} + +function dropperStarted(dropper) { + if (dropper.isStarted) { + return promise.resolve(); + } + return dropper.once("started"); +} + +function dropperLoaded(dropper) { + if (dropper.loaded) { + return promise.resolve(); + } + return dropper.once("load"); +} + +function pressESC() { + EventUtils.synthesizeKey("VK_ESCAPE", { }); +} diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_inherit.js b/toolkit/devtools/styleinspector/test/browser_ruleview_inherit.js new file mode 100644 index 000000000..f8914e3b1 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_inherit.js @@ -0,0 +1,86 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that inherited properties appear as such in the rule-view + +let {ELEMENT_STYLE} = devtools.require("devtools/server/actors/styles"); + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,browser_inspector_changes.js"); + let {toolbox, inspector, view} = yield openRuleView(); + + yield simpleInherit(inspector, view); + yield emptyInherit(inspector, view); + yield elementStyleInherit(inspector, view); +}); + +function* simpleInherit(inspector, view) { + let style = '' + + '#test2 {' + + ' background-color: green;' + + ' color: purple;' + + '}'; + + let styleNode = addStyle(content.document, style); + content.document.body.innerHTML = '<div id="test2"><div id="test1">Styled Node</div></div>'; + + yield selectNode("#test1", inspector); + + 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, 1, "Should only display one inherited style"); + let inheritProp = inheritRule.textProps[0]; + is(inheritProp.name, "color", "color should have been inherited."); + + styleNode.remove(); +} + +function* emptyInherit(inspector, view) { + // No inheritable styles, this rule shouldn't show up. + let style = '' + + '#test2 {' + + ' background-color: green;' + + '}'; + + let styleNode = addStyle(content.document, style); + content.document.body.innerHTML = '<div id="test2"><div id="test1">Styled Node</div></div>'; + + yield selectNode("#test1", inspector); + + 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."); + + styleNode.parentNode.removeChild(styleNode); +} + +function* elementStyleInherit(inspector, view) { + content.document.body.innerHTML = '<div id="test2" style="color: red"><div id="test1">Styled Node</div></div>'; + + yield selectNode("#test1", inspector); + + 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/toolkit/devtools/styleinspector/test/browser_ruleview_keybindings.js b/toolkit/devtools/styleinspector/test/browser_ruleview_keybindings.js new file mode 100644 index 000000000..9ffb85b3c --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_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 {toolbox, inspector, view} = yield openRuleView(); + + info("Selecting the test node"); + yield selectNode("h1", inspector); + + info("Getting the ruleclose brace element"); + let brace = view.doc.querySelector(".ruleview-ruleclose"); + + info("Clicking on the brace element to focus the new property field"); + let onFocus = once(brace.parentNode, "focus", true); + brace.click(); + yield onFocus; + + info("Entering a property name"); + let editor = getCurrentInplaceEditor(view); + editor.input.value = "color"; + + info("Typing ENTER to focus the next field: property value"); + onFocus = once(brace.parentNode, "focus", true); + EventUtils.sendKey("return"); + yield onFocus; + 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); + EventUtils.sendKey("return"); + yield onFocus; + ok(true, "The new property name field was focused"); + getCurrentInplaceEditor(view).input.blur(); +}); + +function getCurrentInplaceEditor(view) { + return inplaceEditor(view.doc.activeElement); +} diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_keyframes-rule_01.js b/toolkit/devtools/styleinspector/test/browser_ruleview_keyframes-rule_01.js new file mode 100644 index 000000000..23a65ddf9 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_keyframes-rule_01.js @@ -0,0 +1,127 @@ +/* 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 = TEST_URL_ROOT + "doc_keyframeanimation.html"; + +add_task(function*() { + yield addTab(TEST_URI); + + let {toolbox, 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"); + + let { + rules, + element, + elementStyle + } = yield assertKeyframeRules("#pacman", inspector, view, { + elementRulesNb: 2, + keyframeRulesNb: 2, + keyframesRules: ["pacman", "pacman"], + keyframeRules: ["100%", "100%"] + }); + + let gutters = assertGutters(view, { + guttersNbs: 2, + gutterHeading: ["Keyframes pacman", "Keyframes pacman"] + }); +} + +function* testBoxy(inspector, view) { + info("Test content and gutter in the keyframes rule of #boxy"); + + let { + rules, + element, + elementStyle + } = yield assertKeyframeRules("#boxy", inspector, view, { + elementRulesNb: 3, + keyframeRulesNb: 3, + keyframesRules: ["boxy", "boxy", "boxy"], + keyframeRules: ["10%", "20%", "100%"] + }); + + let gutters = assertGutters(view, { + guttersNbs: 1, + gutterHeading: ["Keyframes boxy"] + }); +} + +function testMoxy(inspector, view) { + info("Test content and gutter in the keyframes rule of #moxy"); + + let { + rules, + element, + elementStyle + } = yield assertKeyframeRules("#moxy", inspector, view, { + elementRulesNb: 3, + keyframeRulesNb: 4, + keyframesRules: ["boxy", "boxy", "boxy", "moxy"], + keyframeRules: ["10%", "20%", "100%", "100%"] + }); + + let gutters = assertGutters(view, { + guttersNbs: 2, + gutterHeading: ["Keyframes boxy", "Keyframes moxy"] + }); +} + +function* testNode(selector, inspector, view) { + let element = getNode(selector); + yield selectNode(selector, inspector); + let elementStyle = view._elementStyle; + return {element, elementStyle}; +} + +function* assertKeyframeRules(selector, inspector, view, expected) { + let {element, elementStyle} = yield testNode(selector, inspector, view); + + 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++; + } + + return {rules, element, elementStyle}; +} + +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/toolkit/devtools/styleinspector/test/browser_ruleview_keyframes-rule_02.js b/toolkit/devtools/styleinspector/test/browser_ruleview_keyframes-rule_02.js new file mode 100644 index 000000000..93f70e815 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_keyframes-rule_02.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"; + +// Test that verifies the content of the keyframes rule and property changes +// to keyframe rules + +const TEST_URI = TEST_URL_ROOT + "doc_keyframeanimation.html"; + +add_task(function*() { + yield addTab(TEST_URI); + + let {toolbox, 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, + element, + elementStyle + } = 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 + + // 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"); + + // 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, + element, + elementStyle + } = 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) { + let element = getNode(selector); + + 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, element, elementStyle}; +} diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_livepreview.js b/toolkit/devtools/styleinspector/test/browser_ruleview_livepreview.js new file mode 100644 index 000000000..c51e70e6f --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_livepreview.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 changes are previewed when editing a property value + +// 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,test rule view live preview on user changes"); + + let style = '#testid {display:block;}'; + let styleNode = addStyle(content.document, style); + content.document.body.innerHTML = '<div id="testid">Styled Node</div><span>inline element</span>'; + + let {toolbox, 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 testElement = getNode(selector); + let idRuleEditor = getRuleViewRuleEditor(ruleView, 1); + let propEditor = idRuleEditor.rule.textProps[0].editor; + + info("Focusing the property value inplace-editor"); + let editor = yield focusEditableField(propEditor.valueSpan); + is(inplaceEditor(propEditor.valueSpan), editor, "The focused editor is the value"); + + info("Enter a value in the editor") + for (let ch of data.value) { + EventUtils.sendChar(ch, ruleView.doc.defaultView); + } + if (data.escape) { + EventUtils.synthesizeKey("VK_ESCAPE", {}); + } else { + EventUtils.synthesizeKey("VK_RETURN", {}); + } + + // Wait for the modifyproperties request to complete before + // checking the computed style. + for (let rule of ruleView._elementStyle.rules) { + if (rule._applyingModifications) { + yield rule._applyingModifications; + } + } + + // 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/toolkit/devtools/styleinspector/test/browser_ruleview_mathml-element.js b/toolkit/devtools/styleinspector/test/browser_ruleview_mathml-element.js new file mode 100644 index 000000000..288226378 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_mathml-element.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"; + +// Tests that the rule-view displays correctly on MathML elements + +const TEST_URL = [ + "data:text/html;charset=utf-8,", + "<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>" +].join(""); + +add_task(function*() { + yield addTab(TEST_URL); + let {toolbox, 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/toolkit/devtools/styleinspector/test/browser_ruleview_media-queries.js b/toolkit/devtools/styleinspector/test/browser_ruleview_media-queries.js new file mode 100644 index 000000000..0eaebcbdd --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_media-queries.js @@ -0,0 +1,31 @@ +/* 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 = TEST_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 _strings = Services.strings + .createBundle("chrome://global/locale/devtools/styleinspector.properties"); + + let inline = _strings.GetStringFromName("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 + + ":15 @media screen and (min-width: 1px)", "check rule 1 title"); + is(elementStyle.rules[2].title, inline + ":8", "check rule 2 title"); +}); + diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_multiple-properties-duplicates.js b/toolkit/devtools/styleinspector/test/browser_ruleview_multiple-properties-duplicates.js new file mode 100644 index 000000000..0dd816c1a --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_multiple-properties-duplicates.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 + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,test rule view user changes"); + content.document.body.innerHTML = "<h1>Testing Multiple Properties</h1>"; + let {toolbox, inspector, view} = yield openRuleView(); + + info("Creating the test element"); + let newElement = content.document.createElement("div"); + newElement.textContent = "Test Element"; + content.document.body.appendChild(newElement); + yield selectNode("div", inspector); + let ruleEditor = getRuleViewRuleEditor(view, 0); + + yield testCreateNewMultiDuplicates(inspector, ruleEditor); +}); + +function* testCreateNewMultiDuplicates(inspector, ruleEditor) { + yield createNewRuleViewProperty(ruleEditor, + "color:red;color:orange;color:yellow;color:green;color:blue;color:indigo;color:violet;"); + + 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"); + + yield inspector.once("inspector-updated"); +} diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_multiple-properties-priority.js b/toolkit/devtools/styleinspector/test/browser_ruleview_multiple-properties-priority.js new file mode 100644 index 000000000..eb82938c3 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_multiple-properties-priority.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"; + +// Test that the rule-view behaves correctly when entering mutliple and/or +// unfinished properties/values in inplace-editors + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,test rule view user changes"); + content.document.body.innerHTML = "<h1>Testing Multiple Properties</h1>"; + let {toolbox, inspector, view} = yield openRuleView(); + + info("Creating the test element"); + let newElement = content.document.createElement("div"); + newElement.textContent = "Test Element"; + content.document.body.appendChild(newElement); + yield selectNode("div", inspector); + let ruleEditor = getRuleViewRuleEditor(view, 0); + + yield testCreateNewMultiPriority(inspector, ruleEditor); +}); + +function* testCreateNewMultiPriority(inspector, ruleEditor) { + yield createNewRuleViewProperty(ruleEditor, + "color:red;width:100px;height: 100px;"); + + 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"); + + yield inspector.once("inspector-updated"); +} diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_multiple-properties-unfinished_01.js b/toolkit/devtools/styleinspector/test/browser_ruleview_multiple-properties-unfinished_01.js new file mode 100644 index 000000000..1262fd194 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_multiple-properties-unfinished_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 + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,test rule view user changes"); + content.document.body.innerHTML = "<h1>Testing Multiple Properties</h1>"; + let {toolbox, inspector, view} = yield openRuleView(); + + info("Creating the test element"); + let newElement = content.document.createElement("div"); + newElement.textContent = "Test Element"; + content.document.body.appendChild(newElement); + yield selectNode("div", inspector); + let ruleEditor = getRuleViewRuleEditor(view, 0); + + yield testCreateNewMultiUnfinished(inspector, ruleEditor, view); +}); + +function* testCreateNewMultiUnfinished(inspector, ruleEditor, view) { + yield createNewRuleViewProperty(ruleEditor, + "color:blue;background : orange ; text-align:center; border-color: "); + + is(ruleEditor.rule.textProps.length, 4, "Should have created new text properties."); + is(ruleEditor.propertyList.children.length, 4, "Should have created property editors."); + + for (let ch of "red") { + EventUtils.sendChar(ch, view.doc.defaultView); + } + EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView); + + 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"); + + yield inspector.once("inspector-updated"); +} diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_multiple-properties-unfinished_02.js b/toolkit/devtools/styleinspector/test/browser_ruleview_multiple-properties-unfinished_02.js new file mode 100644 index 000000000..18ac71d69 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_multiple-properties-unfinished_02.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"; + +// Test that the rule-view behaves correctly when entering mutliple and/or +// unfinished properties/values in inplace-editors + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,test rule view user changes"); + content.document.body.innerHTML = "<h1>Testing Multiple Properties</h1>"; + let {toolbox, inspector, view} = yield openRuleView(); + + info("Creating the test element"); + let newElement = content.document.createElement("div"); + newElement.textContent = "Test Element"; + content.document.body.appendChild(newElement); + yield selectNode("div", inspector); + let ruleEditor = getRuleViewRuleEditor(view, 0); + + yield testCreateNewMultiPartialUnfinished(inspector, ruleEditor, view); +}); + +function* testCreateNewMultiPartialUnfinished(inspector, ruleEditor, view) { + yield createNewRuleViewProperty(ruleEditor, "width: 100px; heig"); + + 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 + let valueEditor = ruleEditor.propertyList.children[1].querySelector("input"); + valueEditor.value = "10px;background:orangered;color: black;"; + EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView); + + 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"); + + yield inspector.once("inspector-updated"); +} diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_multiple_properties_01.js b/toolkit/devtools/styleinspector/test/browser_ruleview_multiple_properties_01.js new file mode 100644 index 000000000..8c37c7cb1 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_multiple_properties_01.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"; + +// Test that the rule-view behaves correctly when entering mutliple and/or +// unfinished properties/values in inplace-editors + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,test rule view user changes"); + content.document.body.innerHTML = "<h1>Testing Multiple Properties</h1>"; + let {toolbox, inspector, view} = yield openRuleView(); + + info("Creating the test element"); + let newElement = content.document.createElement("div"); + newElement.textContent = "Test Element"; + content.document.body.appendChild(newElement); + yield selectNode("div", inspector); + let ruleEditor = getRuleViewRuleEditor(view, 0); + + yield testCreateNewMulti(inspector, ruleEditor); +}); + +function* testCreateNewMulti(inspector, ruleEditor) { + yield createNewRuleViewProperty(ruleEditor, + "color:blue;background : orange ; text-align:center; border-color: green;"); + + 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"); + + yield inspector.once("inspector-updated"); +} diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_multiple_properties_02.js b/toolkit/devtools/styleinspector/test/browser_ruleview_multiple_properties_02.js new file mode 100644 index 000000000..3179a7b51 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_multiple_properties_02.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 the rule-view behaves correctly when entering mutliple and/or +// unfinished properties/values in inplace-editors + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,test rule view user changes"); + content.document.body.innerHTML = "<h1>Testing Multiple Properties</h1>"; + let {toolbox, inspector, view} = yield openRuleView(); + + info("Creating the test element"); + let newElement = content.document.createElement("div"); + newElement.textContent = "Test Element"; + content.document.body.appendChild(newElement); + yield selectNode("div", inspector); + let ruleEditor = getRuleViewRuleEditor(view, 0); + + yield testMultiValues(inspector, ruleEditor, view); +}); + +function* testMultiValues(inspector, ruleEditor, view) { + yield createNewRuleViewProperty(ruleEditor, "width:"); + + 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 + let valueEditor = ruleEditor.propertyList.children[0].querySelector("input"); + valueEditor.value = "height: 10px;color:blue" + EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView); + + 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.doc.defaultView); + 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"); + + yield inspector.once("inspector-updated"); +} diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_original-source-link.js b/toolkit/devtools/styleinspector/test/browser_ruleview_original-source-link.js new file mode 100644 index 000000000..4baa84df3 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_original-source-link.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"; + +// Test that the stylesheet links in the rule view are correct when source maps +// are involved + +const TESTCASE_URI = TEST_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); + + info("Opening the test page and opening the inspector"); + 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 = promise.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, col} = 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("label"); + return waitForSuccess( + () => label.getAttribute("value") == text, + "Link text changed to display correct location: " + text + ); +} diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_override.js b/toolkit/devtools/styleinspector/test/browser_ruleview_override.js new file mode 100644 index 000000000..289f65da4 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_override.js @@ -0,0 +1,146 @@ +/* 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 display of overridden declarations in the rule-view + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,browser_ruleview_override.js"); + let {toolbox, inspector, view} = yield openRuleView(); + + yield simpleOverride(inspector, view); + yield partialOverride(inspector, view); + yield importantOverride(inspector, view); + yield disableOverride(inspector, view); +}); + +function* createTestContent(inspector, style) { + let onMutated = inspector.once("markupmutation"); + let styleNode = addStyle(content.document, style); + content.document.body.innerHTML = '<div id="testid" class="testclass">Styled Node</div>'; + yield onMutated; + yield selectNode("#testid", inspector); + return styleNode; +} + +function* simpleOverride(inspector, view) { + let styleNode = yield createTestContent(inspector, '' + + '#testid {' + + ' background-color: blue;' + + '} ' + + '.testclass {' + + ' background-color: green;' + + '}'); + + let elementStyle = view._elementStyle; + + let idRule = elementStyle.rules[1]; + let idProp = idRule.textProps[0]; + is(idProp.name, "background-color", "First ID prop should be background-color"); + ok(!idProp.overridden, "ID prop should not be overridden."); + + let classRule = elementStyle.rules[2]; + let classProp = classRule.textProps[0]; + is(classProp.name, "background-color", "First class prop should be background-color"); + ok(classProp.overridden, "Class property should be overridden."); + + // Override background-color by changing the element style. + let elementRule = elementStyle.rules[0]; + elementRule.createProperty("background-color", "purple", ""); + yield elementRule._applyingModifications; + + let elementProp = elementRule.textProps[0]; + is(classProp.name, "background-color", "First element prop should now be background-color"); + ok(!elementProp.overridden, "Element style property should not be overridden"); + ok(idProp.overridden, "ID property should be overridden"); + ok(classProp.overridden, "Class property should be overridden"); + + styleNode.remove(); +} + +function* partialOverride(inspector, view) { + let styleNode = yield createTestContent(inspector, '' + + // Margin shorthand property... + '.testclass {' + + ' margin: 2px;' + + '}' + + // ... will be partially overridden. + '#testid {' + + ' margin-left: 1px;' + + '}'); + + 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."); + } + } + + styleNode.remove(); +} + +function* importantOverride(inspector, view) { + let styleNode = yield createTestContent(inspector, '' + + // Margin shorthand property... + '.testclass {' + + ' background-color: green !important;' + + '}' + + // ... will be partially overridden. + '#testid {' + + ' background-color: blue;' + + '}'); + + let elementStyle = view._elementStyle; + + let idRule = elementStyle.rules[1]; + let idProp = idRule.textProps[0]; + ok(idProp.overridden, "Not-important rule should be overridden."); + + let classRule = elementStyle.rules[2]; + let classProp = classRule.textProps[0]; + ok(!classProp.overridden, "Important rule should not be overridden."); + + styleNode.remove(); + + let elementRule = elementStyle.rules[0]; + let elementProp = elementRule.createProperty("background-color", "purple", "important"); + yield elementRule._applyingModifications; + + ok(classProp.overridden, "New important prop should override class property."); + ok(!elementProp.overridden, "New important prop should not be overriden."); +} + +function* disableOverride(inspector, view) { + let styleNode = yield createTestContent(inspector, '' + + '#testid {' + + ' background-color: blue;' + + '}' + + '.testclass {' + + ' background-color: green;' + + '}'); + + let elementStyle = view._elementStyle; + + let idRule = elementStyle.rules[1]; + let idProp = idRule.textProps[0]; + + idProp.setEnabled(false); + yield idRule._applyingModifications; + + let classRule = elementStyle.rules[2]; + let classProp = classRule.textProps[0]; + ok(!classProp.overridden, "Class prop should not be overridden after id prop was disabled."); + + styleNode.remove(); + yield inspector.once("inspector-updated"); +} diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_pseudo-element_01.js b/toolkit/devtools/styleinspector/test/browser_ruleview_pseudo-element_01.js new file mode 100644 index 000000000..6050f76f1 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_pseudo-element_01.js @@ -0,0 +1,247 @@ +/* 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 = TEST_URL_ROOT + "doc_pseudoelement.html"; + +add_task(function*() { + yield addTab(TEST_URI); + let {toolbox, 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 selector = "#topleft"; + let { + rules, + element, + elementStyle + } = yield assertPseudoElementRulesNumbers(selector, inspector, view, { + elementRulesNb: 4, + firstLineRulesNb: 2, + firstLetterRulesNb: 1, + selectionRulesNb: 0 + }); + + let gutters = assertGutters(view); + + // Make sure that clicking on the twisty hides pseudo elements + let expander = gutters[0].querySelector(".ruleview-expander"); + ok (view.element.firstChild.classList.contains("show-expandable-container"), "Pseudo Elements are expanded"); + expander.click(); + ok (!view.element.firstChild.classList.contains("show-expandable-container"), "Pseudo Elements are collapsed by twisty"); + expander.click(); + ok (view.element.firstChild.classList.contains("show-expandable-container"), "Pseudo Elements are expanded again"); + + // Make sure that dblclicking on the header container also toggles the pseudo elements + EventUtils.synthesizeMouseAtCenter(gutters[0], {clickCount: 2}, inspector.sidebar.getWindowForTab("ruleview")); + ok (!view.element.firstChild.classList.contains("show-expandable-container"), "Pseudo Elements are collapsed by dblclicking"); + + let defaultView = element.ownerDocument.defaultView; + + let elementRule = rules.elementRules[0]; + let elementRuleView = getRuleViewRuleEditor(view, 3); + + let elementFirstLineRule = rules.firstLineRules[0]; + let elementFirstLineRuleView = [].filter.call(view.element.children[1].children, (e) => { + return e._ruleEditor && e._ruleEditor.rule === elementFirstLineRule; + })[0]._ruleEditor; + + is + ( + convertTextPropsToString(elementFirstLineRule.textProps), + "color: orange", + "TopLeft firstLine properties are correct" + ); + + let firstProp = elementFirstLineRuleView.addProperty("background-color", "rgb(0, 255, 0)", ""); + let secondProp = elementFirstLineRuleView.addProperty("font-style", "italic", ""); + + 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"); + + yield elementFirstLineRule._applyingModifications; + + is((yield getComputedStyleProperty(selector, ":first-line", "background-color")), + "rgb(0, 255, 0)", "Added property should have been used."); + is((yield getComputedStyleProperty(selector, ":first-line", "font-style")), + "italic", "Added property should have been used."); + is((yield getComputedStyleProperty(selector, null, "text-decoration")), + "none", "Added property should not apply to element"); + + firstProp.setEnabled(false); + yield elementFirstLineRule._applyingModifications; + + is((yield getComputedStyleProperty(selector, ":first-line", "background-color")), + "rgb(255, 0, 0)", "Disabled property should now have been used."); + is((yield getComputedStyleProperty(selector, null, "background-color")), + "rgb(221, 221, 221)", "Added property should not apply to element"); + + firstProp.setEnabled(true); + yield elementFirstLineRule._applyingModifications; + + is((yield getComputedStyleProperty(selector, ":first-line", "background-color")), + "rgb(0, 255, 0)", "Added property should have been used."); + is((yield getComputedStyleProperty(selector, null, "text-decoration")), + "none", "Added property should not apply to element"); + + firstProp = elementRuleView.addProperty("background-color", "rgb(0, 0, 255)", ""); + yield elementRule._applyingModifications; + + is((yield getComputedStyleProperty(selector, null, "background-color")), + "rgb(0, 0, 255)", "Added property should have been used."); + is((yield getComputedStyleProperty(selector, ":first-line", "background-color")), + "rgb(0, 255, 0)", "Added prop does not apply to pseudo"); +} + +function* testTopRight(inspector, view) { + let { + rules, + element, + elementStyle + } = yield assertPseudoElementRulesNumbers("#topright", inspector, view, { + elementRulesNb: 4, + firstLineRulesNb: 1, + firstLetterRulesNb: 1, + selectionRulesNb: 0 + }); + + 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.firstChild.classList.contains("show-expandable-container"), "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 + }); +} + +function* testBottomLeft(inspector, view) { + yield assertPseudoElementRulesNumbers("#bottomleft", inspector, view, { + elementRulesNb: 4, + firstLineRulesNb: 1, + firstLetterRulesNb: 1, + selectionRulesNb: 0 + }); +} + +function* testParagraph(inspector, view) { + let { + rules, + element, + elementStyle + } = yield assertPseudoElementRulesNumbers("#bottomleft p", inspector, view, { + elementRulesNb: 3, + firstLineRulesNb: 1, + firstLetterRulesNb: 1, + selectionRulesNb: 1 + }); + + let gutters = assertGutters(view); + + let elementFirstLineRule = rules.firstLineRules[0]; + let elementFirstLineRuleView = [].filter.call(view.element.children[1].children, (e) => { + return e._ruleEditor && e._ruleEditor.rule === elementFirstLineRule; + })[0]._ruleEditor; + + is + ( + convertTextPropsToString(elementFirstLineRule.textProps), + "background: blue none repeat scroll 0% 0%", + "Paragraph first-line properties are correct" + ); + + let elementFirstLetterRule = rules.firstLetterRules[0]; + let elementFirstLetterRuleView = [].filter.call(view.element.children[1].children, (e) => { + return e._ruleEditor && e._ruleEditor.rule === elementFirstLetterRule; + })[0]._ruleEditor; + + is + ( + convertTextPropsToString(elementFirstLetterRule.textProps), + "color: red; font-size: 130%", + "Paragraph first-letter properties are correct" + ); + + let elementSelectionRule = rules.selectionRules[0]; + let elementSelectionRuleView = [].filter.call(view.element.children[1].children, (e) => { + return e._ruleEditor && e._ruleEditor.rule === elementSelectionRule; + })[0]._ruleEditor; + + is + ( + convertTextPropsToString(elementSelectionRule.textProps), + "color: white; background: black none repeat scroll 0% 0%", + "Paragraph first-letter properties are correct" + ); +} + +function* testBody(inspector, view) { + let {element, elementStyle} = yield testNode("body", inspector, view); + + let gutters = view.element.querySelectorAll(".theme-gutter"); + 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) { + let element = getNode(selector); + yield selectNode(selector, inspector); + let elementStyle = view._elementStyle; + return {element: element, elementStyle: elementStyle}; +} + +function* assertPseudoElementRulesNumbers(selector, inspector, view, ruleNbs) { + let {element, 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") + }; + + 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"); + + return {rules: rules, element: element, elementStyle: elementStyle}; +} + +function assertGutters(view) { + let gutters = view.element.querySelectorAll(".theme-gutter"); + 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/toolkit/devtools/styleinspector/test/browser_ruleview_pseudo-element_02.js b/toolkit/devtools/styleinspector/test/browser_ruleview_pseudo-element_02.js new file mode 100644 index 000000000..918889816 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_pseudo-element_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"; + +// Test that pseudoelements are displayed correctly in the rule view + +const TEST_URI = TEST_URL_ROOT + "doc_pseudoelement.html"; + +add_task(function*() { + yield addTab(TEST_URI); + let {toolbox, inspector, view} = yield openRuleView(); + + yield testTopLeft(inspector, view); +}); + +function* testTopLeft(inspector, view) { + let node = inspector.markup.walker.frontForRawNode(getNode("#topleft")); + 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/toolkit/devtools/styleinspector/test/browser_ruleview_refresh-on-attribute-change_01.js b/toolkit/devtools/styleinspector/test/browser_ruleview_refresh-on-attribute-change_01.js new file mode 100644 index 000000000..063fc329d --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_refresh-on-attribute-change_01.js @@ -0,0 +1,59 @@ +/* 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 + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,browser_ruleview_refresh-on-attribute-change.js"); + + info("Preparing the test document and node"); + let style = '' + + '#testid {' + + ' background-color: blue;' + + '} ' + + '.testclass {' + + ' background-color: green;' + + '}'; + let styleNode = addStyle(content.document, style); + content.document.body.innerHTML = '<div id="testid" class="testclass">Styled Node</div>'; + let testElement = getNode("#testid"); + let elementStyle = 'margin-top: 1px; padding-top: 5px;' + testElement.setAttribute("style", elementStyle); + + let {toolbox, inspector, view} = 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"); + testElement.setAttribute("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"); + testElement.setAttribute("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.doc.querySelectorAll(".ruleview-selector"); + + 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/toolkit/devtools/styleinspector/test/browser_ruleview_refresh-on-attribute-change_02.js b/toolkit/devtools/styleinspector/test/browser_ruleview_refresh-on-attribute-change_02.js new file mode 100644 index 000000000..26017aac4 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_refresh-on-attribute-change_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"; + +// Test that changing the current element's style attribute refreshes the rule-view + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,browser_ruleview_update.js"); + + content.document.body.innerHTML = '<div id="testid" class="testclass">Styled Node</div>'; + let testElement = getNode("#testid"); + testElement.setAttribute("style", "margin-top: 1px; padding-top: 5px;"); + + let {toolbox, inspector, view} = yield openRuleView(); + yield selectNode("#testid", inspector); + + yield testPropertyChanges(inspector, view, testElement); + yield testPropertyChange0(inspector, view, testElement); + yield testPropertyChange1(inspector, view, testElement); + yield testPropertyChange2(inspector, view, testElement); + yield testPropertyChange3(inspector, view, testElement); + yield testPropertyChange4(inspector, view, testElement); + yield testPropertyChange5(inspector, view, testElement); + yield testPropertyChange6(inspector, view, testElement); +}); + +function* testPropertyChanges(inspector, ruleView, testElement) { + 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", ""); + yield onRefreshed; + + let rule = ruleView._elementStyle.rules[0]; + validateTextProp(rule.textProps[0], false, "margin-top", "1px", "Original margin property active"); +} + +function* testPropertyChange0(inspector, ruleView, testElement) { + yield changeElementStyle(testElement, "margin-top: 1px; padding-top: 5px", inspector); + + 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, testElement) { + info("Now set it back to 5px, the 5px value should be re-enabled."); + yield changeElementStyle(testElement, "margin-top: 5px; padding-top: 5px;", inspector); + + 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, testElement) { + 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(testElement, "margin-top: 15px; padding-top: 5px;", inspector); + + 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, testElement) { + info("Remove the padding-top attribute. Should disable the padding property but not remove it."); + yield changeElementStyle(testElement, "margin-top: 5px;", inspector); + + 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, testElement) { + info("Put the padding-top attribute back in, should re-enable the padding property."); + yield changeElementStyle(testElement, "margin-top: 5px; padding-top: 25px", inspector); + + 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, testElement) { + info("Add an entirely new property"); + yield changeElementStyle(testElement, "margin-top: 5px; padding-top: 25px; padding-left: 20px;", inspector); + + 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, testElement) { + info("Add an entirely new property again"); + yield changeElementStyle(testElement, "background: red url(\"chrome://branding/content/about-logo.png\") repeat scroll 0% 0%", inspector); + + 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", + "#F00 url(\"chrome://branding/content/about-logo.png\") repeat scroll 0% 0%"); +} + +function* changeElementStyle(testElement, style, inspector) { + let onRefreshed = inspector.once("rule-view-refreshed"); + testElement.setAttribute("style", style); + yield onRefreshed; +} + +function validateTextProp(aProp, aEnabled, aName, aValue, aDesc, valueSpanText) { + is(aProp.enabled, aEnabled, aDesc + ": enabled."); + is(aProp.name, aName, aDesc + ": name."); + is(aProp.value, aValue, aDesc + ": value."); + + is(aProp.editor.enable.hasAttribute("checked"), aEnabled, aDesc + ": enabled checkbox."); + is(aProp.editor.nameSpan.textContent, aName, aDesc + ": name span."); + is(aProp.editor.valueSpan.textContent, valueSpanText || aValue, aDesc + ": value span."); +} diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_refresh-on-style-change.js b/toolkit/devtools/styleinspector/test/browser_ruleview_refresh-on-style-change.js new file mode 100644 index 000000000..7813ac927 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_refresh-on-style-change.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 the rule view refreshes when the current node has its style +// changed + +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); + + Services.prefs.setCharPref("devtools.defaultColorUnit", "name"); + + info("Getting the test node"); + let div = getNode("#testdiv"); + + info("Opening the rule view and selecting the test node"); + let {toolbox, inspector, view} = 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"); + div.style.cssText = "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/toolkit/devtools/styleinspector/test/browser_ruleview_select-and-copy-styles.js b/toolkit/devtools/styleinspector/test/browser_ruleview_select-and-copy-styles.js new file mode 100644 index 000000000..cb6d3f912 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_select-and-copy-styles.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 properties can be selected and copied from the rule view + +XPCOMUtils.defineLazyGetter(this, "osString", function() { + return Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS; +}); + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,<p>rule view context menu test</p>"); + + info("Creating the test document"); + content.document.body.innerHTML = '<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">\n' + + '<h1>Some header text</h1>\n' + + '<p id="salutation" style="font-size: 12pt">hi.</p>\n' + + '<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>\n' + + '<p id="closing">more text</p>\n' + + '<p>even more text</p>' + + '</div>'; + content.document.title = "Rule view context menu test"; + + info("Opening the computed view"); + let {toolbox, inspector, view} = yield openRuleView(); + + info("Selecting the test node"); + yield selectNode("div", inspector); + + yield checkCopySelection(view); + yield checkSelectAll(view); +}); + +function checkCopySelection(view) { + info("Testing selection copy"); + + let contentDoc = view.doc; + let prop = contentDoc.querySelector(".ruleview-property"); + let values = contentDoc.querySelectorAll(".ruleview-propertycontainer"); + + let range = contentDoc.createRange(); + range.setStart(prop, 0); + range.setEnd(values[4], 2); + let selection = view.doc.defaultView.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: #000;[\\r\\n]*"; + + return waitForClipboard(() => { + fireCopyEvent(prop); + }, () => { + return checkClipboardData(expectedPattern); + }).then(() => {}, () => { + failedClipboard(expectedPattern); + }); +} + +function checkSelectAll(view) { + info("Testing select-all copy"); + + let contentDoc = view.doc; + let prop = contentDoc.querySelector(".ruleview-property"); + + info("Checking that _SelectAll() then copy returns the correct clipboard value"); + view._onSelectAll(); + let expectedPattern = "[\\r\\n]+" + + "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: #000;[\\r\\n]+" + + "}[\\r\\n]*"; + + return waitForClipboard(() => { + fireCopyEvent(prop); + }, () => { + return checkClipboardData(expectedPattern); + }).then(() => {}, () => { + 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/toolkit/devtools/styleinspector/test/browser_ruleview_selector-highlighter_01.js b/toolkit/devtools/styleinspector/test/browser_ruleview_selector-highlighter_01.js new file mode 100644 index 000000000..03d3f1469 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_selector-highlighter_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"; + +// Test that the selector highlighter is created when hovering over a selector +// in the rule view + +const PAGE_CONTENT = [ + '<style type="text/css">', + ' body, p, td {', + ' background: red;', + ' }', + '</style>', + 'Test the selector highlighter' +].join("\n"); + +let TYPE = "SelectorHighlighter"; + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8," + PAGE_CONTENT); + + let {view: rView} = yield openRuleView(); + let hs = rView.highlighters; + + ok(!hs.highlighters[TYPE], "No highlighter exists in the rule-view (1)"); + ok(!hs.promises[TYPE], "No highlighter is being created in the rule-view (1)"); + + info("Faking a mousemove NOT on a selector"); + let {valueSpan} = getRuleViewProperty(rView, "body, p, td", "background"); + hs._onMouseMove({target: valueSpan}); + ok(!hs.highlighters[TYPE], "No highlighter exists in the rule-view (2)"); + ok(!hs.promises[TYPE], "No highlighter is being created in the rule-view (2)"); + + info("Faking a mousemove on the body selector"); + let selectorContainer = getRuleViewSelector(rView, "body, p, td"); + // The highlighter appears for individual selectors only + let bodySelector = selectorContainer.firstElementChild; + hs._onMouseMove({target: bodySelector}); + ok(hs.promises[TYPE], "The highlighter is being initialized"); + let h = yield hs.promises[TYPE]; + is(h, hs.highlighters[TYPE], "The initialized highlighter is the right one"); +}); diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_selector-highlighter_02.js b/toolkit/devtools/styleinspector/test/browser_ruleview_selector-highlighter_02.js new file mode 100644 index 000000000..3f07cbe34 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_selector-highlighter_02.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 selector highlighter is shown when hovering over a selector +// 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 PAGE_CONTENT = [ + '<style type="text/css">', + ' body {', + ' background: red;', + ' }', + ' p {', + ' color: white;', + ' }', + '</style>', + '<p>Testing the selector highlighter</p>' +].join("\n"); + +const TYPE = "SelectorHighlighter"; + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8," + PAGE_CONTENT); + + let {inspector, view: rView} = 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 + rView.highlighters.promises[TYPE] = { + then: function(cb) { + cb(HighlighterFront); + } + }; + + let selectorSpan = getRuleViewSelector(rView, "body").firstElementChild; + + info("Checking that the HighlighterFront's show/hide methods are called"); + rView.highlighters._onMouseMove({target: selectorSpan}); + ok(HighlighterFront.isShown, "The highlighter is shown"); + rView.highlighters._onMouseLeave(); + ok(!HighlighterFront.isShown, "The highlighter is hidden"); + + info("Checking that the right NodeFront reference and options are passed"); + yield selectNode("p", inspector); + selectorSpan = getRuleViewSelector(rView, "p").firstElementChild; + rView.highlighters._onMouseMove({target: selectorSpan}); + 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); + selectorSpan = getRuleViewSelector(rView, "body").firstElementChild; + rView.highlighters._onMouseMove({target: selectorSpan}); + 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)"); + + info("Checking that the highlighter gets hidden when hovering somewhere else"); + let {valueSpan} = getRuleViewProperty(rView, "body", "background"); + rView.highlighters._onMouseMove({target: valueSpan}); + ok(!HighlighterFront.isShown, "The highlighter is hidden"); +}); diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_style-editor-link.js b/toolkit/devtools/styleinspector/test/browser_ruleview_style-editor-link.js new file mode 100644 index 000000000..559ba0570 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_style-editor-link.js @@ -0,0 +1,163 @@ +/* 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"; + +/////////////////// +// +// 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 = TEST_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>'].join("\n")); + +add_task(function*() { + yield addTab(DOCUMENT_URL); + let {toolbox, inspector, view} = yield openRuleView(); + + info("Select the test node"); + yield selectNode("div", inspector); + + yield testInlineStyle(view, inspector); + yield testFirstInlineStyleSheet(view, toolbox); + yield testSecondInlineStyleSheet(view, toolbox); + yield testExternalStyleSheet(view, toolbox); +}); + +function* testInlineStyle(view, inspector) { + info("Testing inline style"); + + let onWindow = waitForWindow(); + info("Clicking on the first link in the rule-view"); + clickLinkByIndex(view, 0); + + let win = yield onWindow; + + let windowType = win.document.documentElement.getAttribute("windowtype"); + is(windowType, "navigator:view-source", "View source window is open"); + info("Closing window"); + win.close(); +} + +function* testFirstInlineStyleSheet(view, toolbox) { + 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"); + + validateStyleEditorSheet(editor, 0); +} + +function* testSecondInlineStyleSheet(view, toolbox) { + 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"); + validateStyleEditorSheet(editor, 1); +} + +function* testExternalStyleSheet(view, toolbox) { + 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"); + validateStyleEditorSheet(editor, 2); +} + +function validateStyleEditorSheet(editor, expectedSheetIndex) { + info("validating style editor stylesheet"); + is(editor.styleSheet.styleSheetIndex, expectedSheetIndex, + "loaded stylesheet index matches document stylesheet"); + + let sheet = content.document.styleSheets[expectedSheetIndex]; + is(editor.styleSheet.href, sheet.href, "loaded stylesheet href matches document stylesheet"); +} + +function testRuleViewLinkLabel(view) { + let link = getRuleViewLinkByIndex(view, 2); + let labelElem = link.querySelector(".source-link-label"); + let value = labelElem.getAttribute("value"); + let tooltipText = labelElem.getAttribute("tooltiptext"); + + 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 clickLinkByIndex(view, index) { + let link = getRuleViewLinkByIndex(view, index); + link.scrollIntoView(); + link.click(); +} diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_urls-clickable.js b/toolkit/devtools/styleinspector/test/browser_ruleview_urls-clickable.js new file mode 100644 index 000000000..e84df40fa --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_urls-clickable.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 to make sure that URLs are clickable in the rule view + +const TEST_URI = TEST_URL_ROOT + "doc_urls_clickable.html"; +const TEST_IMAGE = TEST_URL_ROOT + "doc_test_image.png"; +const BASE_64_URL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="; + +add_task(function*() { + yield addTab(TEST_URI); + let {toolbox, 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.doc.querySelector(".ruleview-propertycontainer a"); + ok(relativeLink, "Link exists for relative1 node"); + is(relativeLink.getAttribute("href"), TEST_IMAGE, "href matches"); + + yield selectNode(relative2, inspector); + relativeLink = ruleView.doc.querySelector(".ruleview-propertycontainer a"); + ok(relativeLink, "Link exists for relative2 node"); + is(relativeLink.getAttribute("href"), TEST_IMAGE, "href matches"); + + yield selectNode(absolute, inspector); + let absoluteLink = ruleView.doc.querySelector(".ruleview-propertycontainer a"); + ok(absoluteLink, "Link exists for absolute node"); + is(absoluteLink.getAttribute("href"), TEST_IMAGE, "href matches"); + + yield selectNode(inline, inspector); + let inlineLink = ruleView.doc.querySelector(".ruleview-propertycontainer a"); + ok(inlineLink, "Link exists for inline node"); + is(inlineLink.getAttribute("href"), TEST_IMAGE, "href matches"); + + yield selectNode(base64, inspector); + let base64Link = ruleView.doc.querySelector(".ruleview-propertycontainer a"); + ok(base64Link, "Link exists for base64 node"); + is(base64Link.getAttribute("href"), BASE_64_URL, "href matches"); + + yield selectNode(inlineresolved, inspector); + let inlineResolvedLink = ruleView.doc.querySelector(".ruleview-propertycontainer a"); + ok(inlineResolvedLink, "Link exists for style tag node"); + is(inlineResolvedLink.getAttribute("href"), TEST_IMAGE, "href matches"); + + yield selectNode(noimage, inspector); + let noimageLink = ruleView.doc.querySelector(".ruleview-propertycontainer a"); + ok(!noimageLink, "There is no link for the node with no background image"); +} diff --git a/toolkit/devtools/styleinspector/test/browser_ruleview_user-agent-styles-uneditable.js b/toolkit/devtools/styleinspector/test/browser_ruleview_user-agent-styles-uneditable.js new file mode 100644 index 000000000..f898bf607 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_user-agent-styles-uneditable.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"; + +// Check that user agent styles are never editable via +// the UI + +let PREF_UA_STYLES = "devtools.inspector.showUserAgentStyles"; +const TEST_URI = "data:text/html;charset=utf-8," + + "<blockquote type=cite>" + + " <pre _moz_quote=true>" + + " inspect <a href='foo' style='color:orange'>user agent</a> styles" + + " </pre>" + + "</blockquote>"; + +add_task(function*() { + info ("Starting the test with the pref set to true before toolbox is opened"); + Services.prefs.setBoolPref(PREF_UA_STYLES, true); + + info ("Opening the testcase and toolbox") + yield addTab(TEST_URI); + let {toolbox, 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"); + + ok (!rule.textProps[0].editor.nameSpan._editable, "nameSpan is not editable"); + ok (!rule.textProps[0].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/toolkit/devtools/styleinspector/test/browser_ruleview_user-agent-styles.js b/toolkit/devtools/styleinspector/test/browser_ruleview_user-agent-styles.js new file mode 100644 index 000000000..e4142569c --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_user-agent-styles.js @@ -0,0 +1,138 @@ +/* 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. + +let PREF_UA_STYLES = "devtools.inspector.showUserAgentStyles"; +const { PrefObserver } = devtools.require("devtools/styleeditor/utils"); +const TEST_URI = "data:text/html;charset=utf-8," + + "<blockquote type=cite>" + + " <pre _moz_quote=true>" + + " inspect <a href='foo' style='color:orange'>user agent</a> styles" + + " </pre>" + + "</blockquote>"; + +add_task(function*() { + info ("Starting the test with the pref set to true before toolbox is opened"); + yield setUserAgentStylesPref(true); + + info ("Opening the testcase and toolbox") + yield addTab(TEST_URI); + let {toolbox, 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 = promise.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"); + + yield selectNode("blockquote", inspector); + yield compareAppliedStylesWithUI(inspector, view, "ua"); + + let userRules = view._elementStyle.rules.filter(rule=>rule.editor.isEditable); + let uaRules = view._elementStyle.rules.filter(rule=>!rule.editor.isEditable); + is (userRules.length, 1, "Correct number of user rules"); + ok (uaRules.length > 0, "Has UA rules"); + + yield selectNode("pre", 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, 1, "Correct number of user rules"); + ok (uaRules.length > 0, "Has UA rules"); + + yield selectNode("a", 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, 1, "Correct number of user rules"); + + ok (userRules.some(rule=> rule.matchedSelectors.length === 0), + "There is an inline style for element in user styles"); + + ok (uaRules.some(rule=> rule.matchedSelectors.indexOf(":-moz-any-link")), + "There is a rule for :-moz-any-link"); + ok (uaRules.some(rule=> rule.matchedSelectors.indexOf("*|*:link")), + "There is a rule for *|*:link"); + ok (!uaRules.some(rule=> rule.matchedSelectors.length === 0), + "No inline styles for ua styles"); +} + +function* userAgentStylesNotVisible(inspector, view) { + info ("Making sure that user agent styles are not currently visible"); + + yield selectNode("blockquote", inspector); + yield compareAppliedStylesWithUI(inspector, view); + + let userRules = view._elementStyle.rules.filter(rule=>rule.editor.isEditable); + let uaRules = view._elementStyle.rules.filter(rule=>!rule.editor.isEditable); + is (userRules.length, 1, "Correct number of user rules"); + is (uaRules.length, 0, "No UA rules"); + + yield selectNode("pre", 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, 1, "Correct number of user rules"); + is (uaRules.length, 0, "No UA rules"); + + yield selectNode("a", 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, 1, "Correct number of user rules"); + is (uaRules.length, 0, "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 + }); + + let elementStyle = view._elementStyle; + is(elementStyle.rules.length, entries.length, "Should have correct number of rules (" + entries.length + ")"); + + 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/toolkit/devtools/styleinspector/test/browser_ruleview_user-property-reset.js b/toolkit/devtools/styleinspector/test/browser_ruleview_user-property-reset.js new file mode 100644 index 000000000..cbc8ca862 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_ruleview_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 + +let TEST_PAGE = [ + "data:text/html;charset=utf-8,", + "<p id='id1' style='width:200px;'>element 1</p>", + "<p id='id2' style='width:100px;'>element 2</p>" +].join(""); + +add_task(function*() { + yield addTab(TEST_PAGE); + let {toolbox, inspector, view} = 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); + + 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.doc.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(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 onMutation = inspector.once("markupmutation"); + EventUtils.sendKey("return"); + yield onBlur; + yield onMutation; + + info("Escaping out of the new property field that has been created after the value was edited"); + let onNewFieldBlur = once(ruleView.doc.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 ++; + } +} + +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); +} + +function reloadPage(inspector) { + let onNewRoot = inspector.once("new-root"); + content.location.reload(); + return onNewRoot.then(inspector.markup._waitForChildren); +} diff --git a/toolkit/devtools/styleinspector/test/browser_styleinspector_context-menu-copy-color_01.js b/toolkit/devtools/styleinspector/test/browser_styleinspector_context-menu-copy-color_01.js new file mode 100644 index 000000000..998d018a4 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_styleinspector_context-menu-copy-color_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"; + +// Test "Copy color" item of the context menu #1: Test _isColorPopup. + +const TEST_COLOR = "#123ABC"; +const COLOR_SELECTOR = "span[data-color]"; + +add_task(function* () { + const TEST_DOC = '<html> \ + <body> \ + <div style="color: ' + TEST_COLOR + '; \ + margin: 0px; \ + background: ' + TEST_COLOR + ';"> \ + Test "Copy color" context menu option \ + </div> \ + </body> \ + </html>'; + + const TEST_CASES = [ + { + viewName: "RuleView", + initializer: openRuleView + }, + { + viewName: "ComputedView", + initializer: openComputedView + } + ]; + + yield addTab("data:text/html;charset=utf8," + encodeURIComponent(TEST_DOC)); + + for (let test of TEST_CASES) { + yield testView(test); + } +}); + +function* testView({viewName, initializer}) { + info("Testing " + viewName); + + let {inspector, view} = yield initializer(); + yield selectNode("div", inspector); + + testIsColorValueNode(view); + testIsColorPopupOnAllNodes(view); + yield clearCurrentNodeSelection(inspector); +} + +/** + * A function testing that isColorValueNode correctly detects nodes part of + * color values. + */ +function testIsColorValueNode(view) { + info("Testing that child nodes of color nodes are detected."); + let root = rootElement(view); + let colorNode = root.querySelector(COLOR_SELECTOR); + + ok(colorNode, "Color node found"); + for (let node of iterateNodes(colorNode)) { + ok(isColorValueNode(node), "Node is part of color value."); + } +} + +/** + * A function testing that _isColorPopup returns a correct value for all nodes + * in the view. + */ +function testIsColorPopupOnAllNodes(view) { + let root = rootElement(view); + for (let node of iterateNodes(root)) { + testIsColorPopupOnNode(view, node); + } +} + +/** + * Test result of _isColorPopup with given node. + * @param object view + * A CSSRuleView or CssHtmlTree instance. + * @param Node node + * A node to check. + */ +function testIsColorPopupOnNode(view, node) { + info("Testing node " + node); + if (view.doc) { + view.doc.popupNode = node; + } + else { + view.popupNode = node; + } + view._colorToCopy = ""; + + let result = view._isColorPopup(); + let correct = isColorValueNode(node); + + is(result, correct, "_isColorPopup returned the expected value " + correct); + is(view._colorToCopy, (correct) ? TEST_COLOR : "", + "_colorToCopy was set to the expected value"); +} + +/** + * Check if a node is part of color value i.e. it has parent with a 'data-color' + * attribute. + */ +function isColorValueNode(node) { + let container = (node.nodeType == node.TEXT_NODE) ? + node.parentElement : node; + + let isColorNode = el => el.dataset && "color" in el.dataset; + + while (!isColorNode(container)) { + container = container.parentNode; + if (!container) { + info("No color. Node is not part of color value."); + return false; + } + } + + info("Found a color. Node is part of color value."); + + return true; +} + +/** + * A generator that iterates recursively trough all child nodes of baseNode. + */ +function* iterateNodes(baseNode) { + yield baseNode; + + for (let child of baseNode.childNodes) { + yield* iterateNodes(child); + } +} + +/** + * Returns the root element for the given view, rule or computed. + */ +let rootElement = view => (view.element) ? view.element : view.styleDocument; diff --git a/toolkit/devtools/styleinspector/test/browser_styleinspector_context-menu-copy-color_02.js b/toolkit/devtools/styleinspector/test/browser_styleinspector_context-menu-copy-color_02.js new file mode 100644 index 000000000..2ef90a648 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_styleinspector_context-menu-copy-color_02.js @@ -0,0 +1,99 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test "Copy color" item of the context menu #2: Test that correct color is +// copied if the color changes. + +const TEST_COLOR = "#123ABC"; + +add_task(function* () { + const PAGE_CONTENT = [ + '<style type="text/css">', + ' div {', + ' color: ' + TEST_COLOR + ';', + ' }', + '</style>', + '<div>Testing the color picker tooltip!</div>' + ].join("\n"); + + yield addTab("data:text/html;charset=utf8,Test context menu Copy color"); + content.document.body.innerHTML = PAGE_CONTENT; + + let {inspector, view} = yield openRuleView(); + yield testCopyToClipboard(inspector, view); + yield testManualEdit(inspector, view); + yield testColorPickerEdit(inspector, view); +}); + +function* testCopyToClipboard(inspector, view) { + info("Testing that color is copied to clipboard"); + + yield selectNode("div", inspector); + + let win = view.doc.defaultView; + let element = getRuleViewProperty(view, "div", "color").valueSpan + .querySelector(".ruleview-colorswatch"); + + let popup = once(view._contextmenu, "popupshown"); + EventUtils.synthesizeMouseAtCenter(element, {button: 2, type: "contextmenu"}, win); + yield popup; + + ok(!view.menuitemCopyColor.hidden, "Copy color is visible"); + + yield waitForClipboard(() => view.menuitemCopyColor.click(), TEST_COLOR); + view._contextmenu.hidePopup(); +} + +function* testManualEdit(inspector, view) { + info("Testing manually edited colors"); + yield selectNode("div", inspector); + + let {valueSpan} = getRuleViewProperty(view, "div", "color"); + + let newColor = "#C9184E" + let editor = yield focusEditableField(valueSpan); + + info("Typing new value"); + let input = editor.input; + let onBlur = once(input, "blur"); + for (let ch of newColor + ";"){ + EventUtils.sendChar(ch, view.doc.defaultView); + } + + yield onBlur; + yield wait(1); + + let colorValue = getRuleViewProperty(view, "div", "color").valueSpan.firstChild; + is(colorValue.dataset.color, newColor, "data-color was updated"); + + view.doc.popupNode = colorValue; + view._isColorPopup(); + + is(view._colorToCopy, newColor, "_colorToCopy has the new value"); +} + +function* testColorPickerEdit(inspector, view) { + info("Testing colors edited via color picker"); + yield selectNode("div", inspector); + + let swatch = getRuleViewProperty(view, "div", "color").valueSpan + .querySelector(".ruleview-colorswatch"); + + info("Opening the color picker"); + let picker = view.tooltips.colorPicker; + let onShown = picker.tooltip.once("shown"); + swatch.click(); + yield onShown; + + let rgbaColor = [83, 183, 89, 1]; + let rgbaColorText = "rgba(83, 183, 89, 1)"; + yield simulateColorPickerChange(picker, rgbaColor); + + is(swatch.parentNode.dataset.color, rgbaColorText, "data-color was updated"); + view.doc.popupNode = swatch; + view._isColorPopup(); + + is(view._colorToCopy, rgbaColorText, "_colorToCopy has the new value"); +} diff --git a/toolkit/devtools/styleinspector/test/browser_styleinspector_csslogic-content-stylesheets.js b/toolkit/devtools/styleinspector/test/browser_styleinspector_csslogic-content-stylesheets.js new file mode 100644 index 000000000..4b82c166b --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_styleinspector_csslogic-content-stylesheets.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"; + +// Check stylesheets on HMTL and XUL document + +// FIXME: this test opens the devtools for nothing, it should be changed into a +// toolkit/devtools/server/tests/mochitest/test_css-logic-...something...html test + +const TEST_URI_HTML = TEST_URL_ROOT + "doc_content_stylesheet.html"; +const TEST_URI_XUL = TEST_URL_ROOT + "doc_content_stylesheet.xul"; +const XUL_URI = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService) + .newURI(TEST_URI_XUL, null, null); +const XUL_PRINCIPAL = Components.classes["@mozilla.org/scriptsecuritymanager;1"] + .getService(Ci.nsIScriptSecurityManager) + .getNoAppCodebasePrincipal(XUL_URI); + +add_task(function*() { + info("Checking stylesheets on HTML document"); + yield addTab(TEST_URI_HTML); + let target = getNode("#target"); + + let {toolbox, inspector, view} = yield openRuleView(); + yield selectNode("#target", inspector); + + info("Checking stylesheets"); + yield checkSheets(target); + + info("Checking stylesheets on XUL document"); + info("Allowing XUL content"); + allowXUL(); + yield addTab(TEST_URI_XUL); + + ({toolbox, inspector, view} = yield openRuleView()); + target = getNode("#target"); + yield selectNode("#target", inspector); + + yield checkSheets(target); + info("Disallowing XUL content"); + disallowXUL(); +}); + +function allowXUL() { + Cc["@mozilla.org/permissionmanager;1"].getService(Ci.nsIPermissionManager) + .addFromPrincipal(XUL_PRINCIPAL, 'allowXULXBL', Ci.nsIPermissionManager.ALLOW_ACTION); +} + +function disallowXUL() { + Cc["@mozilla.org/permissionmanager;1"].getService(Ci.nsIPermissionManager) + .addFromPrincipal(XUL_PRINCIPAL, 'allowXULXBL', Ci.nsIPermissionManager.DENY_ACTION); +} + +function* checkSheets(target) { + let sheets = yield executeInContent("Test:GetStyleSheetsInfoForNode", {}, {target}); + + for (let sheet of sheets) { + if (!sheet.href || + /doc_content_stylesheet_/.test(sheet.href)) { + ok(sheet.isContentSheet, sheet.href + " identified as content stylesheet"); + } else { + ok(!sheet.isContentSheet, sheet.href + " identified as non-content stylesheet"); + } + } +} diff --git a/toolkit/devtools/styleinspector/test/browser_styleinspector_output-parser.js b/toolkit/devtools/styleinspector/test/browser_styleinspector_output-parser.js new file mode 100644 index 000000000..72f25d8e5 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_styleinspector_output-parser.js @@ -0,0 +1,312 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test expected outputs of the output-parser's parseCssProperty function. + +// This is more of a unit test than a mochitest-browser test, but can't be +// tested with an xpcshell test as the output-parser requires the DOM to work. + +let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); +let {OutputParser} = devtools.require("devtools/output-parser"); + +const COLOR_CLASS = "color-class"; +const URL_CLASS = "url-class"; +const CUBIC_BEZIER_CLASS = "bezier-class"; + +function test() { + function countAll(fragment) { + return fragment.querySelectorAll("*").length; + } + function countColors(fragment) { + return fragment.querySelectorAll("." + COLOR_CLASS).length; + } + function countUrls(fragment) { + return fragment.querySelectorAll("." + URL_CLASS).length; + } + function countCubicBeziers(fragment) { + return fragment.querySelectorAll("." + CUBIC_BEZIER_CLASS).length; + } + function getColor(fragment, index) { + return fragment.querySelectorAll("." + COLOR_CLASS)[index||0].textContent; + } + function getUrl(fragment, index) { + return fragment.querySelectorAll("." + URL_CLASS)[index||0].textContent; + } + function getCubicBezier(fragment, index) { + return fragment.querySelectorAll("." + CUBIC_BEZIER_CLASS)[index||0].textContent; + } + + let testData = [ + { + name: "width", + value: "100%", + test: fragment => { + is(countAll(fragment), 0); + is(fragment.textContent, "100%"); + } + }, + { + name: "width", + value: "blue", + test: fragment => { + is(countAll(fragment), 0); + } + }, + { + name: "content", + value: "'red url(test.png) repeat top left'", + test: fragment => { + is(countAll(fragment), 0); + } + }, + { + name: "content", + value: "\"blue\"", + test: fragment => { + is(countAll(fragment), 0); + } + }, + { + name: "margin-left", + value: "url(something.jpg)", + test: fragment => { + is(countAll(fragment), 0); + } + }, + { + name: "background-color", + value: "transparent", + test: fragment => { + is(countAll(fragment), 2); + is(countColors(fragment), 1); + is(fragment.textContent, "transparent"); + } + }, + { + name: "color", + value: "red", + test: fragment => { + is(countColors(fragment), 1); + is(fragment.textContent, "red"); + } + }, + { + name: "color", + value: "#F06", + test: fragment => { + is(countColors(fragment), 1); + is(fragment.textContent, "#F06"); + } + }, + { + name: "border", + value: "80em dotted pink", + test: fragment => { + is(countAll(fragment), 2); + is(countColors(fragment), 1); + is(getColor(fragment), "pink"); + } + }, + { + name: "color", + value: "red !important", + test: fragment => { + is(countColors(fragment), 1); + is(fragment.textContent, "red !important"); + } + }, + { + name: "background", + value: "red url(test.png) repeat top left", + test: fragment => { + is(countColors(fragment), 1); + is(countUrls(fragment), 1); + is(getColor(fragment), "red"); + is(getUrl(fragment), "test.png"); + is(countAll(fragment), 3); + } + }, + { + name: "background", + value: "blue url(test.png) repeat top left !important", + test: fragment => { + is(countColors(fragment), 1); + is(countUrls(fragment), 1); + is(getColor(fragment), "blue"); + is(getUrl(fragment), "test.png"); + is(countAll(fragment), 3); + } + }, + { + name: "list-style-image", + value: "url(\"images/arrow.gif\")", + test: fragment => { + is(countAll(fragment), 1); + is(getUrl(fragment), "images/arrow.gif"); + } + }, + { + name: "list-style-image", + value: "url(\"images/arrow.gif\")!important", + test: fragment => { + is(countAll(fragment), 1); + is(getUrl(fragment), "images/arrow.gif"); + is(fragment.textContent, "url(\"images/arrow.gif\")!important"); + } + }, + { + name: "-moz-binding", + value: "url(http://somesite.com/path/to/binding.xml#someid)", + test: fragment => { + is(countAll(fragment), 1); + is(countUrls(fragment), 1); + is(getUrl(fragment), "http://somesite.com/path/to/binding.xml#someid"); + } + }, + { + name: "background", + value: "linear-gradient(to right, rgba(183,222,237,1) 0%, rgba(33,180,226,1) 30%, rgba(31,170,217,.5) 44%, #F06 75%, red 100%)", + test: fragment => { + is(countAll(fragment), 10); + let allSwatches = fragment.querySelectorAll("." + COLOR_CLASS); + is(allSwatches.length, 5); + is(allSwatches[0].textContent, "rgba(183,222,237,1)"); + is(allSwatches[1].textContent, "rgba(33,180,226,1)"); + is(allSwatches[2].textContent, "rgba(31,170,217,.5)"); + is(allSwatches[3].textContent, "#F06"); + is(allSwatches[4].textContent, "red"); + } + }, + { + name: "background", + value: "-moz-radial-gradient(center 45deg, circle closest-side, orange 0%, red 100%)", + test: fragment => { + is(countAll(fragment), 4); + let allSwatches = fragment.querySelectorAll("." + COLOR_CLASS); + is(allSwatches.length, 2); + is(allSwatches[0].textContent, "orange"); + is(allSwatches[1].textContent, "red"); + } + }, + { + name: "background", + value: "white url(http://test.com/wow_such_image.png) no-repeat top left", + test: fragment => { + is(countAll(fragment), 3); + is(countUrls(fragment), 1); + is(countColors(fragment), 1); + } + }, + { + name: "background", + value: "url(\"http://test.com/wow_such_(oh-noes)image.png?testid=1&color=red#w00t\")", + test: fragment => { + is(countAll(fragment), 1); + is(getUrl(fragment), "http://test.com/wow_such_(oh-noes)image.png?testid=1&color=red#w00t"); + } + }, + { + name: "background-image", + value: "url(this-is-an-incredible-image.jpeg)", + test: fragment => { + is(countAll(fragment), 1); + is(getUrl(fragment), "this-is-an-incredible-image.jpeg"); + } + }, + { + name: "background", + value: "red url( \"http://wow.com/cool/../../../you're(doingit)wrong\" ) repeat center", + test: fragment => { + is(countAll(fragment), 3); + is(countColors(fragment), 1); + is(getUrl(fragment), "http://wow.com/cool/../../../you're(doingit)wrong"); + } + }, + { + name: "background-image", + value: "url(../../../look/at/this/folder/structure/../../red.blue.green.svg )", + test: fragment => { + is(countAll(fragment), 1); + is(getUrl(fragment), "../../../look/at/this/folder/structure/../../red.blue.green.svg"); + } + }, + { + name: "transition-timing-function", + value: "linear", + test: fragment => { + is(countCubicBeziers(fragment), 1); + is(getCubicBezier(fragment), "linear"); + } + }, + { + name: "animation-timing-function", + value: "ease-in-out", + test: fragment => { + is(countCubicBeziers(fragment), 1); + is(getCubicBezier(fragment), "ease-in-out"); + } + }, + { + name: "animation-timing-function", + value: "cubic-bezier(.1, 0.55, .9, -3.45)", + test: fragment => { + is(countCubicBeziers(fragment), 1); + is(getCubicBezier(fragment), "cubic-bezier(.1, 0.55, .9, -3.45)"); + } + }, + { + name: "animation", + value: "move 3s cubic-bezier(.1, 0.55, .9, -3.45)", + test: fragment => { + is(countCubicBeziers(fragment), 1); + is(getCubicBezier(fragment), "cubic-bezier(.1, 0.55, .9, -3.45)"); + } + }, + { + name: "transition", + value: "top 1s ease-in", + test: fragment => { + is(countCubicBeziers(fragment), 1); + is(getCubicBezier(fragment), "ease-in"); + } + }, + { + name: "transition", + value: "top 3s steps(4, end)", + test: fragment => { + is(countAll(fragment), 0); + } + }, + { + name: "transition", + value: "top 3s step-start", + test: fragment => { + is(countAll(fragment), 0); + } + }, + { + name: "transition", + value: "top 3s step-end", + test: fragment => { + is(countAll(fragment), 0); + } + } + ]; + + let parser = new OutputParser(); + for (let i = 0; i < testData.length; i ++) { + let data = testData[i]; + info("Output-parser test data " + i + ". {" + data.name + " : " + data.value + ";}"); + data.test(parser.parseCssProperty(data.name, data.value, { + colorClass: COLOR_CLASS, + urlClass: URL_CLASS, + bezierClass: CUBIC_BEZIER_CLASS, + defaultColorType: false + })); + } + + finish(); +} diff --git a/toolkit/devtools/styleinspector/test/browser_styleinspector_refresh_when_active.js b/toolkit/devtools/styleinspector/test/browser_styleinspector_refresh_when_active.js new file mode 100644 index 000000000..5a2205ef5 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_styleinspector_refresh_when_active.js @@ -0,0 +1,43 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the style-inspector views only refresh when they are active. + +const TEST_URL = 'data:text/html;charset=utf-8,' + + '<div id="one" style="color:red;">one</div>' + + '<div id="two" style="color:blue;">two</div>'; + +add_task(function*() { + yield addTab(TEST_URL); + + info("Opening the rule-view and selecting test node one"); + let {inspector, view: rView} = yield openRuleView(); + yield selectNode("#one", inspector); + + is(getRuleViewPropertyValue(rView, "element", "color"), "#F00", + "The rule-view shows the properties for test node one"); + + let cView = inspector.sidebar.getWindowForTab("computedview")["computedview"].view; + let prop = getComputedViewProperty(cView, "color"); + ok(!prop, "The computed-view doesn't show the properties for test node one"); + + info("Switching to the computed-view"); + let onComputedViewReady = inspector.once("computed-view-refreshed"); + yield openComputedView(); + yield onComputedViewReady; + + ok(getComputedViewPropertyValue(cView, "color"), "#F00", + "The computed-view shows the properties for test node one"); + + info("Selecting test node two"); + yield selectNode("#two", inspector); + + ok(getComputedViewPropertyValue(cView, "color"), "#00F", + "The computed-view shows the properties for test node two"); + + is(getRuleViewPropertyValue(rView, "element", "color"), "#F00", + "The rule-view doesn't the properties for test node two"); +});
\ No newline at end of file diff --git a/toolkit/devtools/styleinspector/test/browser_styleinspector_tooltip-background-image.js b/toolkit/devtools/styleinspector/test/browser_styleinspector_tooltip-background-image.js new file mode 100644 index 000000000..7a46bea58 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_styleinspector_tooltip-background-image.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"; + +// Test that background-image URLs have image preview tooltips in the rule-view +// and computed-view + +const PAGE_CONTENT = [ + '<style type="text/css">', + ' body {', + ' padding: 1em;', + ' background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAADI5JREFUeNrsWwuQFNUVPf1m5z87szv7HWSWj8CigBFMEFZKiQsB1PgJwUAZg1HBpIQsKmokEhNjWUnFVPnDWBT+KolJYbRMoqUVq0yCClpqiX8sCchPWFwVlt2db7+X93pez7zu6Vn2NxsVWh8987p7pu+9555z7+tZjTGGY3kjOMa34w447oBjfKsY7i/UNM3Y8eFSAkD50Plgw03K5P9gvGv7U5ieeR3PszeREiPNX3/0DL4hjslzhm8THh+OITfXk3dhiv4GDtGPVzCaeJmPLYzuu5qJuWfuw2QTlcN1X9pwQU7LhdZ/ZAseD45cOh9hHvDkc/yAF/DNhdb5Mrr3PvBMaAYW8fMSIi2G497IMEK/YutGtAYr6+ej+nxu/NN8Ks3N7AR6HgcLz0Eg1Ljg1UcxZzi5qewIkMYLRweTr2Kzp+nmyXAd5pS3XQDd+N/4h4zgu9FI7brlXf90nMEnuwQxlvv+hosE3TuexmWeysmT4W+WxkMaLzf9Y8ATgjcUn7T9H1gqrpFq8eV1gMn6t16NhngjfoX6q4DUP032Rd4LJgpSLwJ1yzFqBG69eRkah0MVyo0Acfe+yy9AG4nMiYCkeM53KKFXncBLAXqEm+wCqZwaueq7WCmuLTcKSJmj737ol2hurA9eq9VdyiO8yWa3NNyog+SB5CZodSsQq/dfu34tJpYbBaTMzvVddDZu16q5smXf4G8zEvqm4cyaAmJPuTJk3oJWdS4WzcVtfMZbThSQckb/pYfRGgo3zNOqZnEHbJPGK4abaDCQIIsT8V/qTaBqHkLh6LzXH8XZQhbLhYKyyCC/WeHYcNdmvOgfe8skzbWL270/T3wf7tSx/lGCbTu8xlzzmCSWLc5iwmgikcCHi3Mga0Ry913vBFvQwg90l6M4ImWKfsWOp7DSWxmfpPlCFuPFfsNfKrCnPYpQKIRgqBK7D0SxYaNHwkEiJMtl0ReDp3Lc5D3PGoTo/sKngCl7a5chFqvBatKwjBd7WwqIlzB/78NcoUcp5VSgGxm+7b8eqQRGnHMO634epO4S1EZww09/iFg5UmGoESDuznP1xVhTUX1WWHPzjpd25wyH0hRxI3LGM75nxmuNEEUVpAN0XgxmPoKralakbQnWlIMQyVBD/w+3orkq4lvualjKyWwzt4MaxqspQHVhPOWG64bxYuhZXSFGWhipbSDVragOu5Y9eAsmDDUKyBA703vemVhHoueD6e9wAzJK1WfmN0Umk5GGM4kEMZcuIECqgjm0nldAqmbjwtm4VxZH5AvlADP6mx9Eqy9Q0+KqW8Ch+47FaMMYmnNGfY1iPMshoC6qFxme4wQ+0p+ARE6H3+9veWEDWgUhDhUKyFARn4jM5BNxT0XsMg7bfymGK1ov3wtjDfhL4w0HVGUVBEjDaaE+QNdrcNWch1PG4W6xrjBUXECGivg++Cva3JUT4iQUz3V2RsSVaKLwOuDT89A3HdBQoxhNC+fnVm74ual2EG893P6G+PuP4SfiO4cCBWQooL9qCWKNXPbcI37Aa/lnlZxXRt4RFONGwSDCPAHqOuqjWct1QiEMw5mChM5X4K47FyNqcd3aK9AwFH0CGYLoe1ctxk2eWi57rg5JfGp9rzC6ggCdFlAgHBDw5Yxlcg6G8SyHCjMlsgmDD9zhSeHlF+JnAgWDTQUy2NxfdwOao1UVV3pi3+bE97YSbWpLAbn6zefHNQkp1PMpIBwwvslKgIYTKM2nEpNzrGcH3FXTEal0L38kJ4uDQgEZbO4vnI173LXf5NHZaiUxtaCxyZuo/rK6LpUg54yg3zTWRAArvDcRIPZ6BqzrQ1REpmL+DNw32OKIDCb3X1qPVn8wNNMT4w2bvs+q4bAZrqBh2skaL3yyhhIIZ4i6oHkUK0RckcB8GigEyRIH4A6Mgc8fatl0/+BkkQxC9gIT4ljna1rIZW9rEdNbjJcNjsnoYj7LHWCUwpITzEgzRQKZ3XAFHbTzA3hrz8TEUUZxFBhoKpABQt/97p+w0hMZG68I8R6FtlsJT3FELndZntjM+VMnylKYq8GJI3UZaRMpquGSGFVOEfv0YZBMNzz+uvjbfzS6xQERIhlI9FcvQWNdFVb7x1zCb+QNK8vb9NsiifmI5hBgVoOCBC1sb0ab5RomqENxLO3eA1/0NDRU47q2RQNbRCUDIb7lF2CNL3ZGxEV4n08TVvZWYG4pZyV0zUdS45tyCBByOHWiyvZmxFXDCyRo1ge5+Sy0TA+8lWMiP/6O0S32exGV9Jf4fr8azdUR3zL/CZz4MtvzdX5uOYs6NDOmpkuj5Huh+7qUQSYl0ThHzw0YQzcGo6bhzEqoYq5rN3yRiYiG3Vfe2Ybm/qKA9NNZ3nNm4F7/yDkg9AN+U1mHiBcXP8zuDN76jj8hg1QyiWQigalj02BJPhK8I0zxijAjhp5zhlpLUDvS+BCy2HMAvvB4XDgL9/SXC0g/ou/5+6/xLX8w0uJrOIkXfPvyhY0F6gr7M8H0KWFYikcqAXakB+xwD9CdREBLoau7Gz3cAdSIdLFxFtJTCqRChSjnutvhDcREtzjz2Tswtz+yeNRFUeXZXtWux7C1fuoVcbd3J//ipDX3uZZDLGrwweS+UBLL5TDliVBnF8P7H+XI8aRRGsIBJg/Zlslt1+W+D1JWoSyi+kD9jfhs78t7mhZhSl+fLfY1Bdyv3I8V/qpY3B1McgN7ZFT5/vNO0I5DPLLdPBIJA8qc4h2I0QplYfDpJwHT+aj0246r5S8rToG8OjCle8wk4OLvvYGa+Ovr84uo2qBSwJS9G5egoZFLTfiEqWDtbwGfHgKOdPHcS+ai7XDzMPW/FJRLGGcxnBbK4YJC2K+h+T6Bdu5CqHqCWERd3bawb7JI+iJ735+LNaHaprBLLHBm08U3XxShEsdt+f3eTh3v7aC95Dct4RCWL5OZWh/oXBZThxAIxyOXLzBk8aiEWJID8rK3CpPOmeHaGpvCS+7EHv5FujVHUSJPLXvIFeHcNc+9xrB2gws9KZdxuLFax/WLM5gzzSm/lTXF/OdAcapyvjxPqxqHjr2v4ckX2bS2dRBrc5lSdpKjEJ9/9tdwX2WMd53ZQ2IVo3RES+UwVSpCPvYepNx4gmTGDUKIMQ4eduPnD7mx9xOn/KZKOlFbStjONxHTtR+BYAPmnoZ1Zp8wkBRwP/EL3u0F/C2hGl7vpz7vW37T3vP7if8wroKuoh8ribknX9BK5rcF+mo1qKaKyRPJTgTDjbzY8szcuLb3bpH00u35T47j7prRpwDJTxzyG0dHgxPp5bPG8VdkpfPbUg3SgoOo2mwVukb98D5EqpswZTTulCggTk4gpYhv0++wIhCJxr0+Hq1sondis0SE2oxQe3qWXwWyO4DSQg9gJ8Iiw1VFcGqXxet0N9xE4ygIxv/9W6wo9WyROEX/R+eiobYSq2vHTOR631Eiv2lRfh9dvxkumkXh92Qsx8XrAJ+7YGbWuhxOi/U+31NQmzyqNYG8N/3wfo6CRtRHcN01FzkvojohwLu0VVvDa56IS/xcj2b7nN+O+m0jqpE1wMPXZxAN9iCVThtDvH7gmiRGRpU8Lspv1Uhq4wIVdQoyuGSLNYPKUCS8+CzNURbzMmjK3i8u0U793lmuV0ef9nWQ5MGC/DiUqEUSaCtXna9RJEspZS1lrXINK/pcq+SpT50t98QKMq1FRmDfx3vxty102k0PM4ssEnvuz5+G26Ij4yDpz6z9fV8bkyIkqBFkhej0Ib+ZQ34XJK9AfozaiimqIoX3Jp3tiISrcfYpuN2+iFph/02P36PNC9fVcCnp6H9jYouKyfaWufz5Tp9tVxcUniw7IohZv4dZz81/ns67z3AYPrc2n0+Ix2q8k0PWjgBy88XaibnfK9A+5LdDY2Ivhy36fbT8Zv3Lb1U1qLqUxorXEEXIs0mjjrtxoTZWtdvigNs2sgPiujTv6DIZLld6b/V5742JZV3fUsUVFy5gdsNtKWFzUCEVbNepD1MkSMVbsb6SZm7jI3/zODtQKgUMsOw8wDZ63t5xcV1TnaEAxoc6wrqY+Fj+N4DsqOnhOIdicrQSm1MPYCPlIqHn5bbHg8/bj2D3QfZnCX3mpAICDZV8jH5kpbZqTD0W+DxaA74CWzLN2nd14OlL72J38Lf7+TjC7dadZFDoZJQPrtaIKL/G0L6ktptPZVJ8fMqHYPZOKYPMyQGadIJfDvdXwAFiZOTvDBPydf5vk4rWA+RfdhBlaF/yDDBRoMu9pfnSjv/p7DG+HXfAcQcc49v/BBgAcFAO4DmB2GQAAAAASUVORK5CYII=);', + ' background-repeat: repeat-y;', + ' background-position: right top;', + ' }', + ' .test-element {', + ' font-family: verdana;', + ' color: #333;', + ' background: url(chrome://global/skin/icons/warning-64.png) no-repeat left center;', + ' padding-left: 70px;', + ' }', + '</style>', + '<div class="test-element">test element</div>' +].join("\n"); + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,rule view tooltip test"); + content.document.body.innerHTML = PAGE_CONTENT; + let {toolbox, inspector, view} = yield openRuleView(); + + info("Testing the background-image property on the body rule"); + yield testBodyRuleView(view); + + info("Selecting the test div node"); + yield selectNode(".test-element", inspector); + info("Testing the the background property on the .test-element rule"); + yield testDivRuleView(view); + + info("Testing that image preview tooltips show even when there are fields being edited"); + yield testTooltipAppearsEvenInEditMode(view); + + info("Switching over to the computed-view"); + let onComputedViewReady = inspector.once("computed-view-refreshed"); + ({view} = yield openComputedView()); + yield onComputedViewReady; + + info("Testing that the background-image computed style has a tooltip too"); + yield testComputedView(view); +}); + +function* testBodyRuleView(view) { + info("Testing tooltips in the rule view"); + let panel = view.tooltips.previewTooltip.panel; + + // Check that the rule view has a tooltip and that a XUL panel has been created + ok(view.tooltips.previewTooltip, "Tooltip instance exists"); + ok(panel, "XUL panel exists"); + + // Get the background-image property inside the rule view + let {valueSpan} = getRuleViewProperty(view, "body", "background-image"); + let uriSpan = valueSpan.querySelector(".theme-link"); + + yield assertHoverTooltipOn(view.tooltips.previewTooltip, uriSpan); + + let images = panel.getElementsByTagName("image"); + is(images.length, 1, "Tooltip contains an image"); + ok(images[0].getAttribute("src").indexOf("iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHe") !== -1, + "The image URL seems fine"); +} + +function* testDivRuleView(view) { + let panel = view.tooltips.previewTooltip.panel; + + // Get the background property inside the rule view + let {valueSpan} = getRuleViewProperty(view, ".test-element", "background"); + let uriSpan = valueSpan.querySelector(".theme-link"); + + yield assertHoverTooltipOn(view.tooltips.previewTooltip, uriSpan); + + let images = panel.getElementsByTagName("image"); + is(images.length, 1, "Tooltip contains an image"); + ok(images[0].getAttribute("src").startsWith("data:"), "Tooltip contains a data-uri image as expected"); +} + +function* testTooltipAppearsEvenInEditMode(view) { + let panel = view.tooltips.previewTooltip.panel; + + info("Switching to edit mode in the rule view"); + let editor = yield turnToEditMode(view); + + info("Now trying to show the preview tooltip"); + let {valueSpan} = getRuleViewProperty(view, ".test-element", "background"); + let uriSpan = valueSpan.querySelector(".theme-link"); + yield assertHoverTooltipOn(view.tooltips.previewTooltip, uriSpan); + + is(view.doc.activeElement, editor.input, + "Tooltip was shown in edit mode, and inplace-editor still focused"); +} + +function turnToEditMode(ruleView) { + let brace = ruleView.doc.querySelector(".ruleview-ruleclose"); + return focusEditableField(brace); +} + +function* testComputedView(view) { + let tooltip = view.tooltips.previewTooltip; + ok(tooltip, "The computed-view has a tooltip defined"); + + let panel = tooltip.panel; + ok(panel, "The computed-view tooltip has a XUL panel"); + + let {valueSpan} = getComputedViewProperty(view, "background-image"); + let uriSpan = valueSpan.querySelector(".theme-link"); + + yield assertHoverTooltipOn(view.tooltips.previewTooltip, uriSpan); + + let images = panel.getElementsByTagName("image"); + is(images.length, 1, "Tooltip contains an image"); + + ok(images[0].getAttribute("src").startsWith("data:"), "Tooltip contains a data-uri in the computed-view too"); +} diff --git a/toolkit/devtools/styleinspector/test/browser_styleinspector_tooltip-closes-on-new-selection.js b/toolkit/devtools/styleinspector/test/browser_styleinspector_tooltip-closes-on-new-selection.js new file mode 100644 index 000000000..11ae65f87 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_styleinspector_tooltip-closes-on-new-selection.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 if a tooltip is visible when a new selection is made, it closes + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,<div class='one'>el 1</div><div class='two'>el 2</div>"); + + let {toolbox, inspector, view} = yield openRuleView(); + yield selectNode(".one", inspector); + + info("Testing rule view tooltip closes on new selection"); + yield testRuleView(view, inspector); + + info("Testing computed view tooltip closes on new selection"); + ({view} = yield openComputedView()); + yield testComputedView(view, inspector); +}); + +function* testRuleView(ruleView, inspector) { + info("Showing the tooltip"); + let tooltip = ruleView.tooltips.previewTooltip; + let onShown = tooltip.once("shown"); + tooltip.show(); + yield onShown; + + info("Selecting a new node"); + let onHidden = tooltip.once("hidden"); + yield selectNode(".two", inspector); + + ok(true, "Rule view tooltip closed after a new node got selected"); +} + +function* testComputedView(computedView, inspector) { + info("Showing the tooltip"); + let tooltip = computedView.tooltips.previewTooltip; + let onShown = tooltip.once("shown"); + tooltip.show(); + yield onShown; + + info("Selecting a new node"); + let onHidden = tooltip.once("hidden"); + yield selectNode(".one", inspector); + + ok(true, "Computed view tooltip closed after a new node got selected"); +} diff --git a/toolkit/devtools/styleinspector/test/browser_styleinspector_tooltip-longhand-fontfamily.js b/toolkit/devtools/styleinspector/test/browser_styleinspector_tooltip-longhand-fontfamily.js new file mode 100644 index 000000000..d188f13cd --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_styleinspector_tooltip-longhand-fontfamily.js @@ -0,0 +1,119 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the fontfamily tooltip on longhand properties + +const PAGE_CONTENT = [ + '<style type="text/css">', + ' #testElement {', + ' font-family: cursive;', + ' color: #333;', + ' padding-left: 70px;', + ' }', + '</style>', + '<div id="testElement">test element</div>' +].join("\n"); + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,font family longhand tooltip test"); + + info("Creating the test document"); + content.document.body.innerHTML = PAGE_CONTENT; + + info("Opening the rule view"); + let {toolbox, inspector, view} = yield openRuleView(); + + info("Selecting the test node"); + yield selectNode("#testElement", inspector); + + yield testRuleView(view, inspector.selection.nodeFront); + + info("Opening the computed view"); + let onComputedViewReady = inspector.once("computed-view-refreshed"); + ({toolbox, inspector, view} = yield openComputedView()); + yield onComputedViewReady; + + yield testComputedView(view, inspector.selection.nodeFront); + + yield testExpandedComputedViewProperty(view, inspector.selection.nodeFront); +}); + +function* testRuleView(ruleView, nodeFront) { + info("Testing font-family tooltips in the rule view"); + + let tooltip = ruleView.tooltips.previewTooltip; + let panel = tooltip.panel; + + // Check that the rule view has a tooltip and that a XUL panel has been created + ok(tooltip, "Tooltip instance exists"); + ok(panel, "XUL panel exists"); + + // Get the font family property inside the rule view + let {valueSpan} = getRuleViewProperty(ruleView, "#testElement", "font-family"); + + // And verify that the tooltip gets shown on this property + yield assertHoverTooltipOn(tooltip, valueSpan); + + let images = panel.getElementsByTagName("image"); + is(images.length, 1, "Tooltip contains an image"); + ok(images[0].getAttribute("src").startsWith("data:"), "Tooltip contains a data-uri image as expected"); + + let dataURL = yield getFontFamilyDataURL(valueSpan.textContent, nodeFront); + is(images[0].getAttribute("src"), dataURL, "Tooltip contains the correct data-uri image"); +} + +function* testComputedView(computedView, nodeFront) { + info("Testing font-family tooltips in the computed view"); + + let tooltip = computedView.tooltips.previewTooltip; + let panel = tooltip.panel; + let {valueSpan} = getComputedViewProperty(computedView, "font-family"); + + yield assertHoverTooltipOn(tooltip, valueSpan); + + let images = panel.getElementsByTagName("image"); + is(images.length, 1, "Tooltip contains an image"); + ok(images[0].getAttribute("src").startsWith("data:"), "Tooltip contains a data-uri image as expected"); + + let dataURL = yield getFontFamilyDataURL(valueSpan.textContent, nodeFront); + is(images[0].getAttribute("src"), dataURL, "Tooltip contains the correct data-uri image"); +} + +function* testExpandedComputedViewProperty(computedView, nodeFront) { + info("Testing font-family tooltips in expanded properties of the computed view"); + + info("Expanding the font-family property to reveal matched selectors"); + let propertyView = getPropertyView(computedView, "font-family"); + propertyView.matchedExpanded = true; + yield propertyView.refreshMatchedSelectors(); + + let valueSpan = propertyView.matchedSelectorsContainer + .querySelector(".bestmatch .other-property-value"); + + let tooltip = computedView.tooltips.previewTooltip; + let panel = tooltip.panel; + + yield assertHoverTooltipOn(tooltip, valueSpan); + + let images = panel.getElementsByTagName("image"); + is(images.length, 1, "Tooltip contains an image"); + ok(images[0].getAttribute("src").startsWith("data:"), "Tooltip contains a data-uri image as expected"); + + let dataURL = yield getFontFamilyDataURL(valueSpan.textContent, nodeFront); + is(images[0].getAttribute("src"), dataURL, "Tooltip contains the correct data-uri image"); +} + +function getPropertyView(computedView, name) { + let propertyView = null; + computedView.propertyViews.some(function(view) { + if (view.name == name) { + propertyView = view; + return true; + } + return false; + }); + return propertyView; +} diff --git a/toolkit/devtools/styleinspector/test/browser_styleinspector_tooltip-multiple-background-images.js b/toolkit/devtools/styleinspector/test/browser_styleinspector_tooltip-multiple-background-images.js new file mode 100644 index 000000000..ad92b81ed --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_styleinspector_tooltip-multiple-background-images.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 for bug 1026921: Ensure the URL of hovered url() node is used instead +// of the first found from the declaration as there might be multiple urls. + +let YELLOW_DOT = "data:image/png;base64," + + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAABmJLR0QA/wD/AP+gvaeTAAAACX" + + "BIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gYcDCwCr0o5ngAAABl0RVh0Q29tbWVudABDcmVh" + + "dGVkIHdpdGggR0lNUFeBDhcAAAANSURBVAjXY/j/n6EeAAd9An7Z55GEAAAAAElFTkSuQmCC"; + +let BLUE_DOT = "data:image/png;base64," + + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAABmJLR0QA/wD/AP+gvaeTAAAACX" + + "BIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gYcDCwlCkCM9QAAABl0RVh0Q29tbWVudABDcmVh" + + "dGVkIHdpdGggR0lNUFeBDhcAAAANSURBVAjXY2Bg+F8PAAKCAX/tPkrkAAAAAElFTkSuQmCC"; + +add_task(function* () { + let TEST_STYLE = "h1 {background: url(" + YELLOW_DOT + "), url(" + BLUE_DOT + ");}"; + + let PAGE_CONTENT = "<style>" + TEST_STYLE + "</style>" + + "<h1>browser_styleinspector_tooltip-multiple-background-images.js</h1>"; + + yield addTab("data:text/html;charset=utf-8,background image tooltip test"); + content.document.body.innerHTML = PAGE_CONTENT; + + yield testRuleViewUrls(); + yield testComputedViewUrls(); +}); + +function* testRuleViewUrls() { + info("Testing tooltips in the rule view"); + + let {view, inspector} = yield openRuleView(); + yield selectNode("h1", inspector); + + let {valueSpan} = getRuleViewProperty(view, "h1", "background"); + yield performChecks(view, valueSpan); +} + +function* testComputedViewUrls() { + info("Testing tooltips in the computed view"); + + let {view, inspector} = yield openComputedView(); + yield inspector.once("computed-view-refreshed"); + let {valueSpan} = getComputedViewProperty(view, "background-image"); + + yield performChecks(view, valueSpan); +} + +/** + * A helper that checks url() tooltips contain correct images + */ +function* performChecks(view, propertyValue) { + function checkTooltip(panel, imageSrc) { + let images = panel.getElementsByTagName("image"); + is(images.length, 1, "Tooltip contains an image"); + is(images[0].getAttribute("src"), imageSrc, "The image URL is correct"); + } + + let links = propertyValue.querySelectorAll(".theme-link"); + let panel = view.tooltips.previewTooltip.panel; + + info("Checking first link tooltip"); + yield assertHoverTooltipOn(view.tooltips.previewTooltip, links[0]); + checkTooltip(panel, YELLOW_DOT); + + info("Checking second link tooltip"); + yield assertHoverTooltipOn(view.tooltips.previewTooltip, links[1]); + checkTooltip(panel, BLUE_DOT); +} diff --git a/toolkit/devtools/styleinspector/test/browser_styleinspector_tooltip-shorthand-fontfamily.js b/toolkit/devtools/styleinspector/test/browser_styleinspector_tooltip-shorthand-fontfamily.js new file mode 100644 index 000000000..6be03806b --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_styleinspector_tooltip-shorthand-fontfamily.js @@ -0,0 +1,60 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the fontfamily tooltip on shorthand properties + +const PAGE_CONTENT = [ + '<style type="text/css">', + ' #testElement {', + ' font: italic bold .8em/1.2 Arial;', + ' }', + '</style>', + '<div id="testElement">test element</div>' +].join("\n"); + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8,font family shorthand tooltip test"); + + info("Creating the test document"); + content.document.body.innerHTML = PAGE_CONTENT; + + info("Opening the rule view"); + let {toolbox, inspector, view} = yield openRuleView(); + + info("Selecting the test node"); + yield selectNode("#testElement", inspector); + + yield testRuleView(view, inspector.selection.nodeFront); +}); + +function* testRuleView(ruleView, nodeFront) { + info("Testing font-family tooltips in the rule view"); + + let tooltip = ruleView.tooltips.previewTooltip; + let panel = tooltip.panel; + + // Check that the rule view has a tooltip and that a XUL panel has been created + ok(tooltip, "Tooltip instance exists"); + ok(panel, "XUL panel exists"); + + // Get the computed font family property inside the font rule view + let propertyList = ruleView.element.querySelectorAll(".ruleview-propertylist"); + let fontExpander = propertyList[1].querySelectorAll(".ruleview-expander")[0]; + fontExpander.click(); + + let rule = getRuleViewRule(ruleView, "#testElement"); + let valueSpan = rule.querySelector(".ruleview-computed .ruleview-propertyvalue"); + + // And verify that the tooltip gets shown on this property + yield assertHoverTooltipOn(tooltip, valueSpan); + + let images = panel.getElementsByTagName("image"); + is(images.length, 1, "Tooltip contains an image"); + ok(images[0].getAttribute("src").startsWith("data:"), "Tooltip contains a data-uri image as expected"); + + let dataURL = yield getFontFamilyDataURL(valueSpan.textContent, nodeFront); + is(images[0].getAttribute("src"), dataURL, "Tooltip contains the correct data-uri image"); +} diff --git a/toolkit/devtools/styleinspector/test/browser_styleinspector_tooltip-size.js b/toolkit/devtools/styleinspector/test/browser_styleinspector_tooltip-size.js new file mode 100644 index 000000000..5b97e9434 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_styleinspector_tooltip-size.js @@ -0,0 +1,86 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Checking tooltips dimensions, to make sure their big enough to display their +// content + +const TEST_PAGE = [ + 'data:text/html;charset=utf-8,', + '<style type="text/css">', + ' div {', + ' width: 300px;height: 300px;border-radius: 50%;', + ' background: red url(chrome://global/skin/icons/warning-64.png);', + ' }', + '</style>', + '<div></div>' +].join("\n"); + +add_task(function*() { + yield addTab(TEST_PAGE); + let {toolbox, inspector, view} = yield openRuleView(); + + yield selectNode("div", inspector); + + yield testImageDimension(view); + yield testPickerDimension(view); +}); + +function* testImageDimension(ruleView) { + info("Testing background-image tooltip dimensions"); + + let tooltip = ruleView.tooltips.previewTooltip; + let panel = tooltip.panel; + let {valueSpan} = getRuleViewProperty(ruleView, "div", "background"); + let uriSpan = valueSpan.querySelector(".theme-link"); + + // Make sure there is a hover tooltip for this property, this also will fill + // the tooltip with its content + yield assertHoverTooltipOn(tooltip, uriSpan); + + info("Showing the tooltip"); + let onShown = tooltip.once("shown"); + tooltip.show(); + yield onShown; + + // Let's not test for a specific size, but instead let's make sure it's at + // least as big as the image + let imageRect = panel.querySelector("image").getBoundingClientRect(); + let panelRect = panel.getBoundingClientRect(); + + ok(panelRect.width >= imageRect.width, + "The panel is wide enough to show the image"); + ok(panelRect.height >= imageRect.height, + "The panel is high enough to show the image"); + + let onHidden = tooltip.once("hidden"); + tooltip.hide(); + yield onHidden; +} + +function* testPickerDimension(ruleView) { + info("Testing color-picker tooltip dimensions"); + + let {valueSpan} = getRuleViewProperty(ruleView, "div", "background"); + let swatch = valueSpan.querySelector(".ruleview-colorswatch"); + let cPicker = ruleView.tooltips.colorPicker; + + let onShown = cPicker.tooltip.once("shown"); + swatch.click(); + yield onShown; + + // The colorpicker spectrum's iframe has a fixed width height, so let's + // make sure the tooltip is at least as big as that + let w = cPicker.tooltip.panel.querySelector("iframe").width; + let h = cPicker.tooltip.panel.querySelector("iframe").height; + let panelRect = cPicker.tooltip.panel.getBoundingClientRect(); + + ok(panelRect.width >= w, "The panel is wide enough to show the picker"); + ok(panelRect.height >= h, "The panel is high enough to show the picker"); + + let onHidden = cPicker.tooltip.once("hidden"); + cPicker.hide(); + yield onHidden; +} diff --git a/toolkit/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-01.js b/toolkit/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-01.js new file mode 100644 index 000000000..d9e018bc1 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-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"; + +// Test that the css transform highlighter is created only when asked + +const PAGE_CONTENT = [ + '<style type="text/css">', + ' body {', + ' transform: skew(16deg);', + ' }', + '</style>', + 'Test the css transform highlighter' +].join("\n"); + +const TYPE = "CssTransformHighlighter"; + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8," + PAGE_CONTENT); + + let {inspector, view: rView} = yield openRuleView(); + let overlay = rView.highlighters; + + ok(!overlay.highlighters[TYPE], "No highlighter exists in the rule-view"); + let h = yield overlay._getHighlighter(TYPE); + ok(overlay.highlighters[TYPE], "The highlighter has been created in the rule-view"); + is(h, overlay.highlighters[TYPE], "The right highlighter has been created"); + let h2 = yield overlay._getHighlighter(TYPE); + is(h, h2, "The same instance of highlighter is returned everytime in the rule-view"); + + let onComputedViewReady = inspector.once("computed-view-refreshed"); + let {view: cView} = yield openComputedView(); + yield onComputedViewReady; + overlay = cView.highlighters; + + ok(!overlay.highlighters[TYPE], "No highlighter exists in the computed-view"); + h = yield overlay._getHighlighter(TYPE); + ok(overlay.highlighters[TYPE], "The highlighter has been created in the computed-view"); + is(h, overlay.highlighters[TYPE], "The right highlighter has been created"); + h2 = yield overlay._getHighlighter(TYPE); + is(h, h2, "The same instance of highlighter is returned everytime in the computed-view"); +}); diff --git a/toolkit/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-02.js b/toolkit/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-02.js new file mode 100644 index 000000000..3c00d3008 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-02.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 that the css transform highlighter is created when hovering over a +// transform property + +const PAGE_CONTENT = [ + '<style type="text/css">', + ' body {', + ' transform: skew(16deg);', + ' color: yellow;', + ' }', + '</style>', + 'Test the css transform highlighter' +].join("\n"); + +let TYPE = "CssTransformHighlighter"; + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8," + PAGE_CONTENT); + + let {inspector, view: rView} = yield openRuleView(); + let hs = rView.highlighters; + + ok(!hs.highlighters[TYPE], "No highlighter exists in the rule-view (1)"); + ok(!hs.promises[TYPE], "No highlighter is being created in the rule-view (1)"); + + info("Faking a mousemove on a non-transform property"); + let {valueSpan} = getRuleViewProperty(rView, "body", "color"); + hs._onMouseMove({target: valueSpan}); + ok(!hs.highlighters[TYPE], "No highlighter exists in the rule-view (2)"); + ok(!hs.promises[TYPE], "No highlighter is being created in the rule-view (2)"); + + info("Faking a mousemove on a transform property"); + ({valueSpan} = getRuleViewProperty(rView, "body", "transform")); + hs._onMouseMove({target: valueSpan}); + ok(hs.promises[TYPE], "The highlighter is being initialized"); + let h = yield hs.promises[TYPE]; + is(h, hs.highlighters[TYPE], "The initialized highlighter is the right one"); + + let onComputedViewReady = inspector.once("computed-view-refreshed"); + let {view: cView} = yield openComputedView(); + yield onComputedViewReady; + hs = cView.highlighters; + + ok(!hs.highlighters[TYPE], "No highlighter exists in the computed-view (1)"); + ok(!hs.promises[TYPE], "No highlighter is being created in the computed-view (1)"); + + info("Faking a mousemove on a non-transform property"); + ({valueSpan} = getComputedViewProperty(cView, "color")); + hs._onMouseMove({target: valueSpan}); + ok(!hs.highlighters[TYPE], "No highlighter exists in the computed-view (2)"); + ok(!hs.promises[TYPE], "No highlighter is being created in the computed-view (2)"); + + info("Faking a mousemove on a transform property"); + ({valueSpan} = getComputedViewProperty(cView, "transform")); + hs._onMouseMove({target: valueSpan}); + ok(hs.promises[TYPE], "The highlighter is being initialized"); + h = yield hs.promises[TYPE]; + is(h, hs.highlighters[TYPE], "The initialized highlighter is the right one"); +}); diff --git a/toolkit/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-03.js b/toolkit/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-03.js new file mode 100644 index 000000000..7b0a25c2c --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-03.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"; + +// Test that the css transform highlighter is shown when hovering over transform +// properties + +// Note that in this test, we mock the highlighter front, merely testing the +// behavior of the style-inspector UI for now + +const PAGE_CONTENT = [ + '<style type="text/css">', + ' html {', + ' transform: scale(.9);', + ' }', + ' body {', + ' transform: skew(16deg);', + ' color: purple;', + ' }', + '</style>', + 'Test the css transform highlighter' +].join("\n"); + +const TYPE = "CssTransformHighlighter"; + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8," + PAGE_CONTENT); + + let {inspector, view: rView} = yield openRuleView(); + + // Mock the highlighter front to get the reference of the NodeFront + let HighlighterFront = { + isShown: false, + nodeFront: null, + nbOfTimesShown: 0, + show: function(nodeFront) { + this.nodeFront = nodeFront; + this.isShown = true; + this.nbOfTimesShown ++; + }, + hide: function() { + this.nodeFront = null; + this.isShown = false; + } + }; + + // Inject the mock highlighter in the rule-view + rView.highlighters.promises[TYPE] = { + then: function(cb) { + cb(HighlighterFront); + } + }; + + let {valueSpan} = getRuleViewProperty(rView, "body", "transform"); + + info("Checking that the HighlighterFront's show/hide methods are called"); + rView.highlighters._onMouseMove({target: valueSpan}); + ok(HighlighterFront.isShown, "The highlighter is shown"); + rView.highlighters._onMouseLeave(); + ok(!HighlighterFront.isShown, "The highlighter is hidden"); + + info("Checking that hovering several times over the same property doesn't" + + " show the highlighter several times"); + let nb = HighlighterFront.nbOfTimesShown; + rView.highlighters._onMouseMove({target: valueSpan}); + is(HighlighterFront.nbOfTimesShown, nb + 1, "The highlighter was shown once"); + rView.highlighters._onMouseMove({target: valueSpan}); + rView.highlighters._onMouseMove({target: valueSpan}); + is(HighlighterFront.nbOfTimesShown, nb + 1, + "The highlighter was shown once, after several mousemove"); + + info("Checking that the right NodeFront reference is passed"); + yield selectNode("html", inspector); + ({valueSpan} = getRuleViewProperty(rView, "html", "transform")); + rView.highlighters._onMouseMove({target: valueSpan}); + is(HighlighterFront.nodeFront.tagName, "HTML", + "The right NodeFront is passed to the highlighter (1)"); + + yield selectNode("body", inspector); + ({valueSpan} = getRuleViewProperty(rView, "body", "transform")); + rView.highlighters._onMouseMove({target: valueSpan}); + is(HighlighterFront.nodeFront.tagName, "BODY", + "The right NodeFront is passed to the highlighter (2)"); + + info("Checking that the highlighter gets hidden when hovering a non-transform property"); + ({valueSpan} = getRuleViewProperty(rView, "body", "color")); + rView.highlighters._onMouseMove({target: valueSpan}); + ok(!HighlighterFront.isShown, "The highlighter is hidden"); +}); diff --git a/toolkit/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-04.js b/toolkit/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-04.js new file mode 100644 index 000000000..a70eb0a47 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-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"; + +// Test that the css transform highlighter is shown only when hovering over a +// transform declaration that isn't overriden or disabled + +// Note that unlike the other browser_styleinspector_transform-highlighter-N.js +// tests, this one only tests the rule-view as only this view features disabled +// and overriden properties + +const PAGE_CONTENT = [ + '<style type="text/css">', + ' div {', + ' background: purple;', + ' width:300px;height:300px;', + ' transform: rotate(16deg);', + ' }', + ' .test {', + ' transform: skew(25deg);', + ' }', + '</style>', + '<div class="test"></div>' +].join("\n"); + +const TYPE = "CssTransformHighlighter"; + +add_task(function*() { + yield addTab("data:text/html;charset=utf-8," + PAGE_CONTENT); + + let {view: rView, inspector} = yield openRuleView(); + yield selectNode(".test", inspector); + + let hs = rView.highlighters; + + info("Faking a mousemove on the overriden property"); + let {valueSpan} = getRuleViewProperty(rView, "div", "transform"); + hs._onMouseMove({target: valueSpan}); + ok(!hs.highlighters[TYPE], "No highlighter was created for the overriden property"); + ok(!hs.promises[TYPE], "And no highlighter is being initialized either"); + + info("Disabling the applied property"); + let classRuleEditor = getRuleViewRuleEditor(rView, 1); + let propEditor = classRuleEditor.rule.textProps[0].editor; + propEditor.enable.click(); + yield classRuleEditor.rule._applyingModifications; + + info("Faking a mousemove on the disabled property"); + ({valueSpan} = getRuleViewProperty(rView, ".test", "transform")); + hs._onMouseMove({target: valueSpan}); + ok(!hs.highlighters[TYPE], "No highlighter was created for the disabled property"); + ok(!hs.promises[TYPE], "And no highlighter is being initialized either"); + + info("Faking a mousemove on the now unoverriden property"); + ({valueSpan} = getRuleViewProperty(rView, "div", "transform")); + hs._onMouseMove({target: valueSpan}); + ok(hs.promises[TYPE], "The highlighter is being initialized now"); + let h = yield hs.promises[TYPE]; + is(h, hs.highlighters[TYPE], "The initialized highlighter is the right one"); +}); diff --git a/toolkit/devtools/styleinspector/test/doc_content_stylesheet.html b/toolkit/devtools/styleinspector/test/doc_content_stylesheet.html new file mode 100644 index 000000000..3d3b132d6 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/doc_content_stylesheet.html @@ -0,0 +1,33 @@ +<html> +<head> + <title>test</title> + + <link href="./doc_content_stylesheet_linked.css" rel="stylesheet" type="text/css"> + + <script> + // Load script.css + function loadCSS() { + var 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/toolkit/devtools/styleinspector/test/doc_content_stylesheet.xul b/toolkit/devtools/styleinspector/test/doc_content_stylesheet.xul new file mode 100644 index 000000000..efd53815d --- /dev/null +++ b/toolkit/devtools/styleinspector/test/doc_content_stylesheet.xul @@ -0,0 +1,9 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin/xul.css" type="text/css"?> +<?xml-stylesheet href="./doc_content_stylesheet_xul.css" + type="text/css"?> +<!DOCTYPE window> +<window id="testwindow" xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <label id="target" value="Simple XUL document" /> +</window>
\ No newline at end of file diff --git a/toolkit/devtools/styleinspector/test/doc_content_stylesheet_imported.css b/toolkit/devtools/styleinspector/test/doc_content_stylesheet_imported.css new file mode 100644 index 000000000..ea1a3d986 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/doc_content_stylesheet_imported.css @@ -0,0 +1,5 @@ +@import url("./doc_content_stylesheet_imported2.css"); + +#target { + text-decoration: underline; +} diff --git a/toolkit/devtools/styleinspector/test/doc_content_stylesheet_imported2.css b/toolkit/devtools/styleinspector/test/doc_content_stylesheet_imported2.css new file mode 100644 index 000000000..77c73299e --- /dev/null +++ b/toolkit/devtools/styleinspector/test/doc_content_stylesheet_imported2.css @@ -0,0 +1,3 @@ +#target { + text-decoration: underline; +} diff --git a/toolkit/devtools/styleinspector/test/doc_content_stylesheet_linked.css b/toolkit/devtools/styleinspector/test/doc_content_stylesheet_linked.css new file mode 100644 index 000000000..712ba78fb --- /dev/null +++ b/toolkit/devtools/styleinspector/test/doc_content_stylesheet_linked.css @@ -0,0 +1,3 @@ +table { + border-collapse: collapse; +} diff --git a/toolkit/devtools/styleinspector/test/doc_content_stylesheet_script.css b/toolkit/devtools/styleinspector/test/doc_content_stylesheet_script.css new file mode 100644 index 000000000..5aa5e2c6c --- /dev/null +++ b/toolkit/devtools/styleinspector/test/doc_content_stylesheet_script.css @@ -0,0 +1,5 @@ +@import url("./doc_content_stylesheet_imported.css"); + +table { + opacity: 1; +} diff --git a/toolkit/devtools/styleinspector/test/doc_content_stylesheet_xul.css b/toolkit/devtools/styleinspector/test/doc_content_stylesheet_xul.css new file mode 100644 index 000000000..a14ae7f6f --- /dev/null +++ b/toolkit/devtools/styleinspector/test/doc_content_stylesheet_xul.css @@ -0,0 +1,3 @@ +#target { + font-size: 200px; +} diff --git a/toolkit/devtools/styleinspector/test/doc_frame_script.js b/toolkit/devtools/styleinspector/test/doc_frame_script.js new file mode 100644 index 000000000..6ab0916af --- /dev/null +++ b/toolkit/devtools/styleinspector/test/doc_frame_script.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"; + +// 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:MessageName", data, true); +// The response message should have the same name "Test:MessageName" +// +// Some listeners do not send a response message back. + +let {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +let {require} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools; +let {CssLogic} = require("devtools/styleinspector/css-logic"); + +/** + * 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 information about all the stylesheets that contain rules that apply to + * a given node. The information contains the sheet href and whether or not the + * sheet is a content sheet or not + * @param {Object} objects Expects a 'target' CPOW object + * @return {Array} A list of stylesheet info objects + */ +addMessageListener("Test:GetStyleSheetsInfoForNode", function(msg) { + let target = msg.objects.target; + let sheets = []; + + let domUtils = Cc["@mozilla.org/inspector/dom-utils;1"] + .getService(Ci.inIDOMUtils); + let domRules = domUtils.getCSSStyleRules(target); + + for (let i = 0, n = domRules.Count(); i < n; i++) { + let sheet = domRules.GetElementAt(i).parentStyleSheet; + sheets.push({ + href: sheet.href, + isContentSheet: CssLogic.isContentStylesheet(sheet) + }); + } + + sendAsyncMessage("Test:GetStyleSheetsInfoForNode", sheets); +}); + +/** + * 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); +}); + +let dumpn = msg => dump(msg + "\n"); diff --git a/toolkit/devtools/styleinspector/test/doc_keyframeanimation.css b/toolkit/devtools/styleinspector/test/doc_keyframeanimation.css new file mode 100644 index 000000000..64582ed35 --- /dev/null +++ b/toolkit/devtools/styleinspector/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/toolkit/devtools/styleinspector/test/doc_keyframeanimation.html b/toolkit/devtools/styleinspector/test/doc_keyframeanimation.html new file mode 100644 index 000000000..4e02c32f0 --- /dev/null +++ b/toolkit/devtools/styleinspector/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/toolkit/devtools/styleinspector/test/doc_matched_selectors.html b/toolkit/devtools/styleinspector/test/doc_matched_selectors.html new file mode 100644 index 000000000..8fe007409 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/doc_matched_selectors.html @@ -0,0 +1,28 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> + <head> + <style> + .matched1, .matched2, .matched3, .matched4, .matched5 { + color: #000; + } + + div { + position: absolute; + top: 40px; + left: 20px; + border: 1px solid #000; + color: #111; + width: 100px; + height: 50px; + } + </style> + </head> + <body> + inspectstyle($("test")); + <div id="test" class="matched1 matched2 matched3 matched4 matched5">Test div</div> + <div id="dummy"> + <div></div> + </div> + </body> +</html> diff --git a/toolkit/devtools/styleinspector/test/doc_media_queries.html b/toolkit/devtools/styleinspector/test/doc_media_queries.html new file mode 100644 index 000000000..1adb8bc7a --- /dev/null +++ b/toolkit/devtools/styleinspector/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/toolkit/devtools/styleinspector/test/doc_pseudoelement.html b/toolkit/devtools/styleinspector/test/doc_pseudoelement.html new file mode 100644 index 000000000..6145d4bf1 --- /dev/null +++ b/toolkit/devtools/styleinspector/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/toolkit/devtools/styleinspector/test/doc_sourcemaps.css b/toolkit/devtools/styleinspector/test/doc_sourcemaps.css new file mode 100644 index 000000000..a9b437a40 --- /dev/null +++ b/toolkit/devtools/styleinspector/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/toolkit/devtools/styleinspector/test/doc_sourcemaps.css.map b/toolkit/devtools/styleinspector/test/doc_sourcemaps.css.map new file mode 100644 index 000000000..0f7486fd9 --- /dev/null +++ b/toolkit/devtools/styleinspector/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/toolkit/devtools/styleinspector/test/doc_sourcemaps.html b/toolkit/devtools/styleinspector/test/doc_sourcemaps.html new file mode 100644 index 000000000..0014e55fe --- /dev/null +++ b/toolkit/devtools/styleinspector/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/toolkit/devtools/styleinspector/test/doc_sourcemaps.scss b/toolkit/devtools/styleinspector/test/doc_sourcemaps.scss new file mode 100644 index 000000000..0ff6c471b --- /dev/null +++ b/toolkit/devtools/styleinspector/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/toolkit/devtools/styleinspector/test/doc_style_editor_link.css b/toolkit/devtools/styleinspector/test/doc_style_editor_link.css new file mode 100644 index 000000000..e49e1f587 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/doc_style_editor_link.css @@ -0,0 +1,3 @@ +div { + opacity: 1; +}
\ No newline at end of file diff --git a/toolkit/devtools/styleinspector/test/doc_test_image.png b/toolkit/devtools/styleinspector/test/doc_test_image.png Binary files differnew file mode 100644 index 000000000..769c63634 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/doc_test_image.png diff --git a/toolkit/devtools/styleinspector/test/doc_urls_clickable.css b/toolkit/devtools/styleinspector/test/doc_urls_clickable.css new file mode 100644 index 000000000..802b580d4 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/doc_urls_clickable.css @@ -0,0 +1,9 @@ +.relative1 { + background-image: url(./doc_test_image.png); +} +.absolute { + background: url("http://example.com/browser/browser/devtools/styleinspector/test/doc_test_image.png"); +} +.base64 { + background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=='); +}
\ No newline at end of file diff --git a/toolkit/devtools/styleinspector/test/doc_urls_clickable.html b/toolkit/devtools/styleinspector/test/doc_urls_clickable.html new file mode 100644 index 000000000..b0265a703 --- /dev/null +++ b/toolkit/devtools/styleinspector/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/toolkit/devtools/styleinspector/test/head.js b/toolkit/devtools/styleinspector/test/head.js new file mode 100644 index 000000000..c030cf577 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/head.js @@ -0,0 +1,914 @@ +/* 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 Cu = Components.utils; +let {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {}); +let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); +let TargetFactory = devtools.TargetFactory; +let {CssHtmlTree} = devtools.require("devtools/styleinspector/computed-view"); +let {CssRuleView, _ElementStyle} = devtools.require("devtools/styleinspector/rule-view"); +let {CssLogic, CssSelector} = devtools.require("devtools/styleinspector/css-logic"); +let {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); +let {editableField, getInplaceEditorForSpan: inplaceEditor} = devtools.require("devtools/shared/inplace-editor"); +let {console} = Components.utils.import("resource://gre/modules/devtools/Console.jsm", {}); + +// All tests are asynchronous +waitForExplicitFinish(); + +const TEST_URL_ROOT = "http://example.com/browser/browser/devtools/styleinspector/test/"; +const TEST_URL_ROOT_SSL = "https://example.com/browser/browser/devtools/styleinspector/test/"; +const ROOT_TEST_DIR = getRootDirectory(gTestPath); +const FRAME_SCRIPT_URL = ROOT_TEST_DIR + "doc_frame_script.js"; + +// Auto clean-up when a test ends +registerCleanupFunction(function*() { + let target = TargetFactory.forTab(gBrowser.selectedTab); + yield gDevTools.closeToolbox(target); + + while (gBrowser.tabs.length > 1) { + gBrowser.removeCurrentTab(); + } +}); + +// Uncomment this pref to dump all devtools emitted events to the console. +// Services.prefs.setBoolPref("devtools.dump.emit", true); + +// Set the testing flag on gDevTools and reset it when the test ends +gDevTools.testing = true; +registerCleanupFunction(() => gDevTools.testing = false); + +// Clean-up all prefs that might have been changed during a test run +// (safer here because if the test fails, then the pref is never reverted) +registerCleanupFunction(() => { + Services.prefs.clearUserPref("devtools.inspector.activeSidebar"); + Services.prefs.clearUserPref("devtools.dump.emit"); + Services.prefs.clearUserPref("devtools.defaultColorUnit"); +}); + +/** + * The functions found below are here to ease test development and maintenance. + * Most of these functions are stateless and will require some form of context + * (the instance of the current toolbox, or inspector panel for instance). + * + * Most of these functions are async too and return promises. + * + * All tests should follow the following pattern: + * + * add_task(function*() { + * yield addTab(TEST_URI); + * let {toolbox, inspector, view} = yield openComputedView(); + * + * yield selectNode("#test", inspector); + * yield someAsyncTestFunction(view); + * }); + * + * add_task is the way to define the testcase in the test file. It accepts + * a single generator-function argument. + * The generator function should yield any async call. + * + * There is no need to clean tabs up at the end of a test as this is done + * automatically. + * + * It is advised not to store any references on the global scope. There shouldn't + * be a need to anyway. Thanks to add_task, test steps, even though asynchronous, + * can be described in a nice flat way, and if/for/while/... control flow can be + * used as in sync code, making it possible to write the outline of the test case + * all in add_task, and delegate actual processing and assertions to other + * functions. + */ + +/* ********************************************* + * UTILS + * ********************************************* + * General test utilities. + * Add new tabs, open the toolbox and switch to the various panels, select + * nodes, get node references, ... + */ + +/** + * Add a new test tab in the browser and load the given url. + * @param {String} url The url to be loaded in the new tab + * @return a promise that resolves to the tab object when the url is loaded + */ +function addTab(url) { + info("Adding a new tab with URL: '" + url + "'"); + let def = promise.defer(); + + window.focus(); + + let tab = window.gBrowser.selectedTab = window.gBrowser.addTab(url); + let browser = tab.linkedBrowser; + + info("Loading the helper frame script " + FRAME_SCRIPT_URL); + browser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false); + + browser.addEventListener("load", function onload() { + browser.removeEventListener("load", onload, true); + info("URL '" + url + "' loading complete"); + + def.resolve(tab); + }, true); + + return def.promise; +} + +/** + * Simple DOM node accesor function that takes either a node or a string css + * selector as argument and returns the corresponding node + * @param {String|DOMNode} nodeOrSelector + * @return {DOMNode|CPOW} Note that in e10s mode a CPOW object is returned which + * doesn't implement *all* of the DOMNode's properties + */ +function getNode(nodeOrSelector) { + info("Getting the node for '" + nodeOrSelector + "'"); + return typeof nodeOrSelector === "string" ? + content.document.querySelector(nodeOrSelector) : + nodeOrSelector; +} + +/** + * Get the NodeFront for a given css selector, via the protocol + * @param {String} selector + * @param {InspectorPanel} inspector The instance of InspectorPanel currently + * loaded in the toolbox + * @return {Promise} Resolves to the NodeFront instance + */ +function getNodeFront(selector, {walker}) { + return walker.querySelector(walker.rootNode, selector); +} + +/** + * Highlight a node that matches the given css selector and set the inspector's + * current selection to this node. + * @param {String} selector + * @param {InspectorPanel} inspector The instance of InspectorPanel currently + * loaded in the toolbox + * @return {Promise} Resolves when the inspector is updated with the new node + */ +let selectAndHighlightNode = Task.async(function*(selector, inspector) { + info("Highlighting and selecting the node for " + selector); + + let nodeFront = yield getNodeFront(selector, inspector); + let updated = inspector.toolbox.once("highlighter-ready"); + inspector.selection.setNodeFront(nodeFront, "test-highlight"); + yield updated; +}); + +/* + * Set the inspector's current selection to a node or to the first match of the + * given css selector. + * @param {String|NodeFront} + * data The node to select + * @param {InspectorPanel} inspector + * The instance of InspectorPanel currently + * loaded in the toolbox + * @param {String} reason + * Defaults to "test" which instructs the inspector not + * to highlight the node upon selection + * @return {Promise} Resolves when the inspector is updated with the new node + */ +let selectNode = Task.async(function*(data, inspector, reason="test") { + info("Selecting the node for '" + data + "'"); + let nodeFront = data; + if (!data._form) { + nodeFront = yield getNodeFront(data, inspector); + } + let updated = inspector.once("inspector-updated"); + inspector.selection.setNodeFront(nodeFront, reason); + yield updated; +}); + +/** + * Set the inspector's current selection to null so that no node is selected + * @param {InspectorPanel} inspector The instance of InspectorPanel currently + * loaded in the toolbox + * @return a promise that resolves when the inspector is updated + */ +function clearCurrentNodeSelection(inspector) { + info("Clearing the current selection"); + let updated = inspector.once("inspector-updated"); + inspector.selection.setNodeFront(null); + return updated; +} + +/** + * Open the toolbox, with the inspector tool visible. + * @return a promise that resolves when the inspector is ready + */ +let openInspector = Task.async(function*() { + info("Opening the inspector"); + let target = TargetFactory.forTab(gBrowser.selectedTab); + + let inspector, toolbox; + + // Checking if the toolbox and the inspector are already loaded + // The inspector-updated event should only be waited for if the inspector + // isn't loaded yet + toolbox = gDevTools.getToolbox(target); + if (toolbox) { + inspector = toolbox.getPanel("inspector"); + if (inspector) { + info("Toolbox and inspector already open"); + return { + toolbox: toolbox, + inspector: inspector + }; + } + } + + info("Opening the toolbox"); + toolbox = yield gDevTools.showToolbox(target, "inspector"); + yield waitForToolboxFrameFocus(toolbox); + inspector = toolbox.getPanel("inspector"); + + info("Waiting for the inspector to update"); + yield inspector.once("inspector-updated"); + + return { + toolbox: toolbox, + inspector: inspector + }; +}); + +/** + * Wait for the toolbox frame to receive focus after it loads + * @param {Toolbox} toolbox + * @return a promise that resolves when focus has been received + */ +function waitForToolboxFrameFocus(toolbox) { + info("Making sure that the toolbox's frame is focused"); + let def = promise.defer(); + let win = toolbox.frame.contentWindow; + waitForFocus(def.resolve, win); + return def.promise; +} + +/** + * Open the toolbox, with the inspector tool visible, and the sidebar that + * corresponds to the given id selected + * @return a promise that resolves when the inspector is ready and the sidebar + * view is visible and ready + */ +let openInspectorSideBar = Task.async(function*(id) { + let {toolbox, inspector} = yield openInspector(); + + if (!hasSideBarTab(inspector, id)) { + info("Waiting for the " + id + " sidebar to be ready"); + yield inspector.sidebar.once(id + "-ready"); + } + + info("Selecting the " + id + " sidebar"); + inspector.sidebar.select(id); + + return { + toolbox: toolbox, + inspector: inspector, + view: inspector.sidebar.getWindowForTab(id)[id].view + }; +}); + +/** + * Open the toolbox, with the inspector tool visible, and the computed-view + * sidebar tab selected. + * @return a promise that resolves when the inspector is ready and the computed + * view is visible and ready + */ +function openComputedView() { + return openInspectorSideBar("computedview"); +} + +/** + * Open the toolbox, with the inspector tool visible, and the rule-view + * sidebar tab selected. + * @return a promise that resolves when the inspector is ready and the rule + * view is visible and ready + */ +function openRuleView() { + return openInspectorSideBar("ruleview"); +} + +/** + * Wait for eventName on target. + * @param {Object} target An observable object that either supports on/off or + * addEventListener/removeEventListener + * @param {String} eventName + * @param {Boolean} useCapture Optional, for addEventListener/removeEventListener + * @return A promise that resolves when the event has been handled + */ +function once(target, eventName, useCapture=false) { + info("Waiting for event: '" + eventName + "' on " + target + "."); + + let deferred = promise.defer(); + + for (let [add, remove] of [ + ["addEventListener", "removeEventListener"], + ["addListener", "removeListener"], + ["on", "off"] + ]) { + if ((add in target) && (remove in target)) { + target[add](eventName, function onEvent(...aArgs) { + target[remove](eventName, onEvent, useCapture); + deferred.resolve.apply(deferred, aArgs); + }, useCapture); + break; + } + } + + return deferred.promise; +} + +/** + * This shouldn't be used in the tests, but is useful when writing new tests or + * debugging existing tests in order to introduce delays in the test steps + * @param {Number} ms The time to wait + * @return A promise that resolves when the time is passed + */ +function wait(ms) { + let def = promise.defer(); + content.setTimeout(def.resolve, ms); + return def.promise; +} + +/** + * 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 = promise.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); + } else { + 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: selector, + pseudo: pseudo, + name: propName}); +} + +/** + * 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 + */ +let focusEditableField = Task.async(function*(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); + let event = yield onFocus; + + info("Editable field gained focus, returning the input field now"); + return inplaceEditor(editable.ownerDocument.activeElement); +}); + +/** + * Given a tooltip object instance (see Tooltip.js), checks if it is set to + * toggle and hover and if so, checks if the given target is a valid hover target. + * This won't actually show the tooltip (the less we interact with XUL panels + * during test runs, the better). + * @return a promise that resolves when the answer is known + */ +function isHoverTooltipTarget(tooltip, target) { + if (!tooltip._basedNode || !tooltip.panel) { + return promise.reject(new Error( + "The tooltip passed isn't set to toggle on hover or is not a tooltip")); + } + return tooltip.isValidHoverTarget(target); +} + +/** + * Same as isHoverTooltipTarget except that it will fail the test if there is no + * tooltip defined on hover of the given element + * @return a promise + */ +function assertHoverTooltipOn(tooltip, element) { + return isHoverTooltipTarget(tooltip, element).then(() => { + ok(true, "A tooltip is defined on hover of the given element"); + }, () => { + ok(false, "No tooltip is defined on hover of the given element"); + }); +} + +/** + * Same as assertHoverTooltipOn but fails the test if there is a tooltip defined + * on hover of the given element + * @return a promise + */ +function assertNoHoverTooltipOn(tooltip, element) { + return isHoverTooltipTarget(tooltip, element).then(() => { + ok(false, "A tooltip is defined on hover of the given element"); + }, () => { + ok(true, "No tooltip is defined on hover of the given element"); + }); +} + +/** + * Listen for a new window to open and return a promise that resolves when one + * does and completes its load. + * Only resolves when the new window topic isn't domwindowopened. + * @return a promise that resolves to the window object + */ +function waitForWindow() { + let def = promise.defer(); + + info("Waiting for a window to open"); + Services.ww.registerNotification(function onWindow(subject, topic) { + if (topic != "domwindowopened") { + return; + } + info("A window has been opened"); + let win = subject.QueryInterface(Ci.nsIDOMWindow); + once(win, "load").then(() => { + info("The window load completed"); + Services.ww.unregisterNotification(onWindow); + def.resolve(win); + }); + }); + + return def.promise; +} + +/** + * @see SimpleTest.waitForClipboard + * @param {Function} setup Function to execute before checking for the + * clipboard content + * @param {String|Boolean} expected An expected string or validator function + * @return a promise that resolves when the expected string has been found or + * the validator function has returned true, rejects otherwise. + */ +function waitForClipboard(setup, expected) { + let def = promise.defer(); + SimpleTest.waitForClipboard(expected, setup, def.resolve, def.reject); + return def.promise; +} + +/** + * Dispatch the copy event on the given element + */ +function fireCopyEvent(element) { + let evt = element.ownerDocument.createEvent("Event"); + evt.initEvent("copy", true, true); + element.dispatchEvent(evt); +} + +/** + * 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. If validatorFn never returns true, then + * polling timeouts after several tries and the promise rejects. + * @param {String} name Optional name of the test. This is used to generate + * the success and failure messages. + * @return a promise that resolves when the function returned true or rejects + * if the timeout is reached + */ +function waitForSuccess(validatorFn, name="untitled") { + let def = promise.defer(); + let start = Date.now(); + + function wait(validatorFn) { + if (validatorFn()) { + ok(true, "Validator function " + name + " returned true"); + def.resolve(); + } else { + setTimeout(() => wait(validatorFn), 200); + } + } + wait(validatorFn); + + return def.promise; +} + +/** + * Create a new style tag containing the given style text and append it to the + * document's head node + * @param {Document} doc + * @param {String} style + * @return {DOMNode} The newly created style node + */ +function addStyle(doc, style) { + info("Adding a new style tag to the document with style content: " + + style.substring(0, 50)); + let node = doc.createElement('style'); + node.setAttribute("type", "text/css"); + node.textContent = style; + doc.getElementsByTagName("head")[0].appendChild(node); + return node; +} + +/** + * Checks whether the inspector's sidebar corresponding to the given id already + * exists + * @param {InspectorPanel} + * @param {String} + * @return {Boolean} + */ +function hasSideBarTab(inspector, id) { + return !!inspector.sidebar.getWindowForTab(id); +} + +/** + * Get the dataURL for the font family tooltip. + * @param {String} font The font family value. + * @param {object} nodeFront + * The NodeActor that will used to retrieve the dataURL for the + * font family tooltip contents. + */ +let getFontFamilyDataURL = Task.async(function*(font, nodeFront) { + let fillStyle = (Services.prefs.getCharPref("devtools.theme") === "light") ? + "black" : "white"; + + let {data} = yield nodeFront.getFontFamilyDataURL(font, fillStyle); + let dataURL = yield data.string(); + return dataURL; +}); + +/* ********************************************* + * RULE-VIEW + * ********************************************* + * Rule-view related test utility functions + * This object contains functions to get rules, get properties, ... + */ + +/** + * Get the DOMNode for a css rule in the rule-view that corresponds to the given + * selector + * @param {CssRuleView} view The instance of the rule-view panel + * @param {String} selectorText The selector in the rule-view for which the rule + * object is wanted + * @return {DOMNode} + */ +function getRuleViewRule(view, selectorText) { + let rule; + for (let r of view.doc.querySelectorAll(".ruleview-rule")) { + let selector = r.querySelector(".ruleview-selector, .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"); +} + +/** + * Simulate a color change in a given color picker tooltip, and optionally wait + * for a given element in the page to have its style changed as a result + * @param {SwatchColorPickerTooltip} colorPicker + * @param {Array} newRgba The new color to be set [r, g, b, a] + * @param {Object} expectedChange Optional object that needs the following props: + * - {DOMNode} element The element in the page that will have its + * style changed. + * - {String} name The style name that will be changed + * - {String} value The expected style value + * The style will be checked like so: getComputedStyle(element)[name] === value + */ +let simulateColorPickerChange = Task.async(function*(colorPicker, newRgba, expectedChange) { + info("Getting the spectrum colorpicker object"); + let spectrum = yield colorPicker.spectrum; + info("Setting the new color"); + spectrum.rgb = newRgba; + info("Applying the change"); + spectrum.updateUI(); + spectrum.onChange(); + + if (expectedChange) { + info("Waiting for the style to be applied on the page"); + yield waitForSuccess(() => { + let {element, name, value} = expectedChange; + return content.getComputedStyle(element)[name] === value; + }, "Color picker change applied on the page"); + } +}); + +/** + * Get a rule-link from the rule-view given its index + * @param {CssRuleView} view The instance of the rule-view panel + * @param {Number} index The index of the link to get + * @return {DOMNode} The link if any at this index + */ +function getRuleViewLinkByIndex(view, index) { + let links = view.doc.querySelectorAll(".ruleview-rule-source"); + return links[index]; +} + +/** + * Get the rule editor from the rule-view given its index + * @param {CssRuleView} view The instance of the rule-view panel + * @param {Number} childrenIndex The children index of the element to get + * @param {Number} nodeIndex The child node index of the element to get + * @return {DOMNode} The rule editor if any at this index + */ +function getRuleViewRuleEditor(view, childrenIndex, nodeIndex) { + return nodeIndex !== undefined ? + view.element.children[childrenIndex].childNodes[nodeIndex]._ruleEditor : + view.element.children[childrenIndex]._ruleEditor; +} + +/** + * 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 + */ +let focusNewRuleViewProperty = Task.async(function*(ruleEditor) { + info("Clicking on a close ruleEditor brace to start editing a new property"); + ruleEditor.closeBrace.scrollIntoView(); + let editor = yield focusEditableField(ruleEditor.closeBrace); + + is(inplaceEditor(ruleEditor.newPropSpan), editor, "Focused editor is the new property editor."); + is(ruleEditor.rule.textProps.length, 0, "Starting with one new text property."); + is(ruleEditor.propertyList.children.length, 1, "Starting with two property editors."); + + 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 + */ +let 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; +}); + +/* ********************************************* + * COMPUTED-VIEW + * ********************************************* + * Computed-view related utility functions. + * Allows to get properties, links, expand properties, ... + */ + +/** + * Get references to the name and value span nodes corresponding to a given + * property name in the computed-view + * @param {CssHtmlTree} view The instance of the computed view panel + * @param {String} name The name of the property to retrieve + * @return an object {nameSpan, valueSpan} + */ +function getComputedViewProperty(view, name) { + let prop; + for (let property of view.styleDocument.querySelectorAll(".property-view")) { + let nameSpan = property.querySelector(".property-name"); + let valueSpan = property.querySelector(".property-value"); + + if (nameSpan.textContent === name) { + prop = {nameSpan: nameSpan, valueSpan: valueSpan}; + break; + } + } + return prop; +} + +/** + * Get an instance of PropertyView from the computed-view. + * @param {CssHtmlTree} view The instance of the computed view panel + * @param {String} name The name of the property to retrieve + * @return {PropertyView} + */ +function getComputedViewPropertyView(view, name) { + let propView; + for (let propertyView of view.propertyViews) { + if (propertyView._propertyInfo.name === name) { + propView = propertyView; + break; + } + } + return propView; +} + +/** + * Get a reference to the property-content element for a given property name in + * the computed-view. + * A property-content element always follows (nextSibling) the property itself + * and is only shown when the twisty icon is expanded on the property. + * A property-content element contains matched rules, with selectors, properties, + * values and stylesheet links + * @param {CssHtmlTree} view The instance of the computed view panel + * @param {String} name The name of the property to retrieve + * @return {Promise} A promise that resolves to the property matched rules + * container + */ +let getComputedViewMatchedRules = Task.async(function*(view, name) { + let expander; + let propertyContent; + for (let property of view.styleDocument.querySelectorAll(".property-view")) { + let nameSpan = property.querySelector(".property-name"); + if (nameSpan.textContent === name) { + expander = property.querySelector(".expandable"); + propertyContent = property.nextSibling; + break; + } + } + + if (!expander.hasAttribute("open")) { + // Need to expand the property + let onExpand = view.inspector.once("computed-view-property-expanded"); + expander.click(); + yield onExpand; + } + + return propertyContent; +}); + +/** + * Get the text value of the property corresponding to a given name in the + * computed-view + * @param {CssHtmlTree} view The instance of the computed view panel + * @param {String} name The name of the property to retrieve + * @return {String} The property value + */ +function getComputedViewPropertyValue(view, name, propertyName) { + return getComputedViewProperty(view, name, propertyName) + .valueSpan.textContent; +} + +/** + * Expand a given property, given its index in the current property list of + * the computed view + * @param {CssHtmlTree} view The instance of the computed view panel + * @param {Number} index The index of the property to be expanded + * @return a promise that resolves when the property has been expanded, or + * rejects if the property was not found + */ +function expandComputedViewPropertyByIndex(view, index) { + info("Expanding property " + index + " in the computed view"); + let expandos = view.styleDocument.querySelectorAll(".expandable"); + if (!expandos.length || !expandos[index]) { + return promise.reject(); + } + + let onExpand = view.inspector.once("computed-view-property-expanded"); + expandos[index].click(); + return onExpand; +} + +/** + * Get a rule-link from the computed-view given its index + * @param {CssHtmlTree} view The instance of the computed view panel + * @param {Number} index The index of the link to be retrieved + * @return {DOMNode} The link at the given index, if one exists, null otherwise + */ +function getComputedViewLinkByIndex(view, index) { + let links = view.styleDocument.querySelectorAll(".rule-link .link"); + return links[index]; +} + +/* ********************************************* + * STYLE-EDITOR + * ********************************************* + * Style-editor related utility functions. + */ + +/** + * Wait for the toolbox to emit the styleeditor-selected event and when done + * wait for the stylesheet identified by href to be loaded in the stylesheet + * editor + * @param {Toolbox} toolbox + * @param {String} href Optional, if not provided, wait for the first editor + * to be ready + * @return a promise that resolves to the editor when the stylesheet editor is + * ready + */ +function waitForStyleEditor(toolbox, href) { + let def = promise.defer(); + + info("Waiting for the toolbox to switch to the styleeditor"); + toolbox.once("styleeditor-ready").then(() => { + let panel = toolbox.getCurrentPanel(); + ok(panel && panel.UI, "Styleeditor panel switched to front"); + + panel.UI.on("editor-selected", function onEditorSelected(event, editor) { + let currentHref = editor.styleSheet.href; + if (!href || (href && currentHref.endsWith(href))) { + info("Stylesheet editor selected"); + panel.UI.off("editor-selected", onEditorSelected); + editor.getSourceEditor().then(editor => { + info("Stylesheet editor fully loaded"); + def.resolve(editor); + }); + } + }); + }); + + return def.promise; +} diff --git a/toolkit/devtools/styleinspector/test/unit/test_parseDeclarations.js b/toolkit/devtools/styleinspector/test/unit/test_parseDeclarations.js new file mode 100644 index 000000000..9e33d3f12 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/unit/test_parseDeclarations.js @@ -0,0 +1,208 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const Cu = Components.utils; +Cu.import("resource://gre/modules/devtools/Loader.jsm"); +const {parseDeclarations} = devtools.require("devtools/styleinspector/css-parsing-utils"); + +const TEST_DATA = [ + // Simple test + { + input: "p:v;", + expected: [{name: "p", value: "v", priority: ""}] + }, + // Simple test + { + input: "this:is;a:test;", + expected: [ + {name: "this", value: "is", priority: ""}, + {name: "a", value: "test", priority: ""} + ] + }, + // Test a single declaration with semi-colon + { + input: "name:value;", + expected: [{name: "name", value: "value", priority: ""}] + }, + // Test a single declaration without semi-colon + { + input: "name:value", + expected: [{name: "name", value: "value", priority: ""}] + }, + // Test multiple declarations separated by whitespaces and carriage returns and tabs + { + input: "p1 : v1 ; \t\t \n p2:v2; \n\n\n\n\t p3 : v3;", + expected: [ + {name: "p1", value: "v1", priority: ""}, + {name: "p2", value: "v2", priority: ""}, + {name: "p3", value: "v3", priority: ""}, + ] + }, + // Test simple priority + { + input: "p1: v1; p2: v2 !important;", + expected: [ + {name: "p1", value: "v1", priority: ""}, + {name: "p2", value: "v2", priority: "important"} + ] + }, + // Test simple priority + { + input: "p1: v1 !important; p2: v2", + expected: [ + {name: "p1", value: "v1", priority: "important"}, + {name: "p2", value: "v2", priority: ""} + ] + }, + // Test simple priority + { + input: "p1: v1 ! important; p2: v2 ! important;", + expected: [ + {name: "p1", value: "v1", priority: "important"}, + {name: "p2", value: "v2", priority: "important"} + ] + }, + // Test invalid priority + { + input: "p1: v1 important;", + expected: [ + {name: "p1", value: "v1 important", priority: ""} + ] + }, + // Test various types of background-image urls + { + input: "background-image: url(../../relative/image.png)", + expected: [{name: "background-image", value: "url(\"../../relative/image.png\")", priority: ""}] + }, + { + input: "background-image: url(http://site.com/test.png)", + expected: [{name: "background-image", value: "url(\"http://site.com/test.png\")", priority: ""}] + }, + { + input: "background-image: url(wow.gif)", + expected: [{name: "background-image", value: "url(\"wow.gif\")", priority: ""}] + }, + // Test that urls with :;{} characters in them are parsed correctly + { + input: "background: red url(\"http://site.com/image{}:;.png?id=4#wat\") repeat top right", + expected: [ + {name: "background", value: "red url(\"http://site.com/image{}:;.png?id=4#wat\") repeat top right", priority: ""} + ] + }, + // Test that an empty string results in an empty array + {input: "", expected: []}, + // Test that a string comprised only of whitespaces results in an empty array + {input: " \n \n \n \n \t \t\t\t ", expected: []}, + // Test that a null input throws an exception + {input: null, throws: true}, + // Test that a undefined input throws an exception + {input: undefined, throws: true}, + // Test that :;{} characters in quoted content are not parsed as multiple declarations + { + input: "content: \";color:red;}selector{color:yellow;\"", + expected: [ + {name: "content", value: "\";color:red;}selector{color:yellow;\"", priority: ""} + ] + }, + // Test that rules aren't parsed, just declarations. So { and } found after a + // property name should be part of the property name, same for values. + { + input: "body {color:red;} p {color: blue;}", + expected: [ + {name: "body {color", value: "red", priority: ""}, + {name: "} p {color", value: "blue", priority: ""}, + {name: "}", value: "", priority: ""} + ] + }, + // Test unbalanced : and ; + { + input: "color :red : font : arial;", + expected : [ + {name: "color", value: "red : font : arial", priority: ""} + ] + }, + {input: "background: red;;;;;", expected: [{name: "background", value: "red", priority: ""}]}, + {input: "background:;", expected: [{name: "background", value: "", priority: ""}]}, + {input: ";;;;;", expected: []}, + {input: ":;:;", expected: []}, + // Test name only + {input: "color", expected: [ + {name: "color", value: "", priority: ""} + ]}, + // Test trailing name without : + {input: "color:blue;font", expected: [ + {name: "color", value: "blue", priority: ""}, + {name: "font", value: "", priority: ""} + ]}, + // Test trailing name with : + {input: "color:blue;font:", expected: [ + {name: "color", value: "blue", priority: ""}, + {name: "font", value: "", priority: ""} + ]}, + // Test leading value + {input: "Arial;color:blue;", expected: [ + {name: "", value: "Arial", priority: ""}, + {name: "color", value: "blue", priority: ""} + ]}, + // Test hex colors + {input: "color: #333", expected: [{name: "color", value: "#333", priority: ""}]}, + {input: "color: #456789", expected: [{name: "color", value: "#456789", priority: ""}]}, + {input: "wat: #XYZ", expected: [{name: "wat", value: "#XYZ", priority: ""}]}, + // Test string/url quotes escaping + {input: "content: \"this is a 'string'\"", expected: [{name: "content", value: "\"this is a 'string'\"", priority: ""}]}, + {input: 'content: "this is a \\"string\\""', expected: [{name: "content", value: '\'this is a "string"\'', priority: ""}]}, + {input: "content: 'this is a \"string\"'", expected: [{name: "content", value: '\'this is a "string"\'', priority: ""}]}, + {input: "content: 'this is a \\'string\\'", expected: [{name: "content", value: '"this is a \'string\'"', priority: ""}]}, + {input: "content: 'this \\' is a \" really strange string'", expected: [{name: "content", value: '"this \' is a \" really strange string"', priority: ""}]}, + { + input: "content: \"a not s\\\ + o very long title\"", + expected: [ + {name: "content", value: '"a not s\ + o very long title"', priority: ""} + ] + }, + // Test calc with nested parentheses + {input: "width: calc((100% - 3em) / 2)", expected: [{name: "width", value: "calc((100% - 3em) / 2)", priority: ""}]}, +]; + +function run_test() { + for (let test of TEST_DATA) { + do_print("Test input string " + test.input); + let output; + try { + output = parseDeclarations(test.input); + } catch (e) { + do_print("parseDeclarations threw an exception with the given input string"); + if (test.throws) { + do_print("Exception expected"); + do_check_true(true); + } else { + do_print("Exception unexpected\n" + e); + do_check_true(false); + } + } + if (output) { + assertOutput(output, test.expected); + } + } +} + +function assertOutput(actual, expected) { + if (actual.length === expected.length) { + for (let i = 0; i < expected.length; i ++) { + do_check_true(!!actual[i]); + do_print("Check that the output item has the expected name, value and priority"); + do_check_eq(expected[i].name, actual[i].name); + do_check_eq(expected[i].value, actual[i].value); + do_check_eq(expected[i].priority, actual[i].priority); + } + } else { + for (let prop of actual) { + do_print("Actual output contained: {name: "+prop.name+", value: "+prop.value+", priority: "+prop.priority+"}"); + } + do_check_eq(actual.length, expected.length); + } +} diff --git a/toolkit/devtools/styleinspector/test/unit/test_parseSingleValue.js b/toolkit/devtools/styleinspector/test/unit/test_parseSingleValue.js new file mode 100644 index 000000000..ff39878b7 --- /dev/null +++ b/toolkit/devtools/styleinspector/test/unit/test_parseSingleValue.js @@ -0,0 +1,76 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const Cu = Components.utils; +Cu.import("resource://gre/modules/devtools/Loader.jsm"); +const {parseSingleValue} = devtools.require("devtools/styleinspector/css-parsing-utils"); + +const TEST_DATA = [ + {input: null, throws: true}, + {input: undefined, throws: true}, + {input: "", expected: {value: "", priority: ""}}, + {input: " \t \t \n\n ", expected: {value: "", priority: ""}}, + {input: "blue", expected: {value: "blue", priority: ""}}, + {input: "blue !important", expected: {value: "blue", priority: "important"}}, + {input: "blue!important", expected: {value: "blue", priority: "important"}}, + {input: "blue ! important", expected: {value: "blue", priority: "important"}}, + {input: "blue ! important", expected: {value: "blue", priority: "important"}}, + {input: "blue !", expected: {value: "blue", priority: ""}}, + {input: "blue !mportant", expected: {value: "blue !mportant", priority: ""}}, + {input: " blue !important ", expected: {value: "blue", priority: "important"}}, + { + input: "url(\"http://url.com/whyWouldYouDoThat!important.png\") !important", + expected: { + value: "url(\"http://url.com/whyWouldYouDoThat!important.png\")", + priority: "important" + } + }, + { + input: "url(\"http://url.com/whyWouldYouDoThat!important.png\")", + expected: { + value: "url(\"http://url.com/whyWouldYouDoThat!important.png\")", + priority: "" + } + }, + { + input: "\"content!important\" !important", + expected: { + value: "\"content!important\"", + priority: "important" + } + }, + { + input: "\"content!important\"", + expected: { + value: "\"content!important\"", + priority: "" + } + } +]; + +function run_test() { + for (let test of TEST_DATA) { + do_print("Test input value " + test.input); + try { + let output = parseSingleValue(test.input); + assertOutput(output, test.expected); + } catch (e) { + do_print("parseSingleValue threw an exception with the given input value"); + if (test.throws) { + do_print("Exception expected"); + do_check_true(true); + } else { + do_print("Exception unexpected\n" + e); + do_check_true(false); + } + } + } +} + +function assertOutput(actual, expected) { + do_print("Check that the output has the expected value and priority"); + do_check_eq(expected.value, actual.value); + do_check_eq(expected.priority, actual.priority); +} diff --git a/toolkit/devtools/styleinspector/test/unit/xpcshell.ini b/toolkit/devtools/styleinspector/test/unit/xpcshell.ini new file mode 100644 index 000000000..90b0c5d9c --- /dev/null +++ b/toolkit/devtools/styleinspector/test/unit/xpcshell.ini @@ -0,0 +1,8 @@ +[DEFAULT] +head = +tail = +firefox-appdir = browser +skip-if = toolkit == 'android' || toolkit == 'gonk' + +[test_parseDeclarations.js] +[test_parseSingleValue.js] |