diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /devtools/client/inspector/computed | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/inspector/computed')
43 files changed, 5224 insertions, 0 deletions
diff --git a/devtools/client/inspector/computed/computed.js b/devtools/client/inspector/computed/computed.js new file mode 100644 index 0000000000..7d2c129c7b --- /dev/null +++ b/devtools/client/inspector/computed/computed.js @@ -0,0 +1,1713 @@ +/* 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 flags = require("resource://devtools/shared/flags.js"); +const ToolDefinitions = + require("resource://devtools/client/definitions.js").Tools; +const CssLogic = require("resource://devtools/shared/inspector/css-logic.js"); +const { + style: { ELEMENT_STYLE }, +} = require("resource://devtools/shared/constants.js"); +const OutputParser = require("resource://devtools/client/shared/output-parser.js"); +const { PrefObserver } = require("resource://devtools/client/shared/prefs.js"); +const { + createChild, +} = require("resource://devtools/client/inspector/shared/utils.js"); +const { + VIEW_NODE_SELECTOR_TYPE, + VIEW_NODE_PROPERTY_TYPE, + VIEW_NODE_VALUE_TYPE, + VIEW_NODE_IMAGE_URL_TYPE, + VIEW_NODE_FONT_TYPE, +} = require("resource://devtools/client/inspector/shared/node-types.js"); +const TooltipsOverlay = require("resource://devtools/client/inspector/shared/tooltips-overlay.js"); + +loader.lazyRequireGetter( + this, + "StyleInspectorMenu", + "resource://devtools/client/inspector/shared/style-inspector-menu.js" +); +loader.lazyRequireGetter( + this, + "KeyShortcuts", + "resource://devtools/client/shared/key-shortcuts.js" +); +loader.lazyRequireGetter( + this, + "clipboardHelper", + "resource://devtools/shared/platform/clipboard.js" +); +loader.lazyRequireGetter( + this, + "openContentLink", + "resource://devtools/client/shared/link.js", + true +); + +const STYLE_INSPECTOR_PROPERTIES = + "devtools/shared/locales/styleinspector.properties"; +const { LocalizationHelper } = require("resource://devtools/shared/l10n.js"); +const STYLE_INSPECTOR_L10N = new LocalizationHelper(STYLE_INSPECTOR_PROPERTIES); +const L10N_TWISTY_EXPAND_LABEL = STYLE_INSPECTOR_L10N.getStr( + "rule.twistyExpand.label" +); +const L10N_TWISTY_COLLAPSE_LABEL = STYLE_INSPECTOR_L10N.getStr( + "rule.twistyCollapse.label" +); + +const FILTER_CHANGED_TIMEOUT = 150; + +/** + * 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() { + 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() { + if (this._timeout) { + clearTimeout(this._timeout); + this._timeout = 0; + } + this.canceled = true; + this.onCancel(); + }, + + _timeoutHandler() { + 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() { + const time = Date.now(); + while (!this.canceled) { + const 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() { + 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. + */ +function CssComputedView(inspector, document) { + this.inspector = inspector; + this.styleDocument = document; + this.styleWindow = this.styleDocument.defaultView; + + this.propertyViews = []; + + this._outputParser = new OutputParser(document, inspector.cssProperties); + + // Create bound methods. + this.focusWindow = this.focusWindow.bind(this); + this._onClearSearch = this._onClearSearch.bind(this); + this._onClick = this._onClick.bind(this); + this._onContextMenu = this._onContextMenu.bind(this); + this._onCopy = this._onCopy.bind(this); + this._onFilterStyles = this._onFilterStyles.bind(this); + this._onIncludeBrowserStyles = this._onIncludeBrowserStyles.bind(this); + this.refreshPanel = this.refreshPanel.bind(this); + + const doc = this.styleDocument; + this.element = doc.getElementById("computed-property-container"); + this.searchField = doc.getElementById("computed-searchbox"); + this.searchClearButton = doc.getElementById("computed-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", event => + this._onShortcut("CmdOrCtrl+F", event) + ); + this.shortcuts.on("Escape", event => this._onShortcut("Escape", event)); + this.styleDocument.addEventListener("copy", this._onCopy); + this.styleDocument.addEventListener("mousedown", this.focusWindow); + this.element.addEventListener("click", this._onClick); + this.element.addEventListener("contextmenu", this._onContextMenu); + this.searchField.addEventListener("input", this._onFilterStyles); + this.searchClearButton.addEventListener("click", this._onClearSearch); + this.includeBrowserStylesCheckbox.addEventListener( + "input", + this._onIncludeBrowserStyles + ); + + if (flags.testing) { + // In tests, we start listening immediately to avoid having to simulate a mousemove. + this.highlighters.addToView(this); + } else { + this.element.addEventListener( + "mousemove", + () => { + this.highlighters.addToView(this); + }, + { once: true } + ); + } + + if (!this.inspector.is3PaneModeEnabled) { + // When the rules view is added in 3 pane mode, refresh the Computed view whenever + // the rules are changed. + this.inspector.on( + "ruleview-added", + () => { + this.ruleView.on("ruleview-changed", this.refreshPanel); + }, + { once: true } + ); + } + + if (this.ruleView) { + this.ruleView.on("ruleview-changed", this.refreshPanel); + } + + this.searchClearButton.hidden = true; + + // No results text. + this.noResults = this.styleDocument.getElementById("computed-no-results"); + + // Refresh panel when color unit changed or pref for showing + // original sources changes. + this._handlePrefChange = this._handlePrefChange.bind(this); + this._prefObserver = new PrefObserver("devtools."); + this._prefObserver.on("devtools.defaultColorUnit", this._handlePrefChange); + + // The element that we're inspecting, and the document that it comes from. + this._viewedElement = null; + // The PageStyle front related to the currently selected element + this.viewedElementPageStyle = null; + + this.createStyleViews(); + + // Add the tooltips and highlightersoverlay + this.tooltips = new TooltipsOverlay(this); +} + +/** + * 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, + + // Number of visible properties + numVisibleProperties: 0, + + get contextMenu() { + if (!this._contextMenu) { + this._contextMenu = new StyleInspectorMenu(this); + } + + return this._contextMenu; + }, + + // Get the highlighters overlay from the Inspector. + get highlighters() { + if (!this._highlighters) { + // highlighters is a lazy getter in the inspector. + this._highlighters = this.inspector.highlighters; + } + + return this._highlighters; + }, + + get includeBrowserStyles() { + return this.includeBrowserStylesCheckbox.checked; + }, + + get ruleView() { + return ( + this.inspector.hasPanel("ruleview") && + this.inspector.getPanel("ruleview").view + ); + }, + + _handlePrefChange() { + if (this._computed) { + 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(element) { + if (!element) { + if (this.viewedElementPageStyle) { + this.viewedElementPageStyle.off( + "stylesheet-updated", + this.refreshPanel + ); + this.viewedElementPageStyle = null; + } + this._viewedElement = null; + this.noResults.hidden = false; + + if (this._refreshProcess) { + this._refreshProcess.cancel(); + } + // Hiding all properties + for (const propView of this.propertyViews) { + propView.refresh(); + } + return Promise.resolve(undefined); + } + + if (element === this._viewedElement) { + return Promise.resolve(undefined); + } + + if (this.viewedElementPageStyle) { + this.viewedElementPageStyle.off("stylesheet-updated", this.refreshPanel); + } + this.viewedElementPageStyle = element.inspectorFront.pageStyle; + this.viewedElementPageStyle.on("stylesheet-updated", this.refreshPanel); + + 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: + * - view {String} Always "computed" to indicate the computed view. + * - 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 + */ + // eslint-disable-next-line complexity + getNodeInfo(node) { + if (!node) { + return null; + } + + const 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 (const child of node.childNodes[1].childNodes) { + if (child.nodeType === node.TEXT_NODE) { + selectorText += child.textContent; + } + } + return { + type: VIEW_NODE_SELECTOR_TYPE, + value: selectorText.trim(), + }; + } + + const propertyView = node.closest(".computed-property-view"); + const propertyMatchedSelectors = node.closest(".matchedselectors"); + const parent = propertyMatchedSelectors || propertyView; + + if (!parent) { + return null; + } + + let value, type; + + // Get the property and value for a node that's a property name or value + const isHref = + classes.contains("theme-link") && !classes.contains("computed-link"); + + if (classes.contains("computed-font-family")) { + if (propertyMatchedSelectors) { + const view = propertyMatchedSelectors.closest("li"); + value = { + property: view.querySelector(".computed-property-name").firstChild + .textContent, + value: node.parentNode.textContent, + }; + } else if (propertyView) { + value = { + property: parent.querySelector(".computed-property-name").firstChild + .textContent, + value: node.parentNode.textContent, + }; + } else { + return null; + } + } else if ( + propertyMatchedSelectors && + (classes.contains("computed-other-property-value") || isHref) + ) { + const view = propertyMatchedSelectors.closest("li"); + value = { + property: view.querySelector(".computed-property-name").firstChild + .textContent, + value: node.textContent, + }; + } else if ( + propertyView && + (classes.contains("computed-property-name") || + classes.contains("computed-property-value") || + isHref) + ) { + value = { + property: parent.querySelector(".computed-property-name").firstChild + .textContent, + value: parent.querySelector(".computed-property-value").textContent, + }; + } + + // Get the type + if (classes.contains("computed-property-name")) { + type = VIEW_NODE_PROPERTY_TYPE; + } else if ( + classes.contains("computed-property-value") || + classes.contains("computed-other-property-value") + ) { + type = VIEW_NODE_VALUE_TYPE; + } else if (classes.contains("computed-font-family")) { + type = VIEW_NODE_FONT_TYPE; + } else if (isHref) { + type = VIEW_NODE_IMAGE_URL_TYPE; + value.url = node.href; + } else { + return null; + } + + return { + view: "computed", + type, + value, + }; + }, + + _createPropertyViews() { + if (this._createViewsPromise) { + return this._createViewsPromise; + } + + this.refreshSourceFilter(); + this.numVisibleProperties = 0; + const fragment = this.styleDocument.createDocumentFragment(); + + this._createViewsPromise = new Promise((resolve, reject) => { + this._createViewsProcess = new UpdateProcess( + this.styleWindow, + CssComputedView.propertyNames, + { + onItem: propertyName => { + // Per-item callback. + const propView = new PropertyView(this, propertyName); + fragment.append(propView.createListItemElement()); + + if (propView.visible) { + this.numVisibleProperties++; + } + this.propertyViews.push(propView); + }, + onCancel: () => { + reject("_createPropertyViews cancelled"); + }, + onDone: () => { + // Completed callback. + this.element.appendChild(fragment); + this.noResults.hidden = this.numVisibleProperties > 0; + resolve(undefined); + }, + } + ); + }); + + this._createViewsProcess.schedule(); + + return this._createViewsPromise; + }, + + isPanelVisible() { + return ( + this.inspector.toolbox && + this.inspector.sidebar && + this.inspector.toolbox.currentToolId === "inspector" && + this.inspector.sidebar.getCurrentTabID() == "computedview" + ); + }, + + /** + * Refresh the panel content. This could be called by a "ruleview-changed" event, but + * we avoid the extra processing unless the panel is visible. + */ + refreshPanel() { + if (!this._viewedElement || !this.isPanelVisible()) { + return Promise.resolve(); + } + + // Capture the current viewed element to return from the promise handler + // early if it changed + const viewedElement = this._viewedElement; + + return Promise.all([ + this._createPropertyViews(), + this.viewedElementPageStyle.getComputed(this._viewedElement, { + filter: this._sourceFilter, + onlyMatched: !this.includeBrowserStyles, + markMatched: true, + }), + ]) + .then(([, computed]) => { + if (viewedElement !== this._viewedElement) { + return Promise.resolve(); + } + + this._computed = computed; + this._matchedProperties = new Set(); + const customProperties = new Set(); + + for (const name in computed) { + if (computed[name].matched) { + this._matchedProperties.add(name); + } + if (name.startsWith("--")) { + customProperties.add(name); + } + } + + // Removing custom property PropertyViews which won't be used + let customPropertiesStartIndex; + for (let i = this.propertyViews.length - 1; i >= 0; i--) { + const propView = this.propertyViews[i]; + + // custom properties are displayed at the bottom of the list, and we're looping + // backward through propertyViews, so if the current item does not represent + // a custom property, we can stop looping. + if (!propView.isCustomProperty) { + customPropertiesStartIndex = i + 1; + break; + } + + // If the custom property will be used, move to the next item. + if (customProperties.has(propView.name)) { + customProperties.delete(propView.name); + continue; + } + + // Otherwise remove property view element + if (propView.element) { + propView.element.remove(); + } + + propView.destroy(); + this.propertyViews.splice(i, 1); + } + + // At this point, `customProperties` only contains custom property names for + // which we don't have a PropertyView yet. + let insertIndex = customPropertiesStartIndex; + for (const customPropertyName of Array.from(customProperties).sort()) { + const propertyView = new PropertyView( + this, + customPropertyName, + // isCustomProperty + true + ); + + const len = this.propertyViews.length; + if (insertIndex !== len) { + for (let i = insertIndex; i <= len; i++) { + const existingPropView = this.propertyViews[i]; + if ( + !existingPropView || + !existingPropView.isCustomProperty || + customPropertyName < existingPropView.name + ) { + insertIndex = i; + break; + } + } + } + this.propertyViews.splice(insertIndex, 0, propertyView); + + // Insert the custom property PropertyView at the right spot so we + // keep the list ordered. + const previousSibling = this.element.childNodes[insertIndex - 1]; + previousSibling.insertAdjacentElement( + "afterend", + propertyView.createListItemElement() + ); + } + + if (this._refreshProcess) { + this._refreshProcess.cancel(); + } + + this.noResults.hidden = true; + + // Reset visible property count + this.numVisibleProperties = 0; + + return new Promise((resolve, reject) => { + this._refreshProcess = new UpdateProcess( + this.styleWindow, + this.propertyViews, + { + onItem: propView => { + propView.refresh(); + }, + onCancel: () => { + reject("_refreshProcess of computed view cancelled"); + }, + onDone: () => { + this._refreshProcess = null; + this.noResults.hidden = this.numVisibleProperties > 0; + + const searchBox = this.searchField.parentNode; + searchBox.classList.toggle( + "devtools-searchbox-no-match", + !!this.searchField.value.length && !this.numVisibleProperties + ); + + this.inspector.emit("computed-view-refreshed"); + resolve(undefined); + }, + } + ); + this._refreshProcess.schedule(); + }); + }) + .catch(console.error); + }, + + /** + * Handle the shortcut events in the computed view. + */ + _onShortcut(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(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() { + if (this._filterChangedTimeout) { + clearTimeout(this._filterChangedTimeout); + } + + const filterTimeout = this.searchField.value.length + ? FILTER_CHANGED_TIMEOUT + : 0; + this.searchClearButton.hidden = this.searchField.value.length === 0; + + this._filterChangedTimeout = setTimeout(() => { + 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() { + if (this.searchField.value) { + this.setFilterStyles(""); + return true; + } + + return false; + }, + + /** + * The change event handler for the includeBrowserStyles checkbox. + */ + _onIncludeBrowserStyles() { + 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() { + this._matchedProperties = null; + this._sourceFilter = this.includeBrowserStyles + ? CssLogic.FILTER.UA + : CssLogic.FILTER.USER; + }, + + /** + * The CSS as displayed by the UI. + */ + createStyleViews() { + 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 + const styles = this.styleWindow.getComputedStyle( + this.styleDocument.documentElement + ); + const mozProps = []; + for (let i = 0, numStyles = styles.length; i < numStyles; i++) { + const 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().catch(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() { + this.styleWindow.focus(); + }, + + /** + * Context menu handler. + */ + _onContextMenu(event) { + // Call stopPropagation() and preventDefault() here so that avoid to show default + // context menu in about:devtools-toolbox. See Bug 1515265. + event.stopPropagation(); + event.preventDefault(); + this.contextMenu.show(event); + }, + + _onClick(event) { + const target = event.target; + + if (target.nodeName === "a") { + event.stopPropagation(); + event.preventDefault(); + openContentLink(target.href); + } + }, + + /** + * Callback for copy event. Copy selected text. + * + * @param {Event} event + * copy event object. + */ + _onCopy(event) { + const win = this.styleWindow; + const text = win.getSelection().toString().trim(); + if (text !== "") { + this.copySelection(); + event.preventDefault(); + } + }, + + /** + * Copy the current selection to the clipboard + */ + copySelection() { + try { + const win = this.styleWindow; + const text = win.getSelection().toString().trim(); + + clipboardHelper.copyString(text); + } catch (e) { + console.error(e); + } + }, + + /** + * Destructor for CssComputedView. + */ + destroy() { + this._viewedElement = null; + if (this.viewedElementPageStyle) { + this.viewedElementPageStyle.off("stylesheet-updated", this.refreshPanel); + this.viewedElementPageStyle = null; + } + this._outputParser = null; + + this._prefObserver.off("devtools.defaultColorUnit", this._handlePrefChange); + this._prefObserver.destroy(); + + // Cancel tree construction + if (this._createViewsProcess) { + this._createViewsProcess.cancel(); + } + if (this._refreshProcess) { + this._refreshProcess.cancel(); + } + + if (this._contextMenu) { + this._contextMenu.destroy(); + this._contextMenu = null; + } + + if (this._highlighters) { + this._highlighters.removeFromView(this); + this._highlighters = null; + } + + this.tooltips.destroy(); + + // Remove bound listeners + this.element.removeEventListener("click", this._onClick); + this.element.removeEventListener("contextmenu", this._onContextMenu); + this.searchField.removeEventListener("input", this._onFilterStyles); + this.searchClearButton.removeEventListener("click", this._onClearSearch); + this.styleDocument.removeEventListener("copy", this._onCopy); + this.styleDocument.removeEventListener("mousedown", this.focusWindow); + this.includeBrowserStylesCheckbox.removeEventListener( + "input", + this._onIncludeBrowserStyles + ); + + if (this.ruleView) { + this.ruleView.off("ruleview-changed", this.refreshPanel); + } + + // Nodes used in templating + this.element = null; + this.searchField = null; + this.searchClearButton = null; + this.includeBrowserStylesCheckbox = null; + + // Property views + for (const 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 isSupported() { + // There can be a mismatch between the list of properties + // supported on the server and on the client. + // Ideally we should build PropertyInfo only for property names supported on + // the server. See Bug 1722348. + return this.tree._computed && this.name in this.tree._computed; + }, + + get value() { + if (this.isSupported) { + const value = this.tree._computed[this.name].value; + return value; + } + return null; + }, +}; + +/** + * A container to give easy access to property data from the template engine. + */ +class PropertyView { + /* + * @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. + * @param {Boolean} isCustomProperty + * Set to true if this will represent a custom property. + */ + constructor(tree, name, isCustomProperty = false) { + this.tree = tree; + this.name = name; + + this.isCustomProperty = isCustomProperty; + + if (!this.isCustomProperty) { + this.link = "https://developer.mozilla.org/docs/Web/CSS/" + name; + } + + this.#propertyInfo = new PropertyInfo(tree, name); + const win = this.tree.styleWindow; + this.#abortController = new win.AbortController(); + } + + // The parent element which contains the open attribute + element = null; + + // Property header node + propertyHeader = null; + + // Destination for property values + valueNode = null; + + // Are matched rules expanded? + matchedExpanded = false; + + // Matched selector container + matchedSelectorsContainer = null; + + // Matched selector expando + matchedExpander = null; + + // AbortController for event listeners + #abortController = null; + + // Cache for matched selector views + #matchedSelectorViews = null; + + // The previously selected element used for the selector view caches + #prevViewedElement = null; + + // PropertyInfo + #propertyInfo = 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; + } + + const searchTerm = this.tree.searchField.value.toLowerCase(); + const isValidSearchTerm = !!searchTerm.trim().length; + if ( + isValidSearchTerm && + !this.name.toLowerCase().includes(searchTerm) && + !this.value.toLowerCase().includes(searchTerm) + ) { + return false; + } + + return this.propertyInfo.isSupported; + } + + /** + * Returns the className that should be assigned to the propertyView. + * + * @return {String} + */ + get propertyHeaderClassName() { + return this.visible ? "computed-property-view" : "computed-property-hidden"; + } + + /** + * Create DOM elements for a property + * + * @return {Element} The <li> element + */ + createListItemElement() { + const doc = this.tree.styleDocument; + const baseEventListenerConfig = { signal: this.#abortController.signal }; + + // Build the container element + this.onMatchedToggle = this.onMatchedToggle.bind(this); + this.element = doc.createElement("li"); + this.element.className = this.propertyHeaderClassName; + this.element.addEventListener( + "dblclick", + this.onMatchedToggle, + baseEventListenerConfig + ); + + // Make it keyboard navigable + this.element.setAttribute("tabindex", "0"); + this.shortcuts = new KeyShortcuts({ + window: this.tree.styleWindow, + target: this.element, + }); + this.shortcuts.on("F1", event => { + this.mdnLinkClick(event); + // Prevent opening the options panel + event.preventDefault(); + event.stopPropagation(); + }); + this.shortcuts.on("Return", this.onMatchedToggle); + this.shortcuts.on("Space", this.onMatchedToggle); + + const nameContainer = doc.createElement("span"); + nameContainer.className = "computed-property-name-container"; + + // Build the twisty expand/collapse + this.matchedExpander = doc.createElement("div"); + this.matchedExpander.className = "computed-expander theme-twisty"; + this.matchedExpander.setAttribute("role", "button"); + this.matchedExpander.setAttribute("aria-label", L10N_TWISTY_EXPAND_LABEL); + this.matchedExpander.addEventListener( + "click", + this.onMatchedToggle, + baseEventListenerConfig + ); + + // Build the style name element + const nameNode = doc.createElement("span"); + nameNode.classList.add("computed-property-name", "theme-fg-color3"); + + // Give it a heading role for screen readers. + nameNode.setAttribute("role", "heading"); + + // Reset its tabindex attribute otherwise, if an ellipsis is applied + // it will be reachable via TABing + nameNode.setAttribute("tabindex", ""); + // Avoid english text (css properties) from being altered + // by RTL mode + nameNode.setAttribute("dir", "ltr"); + nameNode.textContent = nameNode.title = this.name; + // Make it hand over the focus to the container + const focusElement = () => this.element.focus(); + nameNode.addEventListener("click", focusElement, baseEventListenerConfig); + + // Build the style name ":" separator + const nameSeparator = doc.createElement("span"); + nameSeparator.classList.add("visually-hidden"); + nameSeparator.textContent = ": "; + nameNode.appendChild(nameSeparator); + + nameContainer.appendChild(nameNode); + + const valueContainer = doc.createElement("span"); + valueContainer.className = "computed-property-value-container"; + + // Build the style value element + this.valueNode = doc.createElement("span"); + this.valueNode.classList.add("computed-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", + focusElement, + baseEventListenerConfig + ); + + // Build the style value ";" separator + const valueSeparator = doc.createElement("span"); + valueSeparator.classList.add("visually-hidden"); + valueSeparator.textContent = ";"; + + // Build the matched selectors container + this.matchedSelectorsContainer = doc.createElement("div"); + this.matchedSelectorsContainer.classList.add("matchedselectors"); + + valueContainer.append(this.valueNode, valueSeparator); + this.element.append( + this.matchedExpander, + nameContainer, + valueContainer, + this.matchedSelectorsContainer + ); + + return this.element; + } + + /** + * Refresh the panel's CSS property value. + */ + refresh() { + const className = this.propertyHeaderClassName; + if (this.element.className !== className) { + this.element.className = className; + } + + 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"); + this.matchedExpander.setAttribute("aria-label", L10N_TWISTY_EXPAND_LABEL); + return; + } + + this.tree.numVisibleProperties++; + + const outputParser = this.tree._outputParser; + const frag = outputParser.parseCssProperty( + this.propertyInfo.name, + this.propertyInfo.value, + { + colorSwatchClass: "computed-colorswatch", + colorClass: "computed-color", + urlClass: "theme-link", + fontFamilyClass: "computed-font-family", + // 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() { + const hasMatchedSelectors = this.hasMatchedSelectors; + this.matchedSelectorsContainer.parentNode.hidden = !hasMatchedSelectors; + + if (hasMatchedSelectors) { + this.matchedExpander.classList.add("computed-expandable"); + } else { + this.matchedExpander.classList.remove("computed-expandable"); + } + + if (this.matchedExpanded && hasMatchedSelectors) { + return this.tree.viewedElementPageStyle + .getMatchedSelectors(this.tree._viewedElement, this.name) + .then(matched => { + if (!this.matchedExpanded) { + return; + } + + this._matchedSelectorResponse = matched; + + this.#buildMatchedSelectors(); + this.matchedExpander.setAttribute("open", ""); + this.matchedExpander.setAttribute( + "aria-label", + L10N_TWISTY_COLLAPSE_LABEL + ); + this.tree.inspector.emit("computed-view-property-expanded"); + }) + .catch(console.error); + } + + this.matchedSelectorsContainer.innerHTML = ""; + this.matchedExpander.removeAttribute("open"); + this.matchedExpander.setAttribute("aria-label", L10N_TWISTY_EXPAND_LABEL); + this.tree.inspector.emit("computed-view-property-collapsed"); + return Promise.resolve(undefined); + } + + get matchedSelectors() { + return this._matchedSelectorResponse; + } + + #buildMatchedSelectors() { + const frag = this.element.ownerDocument.createDocumentFragment(); + + for (const selector of this.matchedSelectorViews) { + const p = createChild(frag, "p"); + const span = createChild(p, "span", { + class: "rule-link", + }); + + const link = createChild(span, "a", { + target: "_blank", + class: "computed-link theme-link", + title: selector.longSource, + sourcelocation: selector.source, + tabindex: "0", + textContent: selector.source, + }); + link.addEventListener("click", selector.openStyleEditor); + const shortcuts = new KeyShortcuts({ + window: this.tree.styleWindow, + target: link, + }); + shortcuts.on("Return", () => selector.openStyleEditor()); + + const status = createChild(p, "span", { + dir: "ltr", + class: "rule-text theme-fg-color3 " + selector.statusClass, + title: selector.statusText, + }); + + // Add an explicit status text span for screen readers. + // They won't pick up the title from the status span. + createChild(status, "span", { + dir: "ltr", + class: "visually-hidden", + textContent: selector.statusText + " ", + }); + + createChild(status, "div", { + class: "fix-get-selection", + textContent: selector.sourceText, + }); + + const valueDiv = createChild(status, "div", { + class: + "fix-get-selection computed-other-property-value theme-fg-color1", + }); + valueDiv.appendChild(selector.outputFragment); + } + + this.matchedSelectorsContainer.innerHTML = ""; + this.matchedSelectorsContainer.appendChild(frag); + } + + /** + * Provide access to the matched SelectorViews that we are currently + * displaying. + */ + get matchedSelectorViews() { + if (!this.#matchedSelectorViews) { + this.#matchedSelectorViews = []; + this._matchedSelectorResponse.forEach(selectorInfo => { + const selectorView = new SelectorView(this.tree, selectorInfo); + this.#matchedSelectorViews.push(selectorView); + }, this); + } + return this.#matchedSelectorViews; + } + + /** + * The action when a user expands matched selectors. + * + * @param {Event} event + * Used to determine the class name of the targets click + * event. + */ + onMatchedToggle(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(event) { + if (!this.link) { + return; + } + openContentLink(this.link); + } + + /** + * Destroy this property view, removing event listeners + */ + destroy() { + if (this.#matchedSelectorViews) { + for (const view of this.#matchedSelectorViews) { + view.destroy(); + } + } + + if (this.#abortController) { + this.#abortController.abort(); + this.#abortController = null; + } + + if (this.shortcuts) { + this.shortcuts.destroy(); + } + + this.shortcuts = null; + this.element = null; + this.matchedExpander = null; + 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._updateLocation = this._updateLocation.bind(this); + + const rule = this.selectorInfo.rule; + if (!rule || !rule.parentStyleSheet || rule.type == ELEMENT_STYLE) { + this.source = CssLogic.l10n("rule.sourceElement"); + this.longSource = this.source; + } else { + // This always refers to the generated location. + const sheet = rule.parentStyleSheet; + const sourceSuffix = rule.line > 0 ? ":" + rule.line : ""; + this.source = CssLogic.shortSource(sheet) + sourceSuffix; + this.longSource = CssLogic.longSource(sheet) + sourceSuffix; + + this.generatedLocation = { + sheet, + href: sheet.href || sheet.nodeHref, + line: rule.line, + column: rule.column, + }; + this.sourceMapURLService = this.tree.inspector.toolbox.sourceMapURLService; + this._unsubscribeCallback = this.sourceMapURLService.subscribeByID( + this.generatedLocation.sheet.resourceId, + this.generatedLocation.line, + this.generatedLocation.column, + this._updateLocation + ); + } +} + +/** + * 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() { + if (SelectorView.STATUS_NAMES.length) { + return; + } + + for (const status in CssLogic.STATUS) { + const i = CssLogic.STATUS[status]; + if (i > CssLogic.STATUS.UNMATCHED) { + const 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; + } + const 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. + const outputParser = this.tree._outputParser; + const frag = outputParser.parseCssProperty( + this.selectorInfo.name, + this.selectorInfo.value, + { + colorSwatchClass: "computed-colorswatch", + colorClass: "computed-color", + urlClass: "theme-link", + fontFamilyClass: "computed-font-family", + baseURI: this.selectorInfo.rule.href, + } + ); + return frag; + }, + + /** + * Update the text of the source link to reflect whether we're showing + * original sources or not. This is a callback for + * SourceMapURLService.subscribe, which see. + * + * @param {Object | null} originalLocation + * The original position object (url/line/column) or null. + */ + _updateLocation(originalLocation) { + if (!this.tree.element) { + return; + } + + // Update |currentLocation| to be whichever location is being + // displayed at the moment. + let currentLocation = this.generatedLocation; + if (originalLocation) { + const { url, line, column } = originalLocation; + currentLocation = { href: url, line, column }; + } + + const selector = '[sourcelocation="' + this.source + '"]'; + const link = this.tree.element.querySelector(selector); + if (link) { + const text = + CssLogic.shortSource(currentLocation) + ":" + currentLocation.line; + link.textContent = text; + } + + this.tree.inspector.emit("computed-view-sourcelinks-updated"); + }, + + /** + * 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() { + const inspector = this.tree.inspector; + const 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. + const parentStyleSheet = rule.parentStyleSheet; + if (!parentStyleSheet || parentStyleSheet.isSystem) { + inspector.toolbox.viewSource(rule.href, rule.line); + return; + } + + const { sheet, line, column } = this.generatedLocation; + if (ToolDefinitions.styleEditor.isToolSupported(inspector.toolbox)) { + inspector.toolbox.viewSourceInStyleEditorByResource(sheet, line, column); + } + }, + + /** + * Destroy this selector view, removing event listeners + */ + destroy() { + if (this._unsubscribeCallback) { + this._unsubscribeCallback(); + } + }, +}; + +function ComputedViewTool(inspector, window) { + this.inspector = inspector; + this.document = window.document; + + this.computedView = new CssComputedView(this.inspector, this.document); + + this.onDetachedFront = this.onDetachedFront.bind(this); + this.onSelected = this.onSelected.bind(this); + this.refresh = this.refresh.bind(this); + this.onPanelSelected = this.onPanelSelected.bind(this); + + this.inspector.selection.on("detached-front", this.onDetachedFront); + 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.styleChangeTracker.on("style-changed", this.refresh); + + this.computedView.selectElement(null); + + this.onSelected(); +} + +ComputedViewTool.prototype = { + isPanelVisible() { + if (!this.computedView) { + return false; + } + return this.computedView.isPanelVisible(); + }, + + onDetachedFront() { + this.onSelected(false); + }, + + async onSelected(selectElement = true) { + // 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; + } + + const isInactive = + !this.isPanelVisible() && this.inspector.selection.nodeFront; + if (isInactive) { + return; + } + + if ( + !this.inspector.selection.isConnected() || + !this.inspector.selection.isElementNode() + ) { + this.computedView.selectElement(null); + return; + } + + if (selectElement) { + const done = this.inspector.updating("computed-view"); + await this.computedView.selectElement(this.inspector.selection.nodeFront); + done(); + } + }, + + refresh() { + if (this.isPanelVisible()) { + this.computedView.refreshPanel(); + } + }, + + onPanelSelected() { + if ( + this.inspector.selection.nodeFront === this.computedView._viewedElement + ) { + this.refresh(); + } else { + this.onSelected(); + } + }, + + destroy() { + this.inspector.styleChangeTracker.off("style-changed", this.refresh); + this.inspector.sidebar.off("computedview-selected", this.refresh); + this.inspector.selection.off("pseudoclass", this.refresh); + this.inspector.selection.off("new-node-front", this.onSelected); + this.inspector.selection.off("detached-front", this.onDetachedFront); + this.inspector.sidebar.off("computedview-selected", this.onPanelSelected); + + this.computedView.destroy(); + + this.computedView = 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 0000000000..4f84b2ebf0 --- /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.toml"] diff --git a/devtools/client/inspector/computed/test/browser.toml b/devtools/client/inspector/computed/test/browser.toml new file mode 100644 index 0000000000..d4ec4fc993 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser.toml @@ -0,0 +1,86 @@ +[DEFAULT] +tags = "devtools" +subsuite = "devtools" +support-files = [ + "doc_matched_selectors_imported_1.css", + "doc_matched_selectors_imported_2.css", + "doc_matched_selectors_imported_3.css", + "doc_matched_selectors_imported_4.css", + "doc_matched_selectors_imported_5.css", + "doc_matched_selectors_imported_6.css", + "doc_matched_selectors.html", + "doc_media_queries.html", + "doc_pseudoelement.html", + "doc_sourcemaps.css", + "doc_sourcemaps.css.map", + "doc_sourcemaps.html", + "doc_sourcemaps.scss", + "head.js", + "!/devtools/client/inspector/test/head.js", + "!/devtools/client/inspector/test/shared-head.js", + "!/devtools/client/shared/test/shared-head.js", + "!/devtools/client/shared/test/telemetry-test-helpers.js", + "!/devtools/client/shared/test/highlighter-test-actor.js", +] + +["browser_computed_browser-styles.js"] + +["browser_computed_custom_properties.js"] + +["browser_computed_cycle_color.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_computed_default_tab.js"] + +["browser_computed_getNodeInfo.js"] +skip-if = [ + "!debug && os == 'mac'", #Bug 1559033 + "a11y_checks", # Bugs 1849028 and 1858041 to investigate intermittent a11y_checks results +] + +["browser_computed_keybindings_01.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_computed_keybindings_02.js"] + +["browser_computed_matched-selectors-order.js"] + +["browser_computed_matched-selectors-toggle.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["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"] +skip-if = ["a11y_checks"] # Bug 1858037 to investigate intermittent a11y_checks results (fails on Autoland, passes on Try) + +["browser_computed_pseudo-element_01.js"] + +["browser_computed_refresh-on-ruleview-change.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"] + +["browser_computed_search-filter_escape-keypress.js"] + +["browser_computed_search-filter_noproperties.js"] + +["browser_computed_select-and-copy-styles-01.js"] + +["browser_computed_select-and-copy-styles-02.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_computed_shadow_host.js"] + +["browser_computed_style-editor-link.js"] +skip-if = ["true"] # bug 1307846 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 0000000000..52477d21ed --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_browser-styles.js @@ -0,0 +1,59 @@ +/* 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(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view } = await openComputedView(); + await 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"); + const doc = view.styleDocument; + const checkbox = doc.querySelector(".includebrowserstyles"); + const onRefreshed = inspector.once("computed-view-refreshed"); + checkbox.click(); + await 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); + const propertyViews = view.propertyViews; + for (const propView of propertyViews) { + if (propView.name == name) { + return propView.visible; + } + } + return false; +} diff --git a/devtools/client/inspector/computed/test/browser_computed_custom_properties.js b/devtools/client/inspector/computed/test/browser_computed_custom_properties.js new file mode 100644 index 0000000000..230bffa726 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_custom_properties.js @@ -0,0 +1,106 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that custom properties are displayed in the computed view. + +const TEST_URI = ` + <style type="text/css"> + body { + --global-custom-property: red; + } + + h1 { + color: var(--global-custom-property); + } + + #match-1 { + --global-custom-property: blue; + --custom-property-1: lime; + } + #match-2 { + --global-custom-property: gold; + --custom-property-2: cyan; + } + </style> + <h1 id="match-1">Hello</h1> + <h1 id="match-2">World</h1> +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view } = await openComputedView(); + + await assertComputedPropertiesForNode(inspector, view, "body", [ + { + name: "--global-custom-property", + value: "red", + }, + ]); + + await assertComputedPropertiesForNode(inspector, view, "#match-1", [ + { + name: "color", + value: "rgb(0, 0, 255)", + }, + { + name: "--custom-property-1", + value: "lime", + }, + { + name: "--global-custom-property", + value: "blue", + }, + ]); + + await assertComputedPropertiesForNode(inspector, view, "#match-2", [ + { + name: "color", + value: "rgb(255, 215, 0)", + }, + { + name: "--custom-property-2", + value: "cyan", + }, + { + name: "--global-custom-property", + value: "gold", + }, + ]); + + await assertComputedPropertiesForNode(inspector, view, "html", []); +}); + +async function assertComputedPropertiesForNode( + inspector, + view, + selector, + expected +) { + await selectNode(selector, inspector); + + const computedItems = getComputedViewProperties(view); + is( + computedItems.length, + expected.length, + `Computed view has the expected number of items for "${selector}"` + ); + for (let i = 0; i < computedItems.length; i++) { + const expectedData = expected[i]; + const computedEl = computedItems[i]; + const nameSpan = computedEl.querySelector(".computed-property-name"); + const valueSpan = computedEl.querySelector(".computed-property-value"); + + is( + nameSpan.firstChild.textContent, + expectedData.name, + `computed item #${i} for "${selector}" is the expected one` + ); + is( + valueSpan.textContent, + expectedData.value, + `computed item #${i} for "${selector}" has expected value` + ); + } +} 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 0000000000..465c453adf --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_cycle_color.js @@ -0,0 +1,90 @@ +/* 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(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view } = await openComputedView(); + await selectNode("#matches", inspector); + + info("Checking the property itself"); + let container = getComputedViewPropertyView(view, "color").valueNode; + await checkColorCycling(container, view); + + info("Checking matched selectors"); + container = await getComputedViewMatchedRules(view, "color"); + await checkColorCycling(container, view); +}); + +async function checkColorCycling(container, view) { + const valueNode = container.querySelector(".computed-color"); + const win = view.styleWindow; + + // "Authored" (default; currently the computed value) + is( + valueNode.textContent, + "rgb(255, 0, 0)", + "Color displayed as an RGB value." + ); + + const tests = [ + { + value: "hwb(0 0% 0%)", + comment: "Color displayed as an HWB value.", + }, + { + value: "red", + comment: "Color displayed as a color name.", + }, + { + value: "#f00", + comment: "Color displayed as a HEX value.", + }, + { + value: "hsl(0, 100%, 50%)", + comment: "Color displayed as an HSL value.", + }, + { + value: "rgb(255, 0, 0)", + comment: "Color displayed as an RGB value again.", + }, + ]; + + for (const test of tests) { + await checkSwatchShiftClick(container, win, test.value, test.comment); + } +} + +async function checkSwatchShiftClick(container, win, expectedValue, comment) { + const swatch = container.querySelector(".computed-colorswatch"); + const valueNode = container.querySelector(".computed-color"); + swatch.scrollIntoView(); + + const onUnitChange = once(swatch, "unit-change"); + EventUtils.synthesizeMouseAtCenter( + swatch, + { + type: "mousedown", + shiftKey: true, + }, + win + ); + // we need to have the mouse up event in order to make sure that the platform + // lets go of the last container, and is not waiting for something to happen. + // Bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1442153 + EventUtils.synthesizeMouseAtCenter(swatch, { type: "mouseup" }, win); + await onUnitChange; + is(valueNode.textContent, expectedValue, comment); +} diff --git a/devtools/client/inspector/computed/test/browser_computed_default_tab.js b/devtools/client/inspector/computed/test/browser_computed_default_tab.js new file mode 100644 index 0000000000..1a485a7cc7 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_default_tab.js @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the computed view is initialized when the computed view is the default tab +// for the inspector. + +const TEST_URI = ` + <style type="text/css"> + #matches { + color: #F00; + } + </style> + <span id="matches">Some styled text</span> +`; + +add_task(async function () { + await pushPref("devtools.inspector.activeSidebar", "computedview"); + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view } = await openComputedView(); + await selectNode("#matches", inspector); + is( + isPropertyVisible("color", view), + true, + "span #matches color property is visible" + ); +}); + +function isPropertyVisible(name, view) { + info("Checking property visibility for " + name); + const propertyViews = view.propertyViews; + for (const propView of propertyViews) { + if (propView.name == name) { + return propView.visible; + } + } + return false; +} 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 0000000000..0aa1c85ff2 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_getNodeInfo.js @@ -0,0 +1,176 @@ +/* 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("resource://devtools/client/inspector/shared/node-types.js"); + +const TEST_URI = ` + <style type="text/css"> + body { + background: red; + color: white; + } + div { + background: green; + } + div div { + background-color: yellow; + background-image: url(chrome://branding/content/icon64.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() { + return null; + }, + assertNodeInfo(nodeInfo) { + is(nodeInfo, null); + }, + }, + { + desc: "Testing a useless node", + getHoveredNode(view) { + return view.element; + }, + assertNodeInfo(nodeInfo) { + is(nodeInfo, null); + }, + }, + { + desc: "Testing a property name", + getHoveredNode(view) { + return getComputedViewProperty(view, "color").nameSpan; + }, + assertNodeInfo(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(view) { + return getComputedViewProperty(view, "color").valueSpan; + }, + assertNodeInfo(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(view) { + const { valueSpan } = getComputedViewProperty(view, "background-image"); + return valueSpan.querySelector(".theme-link"); + }, + assertNodeInfo(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://branding/content/icon64.png")'); + is(nodeInfo.value.url, "chrome://branding/content/icon64.png"); + }, + }, + { + desc: "Testing a matched rule selector (bestmatch)", + async getHoveredNode(view) { + const el = await getComputedViewMatchedRules(view, "background-color"); + return el.querySelector(".bestmatch"); + }, + assertNodeInfo(nodeInfo) { + is(nodeInfo.type, VIEW_NODE_SELECTOR_TYPE); + is(nodeInfo.value, "div div"); + }, + }, + { + desc: "Testing a matched rule selector (matched)", + async getHoveredNode(view) { + const el = await getComputedViewMatchedRules(view, "background-color"); + return el.querySelector(".matched"); + }, + assertNodeInfo(nodeInfo) { + is(nodeInfo.type, VIEW_NODE_SELECTOR_TYPE); + is(nodeInfo.value, "div"); + }, + }, + { + desc: "Testing a matched rule selector (parentmatch)", + async getHoveredNode(view) { + const el = await getComputedViewMatchedRules(view, "color"); + return el.querySelector(".parentmatch"); + }, + assertNodeInfo(nodeInfo) { + is(nodeInfo.type, VIEW_NODE_SELECTOR_TYPE); + is(nodeInfo.value, "body"); + }, + }, + { + desc: "Testing a matched rule value", + async getHoveredNode(view) { + const el = await getComputedViewMatchedRules(view, "color"); + return el.querySelector(".computed-other-property-value"); + }, + assertNodeInfo(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", + async getHoveredNode(view) { + const el = await getComputedViewMatchedRules(view, "color"); + return el.querySelector(".rule-link .theme-link"); + }, + assertNodeInfo(nodeInfo) { + is(nodeInfo, null); + }, + }, +]; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view } = await openComputedView(); + await selectNode("#testElement", inspector); + + for (const { desc, getHoveredNode, assertNodeInfo } of TEST_DATA) { + info(desc); + const nodeInfo = view.getNodeInfo(await 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 0000000000..5a6681f139 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_keybindings_01.js @@ -0,0 +1,92 @@ +/* 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(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view } = await openComputedView(); + await selectNode(".matches", inspector); + + const propView = getFirstVisiblePropertyView(view); + const rulesTable = propView.matchedSelectorsContainer; + const matchedExpander = propView.element; + + info("Focusing the property"); + matchedExpander.scrollIntoView(); + const onMatchedExpanderFocus = once(matchedExpander, "focus", true); + EventUtils.synthesizeMouseAtCenter(matchedExpander, {}, view.styleWindow); + await onMatchedExpanderFocus; + + await checkToggleKeyBinding( + view.styleWindow, + "VK_SPACE", + rulesTable, + inspector + ); + await checkToggleKeyBinding( + view.styleWindow, + "VK_RETURN", + rulesTable, + inspector + ); + await checkHelpLinkKeybinding(view); +}); + +function getFirstVisiblePropertyView(view) { + let propView = null; + view.propertyViews.some(p => { + if (p.visible) { + propView = p; + return true; + } + return false; + }); + + return propView; +} + +async function checkToggleKeyBinding(win, key, rulesTable, inspector) { + info( + "Pressing " + + key + + " key a couple of times to check that the " + + "property gets expanded/collapsed" + ); + + const onExpand = inspector.once("computed-view-property-expanded"); + const onCollapse = inspector.once("computed-view-property-collapsed"); + + info("Expanding the property"); + EventUtils.synthesizeKey(key, {}, win); + await onExpand; + isnot(rulesTable.innerHTML, "", "The property has been expanded"); + + info("Collapsing the property"); + EventUtils.synthesizeKey(key, {}, win); + await onCollapse; + is(rulesTable.innerHTML, "", "The property has been collapsed"); +} + +function checkHelpLinkKeybinding(view) { + info('Check that MDN link is opened on "F1"'); + const propView = getFirstVisiblePropertyView(view); + return new Promise(resolve => { + propView.mdnLinkClick = function (event) { + ok(true, "Pressing F1 opened the MDN link"); + resolve(); + }; + EventUtils.synthesizeKey("VK_F1", {}, view.styleWindow); + }); +} 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 0000000000..46434f0660 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_keybindings_02.js @@ -0,0 +1,69 @@ +/* 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(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view } = await openComputedView(); + await selectNode("span", inspector); + + info("Selecting the first computed style in the list"); + const firstStyle = view.styleDocument.querySelector( + "#computed-container .computed-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("KEY_Tab"); + EventUtils.synthesizeKey("KEY_Enter"); + await onExpanded; + + info("Verify the 2nd style has been expanded"); + const secondStyleSelectors = view.styleDocument.querySelectorAll( + ".computed-property-view .matchedselectors" + )[1]; + ok(!!secondStyleSelectors.childNodes.length, "Matched selectors expanded"); + + info("Tab back up and test the same thing, with space"); + onExpanded = inspector.once("computed-view-property-expanded"); + EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }); + EventUtils.synthesizeKey(" "); + await onExpanded; + + info("Verify the 1st style has been expanded too"); + const firstStyleSelectors = view.styleDocument.querySelectorAll( + ".computed-property-view .matchedselectors" + )[0]; + ok(!!firstStyleSelectors.childNodes.length, "Matched selectors expanded"); +}); diff --git a/devtools/client/inspector/computed/test/browser_computed_matched-selectors-order.js b/devtools/client/inspector/computed/test/browser_computed_matched-selectors-order.js new file mode 100644 index 0000000000..b90e86a295 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_matched-selectors-order.js @@ -0,0 +1,889 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests for the order of matched selector in the computed view. +const TEST_URI = URL_ROOT + "doc_matched_selectors.html"; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, view } = await openComputedView(); + + const checkMatchedSelectors = options => + checkBackgroundColorMatchedSelectors(inspector, view, options); + + info("matching rules with different specificity"); + await checkMatchedSelectors({ + elementAttributes: { + id: "specificity", + class: "mySection", + }, + style: ` + #specificity.mySection { + --spec_highest: var(--winning-color); + background-color: var(--spec_highest); + } + #specificity { + background-color: var(--spec_lowest); + }`, + expectedMatchedSelectors: [ + // Higher specificity wins + { selector: "#specificity.mySection", value: "var(--spec_highest)" }, + { selector: "#specificity", value: "var(--spec_lowest)" }, + ], + }); + + info("matching rules with same specificity"); + await checkMatchedSelectors({ + elementAttributes: { + id: "order-of-appearance", + }, + style: ` + #order-of-appearance { + background-color: var(--appearance-order_first); + } + #order-of-appearance { + --appearance-order_second: var(--winning-color); + background-color: var(--appearance-order_second); + }`, + expectedMatchedSelectors: [ + // Last rule in stylesheet wins + { + selector: "#order-of-appearance", + value: "var(--appearance-order_second)", + }, + { + selector: "#order-of-appearance", + value: "var(--appearance-order_first)", + }, + ], + }); + + info("matching rules on element with style attribute"); + await checkMatchedSelectors({ + elementAttributes: { + id: "style-attr", + style: "background-color: var(--style-attr_in-attr)", + }, + style: ` + main { + --style-attr_in-attr: var(--winning-color); + } + + #style-attr { + background-color: var(--style-attr_in-rule); + } + `, + expectedMatchedSelectors: [ + // style attribute wins + { selector: "this.style", value: "var(--style-attr_in-attr)" }, + { selector: "#style-attr", value: "var(--style-attr_in-rule)" }, + ], + }); + + info("matching rules on different layers"); + await checkMatchedSelectors({ + elementAttributes: { + id: "layers", + class: "layers", + }, + style: ` + @layer second { + .layers { + --layers_in-second: var(--winning-color); + background-color: var(--layers_in-second); + } + } + @layer first { + #layers { + background-color: var(--layers_in-first); + } + } + `, + expectedMatchedSelectors: [ + // rule in last declared layer wins + { selector: ".layers", value: "var(--layers_in-second)" }, + { selector: "#layers", value: "var(--layers_in-first)" }, + ], + }); + + info("matching rules on same layer, with same specificity"); + await checkMatchedSelectors({ + elementAttributes: { + id: "same-layers-order-of-appearance", + }, + style: ` + @layer second { + #same-layers-order-of-appearance { + background-color: var(--same-layers-appearance-order_first); + } + + #same-layers-order-of-appearance { + --same-layers-appearance-order_second: var(--winning-color); + background-color: var(--same-layers-appearance-order_second); + } + } + `, + expectedMatchedSelectors: [ + // last rule in the layer wins + { + selector: "#same-layers-order-of-appearance", + value: "var(--same-layers-appearance-order_second)", + }, + { + selector: "#same-layers-order-of-appearance", + value: "var(--same-layers-appearance-order_first)", + }, + ], + }); + + info("matching rules some in layers, some not"); + await checkMatchedSelectors({ + elementAttributes: { + id: "in-layer-and-no-layer", + }, + style: ` + @layer second { + #in-layer-and-no-layer { + background-color: var(--in-layer-and-no-layer_in-second); + } + } + + @layer first { + #in-layer-and-no-layer { + background-color: var(--in-layer-and-no-layer_in-first); + } + } + + #in-layer-and-no-layer { + --in-layer-and-no-layer_no-layer: var(--winning-color); + background-color: var(--in-layer-and-no-layer_no-layer); + }`, + expectedMatchedSelectors: [ + // rule not in layer wins + { + selector: "#in-layer-and-no-layer", + value: "var(--in-layer-and-no-layer_no-layer)", + }, + { + selector: "#in-layer-and-no-layer", + value: "var(--in-layer-and-no-layer_in-second)", + }, + { + selector: "#in-layer-and-no-layer", + value: "var(--in-layer-and-no-layer_in-first)", + }, + ], + }); + + info( + "matching rules with different specificity and one property declared with !important" + ); + await checkMatchedSelectors({ + elementAttributes: { + id: "important-specificity", + class: "myImportantSection", + }, + style: ` + #important-specificity.myImportantSection { + background-color: var(--important-spec_highest); + } + #important-specificity { + --important-spec_lowest-important: var(--winning-color); + background-color: var(--important-spec_lowest-important) !important; + }`, + expectedMatchedSelectors: [ + // lesser specificity, but value was set with !important + { + selector: "#important-specificity", + value: "var(--important-spec_lowest-important)", + }, + { + selector: "#important-specificity.myImportantSection", + value: "var(--important-spec_highest)", + }, + ], + }); + + info( + "matching rules with different specificity and all properties declared with !important" + ); + await checkMatchedSelectors({ + elementAttributes: { + id: "all-important-specificity", + class: "myAllImportantSection", + }, + style: ` + #all-important-specificity.myAllImportantSection { + --all-important-spec_highest-important: var(--winning-color); + background-color: var(--all-important-spec_highest-important) !important; + } + #all-important-specificity { + background-color: var(--all-important-spec_lowest-important) !important; + }`, + expectedMatchedSelectors: [ + // all values !important, so highest specificity rule wins + { + selector: "#all-important-specificity.myAllImportantSection", + value: "var(--all-important-spec_highest-important)", + }, + { + selector: "#all-important-specificity", + value: "var(--all-important-spec_lowest-important)", + }, + ], + }); + + info( + "matching rules with same specificity and one property declared with !important" + ); + await checkMatchedSelectors({ + elementAttributes: { + id: "important-order-of-appearance", + }, + style: ` + #important-order-of-appearance { + --important-appearance-order_first-important: var(--winning-color); + background-color: var(--important-appearance-order_first-important) !important; + } + #important-order-of-appearance { + background-color: var(--important-appearance-order_second); + }`, + expectedMatchedSelectors: [ + // same specificity, but this value was set with !important + { + selector: "#important-order-of-appearance", + value: "var(--important-appearance-order_first-important)", + }, + { + selector: "#important-order-of-appearance", + value: "var(--important-appearance-order_second)", + }, + ], + }); + + info( + "matching rules with same specificity and all properties declared with !important" + ); + await checkMatchedSelectors({ + elementAttributes: { + id: "all-important-order-of-appearance", + }, + style: ` + #all-important-order-of-appearance { + background-color: var(--all-important-appearance-order_first-important) !important; + } + #all-important-order-of-appearance { + --all-important-appearance-order_second-important: var(--winning-color); + background-color: var(--all-important-appearance-order_second-important) !important; + }`, + expectedMatchedSelectors: [ + // all values !important, so latest rule in stylesheet wins + { + selector: "#all-important-order-of-appearance", + value: "var(--all-important-appearance-order_second-important)", + }, + { + selector: "#all-important-order-of-appearance", + value: "var(--all-important-appearance-order_first-important)", + }, + ], + }); + + info( + "matching rules with important property on element with style attribute" + ); + await checkMatchedSelectors({ + elementAttributes: { + id: "important-style-attr", + style: "background-color: var(--important-style-attr_in-attr);", + }, + style: ` + #important-style-attr { + --important-style-attr_in-rule-important: var(--winning-color); + background-color: var(--important-style-attr_in-rule-important) !important; + }`, + expectedMatchedSelectors: [ + // important property wins over style attribute + { + selector: "#important-style-attr", + value: "var(--important-style-attr_in-rule-important)", + }, + { selector: "this.style", value: "var(--important-style-attr_in-attr)" }, + ], + }); + + info( + "matching rules with important property on element with style attribute and important value" + ); + await checkMatchedSelectors({ + elementAttributes: { + id: "all-important-style-attr", + style: + "background-color: var(--all-important-style-attr_in-attr-important) !important;", + }, + style: ` + main { + --all-important-style-attr_in-attr-important: var(--winning-color); + } + #all-important-style-attr { + background-color: var(--all-important-style-attr_in-rule-important); + }`, + expectedMatchedSelectors: [ + // both values are important, so style attribute wins + { + selector: "this.style", + value: "var(--all-important-style-attr_in-attr-important)", + }, + { + selector: "#all-important-style-attr", + value: "var(--all-important-style-attr_in-rule-important)", + }, + ], + }); + + info( + "matching rules on different layer, with same specificity and important values" + ); + await checkMatchedSelectors({ + elementAttributes: { + id: "important-layers", + }, + style: ` + @layer second { + #important-layers { + background-color: var(--important-layers_in-second); + } + } + @layer first { + #important-layers { + --important-layers_in-first-important: var(--winning-color); + background-color: var(--important-layers_in-first-important) !important; + } + }`, + expectedMatchedSelectors: [ + // rule with important property wins + { + selector: "#important-layers", + value: "var(--important-layers_in-first-important)", + }, + { + selector: "#important-layers", + value: "var(--important-layers_in-second)", + }, + ], + }); + + info( + "matching rules on different layer, with same specificity and all important values" + ); + await checkMatchedSelectors({ + elementAttributes: { + id: "all-important-layers", + }, + style: ` + @layer second { + #all-important-layers { + background-color: var(--all-important-layers_in-second-important) !important; + } + } + @layer first { + #all-important-layers { + --all-important-layers_in-first-important: var(--winning-color); + background-color: var(--all-important-layers_in-first-important) !important; + } + }`, + expectedMatchedSelectors: [ + // all properties are important, rule from first declared layer wins + { + selector: "#all-important-layers", + value: "var(--all-important-layers_in-first-important)", + }, + { + selector: "#all-important-layers", + value: "var(--all-important-layers_in-second-important)", + }, + ], + }); + + info( + "matching rules on same layer, with same specificity and important values" + ); + await checkMatchedSelectors({ + elementAttributes: { + id: "important-same-layers-order-of-appearance", + }, + style: ` + @layer second { + #important-same-layers-order-of-appearance { + --important-same-layers-appearance-order_first-important: var(--winning-color); + background-color: var(--important-same-layers-appearance-order_first-important) !important; + } + + #important-same-layers-order-of-appearance { + background-color: var(--important-same-layers-appearance-order_second); + } + }`, + expectedMatchedSelectors: [ + // rule with important property wins + { + selector: "#important-same-layers-order-of-appearance", + value: "var(--important-same-layers-appearance-order_first-important)", + }, + { + selector: "#important-same-layers-order-of-appearance", + value: "var(--important-same-layers-appearance-order_second)", + }, + ], + }); + + info( + "matching rules on same layer, with same specificity and all important values" + ); + await checkMatchedSelectors({ + elementAttributes: { + id: "all-important-same-layers-order-of-appearance", + }, + style: ` + @layer second { + #all-important-same-layers-order-of-appearance { + background-color: var(--all-important-same-layers-appearance-order_first-important) !important; + } + + #all-important-same-layers-order-of-appearance { + --all-important-same-layers-appearance-order_second-important: var(--winning-color); + background-color: var(--all-important-same-layers-appearance-order_second-important) !important; + } + }`, + expectedMatchedSelectors: [ + // last rule with important property wins + { + selector: "#all-important-same-layers-order-of-appearance", + value: + "var(--all-important-same-layers-appearance-order_second-important)", + }, + { + selector: "#all-important-same-layers-order-of-appearance", + value: + "var(--all-important-same-layers-appearance-order_first-important)", + }, + ], + }); + + info("matching rules ,some in layers, some not, important values in layers"); + await checkMatchedSelectors({ + elementAttributes: { + id: "important-in-layer-and-no-layer", + }, + style: ` + @layer second { + #important-in-layer-and-no-layer { + background-color: var(--important-in-layer-and-no-layer_in-second); + } + } + + @layer first { + #important-in-layer-and-no-layer { + --important-in-layer-and-no-layer_in-first-important: var(--winning-color); + background-color: var(--important-in-layer-and-no-layer_in-first-important) !important; + } + } + + #important-in-layer-and-no-layer { + background-color: var(--important-in-layer-and-no-layer_no-layer); + }`, + expectedMatchedSelectors: [ + // rule with important property wins + { + selector: "#important-in-layer-and-no-layer", + value: "var(--important-in-layer-and-no-layer_in-first-important)", + }, + // then rule not in layer + { + selector: "#important-in-layer-and-no-layer", + value: "var(--important-in-layer-and-no-layer_no-layer)", + }, + { + selector: "#important-in-layer-and-no-layer", + value: "var(--important-in-layer-and-no-layer_in-second)", + }, + ], + }); + + info("matching rules ,some in layers, some not, all important values"); + await checkMatchedSelectors({ + elementAttributes: { + id: "all-important-in-layer-and-no-layer", + }, + style: ` + @layer second { + #all-important-in-layer-and-no-layer { + background-color: var(--all-important-in-layer-and-no-layer_in-second-important) !important; + } + } + + @layer first { + #all-important-in-layer-and-no-layer { + --all-important-in-layer-and-no-layer_in-first-important: var(--winning-color); + background-color: var(--all-important-in-layer-and-no-layer_in-first-important) !important; + } + } + + #all-important-in-layer-and-no-layer { + background-color: var(--all-important-in-layer-and-no-layer_no-layer-important) !important; + }`, + expectedMatchedSelectors: [ + // important properties in first declared layer wins + { + selector: "#all-important-in-layer-and-no-layer", + value: "var(--all-important-in-layer-and-no-layer_in-first-important)", + }, + // then following important rules in layers + { + selector: "#all-important-in-layer-and-no-layer", + value: "var(--all-important-in-layer-and-no-layer_in-second-important)", + }, + // then important rules not in layers + { + selector: "#all-important-in-layer-and-no-layer", + value: "var(--all-important-in-layer-and-no-layer_no-layer-important)", + }, + ], + }); + + info( + "matching rules ,some in layers, some not, and style attribute all important values" + ); + await checkMatchedSelectors({ + elementAttributes: { + id: "all-important-in-layer-no-layer-style-attr", + style: + "background-color: var(--all-important-in-layer-no-layer-style-attr_in-attr-important) !important", + }, + style: ` + main { + --all-important-in-layer-no-layer-style-attr_in-attr-important: var(--winning-color); + } + + @layer second { + #all-important-in-layer-no-layer-style-attr { + background-color: var(--all-important-in-layer-no-layer-style-attr_in-second-important) !important; + } + } + + @layer first { + #all-important-in-layer-no-layer-style-attr { + background-color: var(--all-important-in-layer-no-layer-style-attr_in-first-important) !important; + } + } + + #all-important-in-layer-no-layer-style-attr { + background-color: var(--all-important-in-layer-no-layer-style-attr_no-layer-important) !important; + }`, + expectedMatchedSelectors: [ + // important properties in style attribute wins + { + selector: "this.style", + value: + "var(--all-important-in-layer-no-layer-style-attr_in-attr-important)", + }, + // then important property in first declared layer + { + selector: "#all-important-in-layer-no-layer-style-attr", + value: + "var(--all-important-in-layer-no-layer-style-attr_in-first-important)", + }, + // then following important property in layers + { + selector: "#all-important-in-layer-no-layer-style-attr", + value: + "var(--all-important-in-layer-no-layer-style-attr_in-second-important)", + }, + // then important property not in layers + { + selector: "#all-important-in-layer-no-layer-style-attr", + value: + "var(--all-important-in-layer-no-layer-style-attr_no-layer-important)", + }, + ], + }); + + info( + "matching rules on same layer but different rules and all important values" + ); + await checkMatchedSelectors({ + elementAttributes: { + id: "all-important-same-layer-different-rule", + }, + style: ` + @layer first { + #all-important-same-layer-different-rule { + background-color: var(--all-important-same-layer-different-rule_first-important) !important; + } + } + + @layer first { + #all-important-same-layer-different-rule { + --all-important-same-layer-different-rule_second-important: var(--winning-color); + background-color: var(--all-important-same-layer-different-rule_second-important) !important; + } + }`, + expectedMatchedSelectors: [ + // last rule for the layer with important property wins + { + selector: "#all-important-same-layer-different-rule", + value: + "var(--all-important-same-layer-different-rule_second-important)", + }, + { + selector: "#all-important-same-layer-different-rule", + value: "var(--all-important-same-layer-different-rule_first-important)", + }, + ], + }); + + info( + "matching rules on same layer but different nested rules and all important values" + ); + await checkMatchedSelectors({ + elementAttributes: { + id: "all-important-same-nested-layer-different-rule", + }, + style: ` + @layer first { + @layer { + @layer second { + #all-important-same-nested-layer-different-rule { + background-color: var(--all-important-same-nested-layer-different-rule_first-important) !important; + } + } + + @layer second { + #all-important-same-nested-layer-different-rule { + --all-important-same-nested-layer-different-rule_second-important: var(--winning-color); + background-color: var(--all-important-same-nested-layer-different-rule_second-important) !important; + } + } + } + }`, + expectedMatchedSelectors: [ + // last rule for the layer with important property wins + { + selector: "#all-important-same-nested-layer-different-rule", + value: + "var(--all-important-same-nested-layer-different-rule_second-important)", + }, + { + selector: "#all-important-same-nested-layer-different-rule", + value: + "var(--all-important-same-nested-layer-different-rule_first-important)", + }, + ], + }); + + info("matching rules on different nameless layers and all important values"); + await checkMatchedSelectors({ + elementAttributes: { + id: "all-important-different-nameless-layers", + }, + style: ` + @layer { + @layer first { + #all-important-different-nameless-layers { + --all-important-different-nameless-layers_first-important: var(--winning-color); + background-color: var(--all-important-different-nameless-layers_first-important) !important; + } + } + } + @layer { + @layer first { + #all-important-different-nameless-layers { + background-color: var(--all-important-different-nameless-layers_second-important) !important; + } + } + }`, + expectedMatchedSelectors: [ + // rule with important property in first declared layer wins + { + selector: "#all-important-different-nameless-layers", + value: "var(--all-important-different-nameless-layers_first-important)", + }, + { + selector: "#all-important-different-nameless-layers", + value: + "var(--all-important-different-nameless-layers_second-important)", + }, + ], + }); + + info("matching rules on different imported layers"); + // no provided style as rules are defined in doc_matched_selectors_imported_*.css + await checkMatchedSelectors({ + elementAttributes: { + id: "imported-layers", + }, + expectedMatchedSelectors: [ + // rule in last declared layer wins + { + selector: "#imported-layers", + value: "var(--imported-layers_in-anonymous-second)", + }, + { + selector: "#imported-layers", + value: "var(--imported-layers_in-nested-importedSecond)", + }, + { + selector: "#imported-layers", + value: "var(--imported-layers_in-anonymous-first)", + }, + { + selector: "#imported-layers", + value: "var(--imported-layers_in-importedSecond)", + }, + { + selector: "#imported-layers", + value: "var(--imported-layers_in-importedFirst-second)", + }, + { + selector: "#imported-layers", + value: "var(--imported-layers_in-importedFirst-first)", + }, + ], + }); + + info("matching rules on different imported layers all with important values"); + // no provided style as rules are defined in doc_matched_selectors_imported_*.css + await checkMatchedSelectors({ + elementAttributes: { + id: "all-important-imported-layers", + }, + expectedMatchedSelectors: [ + // last important property in first declared layer wins + { + selector: "#all-important-imported-layers", + value: + "var(--all-important-imported-layers_in-importedFirst-second-important)", + }, + // then earlier important property for first declared layer + { + selector: "#all-important-imported-layers", + value: + "var(--all-important-imported-layers_in-importedFirst-first-important)", + }, + { + selector: "#all-important-imported-layers", + value: + "var(--all-important-imported-layers_in-importedSecond-important)", + }, + { + selector: "#all-important-imported-layers", + value: + "var(--all-important-imported-layers_in-anonymous-first-important)", + }, + { + selector: "#all-important-imported-layers", + value: + "var(--all-important-imported-layers_in-nested-importedSecond-important)", + }, + { + selector: "#all-important-imported-layers", + value: + "var(--all-important-imported-layers_in-anonymous-second-important)", + }, + ], + }); +}); + +async function checkBackgroundColorMatchedSelectors( + inspector, + view, + { elementAttributes, style, expectedMatchedSelectors } +) { + const elementId = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [elementAttributes, style], + (attr, _style) => { + const sectionEl = content.document.createElement("section"); + for (const [name, value] of Object.entries(attr)) { + sectionEl.setAttribute(name, value); + } + + if (_style) { + const styleEl = content.document.createElement("style"); + styleEl.innerText = _style; + styleEl.setAttribute("id", `style-${sectionEl.id}`); + content.document.head.append(styleEl); + } + content.document.querySelector("main").append(sectionEl); + + return sectionEl.id; + } + ); + const selector = `#${elementId}`; + await selectNode(selector, inspector); + + const bgColorComputedValue = await getComputedStyleProperty( + selector, + null, + "background-color" + ); + is( + bgColorComputedValue, + "rgb(0, 0, 255)", + `The created element does have a "blue" background-color` + ); + + const propertyView = getPropertyView(view, "background-color"); + ok(propertyView, "found PropertyView for background-color"); + const valueNode = propertyView.valueNode.querySelector(".computed-color"); + is( + valueNode.textContent, + "rgb(0, 0, 255)", + `The displayed computed value is the expected "blue"` + ); + + is(propertyView.hasMatchedSelectors, true, "hasMatchedSelectors is true"); + + info("Expanding the matched selectors"); + propertyView.matchedExpanded = true; + await propertyView.refreshMatchedSelectors(); + + const selectorsEl = + propertyView.matchedSelectorsContainer.querySelectorAll(".rule-text"); + is( + selectorsEl.length, + expectedMatchedSelectors.length, + "Expected number of selectors are displayed" + ); + + selectorsEl.forEach((selectorEl, index) => { + is( + selectorEl.querySelector(".fix-get-selection").innerText, + expectedMatchedSelectors[index].selector, + `Selector #${index} is the expected one` + ); + is( + selectorEl.querySelector(".computed-other-property-value").innerText, + expectedMatchedSelectors[index].value, + `Selector #${index} has the expected background color` + ); + const classToMatch = index === 0 ? "bestmatch" : "matched"; + ok( + selectorEl.classList.contains(classToMatch), + `selector element has expected "${classToMatch}" class` + ); + }); + + // cleanup + await SpecialPowers.spawn(gBrowser.selectedBrowser, [elementId], id => { + // Remove added element and stylesheet + content.document.getElementById(id).remove(); + // Some test cases don't insert a style element + content.document.getElementById(`style-${id}`)?.remove(); + }); +} + +function getPropertyView(computedView, name) { + return computedView.propertyViews.find(view => view.name === name); +} 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 0000000000..2de29c2607 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_matched-selectors-toggle.js @@ -0,0 +1,125 @@ +/* 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(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view } = await openComputedView(); + await selectNode("h1", inspector); + + await testExpandOnTwistyClick(view, inspector); + await testCollapseOnTwistyClick(view, inspector); + await testExpandOnDblClick(view, inspector); + await testCollapseOnDblClick(view, inspector); +}); + +async function testExpandOnTwistyClick({ styleDocument }, inspector) { + info("Testing that a property expands on twisty click"); + + info("Getting twisty element"); + const twisty = styleDocument.querySelector(".computed-expandable"); + ok(twisty, "Twisty found"); + + const onExpand = inspector.once("computed-view-property-expanded"); + info("Clicking on the twisty element"); + twisty.click(); + + await onExpand; + + // Expanded means the matchedselectors div is not empty + const matchedSelectorsEl = twisty + .closest(".computed-property-view") + .querySelector(".matchedselectors"); + ok( + !!matchedSelectorsEl.childNodes.length, + "Matched selectors are expanded on twisty click" + ); +} + +async function testCollapseOnTwistyClick({ styleDocument }, inspector) { + info("Testing that a property collapses on twisty click"); + + info("Getting twisty element"); + const twisty = styleDocument.querySelector(".computed-expandable"); + ok(twisty, "Twisty found"); + + const onCollapse = inspector.once("computed-view-property-collapsed"); + info("Clicking on the twisty element"); + twisty.click(); + + await onCollapse; + + // Collapsed means the matchedselectors div is empty + const matchedSelectorsEl = twisty + .closest(".computed-property-view") + .querySelector(".matchedselectors"); + is( + matchedSelectorsEl.childNodes.length, + 0, + "Matched selectors are collapsed on twisty click" + ); +} + +async function testExpandOnDblClick({ styleDocument, styleWindow }, inspector) { + info("Testing that a property expands on container dbl-click"); + + info("Getting computed property container"); + const container = styleDocument.querySelector( + "#computed-container .computed-property-view" + ); + ok(container, "Container found"); + + container.scrollIntoView(); + + const onExpand = inspector.once("computed-view-property-expanded"); + info("Dbl-clicking on the container"); + EventUtils.synthesizeMouseAtCenter(container, { clickCount: 2 }, styleWindow); + + await onExpand; + + // Expanded means the matchedselectors div is not empty + const matchedSelectorsEl = container.querySelector(".matchedselectors"); + ok( + !!matchedSelectorsEl.childNodes.length, + "Matched selectors are expanded on dblclick" + ); +} + +async function testCollapseOnDblClick( + { styleDocument, styleWindow }, + inspector +) { + info("Testing that a property collapses on container dbl-click"); + + info("Getting computed property container"); + const container = styleDocument.querySelector( + "#computed-container .computed-property-view" + ); + ok(container, "Container found"); + + const onCollapse = inspector.once("computed-view-property-collapsed"); + info("Dbl-clicking on the container"); + EventUtils.synthesizeMouseAtCenter(container, { clickCount: 2 }, styleWindow); + + await onCollapse; + + // Collapsed means the matchedselectors div is empty + const matchedSelectorsEl = container.querySelector(".matchedselectors"); + is( + matchedSelectorsEl.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 0000000000..bb90dfb4ea --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_matched-selectors_01.js @@ -0,0 +1,48 @@ +/* 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("resource://devtools/client/inspector/computed/computed.js"); +const TEST_URI = URL_ROOT + "doc_matched_selectors.html"; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, view } = await openComputedView(); + + await selectNode("#test", inspector); + await testMatchedSelectors(view, inspector); +}); + +async function testMatchedSelectors(view, inspector) { + info("checking selector counts, matched rules and titles"); + + const nodeFront = await getNodeFront("#test", inspector); + is( + nodeFront, + view._viewedElement, + "style inspector node matches the selected node" + ); + + const propertyView = new PropertyView(view, "color"); + propertyView.createListItemElement(); + propertyView.matchedExpanded = true; + + await propertyView.refreshMatchedSelectors(); + + const numMatchedSelectors = propertyView.matchedSelectors.length; + is( + numMatchedSelectors, + 7, + "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 0000000000..b0327a30cf --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_matched-selectors_02.js @@ -0,0 +1,40 @@ +/* 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(async function () { + await addTab("data:text/html;charset=utf-8,<div style='color:blue;'></div>"); + const { inspector, view } = await openComputedView(); + await selectNode("div", inspector); + + info("Checking the color property view"); + const 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; + await propertyView.refreshMatchedSelectors(); + + const span = + propertyView.matchedSelectorsContainer.querySelector("span.rule-text"); + ok(span, "Found the first table row"); + + const 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 0000000000..9f09fc7e3c --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_media-queries.js @@ -0,0 +1,42 @@ +/* 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("resource://devtools/client/inspector/computed/computed.js"); + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, view } = await openComputedView(); + await selectNode("div", inspector); + await checkPropertyView(view); +}); + +function checkPropertyView(view) { + const propertyView = new PropertyView(view, "width"); + propertyView.createListItemElement(); + propertyView.matchedExpanded = true; + + return propertyView.refreshMatchedSelectors().then(() => { + const 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 0000000000..3e63bfb9b3 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_no-results-placeholder.js @@ -0,0 +1,69 @@ +/* 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(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view } = await openComputedView(); + await selectNode("#matches", inspector); + + await enterInvalidFilter(inspector, view); + checkNoResultsPlaceholderShown(view); + + await clearFilterText(inspector, view); + checkNoResultsPlaceholderHidden(view); +}); + +async function enterInvalidFilter(inspector, computedView) { + const searchbar = computedView.searchField; + const searchTerm = "xxxxx"; + + info('setting filter text to "' + searchTerm + '"'); + + const onRefreshed = inspector.once("computed-view-refreshed"); + searchbar.focus(); + synthesizeKeys(searchTerm, computedView.styleWindow); + await onRefreshed; +} + +function checkNoResultsPlaceholderShown(computedView) { + info("Checking that the no results placeholder is shown"); + + const placeholder = computedView.noResults; + const win = computedView.styleWindow; + const display = win.getComputedStyle(placeholder).display; + is(display, "block", "placeholder is visible"); +} + +async function clearFilterText(inspector, computedView) { + info("Clearing the filter text"); + + const searchbar = computedView.searchField; + + const onRefreshed = inspector.once("computed-view-refreshed"); + searchbar.focus(); + searchbar.value = ""; + EventUtils.synthesizeKey("c", {}, computedView.styleWindow); + await onRefreshed; +} + +function checkNoResultsPlaceholderHidden(computedView) { + info("Checking that the no results placeholder is hidden"); + + const placeholder = computedView.noResults; + const win = computedView.styleWindow; + const 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 0000000000..771d349b3b --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_original-source-link.js @@ -0,0 +1,71 @@ +/* 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.source-map.client-service.enabled"; +const SCSS_LOC = "doc_sourcemaps.scss:4"; +const CSS_LOC = "doc_sourcemaps.css:1"; + +add_task(async function () { + info("Turning the pref " + PREF + " on"); + Services.prefs.setBoolPref(PREF, true); + + await addTab(TESTCASE_URI); + const { toolbox, inspector, view } = await openComputedView(); + let onLinksUpdated = inspector.once("computed-view-sourcelinks-updated"); + await selectNode("div", inspector); + + info("Expanding the first property"); + await expandComputedViewPropertyByIndex(view, 0); + + info("Verifying the link text"); + await onLinksUpdated; + verifyLinkText(view, SCSS_LOC); + + info("Toggling the pref"); + onLinksUpdated = inspector.once("computed-view-sourcelinks-updated"); + Services.prefs.setBoolPref(PREF, false); + await onLinksUpdated; + + info("Verifying that the link text has changed after the pref change"); + await verifyLinkText(view, CSS_LOC); + + info("Toggling the pref again"); + onLinksUpdated = inspector.once("computed-view-sourcelinks-updated"); + Services.prefs.setBoolPref(PREF, true); + await onLinksUpdated; + + info("Testing that clicking on the link works"); + await testClickingLink(toolbox, view); + + info("Turning the pref " + PREF + " off"); + Services.prefs.clearUserPref(PREF); +}); + +async function testClickingLink(toolbox, view) { + const onEditor = waitForStyleEditor(toolbox, "doc_sourcemaps.scss"); + + info("Clicking the computedview stylesheet link"); + const link = getComputedViewLinkByIndex(view, 0); + link.scrollIntoView(); + link.click(); + + const editor = await onEditor; + + const { line } = editor.sourceEditor.getCursor(); + is(line, 3, "cursor is at correct line number in original source"); +} + +function verifyLinkText(view, text) { + const 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 0000000000..e972a0257a --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_pseudo-element_01.js @@ -0,0 +1,38 @@ +/* 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(async function () { + await addTab(TEST_URI); + const { inspector, view } = await openComputedView(); + await testTopLeft(inspector, view); +}); + +async function testTopLeft(inspector, view) { + const node = await getNodeFront("#topleft", inspector.markup); + await selectNode(node, inspector); + const float = getComputedViewPropertyValue(view, "float"); + is(float, "left", "The computed view shows the correct float"); + + const children = await inspector.markup.walker.children(node); + is(children.nodes.length, 3, "Element has correct number of children"); + + const beforeElement = children.nodes[0]; + await 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"); + + const afterElement = children.nodes[children.nodes.length - 1]; + await selectNode(afterElement, inspector); + top = getComputedViewPropertyValue(view, "top"); + is(top, "96px", "The computed view shows the correct top"); + left = getComputedViewPropertyValue(view, "left"); + is(left, "96px", "The computed view shows the correct left"); +} diff --git a/devtools/client/inspector/computed/test/browser_computed_refresh-on-ruleview-change.js b/devtools/client/inspector/computed/test/browser_computed_refresh-on-ruleview-change.js new file mode 100644 index 0000000000..12b7901970 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_refresh-on-ruleview-change.js @@ -0,0 +1,93 @@ +/* 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 rule view is updated in 3 pane mode. + +const TEST_URI = "<div id='target' style='color: rgb(255, 0, 0);'>test</div>"; + +add_task(async function () { + info( + "Check whether the color as well in computed view is updated " + + "when the rule in rule view is changed in case of 3 pane mode" + ); + await pushPref("devtools.inspector.three-pane-enabled", true); + + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view } = await openComputedView(); + await selectNode("#target", inspector); + + is( + getComputedViewPropertyValue(view, "color"), + "rgb(255, 0, 0)", + "The computed view shows the right color" + ); + + info("Change the value in the ruleview"); + const ruleView = inspector.getPanel("ruleview").view; + const editor = await getValueEditor(ruleView); + const onRuleViewChanged = ruleView.once("ruleview-changed"); + const onComputedViewRefreshed = inspector.once("computed-view-refreshed"); + editor.input.value = "rgb(0, 255, 0)"; + EventUtils.synthesizeKey("VK_RETURN", {}, ruleView.styleWindow); + await Promise.all([onRuleViewChanged, onComputedViewRefreshed]); + + info("Check the value in the computed view"); + is( + getComputedViewPropertyValue(view, "color"), + "rgb(0, 255, 0)", + "The computed value is updated when the rule in ruleview is changed" + ); +}); + +add_task(async function () { + info( + "Check that the computed view is not updated " + + "if the rule view is changed in 2 pane mode." + ); + await pushPref("devtools.inspector.three-pane-enabled", false); + + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector } = await openComputedView(); + await selectNode("#target", inspector); + + info("Select the rule view"); + const ruleView = inspector.getPanel("ruleview").view; + const onRuleViewReady = ruleView.once("ruleview-refreshed"); + const onSidebarSelect = inspector.sidebar.once("select"); + inspector.sidebar.select("ruleview"); + await Promise.all([onSidebarSelect, onRuleViewReady]); + + info( + "Prepare the counter which counts how many times computed view is refreshed" + ); + let computedViewRefreshCount = 0; + const computedViewRefreshListener = () => { + computedViewRefreshCount += 1; + }; + inspector.on("computed-view-refreshed", computedViewRefreshListener); + + info("Change the value in the rule view"); + const editor = await getValueEditor(ruleView); + const onRuleViewChanged = ruleView.once("ruleview-changed"); + editor.input.value = "rgb(0, 255, 0)"; + EventUtils.synthesizeKey("VK_RETURN", {}, ruleView.styleWindow); + await onRuleViewChanged; + + info( + "Wait for time enough to check whether the computed value is updated or not" + ); + await wait(1000); + + info("Check the counter"); + is(computedViewRefreshCount, 0, "The computed view is not updated"); + + inspector.off("computed-view-refreshed", computedViewRefreshListener); +}); + +async function getValueEditor(ruleView) { + const ruleEditor = ruleView.element.children[0]._ruleEditor; + const propEditor = ruleEditor.rule.textProps[0].editor; + return focusEditableField(ruleView, propEditor.valueSpan); +} 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 0000000000..190593497b --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_refresh-on-style-change_01.js @@ -0,0 +1,32 @@ +/* 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(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view } = await openComputedView(); + await 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"); + const onUpdated = inspector.once("computed-view-refreshed"); + await setContentPageElementAttribute( + "#testdiv", + "style", + "font-size: 15px; color: red;" + ); + await onUpdated; + + fontSize = getComputedViewPropertyValue(view, "font-size"); + is(fontSize, "15px", "The computed view shows the updated font-size"); + const 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 0000000000..a22cdb4038 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_search-filter.js @@ -0,0 +1,67 @@ +/* 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(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view } = await openComputedView(); + await selectNode("#matches", inspector); + await testToggleDefaultStyles(inspector, view); + await testAddTextInFilter(inspector, view); +}); + +async function testToggleDefaultStyles(inspector, computedView) { + info('checking "Browser styles" checkbox'); + const checkbox = computedView.includeBrowserStylesCheckbox; + const onRefreshed = inspector.once("computed-view-refreshed"); + checkbox.click(); + await onRefreshed; +} + +async function testAddTextInFilter(inspector, computedView) { + info('setting filter text to "color"'); + const searchField = computedView.searchField; + const onRefreshed = inspector.once("computed-view-refreshed"); + const 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); + await onRefreshed; + + info("check that the correct properties are visible"); + + const propertyViews = computedView.propertyViews; + propertyViews.forEach(propView => { + const 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 0000000000..f77a71cc18 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_search-filter_clear.js @@ -0,0 +1,72 @@ +/* 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(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view } = await openComputedView(); + await selectNode("#matches", inspector); + await testAddTextInFilter(inspector, view); + await testClearSearchFilter(inspector, view); +}); + +async function testAddTextInFilter(inspector, computedView) { + info('Setting filter text to "background-color"'); + + const win = computedView.styleWindow; + const propertyViews = computedView.propertyViews; + const searchField = computedView.searchField; + + searchField.focus(); + synthesizeKeys("background-color", win); + await inspector.once("computed-view-refreshed"); + + info("Check that the correct properties are visible"); + + propertyViews.forEach(propView => { + const name = propView.name; + is( + propView.visible, + name.indexOf("background-color") > -1, + "span " + name + " property visibility check" + ); + }); +} + +async function testClearSearchFilter(inspector, computedView) { + info("Clearing the search filter"); + + const win = computedView.styleWindow; + const propertyViews = computedView.propertyViews; + const searchField = computedView.searchField; + const searchClearButton = computedView.searchClearButton; + const onRefreshed = inspector.once("computed-view-refreshed"); + + EventUtils.synthesizeMouseAtCenter(searchClearButton, {}, win); + await onRefreshed; + + 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 0000000000..0069d644c7 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_search-filter_context-menu.js @@ -0,0 +1,101 @@ +/* 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(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { toolbox, inspector, view } = await openComputedView(); + await selectNode("h1", inspector); + + const searchField = view.searchField; + + info("Opening context menu"); + + emptyClipboard(); + + const onFocus = once(searchField, "focus"); + searchField.focus(); + await onFocus; + + let onContextMenuOpen = toolbox.once("menu-open"); + synthesizeContextMenuEvent(searchField); + await onContextMenuOpen; + + let searchContextMenu = toolbox.getTextBoxContextMenu(); + ok( + searchContextMenu, + "The search filter context menu is loaded in the computed view" + ); + + let cmdUndo = searchContextMenu.querySelector("#editmenu-undo"); + let cmdDelete = searchContextMenu.querySelector("#editmenu-delete"); + let cmdSelectAll = searchContextMenu.querySelector("#editmenu-selectAll"); + let cmdCut = searchContextMenu.querySelector("#editmenu-cut"); + let cmdCopy = searchContextMenu.querySelector("#editmenu-copy"); + let cmdPaste = searchContextMenu.querySelector("#editmenu-paste"); + + is(cmdUndo.getAttribute("disabled"), "true", "cmdUndo is disabled"); + is(cmdDelete.getAttribute("disabled"), "true", "cmdDelete is disabled"); + is(cmdSelectAll.getAttribute("disabled"), "true", "cmdSelectAll is disabled"); + + is(cmdCut.getAttribute("disabled"), "true", "cmdCut is disabled"); + is(cmdCopy.getAttribute("disabled"), "true", "cmdCopy is disabled"); + 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 onContextMenuClose = toolbox.once("menu-close"); + searchContextMenu.hidePopup(); + await onContextMenuClose; + + info("Copy text in search field using the context menu"); + searchField.setUserInput(TEST_INPUT); + searchField.select(); + + onContextMenuOpen = toolbox.once("menu-open"); + synthesizeContextMenuEvent(searchField); + await onContextMenuOpen; + + searchContextMenu = toolbox.getTextBoxContextMenu(); + cmdCopy = searchContextMenu.querySelector("#editmenu-copy"); + onContextMenuClose = toolbox.once("menu-close"); + await waitForClipboardPromise( + () => searchContextMenu.activateItem(cmdCopy), + TEST_INPUT + ); + await onContextMenuClose; + + info("Reopen context menu and check command properties"); + + onContextMenuOpen = toolbox.once("menu-open"); + synthesizeContextMenuEvent(searchField); + await onContextMenuOpen; + + searchContextMenu = toolbox.getTextBoxContextMenu(); + cmdUndo = searchContextMenu.querySelector("#editmenu-undo"); + cmdDelete = searchContextMenu.querySelector("#editmenu-delete"); + cmdSelectAll = searchContextMenu.querySelector("#editmenu-selectAll"); + cmdCut = searchContextMenu.querySelector("#editmenu-cut"); + cmdCopy = searchContextMenu.querySelector("#editmenu-copy"); + cmdPaste = searchContextMenu.querySelector("#editmenu-paste"); + + 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"); + + onContextMenuClose = toolbox.once("menu-close"); + searchContextMenu.hidePopup(); + await onContextMenuClose; +}); 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 0000000000..59c71d01fe --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_search-filter_escape-keypress.js @@ -0,0 +1,76 @@ +/* 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(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view } = await openComputedView(); + await selectNode("#matches", inspector); + await testAddTextInFilter(inspector, view); + await testEscapeKeypress(inspector, view); +}); + +async function testAddTextInFilter(inspector, computedView) { + info('Setting filter text to "background-color"'); + + const win = computedView.styleWindow; + const propertyViews = computedView.propertyViews; + const searchField = computedView.searchField; + const checkbox = computedView.includeBrowserStylesCheckbox; + + info("Include browser styles"); + checkbox.click(); + await inspector.once("computed-view-refreshed"); + + searchField.focus(); + synthesizeKeys("background-color", win); + await inspector.once("computed-view-refreshed"); + + info("Check that the correct properties are visible"); + + propertyViews.forEach(propView => { + const name = propView.name; + is( + propView.visible, + name.indexOf("background-color") > -1, + "span " + name + " property visibility check" + ); + }); +} + +async function testEscapeKeypress(inspector, computedView) { + info("Pressing the escape key on search filter"); + + const win = computedView.styleWindow; + const propertyViews = computedView.propertyViews; + const searchField = computedView.searchField; + const onRefreshed = inspector.once("computed-view-refreshed"); + + searchField.focus(); + EventUtils.synthesizeKey("VK_ESCAPE", {}, win); + await onRefreshed; + + info("Check that the correct properties are visible"); + + ok(!searchField.value, "Search filter is cleared"); + propertyViews.forEach(propView => { + const 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 0000000000..b24722e237 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_search-filter_noproperties.js @@ -0,0 +1,68 @@ +/* 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(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view } = await openComputedView(); + const propertyViews = view.propertyViews; + + info("Select the #matches node"); + const matchesNode = await getNodeFront("#matches", inspector); + let onRefresh = inspector.once("computed-view-refreshed"); + await selectNode(matchesNode, inspector); + await onRefresh; + + ok( + !!propertyViews.filter(p => p.visible).length, + "CSS properties are displayed" + ); + ok(view.noResults.hasAttribute("hidden"), "no-results message is hidden"); + + info("Select a comment node"); + const commentNode = await inspector.walker.previousSibling(matchesNode); + await 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"); + await selectNode(matchesNode, inspector); + await onRefresh; + + ok( + !!propertyViews.filter(p => p.visible).length, + "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" + ); + const searchField = view.searchField; + searchField.focus(); + synthesizeKeys("will-not-match", view.styleWindow); + await 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-01.js b/devtools/client/inspector/computed/test/browser_computed_select-and-copy-styles-01.js new file mode 100644 index 0000000000..4c75d2885e --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_select-and-copy-styles-01.js @@ -0,0 +1,67 @@ +/* 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 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(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view } = await openComputedView(); + await selectNode("span", inspector); + + await testCopySome(view); + await testCopyAll(view); +}); + +async function testCopySome(view) { + const expectedPattern = + "font-family: helvetica, sans-serif;[\\r\\n]+" + + "font-size: 16px;[\\r\\n]+" + + "font-variant-caps: small-caps;[\\r\\n]*"; + + await copySomeTextAndCheckClipboard( + view, + { + start: { prop: 1, offset: 0 }, + end: { prop: 3, offset: 3 }, + }, + expectedPattern + ); +} + +async function testCopyAll(view) { + const 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]*"; + + await copyAllAndCheckClipboard(view, expectedPattern); +} diff --git a/devtools/client/inspector/computed/test/browser_computed_select-and-copy-styles-02.js b/devtools/client/inspector/computed/test/browser_computed_select-and-copy-styles-02.js new file mode 100644 index 0000000000..87fe1d9629 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_select-and-copy-styles-02.js @@ -0,0 +1,35 @@ +/* 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 TEST_URI = `<div style="text-align:left;width:25px;">Hello world</div>`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector, view } = await openComputedView(); + await selectNode("div", inspector); + + let expectedPattern = "text-align: left;[\\r\\n]+" + "width: 25px;[\\r\\n]*"; + await copyAllAndCheckClipboard(view, expectedPattern); + + info("Testing expand then select all copy"); + + expectedPattern = + "text-align: left;[\\r\\n]+" + + "element[\\r\\n]+" + + "Best Match this.style[\\r\\n]+" + + "left[\\r\\n]+" + + "width: 25px;[\\r\\n]+" + + "element[\\r\\n]+" + + "Best Match this.style[\\r\\n]+" + + "25px[\\r\\n]*"; + + info("Expanding computed view properties"); + await expandComputedViewPropertyByIndex(view, 0); + await expandComputedViewPropertyByIndex(view, 1); + + await copyAllAndCheckClipboard(view, expectedPattern); +}); diff --git a/devtools/client/inspector/computed/test/browser_computed_shadow_host.js b/devtools/client/inspector/computed/test/browser_computed_shadow_host.js new file mode 100644 index 0000000000..19562eb1ed --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_shadow_host.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { + PropertyView, +} = require("resource://devtools/client/inspector/computed/computed.js"); + +// Test matched selectors for a :host selector in the computed view. + +const SHADOW_DOM = `<style> + :host { + color: red; + } + + .test-span { + color: blue; + } +</style> +<span class="test-span">test</span>`; + +const TEST_PAGE = ` + <div id="host"></div> + <script> + const div = document.querySelector("div"); + div.attachShadow({ mode: "open" }).innerHTML = \`${SHADOW_DOM}\`; + </script>`; + +const TEST_URI = `https://example.com/document-builder.sjs?html=${encodeURIComponent( + TEST_PAGE +)}`; + +add_task(async function () { + await addTab(TEST_URI); + const { inspector, view } = await openComputedView(); + + { + await selectNode("#host", inspector); + const propertyView = await getPropertyViewWithSelectors(view, "color"); + const selectors = propertyView.matchedSelectors.map(s => s.selector); + Assert.deepEqual( + selectors, + [":host", ":root"], + "host has the expected selectors for color" + ); + } + + { + const nodeFront = await getNodeFrontInShadowDom( + ".test-span", + "#host", + inspector + ); + await selectNode(nodeFront, inspector); + const propertyView = await getPropertyViewWithSelectors(view, "color"); + const selectors = propertyView.matchedSelectors.map(s => s.selector); + Assert.deepEqual( + selectors, + [".test-span", ":host", ":root"], + "shadow host child has the expected selectors for color" + ); + } +}); + +async function getPropertyViewWithSelectors(view, property) { + const propertyView = new PropertyView(view, property); + propertyView.createListItemElement(); + propertyView.matchedExpanded = true; + + await propertyView.refreshMatchedSelectors(); + + return propertyView; +} 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 0000000000..832c1658a5 --- /dev/null +++ b/devtools/client/inspector/computed/test/browser_computed_style-editor-link.js @@ -0,0 +1,210 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// 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}"> + <script> + const sheet = new CSSStyleSheet(); + sheet.replaceSync(".highlight { color: tomato; }"); + document.adoptedStyleSheets.push(sheet); + </script> + </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(async function () { + await addTab(DOCUMENT_URL); + const { toolbox, inspector, view } = await openComputedView(); + await selectNode("span", inspector); + + await testInlineStyle(view); + await testFirstInlineStyleSheet(view, toolbox); + await testSecondInlineStyleSheet(view, toolbox); + await testExternalStyleSheet(view, toolbox); + await testConstructedStyleSheet(view, toolbox); +}); + +async function testInlineStyle(view) { + info("Testing inline style"); + + await expandComputedViewPropertyByIndex(view, 0); + + const onTab = waitForTab(); + info("Clicking on the first rule-link in the computed-view"); + checkComputedViewLink(view, { + index: 0, + expectedText: "element", + expectedTitle: "element", + }); + + const tab = await onTab; + + const tabURI = tab.linkedBrowser.documentURI.spec; + ok(tabURI.startsWith("view-source:"), "View source tab is open"); + info("Closing tab"); + gBrowser.removeTab(tab); +} + +async function testFirstInlineStyleSheet(view, toolbox) { + info("Testing inline stylesheet"); + + info("Listening for toolbox switch to the styleeditor"); + const onSwitch = waitForStyleEditor(toolbox); + + info("Clicking an inline stylesheet"); + checkComputedViewLink(view, { + index: 3, + expectedText: "inline:3", + expectedTitle: "inline:3", + }); + const editor = await onSwitch; + + ok(true, "Switched to the style-editor panel in the toolbox"); + + await validateStyleEditorSheet(editor, 0); +} + +async function testSecondInlineStyleSheet(view, toolbox) { + info("Testing second inline stylesheet"); + + const panel = toolbox.getCurrentPanel(); + const onSelected = panel.UI.once("editor-selected"); + + info("Switching back to the inspector panel in the toolbox"); + await toolbox.selectTool("inspector"); + + info("Clicking on second inline stylesheet link"); + checkComputedViewLink(view, { + index: 5, + expectedText: "inline:2", + expectedTitle: "inline:2", + }); + + info("Waiting for an editor to be selected in StyleEditor"); + const editor = await onSelected; + + is( + toolbox.currentToolId, + "styleeditor", + "The style editor is selected again" + ); + await validateStyleEditorSheet(editor, 1); +} + +async function testExternalStyleSheet(view, toolbox) { + info("Testing external stylesheet"); + + const panel = toolbox.getCurrentPanel(); + const onSelected = panel.UI.once("editor-selected"); + + info("Switching back to the inspector panel in the toolbox"); + await toolbox.selectTool("inspector"); + + info("Clicking on an external stylesheet link"); + checkComputedViewLink(view, { + index: 2, + expectedText: `${STYLESHEET_URL.replace("data:text/css,", "")}:1`, + expectedTitle: `${STYLESHEET_URL}:1`, + }); + + info("Waiting for an editor to be selected in StyleEditor"); + const editor = await onSelected; + + is( + toolbox.currentToolId, + "styleeditor", + "The style editor is selected again" + ); + await validateStyleEditorSheet(editor, 2); +} + +async function testConstructedStyleSheet(view, toolbox) { + info("Testing constructed stylesheet"); + + const panel = toolbox.getCurrentPanel(); + const onSelected = panel.UI.once("editor-selected"); + + info("Switching back to the inspector panel in the toolbox"); + await toolbox.selectTool("inspector"); + + info("Clicking on an constructed stylesheet link"); + + checkComputedViewLink(view, { + index: 1, + expectedText: "constructed", + expectedTitle: "constructed", + }); + + info("Waiting for an editor to be selected in StyleEditor"); + const editor = await onSelected; + + is( + toolbox.currentToolId, + "styleeditor", + "The style editor is selected again" + ); + ok(editor.styleSheet.constructed, "The constructed stylesheet is selected"); +} + +async function validateStyleEditorSheet(editor, expectedSheetIndex) { + info("Validating style editor stylesheet"); + const expectedHref = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [expectedSheetIndex], + _expectedSheetIndex => + content.document.styleSheets[_expectedSheetIndex].href + ); + is( + editor.styleSheet.href, + expectedHref, + "loaded stylesheet matches document stylesheet" + ); +} + +function checkComputedViewLink(view, { index, expectedText, expectedTitle }) { + const link = getComputedViewLinkByIndex(view, index); + is(link.innerText, expectedText, `Link #${index} has expected label`); + is( + link.getAttribute("title"), + expectedTitle, + `Link #${index} has expected title attribute` + ); + 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 0000000000..41abe48826 --- /dev/null +++ b/devtools/client/inspector/computed/test/doc_matched_selectors.html @@ -0,0 +1,54 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE html> +<html> + <head> + <meta charset="utf8"> + <style> + @import url(./doc_matched_selectors_imported_1.css) layer(importedFirst); + @import url(./doc_matched_selectors_imported_2.css) layer(importedSecond); + @import url(./doc_matched_selectors_imported_3.css) layer(importedFirst); + @import url(./doc_matched_selectors_imported_4.css) layer; + @import url(./doc_matched_selectors_imported_5.css) layer; + + @layer first, second; + + .matched1, .matched2, .matched3, .matched4, .matched5 { + color: #000; + } + + div { + position: absolute; + top: 40px; + left: 20px; + border: 1px solid #000; + color: #111; + width: 100px; + height: 50px; + } + + main { + /* + * Set "winning" custom properties values to "blue" so we can check in the + * test that the best matching rule/property is actually what is applied by + * the engine. + */ + --winning-color: blue; + } + + section { + min-width: 10px; + min-height: 10px; + display: inline-block; + } + </style> + </head> + <body> + inspectstyle($("test")); + <div id="test" class="matched1 matched2 matched3 matched4 matched5">Test div</div> + <div id="dummy"> + <div></div> + </div> + <main></main> + </body> +</html> diff --git a/devtools/client/inspector/computed/test/doc_matched_selectors_imported_1.css b/devtools/client/inspector/computed/test/doc_matched_selectors_imported_1.css new file mode 100644 index 0000000000..3eca2e8086 --- /dev/null +++ b/devtools/client/inspector/computed/test/doc_matched_selectors_imported_1.css @@ -0,0 +1,7 @@ +#imported-layers { + background-color: var(--imported-layers_in-importedFirst-first); +} + +#all-important-imported-layers { + background-color: var(--all-important-imported-layers_in-importedFirst-first-important) !important; +} diff --git a/devtools/client/inspector/computed/test/doc_matched_selectors_imported_2.css b/devtools/client/inspector/computed/test/doc_matched_selectors_imported_2.css new file mode 100644 index 0000000000..035d9e0ff5 --- /dev/null +++ b/devtools/client/inspector/computed/test/doc_matched_selectors_imported_2.css @@ -0,0 +1,7 @@ +#imported-layers { + background-color: var(--imported-layers_in-importedSecond); +} + +#all-important-imported-layers { + background-color: var(--all-important-imported-layers_in-importedSecond-important) !important; +} diff --git a/devtools/client/inspector/computed/test/doc_matched_selectors_imported_3.css b/devtools/client/inspector/computed/test/doc_matched_selectors_imported_3.css new file mode 100644 index 0000000000..1139d7e107 --- /dev/null +++ b/devtools/client/inspector/computed/test/doc_matched_selectors_imported_3.css @@ -0,0 +1,8 @@ +#imported-layers { + background-color: var(--imported-layers_in-importedFirst-second); +} + +#all-important-imported-layers { + --all-important-imported-layers_in-importedFirst-second-important: var(--winning-color); + background-color: var(--all-important-imported-layers_in-importedFirst-second-important) !important; +} diff --git a/devtools/client/inspector/computed/test/doc_matched_selectors_imported_4.css b/devtools/client/inspector/computed/test/doc_matched_selectors_imported_4.css new file mode 100644 index 0000000000..abee8206b6 --- /dev/null +++ b/devtools/client/inspector/computed/test/doc_matched_selectors_imported_4.css @@ -0,0 +1,7 @@ +#imported-layers { + background-color: var(--imported-layers_in-anonymous-first); +} + +#all-important-imported-layers { + background-color: var(--all-important-imported-layers_in-anonymous-first-important) !important; +} diff --git a/devtools/client/inspector/computed/test/doc_matched_selectors_imported_5.css b/devtools/client/inspector/computed/test/doc_matched_selectors_imported_5.css new file mode 100644 index 0000000000..26fb567293 --- /dev/null +++ b/devtools/client/inspector/computed/test/doc_matched_selectors_imported_5.css @@ -0,0 +1,10 @@ +@import url(./doc_matched_selectors_imported_6.css) layer(importedSecond); + +#imported-layers { + --imported-layers_in-anonymous-second: var(--winning-color); + background-color: var(--imported-layers_in-anonymous-second); +} + +#all-important-imported-layers { + background-color: var(--all-important-imported-layers_in-anonymous-second-important) !important; +} diff --git a/devtools/client/inspector/computed/test/doc_matched_selectors_imported_6.css b/devtools/client/inspector/computed/test/doc_matched_selectors_imported_6.css new file mode 100644 index 0000000000..63b1cf0dc9 --- /dev/null +++ b/devtools/client/inspector/computed/test/doc_matched_selectors_imported_6.css @@ -0,0 +1,7 @@ +#imported-layers { + background-color: var(--imported-layers_in-nested-importedSecond); +} + +#all-important-imported-layers { + background-color: var(--all-important-imported-layers_in-nested-importedSecond-important) !important; +} 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 0000000000..819e1ea7aa --- /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 0000000000..6145d4bf1b --- /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 0000000000..f62fbda21e --- /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 */ 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 0000000000..0f7486fd91 --- /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 0000000000..0014e55fe9 --- /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 0000000000..0ff6c471bb --- /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 0000000000..dfa1f87e9c --- /dev/null +++ b/devtools/client/inspector/computed/test/head.js @@ -0,0 +1,279 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint no-unused-vars: [2, {"vars": "local"}] */ + +"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) { + const evt = element.ownerDocument.createEvent("Event"); + evt.initEvent("copy", true, true); + element.dispatchEvent(evt); +} + +/** + * Return all the computed items in the computed view + * + * @param {CssComputedView} view + * The instance of the computed view panel + * @returns {Array<Element>} + */ +function getComputedViewProperties(view) { + return Array.from( + view.styleDocument.querySelectorAll( + "#computed-container .computed-property-view" + ) + ); +} + +/** + * 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 (const property of getComputedViewProperties(view)) { + const nameSpan = property.querySelector(".computed-property-name"); + const valueSpan = property.querySelector(".computed-property-value"); + + if (nameSpan.firstChild.textContent === name) { + prop = { nameSpan, 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 (const propertyView of view.propertyViews) { + if (propertyView.propertyInfo.name === name) { + propView = propertyView; + break; + } + } + return propView; +} + +/** + * Get a reference to the matched rules element for a given property name in + * the computed-view. + * A matched rule element is inside the property element (<li>) itself + * and is only shown when the twisty icon is expanded on the property. + * It 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 = async function (view, name) { + let expander; + let matchedRulesEl; + for (const property of view.styleDocument.querySelectorAll( + "#computed-container .computed-property-view" + )) { + const nameSpan = property.querySelector(".computed-property-name"); + if (nameSpan.firstChild.textContent === name) { + expander = property.querySelector(".computed-expandable"); + matchedRulesEl = property.querySelector(".matchedselectors"); + + break; + } + } + + if (!expander.hasAttribute("open")) { + // Need to expand the property + const onExpand = view.inspector.once("computed-view-property-expanded"); + expander.click(); + await onExpand; + + await waitFor(() => expander.hasAttribute("open")); + } + + return matchedRulesEl; +}; + +/** + * 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) { + return getComputedViewProperty(view, name).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"); + const expandos = view.styleDocument.querySelectorAll(".computed-expandable"); + if (!expandos.length || !expandos[index]) { + return Promise.reject(); + } + + const 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) { + const links = view.styleDocument.querySelectorAll( + ".rule-link .computed-link" + ); + return links[index]; +} + +/** + * Trigger the select all action in the computed view. + * + * @param {CssComputedView} view + * The instance of the computed view panel + */ +function selectAllText(view) { + info("Selecting all the text"); + view.contextMenu._onSelectAll(); +} + +/** + * Select all the text, copy it, and check the content in the clipboard. + * + * @param {CssComputedView} view + * The instance of the computed view panel + * @param {String} expectedPattern + * A regular expression used to check the content of the clipboard + */ +async function copyAllAndCheckClipboard(view, expectedPattern) { + selectAllText(view); + const contentDoc = view.styleDocument; + const prop = contentDoc.querySelector( + "#computed-container .computed-property-view" + ); + + try { + info("Trigger a copy event and wait for the clipboard content"); + await waitForClipboardPromise( + () => fireCopyEvent(prop), + () => checkClipboard(expectedPattern) + ); + } catch (e) { + failClipboardCheck(expectedPattern); + } +} + +/** + * Select some text, copy it, and check the content in the clipboard. + * + * @param {CssComputedView} view + * The instance of the computed view panel + * @param {Object} positions + * The start and end positions of the text to be selected. This must be an object + * like this: + * { start: {prop: 1, offset: 0}, end: {prop: 3, offset: 5} } + * @param {String} expectedPattern + * A regular expression used to check the content of the clipboard + */ +async function copySomeTextAndCheckClipboard(view, positions, expectedPattern) { + info("Testing selection copy"); + + const contentDocument = view.styleDocument; + const props = contentDocument.querySelectorAll( + "#computed-container .computed-property-view" + ); + + info("Create the text selection range"); + const range = contentDocument.createRange(); + range.setStart(props[positions.start.prop], positions.start.offset); + range.setEnd(props[positions.end.prop], positions.end.offset); + contentDocument.defaultView.getSelection().addRange(range); + + try { + info("Trigger a copy event and wait for the clipboard content"); + await waitForClipboardPromise( + () => fireCopyEvent(props[0]), + () => checkClipboard(expectedPattern) + ); + } catch (e) { + failClipboardCheck(expectedPattern); + } +} + +function checkClipboard(expectedPattern) { + const actual = SpecialPowers.getClipboardData("text/plain"); + const expectedRegExp = new RegExp(expectedPattern, "g"); + return expectedRegExp.test(actual); +} + +function failClipboardCheck(expectedPattern) { + // Format expected text for comparison + const terminator = Services.appinfo.OS == "WINNT" ? "\r\n" : "\n"; + expectedPattern = expectedPattern.replace(/\[\\r\\n\][+*]/g, terminator); + expectedPattern = expectedPattern.replace(/\\\(/g, "("); + expectedPattern = expectedPattern.replace(/\\\)/g, ")"); + + let actual = SpecialPowers.getClipboardData("text/plain"); + + // 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)); +} |