summaryrefslogtreecommitdiffstats
path: root/devtools/client/inspector/computed
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /devtools/client/inspector/computed
parentInitial commit. (diff)
downloadfirefox-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')
-rw-r--r--devtools/client/inspector/computed/computed.js1713
-rw-r--r--devtools/client/inspector/computed/moz.build11
-rw-r--r--devtools/client/inspector/computed/test/browser.toml86
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_browser-styles.js59
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_custom_properties.js106
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_cycle_color.js90
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_default_tab.js39
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_getNodeInfo.js176
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_keybindings_01.js92
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_keybindings_02.js69
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_matched-selectors-order.js889
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_matched-selectors-toggle.js125
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_matched-selectors_01.js48
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_matched-selectors_02.js40
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_media-queries.js42
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_no-results-placeholder.js69
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_original-source-link.js71
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_pseudo-element_01.js38
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_refresh-on-ruleview-change.js93
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_refresh-on-style-change_01.js32
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_search-filter.js67
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_search-filter_clear.js72
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_search-filter_context-menu.js101
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_search-filter_escape-keypress.js76
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_search-filter_noproperties.js68
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_select-and-copy-styles-01.js67
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_select-and-copy-styles-02.js35
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_shadow_host.js74
-rw-r--r--devtools/client/inspector/computed/test/browser_computed_style-editor-link.js210
-rw-r--r--devtools/client/inspector/computed/test/doc_matched_selectors.html54
-rw-r--r--devtools/client/inspector/computed/test/doc_matched_selectors_imported_1.css7
-rw-r--r--devtools/client/inspector/computed/test/doc_matched_selectors_imported_2.css7
-rw-r--r--devtools/client/inspector/computed/test/doc_matched_selectors_imported_3.css8
-rw-r--r--devtools/client/inspector/computed/test/doc_matched_selectors_imported_4.css7
-rw-r--r--devtools/client/inspector/computed/test/doc_matched_selectors_imported_5.css10
-rw-r--r--devtools/client/inspector/computed/test/doc_matched_selectors_imported_6.css7
-rw-r--r--devtools/client/inspector/computed/test/doc_media_queries.html21
-rw-r--r--devtools/client/inspector/computed/test/doc_pseudoelement.html131
-rw-r--r--devtools/client/inspector/computed/test/doc_sourcemaps.css7
-rw-r--r--devtools/client/inspector/computed/test/doc_sourcemaps.css.map7
-rw-r--r--devtools/client/inspector/computed/test/doc_sourcemaps.html11
-rw-r--r--devtools/client/inspector/computed/test/doc_sourcemaps.scss10
-rw-r--r--devtools/client/inspector/computed/test/head.js279
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));
+}