Skip to content

Commit c8a7dad

Browse files
Nick Wilsonfacebook-github-bot
authored andcommitted
Add spring animation Fantom integration tests
Summary: Changelog: [Internal] Add 3 Fantom integration tests for `Animated.spring` targeting `SpringAnimationDriver.cpp` (0% → 89.4% line coverage): - **basic spring animation**: underdamped spring (stiffness=100, damping=10), verifies mid-animation progress, final value, and finish callback - **overshoot clamping**: highly underdamped spring (stiffness=300, damping=5) with `overshootClamping: true`, samples 25 points to assert value never exceeds toValue — exercises `isOvershooting()` and clamping branches - **critically damped**: damping=20 (zeta=1.0), verifies monotonic convergence without oscillation — exercises overdamped/critically damped branch in `getValueAndVelocityForTime()` Reviewed By: cortinico Differential Revision: D95058965
1 parent d39d5d8 commit c8a7dad

File tree

1 file changed

+149
-0
lines changed

1 file changed

+149
-0
lines changed

packages/react-native/Libraries/Animated/__tests__/Animated-itest.js

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -735,3 +735,152 @@ test('Animated.sequence', () => {
735735

736736
expect(_isSequenceFinished).toBe(true);
737737
});
738+
739+
describe('Animated.spring', () => {
740+
test('basic spring animation', () => {
741+
let _translateX;
742+
const viewRef = createRef<HostInstance>();
743+
744+
function MyApp() {
745+
const translateX = useAnimatedValue(0);
746+
_translateX = translateX;
747+
return (
748+
<Animated.View
749+
ref={viewRef}
750+
style={[{width: 100, height: 100}, {transform: [{translateX}]}]}
751+
/>
752+
);
753+
}
754+
755+
const root = Fantom.createRoot();
756+
Fantom.runTask(() => {
757+
root.render(<MyApp />);
758+
});
759+
760+
const viewElement = ensureInstance(viewRef.current, ReactNativeElement);
761+
expect(viewElement.getBoundingClientRect().x).toBe(0);
762+
763+
let finishResult = null;
764+
Fantom.runTask(() => {
765+
Animated.spring(_translateX, {
766+
toValue: 100,
767+
stiffness: 100,
768+
damping: 10,
769+
mass: 1,
770+
useNativeDriver: true,
771+
}).start(result => {
772+
finishResult = result;
773+
});
774+
});
775+
776+
Fantom.unstable_produceFramesForDuration(500);
777+
778+
const transform =
779+
// $FlowFixMe[incompatible-use]
780+
Fantom.unstable_getDirectManipulationProps(viewElement).transform[0];
781+
expect(transform.translateX).toBeGreaterThan(0);
782+
783+
Fantom.unstable_produceFramesForDuration(4500);
784+
785+
Fantom.runWorkLoop();
786+
expect(viewElement.getBoundingClientRect().x).toBe(100);
787+
expect(finishResult?.finished).toBe(true);
788+
});
789+
790+
test('with overshoot clamping', () => {
791+
let _translateX;
792+
const viewRef = createRef<HostInstance>();
793+
794+
function MyApp() {
795+
const translateX = useAnimatedValue(0);
796+
_translateX = translateX;
797+
return (
798+
<Animated.View
799+
ref={viewRef}
800+
style={[{width: 100, height: 100}, {transform: [{translateX}]}]}
801+
/>
802+
);
803+
}
804+
805+
const root = Fantom.createRoot();
806+
Fantom.runTask(() => {
807+
root.render(<MyApp />);
808+
});
809+
810+
const viewElement = ensureInstance(viewRef.current, ReactNativeElement);
811+
812+
Fantom.runTask(() => {
813+
Animated.spring(_translateX, {
814+
toValue: 100,
815+
stiffness: 300,
816+
damping: 5,
817+
mass: 1,
818+
overshootClamping: true,
819+
useNativeDriver: true,
820+
}).start();
821+
});
822+
823+
// Sample at multiple points to ensure value never exceeds toValue
824+
for (let i = 0; i < 25; i++) {
825+
Fantom.unstable_produceFramesForDuration(200);
826+
const t =
827+
// $FlowFixMe[incompatible-use]
828+
Fantom.unstable_getDirectManipulationProps(viewElement).transform[0];
829+
expect(t.translateX).toBeLessThanOrEqual(100);
830+
expect(t.translateX).toBeGreaterThanOrEqual(0);
831+
}
832+
833+
Fantom.runWorkLoop();
834+
expect(viewElement.getBoundingClientRect().x).toBe(100);
835+
});
836+
837+
test('critically damped', () => {
838+
let _translateX;
839+
const viewRef = createRef<HostInstance>();
840+
841+
function MyApp() {
842+
const translateX = useAnimatedValue(0);
843+
_translateX = translateX;
844+
return (
845+
<Animated.View
846+
ref={viewRef}
847+
style={[{width: 100, height: 100}, {transform: [{translateX}]}]}
848+
/>
849+
);
850+
}
851+
852+
const root = Fantom.createRoot();
853+
Fantom.runTask(() => {
854+
root.render(<MyApp />);
855+
});
856+
857+
const viewElement = ensureInstance(viewRef.current, ReactNativeElement);
858+
859+
Fantom.runTask(() => {
860+
Animated.spring(_translateX, {
861+
toValue: 100,
862+
stiffness: 100,
863+
damping: 20,
864+
mass: 1,
865+
useNativeDriver: true,
866+
}).start();
867+
});
868+
869+
// Verify monotonic convergence (no oscillation)
870+
let prevValue = 0;
871+
for (let i = 0; i < 10; i++) {
872+
Fantom.unstable_produceFramesForDuration(200);
873+
const t =
874+
// $FlowFixMe[incompatible-use]
875+
Fantom.unstable_getDirectManipulationProps(viewElement).transform[0];
876+
// Should approach 100 monotonically from below
877+
expect(t.translateX).toBeGreaterThanOrEqual(prevValue - 0.01);
878+
expect(t.translateX).toBeLessThanOrEqual(100.01);
879+
prevValue = t.translateX;
880+
}
881+
882+
Fantom.unstable_produceFramesForDuration(3000);
883+
Fantom.runWorkLoop();
884+
expect(viewElement.getBoundingClientRect().x).toBe(100);
885+
});
886+
});

0 commit comments

Comments
 (0)