diff --git a/packages/react-native/ReactCommon/react/nativemodule/mutationobserver/NativeMutationObserver.cpp b/packages/react-native/ReactCommon/react/nativemodule/mutationobserver/NativeMutationObserver.cpp index b45eb0cfeb97cd..4b3f3f64222ac2 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/mutationobserver/NativeMutationObserver.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/mutationobserver/NativeMutationObserver.cpp @@ -44,15 +44,6 @@ void NativeMutationObserver::observe( mutationObserverId, shadowNode, subtree, uiManager); } -// TODO: remove in the next version -void NativeMutationObserver::unobserve( - jsi::Runtime& /*runtime*/, - MutationObserverId mutationObserverId, - jsi::Object targetShadowNode) { - // This will not be used but cannot be removed yet because the compatibility - // check does not allow it. -} - void NativeMutationObserver::unobserveAll( jsi::Runtime& runtime, MutationObserverId mutationObserverId) { diff --git a/packages/react-native/ReactCommon/react/nativemodule/mutationobserver/NativeMutationObserver.h b/packages/react-native/ReactCommon/react/nativemodule/mutationobserver/NativeMutationObserver.h index 8003df42d24ac2..07d637b46b28a4 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/mutationobserver/NativeMutationObserver.h +++ b/packages/react-native/ReactCommon/react/nativemodule/mutationobserver/NativeMutationObserver.h @@ -52,13 +52,6 @@ class NativeMutationObserver jsi::Runtime& runtime, NativeMutationObserverObserveOptions options); - // TODO: remove in the next version - [[deprecated("use unobserveAll instead")]] - void unobserve( - jsi::Runtime& runtime, - MutationObserverId mutationObserverId, - jsi::Object targetShadowNode); - void unobserveAll( jsi::Runtime& runtime, MutationObserverId mutationObserverId); diff --git a/packages/react-native/src/private/webapis/mutationobserver/MutationObserver.js b/packages/react-native/src/private/webapis/mutationobserver/MutationObserver.js index 869d9380b76e9b..f35efdd1c5c5a0 100644 --- a/packages/react-native/src/private/webapis/mutationobserver/MutationObserver.js +++ b/packages/react-native/src/private/webapis/mutationobserver/MutationObserver.js @@ -118,15 +118,11 @@ export default class MutationObserver { const mutationObserverId = this._getOrCreateMutationObserverId(); - const didStartObserving = MutationObserverManager.observe({ + MutationObserverManager.observe({ mutationObserverId, target, subtree: Boolean(options?.subtree), }); - - if (didStartObserving && !MutationObserverManager.unobserveAll) { - this._observationTargets.add(target); - } } /** @@ -139,17 +135,7 @@ export default class MutationObserver { return; } - if (MutationObserverManager.unobserveAll) { - MutationObserverManager.unobserveAll(mutationObserverId); - } else if (MutationObserverManager.unobserve) { - for (const target of this._observationTargets.keys()) { - nullthrows(MutationObserverManager.unobserve)( - mutationObserverId, - target, - ); - } - this._observationTargets.clear(); - } + MutationObserverManager.unobserveAll(mutationObserverId); MutationObserverManager.unregisterObserver(mutationObserverId); this._mutationObserverId = null; diff --git a/packages/react-native/src/private/webapis/mutationobserver/__tests__/MutationObserver-itest.js b/packages/react-native/src/private/webapis/mutationobserver/__tests__/MutationObserver-itest.js index af73dc71a71f7f..3358b837528f9e 100644 --- a/packages/react-native/src/private/webapis/mutationobserver/__tests__/MutationObserver-itest.js +++ b/packages/react-native/src/private/webapis/mutationobserver/__tests__/MutationObserver-itest.js @@ -17,7 +17,6 @@ import type MutationRecordType from 'react-native/src/private/webapis/mutationob import ensureInstance from '../../../__tests__/utilities/ensureInstance'; import {createShadowNodeReferenceCountingRef} from '../../../__tests__/utilities/ShadowNodeReferenceCounter'; -import NativeMutationObserver from '../specs/NativeMutationObserver'; import * as Fantom from '@react-native/fantom'; import nullthrows from 'nullthrows'; import * as React from 'react'; @@ -42,921 +41,891 @@ function ensureMutationRecordArray( ); } -const nativeUnobserveAll = nullthrows(NativeMutationObserver?.unobserveAll); +describe('MutationObserver', () => { + describe('constructor(callback)', () => { + it('should throw if `callback` is not provided', () => { + expect(() => { + // $FlowExpectedError[incompatible-call] + return new MutationObserver(); + }).toThrow( + "Failed to construct 'MutationObserver': 1 argument required, but only 0 present.", + ); + }); -[true, false].forEach(withUnobserveAll => { - describe(`MutationObserver (${withUnobserveAll ? 'with' : 'without'} NativeMutationObserver.unobserveAll)`, () => { - beforeAll(() => { - // $FlowExpectedError[incompatible-use] - // $FlowExpectedError[cannot-write] - NativeMutationObserver.unobserveAll = withUnobserveAll - ? nativeUnobserveAll - : null; + it('should throw if `callback` is not a function', () => { + expect(() => { + // $FlowExpectedError[incompatible-call] + return new MutationObserver('not a function!'); + }).toThrow( + "Failed to construct 'MutationObserver': parameter 1 is not of type 'Function'.", + ); }); + }); - it(`should ${withUnobserveAll ? 'have' : 'NOT have'} NativeMutationObserver.unobserveAll`, () => { - expect(NativeMutationObserver?.unobserveAll).toBe( - withUnobserveAll ? nativeUnobserveAll : null, + describe('observe(target, {childList: boolean, subtree: boolean})', () => { + it('should throw if `target` is not a `ReactNativeElement`', () => { + const observer = new MutationObserver(() => {}); + expect(() => { + // $FlowExpectedError[incompatible-call] + observer.observe('something'); + }).toThrow( + "Failed to execute 'observe' on 'MutationObserver': parameter 1 is not of type 'ReactNativeElement'.", ); }); - describe('constructor(callback)', () => { - it('should throw if `callback` is not provided', () => { - expect(() => { - // $FlowExpectedError[incompatible-call] - return new MutationObserver(); - }).toThrow( - "Failed to construct 'MutationObserver': 1 argument required, but only 0 present.", - ); + it('should throw if the `childList` option is not provided', () => { + const nodeRef = React.createRef(); + + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render(); }); - it('should throw if `callback` is not a function', () => { - expect(() => { - // $FlowExpectedError[incompatible-call] - return new MutationObserver('not a function!'); - }).toThrow( - "Failed to construct 'MutationObserver': parameter 1 is not of type 'Function'.", - ); + const node = ensureReactNativeElement(nodeRef.current); + + expect(() => { + const observer = new MutationObserver(() => {}); + observer.observe(node); + }).toThrow( + "Failed to execute 'observe' on 'MutationObserver': The options object must set 'childList' to true.", + ); + + expect(() => { + const observer = new MutationObserver(() => {}); + // $FlowExpectedError[incompatible-call] + observer.observe(node, {childList: false}); + }).toThrow( + "Failed to execute 'observe' on 'MutationObserver': The options object must set 'childList' to true.", + ); + + expect(() => { + const observer = new MutationObserver(() => {}); + observer.observe(node, {childList: true}); + }).not.toThrow(); + + expect(() => { + const observer = new MutationObserver(() => {}); + // $FlowExpectedError[incompatible-call] + observer.observe(node, {childList: 1}); + }).not.toThrow(); + }); + + it('should throw if the `attributes` option is provided', () => { + const nodeRef = React.createRef(); + + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render(); }); + + const node = ensureReactNativeElement(nodeRef.current); + + expect(() => { + const observer = new MutationObserver(() => {}); + observer.observe(node, {childList: true, attributes: true}); + }).toThrow( + "Failed to execute 'observe' on 'MutationObserver': attributes is not supported", + ); }); - describe('observe(target, {childList: boolean, subtree: boolean})', () => { - it('should throw if `target` is not a `ReactNativeElement`', () => { + it('should throw if the `attributeFilter` option is provided', () => { + const nodeRef = React.createRef(); + + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render(); + }); + + const node = ensureReactNativeElement(nodeRef.current); + + expect(() => { const observer = new MutationObserver(() => {}); - expect(() => { - // $FlowExpectedError[incompatible-call] - observer.observe('something'); - }).toThrow( - "Failed to execute 'observe' on 'MutationObserver': parameter 1 is not of type 'ReactNativeElement'.", - ); + observer.observe(node, {childList: true, attributeFilter: []}); + }).toThrow( + "Failed to execute 'observe' on 'MutationObserver': attributeFilter is not supported", + ); + }); + + it('should throw if the `attributeOldValue` option is provided', () => { + const nodeRef = React.createRef(); + + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render(); }); - it('should throw if the `childList` option is not provided', () => { - const nodeRef = React.createRef(); + const node = ensureReactNativeElement(nodeRef.current); - const root = Fantom.createRoot(); - Fantom.runTask(() => { - root.render(); - }); + expect(() => { + const observer = new MutationObserver(() => {}); + // $FlowExpected + observer.observe(node, {childList: true, attributeOldValue: true}); + }).toThrow( + "Failed to execute 'observe' on 'MutationObserver': attributeOldValue is not supported", + ); + }); - const node = ensureReactNativeElement(nodeRef.current); + it('should throw if the `characterData` option is provided', () => { + const nodeRef = React.createRef(); - expect(() => { - const observer = new MutationObserver(() => {}); - observer.observe(node); - }).toThrow( - "Failed to execute 'observe' on 'MutationObserver': The options object must set 'childList' to true.", - ); + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render(); + }); - expect(() => { - const observer = new MutationObserver(() => {}); - // $FlowExpectedError[incompatible-call] - observer.observe(node, {childList: false}); - }).toThrow( - "Failed to execute 'observe' on 'MutationObserver': The options object must set 'childList' to true.", - ); + const node = ensureReactNativeElement(nodeRef.current); - expect(() => { - const observer = new MutationObserver(() => {}); - observer.observe(node, {childList: true}); - }).not.toThrow(); + expect(() => { + const observer = new MutationObserver(() => {}); + observer.observe(node, {childList: true, characterData: true}); + }).toThrow( + "Failed to execute 'observe' on 'MutationObserver': characterData is not supported", + ); + }); - expect(() => { - const observer = new MutationObserver(() => {}); - // $FlowExpectedError[incompatible-call] - observer.observe(node, {childList: 1}); - }).not.toThrow(); + it('should throw if the `characterDataOldValue` option is provided', () => { + const nodeRef = React.createRef(); + + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render(); }); - it('should throw if the `attributes` option is provided', () => { - const nodeRef = React.createRef(); + const node = ensureReactNativeElement(nodeRef.current); - const root = Fantom.createRoot(); - Fantom.runTask(() => { - root.render(); + expect(() => { + const observer = new MutationObserver(() => {}); + observer.observe(node, { + childList: true, + characterDataOldValue: true, }); + }).toThrow( + "Failed to execute 'observe' on 'MutationObserver': characterDataOldValue is not supported", + ); + }); + + it('should ignore calls to observe disconnected targets', () => { + const nodeRef = React.createRef(); - const node = ensureReactNativeElement(nodeRef.current); + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render(); + }); + + const node = ensureReactNativeElement(nodeRef.current); + + Fantom.runTask(() => { + root.render(<>); + }); - expect(() => { - const observer = new MutationObserver(() => {}); - observer.observe(node, {childList: true, attributes: true}); - }).toThrow( - "Failed to execute 'observe' on 'MutationObserver': attributes is not supported", + expect(node.isConnected).toBe(false); + + const observerCallback = () => {}; + const observer = new MutationObserver(observerCallback); + + expect(() => { + observer.observe(node, {childList: true}); + }).not.toThrow(); + }); + + it('should report direct children added to and removed from an observed node (childList: true, subtree: false) ', () => { + const nodeRef = React.createRef(); + + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render(); + }); + + const node = ensureReactNativeElement(nodeRef.current); + + const observerCallbackCallArgs = []; + const observerCallback = (...args: $ReadOnlyArray) => { + observerCallbackCallArgs.push(args); + }; + const observer = new MutationObserver(observerCallback); + observer.observe(node, {childList: true}); + + // Does not report anything initially + expect(observerCallbackCallArgs.length).toBe(0); + + const childNode1Ref = React.createRef(); + const childNode2Ref = React.createRef(); + + Fantom.runTask(() => { + root.render( + + + + , ); }); - it('should throw if the `attributeFilter` option is provided', () => { - const nodeRef = React.createRef(); + const childNode1 = ensureReactNativeElement(childNode1Ref.current); + const childNode2 = ensureReactNativeElement(childNode2Ref.current); + + expect(observerCallbackCallArgs.length).toBe(1); + const firstCall = nullthrows(observerCallbackCallArgs.at(-1)); + expect(firstCall.length).toBe(2); + + const firstRecords = ensureMutationRecordArray(firstCall[0]); + expect(firstRecords).toBeInstanceOf(Array); + expect(firstRecords.length).toBe(1); + expect(firstRecords[0]).toBeInstanceOf(MutationRecord); + + const firstRecord = firstRecords[0]; + expect(firstRecord.type).toBe('childList'); + expect(firstRecord.target).toBe(node); + expect(firstRecord.addedNodes).toBeInstanceOf(NodeList); + expect(firstRecord.addedNodes[0]).toBe(childNode1); + expect(firstRecord.addedNodes[1]).toBe(childNode2); + expect(firstRecord.removedNodes).toBeInstanceOf(NodeList); + expect(firstRecord.removedNodes.length).toBe(0); + + expect(firstCall[1]).toBe(observer); + + Fantom.runTask(() => { + root.render( + + + , + ); + }); - const root = Fantom.createRoot(); - Fantom.runTask(() => { - root.render(); - }); + expect(observerCallbackCallArgs.length).toBe(2); + const secondCall = nullthrows(observerCallbackCallArgs.at(-1)); + expect(secondCall.length).toBe(2); + + const secondRecords = ensureMutationRecordArray(secondCall[0]); + expect(secondRecords.length).toBe(1); + expect(secondRecords[0]).toBeInstanceOf(MutationRecord); - const node = ensureReactNativeElement(nodeRef.current); + const secondRecord = secondRecords[0]; + expect(secondRecord.type).toBe('childList'); + expect(secondRecord.target).toBe(node); + expect(secondRecord.addedNodes).toBeInstanceOf(NodeList); + expect([...secondRecord.addedNodes]).toEqual([]); + expect(secondRecord.removedNodes).toBeInstanceOf(NodeList); + expect([...secondRecord.removedNodes]).toEqual([childNode1]); - expect(() => { - const observer = new MutationObserver(() => {}); - observer.observe(node, {childList: true, attributeFilter: []}); - }).toThrow( - "Failed to execute 'observe' on 'MutationObserver': attributeFilter is not supported", + expect(secondCall[1]).toBe(observer); + }); + + it('should NOT report changes in transitive children when `subtree` is not set to true', () => { + const observedNodeRef = React.createRef(); + + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render( + + + , ); }); - it('should throw if the `attributeOldValue` option is provided', () => { - const nodeRef = React.createRef(); + const observedNode = ensureReactNativeElement(observedNodeRef.current); - const root = Fantom.createRoot(); - Fantom.runTask(() => { - root.render(); - }); + const observerCallback = jest.fn(); + const observer = new MutationObserver(observerCallback); + observer.observe(observedNode, {childList: true}); - const node = ensureReactNativeElement(nodeRef.current); + // Does not report anything initially + expect(observerCallback).not.toHaveBeenCalled(); - expect(() => { - const observer = new MutationObserver(() => {}); - // $FlowExpected - observer.observe(node, {childList: true, attributeOldValue: true}); - }).toThrow( - "Failed to execute 'observe' on 'MutationObserver': attributeOldValue is not supported", + Fantom.runTask(() => { + root.render( + + + + + , ); }); - it('should throw if the `characterData` option is provided', () => { - const nodeRef = React.createRef(); + expect(observerCallback).not.toHaveBeenCalled(); - const root = Fantom.createRoot(); - Fantom.runTask(() => { - root.render(); - }); + Fantom.runTask(() => { + root.render( + + + , + ); + }); + + expect(observerCallback).not.toHaveBeenCalled(); + }); - const node = ensureReactNativeElement(nodeRef.current); + it('should report changes in transitive children when `subtree` is set to true', () => { + const nodeRef = React.createRef(); - expect(() => { - const observer = new MutationObserver(() => {}); - observer.observe(node, {childList: true, characterData: true}); - }).toThrow( - "Failed to execute 'observe' on 'MutationObserver': characterData is not supported", + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render( + + + , ); }); - it('should throw if the `characterDataOldValue` option is provided', () => { - const nodeRef = React.createRef(); + const node = ensureReactNativeElement(nodeRef.current); - const root = Fantom.createRoot(); - Fantom.runTask(() => { - root.render(); - }); + const observerCallback = jest.fn(); + const observer = new MutationObserver(observerCallback); + observer.observe(node, {childList: true, subtree: true}); - const node = ensureReactNativeElement(nodeRef.current); + // Does not report anything initially + expect(observerCallback).not.toHaveBeenCalled(); - expect(() => { - const observer = new MutationObserver(() => {}); - observer.observe(node, { - childList: true, - characterDataOldValue: true, - }); - }).toThrow( - "Failed to execute 'observe' on 'MutationObserver': characterDataOldValue is not supported", + const node111Ref = React.createRef(); + + Fantom.runTask(() => { + root.render( + + + + + , ); }); - it('should ignore calls to observe disconnected targets', () => { - const nodeRef = React.createRef(); + const node111 = ensureReactNativeElement(node111Ref.current); - const root = Fantom.createRoot(); - Fantom.runTask(() => { - root.render(); - }); + expect(observerCallback).toHaveBeenCalledTimes(1); + const firstCall = observerCallback.mock.lastCall; + const firstRecords = ensureMutationRecordArray(firstCall[0]); + expect(firstRecords.length).toBe(1); + expect([...firstRecords[0].addedNodes]).toEqual([node111]); + expect([...firstRecords[0].removedNodes]).toEqual([]); - const node = ensureReactNativeElement(nodeRef.current); + Fantom.runTask(() => { + root.render( + + + , + ); + }); - Fantom.runTask(() => { - root.render(<>); - }); + expect(observerCallback).toHaveBeenCalledTimes(2); + const secondRecords = ensureMutationRecordArray( + observerCallback.mock.lastCall[0], + ); + expect(secondRecords.length).toBe(1); + expect([...secondRecords[0].addedNodes]).toEqual([]); + expect([...secondRecords[0].removedNodes]).toEqual([node111]); + }); - expect(node.isConnected).toBe(false); + it('should report changes in different parts of the subtree as separate entries (subtree = true)', () => { + const nodeRef = React.createRef(); - const observerCallback = () => {}; - const observer = new MutationObserver(observerCallback); + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render( + + + + , + ); + }); + + const node = ensureReactNativeElement(nodeRef.current); + + const observerCallback = jest.fn(); + const observer = new MutationObserver(observerCallback); + observer.observe(node, {childList: true, subtree: true}); + + // Does not report anything initially + expect(observerCallback).not.toHaveBeenCalled(); - expect(() => { - observer.observe(node, {childList: true}); - }).not.toThrow(); + const node111Ref = React.createRef(); + const node121Ref = React.createRef(); + + Fantom.runTask(() => { + root.render( + + + + + + + + , + ); }); - it('should report direct children added to and removed from an observed node (childList: true, subtree: false) ', () => { - const nodeRef = React.createRef(); + const node111 = ensureReactNativeElement(node111Ref.current); + const node121 = ensureReactNativeElement(node121Ref.current); + + expect(observerCallback).toHaveBeenCalledTimes(1); + const firstCall = observerCallback.mock.lastCall; + const firstRecords = ensureMutationRecordArray(firstCall[0]); + expect(firstRecords.length).toBe(2); + expect([...firstRecords[0].addedNodes]).toEqual([node111]); + expect([...firstRecords[0].removedNodes]).toEqual([]); + expect([...firstRecords[1].addedNodes]).toEqual([node121]); + expect([...firstRecords[1].removedNodes]).toEqual([]); + + Fantom.runTask(() => { + root.render( + + + + , + ); + }); + + expect(observerCallback).toHaveBeenCalledTimes(2); + const secondCall = observerCallback.mock.lastCall; + const secondRecords = ensureMutationRecordArray(secondCall[0]); + expect(secondRecords.length).toBe(2); + expect([...secondRecords[0].addedNodes]).toEqual([]); + expect([...secondRecords[0].removedNodes]).toEqual([node111]); + expect([...secondRecords[1].addedNodes]).toEqual([]); + expect([...secondRecords[1].removedNodes]).toEqual([node121]); + + expect(secondCall[1]).toBe(observer); + }); + + describe('multiple observers', () => { + it('should report changes to multiple observers observing different subtrees', () => { + const node1Ref = React.createRef(); + const node2Ref = React.createRef(); const root = Fantom.createRoot(); Fantom.runTask(() => { - root.render(); + root.render( + <> + + + , + ); }); - const node = ensureReactNativeElement(nodeRef.current); + const node1 = ensureReactNativeElement(node1Ref.current); + const node2 = ensureReactNativeElement(node2Ref.current); - const observerCallbackCallArgs = []; - const observerCallback = (...args: $ReadOnlyArray) => { - observerCallbackCallArgs.push(args); - }; - const observer = new MutationObserver(observerCallback); - observer.observe(node, {childList: true}); + const observerCallback1 = jest.fn(); + const observer1 = new MutationObserver(observerCallback1); + observer1.observe(node1, {childList: true, subtree: true}); + + const observerCallback2 = jest.fn(); + const observer2 = new MutationObserver(observerCallback2); + observer2.observe(node2, {childList: true, subtree: true}); // Does not report anything initially - expect(observerCallbackCallArgs.length).toBe(0); + expect(observerCallback1).not.toHaveBeenCalled(); + expect(observerCallback2).not.toHaveBeenCalled(); - const childNode1Ref = React.createRef(); - const childNode2Ref = React.createRef(); + const childNode11Ref = React.createRef(); + const childNode21Ref = React.createRef(); Fantom.runTask(() => { root.render( - - - - , + <> + + + + + + + , ); }); - const childNode1 = ensureReactNativeElement(childNode1Ref.current); - const childNode2 = ensureReactNativeElement(childNode2Ref.current); - - expect(observerCallbackCallArgs.length).toBe(1); - const firstCall = nullthrows(observerCallbackCallArgs.at(-1)); - expect(firstCall.length).toBe(2); + const childNode11 = ensureReactNativeElement(childNode11Ref.current); + const childNode21 = ensureReactNativeElement(childNode21Ref.current); - const firstRecords = ensureMutationRecordArray(firstCall[0]); - expect(firstRecords).toBeInstanceOf(Array); - expect(firstRecords.length).toBe(1); - expect(firstRecords[0]).toBeInstanceOf(MutationRecord); - - const firstRecord = firstRecords[0]; - expect(firstRecord.type).toBe('childList'); - expect(firstRecord.target).toBe(node); - expect(firstRecord.addedNodes).toBeInstanceOf(NodeList); - expect(firstRecord.addedNodes[0]).toBe(childNode1); - expect(firstRecord.addedNodes[1]).toBe(childNode2); - expect(firstRecord.removedNodes).toBeInstanceOf(NodeList); - expect(firstRecord.removedNodes.length).toBe(0); + expect(observerCallback1).toHaveBeenCalledTimes(1); + const observer1Records1 = ensureMutationRecordArray( + observerCallback1.mock.lastCall[0], + ); + expect(observer1Records1.length).toBe(1); + expect([...observer1Records1[0].addedNodes]).toEqual([childNode11]); + expect([...observer1Records1[0].removedNodes]).toEqual([]); - expect(firstCall[1]).toBe(observer); + expect(observerCallback2).toHaveBeenCalledTimes(1); + const observer2Records1 = ensureMutationRecordArray( + observerCallback2.mock.lastCall[0], + ); + expect(observer2Records1.length).toBe(1); + expect([...observer2Records1[0].addedNodes]).toEqual([childNode21]); + expect([...observer2Records1[0].removedNodes]).toEqual([]); Fantom.runTask(() => { root.render( - - - , + <> + + + , ); }); - expect(observerCallbackCallArgs.length).toBe(2); - const secondCall = nullthrows(observerCallbackCallArgs.at(-1)); - expect(secondCall.length).toBe(2); - - const secondRecords = ensureMutationRecordArray(secondCall[0]); - expect(secondRecords.length).toBe(1); - expect(secondRecords[0]).toBeInstanceOf(MutationRecord); - - const secondRecord = secondRecords[0]; - expect(secondRecord.type).toBe('childList'); - expect(secondRecord.target).toBe(node); - expect(secondRecord.addedNodes).toBeInstanceOf(NodeList); - expect([...secondRecord.addedNodes]).toEqual([]); - expect(secondRecord.removedNodes).toBeInstanceOf(NodeList); - expect([...secondRecord.removedNodes]).toEqual([childNode1]); + expect(observerCallback1).toHaveBeenCalledTimes(2); + const observer1Records2 = ensureMutationRecordArray( + observerCallback1.mock.lastCall[0], + ); + expect(observer1Records2.length).toBe(1); + expect([...observer1Records2[0].addedNodes]).toEqual([]); + expect([...observer1Records2[0].removedNodes]).toEqual([childNode11]); - expect(secondCall[1]).toBe(observer); + expect(observerCallback2).toHaveBeenCalledTimes(2); + const observer2Records2 = ensureMutationRecordArray( + observerCallback2.mock.lastCall[0], + ); + expect(observer2Records2.length).toBe(1); + expect([...observer2Records2[0].addedNodes]).toEqual([]); + expect([...observer2Records2[0].removedNodes]).toEqual([childNode21]); }); - it('should NOT report changes in transitive children when `subtree` is not set to true', () => { - const observedNodeRef = React.createRef(); + it('should report changes to multiple observers observing the same subtree', () => { + const node1Ref = React.createRef(); + const node2Ref = React.createRef(); const root = Fantom.createRoot(); Fantom.runTask(() => { root.render( - - + + , ); }); - const observedNode = ensureReactNativeElement(observedNodeRef.current); + const node1 = ensureReactNativeElement(node1Ref.current); + const node2 = ensureReactNativeElement(node2Ref.current); - const observerCallback = jest.fn(); - const observer = new MutationObserver(observerCallback); - observer.observe(observedNode, {childList: true}); + const observerCallback1 = jest.fn(); + const observer1 = new MutationObserver(observerCallback1); + observer1.observe(node1, {childList: true, subtree: true}); + + const observerCallback2 = jest.fn(); + const observer2 = new MutationObserver(observerCallback2); + observer2.observe(node2, {childList: true, subtree: true}); // Does not report anything initially - expect(observerCallback).not.toHaveBeenCalled(); + expect(observerCallback1).not.toHaveBeenCalled(); + expect(observerCallback2).not.toHaveBeenCalled(); + + const childNode111Ref = React.createRef(); Fantom.runTask(() => { root.render( - + , ); }); - expect(observerCallback).not.toHaveBeenCalled(); + const childNode111 = ensureReactNativeElement(childNode111Ref.current); + + expect(observerCallback1).toHaveBeenCalledTimes(1); + const observer1Records1 = ensureMutationRecordArray( + observerCallback1.mock.lastCall[0], + ); + expect(observer1Records1.length).toBe(1); + expect([...observer1Records1[0].addedNodes]).toEqual([childNode111]); + expect([...observer1Records1[0].removedNodes]).toEqual([]); + expect(observerCallback2).toHaveBeenCalledTimes(1); + const observer2Records1 = ensureMutationRecordArray( + observerCallback2.mock.lastCall[0], + ); + expect(observer2Records1.length).toBe(1); + expect([...observer2Records1[0].addedNodes]).toEqual([childNode111]); + expect([...observer2Records1[0].removedNodes]).toEqual([]); Fantom.runTask(() => { root.render( - - - , + <> + + + + , ); }); - expect(observerCallback).not.toHaveBeenCalled(); + expect(observerCallback1).toHaveBeenCalledTimes(2); + const observer1Records2 = ensureMutationRecordArray( + observerCallback1.mock.lastCall[0], + ); + expect(observer1Records2.length).toBe(1); + expect([...observer1Records2[0].addedNodes]).toEqual([]); + expect([...observer1Records2[0].removedNodes]).toEqual([childNode111]); + + expect(observerCallback2).toHaveBeenCalledTimes(2); + const observer2Records2 = ensureMutationRecordArray( + observerCallback2.mock.lastCall[0], + ); + expect(observer2Records2.length).toBe(1); + expect([...observer2Records2[0].addedNodes]).toEqual([]); + expect([...observer2Records2[0].removedNodes]).toEqual([childNode111]); }); + }); - it('should report changes in transitive children when `subtree` is set to true', () => { - const nodeRef = React.createRef(); + describe('multiple observed nodes in the same observer', () => { + it('should report changes in disjoint observations', () => { + const node1Ref = React.createRef(); + const node2Ref = React.createRef(); const root = Fantom.createRoot(); Fantom.runTask(() => { root.render( - - - , + <> + + + , ); }); - const node = ensureReactNativeElement(nodeRef.current); + const node1 = ensureReactNativeElement(node1Ref.current); + const node2 = ensureReactNativeElement(node2Ref.current); const observerCallback = jest.fn(); const observer = new MutationObserver(observerCallback); - observer.observe(node, {childList: true, subtree: true}); + observer.observe(node1, {childList: true, subtree: true}); + observer.observe(node2, {childList: true, subtree: true}); // Does not report anything initially expect(observerCallback).not.toHaveBeenCalled(); - const node111Ref = React.createRef(); + const childNode11Ref = React.createRef(); + const childNode21Ref = React.createRef(); Fantom.runTask(() => { root.render( - - - + <> + + - , + + + + , ); }); - const node111 = ensureReactNativeElement(node111Ref.current); + const childNode11 = ensureReactNativeElement(childNode11Ref.current); + const childNode21 = ensureReactNativeElement(childNode21Ref.current); expect(observerCallback).toHaveBeenCalledTimes(1); - const firstCall = observerCallback.mock.lastCall; - const firstRecords = ensureMutationRecordArray(firstCall[0]); - expect(firstRecords.length).toBe(1); - expect([...firstRecords[0].addedNodes]).toEqual([node111]); - expect([...firstRecords[0].removedNodes]).toEqual([]); + const records = ensureMutationRecordArray( + observerCallback.mock.lastCall[0], + ); + expect(records.length).toBe(2); + expect([...records[0].addedNodes]).toEqual([childNode11]); + expect([...records[0].removedNodes]).toEqual([]); + expect([...records[1].addedNodes]).toEqual([childNode21]); + expect([...records[1].removedNodes]).toEqual([]); Fantom.runTask(() => { root.render( - - - , + <> + + + , ); }); expect(observerCallback).toHaveBeenCalledTimes(2); - const secondRecords = ensureMutationRecordArray( + const records2 = ensureMutationRecordArray( observerCallback.mock.lastCall[0], ); - expect(secondRecords.length).toBe(1); - expect([...secondRecords[0].addedNodes]).toEqual([]); - expect([...secondRecords[0].removedNodes]).toEqual([node111]); + expect(records2.length).toBe(2); + expect([...records2[0].addedNodes]).toEqual([]); + expect([...records2[0].removedNodes]).toEqual([childNode11]); + expect([...records2[1].addedNodes]).toEqual([]); + expect([...records2[1].removedNodes]).toEqual([childNode21]); }); - it('should report changes in different parts of the subtree as separate entries (subtree = true)', () => { - const nodeRef = React.createRef(); + it('should report changes in joint observations', () => { + const node1Ref = React.createRef(); + const node11Ref = React.createRef(); const root = Fantom.createRoot(); Fantom.runTask(() => { root.render( - - - + + , ); }); - const node = ensureReactNativeElement(nodeRef.current); + const node1 = ensureReactNativeElement(node1Ref.current); + const node11 = ensureReactNativeElement(node11Ref.current); const observerCallback = jest.fn(); const observer = new MutationObserver(observerCallback); - observer.observe(node, {childList: true, subtree: true}); + observer.observe(node1, {childList: true, subtree: true}); + observer.observe(node11, {childList: true, subtree: true}); // Does not report anything initially expect(observerCallback).not.toHaveBeenCalled(); - const node111Ref = React.createRef(); - const node121Ref = React.createRef(); + const childNode111Ref = React.createRef(); Fantom.runTask(() => { root.render( - - - - + , ); }); - const node111 = ensureReactNativeElement(node111Ref.current); - const node121 = ensureReactNativeElement(node121Ref.current); + const childNode111 = ensureReactNativeElement(childNode111Ref.current); expect(observerCallback).toHaveBeenCalledTimes(1); - const firstCall = observerCallback.mock.lastCall; - const firstRecords = ensureMutationRecordArray(firstCall[0]); - expect(firstRecords.length).toBe(2); - expect([...firstRecords[0].addedNodes]).toEqual([node111]); - expect([...firstRecords[0].removedNodes]).toEqual([]); - expect([...firstRecords[1].addedNodes]).toEqual([node121]); - expect([...firstRecords[1].removedNodes]).toEqual([]); + const records = ensureMutationRecordArray( + observerCallback.mock.lastCall[0], + ); + expect(records.length).toBe(1); + expect([...records[0].addedNodes]).toEqual([childNode111]); + expect([...records[0].removedNodes]).toEqual([]); Fantom.runTask(() => { root.render( - , ); }); expect(observerCallback).toHaveBeenCalledTimes(2); - const secondCall = observerCallback.mock.lastCall; - const secondRecords = ensureMutationRecordArray(secondCall[0]); - expect(secondRecords.length).toBe(2); - expect([...secondRecords[0].addedNodes]).toEqual([]); - expect([...secondRecords[0].removedNodes]).toEqual([node111]); - expect([...secondRecords[1].addedNodes]).toEqual([]); - expect([...secondRecords[1].removedNodes]).toEqual([node121]); - - expect(secondCall[1]).toBe(observer); - }); - - describe('multiple observers', () => { - it('should report changes to multiple observers observing different subtrees', () => { - const node1Ref = React.createRef(); - const node2Ref = React.createRef(); - - const root = Fantom.createRoot(); - Fantom.runTask(() => { - root.render( - <> - - - , - ); - }); - - const node1 = ensureReactNativeElement(node1Ref.current); - const node2 = ensureReactNativeElement(node2Ref.current); - - const observerCallback1 = jest.fn(); - const observer1 = new MutationObserver(observerCallback1); - observer1.observe(node1, {childList: true, subtree: true}); - - const observerCallback2 = jest.fn(); - const observer2 = new MutationObserver(observerCallback2); - observer2.observe(node2, {childList: true, subtree: true}); - - // Does not report anything initially - expect(observerCallback1).not.toHaveBeenCalled(); - expect(observerCallback2).not.toHaveBeenCalled(); - - const childNode11Ref = React.createRef(); - const childNode21Ref = React.createRef(); - - Fantom.runTask(() => { - root.render( - <> - - - - - - - , - ); - }); - - const childNode11 = ensureReactNativeElement(childNode11Ref.current); - const childNode21 = ensureReactNativeElement(childNode21Ref.current); + const records2 = ensureMutationRecordArray( + observerCallback.mock.lastCall[0], + ); + expect(records2.length).toBe(1); + expect([...records2[0].addedNodes]).toEqual([]); + expect([...records2[0].removedNodes]).toEqual([childNode111]); + }); + }); - expect(observerCallback1).toHaveBeenCalledTimes(1); - const observer1Records1 = ensureMutationRecordArray( - observerCallback1.mock.lastCall[0], - ); - expect(observer1Records1.length).toBe(1); - expect([...observer1Records1[0].addedNodes]).toEqual([childNode11]); - expect([...observer1Records1[0].removedNodes]).toEqual([]); + describe('memory handling', () => { + it('should not retain initial children of observed targets', () => { + const root = Fantom.createRoot(); + const observer = new MutationObserver(() => {}); - expect(observerCallback2).toHaveBeenCalledTimes(1); - const observer2Records1 = ensureMutationRecordArray( - observerCallback2.mock.lastCall[0], - ); - expect(observer2Records1.length).toBe(1); - expect([...observer2Records1[0].addedNodes]).toEqual([childNode21]); - expect([...observer2Records1[0].removedNodes]).toEqual([]); - - Fantom.runTask(() => { - root.render( - <> - - - , - ); - }); + const [getReferenceCount, childRef] = + createShadowNodeReferenceCountingRef(); - expect(observerCallback1).toHaveBeenCalledTimes(2); - const observer1Records2 = ensureMutationRecordArray( - observerCallback1.mock.lastCall[0], - ); - expect(observer1Records2.length).toBe(1); - expect([...observer1Records2[0].addedNodes]).toEqual([]); - expect([...observer1Records2[0].removedNodes]).toEqual([childNode11]); + const parentRef = React.createRef(); - expect(observerCallback2).toHaveBeenCalledTimes(2); - const observer2Records2 = ensureMutationRecordArray( - observerCallback2.mock.lastCall[0], + Fantom.runTask(() => { + root.render( + + + , ); - expect(observer2Records2.length).toBe(1); - expect([...observer2Records2[0].addedNodes]).toEqual([]); - expect([...observer2Records2[0].removedNodes]).toEqual([childNode21]); }); - it('should report changes to multiple observers observing the same subtree', () => { - const node1Ref = React.createRef(); - const node2Ref = React.createRef(); - - const root = Fantom.createRoot(); - Fantom.runTask(() => { - root.render( - - - , - ); - }); - - const node1 = ensureReactNativeElement(node1Ref.current); - const node2 = ensureReactNativeElement(node2Ref.current); - - const observerCallback1 = jest.fn(); - const observer1 = new MutationObserver(observerCallback1); - observer1.observe(node1, {childList: true, subtree: true}); - - const observerCallback2 = jest.fn(); - const observer2 = new MutationObserver(observerCallback2); - observer2.observe(node2, {childList: true, subtree: true}); - - // Does not report anything initially - expect(observerCallback1).not.toHaveBeenCalled(); - expect(observerCallback2).not.toHaveBeenCalled(); - - const childNode111Ref = React.createRef(); - - Fantom.runTask(() => { - root.render( - - - - - , - ); - }); - - const childNode111 = ensureReactNativeElement( - childNode111Ref.current, - ); - - expect(observerCallback1).toHaveBeenCalledTimes(1); - const observer1Records1 = ensureMutationRecordArray( - observerCallback1.mock.lastCall[0], - ); - expect(observer1Records1.length).toBe(1); - expect([...observer1Records1[0].addedNodes]).toEqual([childNode111]); - expect([...observer1Records1[0].removedNodes]).toEqual([]); - expect(observerCallback2).toHaveBeenCalledTimes(1); - const observer2Records1 = ensureMutationRecordArray( - observerCallback2.mock.lastCall[0], - ); - expect(observer2Records1.length).toBe(1); - expect([...observer2Records1[0].addedNodes]).toEqual([childNode111]); - expect([...observer2Records1[0].removedNodes]).toEqual([]); - - Fantom.runTask(() => { - root.render( - <> - - - - , - ); + Fantom.runTask(() => { + observer.observe(ensureReactNativeElement(parentRef.current), { + childList: true, }); - - expect(observerCallback1).toHaveBeenCalledTimes(2); - const observer1Records2 = ensureMutationRecordArray( - observerCallback1.mock.lastCall[0], - ); - expect(observer1Records2.length).toBe(1); - expect([...observer1Records2[0].addedNodes]).toEqual([]); - expect([...observer1Records2[0].removedNodes]).toEqual([ - childNode111, - ]); - - expect(observerCallback2).toHaveBeenCalledTimes(2); - const observer2Records2 = ensureMutationRecordArray( - observerCallback2.mock.lastCall[0], - ); - expect(observer2Records2.length).toBe(1); - expect([...observer2Records2[0].addedNodes]).toEqual([]); - expect([...observer2Records2[0].removedNodes]).toEqual([ - childNode111, - ]); }); - }); - - describe('multiple observed nodes in the same observer', () => { - it('should report changes in disjoint observations', () => { - const node1Ref = React.createRef(); - const node2Ref = React.createRef(); - - const root = Fantom.createRoot(); - Fantom.runTask(() => { - root.render( - <> - - - , - ); - }); - - const node1 = ensureReactNativeElement(node1Ref.current); - const node2 = ensureReactNativeElement(node2Ref.current); - - const observerCallback = jest.fn(); - const observer = new MutationObserver(observerCallback); - observer.observe(node1, {childList: true, subtree: true}); - observer.observe(node2, {childList: true, subtree: true}); - - // Does not report anything initially - expect(observerCallback).not.toHaveBeenCalled(); - - const childNode11Ref = React.createRef(); - const childNode21Ref = React.createRef(); - - Fantom.runTask(() => { - root.render( - <> - - - - - - - , - ); - }); - const childNode11 = ensureReactNativeElement(childNode11Ref.current); - const childNode21 = ensureReactNativeElement(childNode21Ref.current); + expect(getReferenceCount()).toBeGreaterThan(0); - expect(observerCallback).toHaveBeenCalledTimes(1); - const records = ensureMutationRecordArray( - observerCallback.mock.lastCall[0], - ); - expect(records.length).toBe(2); - expect([...records[0].addedNodes]).toEqual([childNode11]); - expect([...records[0].removedNodes]).toEqual([]); - expect([...records[1].addedNodes]).toEqual([childNode21]); - expect([...records[1].removedNodes]).toEqual([]); - - Fantom.runTask(() => { - root.render( - <> - - - , - ); - }); - - expect(observerCallback).toHaveBeenCalledTimes(2); - const records2 = ensureMutationRecordArray( - observerCallback.mock.lastCall[0], - ); - expect(records2.length).toBe(2); - expect([...records2[0].addedNodes]).toEqual([]); - expect([...records2[0].removedNodes]).toEqual([childNode11]); - expect([...records2[1].addedNodes]).toEqual([]); - expect([...records2[1].removedNodes]).toEqual([childNode21]); + Fantom.runTask(() => { + root.render(); }); - it('should report changes in joint observations', () => { - const node1Ref = React.createRef(); - const node11Ref = React.createRef(); - - const root = Fantom.createRoot(); - Fantom.runTask(() => { - root.render( - - - , - ); - }); - - const node1 = ensureReactNativeElement(node1Ref.current); - const node11 = ensureReactNativeElement(node11Ref.current); - - const observerCallback = jest.fn(); - const observer = new MutationObserver(observerCallback); - observer.observe(node1, {childList: true, subtree: true}); - observer.observe(node11, {childList: true, subtree: true}); - - // Does not report anything initially - expect(observerCallback).not.toHaveBeenCalled(); + expect(getReferenceCount()).toBe(0); - const childNode111Ref = React.createRef(); - - Fantom.runTask(() => { - root.render( - - - - - , - ); - }); - - const childNode111 = ensureReactNativeElement( - childNode111Ref.current, - ); - - expect(observerCallback).toHaveBeenCalledTimes(1); - const records = ensureMutationRecordArray( - observerCallback.mock.lastCall[0], - ); - expect(records.length).toBe(1); - expect([...records[0].addedNodes]).toEqual([childNode111]); - expect([...records[0].removedNodes]).toEqual([]); + observer.disconnect(); + }); + }); + }); - Fantom.runTask(() => { - root.render( - - - , - ); - }); + describe('disconnect()', () => { + it('should stop observing targets', () => { + const observedNodeRef = React.createRef(); - expect(observerCallback).toHaveBeenCalledTimes(2); - const records2 = ensureMutationRecordArray( - observerCallback.mock.lastCall[0], - ); - expect(records2.length).toBe(1); - expect([...records2[0].addedNodes]).toEqual([]); - expect([...records2[0].removedNodes]).toEqual([childNode111]); - }); + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render(); }); - if (withUnobserveAll) { - describe('memory handling', () => { - it('should not retain initial children of observed targets', () => { - const root = Fantom.createRoot(); - const observer = new MutationObserver(() => {}); - - const [getReferenceCount, childRef] = - createShadowNodeReferenceCountingRef(); + const observedNode = ensureReactNativeElement(observedNodeRef.current); - const parentRef = React.createRef(); + const observerCallback = jest.fn(); + const observer = new MutationObserver(observerCallback); + observer.observe(observedNode, {childList: true}); - Fantom.runTask(() => { - root.render( - - - , - ); - }); + // Does not report anything initially + expect(observerCallback).not.toHaveBeenCalled(); - Fantom.runTask(() => { - observer.observe(ensureReactNativeElement(parentRef.current), { - childList: true, - }); - }); + Fantom.runTask(() => { + root.render( + + + + , + ); + }); - expect(getReferenceCount()).toBeGreaterThan(0); + expect(observerCallback).toHaveBeenCalledTimes(1); - // This updates the working tree in the reconciler - Fantom.runTask(() => { - // Set style to force a state update - root.render(); - }); + observer.disconnect(); - expect(getReferenceCount()).toBe(0); + Fantom.runTask(() => { + root.render( + + + , + ); + }); - observer.disconnect(); - }); - }); - } + expect(observerCallback).toHaveBeenCalledTimes(1); }); - describe('disconnect()', () => { - it('should stop observing targets', () => { - const observedNodeRef = React.createRef(); + it('should correctly unobserve targets that are disconnected after observing', () => { + const observedNodeRef = React.createRef(); - const root = Fantom.createRoot(); - Fantom.runTask(() => { - root.render(); - }); - - const observedNode = ensureReactNativeElement(observedNodeRef.current); + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render(); + }); - const observerCallback = jest.fn(); - const observer = new MutationObserver(observerCallback); - observer.observe(observedNode, {childList: true}); + const observedNode = ensureReactNativeElement(observedNodeRef.current); - // Does not report anything initially - expect(observerCallback).not.toHaveBeenCalled(); + const observerCallback = jest.fn(); + const observer = new MutationObserver(observerCallback); + observer.observe(observedNode, {childList: true}); - Fantom.runTask(() => { - root.render( - - - - , - ); - }); + Fantom.runTask(() => { + root.render(<>); + }); - expect(observerCallback).toHaveBeenCalledTimes(1); + expect(observedNode.isConnected).toBe(false); + expect(() => { observer.disconnect(); + }).not.toThrow(); + }); - Fantom.runTask(() => { - root.render( - - - , - ); - }); + it('should correctly unobserve targets that are disconnected before observing', () => { + const observedNodeRef = React.createRef(); - expect(observerCallback).toHaveBeenCalledTimes(1); + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render(); }); - it('should correctly unobserve targets that are disconnected after observing', () => { - const observedNodeRef = React.createRef(); + const observedNode = ensureReactNativeElement(observedNodeRef.current); - const root = Fantom.createRoot(); - Fantom.runTask(() => { - root.render(); - }); - - const observedNode = ensureReactNativeElement(observedNodeRef.current); - - const observerCallback = jest.fn(); - const observer = new MutationObserver(observerCallback); - observer.observe(observedNode, {childList: true}); - - Fantom.runTask(() => { - root.render(<>); - }); - - expect(observedNode.isConnected).toBe(false); - - expect(() => { - observer.disconnect(); - }).not.toThrow(); + Fantom.runTask(() => { + root.render(<>); }); - it('should correctly unobserve targets that are disconnected before observing', () => { - const observedNodeRef = React.createRef(); - - const root = Fantom.createRoot(); - Fantom.runTask(() => { - root.render(); - }); - - const observedNode = ensureReactNativeElement(observedNodeRef.current); + expect(observedNode.isConnected).toBe(false); - Fantom.runTask(() => { - root.render(<>); - }); + const observerCallback = jest.fn(); + const observer = new MutationObserver(observerCallback); + observer.observe(observedNode, {childList: true}); - expect(observedNode.isConnected).toBe(false); - - const observerCallback = jest.fn(); - const observer = new MutationObserver(observerCallback); - observer.observe(observedNode, {childList: true}); - - expect(() => { - observer.disconnect(); - }).not.toThrow(); - }); + expect(() => { + observer.disconnect(); + }).not.toThrow(); }); }); }); diff --git a/packages/react-native/src/private/webapis/mutationobserver/internals/MutationObserverManager.js b/packages/react-native/src/private/webapis/mutationobserver/internals/MutationObserverManager.js index 929485a2e826bf..b74f420f02020b 100644 --- a/packages/react-native/src/private/webapis/mutationobserver/internals/MutationObserverManager.js +++ b/packages/react-native/src/private/webapis/mutationobserver/internals/MutationObserverManager.js @@ -41,14 +41,6 @@ const registeredMutationObservers: Map< $ReadOnly<{observer: MutationObserver, callback: MutationObserverCallback}>, > = new Map(); -// The mapping between ReactNativeElement and their corresponding shadow node -// needs to be kept here because React removes the link when unmounting. -// TODO: remove this code when NativeMutationObserver.unobserveAll is available in all apps -const targetToShadowNodeMap: WeakMap< - ReactNativeElement, - ReturnType, -> = new WeakMap(); - /** * Registers the given mutation observer and returns a unique ID for it, * which is required to start observing targets. @@ -91,10 +83,10 @@ export function observe({ mutationObserverId: MutationObserverId, target: ReactNativeElement, subtree: boolean, -}): boolean { +}): void { if (NativeMutationObserver == null) { warnNoNativeMutationObserver(); - return false; + return; } const registeredObserver = @@ -103,19 +95,13 @@ export function observe({ console.error( `MutationObserverManager: could not start observing target because MutationObserver with ID ${mutationObserverId} was not registered.`, ); - return false; + return; } const targetShadowNode = getNativeNodeReference(target); if (targetShadowNode == null) { // The target is disconnected. We can't observe it anymore. - return false; - } - - // We need to keep this temporarily until the changes in the native module have propagated. - // After that, we don't need to keep this mapping that can cause memory leaks. - if (!nativeUnobserveAll) { - targetToShadowNodeMap.set(target, targetShadowNode); + return; } if (!isConnected) { @@ -136,65 +122,25 @@ export function observe({ targetShadowNode, subtree, }); - - return true; } -const nativeUnobserve = NativeMutationObserver?.unobserve; - -// TODO: delete in the next version, when NativeMutationObserver.unobserveAll is available in all apps -export const unobserve: ?( - mutationObserverId: number, - target: ReactNativeElement, -) => void = nativeUnobserve - ? function unobserve( - mutationObserverId: number, - target: ReactNativeElement, - ): void { - if (NativeMutationObserver == null) { - warnNoNativeMutationObserver(); - return; - } - - const registeredObserver = - registeredMutationObservers.get(mutationObserverId); - if (registeredObserver == null) { - console.error( - `MutationObserverManager: could not stop observing target because MutationObserver with ID ${mutationObserverId} was not registered.`, - ); - return; - } - - const targetShadowNode = targetToShadowNodeMap.get(target); - if (targetShadowNode == null) { - console.error( - 'MutationObserverManager: could not find registration data for target', - ); - return; - } - - nativeUnobserve(mutationObserverId, targetShadowNode); - } - : null; - -const nativeUnobserveAll = NativeMutationObserver?.unobserveAll; +export function unobserveAll(mutationObserverId: number): void { + if (NativeMutationObserver == null) { + warnNoNativeMutationObserver(); + return; + } -// TODO: clean up as a regular export in the next version, when NativeMutationObserver.unobserveAll is available in all apps -export const unobserveAll: ?(mutationObserverId: number) => void = - nativeUnobserveAll - ? function unobserveAll(mutationObserverId: MutationObserverId): void { - const registeredObserver = - registeredMutationObservers.get(mutationObserverId); - if (registeredObserver == null) { - console.error( - `MutationObserverManager: could not stop observing target because MutationObserver with ID ${mutationObserverId} was not registered.`, - ); - return; - } + const registeredObserver = + registeredMutationObservers.get(mutationObserverId); + if (registeredObserver == null) { + console.error( + `MutationObserverManager: could not disconnect MutationObserver with ID ${mutationObserverId} because it was not registered.`, + ); + return; + } - nativeUnobserveAll(mutationObserverId); - } - : null; + NativeMutationObserver.unobserveAll(mutationObserverId); +} /** * This function is called from native when there are `MutationObserver` diff --git a/packages/react-native/src/private/webapis/mutationobserver/specs/NativeMutationObserver.js b/packages/react-native/src/private/webapis/mutationobserver/specs/NativeMutationObserver.js index a16f730c6ec743..c4fbcb2517e8d2 100644 --- a/packages/react-native/src/private/webapis/mutationobserver/specs/NativeMutationObserver.js +++ b/packages/react-native/src/private/webapis/mutationobserver/specs/NativeMutationObserver.js @@ -36,13 +36,7 @@ export type NativeMutationObserverObserveOptions = { export interface Spec extends TurboModule { +observe: (options: NativeMutationObserverObserveOptions) => void; - // TODO: remove in the next version - +unobserve?: ( - mutationObserverId: number, - targetShadowNode: ShadowNode, - ) => void; - // TODO: remove optionality in the next version - +unobserveAll?: (mutationObserverId: number) => void; + +unobserveAll: (mutationObserverId: number) => void; +connect: ( notifyMutationObservers: () => void, // We need this to retain the public instance before React removes the