diff options
Diffstat (limited to 'devtools/client/inspector/computed')
32 files changed, 3452 insertions, 0 deletions
diff --git a/devtools/client/inspector/computed/computed.js b/devtools/client/inspector/computed/computed.js new file mode 100644 index 000000000..71d602a4e --- /dev/null +++ b/devtools/client/inspector/computed/computed.js @@ -0,0 +1,1522 @@ +/* -*- 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 ToolDefinitions = require("devtools/client/definitions").Tools; +const CssLogic = require("devtools/shared/inspector/css-logic"); +const {ELEMENT_STYLE} = require("devtools/shared/specs/styles"); +const promise = require("promise"); +const defer = require("devtools/shared/defer"); +const Services = require("Services"); +const {OutputParser} = require("devtools/client/shared/output-parser"); +const {PrefObserver, PREF_ORIG_SOURCES} = require("devtools/client/styleeditor/utils"); +const {createChild} = require("devtools/client/inspector/shared/utils"); +const {gDevTools} = require("devtools/client/framework/devtools"); +const {getCssProperties} = require("devtools/shared/fronts/css-properties"); +const HighlightersOverlay = require("devtools/client/inspector/shared/highlighters-overlay"); +const { + VIEW_NODE_SELECTOR_TYPE, + VIEW_NODE_PROPERTY_TYPE, + VIEW_NODE_VALUE_TYPE, + VIEW_NODE_IMAGE_URL_TYPE, +} = require("devtools/client/inspector/shared/node-types"); +const StyleInspectorMenu = require("devtools/client/inspector/shared/style-inspector-menu"); +const TooltipsOverlay = require("devtools/client/inspector/shared/tooltips-overlay"); +const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts"); +const {BoxModelView} = require("devtools/client/inspector/components/box-model"); +const clipboardHelper = require("devtools/shared/platform/clipboard"); + +const STYLE_INSPECTOR_PROPERTIES = "devtools/shared/locales/styleinspector.properties"; +const {LocalizationHelper} = require("devtools/shared/l10n"); +const STYLE_INSPECTOR_L10N = new LocalizationHelper(STYLE_INSPECTOR_PROPERTIES); + +const FILTER_CHANGED_TIMEOUT = 150; +const HTML_NS = "http://www.w3.org/1999/xhtml"; + +/** + * Helper for long-running processes that should yield occasionally to + * the mainloop. + * + * @param {Window} win + * Timeouts will be set on this window when appropriate. + * @param {Array} array + * The array of items to process. + * @param {Object} options + * 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. + */ +function UpdateProcess(win, array, options) { + this.win = win; + this.index = 0; + this.array = array; + + this.onItem = options.onItem || function () {}; + this.onBatch = options.onBatch || function () {}; + this.onDone = options.onDone || function () {}; + this.onCancel = options.onCancel || function () {}; + this.threshold = options.threshold || 45; + + this.canceled = false; +} + +UpdateProcess.prototype = { + /** + * Error thrown when the array of items to process is empty. + */ + ERROR_ITERATION_DONE: new Error("UpdateProcess iteration done"), + + /** + * Schedule a new batch on the main loop. + */ + schedule: function () { + if (this.canceled) { + return; + } + this._timeout = setTimeout(this._timeoutHandler.bind(this), 0); + }, + + /** + * Cancel the running process. onItem will not be called again, + * and onCancel will be called. + */ + cancel: function () { + if (this._timeout) { + clearTimeout(this._timeout); + this._timeout = 0; + } + this.canceled = true; + this.onCancel(); + }, + + _timeoutHandler: function () { + this._timeout = null; + try { + this._runBatch(); + this.schedule(); + } catch (e) { + if (e === this.ERROR_ITERATION_DONE) { + this.onBatch(); + this.onDone(); + return; + } + console.error(e); + throw e; + } + }, + + _runBatch: function () { + let time = Date.now(); + while (!this.canceled) { + let next = this._next(); + this.onItem(next); + if ((Date.now() - time) > this.threshold) { + this.onBatch(); + return; + } + } + }, + + /** + * Returns the item at the current index and increases the index. + * If all items have already been processed, will throw ERROR_ITERATION_DONE. + */ + _next: function () { + if (this.index < this.array.length) { + return this.array[this.index++]; + } + throw this.ERROR_ITERATION_DONE; + }, +}; + +/** + * CssComputedView is a panel that manages the display of a table + * sorted by style. There should be one instance of CssComputedView + * per style display (of which there will generally only be one). + * + * @param {Inspector} inspector + * Inspector toolbox panel + * @param {Document} document + * The document that will contain the computed view. + * @param {PageStyleFront} pageStyle + * Front for the page style actor that will be providing + * the style information. + */ +function CssComputedView(inspector, document, pageStyle) { + this.inspector = inspector; + this.styleDocument = document; + this.styleWindow = this.styleDocument.defaultView; + this.pageStyle = pageStyle; + + this.propertyViews = []; + + let cssProperties = getCssProperties(inspector.toolbox); + this._outputParser = new OutputParser(document, cssProperties); + + // Create bound methods. + this.focusWindow = this.focusWindow.bind(this); + this._onContextMenu = this._onContextMenu.bind(this); + this._onClick = this._onClick.bind(this); + this._onCopy = this._onCopy.bind(this); + this._onFilterStyles = this._onFilterStyles.bind(this); + this._onClearSearch = this._onClearSearch.bind(this); + this._onIncludeBrowserStyles = this._onIncludeBrowserStyles.bind(this); + + let doc = this.styleDocument; + this.element = doc.getElementById("propertyContainer"); + this.searchField = doc.getElementById("computedview-searchbox"); + this.searchClearButton = doc.getElementById("computedview-searchinput-clear"); + this.includeBrowserStylesCheckbox = + doc.getElementById("browser-style-checkbox"); + + this.shortcuts = new KeyShortcuts({ window: this.styleWindow }); + this._onShortcut = this._onShortcut.bind(this); + this.shortcuts.on("CmdOrCtrl+F", this._onShortcut); + this.shortcuts.on("Escape", this._onShortcut); + this.styleDocument.addEventListener("mousedown", this.focusWindow); + this.element.addEventListener("click", this._onClick); + this.element.addEventListener("copy", this._onCopy); + this.element.addEventListener("contextmenu", this._onContextMenu); + this.searchField.addEventListener("input", this._onFilterStyles); + this.searchField.addEventListener("contextmenu", this.inspector.onTextBoxContextMenu); + this.searchClearButton.addEventListener("click", this._onClearSearch); + this.includeBrowserStylesCheckbox.addEventListener("input", + this._onIncludeBrowserStyles); + + this.searchClearButton.hidden = true; + + // No results text. + this.noResults = this.styleDocument.getElementById("computedview-no-results"); + + // 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._onSourcePrefChanged = this._onSourcePrefChanged.bind(this); + this._prefObserver = new PrefObserver("devtools."); + this._prefObserver.on(PREF_ORIG_SOURCES, this._onSourcePrefChanged); + + // The element that we're inspecting, and the document that it comes from. + this._viewedElement = null; + + this.createStyleViews(); + + this._contextmenu = new StyleInspectorMenu(this, { isRuleView: false }); + + // Add the tooltips and highlightersoverlay + this.tooltips = new TooltipsOverlay(this); + this.tooltips.addToView(); + + this.highlighters = new HighlightersOverlay(this); + this.highlighters.addToView(); +} + +/** + * Lookup a l10n string in the shared styleinspector string bundle. + * + * @param {String} name + * The key to lookup. + * @returns {String} localized version of the given key. + */ +CssComputedView.l10n = function (name) { + try { + return STYLE_INSPECTOR_L10N.getStr(name); + } catch (ex) { + console.log("Error reading '" + name + "'"); + throw new Error("l10n error with " + name); + } +}; + +CssComputedView.prototype = { + // Cache the list of properties that match the selected element. + _matchedProperties: null, + + // Used for cancelling timeouts in the style filter. + _filterChangedTimeout: 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 CssComputedView panel + * will show the style information for the given element. + * + * @param {NodeFront} element + * The highlighted node to get styles for. + * @returns a promise that will be resolved when highlighting is complete. + */ + selectElement: function (element) { + if (!element) { + 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 (element === this._viewedElement) { + return promise.resolve(undefined); + } + + this._viewedElement = element; + 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 + * client/inspector/shared/node-types + * - value {Object} Depends on the type of the node + * returns null if 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: 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 = VIEW_NODE_PROPERTY_TYPE; + } else if (classes.contains("property-value") || + classes.contains("other-property-value")) { + type = VIEW_NODE_VALUE_TYPE; + } else if (isHref) { + type = 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 = defer(); + this._createViewsPromise = deferred.promise; + + this.refreshSourceFilter(); + this.numVisibleProperties = 0; + let fragment = this.styleDocument.createDocumentFragment(); + + this._createViewsProcess = new UpdateProcess( + this.styleWindow, CssComputedView.propertyNames, { + onItem: (propertyName) => { + // Per-item callback. + let propView = new PropertyView(this, propertyName); + 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 () { + 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(([, computed]) => { + if (viewedElement !== this._viewedElement) { + return promise.resolve(); + } + + 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 = defer(); + this._refreshProcess = new UpdateProcess( + this.styleWindow, this.propertyViews, { + onItem: (propView) => { + propView.refresh(); + }, + onCancel: () => { + deferred.reject("_refreshProcess of computed view cancelled"); + }, + onDone: () => { + this._refreshProcess = null; + this.noResults.hidden = this.numVisibleProperties > 0; + + if (this.searchField.value.length > 0 && + !this.numVisibleProperties) { + this.searchField.classList + .add("devtools-style-searchbox-no-match"); + } else { + this.searchField.classList + .remove("devtools-style-searchbox-no-match"); + } + + this.inspector.emit("computed-view-refreshed"); + deferred.resolve(undefined); + } + } + ); + this._refreshProcess.schedule(); + return deferred.promise; + }).then(null, (err) => console.error(err)); + }, + + /** + * Handle the shortcut events in the computed view. + */ + _onShortcut: function (name, event) { + if (!event.target.closest("#sidebar-panel-computedview")) { + return; + } + // Handle the search box's keypress event. If the escape key is pressed, + // clear the search box field. + if (name === "Escape" && event.target === this.searchField && + this._onClearSearch()) { + event.preventDefault(); + event.stopPropagation(); + } else if (name === "CmdOrCtrl+F") { + this.searchField.focus(); + event.preventDefault(); + } + }, + + /** + * Set the filter style search value. + * @param {String} value + * The search value. + */ + setFilterStyles: function (value = "") { + this.searchField.value = value; + this.searchField.focus(); + this._onFilterStyles(); + }, + + /** + * Called when the user enters a search term in the filter style search box. + */ + _onFilterStyles: function () { + if (this._filterChangedTimeout) { + clearTimeout(this._filterChangedTimeout); + } + + let filterTimeout = (this.searchField.value.length > 0) + ? FILTER_CHANGED_TIMEOUT : 0; + this.searchClearButton.hidden = this.searchField.value.length === 0; + + this._filterChangedTimeout = setTimeout(() => { + if (this.searchField.value.length > 0) { + this.searchField.setAttribute("filled", true); + this.inspector.emit("computed-view-filtered", true); + } else { + this.searchField.removeAttribute("filled"); + this.inspector.emit("computed-view-filtered", false); + } + + this.refreshPanel(); + this._filterChangeTimeout = null; + }, filterTimeout); + }, + + /** + * Called when the user clicks on the clear button in the filter style search + * box. Returns true if the search box is cleared and false otherwise. + */ + _onClearSearch: function () { + if (this.searchField.value) { + this.setFilterStyles(""); + return true; + } + + return false; + }, + + /** + * The change event handler for the includeBrowserStyles checkbox. + */ + _onIncludeBrowserStyles: function () { + this.refreshSourceFilter(); + this.refreshPanel(); + }, + + /** + * When includeBrowserStylesCheckbox.checked is false we only display + * properties that have matched selectors and have been included by the + * document or one of thedocument's stylesheets. If .checked is false we + * display all properties including those that come from UA stylesheets. + */ + refreshSourceFilter: function () { + this._matchedProperties = null; + this._sourceFilter = this.includeBrowserStyles ? + CssLogic.FILTER.UA : + CssLogic.FILTER.USER; + }, + + _onSourcePrefChanged: function () { + for (let propView of this.propertyViews) { + propView.updateSourceLinks(); + } + this.inspector.emit("computed-view-sourcelinks-updated"); + }, + + /** + * The CSS as displayed by the UI. + */ + createStyleViews: function () { + if (CssComputedView.propertyNames) { + return; + } + + CssComputedView.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 { + CssComputedView.propertyNames.push(prop); + } + } + + CssComputedView.propertyNames.sort(); + CssComputedView.propertyNames.push.apply(CssComputedView.propertyNames, + mozProps.sort()); + + this._createPropertyViews().then(null, e => { + if (!this._isDestroyed) { + 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. + */ + focusWindow: function () { + this.styleWindow.focus(); + }, + + /** + * Context menu handler. + */ + _onContextMenu: function (event) { + this._contextmenu.show(event); + }, + + _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"); + } + }, + + /** + * Callback for copy event. Copy selected text. + * + * @param {Event} event + * copy event object. + */ + _onCopy: function (event) { + this.copySelection(); + event.preventDefault(); + }, + + /** + * Copy the current selection to the clipboard + */ + copySelection: function () { + try { + let win = this.styleWindow; + 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 (CssComputedView.propertyNames.indexOf(prop) !== -1) { + // Property name + result += prop; + } else { + // Property value + result += ": " + prop + ";\n"; + } + } + } else { + // Short text fragment. + result = textArray[0]; + } + + clipboardHelper.copyString(result); + } catch (e) { + console.error(e); + } + }, + + /** + * Destructor for CssComputedView. + */ + destroy: function () { + this._viewedElement = null; + this._outputParser = null; + + gDevTools.off("pref-changed", this._handlePrefChange); + + this._prefObserver.off(PREF_ORIG_SOURCES, this._onSourcePrefChanged); + this._prefObserver.destroy(); + + // Cancel tree construction + if (this._createViewsProcess) { + this._createViewsProcess.cancel(); + } + if (this._refreshProcess) { + this._refreshProcess.cancel(); + } + + // Remove context menu + if (this._contextmenu) { + this._contextmenu.destroy(); + this._contextmenu = null; + } + + this.tooltips.destroy(); + this.highlighters.destroy(); + + // Remove bound listeners + this.styleDocument.removeEventListener("mousedown", this.focusWindow); + this.element.removeEventListener("click", this._onClick); + this.element.removeEventListener("copy", this._onCopy); + this.element.removeEventListener("contextmenu", this._onContextMenu); + this.searchField.removeEventListener("input", this._onFilterStyles); + this.searchField.removeEventListener("contextmenu", + this.inspector.onTextBoxContextMenu); + this.searchClearButton.removeEventListener("click", this._onClearSearch); + this.includeBrowserStylesCheckbox.removeEventListener("input", + this._onIncludeBrowserStyles); + + // Nodes used in templating + this.element = null; + this.panel = null; + this.searchField = null; + this.searchClearButton = null; + this.includeBrowserStylesCheckbox = null; + + // Property views + for (let propView of this.propertyViews) { + propView.destroy(); + } + this.propertyViews = null; + + this.inspector = null; + this.styleDocument = null; + this.styleWindow = null; + + this._isDestroyed = true; + } +}; + +function PropertyInfo(tree, name) { + this.tree = tree; + this.name = name; +} + +PropertyInfo.prototype = { + get value() { + if (this.tree._computed) { + let value = this.tree._computed[this.name].value; + return value; + } + return null; + } +}; + +/** + * A container to give easy access to property data from the template engine. + * + * @param {CssComputedView} tree + * The CssComputedView instance we are working with. + * @param {String} name + * The CSS property name for which this PropertyView + * instance will render the rules. + */ +function PropertyView(tree, name) { + this.tree = tree; + this.name = name; + + this.link = "https://developer.mozilla.org/CSS/" + name; + + this._propertyInfo = new PropertyInfo(tree, name); +} + +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(); + let isValidSearchTerm = searchTerm.trim().length > 0; + if (isValidSearchTerm && + 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 () { + 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.shortcuts = new KeyShortcuts({ + window: this.tree.styleWindow, + target: this.element + }); + this.shortcuts.on("F1", (name, event) => { + this.mdnLinkClick(event); + // Prevent opening the options panel + event.preventDefault(); + event.stopPropagation(); + }); + this.shortcuts.on("Return", (name, event) => this.onMatchedToggle(event)); + this.shortcuts.on("Space", (name, event) => this.onMatchedToggle(event)); + + let nameContainer = doc.createElementNS(HTML_NS, "div"); + nameContainer.className = "property-name-container"; + this.element.appendChild(nameContainer); + + // 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); + nameContainer.appendChild(this.matchedExpander); + + // 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", ""); + // Avoid english text (css properties) from being altered + // by RTL mode + this.nameNode.setAttribute("dir", "ltr"); + 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); + nameContainer.appendChild(this.nameNode); + + let valueContainer = doc.createElementNS(HTML_NS, "div"); + valueContainer.className = "property-value-container"; + this.element.appendChild(valueContainer); + + // 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); + valueContainer.appendChild(this.valueNode); + + return this.element; + }, + + buildSelectorContainer: function () { + 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 () { + 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", + colorClass: "computedview-color", + 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 () { + 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 promise.resolve(undefined); + } + + this._matchedSelectorResponse = matched; + + return this._buildMatchedSelectors().then(() => { + this.matchedExpander.setAttribute("open", ""); + this.tree.inspector.emit("computed-view-property-expanded"); + }); + }).then(null, console.error); + } + + this.matchedSelectorsContainer.innerHTML = ""; + this.matchedExpander.removeAttribute("open"); + this.tree.inspector.emit("computed-view-property-collapsed"); + return promise.resolve(undefined); + }, + + get matchedSelectors() { + return this._matchedSelectorResponse; + }, + + _buildMatchedSelectors: function () { + let promises = []; + let frag = this.element.ownerDocument.createDocumentFragment(); + + for (let selector of this.matchedSelectorViews) { + let p = createChild(frag, "p"); + let span = createChild(p, "span", { + class: "rule-link" + }); + let link = createChild(span, "a", { + target: "_blank", + class: "link theme-link", + title: selector.href, + sourcelocation: selector.source, + tabindex: "0", + textContent: selector.source + }); + link.addEventListener("click", selector.openStyleEditor, false); + let shortcuts = new KeyShortcuts({ + window: this.tree.styleWindow, + target: link + }); + shortcuts.on("Return", () => selector.openStyleEditor()); + + let status = createChild(p, "span", { + dir: "ltr", + class: "rule-text theme-fg-color3 " + selector.statusClass, + title: selector.statusText, + textContent: selector.sourceText + }); + let valueSpan = createChild(status, "span", { + class: "other-property-value theme-fg-color1" + }); + valueSpan.appendChild(selector.outputFragment); + promises.push(selector.ready); + } + + this.matchedSelectorsContainer.innerHTML = ""; + this.matchedSelectorsContainer.appendChild(frag); + return promise.all(promises); + }, + + /** + * Provide access to the matched SelectorViews that we are currently + * displaying. + */ + get matchedSelectorViews() { + if (!this._matchedSelectorViews) { + this._matchedSelectorViews = []; + this._matchedSelectorResponse.forEach(selectorInfo => { + let selectorView = new SelectorView(this.tree, selectorInfo); + this._matchedSelectorViews.push(selectorView); + }, 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 () { + if (!this._matchedSelectorViews) { + return; + } + for (let view of this._matchedSelectorViews) { + view.updateSourceLink(); + } + }, + + /** + * The action when a user expands matched selectors. + * + * @param {Event} event + * Used to determine the class name of the targets click + * event. + */ + onMatchedToggle: function (event) { + if (event.shiftKey) { + return; + } + this.matchedExpanded = !this.matchedExpanded; + this.refreshMatchedSelectors(); + event.preventDefault(); + }, + + /** + * The action when a user clicks on the MDN help link for a property. + */ + mdnLinkClick: function (event) { + let inspector = this.tree.inspector; + + if (inspector.target.tab) { + let browserWin = inspector.target.tab.ownerDocument.defaultView; + browserWin.openUILinkIn(this.link, "tab"); + } + }, + + /** + * Destroy this property view, removing event listeners + */ + destroy: function () { + this.element.removeEventListener("dblclick", this.onMatchedToggle, false); + this.shortcuts.destroy(); + 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 CssComputedView tree + * the owning CssComputedView + * @param selectorInfo + */ +function SelectorView(tree, selectorInfo) { + this.tree = tree; + this.selectorInfo = selectorInfo; + this._cacheStatusNames(); + + this.openStyleEditor = this.openStyleEditor.bind(this); + + this.ready = 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. + */ + _cacheStatusNames: function () { + if (SelectorView.STATUS_NAMES.length) { + return; + } + + for (let status in CssLogic.STATUS) { + let i = CssLogic.STATUS[status]; + if (i > CssLogic.STATUS.UNMATCHED) { + let value = CssComputedView.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", + colorClass: "computedview-color", + 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"); + return promise.resolve(oldSource); + } + + let showOrig = Services.prefs.getBoolPref(PREF_ORIG_SOURCES); + + if (showOrig && rule.type !== ELEMENT_STYLE) { + let deferred = 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}) => { + 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); + }, + + /** + * 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. + */ + openStyleEditor: function () { + 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 parentStyleSheet = rule.parentStyleSheet; + if (!parentStyleSheet || parentStyleSheet.isSystem) { + let toolbox = gDevTools.getToolbox(inspector.target); + toolbox.viewSource(rule.href, rule.line); + return; + } + + let location = promise.resolve(rule.location); + if (Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) { + location = rule.getOriginalLocation(); + } + + location.then(({source, href, line, column}) => { + let target = inspector.target; + if (ToolDefinitions.styleEditor.isTargetSupported(target)) { + gDevTools.showToolbox(target, "styleeditor").then(function (toolbox) { + let sheet = source || href; + toolbox.getCurrentPanel().selectStyleSheet(sheet, line, column); + }); + } + }); + } +}; + +function ComputedViewTool(inspector, window) { + this.inspector = inspector; + this.document = window.document; + + this.computedView = new CssComputedView(this.inspector, this.document, + this.inspector.pageStyle); + this.boxModelView = new BoxModelView(this.inspector, this.document); + + this.onSelected = this.onSelected.bind(this); + this.refresh = this.refresh.bind(this); + this.onPanelSelected = this.onPanelSelected.bind(this); + this.onMutations = this.onMutations.bind(this); + this.onResized = this.onResized.bind(this); + + this.inspector.selection.on("detached-front", this.onSelected); + this.inspector.selection.on("new-node-front", this.onSelected); + this.inspector.selection.on("pseudoclass", this.refresh); + this.inspector.sidebar.on("computedview-selected", this.onPanelSelected); + this.inspector.pageStyle.on("stylesheet-updated", this.refresh); + this.inspector.walker.on("mutations", this.onMutations); + this.inspector.walker.on("resize", this.onResized); + + this.computedView.selectElement(null); + + this.onSelected(); +} + +ComputedViewTool.prototype = { + isSidebarActive: function () { + if (!this.computedView) { + return false; + } + 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.computedView) { + return; + } + + let isInactive = !this.isSidebarActive() && + this.inspector.selection.nodeFront; + if (isInactive) { + return; + } + + this.computedView.setPageStyle(this.inspector.pageStyle); + + if (!this.inspector.selection.isConnected() || + !this.inspector.selection.isElementNode()) { + this.computedView.selectElement(null); + return; + } + + if (!event || event == "new-node-front") { + let done = this.inspector.updating("computed-view"); + this.computedView.selectElement(this.inspector.selection.nodeFront).then(() => { + done(); + }); + } + }, + + refresh: function () { + if (this.isSidebarActive()) { + this.computedView.refreshPanel(); + } + }, + + onPanelSelected: function () { + if (this.inspector.selection.nodeFront === this.computedView._viewedElement) { + this.refresh(); + } else { + this.onSelected(); + } + }, + + /** + * When markup mutations occur, if an attribute of the selected node changes, + * we need to refresh the view as that might change the node's styles. + */ + onMutations: function (mutations) { + for (let {type, target} of mutations) { + if (target === this.inspector.selection.nodeFront && + type === "attributes") { + this.refresh(); + break; + } + } + }, + + /** + * When the window gets resized, this may cause media-queries to match, and + * therefore, different styles may apply. + */ + onResized: function () { + this.refresh(); + }, + + destroy: function () { + this.inspector.walker.off("mutations", this.onMutations); + this.inspector.walker.off("resize", this.onResized); + this.inspector.sidebar.off("computedview-selected", this.refresh); + this.inspector.selection.off("pseudoclass", this.refresh); + this.inspector.selection.off("new-node-front", this.onSelected); + this.inspector.selection.off("detached-front", this.onSelected); + this.inspector.sidebar.off("computedview-selected", this.onPanelSelected); + if (this.inspector.pageStyle) { + this.inspector.pageStyle.off("stylesheet-updated", this.refresh); + } + + this.computedView.destroy(); + this.boxModelView.destroy(); + + this.computedView = this.boxModelView = this.document = this.inspector = null; + } +}; + +exports.CssComputedView = CssComputedView; +exports.ComputedViewTool = ComputedViewTool; +exports.PropertyView = PropertyView; diff --git a/devtools/client/inspector/computed/moz.build b/devtools/client/inspector/computed/moz.build new file mode 100644 index 000000000..5ce950325 --- /dev/null +++ b/devtools/client/inspector/computed/moz.build @@ -0,0 +1,11 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + 'computed.js', +) + +BROWSER_CHROME_MANIFESTS += ['test/browser.ini'] diff --git a/devtools/client/inspector/computed/test/.eslintrc.js b/devtools/client/inspector/computed/test/.eslintrc.js new file mode 100644 index 000000000..698ae9181 --- /dev/null +++ b/devtools/client/inspector/computed/test/.eslintrc.js @@ -0,0 +1,6 @@ +"use strict"; + +module.exports = { + // Extend from the shared list of defined globals for mochitests. + "extends": "../../../../.eslintrc.mochitests.js" +}; diff --git a/devtools/client/inspector/computed/test/browser.ini b/devtools/client/inspector/computed/test/browser.ini new file mode 100644 index 000000000..33293e1eb --- /dev/null +++ b/devtools/client/inspector/computed/test/browser.ini @@ -0,0 +1,41 @@ +[DEFAULT] +tags = devtools +subsuite = devtools +support-files = + doc_matched_selectors.html + doc_media_queries.html + doc_pseudoelement.html + doc_sourcemaps.css + doc_sourcemaps.css.map + doc_sourcemaps.html + doc_sourcemaps.scss + head.js + !/devtools/client/commandline/test/helpers.js + !/devtools/client/framework/test/shared-head.js + !/devtools/client/inspector/test/head.js + !/devtools/client/inspector/test/shared-head.js + !/devtools/client/shared/test/test-actor.js + !/devtools/client/shared/test/test-actor-registry.js + +[browser_computed_browser-styles.js] +[browser_computed_cycle_color.js] +[browser_computed_getNodeInfo.js] +[browser_computed_keybindings_01.js] +[browser_computed_keybindings_02.js] +[browser_computed_matched-selectors-toggle.js] +[browser_computed_matched-selectors_01.js] +[browser_computed_matched-selectors_02.js] +[browser_computed_media-queries.js] +[browser_computed_no-results-placeholder.js] +[browser_computed_original-source-link.js] +[browser_computed_pseudo-element_01.js] +[browser_computed_refresh-on-style-change_01.js] +[browser_computed_search-filter.js] +[browser_computed_search-filter_clear.js] +[browser_computed_search-filter_context-menu.js] +subsuite = clipboard +[browser_computed_search-filter_escape-keypress.js] +[browser_computed_search-filter_noproperties.js] +[browser_computed_select-and-copy-styles.js] +subsuite = clipboard +[browser_computed_style-editor-link.js] diff --git a/devtools/client/inspector/computed/test/browser_computed_browser-styles.js b/devtools/client/inspector/computed/test/browser_computed_browser-styles.js new file mode 100644 index 000000000..32de63650 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_browser-styles.js @@ -0,0 +1,52 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that the checkbox to include browser styles works properly. + +const TEST_URI = ` + <style type="text/css"> + .matches { + color: #F00; + } + </style> + <span id="matches" class="matches">Some styled text</span> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openComputedView(); + 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 (let propView of propertyViews) { + if (propView.name == name) { + return propView.visible; + } + } + return false; +} diff --git a/devtools/client/inspector/computed/test/browser_computed_cycle_color.js b/devtools/client/inspector/computed/test/browser_computed_cycle_color.js new file mode 100644 index 000000000..c9892fafe --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_cycle_color.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"; + +// Computed view color cycling test. + +const TEST_URI = ` + <style type="text/css"> + .matches { + color: #f00; + } + </style> + <span id="matches" class="matches">Some styled text</span> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openComputedView(); + yield selectNode("#matches", inspector); + + info("Checking the property itself"); + let container = getComputedViewPropertyView(view, "color").valueNode; + checkColorCycling(container, view); + + info("Checking matched selectors"); + container = yield getComputedViewMatchedRules(view, "color"); + yield checkColorCycling(container, view); +}); + +function* checkColorCycling(container, view) { + let valueNode = container.querySelector(".computedview-color"); + let win = view.styleWindow; + + // "Authored" (default; currently the computed value) + is(valueNode.textContent, "rgb(255, 0, 0)", + "Color displayed as an RGB value."); + + let tests = [{ + value: "red", + comment: "Color displayed as a color name." + }, { + value: "#f00", + comment: "Color displayed as an authored value." + }, { + value: "hsl(0, 100%, 50%)", + comment: "Color displayed as an HSL value again." + }, { + value: "rgb(255, 0, 0)", + comment: "Color displayed as an RGB value again." + }]; + + for (let test of tests) { + yield checkSwatchShiftClick(container, win, test.value, test.comment); + } +} + +function* checkSwatchShiftClick(container, win, expectedValue, comment) { + let swatch = container.querySelector(".computedview-colorswatch"); + let valueNode = container.querySelector(".computedview-color"); + swatch.scrollIntoView(); + + let onUnitChange = swatch.once("unit-change"); + EventUtils.synthesizeMouseAtCenter(swatch, { + type: "mousedown", + shiftKey: true + }, win); + yield onUnitChange; + is(valueNode.textContent, expectedValue, comment); +} diff --git a/devtools/client/inspector/computed/test/browser_computed_getNodeInfo.js b/devtools/client/inspector/computed/test/browser_computed_getNodeInfo.js new file mode 100644 index 000000000..30113e7ec --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_getNodeInfo.js @@ -0,0 +1,178 @@ +/* vim: set ft=javascript 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 various output of the computed-view's getNodeInfo method. +// This method is used by the HighlightersOverlay and TooltipsOverlay on mouseover to +// decide which highlighter or tooltip 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 +} = require("devtools/client/inspector/shared/node-types"); + +const TEST_URI = ` + <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> +`; + +// 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, "rgb(255, 0, 0)"); + } + }, + { + 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, "rgb(255, 0, 0)"); + } + }, + { + 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 el = yield getComputedViewMatchedRules(view, "background-color"); + return el.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 el = yield getComputedViewMatchedRules(view, "background-color"); + return el.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 el = yield getComputedViewMatchedRules(view, "color"); + return el.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 el = yield getComputedViewMatchedRules(view, "color"); + return el.querySelector(".other-property-value"); + }, + assertNodeInfo: function (nodeInfo) { + is(nodeInfo.type, VIEW_NODE_VALUE_TYPE); + is(nodeInfo.value.property, "color"); + is(nodeInfo.value.value, "red"); + } + }, + { + desc: "Testing a matched rule stylesheet link", + getHoveredNode: function* (view) { + let el = yield getComputedViewMatchedRules(view, "color"); + return el.querySelector(".rule-link .theme-link"); + }, + assertNodeInfo: function (nodeInfo) { + is(nodeInfo, null); + } + } +]; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + 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/devtools/client/inspector/computed/test/browser_computed_keybindings_01.js b/devtools/client/inspector/computed/test/browser_computed_keybindings_01.js new file mode 100644 index 000000000..199e125af --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_keybindings_01.js @@ -0,0 +1,83 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests computed view key bindings. + +const TEST_URI = ` + <style type="text/css"> + .matches { + color: #F00; + } + </style> + <span class="matches">Some styled text</span> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openComputedView(); + yield selectNode(".matches", inspector); + + let propView = getFirstVisiblePropertyView(view); + let rulesTable = propView.matchedSelectorsContainer; + let matchedExpander = propView.element; + + info("Focusing the property"); + matchedExpander.scrollIntoView(); + 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 = defer(); + + let propView = getFirstVisiblePropertyView(view); + propView.mdnLinkClick = function (event) { + ok(true, "Pressing F1 opened the MDN link"); + def.resolve(); + }; + + EventUtils.synthesizeKey("VK_F1", {}, view.styleWindow); + return def.promise; +} diff --git a/devtools/client/inspector/computed/test/browser_computed_keybindings_02.js b/devtools/client/inspector/computed/test/browser_computed_keybindings_02.js new file mode 100644 index 000000000..2a9220ec8 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_keybindings_02.js @@ -0,0 +1,66 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests the computed-view keyboard navigation. + +const TEST_URI = ` + <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"> + <h1>Some header text</h1> + <p id="salutation" style="font-size: 12pt">hi.</p> + <p id="body" style="font-size: 12pt">I am a test-case. This text exists + solely to provide some things to <span style="color: yellow"> + highlight</span> and <span style="font-weight: bold">count</span> + style list-items in the box at right. If you are reading this, + you should go do something else instead. Maybe read a book. Or better + yet, write some test-cases for another bit of code. + <span style="font-style: italic">some text</span></p> + <p id="closing">more text</p> + <p>even more text</p> + </div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openComputedView(); + 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/devtools/client/inspector/computed/test/browser_computed_matched-selectors-toggle.js b/devtools/client/inspector/computed/test/browser_computed_matched-selectors-toggle.js new file mode 100644 index 000000000..abbbb77be --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_matched-selectors-toggle.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"; + +// Tests that the computed view properties can be expanded and collapsed with +// either the twisty or by dbl-clicking on the container. + +const TEST_URI = ` + <style type="text/css"> , + html { color: #000000; font-size: 15pt; } + h1 { color: red; } + </style> + <h1>Some header text</h1> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openComputedView(); + 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("#propertyContainer .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("#propertyContainer .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"); + + container.scrollIntoView(); + + 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/devtools/client/inspector/computed/test/browser_computed_matched-selectors_01.js b/devtools/client/inspector/computed/test/browser_computed_matched-selectors_01.js new file mode 100644 index 000000000..66cabe7a9 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_matched-selectors_01.js @@ -0,0 +1,40 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Checking selector counts, matched rules and titles in the computed-view. + +const {PropertyView} = + require("devtools/client/inspector/computed/computed"); +const TEST_URI = URL_ROOT + "doc_matched_selectors.html"; + +add_task(function* () { + yield addTab(TEST_URI); + let {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/devtools/client/inspector/computed/test/browser_computed_matched-selectors_02.js b/devtools/client/inspector/computed/test/browser_computed_matched-selectors_02.js new file mode 100644 index 000000000..43172d55f --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_matched-selectors_02.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 for matched selector texts in the computed view. + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8,<div style='color:blue;'></div>"); + let {inspector, view} = yield openComputedView(); + 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/devtools/client/inspector/computed/test/browser_computed_media-queries.js b/devtools/client/inspector/computed/test/browser_computed_media-queries.js new file mode 100644 index 000000000..79cccb49b --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_media-queries.js @@ -0,0 +1,36 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that we correctly display appropriate media query titles in the +// property view. + +const TEST_URI = URL_ROOT + "doc_media_queries.html"; + +var {PropertyView} = require("devtools/client/inspector/computed/computed"); + +add_task(function* () { + yield addTab(TEST_URI); + let {inspector, view} = yield openComputedView(); + yield selectNode("div", inspector); + 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/devtools/client/inspector/computed/test/browser_computed_no-results-placeholder.js b/devtools/client/inspector/computed/test/browser_computed_no-results-placeholder.js new file mode 100644 index 000000000..b1371abd7 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_no-results-placeholder.js @@ -0,0 +1,70 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that the no results placeholder works properly. + +const TEST_URI = ` + <style type="text/css"> + .matches { + color: #F00; + } + </style> + <span id="matches" class="matches">Some styled text</span> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openComputedView(); + 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(); + synthesizeKeys(searchTerm, 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/devtools/client/inspector/computed/test/browser_computed_original-source-link.js b/devtools/client/inspector/computed/test/browser_computed_original-source-link.js new file mode 100644 index 000000000..1bceed4e3 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_original-source-link.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 computed view shows the original source link when source maps +// are enabled. + +const TESTCASE_URI = 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(); + 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} = 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/devtools/client/inspector/computed/test/browser_computed_pseudo-element_01.js b/devtools/client/inspector/computed/test/browser_computed_pseudo-element_01.js new file mode 100644 index 000000000..9ca5451a5 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_pseudo-element_01.js @@ -0,0 +1,39 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that pseudoelements are displayed correctly in the rule view. + +const TEST_URI = URL_ROOT + "doc_pseudoelement.html"; + +add_task(function* () { + yield addTab(TEST_URI); + let {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/devtools/client/inspector/computed/test/browser_computed_refresh-on-style-change_01.js b/devtools/client/inspector/computed/test/browser_computed_refresh-on-style-change_01.js new file mode 100644 index 000000000..43f210307 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_refresh-on-style-change_01.js @@ -0,0 +1,30 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that the computed view refreshes when the current node has its style +// changed. + +const TEST_URI = "<div id='testdiv' style='font-size:10px;'>Test div!</div>"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view, testActor} = 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"); + yield testActor.setAttribute("#testdiv", "style", + "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, "rgb(255, 0, 0)", "The computed view also shows the color now"); +}); diff --git a/devtools/client/inspector/computed/test/browser_computed_search-filter.js b/devtools/client/inspector/computed/test/browser_computed_search-filter.js new file mode 100644 index 000000000..10ba82293 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_search-filter.js @@ -0,0 +1,66 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that the search filter works properly. + +const TEST_URI = ` + <style type="text/css"> + .matches { + color: #F00; + } + </style> + <span id="matches" class="matches">Some styled text</span> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openComputedView(); + yield selectNode("#matches", inspector); + yield testToggleDefaultStyles(inspector, view); + yield testAddTextInFilter(inspector, view); +}); + +function* testToggleDefaultStyles(inspector, computedView) { + info("checking \"Browser styles\" checkbox"); + let checkbox = computedView.includeBrowserStylesCheckbox; + 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 boxModelWrapper = doc.querySelector("#boxmodel-wrapper"); + let searchField = computedView.searchField; + let onRefreshed = inspector.once("computed-view-refreshed"); + let win = computedView.styleWindow; + + // First check to make sure that accel + F doesn't focus search if the + // container isn't focused + inspector.panelWin.focus(); + EventUtils.synthesizeKey("f", { accelKey: true }); + isnot(inspector.panelDoc.activeElement, searchField, + "Search field isn't focused"); + + computedView.element.focus(); + EventUtils.synthesizeKey("f", { accelKey: true }); + is(inspector.panelDoc.activeElement, searchField, "Search field is focused"); + + synthesizeKeys("color", win); + yield onRefreshed; + + ok(boxModelWrapper.hidden, "Box model is hidden"); + + info("check that the correct properties are visible"); + + let propertyViews = computedView.propertyViews; + propertyViews.forEach(propView => { + let name = propView.name; + is(propView.visible, name.indexOf("color") > -1, + "span " + name + " property visibility check"); + }); +} diff --git a/devtools/client/inspector/computed/test/browser_computed_search-filter_clear.js b/devtools/client/inspector/computed/test/browser_computed_search-filter_clear.js new file mode 100644 index 000000000..bd989854f --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_search-filter_clear.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"; + +// Tests that the search filter clear button works properly. + +const TEST_URI = ` + <style type="text/css"> + .matches { + color: #F00; + background-color: #00F; + border-color: #0F0; + } + </style> + <span id="matches" class="matches">Some styled text</span> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openComputedView(); + yield selectNode("#matches", inspector); + yield testAddTextInFilter(inspector, view); + yield testClearSearchFilter(inspector, view); +}); + +function* testAddTextInFilter(inspector, computedView) { + info("Setting filter text to \"background-color\""); + + let win = computedView.styleWindow; + let propertyViews = computedView.propertyViews; + let searchField = computedView.searchField; + + searchField.focus(); + synthesizeKeys("background-color", win); + yield inspector.once("computed-view-refreshed"); + + info("Check that the correct properties are visible"); + + propertyViews.forEach((propView) => { + let name = propView.name; + is(propView.visible, name.indexOf("background-color") > -1, + "span " + name + " property visibility check"); + }); +} + +function* testClearSearchFilter(inspector, computedView) { + info("Clearing the search filter"); + + let win = computedView.styleWindow; + let doc = computedView.styleDocument; + let boxModelWrapper = doc.querySelector("#boxmodel-wrapper"); + let propertyViews = computedView.propertyViews; + let searchField = computedView.searchField; + let searchClearButton = computedView.searchClearButton; + let onRefreshed = inspector.once("computed-view-refreshed"); + + EventUtils.synthesizeMouseAtCenter(searchClearButton, {}, win); + yield onRefreshed; + + ok(!boxModelWrapper.hidden, "Box model is displayed"); + + info("Check that the correct properties are visible"); + + ok(!searchField.value, "Search filter is cleared"); + propertyViews.forEach((propView) => { + is(propView.visible, propView.hasMatchedSelectors, + "span " + propView.name + " property visibility check"); + }); +} diff --git a/devtools/client/inspector/computed/test/browser_computed_search-filter_context-menu.js b/devtools/client/inspector/computed/test/browser_computed_search-filter_context-menu.js new file mode 100644 index 000000000..b5dbe4475 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_search-filter_context-menu.js @@ -0,0 +1,84 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests computed view search filter context menu works properly. + +const TEST_INPUT = "h1"; + +const TEST_URI = "<h1>test filter context menu</h1>"; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {toolbox, inspector, view} = yield openComputedView(); + yield selectNode("h1", inspector); + + let win = view.styleWindow; + let searchField = view.searchField; + let searchContextMenu = toolbox.textBoxContextMenuPopup; + ok(searchContextMenu, + "The search filter context menu is loaded in the computed view"); + + let cmdUndo = searchContextMenu.querySelector("[command=cmd_undo]"); + let cmdDelete = searchContextMenu.querySelector("[command=cmd_delete]"); + let cmdSelectAll = searchContextMenu.querySelector("[command=cmd_selectAll]"); + let cmdCut = searchContextMenu.querySelector("[command=cmd_cut]"); + let cmdCopy = searchContextMenu.querySelector("[command=cmd_copy]"); + let cmdPaste = searchContextMenu.querySelector("[command=cmd_paste]"); + + info("Opening context menu"); + + emptyClipboard(); + + let onFocus = once(searchField, "focus"); + searchField.focus(); + yield onFocus; + + let onContextMenuPopup = once(searchContextMenu, "popupshowing"); + EventUtils.synthesizeMouse(searchField, 2, 2, + {type: "contextmenu", button: 2}, win); + yield onContextMenuPopup; + + is(cmdUndo.getAttribute("disabled"), "true", "cmdUndo is disabled"); + is(cmdDelete.getAttribute("disabled"), "true", "cmdDelete is disabled"); + is(cmdSelectAll.getAttribute("disabled"), "true", "cmdSelectAll is disabled"); + + // Cut/Copy items are enabled in context menu even if there + // is no selection. See also Bug 1303033 + is(cmdCut.getAttribute("disabled"), "", "cmdCut is enabled"); + is(cmdCopy.getAttribute("disabled"), "", "cmdCopy is enabled"); + + if (isWindows()) { + // emptyClipboard only works on Windows (666254), assert paste only for this OS. + is(cmdPaste.getAttribute("disabled"), "true", "cmdPaste is disabled"); + } + + info("Closing context menu"); + let onContextMenuHidden = once(searchContextMenu, "popuphidden"); + searchContextMenu.hidePopup(); + yield onContextMenuHidden; + + info("Copy text in search field using the context menu"); + searchField.value = TEST_INPUT; + searchField.select(); + EventUtils.synthesizeMouse(searchField, 2, 2, + {type: "contextmenu", button: 2}, win); + yield onContextMenuPopup; + yield waitForClipboardPromise(() => cmdCopy.click(), TEST_INPUT); + searchContextMenu.hidePopup(); + yield onContextMenuHidden; + + info("Reopen context menu and check command properties"); + EventUtils.synthesizeMouse(searchField, 2, 2, + {type: "contextmenu", button: 2}, win); + yield onContextMenuPopup; + + is(cmdUndo.getAttribute("disabled"), "", "cmdUndo is enabled"); + is(cmdDelete.getAttribute("disabled"), "", "cmdDelete is enabled"); + is(cmdSelectAll.getAttribute("disabled"), "", "cmdSelectAll is enabled"); + is(cmdCut.getAttribute("disabled"), "", "cmdCut is enabled"); + is(cmdCopy.getAttribute("disabled"), "", "cmdCopy is enabled"); + is(cmdPaste.getAttribute("disabled"), "", "cmdPaste is enabled"); +}); diff --git a/devtools/client/inspector/computed/test/browser_computed_search-filter_escape-keypress.js b/devtools/client/inspector/computed/test/browser_computed_search-filter_escape-keypress.js new file mode 100644 index 000000000..e52e2cc89 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_search-filter_escape-keypress.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"; + +// Avoid test timeouts on Linux debug builds where the test takes just a bit too long to +// run (see bug 1258081). +requestLongerTimeout(2); + +// Tests that search filter escape keypress will clear the search field. + +const TEST_URI = ` + <style type="text/css"> + .matches { + color: #F00; + } + </style> + <span id="matches" class="matches">Some styled text</span> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openComputedView(); + yield selectNode("#matches", inspector); + yield testAddTextInFilter(inspector, view); + yield testEscapeKeypress(inspector, view); +}); + +function* testAddTextInFilter(inspector, computedView) { + info("Setting filter text to \"background-color\""); + + let win = computedView.styleWindow; + let propertyViews = computedView.propertyViews; + let searchField = computedView.searchField; + let checkbox = computedView.includeBrowserStylesCheckbox; + + info("Include browser styles"); + checkbox.click(); + yield inspector.once("computed-view-refreshed"); + + searchField.focus(); + synthesizeKeys("background-color", win); + yield inspector.once("computed-view-refreshed"); + + info("Check that the correct properties are visible"); + + propertyViews.forEach((propView) => { + let name = propView.name; + is(propView.visible, name.indexOf("background-color") > -1, + "span " + name + " property visibility check"); + }); +} + +function* testEscapeKeypress(inspector, computedView) { + info("Pressing the escape key on search filter"); + + let win = computedView.styleWindow; + let propertyViews = computedView.propertyViews; + let searchField = computedView.searchField; + let onRefreshed = inspector.once("computed-view-refreshed"); + + searchField.focus(); + EventUtils.synthesizeKey("VK_ESCAPE", {}, win); + yield onRefreshed; + + info("Check that the correct properties are visible"); + + ok(!searchField.value, "Search filter is cleared"); + propertyViews.forEach((propView) => { + let name = propView.name; + is(propView.visible, true, + "span " + name + " property is visible"); + }); +} diff --git a/devtools/client/inspector/computed/test/browser_computed_search-filter_noproperties.js b/devtools/client/inspector/computed/test/browser_computed_search-filter_noproperties.js new file mode 100644 index 000000000..99ee6d58a --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_search-filter_noproperties.js @@ -0,0 +1,61 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that the "no-results" message is displayed when selecting an invalid element or +// when all properties have been filtered out. + +const TEST_URI = ` + <style type="text/css"> + .matches { + color: #F00; + background-color: #00F; + border-color: #0F0; + } + </style> + <div> + <!-- comment node --> + <span id="matches" class="matches">Some styled text</span> + </div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openComputedView(); + let propertyViews = view.propertyViews; + + info("Select the #matches node"); + let matchesNode = yield getNodeFront("#matches", inspector); + let onRefresh = inspector.once("computed-view-refreshed"); + yield selectNode(matchesNode, inspector); + yield onRefresh; + + ok(propertyViews.filter(p => p.visible).length > 0, "CSS properties are displayed"); + ok(view.noResults.hasAttribute("hidden"), "no-results message is hidden"); + + info("Select a comment node"); + let commentNode = yield inspector.walker.previousSibling(matchesNode); + yield selectNode(commentNode, inspector); + + is(propertyViews.filter(p => p.visible).length, 0, "No properties displayed"); + ok(!view.noResults.hasAttribute("hidden"), "no-results message is displayed"); + + info("Select the #matches node again"); + onRefresh = inspector.once("computed-view-refreshed"); + yield selectNode(matchesNode, inspector); + yield onRefresh; + + ok(propertyViews.filter(p => p.visible).length > 0, "CSS properties are displayed"); + ok(view.noResults.hasAttribute("hidden"), "no-results message is hidden"); + + info("Filter by 'will-not-match' and check the no-results message is displayed"); + let searchField = view.searchField; + searchField.focus(); + synthesizeKeys("will-not-match", view.styleWindow); + yield inspector.once("computed-view-refreshed"); + + is(propertyViews.filter(p => p.visible).length, 0, "No properties displayed"); + ok(!view.noResults.hasAttribute("hidden"), "no-results message is displayed"); +}); diff --git a/devtools/client/inspector/computed/test/browser_computed_select-and-copy-styles.js b/devtools/client/inspector/computed/test/browser_computed_select-and-copy-styles.js new file mode 100644 index 000000000..ce8be59ad --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_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. + +const osString = Services.appinfo.OS; + +const TEST_URI = ` + <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"> + <h1>Some header text</h1> + <p id="salutation" style="font-size: 12pt">hi.</p> + <p id="body" style="font-size: 12pt">I am a test-case. This text exists + solely to provide some things to <span style="color: yellow"> + highlight</span> and <span style="font-weight: bold">count</span> + style list-items in the box at right. If you are reading this, + you should go do something else instead. Maybe read a book. Or better + yet, write some test-cases for another bit of code. + <span style="font-style: italic">some text</span></p> + <p id="closing">more text</p> + <p>even more text</p> + </div> +`; + +add_task(function* () { + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + let {inspector, view} = yield openComputedView(); + 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], 2); + 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]*"; + + try { + yield waitForClipboardPromise(() => fireCopyEvent(props[0]), + () => checkClipboardData(expectedPattern)); + } catch (e) { + failedClipboard(expectedPattern); + } +} + +function* checkSelectAll(view) { + info("Testing select-all copy"); + + let contentDoc = view.styleDocument; + let prop = contentDoc.querySelector(".property-view"); + + info("Checking that _onSelectAll() then copy returns the correct " + + "clipboard value"); + view._contextmenu._onSelectAll(); + let expectedPattern = "color: rgb\\(255, 255, 0\\);[\\r\\n]+" + + "font-family: helvetica,sans-serif;[\\r\\n]+" + + "font-size: 16px;[\\r\\n]+" + + "font-variant-caps: small-caps;[\\r\\n]*"; + + try { + yield waitForClipboardPromise(() => fireCopyEvent(prop), + () => checkClipboardData(expectedPattern)); + } catch (e) { + failedClipboard(expectedPattern); + } +} + +function checkClipboardData(expectedPattern) { + let actual = SpecialPowers.getClipboardData("text/unicode"); + let expectedRegExp = new RegExp(expectedPattern, "g"); + return expectedRegExp.test(actual); +} + +function failedClipboard(expectedPattern) { + // Format expected text for comparison + let terminator = osString == "WINNT" ? "\r\n" : "\n"; + expectedPattern = expectedPattern.replace(/\[\\r\\n\][+*]/g, terminator); + expectedPattern = expectedPattern.replace(/\\\(/g, "("); + expectedPattern = expectedPattern.replace(/\\\)/g, ")"); + + let actual = SpecialPowers.getClipboardData("text/unicode"); + + // Trim the right hand side of our strings. This is because expectedPattern + // accounts for windows sometimes adding a newline to our copied data. + expectedPattern = expectedPattern.trimRight(); + actual = actual.trimRight(); + + dump("TEST-UNEXPECTED-FAIL | Clipboard text does not match expected ... " + + "results (escaped for accurate comparison):\n"); + info("Actual: " + escape(actual)); + info("Expected: " + escape(expectedPattern)); +} diff --git a/devtools/client/inspector/computed/test/browser_computed_style-editor-link.js b/devtools/client/inspector/computed/test/browser_computed_style-editor-link.js new file mode 100644 index 000000000..6a95fd83f --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_style-editor-link.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"; + +// Whitelisting this test. +// As part of bug 1077403, the leaking uncaught rejection should be fixed. +thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Unknown sheet source"); + +// Tests the links from the computed view to the style editor. + +const STYLESHEET_URL = "data:text/css," + encodeURIComponent( + ".highlight {color: blue}"); + +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>`); + +add_task(function* () { + yield addTab(DOCUMENT_URL); + let {toolbox, inspector, view, testActor} = yield openComputedView(); + yield selectNode("span", inspector); + + yield testInlineStyle(view); + yield testFirstInlineStyleSheet(view, toolbox, testActor); + yield testSecondInlineStyleSheet(view, toolbox, testActor); + yield testExternalStyleSheet(view, toolbox, testActor); +}); + +function* testInlineStyle(view) { + info("Testing inline style"); + + yield expandComputedViewPropertyByIndex(view, 0); + + let onTab = waitForTab(); + info("Clicking on the first rule-link in the computed-view"); + clickLinkByIndex(view, 0); + + let tab = yield onTab; + + let tabURI = tab.linkedBrowser.documentURI.spec; + ok(tabURI.startsWith("view-source:"), "View source tab is open"); + info("Closing tab"); + gBrowser.removeTab(tab); +} + +function* testFirstInlineStyleSheet(view, toolbox, testActor) { + info("Testing inline stylesheet"); + + info("Listening for toolbox switch to the styleeditor"); + let onSwitch = waitForStyleEditor(toolbox); + + info("Clicking an inline stylesheet"); + clickLinkByIndex(view, 2); + let editor = yield onSwitch; + + ok(true, "Switched to the style-editor panel in the toolbox"); + + yield validateStyleEditorSheet(editor, 0, testActor); +} + +function* testSecondInlineStyleSheet(view, toolbox, testActor) { + info("Testing second inline stylesheet"); + + info("Waiting for the stylesheet editor to be selected"); + let panel = toolbox.getCurrentPanel(); + let onSelected = panel.UI.once("editor-selected"); + + info("Switching back to the inspector panel in the toolbox"); + yield toolbox.selectTool("inspector"); + + info("Clicking on second inline stylesheet link"); + clickLinkByIndex(view, 4); + let editor = yield onSelected; + + is(toolbox.currentToolId, "styleeditor", + "The style editor is selected again"); + yield validateStyleEditorSheet(editor, 1, testActor); +} + +function* testExternalStyleSheet(view, toolbox, testActor) { + info("Testing external stylesheet"); + + info("Waiting for the stylesheet editor to be selected"); + let panel = toolbox.getCurrentPanel(); + let onSelected = panel.UI.once("editor-selected"); + + info("Switching back to the inspector panel in the toolbox"); + yield toolbox.selectTool("inspector"); + + info("Clicking on an external stylesheet link"); + clickLinkByIndex(view, 1); + let editor = yield onSelected; + + is(toolbox.currentToolId, "styleeditor", + "The style editor is selected again"); + yield validateStyleEditorSheet(editor, 2, testActor); +} + +function* validateStyleEditorSheet(editor, expectedSheetIndex, testActor) { + info("Validating style editor stylesheet"); + let expectedHref = yield testActor.eval(` + document.styleSheets[${expectedSheetIndex}].href; + `); + is(editor.styleSheet.href, expectedHref, + "loaded stylesheet matches document stylesheet"); +} + +function clickLinkByIndex(view, index) { + let link = getComputedViewLinkByIndex(view, index); + link.scrollIntoView(); + link.click(); +} diff --git a/devtools/client/inspector/computed/test/doc_matched_selectors.html b/devtools/client/inspector/computed/test/doc_matched_selectors.html new file mode 100644 index 000000000..8fe007409 --- /dev/null +++ b/devtools/client/inspector/computed/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/devtools/client/inspector/computed/test/doc_media_queries.html b/devtools/client/inspector/computed/test/doc_media_queries.html new file mode 100644 index 000000000..819e1ea7a --- /dev/null +++ b/devtools/client/inspector/computed/test/doc_media_queries.html @@ -0,0 +1,21 @@ +<html> +<head> + <title>test</title> + <style> + div { + width: 1000px; + height: 100px; + background-color: #f00; + } + + @media screen and (min-width: 1px) { + div { + width: 200px; + } + } + </style> +</head> +<body> +<div></div> +</body> +</html> diff --git a/devtools/client/inspector/computed/test/doc_pseudoelement.html b/devtools/client/inspector/computed/test/doc_pseudoelement.html new file mode 100644 index 000000000..6145d4bf1 --- /dev/null +++ b/devtools/client/inspector/computed/test/doc_pseudoelement.html @@ -0,0 +1,131 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> + <head> + <style> + +body { + color: #333; +} + +.box { + float:left; + width: 128px; + height: 128px; + background: #ddd; + padding: 32px; + margin: 32px; + position:relative; +} + +.box:first-line { + color: orange; + background: red; +} + +.box:first-letter { + color: green; +} + +* { + cursor: default; +} + +nothing { + cursor: pointer; +} + +p::-moz-selection { + color: white; + background: black; +} +p::selection { + color: white; + background: black; +} + +p:first-line { + background: blue; +} +p:first-letter { + color: red; + font-size: 130%; +} + +.box:before { + background: green; + content: " "; + position: absolute; + height:32px; + width:32px; +} + +.box:after { + background: red; + content: " "; + position: absolute; + border-radius: 50%; + height:32px; + width:32px; + top: 50%; + left: 50%; + margin-top: -16px; + margin-left: -16px; +} + +.topleft:before { + top:0; + left:0; +} + +.topleft:first-line { + color: orange; +} +.topleft::selection { + color: orange; +} + +.topright:before { + top:0; + right:0; +} + +.bottomright:before { + bottom:10px; + right:10px; + color: red; +} + +.bottomright:before { + bottom:0; + right:0; +} + +.bottomleft:before { + bottom:0; + left:0; +} + + </style> + </head> + <body> + <h1>ruleview pseudoelement($("test"));</h1> + + <div id="topleft" class="box topleft"> + <p>Top Left<br />Position</p> + </div> + + <div id="topright" class="box topright"> + <p>Top Right<br />Position</p> + </div> + + <div id="bottomright" class="box bottomright"> + <p>Bottom Right<br />Position</p> + </div> + + <div id="bottomleft" class="box bottomleft"> + <p>Bottom Left<br />Position</p> + </div> + + </body> +</html> diff --git a/devtools/client/inspector/computed/test/doc_sourcemaps.css b/devtools/client/inspector/computed/test/doc_sourcemaps.css new file mode 100644 index 000000000..a9b437a40 --- /dev/null +++ b/devtools/client/inspector/computed/test/doc_sourcemaps.css @@ -0,0 +1,7 @@ +div { + color: #ff0066; } + +span { + background-color: #EEE; } + +/*# sourceMappingURL=doc_sourcemaps.css.map */
\ No newline at end of file diff --git a/devtools/client/inspector/computed/test/doc_sourcemaps.css.map b/devtools/client/inspector/computed/test/doc_sourcemaps.css.map new file mode 100644 index 000000000..0f7486fd9 --- /dev/null +++ b/devtools/client/inspector/computed/test/doc_sourcemaps.css.map @@ -0,0 +1,7 @@ +{ +"version": 3, +"mappings": "AAGA,GAAI;EACF,KAAK,EAHU,OAAI;;AAMrB,IAAK;EACH,gBAAgB,EAAE,IAAI", +"sources": ["doc_sourcemaps.scss"], +"names": [], +"file": "doc_sourcemaps.css" +} diff --git a/devtools/client/inspector/computed/test/doc_sourcemaps.html b/devtools/client/inspector/computed/test/doc_sourcemaps.html new file mode 100644 index 000000000..0014e55fe --- /dev/null +++ b/devtools/client/inspector/computed/test/doc_sourcemaps.html @@ -0,0 +1,11 @@ +<!doctype html> +<html> +<head> + <title>testcase for testing CSS source maps</title> + <link rel="stylesheet" type="text/css" href="simple.css"/> + <link rel="stylesheet" type="text/css" href="doc_sourcemaps.css"/> +</head> +<body> + <div>source maps <span>testcase</span></div> +</body> +</html> diff --git a/devtools/client/inspector/computed/test/doc_sourcemaps.scss b/devtools/client/inspector/computed/test/doc_sourcemaps.scss new file mode 100644 index 000000000..0ff6c471b --- /dev/null +++ b/devtools/client/inspector/computed/test/doc_sourcemaps.scss @@ -0,0 +1,10 @@ + +$paulrougetpink: #f06; + +div { + color: $paulrougetpink; +} + +span { + background-color: #EEE; +}
\ No newline at end of file diff --git a/devtools/client/inspector/computed/test/head.js b/devtools/client/inspector/computed/test/head.js new file mode 100644 index 000000000..17c47be1a --- /dev/null +++ b/devtools/client/inspector/computed/test/head.js @@ -0,0 +1,157 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint no-unused-vars: [2, {"vars": "local"}] */ +/* import-globals-from ../../test/head.js */ +"use strict"; + +// Import the inspector's head.js first (which itself imports shared-head.js). +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/inspector/test/head.js", + this); + +registerCleanupFunction(() => { + Services.prefs.clearUserPref("devtools.defaultColorUnit"); +}); + +/** + * 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); +} + +/** + * Get references to the name and value span nodes corresponding to a given + * property name in the computed-view + * + * @param {CssComputedView} view + * The instance of the computed view panel + * @param {String} name + * The name of the property to retrieve + * @return an object {nameSpan, valueSpan} + */ +function getComputedViewProperty(view, name) { + let prop; + for (let property of view.styleDocument.querySelectorAll(".property-view")) { + let nameSpan = property.querySelector(".property-name"); + let valueSpan = property.querySelector(".property-value"); + + if (nameSpan.textContent === name) { + prop = {nameSpan: nameSpan, valueSpan: valueSpan}; + break; + } + } + return prop; +} + +/** + * Get an instance of PropertyView from the computed-view. + * + * @param {CssComputedView} 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 {CssComputedView} 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 + */ +var 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 {CssComputedView} view + * The instance of the computed view panel + * @param {String} name + * The name of the property to retrieve + * @return {String} The property value + */ +function getComputedViewPropertyValue(view, name, propertyName) { + return getComputedViewProperty(view, name, propertyName) + .valueSpan.textContent; +} + +/** + * Expand a given property, given its index in the current property list of + * the computed view + * + * @param {CssComputedView} 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("#propertyContainer .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 {CssComputedView} 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]; +} |