Bug 1428246 - The attributeChangedCallback is fired twice for the *first* style attribute change, r=peterv
authorOlli Pettay <Olli.Pettay@helsinki.fi>
Tue, 26 Jun 2018 12:54:00 +0300
changeset 478780 e8c0ffefb34fdd3e24161152af148553aa58e624
parent 478779 43040128202efc47d4249e623e6d3ffd1a5d9588
child 478781 e3d37fde9b63f34a9af7d5529989ffaac3c28616
push id9719
push userffxbld-merge
push dateFri, 24 Aug 2018 17:49:46 +0000
treeherdermozilla-beta@719ec98fba77 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspeterv
bugs1428246, 1466963, 1468665
milestone63.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1428246 - The attributeChangedCallback is fired twice for the *first* style attribute change, r=peterv The idea with this patch is that style code will first call InlineStyleDeclarationWillChange before style declaration has changed, and SetInlineStyleDeclaration once it has changed. In order to be able to report old attribute value, InlineStyleDeclarationWillChange reads the value and also calls AttributeWillChange (so that DOMMutationObserser can grab the old value). Later SetInlineStyleDeclaration passes the old value to SetAttrAndNotify so that mutation events and attributeChanged callbacks are handled correctly. Because of performance, declaration can't be cloned for reading the old value. And that is why the recently-added callback is used to detect when declaration is about to change (bug 1466963 and followup bug 1468665). To keep the expected existing behavior, even if declaration isn't changed, but just a new declaration was created (since there wasn't any), we need to still run all these willchange/set calls. That is when the code has 'if (created)' checks. Since there are several declaration implementation and only nsDOMCSSAttributeDeclaration needs the about-to-change callback, GetPropertyChangeClosure is the one to initialize the callback closure, and the struct which is then passes as data to the closure. Apparently we lost mutation event testing on style attribute when the pref was added, so test_style_attr_listener.html is modified to test both pref values.
dom/base/Element.cpp
dom/base/Element.h
dom/base/nsIDocument.h
dom/base/nsStyledElement.cpp
dom/base/nsStyledElement.h
dom/tests/mochitest/webcomponents/test_custom_element_lifecycle.html
layout/style/CSSKeyframeRule.cpp
layout/style/CSSPageRule.cpp
layout/style/CSSPageRule.h
layout/style/CSSStyleRule.cpp
layout/style/CSSStyleRule.h
layout/style/nsComputedDOMStyle.cpp
layout/style/nsComputedDOMStyle.h
layout/style/nsDOMCSSAttrDeclaration.cpp
layout/style/nsDOMCSSAttrDeclaration.h
layout/style/nsDOMCSSDeclaration.cpp
layout/style/nsDOMCSSDeclaration.h
layout/style/test/test_style_attr_listener.html
testing/web-platform/meta/css/cssom/css-style-attr-decl-block.html.ini
testing/web-platform/meta/css/cssom/cssstyledeclaration-mutationrecord-003.html.ini
testing/web-platform/meta/css/cssom/cssstyledeclaration-mutationrecord-004.html.ini
testing/web-platform/meta/custom-elements/attribute-changed-callback.html.ini
testing/web-platform/meta/custom-elements/reactions/CSSStyleDeclaration.html.ini
--- a/dom/base/Element.cpp
+++ b/dom/base/Element.cpp
@@ -2117,20 +2117,25 @@ Element::GetInlineStyleDeclaration() con
 }
 
 const nsMappedAttributes*
 Element::GetMappedAttributes() const
 {
   return mAttrsAndChildren.GetMapped();
 }
 
+void
+Element::InlineStyleDeclarationWillChange(MutationClosureData& aData)
+{
+  MOZ_ASSERT_UNREACHABLE("Element::InlineStyleDeclarationWillChange");
+}
+
 nsresult
-Element::SetInlineStyleDeclaration(DeclarationBlock* aDeclaration,
-                                   const nsAString* aSerialized,
-                                   bool aNotify)
+Element::SetInlineStyleDeclaration(DeclarationBlock& aDeclaration,
+                                   MutationClosureData& aData)
 {
   MOZ_ASSERT_UNREACHABLE("Element::SetInlineStyleDeclaration");
   return NS_ERROR_NOT_IMPLEMENTED;
 }
 
 NS_IMETHODIMP_(bool)
 Element::IsAttributeMapped(const nsAtom* aAttribute) const
 {
--- a/dom/base/Element.h
+++ b/dom/base/Element.h
@@ -58,16 +58,17 @@ class nsGlobalWindowInner;
 class nsGlobalWindowOuter;
 class nsDOMCSSAttributeDeclaration;
 class nsISMILAttr;
 class nsDocument;
 class nsDOMStringMap;
 
 namespace mozilla {
 class DeclarationBlock;
+struct MutationClosureData;
 class TextEditor;
 namespace css {
   struct URLValue;
 } // namespace css
 namespace dom {
   struct AnimationFilter;
   struct ScrollIntoViewOptions;
   struct ScrollToOptions;
@@ -319,22 +320,27 @@ public:
    */
   const nsMappedAttributes* GetMappedAttributes() const;
 
   void ClearMappedServoStyle() {
     mAttrsAndChildren.ClearMappedServoStyle();
   }
 
   /**
-   * Set the inline style declaration for this element. This will send
-   * an appropriate AttributeChanged notification if aNotify is true.
+   * InlineStyleDeclarationWillChange is called before SetInlineStyleDeclaration
+   * so that the element implementation can access the old style attribute
+   * value.
    */
-  virtual nsresult SetInlineStyleDeclaration(DeclarationBlock* aDeclaration,
-                                             const nsAString* aSerialized,
-                                             bool aNotify);
+  virtual void InlineStyleDeclarationWillChange(MutationClosureData& aData);
+
+  /**
+   * Set the inline style declaration for this element.
+   */
+  virtual nsresult SetInlineStyleDeclaration(DeclarationBlock& aDeclaration,
+                                             MutationClosureData& aData);
 
   /**
    * Get the SMIL override style declaration for this element. If the
    * rule hasn't been created, this method simply returns null.
    */
   DeclarationBlock* GetSMILOverrideStyleDeclaration();
 
   /**
--- a/dom/base/nsIDocument.h
+++ b/dom/base/nsIDocument.h
@@ -1834,16 +1834,17 @@ public:
   bool RemoveObserver(nsIDocumentObserver* aObserver);
 
   // Observation hooks used to propagate notifications to document observers.
   // BeginUpdate must be called before any batch of modifications of the
   // content model or of style data, EndUpdate must be called afterward.
   // To make this easy and painless, use the mozAutoDocUpdate helper class.
   void BeginUpdate();
   virtual void EndUpdate() = 0;
+  uint32_t UpdateNestingLevel() { return mUpdateNestLevel; }
 
   virtual void BeginLoad() = 0;
   virtual void EndLoad() = 0;
 
   enum ReadyState { READYSTATE_UNINITIALIZED = 0, READYSTATE_LOADING = 1, READYSTATE_INTERACTIVE = 3, READYSTATE_COMPLETE = 4};
   void SetReadyStateInternal(ReadyState rs);
   ReadyState GetReadyStateEnum()
   {
--- a/dom/base/nsStyledElement.cpp
+++ b/dom/base/nsStyledElement.cpp
@@ -62,63 +62,85 @@ nsStyledElement::BeforeSetAttr(int32_t a
       }
     }
   }
 
   return nsStyledElementBase::BeforeSetAttr(aNamespaceID, aName, aValue,
                                             aNotify);
 }
 
-nsresult
-nsStyledElement::SetInlineStyleDeclaration(DeclarationBlock* aDeclaration,
-                                           const nsAString* aSerialized,
-                                           bool aNotify)
+void
+nsStyledElement::InlineStyleDeclarationWillChange(MutationClosureData& aData)
 {
-  SetMayHaveStyle();
+  MOZ_ASSERT(OwnerDoc()->UpdateNestingLevel() > 0,
+             "Should be inside document update!");
   bool modification = false;
-  nsAttrValue oldValue;
-  bool oldValueSet = false;
+  if (MayHaveStyle()) {
+    bool needsOldValue =
+      !StaticPrefs::dom_mutation_events_cssom_disabled() &&
+      nsContentUtils::HasMutationListeners(this,
+                                           NS_EVENT_BITS_MUTATION_ATTRMODIFIED,
+                                           this);
+
+    if (!needsOldValue) {
+      CustomElementDefinition* definition = GetCustomElementDefinition();
+      if (definition && definition->IsInObservedAttributeList(nsGkAtoms::style)) {
+        needsOldValue = true;
+      }
+    }
 
-  bool hasListeners = aNotify &&
+    if (needsOldValue) {
+      nsAutoString oldValueStr;
+      modification = GetAttr(kNameSpaceID_None, nsGkAtoms::style,
+                             oldValueStr);
+      if (modification) {
+        aData.mOldValue.emplace();
+        aData.mOldValue->SetTo(oldValueStr);
+      }
+    } else {
+      modification = HasAttr(kNameSpaceID_None, nsGkAtoms::style);
+    }
+  }
+
+  aData.mModType = modification ?
+    static_cast<uint8_t>(MutationEventBinding::MODIFICATION) :
+    static_cast<uint8_t>(MutationEventBinding::ADDITION);
+  nsNodeUtils::AttributeWillChange(this, kNameSpaceID_None,
+                                   nsGkAtoms::style,
+                                   aData.mModType, nullptr);
+
+  //XXXsmaug In order to make attribute handling more consistent, consider to
+  //         call BeforeSetAttr and pass kCallAfterSetAttr to
+  //         SetAttrAndNotify in SetInlineStyleDeclaration.
+  //         Handling of mozAutoDocUpdate may require changes in that case.
+}
+
+nsresult
+nsStyledElement::SetInlineStyleDeclaration(DeclarationBlock& aDeclaration,
+                                           MutationClosureData& aData)
+{
+  MOZ_ASSERT(OwnerDoc()->UpdateNestingLevel(),
+             "Should be inside document update!");
+
+  bool hasListeners =
     !StaticPrefs::dom_mutation_events_cssom_disabled() &&
     nsContentUtils::HasMutationListeners(this,
                                          NS_EVENT_BITS_MUTATION_ATTRMODIFIED,
                                          this);
 
-  // There's no point in comparing the stylerule pointers since we're always
-  // getting a new stylerule here. And we can't compare the stringvalues of
-  // the old and the new rules since both will point to the same declaration
-  // and thus will be the same.
-  if (hasListeners || GetCustomElementData()) {
-    // save the old attribute so we can set up the mutation event properly
-    nsAutoString oldValueStr;
-    modification = GetAttr(kNameSpaceID_None, nsGkAtoms::style,
-                           oldValueStr);
-    if (modification) {
-      oldValue.SetTo(oldValueStr);
-      oldValueSet = true;
-    }
-  }
-  else if (aNotify && IsInUncomposedDoc()) {
-    modification = !!mAttrsAndChildren.GetAttr(nsGkAtoms::style);
-  }
-
-  nsAttrValue attrValue(do_AddRef(aDeclaration), aSerialized);
-
-  // XXXbz do we ever end up with ADDITION here?  I doubt it.
-  uint8_t modType = modification ?
-    static_cast<uint8_t>(MutationEventBinding::MODIFICATION) :
-    static_cast<uint8_t>(MutationEventBinding::ADDITION);
+  nsAttrValue attrValue(do_AddRef(&aDeclaration), nullptr);
+  SetMayHaveStyle();
 
   nsIDocument* document = GetComposedDoc();
-  mozAutoDocUpdate updateBatch(document, aNotify);
+  mozAutoDocUpdate updateBatch(document, true);
   return SetAttrAndNotify(kNameSpaceID_None, nsGkAtoms::style, nullptr,
-                          oldValueSet ? &oldValue : nullptr, attrValue,
-                          nullptr, modType,
-                          hasListeners, aNotify, kDontCallAfterSetAttr,
+                          aData.mOldValue.isSome() ?
+                            aData.mOldValue.ptr() : nullptr,
+                          attrValue, nullptr, aData.mModType,
+                          hasListeners, true, kDontCallAfterSetAttr,
                           document, updateBatch);
 }
 
 // ---------------------------------------------------------------
 // Others and helpers
 
 nsICSSDeclaration*
 nsStyledElement::Style()
--- a/dom/base/nsStyledElement.h
+++ b/dom/base/nsStyledElement.h
@@ -39,19 +39,21 @@ protected:
 
 public:
   // We don't want to implement AddRef/Release because that would add an extra
   // function call for those on pretty much all elements.  But we do need QI, so
   // we can QI to nsStyledElement.
   NS_IMETHOD QueryInterface(REFNSIID aIID, void** aInstancePtr) override;
 
   // Element interface methods
-  virtual nsresult SetInlineStyleDeclaration(mozilla::DeclarationBlock* aDeclaration,
-                                             const nsAString* aSerialized,
-                                             bool aNotify) override;
+  virtual void
+  InlineStyleDeclarationWillChange(mozilla::MutationClosureData& aData) override;
+  virtual nsresult
+  SetInlineStyleDeclaration(mozilla::DeclarationBlock& aDeclaration,
+                            mozilla::MutationClosureData& aData) override;
 
   nsICSSDeclaration* Style();
 
   NS_DECLARE_STATIC_IID_ACCESSOR(NS_STYLED_ELEMENT_IID)
 
 protected:
 
   nsICSSDeclaration* GetExistingStyle();
--- a/dom/tests/mochitest/webcomponents/test_custom_element_lifecycle.html
+++ b/dom/tests/mochitest/webcomponents/test_custom_element_lifecycle.html
@@ -290,20 +290,17 @@ function testAttributeChangedExtended() 
 
   var elem = document.createElement("button", {is: "x-extended-attribute-change"});
   elem.setAttribute("foo", "bar");
 }
 
 function testStyleAttributeChange() {
   var expectedCallbackArguments = [
     // [name, oldValue, newValue]
-    // This is an additional attributeChangedCallback from *first* style
-    // attribute change, see https://bugzilla.mozilla.org/show_bug.cgi?id=1428246.
-    ["style", null, ""],
-    ["style", "", "font-size: 10px;"],
+    ["style", null, "font-size: 10px;"],
     ["style", "font-size: 10px;", "font-size: 20px;"],
     ["style", "font-size: 20px;", "font-size: 30px;"],
   ];
 
   customElements.define("x-style-attribute-change", class extends HTMLElement {
     attributeChangedCallback(name, oldValue, newValue) {
       if (expectedCallbackArguments.length === 0) {
         ok(false, "Got unexpected attributeChangedCallback?");
--- a/layout/style/CSSKeyframeRule.cpp
+++ b/layout/style/CSSKeyframeRule.cpp
@@ -33,21 +33,23 @@ public:
 
   css::Rule* GetParentRule() final { return mRule; }
 
   void DropReference() {
     mRule = nullptr;
     mDecls->SetOwningRule(nullptr);
   }
 
-  DeclarationBlock* GetCSSDeclaration(Operation aOperation) final
+  DeclarationBlock* GetOrCreateCSSDeclaration(
+    Operation aOperation, DeclarationBlock** aCreated) final
   {
     return mDecls;
   }
-  nsresult SetCSSDeclaration(DeclarationBlock* aDecls) final
+  nsresult SetCSSDeclaration(DeclarationBlock* aDecls,
+                             MutationClosureData* aClosureData) final
   {
     if (!mRule) {
       return NS_OK;
     }
     mRule->UpdateRule([this, aDecls]() {
       if (mDecls != aDecls) {
         mDecls->SetOwningRule(nullptr);
         mDecls = aDecls;
--- a/layout/style/CSSPageRule.cpp
+++ b/layout/style/CSSPageRule.cpp
@@ -51,23 +51,25 @@ CSSPageRuleDeclaration::GetParentRule()
 
 nsINode*
 CSSPageRuleDeclaration::GetParentObject()
 {
   return Rule()->GetParentObject();
 }
 
 DeclarationBlock*
-CSSPageRuleDeclaration::GetCSSDeclaration(Operation aOperation)
+CSSPageRuleDeclaration::GetOrCreateCSSDeclaration(Operation aOperation,
+                                                  DeclarationBlock** aCreated)
 {
   return mDecls;
 }
 
 nsresult
-CSSPageRuleDeclaration::SetCSSDeclaration(DeclarationBlock* aDecl)
+CSSPageRuleDeclaration::SetCSSDeclaration(DeclarationBlock* aDecl,
+                                          MutationClosureData* aClosureData)
 {
   MOZ_ASSERT(aDecl, "must be non-null");
   CSSPageRule* rule = Rule();
 
   if (aDecl != mDecls) {
     mDecls->SetOwningRule(nullptr);
     RefPtr<DeclarationBlock> decls = aDecl;
     Servo_PageRule_SetStyle(rule->Raw(), decls->Raw());
--- a/layout/style/CSSPageRule.h
+++ b/layout/style/CSSPageRule.h
@@ -24,18 +24,20 @@ class CSSPageRuleDeclaration final : pub
 {
 public:
   NS_DECL_ISUPPORTS_INHERITED
 
   css::Rule* GetParentRule() final;
   nsINode* GetParentObject() final;
 
 protected:
-  DeclarationBlock* GetCSSDeclaration(Operation aOperation) final;
-  nsresult SetCSSDeclaration(DeclarationBlock* aDecl) final;
+  mozilla::DeclarationBlock* GetOrCreateCSSDeclaration(
+    Operation aOperation, mozilla::DeclarationBlock** aCreated) final;
+  nsresult SetCSSDeclaration(DeclarationBlock* aDecl,
+                             MutationClosureData* aClosureData) final;
   nsIDocument* DocToUpdate() final;
   nsDOMCSSDeclaration::ParsingEnvironment
   GetParsingEnvironment(nsIPrincipal* aSubjectPrincipal) const final;
 
 private:
   // For accessing the constructor.
   friend class CSSPageRule;
 
--- a/layout/style/CSSStyleRule.cpp
+++ b/layout/style/CSSStyleRule.cpp
@@ -55,23 +55,25 @@ CSSStyleRuleDeclaration::GetParentRule()
 
 nsINode*
 CSSStyleRuleDeclaration::GetParentObject()
 {
   return Rule()->GetParentObject();
 }
 
 DeclarationBlock*
-CSSStyleRuleDeclaration::GetCSSDeclaration(Operation aOperation)
+CSSStyleRuleDeclaration::GetOrCreateCSSDeclaration(Operation aOperation,
+                                                   DeclarationBlock** aCreated)
 {
   return mDecls;
 }
 
 nsresult
-CSSStyleRuleDeclaration::SetCSSDeclaration(DeclarationBlock* aDecl)
+CSSStyleRuleDeclaration::SetCSSDeclaration(DeclarationBlock* aDecl,
+                                           MutationClosureData* aClosureData)
 {
   CSSStyleRule* rule = Rule();
   if (RefPtr<StyleSheet> sheet = rule->GetStyleSheet()) {
     if (aDecl != mDecls) {
       mDecls->SetOwningRule(nullptr);
       RefPtr<DeclarationBlock> decls = aDecl;
       Servo_StyleRule_SetStyle(rule->Raw(), decls->Raw());
       mDecls = decls.forget();
--- a/layout/style/CSSStyleRule.h
+++ b/layout/style/CSSStyleRule.h
@@ -25,18 +25,20 @@ class CSSStyleRuleDeclaration final : pu
 {
 public:
   NS_DECL_ISUPPORTS_INHERITED
 
   css::Rule* GetParentRule() final;
   nsINode* GetParentObject() final;
 
 protected:
-  DeclarationBlock* GetCSSDeclaration(Operation aOperation) final;
-  nsresult SetCSSDeclaration(DeclarationBlock* aDecl) final;
+  mozilla::DeclarationBlock* GetOrCreateCSSDeclaration(
+    Operation aOperation, mozilla::DeclarationBlock** aCreated) final;
+  nsresult SetCSSDeclaration(DeclarationBlock* aDecl,
+                             MutationClosureData* aClosureData) final;
   nsIDocument* DocToUpdate() final;
   ParsingEnvironment
   GetParsingEnvironment(nsIPrincipal* aSubjectPrincipal) const final;
 
 private:
   // For accessing the constructor.
   friend class CSSStyleRule;
 
--- a/layout/style/nsComputedDOMStyle.cpp
+++ b/layout/style/nsComputedDOMStyle.cpp
@@ -772,23 +772,25 @@ nsComputedDOMStyle::GetCSSImageURLs(cons
   CollectImageURLsForProperty(prop, *mComputedStyle, aImageURLs);
   ClearCurrentStyleSources();
 }
 
 // nsDOMCSSDeclaration abstract methods which should never be called
 // on a nsComputedDOMStyle object, but must be defined to avoid
 // compile errors.
 DeclarationBlock*
-nsComputedDOMStyle::GetCSSDeclaration(Operation)
+nsComputedDOMStyle::GetOrCreateCSSDeclaration(Operation aOperation,
+                                              DeclarationBlock** aCreated)
 {
   MOZ_CRASH("called nsComputedDOMStyle::GetCSSDeclaration");
 }
 
 nsresult
-nsComputedDOMStyle::SetCSSDeclaration(DeclarationBlock*)
+nsComputedDOMStyle::SetCSSDeclaration(DeclarationBlock*,
+                                      MutationClosureData*)
 {
   MOZ_CRASH("called nsComputedDOMStyle::SetCSSDeclaration");
 }
 
 nsIDocument*
 nsComputedDOMStyle::DocToUpdate()
 {
   MOZ_CRASH("called nsComputedDOMStyle::DocToUpdate");
--- a/layout/style/nsComputedDOMStyle.h
+++ b/layout/style/nsComputedDOMStyle.h
@@ -116,18 +116,20 @@ public:
 
   void GetCSSImageURLs(const nsAString& aPropertyName,
                        nsTArray<nsString>& aImageURLs,
                        mozilla::ErrorResult& aRv) final;
 
   // nsDOMCSSDeclaration abstract methods which should never be called
   // on a nsComputedDOMStyle object, but must be defined to avoid
   // compile errors.
-  virtual mozilla::DeclarationBlock* GetCSSDeclaration(Operation) override;
-  virtual nsresult SetCSSDeclaration(mozilla::DeclarationBlock*) override;
+  mozilla::DeclarationBlock* GetOrCreateCSSDeclaration(
+    Operation aOperation, mozilla::DeclarationBlock** aCreated) final;
+  virtual nsresult SetCSSDeclaration(mozilla::DeclarationBlock*,
+                                     mozilla::MutationClosureData*) override;
   virtual nsIDocument* DocToUpdate() override;
 
   nsDOMCSSDeclaration::ParsingEnvironment
   GetParsingEnvironment(nsIPrincipal* aSubjectPrincipal) const final;
 
   static already_AddRefed<nsROCSSPrimitiveValue>
     MatrixToCSSValue(const mozilla::gfx::Matrix4x4& aMatrix);
 
--- a/layout/style/nsDOMCSSAttrDeclaration.cpp
+++ b/layout/style/nsDOMCSSAttrDeclaration.cpp
@@ -64,100 +64,82 @@ NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_E
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsDOMCSSAttributeDeclaration)
   NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
 NS_IMPL_QUERY_TAIL_INHERITING(nsDOMCSSDeclaration)
 
 NS_IMPL_CYCLE_COLLECTING_ADDREF(nsDOMCSSAttributeDeclaration)
 NS_IMPL_CYCLE_COLLECTING_RELEASE(nsDOMCSSAttributeDeclaration)
 
 nsresult
-nsDOMCSSAttributeDeclaration::SetCSSDeclaration(DeclarationBlock* aDecl)
+nsDOMCSSAttributeDeclaration::SetCSSDeclaration(DeclarationBlock* aDecl,
+                                                MutationClosureData* aClosureData)
 {
   NS_ASSERTION(mElement, "Must have Element to set the declaration!");
+
+  // Whenever changing element.style values, aClosureData must be non-null.
+  // SMIL doesn't update Element's attribute values, so closure data isn't
+  // needed.
+  MOZ_ASSERT_IF(!mIsSMILOverride, aClosureData);
+
+  // If the closure hasn't been called because the declaration wasn't changed,
+  // we need to explicitly call it now to get InlineStyleDeclarationWillChange
+  // notification before SetInlineStyleDeclaration.
+  if (aClosureData && aClosureData->mClosure) {
+    aClosureData->mClosure(aClosureData);
+  }
+
   aDecl->SetDirty();
   return mIsSMILOverride
     ? mElement->SetSMILOverrideStyleDeclaration(aDecl, true)
-    : mElement->SetInlineStyleDeclaration(aDecl, nullptr, true);
+    : mElement->SetInlineStyleDeclaration(*aDecl, *aClosureData);
 }
 
 nsIDocument*
 nsDOMCSSAttributeDeclaration::DocToUpdate()
 {
   // We need OwnerDoc() rather than GetUncomposedDoc() because it might
   // be the BeginUpdate call that inserts mElement into the document.
   return mElement->OwnerDoc();
 }
 
 DeclarationBlock*
-nsDOMCSSAttributeDeclaration::GetCSSDeclaration(Operation aOperation)
+nsDOMCSSAttributeDeclaration::GetOrCreateCSSDeclaration(Operation aOperation,
+                                                        DeclarationBlock** aCreated)
 {
+  MOZ_ASSERT(aOperation != eOperation_Modify || aCreated);
+
   if (!mElement)
     return nullptr;
 
   DeclarationBlock* declaration;
   if (mIsSMILOverride) {
     declaration = mElement->GetSMILOverrideStyleDeclaration();
   } else {
     declaration = mElement->GetInlineStyleDeclaration();
   }
 
-  // Notify observers that our style="" attribute is going to change
-  // unless:
-  //   * this is a declaration that holds SMIL animation values (which
-  //     aren't reflected in the DOM style="" attribute), or
-  //   * we're getting the declaration for reading, or
-  //   * we're getting it for property removal but we don't currently have
-  //     a declaration.
-
-  // XXXbz this is a bit of a hack, especially doing it before the
-  // BeginUpdate(), but this is a good chokepoint where we know we
-  // plan to modify the CSSDeclaration, so need to notify
-  // AttributeWillChange if this is inline style.
-  if (!mIsSMILOverride &&
-      ((aOperation == eOperation_Modify) ||
-       (aOperation == eOperation_RemoveProperty && declaration))) {
-    nsNodeUtils::AttributeWillChange(mElement, kNameSpaceID_None,
-                                     nsGkAtoms::style,
-                                     dom::MutationEventBinding::MODIFICATION,
-                                     nullptr);
-  }
-
   if (declaration) {
-    if (aOperation != eOperation_Read &&
-        nsContentUtils::HasMutationListeners(
-          mElement, NS_EVENT_BITS_MUTATION_ATTRMODIFIED, mElement)) {
-      // If there is any mutation listener on the element, we need to
-      // ensure that any change would create a new declaration so that
-      // nsStyledElement::SetInlineStyleDeclaration can generate the
-      // correct old value.
-      declaration->SetImmutable();
-    }
     return declaration;
   }
 
   if (aOperation != eOperation_Modify) {
     return nullptr;
   }
 
   // cannot fail
   RefPtr<DeclarationBlock> decl = new DeclarationBlock();
-
-  // this *can* fail (inside SetAttrAndNotify, at least).
-  nsresult rv;
-  if (mIsSMILOverride) {
-    rv = mElement->SetSMILOverrideStyleDeclaration(decl, false);
-  } else {
-    rv = mElement->SetInlineStyleDeclaration(decl, nullptr, false);
-  }
-
-  if (NS_FAILED(rv)) {
-    return nullptr; // the decl will be destroyed along with the style rule
-  }
-
-  return decl;
+  // Mark the declaration dirty so that it can be reused by the caller.
+  // Normally SetDirty is called later in SetCSSDeclaration.
+  decl->SetDirty();
+#ifdef DEBUG
+  RefPtr<DeclarationBlock> mutableDecl = decl->EnsureMutable();
+  MOZ_ASSERT(mutableDecl == decl);
+#endif
+  decl.swap(*aCreated);
+  return *aCreated;
 }
 
 nsDOMCSSDeclaration::ParsingEnvironment
 nsDOMCSSAttributeDeclaration::GetParsingEnvironment(
     nsIPrincipal* aSubjectPrincipal) const
 {
   return {
     mElement->GetURLDataForStyleAttr(aSubjectPrincipal),
@@ -169,25 +151,29 @@ nsDOMCSSAttributeDeclaration::GetParsing
 nsresult
 nsDOMCSSAttributeDeclaration::SetSMILValue(const nsCSSPropertyID aPropID,
                                            const nsSMILValue& aValue)
 {
   MOZ_ASSERT(mIsSMILOverride);
   // No need to do the ActiveLayerTracker / ScrollLinkedEffectDetector bits,
   // since we're in a SMIL animation anyway, no need to try to detect we're a
   // scripted animation.
-  DeclarationBlock* olddecl = GetCSSDeclaration(eOperation_Modify);
+  RefPtr<DeclarationBlock> created;
+  DeclarationBlock* olddecl =
+    GetOrCreateCSSDeclaration(eOperation_Modify, getter_AddRefs(created));
   if (!olddecl) {
     return NS_ERROR_NOT_AVAILABLE;
   }
   mozAutoDocUpdate autoUpdate(DocToUpdate(), true);
   RefPtr<DeclarationBlock> decl = olddecl->EnsureMutable();
   bool changed = nsSMILCSSValueType::SetPropertyValues(aValue, *decl);
   if (changed) {
-    SetCSSDeclaration(decl);
+    // We can pass nullptr as the latter param, since this is
+    // mIsSMILOverride == true case.
+    SetCSSDeclaration(decl, nullptr);
   }
   return NS_OK;
 }
 
 nsresult
 nsDOMCSSAttributeDeclaration::SetPropertyValue(const nsCSSPropertyID aPropID,
                                                const nsAString& aValue,
                                                nsIPrincipal* aSubjectPrincipal)
@@ -205,8 +191,17 @@ nsDOMCSSAttributeDeclaration::SetPropert
       aPropID == eCSSProperty_background_position) {
     nsIFrame* frame = mElement->GetPrimaryFrame();
     if (frame) {
       ActiveLayerTracker::NotifyInlineStyleRuleModified(frame, aPropID, aValue, this);
     }
   }
   return nsDOMCSSDeclaration::SetPropertyValue(aPropID, aValue, aSubjectPrincipal);
 }
+
+void
+nsDOMCSSAttributeDeclaration::MutationClosureFunction(void* aData)
+{
+  MutationClosureData* data = static_cast<MutationClosureData*>(aData);
+  // Clear mClosure pointer so that it doesn't get called again.
+  data->mClosure = nullptr;
+  data->mElement->InlineStyleDeclarationWillChange(*data);
+}
--- a/layout/style/nsDOMCSSAttrDeclaration.h
+++ b/layout/style/nsDOMCSSAttrDeclaration.h
@@ -10,16 +10,17 @@
 #define nsDOMCSSAttributeDeclaration_h
 
 #include "mozilla/Attributes.h"
 #include "mozilla/dom/DocGroup.h"
 #include "nsDOMCSSDeclaration.h"
 
 
 class nsSMILValue;
+struct RawServoUnlockedDeclarationBlock;
 namespace mozilla {
 namespace dom {
 class DomGroup;
 class Element;
 } // namespace dom
 } // namespace mozilla
 
 class nsDOMCSSAttributeDeclaration final : public nsDOMCSSDeclaration
@@ -27,19 +28,18 @@ class nsDOMCSSAttributeDeclaration final
 public:
   typedef mozilla::dom::Element Element;
   nsDOMCSSAttributeDeclaration(Element* aContent, bool aIsSMILOverride);
 
   NS_DECL_CYCLE_COLLECTING_ISUPPORTS
   NS_DECL_CYCLE_COLLECTION_SKIPPABLE_SCRIPT_HOLDER_CLASS_AMBIGUOUS(nsDOMCSSAttributeDeclaration,
                                                                    nsICSSDeclaration)
 
-  // If GetCSSDeclaration returns non-null, then the decl it returns
-  // is owned by our current style rule.
-  mozilla::DeclarationBlock* GetCSSDeclaration(Operation aOperation) final;
+  mozilla::DeclarationBlock* GetOrCreateCSSDeclaration(
+    Operation aOperation, mozilla::DeclarationBlock** aCreated) final;
 
   nsDOMCSSDeclaration::ParsingEnvironment
     GetParsingEnvironment(nsIPrincipal* aSubjectPrincipal) const final;
 
   mozilla::css::Rule* GetParentRule() override
   {
     return nullptr;
   }
@@ -50,20 +50,33 @@ public:
   }
 
   nsresult SetSMILValue(const nsCSSPropertyID aPropID, const nsSMILValue&);
 
   nsresult SetPropertyValue(const nsCSSPropertyID aPropID,
                             const nsAString& aValue,
                             nsIPrincipal* aSubjectPrincipal) override;
 
+  static void MutationClosureFunction(void* aData);
+
+  void
+  GetPropertyChangeClosure(DeclarationBlockMutationClosure* aClosure,
+                           MutationClosureData* aClosureData) final
+  {
+    aClosure->function = MutationClosureFunction;
+    aClosure->data = aClosureData;
+    aClosureData->mClosure = MutationClosureFunction;
+    aClosureData->mElement = mElement;
+  }
+
 protected:
   ~nsDOMCSSAttributeDeclaration();
 
-  virtual nsresult SetCSSDeclaration(mozilla::DeclarationBlock* aDecl) override;
+  virtual nsresult SetCSSDeclaration(mozilla::DeclarationBlock* aDecl,
+                                     MutationClosureData* aClosureData) override;
   virtual nsIDocument* DocToUpdate() override;
 
   RefPtr<Element> mElement;
 
   /* If true, this indicates that this nsDOMCSSAttributeDeclaration
    * should interact with mContent's SMIL override style rule (rather
    * than the inline style rule).
    */
--- a/layout/style/nsDOMCSSDeclaration.cpp
+++ b/layout/style/nsDOMCSSDeclaration.cpp
@@ -7,16 +7,17 @@
 /* base class for DOM objects for element.style and cssStyleRule.style */
 
 #include "nsDOMCSSDeclaration.h"
 
 #include "mozilla/DeclarationBlock.h"
 #include "mozilla/StyleSheetInlines.h"
 #include "mozilla/css/Rule.h"
 #include "mozilla/dom/CSS2PropertiesBinding.h"
+#include "mozilla/dom/MutationEventBinding.h"
 #include "nsCSSProps.h"
 #include "nsCOMPtr.h"
 #include "mozAutoDocUpdate.h"
 #include "nsIURI.h"
 #include "mozilla/dom/BindingUtils.h"
 #include "nsContentUtils.h"
 #include "nsQueryObject.h"
 #include "mozilla/layers/ScrollLinkedEffectDetector.h"
@@ -37,17 +38,18 @@ NS_IMPL_QUERY_INTERFACE(nsDOMCSSDeclarat
 nsresult
 nsDOMCSSDeclaration::GetPropertyValue(const nsCSSPropertyID aPropID,
                                       nsAString& aValue)
 {
   MOZ_ASSERT(aPropID != eCSSProperty_UNKNOWN,
              "Should never pass eCSSProperty_UNKNOWN around");
 
   aValue.Truncate();
-  if (DeclarationBlock* decl = GetCSSDeclaration(eOperation_Read)) {
+  if (DeclarationBlock* decl =
+        GetOrCreateCSSDeclaration(eOperation_Read, nullptr)) {
     decl->GetPropertyValueByID(aPropID, aValue);
   }
   return NS_OK;
 }
 
 nsresult
 nsDOMCSSDeclaration::SetPropertyValue(const nsCSSPropertyID aPropID,
                                       const nsAString& aValue,
@@ -85,93 +87,112 @@ nsDOMCSSDeclaration::SetPropertyValue(co
 
   return ParsePropertyValue(aPropID, aValue, false, aSubjectPrincipal);
 }
 
 
 void
 nsDOMCSSDeclaration::GetCssText(nsAString& aCssText)
 {
-  DeclarationBlock* decl = GetCSSDeclaration(eOperation_Read);
+  DeclarationBlock* decl = GetOrCreateCSSDeclaration(eOperation_Read, nullptr);
   aCssText.Truncate();
 
   if (decl) {
     decl->ToString(aCssText);
   }
 }
 
 void
 nsDOMCSSDeclaration::SetCssText(const nsAString& aCssText,
                                 nsIPrincipal* aSubjectPrincipal,
                                 ErrorResult& aRv)
 {
   // We don't need to *do* anything with the old declaration, but we need
   // to ensure that it exists, or else SetCSSDeclaration may crash.
-  DeclarationBlock* olddecl = GetCSSDeclaration(eOperation_Modify);
+  RefPtr<DeclarationBlock> created;
+  DeclarationBlock* olddecl =
+    GetOrCreateCSSDeclaration(eOperation_Modify, getter_AddRefs(created));
   if (!olddecl) {
     aRv.Throw(NS_ERROR_NOT_AVAILABLE);
     return;
   }
 
   // For nsDOMCSSAttributeDeclaration, SetCSSDeclaration will lead to
   // Attribute setting code, which leads in turn to BeginUpdate.  We
   // need to start the update now so that the old rule doesn't get used
   // between when we mutate the declaration and when we set the new
   // rule (see stack in bug 209575).
   mozAutoDocUpdate autoUpdate(DocToUpdate(), true);
+  DeclarationBlockMutationClosure closure = {};
+  MutationClosureData closureData;
+  GetPropertyChangeClosure(&closure, &closureData);
 
   ParsingEnvironment servoEnv =
     GetParsingEnvironment(aSubjectPrincipal);
   if (!servoEnv.mUrlExtraData) {
+    if (created) {
+      // In case we can't set a new declaration, but one was
+      // created for the old one, we need to set the old declaration to
+      // get right style attribute handling.
+      SetCSSDeclaration(olddecl, &closureData);
+    }
     aRv.Throw(NS_ERROR_NOT_AVAILABLE);
     return;
   }
 
+  // Need to special case closure calling here, since parsing css text
+  // doesn't modify any existing declaration and that is why the callback isn't
+  // called implicitly.
+  if (closureData.mClosure) {
+    closureData.mClosure(&closureData);
+  }
+
   RefPtr<DeclarationBlock> newdecl =
     DeclarationBlock::FromCssText(aCssText, servoEnv.mUrlExtraData,
                                   servoEnv.mCompatMode, servoEnv.mLoader);
 
-  aRv = SetCSSDeclaration(newdecl);
+  aRv = SetCSSDeclaration(newdecl, &closureData);
 }
 
 uint32_t
 nsDOMCSSDeclaration::Length()
 {
-  DeclarationBlock* decl = GetCSSDeclaration(eOperation_Read);
+  DeclarationBlock* decl = GetOrCreateCSSDeclaration(eOperation_Read, nullptr);
 
   if (decl) {
     return decl->Count();
   }
 
   return 0;
 }
 
 void
 nsDOMCSSDeclaration::IndexedGetter(uint32_t aIndex, bool& aFound, nsAString& aPropName)
 {
-  DeclarationBlock* decl = GetCSSDeclaration(eOperation_Read);
+  DeclarationBlock* decl = GetOrCreateCSSDeclaration(eOperation_Read, nullptr);
   aFound = decl && decl->GetNthProperty(aIndex, aPropName);
 }
 
 NS_IMETHODIMP
 nsDOMCSSDeclaration::GetPropertyValue(const nsAString& aPropertyName,
                                       nsAString& aReturn)
 {
   aReturn.Truncate();
-  if (DeclarationBlock* decl = GetCSSDeclaration(eOperation_Read)) {
+  if (DeclarationBlock* decl =
+        GetOrCreateCSSDeclaration(eOperation_Read, nullptr)) {
     decl->GetPropertyValue(aPropertyName, aReturn);
   }
   return NS_OK;
 }
 
 void
 nsDOMCSSDeclaration::GetPropertyPriority(const nsAString& aPropertyName,
                                          nsAString& aPriority)
 {
-  DeclarationBlock* decl = GetCSSDeclaration(eOperation_Read);
+  DeclarationBlock* decl = GetOrCreateCSSDeclaration(eOperation_Read, nullptr);
 
   aPriority.Truncate();
   if (decl && decl->GetPropertyIsImportant(aPropertyName)) {
     aPriority.AssignLiteral("important");
   }
 }
 
 NS_IMETHODIMP
@@ -241,117 +262,151 @@ nsDOMCSSDeclaration::GetParsingEnvironme
     eCompatibility_FullStandards,
     nullptr,
   };
 }
 
 template<typename Func>
 nsresult
 nsDOMCSSDeclaration::ModifyDeclaration(nsIPrincipal* aSubjectPrincipal,
+                                       MutationClosureData* aClosureData,
                                        Func aFunc)
 {
-  DeclarationBlock* olddecl = GetCSSDeclaration(eOperation_Modify);
+  RefPtr<DeclarationBlock> created;
+  DeclarationBlock* olddecl =
+    GetOrCreateCSSDeclaration(eOperation_Modify, getter_AddRefs(created));
   if (!olddecl) {
     return NS_ERROR_NOT_AVAILABLE;
   }
 
   // For nsDOMCSSAttributeDeclaration, SetCSSDeclaration will lead to
   // Attribute setting code, which leads in turn to BeginUpdate.  We
   // need to start the update now so that the old rule doesn't get used
   // between when we mutate the declaration and when we set the new
   // rule (see stack in bug 209575).
   mozAutoDocUpdate autoUpdate(DocToUpdate(), true);
   RefPtr<DeclarationBlock> decl = olddecl->EnsureMutable();
 
   bool changed;
   ParsingEnvironment servoEnv =
     GetParsingEnvironment(aSubjectPrincipal);
   if (!servoEnv.mUrlExtraData) {
+    if (created) {
+      // In case we can't set a new declaration, but one was
+      // created for the old one, we need to set the old declaration to
+      // get right style attribute handling.
+      SetCSSDeclaration(olddecl, aClosureData);
+    }
     return NS_ERROR_NOT_AVAILABLE;
   }
 
   changed = aFunc(decl, servoEnv);
 
   if (!changed) {
+    if (created) {
+      // See comment above about setting old declaration.
+      SetCSSDeclaration(olddecl, aClosureData);
+    }
     // Parsing failed -- but we don't throw an exception for that.
     return NS_OK;
   }
 
-  return SetCSSDeclaration(decl);
+  return SetCSSDeclaration(decl, aClosureData);
 }
 
 nsresult
 nsDOMCSSDeclaration::ParsePropertyValue(const nsCSSPropertyID aPropID,
                                         const nsAString& aPropValue,
                                         bool aIsImportant,
                                         nsIPrincipal* aSubjectPrincipal)
 {
+  DeclarationBlockMutationClosure closure = {};
+  MutationClosureData closureData;
+  GetPropertyChangeClosure(&closure, &closureData);
+
   return ModifyDeclaration(
     aSubjectPrincipal,
+    &closureData,
     [&](DeclarationBlock* decl, ParsingEnvironment& env) {
       NS_ConvertUTF16toUTF8 value(aPropValue);
       return Servo_DeclarationBlock_SetPropertyById(
         decl->Raw(), aPropID, &value, aIsImportant, env.mUrlExtraData,
-        ParsingMode::Default, env.mCompatMode, env.mLoader, /* aClosure = */ { });
+        ParsingMode::Default, env.mCompatMode, env.mLoader, closure);
     });
 }
 
 nsresult
 nsDOMCSSDeclaration::ParseCustomPropertyValue(const nsAString& aPropertyName,
                                               const nsAString& aPropValue,
                                               bool aIsImportant,
                                               nsIPrincipal* aSubjectPrincipal)
 {
   MOZ_ASSERT(nsCSSProps::IsCustomPropertyName(aPropertyName));
+
+  DeclarationBlockMutationClosure closure = {};
+  MutationClosureData closureData;
+  GetPropertyChangeClosure(&closure, &closureData);
+
   return ModifyDeclaration(
     aSubjectPrincipal,
+    &closureData,
     [&](DeclarationBlock* decl, ParsingEnvironment& env) {
       NS_ConvertUTF16toUTF8 property(aPropertyName);
       NS_ConvertUTF16toUTF8 value(aPropValue);
       return Servo_DeclarationBlock_SetProperty(
         decl->Raw(), &property, &value, aIsImportant, env.mUrlExtraData,
-        ParsingMode::Default, env.mCompatMode, env.mLoader, /* aClosure = */ { });
+        ParsingMode::Default, env.mCompatMode, env.mLoader, closure);
     });
 }
 
 nsresult
 nsDOMCSSDeclaration::RemovePropertyInternal(nsCSSPropertyID aPropID)
 {
-  DeclarationBlock* olddecl = GetCSSDeclaration(eOperation_RemoveProperty);
+  DeclarationBlock* olddecl =
+    GetOrCreateCSSDeclaration(eOperation_RemoveProperty, nullptr);
   if (!olddecl) {
     return NS_OK; // no decl, so nothing to remove
   }
 
   // For nsDOMCSSAttributeDeclaration, SetCSSDeclaration will lead to
   // Attribute setting code, which leads in turn to BeginUpdate.  We
   // need to start the update now so that the old rule doesn't get used
   // between when we mutate the declaration and when we set the new
   // rule (see stack in bug 209575).
   mozAutoDocUpdate autoUpdate(DocToUpdate(), true);
 
+  DeclarationBlockMutationClosure closure = {};
+  MutationClosureData closureData;
+  GetPropertyChangeClosure(&closure, &closureData);
+
   RefPtr<DeclarationBlock> decl = olddecl->EnsureMutable();
-  if (!decl->RemovePropertyByID(aPropID)) {
+  if (!decl->RemovePropertyByID(aPropID, closure)) {
     return NS_OK;
   }
-  return SetCSSDeclaration(decl);
+  return SetCSSDeclaration(decl, &closureData);
 }
 
 nsresult
 nsDOMCSSDeclaration::RemovePropertyInternal(const nsAString& aPropertyName)
 {
-  DeclarationBlock* olddecl = GetCSSDeclaration(eOperation_RemoveProperty);
+  DeclarationBlock* olddecl =
+    GetOrCreateCSSDeclaration(eOperation_RemoveProperty, nullptr);
   if (!olddecl) {
     return NS_OK; // no decl, so nothing to remove
   }
 
   // For nsDOMCSSAttributeDeclaration, SetCSSDeclaration will lead to
   // Attribute setting code, which leads in turn to BeginUpdate.  We
   // need to start the update now so that the old rule doesn't get used
   // between when we mutate the declaration and when we set the new
   // rule (see stack in bug 209575).
   mozAutoDocUpdate autoUpdate(DocToUpdate(), true);
 
+  DeclarationBlockMutationClosure closure = {};
+  MutationClosureData closureData;
+  GetPropertyChangeClosure(&closure, &closureData);
+
   RefPtr<DeclarationBlock> decl = olddecl->EnsureMutable();
-  if (!decl->RemoveProperty(aPropertyName)) {
+  if (!decl->RemoveProperty(aPropertyName, closure)) {
     return NS_OK;
   }
-  return SetCSSDeclaration(decl);
+  return SetCSSDeclaration(decl, &closureData);
 }
--- a/layout/style/nsDOMCSSDeclaration.h
+++ b/layout/style/nsDOMCSSDeclaration.h
@@ -7,32 +7,58 @@
 /* base class for DOM objects for element.style and cssStyleRule.style */
 
 #ifndef nsDOMCSSDeclaration_h___
 #define nsDOMCSSDeclaration_h___
 
 #include "nsICSSDeclaration.h"
 
 #include "mozilla/Attributes.h"
+#include "mozilla/Maybe.h"
 #include "mozilla/URLExtraData.h"
+#include "nsAttrValue.h"
 #include "nsIURI.h"
 #include "nsCOMPtr.h"
 #include "nsCompatibility.h"
 
 class nsIPrincipal;
 class nsIDocument;
 struct JSContext;
 class JSObject;
+struct DeclarationBlockMutationClosure;
 
 namespace mozilla {
 class DeclarationBlock;
 namespace css {
 class Loader;
 class Rule;
 } // namespace css
+namespace dom {
+class Element;
+}
+
+struct MutationClosureData
+{
+  MutationClosureData()
+    : mClosure(nullptr)
+    , mElement(nullptr)
+    , mModType(0)
+  {
+  }
+
+  // mClosure is non-null as long as the closure hasn't been called.
+  // This is needed so that it can be guaranteed that
+  // InlineStyleDeclarationWillChange is always called before
+  // SetInlineStyleDeclaration.
+  void (*mClosure)(void*);
+  mozilla::dom::Element* mElement;
+  Maybe<nsAttrValue> mOldValue;
+  uint8_t mModType;
+};
+
 } // namespace mozilla
 
 class nsDOMCSSDeclaration : public nsICSSDeclaration
 {
 public:
   // Only implement QueryInterface; subclasses have the responsibility
   // of implementing AddRef/Release.
   NS_IMETHOD QueryInterface(REFNSIID aIID, void** aInstancePtr) override;
@@ -130,37 +156,42 @@ public:
                                mozilla::css::Loader* aLoader)
       : mUrlExtraData(aUrlData)
       , mCompatMode(aCompatMode)
       , mLoader(aLoader)
     {}
   };
 
 protected:
-  // The reason for calling GetCSSDeclaration.
+  // The reason for calling GetOrCreateCSSDeclaration.
   enum Operation {
-    // We are calling GetCSSDeclaration so that we can read from it.  Does not
-    // allocate a new declaration if we don't have one yet; returns nullptr in
-    // this case.
+    // We are calling GetOrCreateCSSDeclaration so that we can read from it.
+    // Does not allocate a new declaration if we don't have one yet; returns
+    // nullptr in this case.
     eOperation_Read,
 
-    // We are calling GetCSSDeclaration so that we can set a property on it
-    // or re-parse the whole declaration.  Allocates a new declaration if we
-    // don't have one yet and calls AttributeWillChange.  A nullptr return value
-    // indicates an error allocating the declaration.
+    // We are calling GetOrCreateCSSDeclaration so that we can set a property on
+    // it or re-parse the whole declaration.  Allocates a new declaration if we
+    // don't have one yet. A nullptr return value indicates an error allocating
+    // the declaration.
     eOperation_Modify,
 
-    // We are calling GetCSSDeclaration so that we can remove a property from
-    // it.  Does not allocates a new declaration if we don't have one yet;
-    // returns nullptr in this case.  If we do have a declaration, calls
-    // AttributeWillChange.
+    // We are calling GetOrCreateCSSDeclaration so that we can remove a property
+    // from it. Does not allocate a new declaration if we don't have one yet;
+    // returns nullptr in this case.
     eOperation_RemoveProperty
   };
-  virtual mozilla::DeclarationBlock* GetCSSDeclaration(Operation aOperation) = 0;
-  virtual nsresult SetCSSDeclaration(mozilla::DeclarationBlock* aDecl) = 0;
+
+  // If aOperation is eOperation_Modify, aCreated must be non-null and
+  // the call may set it to point to the newly created object.
+  virtual mozilla::DeclarationBlock* GetOrCreateCSSDeclaration(
+    Operation aOperation, mozilla::DeclarationBlock** aCreated) = 0;
+
+  virtual nsresult SetCSSDeclaration(mozilla::DeclarationBlock* aDecl,
+                                     mozilla::MutationClosureData* aClosureData) = 0;
   // Document that we must call BeginUpdate/EndUpdate on around the
   // calls to SetCSSDeclaration and the style rule mutation that leads
   // to it.
   virtual nsIDocument* DocToUpdate() = 0;
 
   // mUrlExtraData returns URL data for parsing url values in
   // CSS. Returns nullptr on failure. If mUrlExtraData is nullptr,
   // mCompatMode may not be set to anything meaningful.
@@ -182,18 +213,25 @@ protected:
   nsresult ParseCustomPropertyValue(const nsAString& aPropertyName,
                                     const nsAString& aPropValue,
                                     bool aIsImportant,
                                     nsIPrincipal* aSubjectPrincipal);
 
   nsresult RemovePropertyInternal(nsCSSPropertyID aPropID);
   nsresult RemovePropertyInternal(const nsAString& aProperty);
 
+  virtual void
+  GetPropertyChangeClosure(DeclarationBlockMutationClosure* aClosure,
+                           mozilla::MutationClosureData* aClosureData)
+  {
+  }
+
 protected:
   virtual ~nsDOMCSSDeclaration();
 
 private:
   template<typename ServoFunc>
   inline nsresult ModifyDeclaration(nsIPrincipal* aSubjectPrincipal,
+                                    mozilla::MutationClosureData* aClosureData,
                                     ServoFunc aServoFunc);
 };
 
 #endif // nsDOMCSSDeclaration_h___
--- a/layout/style/test/test_style_attr_listener.html
+++ b/layout/style/test/test_style_attr_listener.html
@@ -3,29 +3,50 @@
 <head>
   <title>Test for Bug 338679 (from bug 1340341)</title>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
 <body>
 <div style=""></div>
 <script>
-let div = document.querySelector('div');
-let expectation;
-let count = 0;
-div.style.color = "red";
-div.addEventListener('DOMAttrModified', function(evt) {
-  count++;
-  is(evt.prevValue, expectation.prevValue, "Previous value for event ${count}");
-  is(evt.newValue, expectation.newValue, "New value for event ${count}");
-});
-expectation = { prevValue: 'color: red;', newValue: 'color: green;' };
-div.style.color = "green";
-expectation = { prevValue: 'color: green;', newValue: '' };
-div.style.color = '';
-if (SpecialPowers.getBoolPref("dom.mutation-events.cssom.disabled")) {
-  is(count, 0, "No DOMAttrModified event should be triggered");
-} else {
-  is(count, 2, "DOMAttrModified events should have been triggered");
+SimpleTest.waitForExplicitFinish();
+
+// Run the test first with mutation events enabled and then disabled.
+SpecialPowers.pushPrefEnv(
+  { 'set': [['dom.mutation-events.cssom.disabled', false]] },
+  runTest
+);
+
+function runTest(stop) {
+  let div = document.querySelector('div');
+  let expectation;
+  let count = 0;
+  div.style.color = "red";
+  div.addEventListener('DOMAttrModified', function(evt) {
+    count++;
+    is(evt.prevValue, expectation.prevValue, `Previous value for event ${count}`);
+    is(evt.newValue, expectation.newValue, `New value for event ${count}`);
+  });
+  expectation = { prevValue: 'color: red;', newValue: 'color: green;' };
+  div.style.color = "green";
+  expectation = { prevValue: 'color: green;', newValue: '' };
+  div.style.color = '';
+  if (SpecialPowers.getBoolPref("dom.mutation-events.cssom.disabled")) {
+    is(count, 0, "No DOMAttrModified event should be triggered");
+  } else {
+    is(count, 2, "DOMAttrModified events should have been triggered");
+  }
+
+  if (!stop) {
+    div.setAttribute("style", "");
+    SpecialPowers.pushPrefEnv(
+      { 'set': [['dom.mutation-events.cssom.disabled', true]] },
+      function() {
+        runTest(true);
+        SimpleTest.finish();
+      }
+    );
+  }
 }
 </script>
 </body>
 </html>
deleted file mode 100644
--- a/testing/web-platform/meta/css/cssom/css-style-attr-decl-block.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[css-style-attr-decl-block.html]
-  [Removing non-existing property or setting invalid value on CSS declaration block shouldn't queue mutation record]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/css/cssom/cssstyledeclaration-mutationrecord-003.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[cssstyledeclaration-mutationrecord-003.html]
-  [CSSStyleDeclaration.removeProperty doesn't queue a mutation record when not actually removed, invoked from setPropertyValue]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/css/cssom/cssstyledeclaration-mutationrecord-004.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[cssstyledeclaration-mutationrecord-004.html]
-  [CSSStyleDeclaration.removeProperty doesn't queue a mutation record when not actually removed]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/custom-elements/attribute-changed-callback.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[attribute-changed-callback.html]
-  [attributedChangedCallback must be enqueued for style attribute change by mutating inline style declaration]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/custom-elements/reactions/CSSStyleDeclaration.html.ini
+++ /dev/null
@@ -1,58 +0,0 @@
-[CSSStyleDeclaration.html]
-  [cssText on CSSStyleDeclaration must enqueue an attributeChanged reaction when it adds the observed style attribute]
-    expected: FAIL
-
-  [cssText on CSSStyleDeclaration must enqueue an attributeChanged reaction when it mutates the observed style attribute]
-    expected: FAIL
-
-  [setProperty on CSSStyleDeclaration must enqueue an attributeChanged reaction when it adds the observed style attribute]
-    expected: FAIL
-
-  [setProperty on CSSStyleDeclaration must enqueue an attributeChanged reaction when it mutates the observed style attribute]
-    expected: FAIL
-
-  [setPropertyValue on CSSStyleDeclaration must enqueue an attributeChanged reaction when it adds the observed style attribute]
-    expected: FAIL
-
-  [setPropertyValue on CSSStyleDeclaration must not enqueue an attributeChanged reaction when it adds the style attribute but the style attribute is not observed]
-    expected: FAIL
-
-  [setPropertyValue on CSSStyleDeclaration must enqueue an attributeChanged reaction when it mutates the observed style attribute]
-    expected: FAIL
-
-  [setPropertyValue on CSSStyleDeclaration must not enqueue an attributeChanged reaction when it mutates the style attribute but the style attribute is not observed]
-    expected: FAIL
-
-  [setPropertyPriority on CSSStyleDeclaration must enqueue an attributeChanged reaction when it makes a property important and the style attribute is observed]
-    expected: FAIL
-
-  [setPropertyPriority on CSSStyleDeclaration must enqueue an attributeChanged reaction when it makes a property important but the style attribute is not observed]
-    expected: FAIL
-
-  [cssFloat on CSSStyleDeclaration must enqueue an attributeChanged reaction when it adds the observed style attribute]
-    expected: FAIL
-
-  [A camel case attribute (borderWidth) on CSSStyleDeclaration must enqueue an attributeChanged reaction when it adds the observed style attribute]
-    expected: FAIL
-
-  [A camel case attribute (borderWidth) on CSSStyleDeclaration must enqueue an attributeChanged reaction when it mutates the observed style attribute]
-    expected: FAIL
-
-  [A dashed property (border-width) on CSSStyleDeclaration must enqueue an attributeChanged reaction when it adds the observed style attribute]
-    expected: FAIL
-
-  [A dashed property (border-width) on CSSStyleDeclaration must enqueue an attributeChanged reaction when it mutates the observed style attribute]
-    expected: FAIL
-
-  [A webkit prefixed camel case attribute (webkitFilter) on CSSStyleDeclaration must enqueue an attributeChanged reaction when it adds the observed style attribute]
-    expected: FAIL
-
-  [A webkit prefixed camel case attribute (webkitFilter) on CSSStyleDeclaration must enqueue an attributeChanged reaction when it mutates the observed style attribute]
-    expected: FAIL
-
-  [A webkit prefixed dashed property (-webkit-filter) on CSSStyleDeclaration must enqueue an attributeChanged reaction when it adds the observed style attribute]
-    expected: FAIL
-
-  [A webkit prefixed dashed property (-webkit-filter) on CSSStyleDeclaration must enqueue an attributeChanged reaction when it mutates the observed style attribute]
-    expected: FAIL
-