diff --git a/devtools/client/debugger/test/mochitest/browser_dbg-dom-mutation-breakpoints.js b/devtools/client/debugger/test/mochitest/browser_dbg-dom-mutation-breakpoints.js index 8ab7a1d08003..2e45493bd2d1 100644 --- a/devtools/client/debugger/test/mochitest/browser_dbg-dom-mutation-breakpoints.js +++ b/devtools/client/debugger/test/mochitest/browser_dbg-dom-mutation-breakpoints.js @@ -15,12 +15,16 @@ Services.scriptloader.loadSubScript( const DMB_TEST_URL = "http://example.com/browser/devtools/client/debugger/test/mochitest/examples/doc-dom-mutation.html"; -add_task(async function() { - // Enable features +async function enableMutationBreakpoints() { await pushPref("devtools.debugger.features.dom-mutation-breakpoints", true); await pushPref("devtools.markup.mutationBreakpoints.enabled", true); await pushPref("devtools.debugger.dom-mutation-breakpoints-visible", true); +} + +add_task(async function() { + // Enable features + await enableMutationBreakpoints(); info("Switches over to the inspector pane"); const { inspector, toolbox } = await openInspectorForURL(DMB_TEST_URL); @@ -66,6 +70,13 @@ add_task(async function() { await waitForPaused(dbg); await resume(dbg); + info("Changing style to trigger debugger pause"); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { + content.document.querySelector("#style-attribute").click(); + }); + await waitForPaused(dbg); + await resume(dbg); + info("Changing subtree to trigger debugger pause"); SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { content.document.querySelector("#subtree").click(); diff --git a/devtools/client/debugger/test/mochitest/examples/doc-dom-mutation.html b/devtools/client/debugger/test/mochitest/examples/doc-dom-mutation.html index 5ad60f4c6439..fcd8819f068b 100644 --- a/devtools/client/debugger/test/mochitest/examples/doc-dom-mutation.html +++ b/devtools/client/debugger/test/mochitest/examples/doc-dom-mutation.html @@ -10,6 +10,7 @@ + diff --git a/devtools/client/debugger/test/mochitest/examples/dom-mutation.js b/devtools/client/debugger/test/mochitest/examples/dom-mutation.js index 3d3f5c75d8eb..ceb64dea6749 100644 --- a/devtools/client/debugger/test/mochitest/examples/dom-mutation.js +++ b/devtools/client/debugger/test/mochitest/examples/dom-mutation.js @@ -3,6 +3,10 @@ function changeAttribute() { document.body.setAttribute("title", title); } +function changeStyleAttribute() { + document.body.style.color = "blue"; +} + function changeSubtree() { document.body.appendChild(document.createElement("div")); } diff --git a/devtools/server/actors/inspector/walker.js b/devtools/server/actors/inspector/walker.js index aba11d82ce8a..836b4600378d 100644 --- a/devtools/server/actors/inspector/walker.js +++ b/devtools/server/actors/inspector/walker.js @@ -231,8 +231,8 @@ var WalkerActor = protocol.ActorClassWithSpec(walkerSpec, { // list contains orphaned nodes that were so retained. this._retainedOrphans = new Set(); - this.onNodeInserted = this.onNodeInserted.bind(this); - this.onNodeInserted[EXCLUDED_LISTENER] = true; + this.onSubtreeModified = this.onSubtreeModified.bind(this); + this.onSubtreeModified[EXCLUDED_LISTENER] = true; this.onNodeRemoved = this.onNodeRemoved.bind(this); this.onNodeRemoved[EXCLUDED_LISTENER] = true; this.onAttributeModified = this.onAttributeModified.bind(this); @@ -2076,65 +2076,58 @@ var WalkerActor = protocol.ActorClassWithSpec(walkerSpec, { _updateDocumentMutationListeners(rawDoc) { const docMutationBreakpoints = this._mutationBreakpointsForDoc(rawDoc); if (!docMutationBreakpoints) { + rawDoc.devToolsWatchingDOMMutations = false; return; } - const origFlag = rawDoc.devToolsWatchingDOMMutations; - rawDoc.devToolsWatchingDOMMutations = true; + const anyBreakpoint = + docMutationBreakpoints.counts.subtree > 0 || + docMutationBreakpoints.counts.removal > 0 || + docMutationBreakpoints.counts.attribute > 0; + + rawDoc.devToolsWatchingDOMMutations = anyBreakpoint; if (docMutationBreakpoints.counts.subtree > 0) { - eventListenerService.addSystemEventListener( - rawDoc, - "DOMNodeInserted", - this.onNodeInserted, + this.chromeEventHandler.addEventListener( + "devtoolschildinserted", + this.onSubtreeModified, true /* capture */ ); } else { - eventListenerService.removeSystemEventListener( - rawDoc, - "DOMNodeInserted", - this.onNodeInserted, + this.chromeEventHandler.removeEventListener( + "devtoolschildinserted", + this.onSubtreeModified, true /* capture */ ); } - if ( - docMutationBreakpoints.counts.subtree > 0 || - docMutationBreakpoints.counts.removal > 0 || - docMutationBreakpoints.counts.attribute > 0 - ) { - eventListenerService.addSystemEventListener( - rawDoc, - "DOMNodeRemoved", + if (anyBreakpoint) { + this.chromeEventHandler.addEventListener( + "devtoolschildremoved", this.onNodeRemoved, true /* capture */ ); } else { - eventListenerService.removeSystemEventListener( - rawDoc, - "DOMNodeRemoved", + this.chromeEventHandler.removeEventListener( + "devtoolschildremoved", this.onNodeRemoved, true /* capture */ ); } if (docMutationBreakpoints.counts.attribute > 0) { - eventListenerService.addSystemEventListener( - rawDoc, - "DOMAttrModified", + this.chromeEventHandler.addEventListener( + "devtoolsattrmodified", this.onAttributeModified, true /* capture */ ); } else { - eventListenerService.removeSystemEventListener( - rawDoc, - "DOMAttrModified", + this.chromeEventHandler.removeEventListener( + "devtoolsattrmodified", this.onAttributeModified, true /* capture */ ); } - - rawDoc.devToolsWatchingDOMMutations = origFlag; }, _breakOnMutation: function(mutationType, targetNode, ancestorNode, action) { @@ -2172,20 +2165,15 @@ var WalkerActor = protocol.ActorClassWithSpec(walkerSpec, { ); }, - onNodeInserted: function(evt) { - this.onSubtreeModified(evt, "add"); - }, - onNodeRemoved: function(evt) { const mutationBpInfo = this._breakpointInfoForNode(evt.target); const hasNodeRemovalEvent = mutationBpInfo?.removal; this._clearMutationBreakpointsFromSubtree(evt.target); - if (hasNodeRemovalEvent) { this._breakOnMutation("nodeRemoved", evt.target); } else { - this.onSubtreeModified(evt, "remove"); + this.onSubtreeModified(evt); } }, @@ -2196,7 +2184,8 @@ var WalkerActor = protocol.ActorClassWithSpec(walkerSpec, { } }, - onSubtreeModified: function(evt, action) { + onSubtreeModified: function(evt) { + const action = evt.type === "devtoolschildinserted" ? "add" : "remove"; let node = evt.target; while ((node = node.parentNode) !== null) { const mutationBpInfo = this._breakpointInfoForNode(node); diff --git a/dom/base/Document.cpp b/dom/base/Document.cpp index efbb00cd2ebc..cbd69f616a23 100644 --- a/dom/base/Document.cpp +++ b/dom/base/Document.cpp @@ -9567,8 +9567,7 @@ nsINode* Document::AdoptNode(nsINode& aAdoptedNode, ErrorResult& rv) { // Scope firing mutation events so that we don't carry any state that // might be stale { - nsINode* parent = adoptedNode->GetParentNode(); - if (parent) { + if (nsINode* parent = adoptedNode->GetParentNode()) { nsContentUtils::MaybeFireNodeRemoved(adoptedNode, parent); } } @@ -13310,6 +13309,80 @@ already_AddRefed Document::GetTooltipNode() { return nullptr; } +namespace { + +class DevToolsMutationObserver final : public nsStubMutationObserver { + NS_DECL_ISUPPORTS + NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED + + // We handle this in nsContentUtils::MaybeFireNodeRemoved, since devtools + // relies on the event firing _before_ the removal happens. + // NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED + + // NOTE(emilio, bug 1694627): DevTools doesn't seem to deal with character + // data changes right now (maybe intentionally?). + // NS_DECL_NSIMUTATIONOBSERVER_CHARACTERDATACHANGED + + DevToolsMutationObserver() = default; + + private: + void FireEvent(nsINode* aTarget, const nsAString& aType); + + ~DevToolsMutationObserver() = default; +}; + +NS_IMPL_ISUPPORTS(DevToolsMutationObserver, nsIMutationObserver) + +void DevToolsMutationObserver::FireEvent(nsINode* aTarget, + const nsAString& aType) { + if (aTarget->ChromeOnlyAccess()) { + return; + } + (new AsyncEventDispatcher(aTarget, aType, CanBubble::eNo, + ChromeOnlyDispatch::eYes, Composed::eYes)) + ->RunDOMEventWhenSafe(); +} + +void DevToolsMutationObserver::AttributeChanged(Element* aElement, + int32_t aNamespaceID, + nsAtom* aAttribute, + int32_t aModType, + const nsAttrValue* aOldValue) { + FireEvent(aElement, u"devtoolsattrmodified"_ns); +} + +void DevToolsMutationObserver::ContentAppended(nsIContent* aFirstNewContent) { + for (nsIContent* c = aFirstNewContent; c; c = c->GetNextSibling()) { + ContentInserted(c); + } +} + +void DevToolsMutationObserver::ContentInserted(nsIContent* aChild) { + FireEvent(aChild, u"devtoolschildinserted"_ns); +} + +static StaticRefPtr sDevToolsMutationObserver; + +} // namespace + +void Document::SetDevToolsWatchingDOMMutations(bool aValue) { + if (mDevToolsWatchingDOMMutations == aValue) { + return; + } + mDevToolsWatchingDOMMutations = aValue; + if (aValue) { + if (MOZ_UNLIKELY(!sDevToolsMutationObserver)) { + sDevToolsMutationObserver = new DevToolsMutationObserver(); + ClearOnShutdown(&sDevToolsMutationObserver); + } + AddMutationObserver(sDevToolsMutationObserver); + } else if (sDevToolsMutationObserver) { + RemoveMutationObserver(sDevToolsMutationObserver); + } +} + void Document::MaybeWarnAboutZoom() { if (mHasWarnedAboutZoom) { return; diff --git a/dom/base/Document.h b/dom/base/Document.h index 791af9f92e3f..59ae90919eae 100644 --- a/dom/base/Document.h +++ b/dom/base/Document.h @@ -3549,9 +3549,7 @@ class Document : public nsINode, bool DevToolsWatchingDOMMutations() const { return mDevToolsWatchingDOMMutations; } - void SetDevToolsWatchingDOMMutations(bool aValue) { - mDevToolsWatchingDOMMutations = aValue; - } + void SetDevToolsWatchingDOMMutations(bool aValue); void MaybeWarnAboutZoom(); diff --git a/dom/base/nsContentUtils.cpp b/dom/base/nsContentUtils.cpp index 0a798533c553..bc7c37503245 100644 --- a/dom/base/nsContentUtils.cpp +++ b/dom/base/nsContentUtils.cpp @@ -4617,8 +4617,7 @@ void nsContentUtils::MaybeFireNodeRemoved(nsINode* aChild, nsINode* aParent) { // that is a know case when we'd normally fire a mutation event, but can't // make that safe and so we suppress it at this time. Ideally this should // go away eventually. - if (!(aChild->IsContent() && - aChild->AsContent()->IsInNativeAnonymousSubtree()) && + if (!aChild->IsInNativeAnonymousSubtree() && !sDOMNodeRemovedSuppressCount) { NS_ERROR("Want to fire DOMNodeRemoved event, but it's not safe"); WarnScriptWasIgnored(aChild->OwnerDoc()); @@ -4626,6 +4625,15 @@ void nsContentUtils::MaybeFireNodeRemoved(nsINode* aChild, nsINode* aParent) { return; } + { + Document* doc = aParent->OwnerDoc(); + if (MOZ_UNLIKELY(doc->DevToolsWatchingDOMMutations()) && + aChild->IsInComposedDoc() && !aChild->ChromeOnlyAccess()) { + DispatchChromeEvent(doc, aChild, u"devtoolschildremoved"_ns, + CanBubble::eNo, Cancelable::eNo); + } + } + if (HasMutationListeners(aChild, NS_EVENT_BITS_MUTATION_NODEREMOVED, aParent)) { InternalMutationEvent mutation(true, eLegacyNodeRemoved); diff --git a/dom/events/EventListenerManager.cpp b/dom/events/EventListenerManager.cpp index 51a8240bd2db..cf29b35600dc 100644 --- a/dom/events/EventListenerManager.cpp +++ b/dom/events/EventListenerManager.cpp @@ -287,9 +287,7 @@ void EventListenerManager::AddEventListenerInternal( mMayHaveMutationListeners = true; // Go from our target to the nearest enclosing DOM window. if (nsPIDOMWindowInner* window = GetInnerWindowForTarget()) { - nsCOMPtr doc = window->GetExtantDoc(); - if (doc && - !(aFlags.mInSystemGroup && doc->DevToolsWatchingDOMMutations())) { + if (Document* doc = window->GetExtantDoc()) { doc->WarnOnceAbout(DeprecatedOperations::eMutationEvent); } // If aEventMessage is eLegacySubtreeModified, we need to listen all