|
4 | 4 | * This source code is licensed under the MIT license found in the |
5 | 5 | * LICENSE file in the root directory of this source tree. |
6 | 6 | * |
7 | | - * @fantom_flags useSharedAnimatedBackend:* |
| 7 | + * @fantom_flags useSharedAnimatedBackend:* animatedShouldSyncValueBeforeStartCallback:* |
8 | 8 | * @flow strict-local |
9 | 9 | * @format |
10 | 10 | */ |
@@ -735,3 +735,74 @@ test('Animated.sequence', () => { |
735 | 735 |
|
736 | 736 | expect(_isSequenceFinished).toBe(true); |
737 | 737 | }); |
| 738 | + |
| 739 | +// Regression test for native-driver completion-callback ordering. |
| 740 | +// |
| 741 | +// `Animation.__startAnimationIfNative` must sync the JS-side AnimatedValue |
| 742 | +// with the post-animation value BEFORE firing the user's `start({finished})` |
| 743 | +// callback. Otherwise, code that reads the AnimatedValue from inside the |
| 744 | +// callback — or from any React re-render the callback triggers — observes the |
| 745 | +// pre-animation value and renders stale style (e.g. a `scaleX` interpolation |
| 746 | +// that resolves to the starting `outputRange[0]` instead of the final |
| 747 | +// `outputRange[1]`). |
| 748 | +// |
| 749 | +// The fix is gated behind `animatedShouldSyncValueBeforeStartCallback`. This |
| 750 | +// test runs under both flag values (via `@fantom_flags ...:*`) and asserts: |
| 751 | +// - flag OFF: bug is present (callback observes pre-animation value). |
| 752 | +// - flag ON : bug is fixed (callback observes post-animation value). |
| 753 | +test('useNativeDriver: JS-side _value is synced before the completion callback fires', () => { |
| 754 | + let _value; |
| 755 | + let _valueInCallback = null; |
| 756 | + let _interpolationInCallback = null; |
| 757 | + |
| 758 | + function MyApp() { |
| 759 | + const value = useAnimatedValue(0); |
| 760 | + _value = value; |
| 761 | + const scaleX = value.interpolate({ |
| 762 | + inputRange: [0, 1], |
| 763 | + outputRange: [0.5, 1], |
| 764 | + }); |
| 765 | + return ( |
| 766 | + <Animated.View |
| 767 | + style={[{width: 100, height: 100}, {transform: [{scaleX}]}]} |
| 768 | + /> |
| 769 | + ); |
| 770 | + } |
| 771 | + |
| 772 | + const root = Fantom.createRoot(); |
| 773 | + |
| 774 | + Fantom.runTask(() => { |
| 775 | + root.render(<MyApp />); |
| 776 | + }); |
| 777 | + |
| 778 | + Fantom.runTask(() => { |
| 779 | + Animated.timing(_value, { |
| 780 | + toValue: 1, |
| 781 | + duration: 100, |
| 782 | + useNativeDriver: true, |
| 783 | + }).start(({finished}) => { |
| 784 | + if (finished) { |
| 785 | + // $FlowFixMe[prop-missing] _value is internal but stable for testing. |
| 786 | + _valueInCallback = _value._value; |
| 787 | + // $FlowFixMe[prop-missing] |
| 788 | + _interpolationInCallback = _value |
| 789 | + .interpolate({inputRange: [0, 1], outputRange: [0.5, 1]}) |
| 790 | + .__getValue(); |
| 791 | + } |
| 792 | + }); |
| 793 | + }); |
| 794 | + |
| 795 | + Fantom.unstable_produceFramesForDuration(150); |
| 796 | + Fantom.runWorkLoop(); |
| 797 | + |
| 798 | + if (ReactNativeFeatureFlags.animatedShouldSyncValueBeforeStartCallback()) { |
| 799 | + // With the fix: the callback observes the post-animation value. |
| 800 | + expect(_valueInCallback).toBe(1); |
| 801 | + expect(_interpolationInCallback).toBe(1); |
| 802 | + } else { |
| 803 | + // Without the fix: the callback observes the pre-animation value. |
| 804 | + // interp(0) = outputRange[0] = 0.5. |
| 805 | + expect(_valueInCallback).toBe(0); |
| 806 | + expect(_interpolationInCallback).toBe(0.5); |
| 807 | + } |
| 808 | +}); |
0 commit comments