From 27f4801c82115f52a2252b480dd500afeb5bb97d Mon Sep 17 00:00:00 2001 From: Samuel Susla Date: Wed, 4 Jun 2025 11:51:54 -0700 Subject: [PATCH] introduce Fantom.unstable_getDirectManipulationProps (#51814) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/51814 changelog: [internal] introduce Fantom.unstable_getDirectManipulationProps to make it possible to inspect what is changed by C++ Animated module before animation finishes. Reviewed By: zeyap, rubennorte Differential Revision: D75816105 fbshipit-source-id: 681829ca9d3f77e57213e9f88f4f24b340a982ed --- packages/react-native-fantom/src/index.js | 18 +++++++++++++ .../Animated/__tests__/Animated-itest.js | 27 +++++++++++++------ .../testing/fantom/specs/NativeFantom.js | 1 + 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/packages/react-native-fantom/src/index.js b/packages/react-native-fantom/src/index.js index 70e98f25709..30b61051efe 100644 --- a/packages/react-native-fantom/src/index.js +++ b/packages/react-native-fantom/src/index.js @@ -207,6 +207,24 @@ export function unstable_produceFramesForDuration(milliseconds: number) { NativeFantom.produceFramesForDuration(milliseconds); } +/** + * Returns props appplied via direct manipulation to a view represented by shadow node. + * Direct manipulation is used by C++ Animated to change view properties on UI tick + * while the animation is in progress. Once animation finishes, the final state is committed + * to the shadow tree and result is observable through other JavaScript APIs, like `measure`. + * + * @param node - The node for which to retrieve direct manipulation props. + * @returns Mixed type data containing the direct manipulation properties + * + * Note: This API is marked as unstable and may change in future versions. + */ +export function unstable_getDirectManipulationProps( + node: ReactNativeElement, +): mixed { + const shadowNode = getNativeNodeReference(node); + return NativeFantom.getDirectManipulationProps(shadowNode); +} + /** * Simulates running a task on the UI thread and forces side effect to drain * the event queue, scheduling events to be dispatched to JavaScript. diff --git a/packages/react-native/Libraries/Animated/__tests__/Animated-itest.js b/packages/react-native/Libraries/Animated/__tests__/Animated-itest.js index dfe5aac78ab..6978f3494f5 100644 --- a/packages/react-native/Libraries/Animated/__tests__/Animated-itest.js +++ b/packages/react-native/Libraries/Animated/__tests__/Animated-itest.js @@ -49,9 +49,7 @@ test('moving box by 100 points', () => { const viewElement = ensureInstance(viewRef.current, ReactNativeElement); - let boundingClientRect = viewElement.getBoundingClientRect(); - - expect(boundingClientRect.x).toBe(0); + expect(viewElement.getBoundingClientRect().x).toBe(0); Fantom.runTask(() => { Animated.timing(_translateX, { @@ -61,13 +59,26 @@ test('moving box by 100 points', () => { }).start(); }); - Fantom.unstable_produceFramesForDuration(1000); - boundingClientRect = viewElement.getBoundingClientRect(); - expect(boundingClientRect.x).toBe(100); + Fantom.unstable_produceFramesForDuration(500); + + // shadow tree is not synchronised yet, position X is still 0. + expect(viewElement.getBoundingClientRect().x).toBe(0); + + const transform = + // $FlowFixMe[incompatible-use] + Fantom.unstable_getDirectManipulationProps(viewElement).transform[0]; + + // direct manipulation has been applied. 50% through the animation + // and with linear animation, that is position X = 50. + expect(transform.translateX).toBeCloseTo(50, 0.001); + + Fantom.unstable_produceFramesForDuration(500); + + // Animation is completed now. C++ Animated will commit the final position to the shadow tree. + expect(viewElement.getBoundingClientRect().x).toBe(100); // TODO: this shouldn't be needed but C++ Animated still schedules a React state update // for synchronisation, even though it doesn't need to. Fantom.runWorkLoop(); - boundingClientRect = viewElement.getBoundingClientRect(); - expect(boundingClientRect.x).toBe(100); + expect(viewElement.getBoundingClientRect().x).toBe(100); }); diff --git a/packages/react-native/src/private/testing/fantom/specs/NativeFantom.js b/packages/react-native/src/private/testing/fantom/specs/NativeFantom.js index a4a827b7c4a..b696691f0da 100644 --- a/packages/react-native/src/private/testing/fantom/specs/NativeFantom.js +++ b/packages/react-native/src/private/testing/fantom/specs/NativeFantom.js @@ -88,6 +88,7 @@ interface Spec extends TurboModule { width: number, ) => void; takeMountingManagerLogs: (surfaceId: number) => Array; + getDirectManipulationProps: (shadowNode: mixed /* ShadowNode */) => mixed; flushMessageQueue: () => void; flushEventQueue: () => void; produceFramesForDuration: (miliseconds: number) => void;