author | Patrick Brosset <pbrosset@mozilla.com> |
Wed, 28 Mar 2018 17:20:22 +0200 | |
changeset 775128 | 9477160337327fa4059ea88871fc76519379c803 |
parent 775051 | dcd10220d55aea46db212314c46d25a96a7be243 |
push id | 104624 |
push user | bmo:pbrosset@mozilla.com |
push date | Fri, 30 Mar 2018 08:50:04 +0000 |
reviewers | jdescottes |
bugs | 1447736 |
milestone | 61.0a1 |
--- a/devtools/client/inspector/computed/computed.js +++ b/devtools/client/inspector/computed/computed.js @@ -1409,26 +1409,23 @@ function ComputedViewTool(inspector, win this.document = window.document; this.computedView = new CssComputedView(this.inspector, this.document, this.inspector.pageStyle); this.onSelected = this.onSelected.bind(this); this.refresh = this.refresh.bind(this); this.onPanelSelected = this.onPanelSelected.bind(this); - this.onMutations = this.onMutations.bind(this); - this.onResized = this.onResized.bind(this); this.inspector.selection.on("detached-front", this.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.pageStyle.on("stylesheet-updated", this.refresh); - this.inspector.walker.on("mutations", this.onMutations); - this.inspector.walker.on("resize", this.onResized); + this.inspector.styleChangeTracker.on("style-changed", this.refresh); this.computedView.selectElement(null); this.onSelected(); } ComputedViewTool.prototype = { isSidebarActive: function() { @@ -1482,41 +1479,18 @@ ComputedViewTool.prototype = { onPanelSelected: function() { if (this.inspector.selection.nodeFront === this.computedView._viewedElement) { this.refresh(); } else { this.onSelected(); } }, - /** - * When markup mutations occur, if an attribute of the selected node changes, - * we need to refresh the view as that might change the node's styles. - */ - onMutations: function(mutations) { - for (let {type, target} of mutations) { - if (target === this.inspector.selection.nodeFront && - type === "attributes") { - this.refresh(); - break; - } - } - }, - - /** - * When the window gets resized, this may cause media-queries to match, and - * therefore, different styles may apply. - */ - onResized: function() { - this.refresh(); - }, - destroy: function() { - this.inspector.walker.off("mutations", this.onMutations); - this.inspector.walker.off("resize", this.onResized); + this.inspector.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); if (this.inspector.pageStyle) { this.inspector.pageStyle.off("stylesheet-updated", this.refresh); }
--- a/devtools/client/inspector/inspector.js +++ b/devtools/client/inspector/inspector.js @@ -12,16 +12,17 @@ const Services = require("Services"); const promise = require("promise"); const EventEmitter = require("devtools/shared/event-emitter"); const {executeSoon} = require("devtools/shared/DevToolsUtils"); const {PrefObserver} = require("devtools/client/shared/prefs"); const Telemetry = require("devtools/client/shared/telemetry"); const HighlightersOverlay = require("devtools/client/inspector/shared/highlighters-overlay"); const ReflowTracker = require("devtools/client/inspector/shared/reflow-tracker"); const Store = require("devtools/client/inspector/store"); +const InspectorStyleChangeTracker = require("devtools/client/inspector/shared/style-change-tracker"); // Use privileged promise in panel documents to prevent having them to freeze // during toolbox destruction. See bug 1402779. const Promise = require("Promise"); loader.lazyRequireGetter(this, "initCssProperties", "devtools/shared/fronts/css-properties", true); loader.lazyRequireGetter(this, "HTMLBreadcrumbs", "devtools/client/inspector/breadcrumbs", true); loader.lazyRequireGetter(this, "KeyShortcuts", "devtools/client/shared/key-shortcuts"); @@ -99,16 +100,17 @@ function Inspector(toolbox) { // Map [panel id => panel instance] // Stores all the instances of sidebar panels like rule view, computed view, ... this._panels = new Map(); this.highlighters = new HighlightersOverlay(this); this.prefsObserver = new PrefObserver("devtools."); this.reflowTracker = new ReflowTracker(this._target); + this.styleChangeTracker = new InspectorStyleChangeTracker(this); this.store = Store(); this.telemetry = new Telemetry(); // Store the URL of the target page prior to navigation in order to ensure // telemetry counts in the Grid Inspector are not double counted on reload. this.previousURL = this.target.url; this.showSplitSidebarToggle = Services.prefs.getBoolPref( @@ -1309,16 +1311,17 @@ Inspector.prototype = { this.selection.off("new-node-front", this.onNewSelection); this.selection.off("detached-front", this.onDetached); let markupDestroyer = this._destroyMarkup(); this.highlighters.destroy(); this.prefsObserver.destroy(); this.reflowTracker.destroy(); + this.styleChangeTracker.destroy(); this.search.destroy(); this._toolbox = null; this.breadcrumbs = null; this.highlighters = null; this.isSplitRuleViewEnabled = null; this.panelDoc = null; this.panelWin.inspector = null;
--- a/devtools/client/inspector/rules/rules.js +++ b/devtools/client/inspector/rules/rules.js @@ -1743,32 +1743,30 @@ function getShapePoint(node) { function RuleViewTool(inspector, window) { this.inspector = inspector; this.document = window.document; this.view = new CssRuleView(this.inspector, this.document); this.clearUserProperties = this.clearUserProperties.bind(this); this.refresh = this.refresh.bind(this); - this.onMutations = this.onMutations.bind(this); + this.onDetachedFront = this.onDetachedFront.bind(this); this.onPanelSelected = this.onPanelSelected.bind(this); - this.onResized = this.onResized.bind(this); this.onSelected = this.onSelected.bind(this); this.onViewRefreshed = this.onViewRefreshed.bind(this); this.view.on("ruleview-refreshed", this.onViewRefreshed); 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.target.on("navigate", this.clearUserProperties); this.inspector.ruleViewSideBar.on("ruleview-selected", this.onPanelSelected); this.inspector.sidebar.on("ruleview-selected", this.onPanelSelected); this.inspector.pageStyle.on("stylesheet-updated", this.refresh); - this.inspector.walker.on("mutations", this.onMutations); - this.inspector.walker.on("resize", this.onResized); + this.inspector.styleChangeTracker.on("style-changed", this.refresh); this.onSelected(); } RuleViewTool.prototype = { isSidebarActive: function() { if (!this.view) { return false; @@ -1831,41 +1829,18 @@ RuleViewTool.prototype = { this.onSelected(); } }, onViewRefreshed: function() { this.inspector.emit("rule-view-refreshed"); }, - /** - * When markup mutations occur, if an attribute of the selected node changes, - * we need to refresh the view as that might change the node's styles. - */ - onMutations: function(mutations) { - for (let {type, target} of mutations) { - if (target === this.inspector.selection.nodeFront && - type === "attributes") { - this.refresh(); - break; - } - } - }, - - /** - * When the window gets resized, this may cause media-queries to match, and - * therefore, different styles may apply. - */ - onResized: function() { - this.refresh(); - }, - destroy: function() { - this.inspector.walker.off("mutations", this.onMutations); - this.inspector.walker.off("resize", this.onResized); + this.inspector.styleChangeTracker.off("style-changed", this.refresh); this.inspector.selection.off("detached-front", this.onDetachedFront); this.inspector.selection.off("pseudoclass", this.refresh); this.inspector.selection.off("new-node-front", this.onSelected); this.inspector.target.off("navigate", this.clearUserProperties); this.inspector.sidebar.off("ruleview-selected", this.onPanelSelected); if (this.inspector.pageStyle) { this.inspector.pageStyle.off("stylesheet-updated", this.refresh); }
--- a/devtools/client/inspector/rules/test/browser_rules_refresh-on-attribute-change_02.js +++ b/devtools/client/inspector/rules/test/browser_rules_refresh-on-attribute-change_02.js @@ -8,146 +8,146 @@ // rule-view const TEST_URI = ` <div id="testid" class="testclass" style="margin-top: 1px; padding-top: 5px;"> Styled Node </div> `; +// The series of test cases to run. Each case is an object with the following properties: +// - {String} desc The test case description +// - {Function} setup The setup function to execute for this case +// - {Array} properties The properties to expect as a result. Each of them is an object: +// - {String} name The expected property name +// - {String} value The expected property value +// - {Boolean} overridden The expected property overridden state +// - {Boolean} enabled The expected property enabled state +const TEST_DATA = [{ + desc: "Adding a second margin-top value in the element selector", + setup: async function({ view }) { + await addProperty(view, 0, "margin-top", "5px"); + }, + properties: [ + { name: "margin-top", value: "1px", overridden: true, enabled: true }, + { name: "padding-top", value: "5px", overridden: false, enabled: true }, + { name: "margin-top", value: "5px", overridden: false, enabled: true } + ] +}, { + desc: "Setting the element style to its original value", + setup: async function({ inspector, testActor }) { + await changeElementStyle("#testid", "margin-top: 1px; padding-top: 5px", inspector, + testActor); + }, + properties: [ + { name: "margin-top", value: "1px", overridden: false, enabled: true }, + { name: "padding-top", value: "5px", overridden: false, enabled: true }, + { name: "margin-top", value: "5px", overridden: false, enabled: false } + ] +}, { + desc: "Set the margin-top back to 5px, the previous property should be re-enabled", + setup: async function({ inspector, testActor }) { + await changeElementStyle("#testid", "margin-top: 5px; padding-top: 5px;", inspector, + testActor); + }, + properties: [ + { name: "margin-top", value: "1px", overridden: false, enabled: false }, + { name: "padding-top", value: "5px", overridden: false, enabled: true }, + { name: "margin-top", value: "5px", overridden: false, enabled: true } + ] +}, { + desc: "Set the margin property to a value that doesn't exist in the editor, which " + + "should reuse the currently re-enabled property (the second one)", + setup: async function({ inspector, testActor }) { + await changeElementStyle("#testid", "margin-top: 15px; padding-top: 5px;", inspector, + testActor); + }, + properties: [ + { name: "margin-top", value: "1px", overridden: false, enabled: false }, + { name: "padding-top", value: "5px", overridden: false, enabled: true }, + { name: "margin-top", value: "15px", overridden: false, enabled: true } + ] +}, { + desc: "Remove the padding-top attribute. Should disable the padding property but not " + + "remove it", + setup: async function({ inspector, testActor }) { + await changeElementStyle("#testid", "margin-top: 5px;", inspector, testActor); + }, + properties: [ + { name: "margin-top", value: "1px", overridden: false, enabled: false }, + { name: "padding-top", value: "5px", overridden: false, enabled: false }, + { name: "margin-top", value: "5px", overridden: false, enabled: true } + ] +}, { + desc: "Put the padding-top attribute back in, should re-enable the padding property", + setup: async function({ inspector, testActor }) { + await changeElementStyle("#testid", "margin-top: 5px; padding-top: 25px", inspector, + testActor); + }, + properties: [ + { name: "margin-top", value: "1px", overridden: false, enabled: false }, + { name: "padding-top", value: "25px", overridden: false, enabled: true }, + { name: "margin-top", value: "5px", overridden: false, enabled: true } + ] +}, { + desc: "Add an entirely new property", + setup: async function({ inspector, testActor }) { + await changeElementStyle("#testid", + "margin-top: 5px; padding-top: 25px; padding-left: 20px;", + inspector, testActor); + }, + properties: [ + { name: "margin-top", value: "1px", overridden: false, enabled: false }, + { name: "padding-top", value: "25px", overridden: false, enabled: true }, + { name: "margin-top", value: "5px", overridden: false, enabled: true }, + { name: "padding-left", value: "20px", overridden: false, enabled: true } + ] +}, { + desc: "Add an entirely new property again", + setup: async function({ inspector, testActor }) { + await changeElementStyle("#testid", "color: red", inspector, testActor); + }, + properties: [ + { name: "margin-top", value: "1px", overridden: false, enabled: false }, + { name: "padding-top", value: "25px", overridden: false, enabled: false }, + { name: "margin-top", value: "5px", overridden: false, enabled: false }, + { name: "padding-left", value: "20px", overridden: false, enabled: false }, + { name: "color", value: "red", overridden: false, enabled: true } + ] +}]; + add_task(async function() { await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); - let {inspector, view, testActor} = await openRuleView(); + let { inspector, view, testActor } = await openRuleView(); await selectNode("#testid", inspector); - await testPropertyChanges(inspector, view); - await testPropertyChange0(inspector, view, "#testid", testActor); - await testPropertyChange1(inspector, view, "#testid", testActor); - await testPropertyChange2(inspector, view, "#testid", testActor); - await testPropertyChange3(inspector, view, "#testid", testActor); - await testPropertyChange4(inspector, view, "#testid", testActor); - await testPropertyChange5(inspector, view, "#testid", testActor); - await testPropertyChange6(inspector, view, "#testid", testActor); + for (let { desc, setup, properties } of TEST_DATA) { + info(desc); + + await setup({ inspector, view, testActor }); + + let rule = view._elementStyle.rules[0]; + is(rule.editor.element.querySelectorAll(".ruleview-property").length, + properties.length, "The correct number of properties was found"); + + properties.forEach(({ name, value, overridden, enabled }, index) => { + validateTextProp(rule.textProps[index], overridden, enabled, name, value); + }); + } }); -async function testPropertyChanges(inspector, ruleView) { - info("Adding a second margin-top value in the element selector"); - let ruleEditor = ruleView._elementStyle.rules[0].editor; - let onRefreshed = inspector.once("rule-view-refreshed"); - ruleEditor.addProperty("margin-top", "5px", "", true); - await onRefreshed; - - let rule = ruleView._elementStyle.rules[0]; - validateTextProp(rule.textProps[0], false, "margin-top", "1px", - "Original margin property active"); -} - -async function testPropertyChange0(inspector, ruleView, selector, testActor) { - await changeElementStyle(selector, "margin-top: 1px; padding-top: 5px", - inspector, testActor); - - let rule = ruleView._elementStyle.rules[0]; - is(rule.editor.element.querySelectorAll(".ruleview-property").length, 3, - "Correct number of properties"); - validateTextProp(rule.textProps[0], true, "margin-top", "1px", - "First margin property re-enabled"); - validateTextProp(rule.textProps[2], false, "margin-top", "5px", - "Second margin property disabled"); -} - -async function testPropertyChange1(inspector, ruleView, selector, testActor) { - info("Now set it back to 5px, the 5px value should be re-enabled."); - await changeElementStyle(selector, "margin-top: 5px; padding-top: 5px;", - inspector, testActor); - - let rule = ruleView._elementStyle.rules[0]; - is(rule.editor.element.querySelectorAll(".ruleview-property").length, 3, - "Correct number of properties"); - validateTextProp(rule.textProps[0], false, "margin-top", "1px", - "First margin property re-enabled"); - validateTextProp(rule.textProps[2], true, "margin-top", "5px", - "Second margin property disabled"); -} - -async function testPropertyChange2(inspector, ruleView, selector, testActor) { - info("Set the margin property to a value that doesn't exist in the editor."); - info("Should reuse the currently-enabled element (the second one.)"); - await changeElementStyle(selector, "margin-top: 15px; padding-top: 5px;", - inspector, testActor); - - let rule = ruleView._elementStyle.rules[0]; - is(rule.editor.element.querySelectorAll(".ruleview-property").length, 3, - "Correct number of properties"); - validateTextProp(rule.textProps[0], false, "margin-top", "1px", - "First margin property re-enabled"); - validateTextProp(rule.textProps[2], true, "margin-top", "15px", - "Second margin property disabled"); -} - -async function testPropertyChange3(inspector, ruleView, selector, testActor) { - info("Remove the padding-top attribute. Should disable the padding " + - "property but not remove it."); - await changeElementStyle(selector, "margin-top: 5px;", inspector, testActor); - - let rule = ruleView._elementStyle.rules[0]; - is(rule.editor.element.querySelectorAll(".ruleview-property").length, 3, - "Correct number of properties"); - validateTextProp(rule.textProps[1], false, "padding-top", "5px", - "Padding property disabled"); -} - -async function testPropertyChange4(inspector, ruleView, selector, testActor) { - info("Put the padding-top attribute back in, should re-enable the " + - "padding property."); - await changeElementStyle(selector, "margin-top: 5px; padding-top: 25px", - inspector, testActor); - - let rule = ruleView._elementStyle.rules[0]; - is(rule.editor.element.querySelectorAll(".ruleview-property").length, 3, - "Correct number of properties"); - validateTextProp(rule.textProps[1], true, "padding-top", "25px", - "Padding property enabled"); -} - -async function testPropertyChange5(inspector, ruleView, selector, testActor) { - info("Add an entirely new property"); - await changeElementStyle(selector, - "margin-top: 5px; padding-top: 25px; padding-left: 20px;", - inspector, testActor); - - let rule = ruleView._elementStyle.rules[0]; - is(rule.editor.element.querySelectorAll(".ruleview-property").length, 4, - "Added a property"); - validateTextProp(rule.textProps[3], true, "padding-left", "20px", - "Padding property enabled"); -} - -async function testPropertyChange6(inspector, ruleView, selector, testActor) { - info("Add an entirely new property again"); - await changeElementStyle(selector, "background: red " + - "url(\"chrome://branding/content/about-logo.png\") repeat scroll 0% 0%", - inspector, testActor); - - let rule = ruleView._elementStyle.rules[0]; - is(rule.editor.element.querySelectorAll(".ruleview-property").length, 5, - "Added a property"); - validateTextProp(rule.textProps[4], true, "background", - "red url(\"chrome://branding/content/about-logo.png\") repeat scroll 0% 0%", - "shortcut property correctly set"); -} - async function changeElementStyle(selector, style, inspector, testActor) { + info(`Setting ${selector}'s element style to ${style}`); let onRefreshed = inspector.once("rule-view-refreshed"); await testActor.setAttribute(selector, "style", style); await onRefreshed; } -function validateTextProp(prop, enabled, name, value, desc) { - is(prop.enabled, enabled, desc + ": enabled."); - is(prop.name, name, desc + ": name."); - is(prop.value, value, desc + ": value."); +function validateTextProp(prop, overridden, enabled, name, value) { + is(prop.name, name, `${name} property name is correct`); + is(prop.editor.nameSpan.textContent, name, `${name} property name is correct in UI`); - is(prop.editor.enable.hasAttribute("checked"), enabled, - desc + ": enabled checkbox."); - is(prop.editor.nameSpan.textContent, name, desc + ": name span."); - is(prop.editor.valueSpan.textContent, - value, desc + ": value span."); + is(prop.value, value, `${name} property value is correct`); + is(prop.editor.valueSpan.textContent, value, `${name} property value is correct in UI`); + + is(prop.enabled, enabled, `${name} property enabled state correct`); + is(prop.overridden, overridden, `${name} property overridden state correct`); }
--- a/devtools/client/inspector/rules/test/head.js +++ b/devtools/client/inspector/rules/test/head.js @@ -242,42 +242,60 @@ var openCubicBezierAndChangeCoords = asy }; /** * Simulate adding a new property in an existing rule in the rule-view. * * @param {CssRuleView} view * The instance of the rule-view panel * @param {Number} ruleIndex - * The index of the rule to use. Note that if ruleIndex is 0, you might - * want to also listen to markupmutation events in your test since - * that's going to change the style attribute of the selected node. + * The index of the rule to use. * @param {String} name * The name for the new property * @param {String} value * The value for the new property * @param {String} commitValueWith * Which key should be used to commit the new value. VK_RETURN is used by * default, but tests might want to use another key to test cancelling * for exemple. * @param {Boolean} blurNewProperty * After the new value has been added, a new property would have been * focused. This parameter is true by default, and that causes the new * property to be blurred. Set to false if you don't want this. * @return {TextProperty} The instance of the TextProperty that was added */ var addProperty = async function(view, ruleIndex, name, value, - commitValueWith = "VK_RETURN", - blurNewProperty = true) { + commitValueWith = "VK_RETURN", + blurNewProperty = true) { info("Adding new property " + name + ":" + value + " to rule " + ruleIndex); let ruleEditor = getRuleViewRuleEditor(view, ruleIndex); let editor = await focusNewRuleViewProperty(ruleEditor); let numOfProps = ruleEditor.rule.textProps.length; + let onMutations = new Promise(r => { + // If we're adding the property to a non-element style rule, we don't need to wait + // for mutations. + if (ruleIndex !== 0) { + r(); + } + + // Otherwise, adding the property to the element style rule causes 2 mutations to the + // style attribute on the element: first when the name is added with an empty value, + // and then when the value is added. + let receivedMutations = 0; + view.inspector.walker.on("mutations", function onWalkerMutations(mutations) { + receivedMutations += mutations.length; + if (receivedMutations >= 2) { + view.inspector.walker.off("mutations", onWalkerMutations); + r(); + } + }); + }); + info("Adding name " + name); editor.input.value = name; let onNameAdded = view.once("ruleview-changed"); EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow); await onNameAdded; // Focus has moved to the value inplace-editor automatically. editor = inplaceEditor(view.styleDocument.activeElement); @@ -296,16 +314,19 @@ var addProperty = async function(view, r editor.input.value = value; view.debounce.flush(); await onPreview; let onValueAdded = view.once("ruleview-changed"); EventUtils.synthesizeKey(commitValueWith, {}, view.styleWindow); await onValueAdded; + info("Waiting for DOM mutations in case the property was added to the element style"); + await onMutations; + if (blurNewProperty) { view.styleDocument.activeElement.blur(); } return textProp; }; /**
--- a/devtools/client/inspector/shared/moz.build +++ b/devtools/client/inspector/shared/moz.build @@ -4,14 +4,15 @@ # 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( 'dom-node-preview.js', 'highlighters-overlay.js', 'node-types.js', 'reflow-tracker.js', + 'style-change-tracker.js', 'style-inspector-menu.js', 'tooltips-overlay.js', 'utils.js' ) BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
new file mode 100644 --- /dev/null +++ b/devtools/client/inspector/shared/style-change-tracker.js @@ -0,0 +1,98 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const EventEmitter = require("devtools/shared/event-emitter"); + +/** + * The InspectorStyleChangeTracker simply emits an event when it detects any changes in + * the page that may cause the current inspector selection to have different style applied + * to it. + * It currently tracks: + * - markup mutations, because they may cause different CSS rules to apply to the current + * node. + * - window resize, because they may cause media query changes and therefore also + * different CSS rules to apply to the current node. + */ +class InspectorStyleChangeTracker { + constructor(inspector) { + this.walker = inspector.walker; + this.selection = inspector.selection; + + this.onMutations = this.onMutations.bind(this); + this.onResized = this.onResized.bind(this); + + this.walker.on("mutations", this.onMutations); + this.walker.on("resize", this.onResized); + + EventEmitter.decorate(this); + } + + destroy() { + this.walker.off("mutations", this.onMutations); + this.walker.off("resize", this.onResized); + + this.walker = this.selection = null; + } + + /** + * When markup mutations occur, if an attribute of the selected node, one of its + * ancestors or siblings changes, we need to consider this as potentially causing a + * style change for the current node. + */ + onMutations(mutations) { + const canMutationImpactCurrentStyles = ({ type, target }) => { + // Only attributes mutations are interesting here. + if (type !== "attributes") { + return false; + } + + // Is the mutation on the current selected node? + let currentNode = this.selection.nodeFront; + if (target === currentNode) { + return true; + } + + // Is the mutation on one of the current selected node's siblings? + // We can't know the order of nodes on the client-side without calling + // walker.children, so don't attempt to check the previous or next element siblings. + // It's good enough to know that one sibling changed. + let parent = currentNode.parentNode(); + let siblings = parent.treeChildren(); + if (siblings.includes(target)) { + return true; + } + + // Is the mutation on one of the current selected node's parents? + while (parent) { + if (target === parent) { + return true; + } + parent = parent.parentNode(); + } + + return false; + }; + + for (let mutation of mutations) { + if (canMutationImpactCurrentStyles(mutation)) { + this.emit("style-changed"); + break; + } + } + } + + /** + * When the window gets resized, this may cause media-queries to match, and we therefore + * need to consider this as a style change for the current node. + */ + onResized() { + this.emit("style-changed"); + } +} + +module.exports = InspectorStyleChangeTracker;
--- a/devtools/client/inspector/shared/test/browser.ini +++ b/devtools/client/inspector/shared/test/browser.ini @@ -1,13 +1,14 @@ [DEFAULT] tags = devtools subsuite = devtools support-files = doc_author-sheet.html + doc_content_style_changes.html doc_content_stylesheet.html doc_content_stylesheet.xul doc_content_stylesheet_imported.css doc_content_stylesheet_imported2.css doc_content_stylesheet_linked.css doc_content_stylesheet_script.css doc_content_stylesheet_xul.css doc_frame_script.js @@ -25,16 +26,17 @@ subsuite = clipboard skip-if = (os == 'linux' && bits == 32 && debug) # bug 1328915, disable linux32 debug devtools for timeouts [browser_styleinspector_context-menu-copy-urls.js] subsuite = clipboard skip-if = (os == 'linux' && bits == 32 && debug) # bug 1328915, disable linux32 debug devtools for timeouts [browser_styleinspector_csslogic-content-stylesheets.js] skip-if = e10s && debug # Bug 1250058 (docshell leak when opening 2 toolboxes) [browser_styleinspector_output-parser.js] [browser_styleinspector_refresh_when_active.js] +[browser_styleinspector_refresh_when_style_changes.js] [browser_styleinspector_tooltip-background-image.js] [browser_styleinspector_tooltip-closes-on-new-selection.js] skip-if = e10s # Bug 1111546 (e10s) [browser_styleinspector_tooltip-longhand-fontfamily.js] [browser_styleinspector_tooltip-multiple-background-images.js] [browser_styleinspector_tooltip-shorthand-fontfamily.js] [browser_styleinspector_tooltip-size.js] [browser_styleinspector_transform-highlighter-01.js]
new file mode 100644 --- /dev/null +++ b/devtools/client/inspector/shared/test/browser_styleinspector_refresh_when_style_changes.js @@ -0,0 +1,80 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the rule and computed views refresh when style changes that impact the +// current selection occur. +// This test does not need to worry about the correctness of the styles and rules +// displayed in these views (other tests do this) but only cares that they do catch the +// change. + +const TEST_URI = TEST_URL_ROOT + "doc_content_style_changes.html"; + +const TEST_DATA = [{ + target: "#test", + className: "green-class", + force: true +}, { + target: "#test", + className: "green-class", + force: false +}, { + target: "#parent", + className: "purple-class", + force: true +}, { + target: "#parent", + className: "purple-class", + force: false +}, { + target: "#sibling", + className: "blue-class", + force: true +}, { + target: "#sibling", + className: "blue-class", + force: false +}]; + +add_task(async function() { + let tab = await addTab(TEST_URI); + + let { inspector } = await openRuleView(); + await selectNode("#test", inspector); + + info("Run the test on the rule-view"); + await runViewTest(inspector, tab, "rule"); + + info("Switch to the computed view"); + let onComputedViewReady = inspector.once("computed-view-refreshed"); + selectComputedView(inspector); + await onComputedViewReady; + + info("Run the test again on the computed view"); + await runViewTest(inspector, tab, "computed"); +}); + +async function runViewTest(inspector, tab, viewName) { + for (let { target, className, force } of TEST_DATA) { + info((force ? "Adding" : "Removing") + + ` class ${className} on ${target} and expecting a ${viewName}-view refresh`); + + await toggleClassAndWaitForViewChange( + { target, className, force }, inspector, tab, `${viewName}-view-refreshed`); + } +} + +async function toggleClassAndWaitForViewChange(whatToMutate, inspector, tab, eventName) { + let onRefreshed = inspector.once(eventName); + + await ContentTask.spawn(tab.linkedBrowser, whatToMutate, + function({ target, className, force }) { + content.document.querySelector(target).classList.toggle(className, force); + } + ); + + await onRefreshed; + ok(true, "The view was refreshed after the class was changed"); +}
new file mode 100644 --- /dev/null +++ b/devtools/client/inspector/shared/test/doc_content_style_changes.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<style> +#test { + color: red; +} +/* Adding/removing the green-class on #test should refresh the rule-view when #test is + selected */ +#test.green-class { + color: green; +} +/* Adding/removing the purple-class on #parent should refresh the rule-view when #test is + selected */ +#parent.purple-class #test { + color: purple; +} +/* Adding/removing the blue-class on #sibling should refresh the rule-view when #test is + selected*/ +#sibling.blue-class + #test { + color: blue; +} +</style> +<div id="parent"> + <div> + <div id="sibling"></div> + <div id="test">test</div> + </div> +</div>