From 151a65e87c89692d16d47dacfc8d7f9e65adcb95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Mon, 6 Jul 2026 04:16:42 -0700 Subject: [PATCH] Add Fantom test verifying `onLayout` does not bubble to ancestors Summary: `onLayout` is a direct (non-bubbling) event, so a layout event fired on a child should not invoke an ancestor's `onLayout` prop handler. Add a Fantom integration test that renders a parent and child ``, both with `onLayout`, then dispatches a layout event on the child and asserts the parent's `onLayout` prop is not called as a result. The initial layout pass emits a real layout event for every view that has `onLayout`, and those events are flushed lazily on the first dispatched event. The test flushes them up front and snapshots the call counts before dispatching, so the parent's own mount-time layout event is not mistaken for bubbling. Changelog: [Internal] Differential Revision: D110755346 --- .../__tests__/EventTargetDispatching-itest.js | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/packages/react-native/src/private/renderer/core/__tests__/EventTargetDispatching-itest.js b/packages/react-native/src/private/renderer/core/__tests__/EventTargetDispatching-itest.js index 8336899f4c5f..e232633f432c 100644 --- a/packages/react-native/src/private/renderer/core/__tests__/EventTargetDispatching-itest.js +++ b/packages/react-native/src/private/renderer/core/__tests__/EventTargetDispatching-itest.js @@ -1582,5 +1582,49 @@ const {isOSS} = Fantom.getConstants(); expect(parentSpy).toHaveBeenCalledTimes(1); }); }); + + describe('direct events (onLayout)', () => { + it('does not bubble onLayout to ancestor views via the onLayout prop', () => { + const root = Fantom.createRoot(); + + const childRef = React.createRef>(); + + const parentSpy = jest.fn(); + const childSpy = jest.fn(); + + Fantom.runTask(() => { + root.render( + + + , + ); + }); + + // Flush the layout events emitted for both views during the initial + // layout pass so they are not conflated with the dispatch below. + Fantom.flushAllNativeEvents(); + + // Both onLayout handlers fired for their own layout during mount. + const childCallsBefore = childSpy.mock.calls.length; + const parentCallsBefore = parentSpy.mock.calls.length; + + Fantom.dispatchNativeEvent( + childRef, + 'onLayout', + {layout: {x: 0, y: 0, width: 100, height: 50}}, + { + category: Fantom.NativeEventCategory.Discrete, + }, + ); + + // Child's onLayout handler fires for the dispatched event + expect(childSpy.mock.calls.length - childCallsBefore).toBeGreaterThan( + 0, + ); + // Parent's onLayout prop handler does NOT fire from the child's event + // because layout is a direct (non-bubbling) event + expect(parentSpy.mock.calls.length - parentCallsBefore).toBe(0); + }); + }); }, );