Skip to content

Commit c4fcf12

Browse files
Keep C++ Animated's direct updates out of React commits with the animation backend: tests
1 parent e65bf49 commit c4fcf12

6 files changed

Lines changed: 462 additions & 19 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @fantom_flags useSharedAnimatedBackend:true updateRuntimeShadowNodeReferencesOnCommitThread:* optimizedAnimatedPropUpdates:*
7+
* @fantom_flags useSharedAnimatedBackend:true updateRuntimeShadowNodeReferencesOnCommitThread:* optimizedAnimatedPropUpdates:* enableCxxAnimatedDirectPathInBackend:*
88
* @flow strict-local
99
* @format
1010
*/
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @fantom_flags useSharedAnimatedBackend:true enableCxxAnimatedDirectPathInBackend:* updateRuntimeShadowNodeReferencesOnCommitThread:*
8+
* @flow strict-local
9+
* @format
10+
*/
11+
12+
import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';
13+
14+
import type {HostInstance} from 'react-native';
15+
16+
import * as Fantom from '@react-native/fantom';
17+
import * as React from 'react';
18+
import {createRef, useState} from 'react';
19+
import {Animated, View, useAnimatedValue} from 'react-native';
20+
21+
let _opacity;
22+
let _setAnimatedWidth;
23+
24+
// `still` has the same position and structure as `animated` but never
25+
// animates, so it is cloned only for the reasons unrelated to animations.
26+
function MyApp({
27+
animatedRef,
28+
stillRef,
29+
}: {
30+
animatedRef: React.RefSetter<HostInstance>,
31+
stillRef?: React.RefSetter<HostInstance>,
32+
}) {
33+
const opacity = useAnimatedValue(1);
34+
const stillOpacity = useAnimatedValue(1);
35+
const [animatedWidth, setAnimatedWidth] = useState(100);
36+
_opacity = opacity;
37+
_setAnimatedWidth = setAnimatedWidth;
38+
return (
39+
<View collapsable={false}>
40+
<Animated.View
41+
ref={animatedRef}
42+
style={{
43+
width: animatedWidth,
44+
height: 100,
45+
opacity,
46+
}}
47+
/>
48+
<Animated.View
49+
ref={stillRef}
50+
style={{width: 100, height: 100, opacity: stillOpacity}}
51+
/>
52+
<View collapsable={false} style={{width: 100, height: 100}} />
53+
</View>
54+
);
55+
}
56+
57+
function startOpacityAnimation(duration: number) {
58+
Fantom.runTask(() => {
59+
Animated.timing(_opacity, {
60+
toValue: 0,
61+
duration,
62+
useNativeDriver: true,
63+
}).start();
64+
});
65+
}
66+
67+
test('a React commit of the animated view keeps its direct animated value mounted', () => {
68+
const animatedRef = createRef<HostInstance>();
69+
const root = Fantom.createRoot();
70+
Fantom.runTask(() => {
71+
root.render(<MyApp animatedRef={animatedRef} />);
72+
});
73+
74+
startOpacityAnimation(1000);
75+
Fantom.unstable_produceFramesForDuration(500);
76+
77+
Fantom.runTask(() => {
78+
_setAnimatedWidth(150);
79+
});
80+
81+
expect(root.getRenderedOutput({props: ['opacity', 'width']}).toJSX()).toEqual(
82+
<rn-view>
83+
<rn-view key={0} opacity="0.5" width="150" />
84+
<rn-view key={1} width="100" />
85+
<rn-view key={2} width="100" />
86+
</rn-view>,
87+
);
88+
});

‎packages/react-native/ReactCommon/react/renderer/animated/tests/EventAnimationDriverTests.cpp‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ class FakeAnimationBackend : public UIManagerAnimationBackend {
3838
void pushAnimationMutations(const Callback& callback) override {
3939
callback(pushTimestamp);
4040
}
41+
void pushAnimationMutations(
42+
AnimationFrontendId /*frontendId*/,
43+
const Callback& callback) override {
44+
callback(pushTimestamp);
45+
}
4146
void registerJSInvoker(std::shared_ptr<CallInvoker> /*jsInvoker*/) override {}
4247
};
4348

‎packages/react-native/ReactCommon/react/renderer/animated/tests/NativeAnimatedFrontendTests.cpp‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77

88
#include "AnimationTestsBase.h"
99

10+
#include <react/featureflags/ReactNativeFeatureFlags.h>
11+
#include <react/featureflags/ReactNativeFeatureFlagsDefaults.h>
1012
#include <react/renderer/animated/NativeAnimatedNodesManager.h>
13+
#include <react/renderer/animated/drivers/AnimationDriverUtils.h>
1114
#include <react/renderer/animationbackend/AnimationMutation.h>
1215
#include <react/renderer/components/view/ViewComponentDescriptor.h>
1316
#include <react/renderer/element/ComponentBuilder.h>
@@ -35,6 +38,9 @@ class RecordingAnimationBackend : public UIManagerAnimationBackend {
3538
startedFrontends.push_back(frontendId);
3639
}
3740
void stopFrontend(AnimationFrontendId /*frontendId*/) override {}
41+
void pushAnimationMutations(
42+
AnimationFrontendId /*frontendId*/,
43+
const Callback& /*callback*/) override {}
3844
CallbackId start(const Callback& /*callback*/) override {
3945
startedCallbacks++;
4046
return 0;
@@ -47,6 +53,18 @@ class RecordingAnimationBackend : public UIManagerAnimationBackend {
4753
void registerJSInvoker(std::shared_ptr<CallInvoker> /*jsInvoker*/) override {}
4854
};
4955

56+
class DirectPathFlags : public ReactNativeFeatureFlagsDefaults {
57+
public:
58+
explicit DirectPathFlags(bool directPath) : directPath_(directPath) {}
59+
60+
bool enableCxxAnimatedDirectPathInBackend() override {
61+
return directPath_;
62+
}
63+
64+
private:
65+
bool directPath_;
66+
};
67+
5068
constexpr Tag kValueNode = 1;
5169
constexpr Tag kStyleNode = 2;
5270
constexpr Tag kPropsNode = 3;
@@ -60,6 +78,10 @@ class NativeAnimatedFrontendTests : public AnimationTestsBase {
6078
runOnRenderThread();
6179
}
6280

81+
void TearDown() override {
82+
ReactNativeFeatureFlags::dangerouslyReset();
83+
}
84+
6385
// Starts animating the view's opacity in four frames.
6486
void startAnimation() {
6587
manager_ = std::make_shared<NativeAnimatedNodesManager>(backend_);
@@ -89,6 +111,26 @@ class NativeAnimatedFrontendTests : public AnimationTestsBase {
89111
manager_->startRenderCallbackIfNeeded(false);
90112
}
91113

114+
// Animates the view's opacity and returns whether each frame's mutation of
115+
// the view finishes the animation.
116+
std::vector<bool> finishesAnimationPerFrame(bool directPath) {
117+
ReactNativeFeatureFlags::dangerouslyForceOverride(
118+
std::make_unique<DirectPathFlags>(directPath));
119+
startAnimation();
120+
121+
std::vector<bool> finishes;
122+
for (int frame = 0; frame < 4; frame++) {
123+
auto mutations = manager_->pullAnimationMutations(
124+
AnimationTimestamp(1000 + SingleFrameIntervalMs * frame));
125+
for (const auto& mutation : mutations.batch) {
126+
if (mutation.tag == kView) {
127+
finishes.push_back(mutation.finishesAnimation);
128+
}
129+
}
130+
}
131+
return finishes;
132+
}
133+
92134
std::shared_ptr<RecordingAnimationBackend> backend_ =
93135
std::make_shared<RecordingAnimationBackend>();
94136
std::shared_ptr<NativeAnimatedNodesManager> manager_;
@@ -106,4 +148,35 @@ TEST_F(NativeAnimatedFrontendTests, registersAndStartsAFrontend) {
106148
EXPECT_EQ(backend_->startedCallbacks, 0);
107149
}
108150

151+
TEST_F(NativeAnimatedFrontendTests, onlyTheLastFrameFinishesTheAnimation) {
152+
auto finishes = finishesAnimationPerFrame(true);
153+
ASSERT_GE(finishes.size(), 2u);
154+
for (size_t frame = 0; frame + 1 < finishes.size(); frame++) {
155+
EXPECT_FALSE(finishes[frame]) << "frame " << frame;
156+
}
157+
EXPECT_TRUE(finishes.back());
158+
}
159+
160+
TEST_F(
161+
NativeAnimatedFrontendTests,
162+
registersAFrontendThatKeepsDirectValuesOutOfReactCommits) {
163+
finishesAnimationPerFrame(true);
164+
165+
ASSERT_EQ(backend_->frontends.size(), 1u);
166+
EXPECT_FALSE(backend_->frontends[0].commitDirectProps);
167+
EXPECT_EQ(backend_->startedFrontends, std::vector<AnimationFrontendId>{1});
168+
EXPECT_EQ(backend_->startedCallbacks, 0);
169+
}
170+
171+
TEST_F(
172+
NativeAnimatedFrontendTests,
173+
registersAFrontendThatCommitsDirectValuesWithoutTheFlag) {
174+
finishesAnimationPerFrame(false);
175+
176+
ASSERT_EQ(backend_->frontends.size(), 1u);
177+
EXPECT_TRUE(backend_->frontends[0].commitDirectProps);
178+
EXPECT_EQ(backend_->startedFrontends, std::vector<AnimationFrontendId>{1});
179+
EXPECT_EQ(backend_->startedCallbacks, 0);
180+
}
181+
109182
} // namespace facebook::react

‎packages/react-native/ReactCommon/react/renderer/animationbackend/tests/AnimatedPropsRegistryTest.cpp‎

Lines changed: 80 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,29 @@ class AnimatedPropsRegistryTest : public ::testing::Test {
2929
registry.initializeSurface(1);
3030
}
3131

32-
void update(AnimatedProps props) {
33-
std::vector<AnimationMutations> batches(1);
34-
batches[0].batch.push_back(
32+
void update(
33+
AnimatedProps props,
34+
bool commitDirectProps = true,
35+
bool finishesAnimation = false) {
36+
std::vector<FrontendMutations> batches(1);
37+
batches[0].commitDirectProps = commitDirectProps;
38+
batches[0].mutations.batch.push_back(
3539
AnimationMutation{
3640
.tag = 10,
3741
.family = node->getFamilyShared(),
38-
.props = std::move(props)});
42+
.props = std::move(props),
43+
.finishesAnimation = finishesAnimation});
3944
registry.update(batches);
4045
}
4146

42-
void updateRaw(folly::dynamic props) {
47+
void updateRaw(
48+
folly::dynamic props,
49+
bool commitDirectProps = true,
50+
bool finishesAnimation = false) {
4351
update(
44-
{.props = {},
45-
.rawProps = std::make_unique<RawProps>(std::move(props))});
52+
{.props = {}, .rawProps = std::make_unique<RawProps>(std::move(props))},
53+
commitDirectProps,
54+
finishesAnimation);
4655
}
4756

4857
void expectRawProps(const folly::dynamic& expected) {
@@ -54,6 +63,66 @@ class AnimatedPropsRegistryTest : public ::testing::Test {
5463
}
5564
};
5665

66+
TEST_F(AnimatedPropsRegistryTest, unsyncedUpdatesAreNotRecorded) {
67+
updateRaw(folly::dynamic::object("opacity", 0.5), false);
68+
auto [families, map] = registry.getMap(1);
69+
EXPECT_TRUE(families.empty());
70+
EXPECT_FALSE(map.contains(10));
71+
}
72+
73+
TEST_F(AnimatedPropsRegistryTest, unsyncedFinalValuesAreKeptApart) {
74+
updateRaw(folly::dynamic::object("opacity", 0.5), false);
75+
updateRaw(folly::dynamic::object("opacity", 0.2), false, true);
76+
77+
auto [families, map] = registry.getMap(1);
78+
EXPECT_TRUE(families.empty());
79+
EXPECT_FALSE(map.contains(10));
80+
auto finalValues = registry.takeFinalValues(1);
81+
ASSERT_TRUE(finalValues.contains(10));
82+
folly::dynamic expected = folly::dynamic::object("opacity", 0.2);
83+
EXPECT_EQ(finalValues.at(10).props, expected);
84+
}
85+
86+
TEST_F(AnimatedPropsRegistryTest, takenFinalValuesAreNotTakenAgain) {
87+
updateRaw(folly::dynamic::object("opacity", 0.2), false, true);
88+
registry.takeFinalValues(1);
89+
updateRaw(folly::dynamic::object("width", 10), false, true);
90+
91+
auto finalValues = registry.takeFinalValues(1);
92+
ASSERT_TRUE(finalValues.contains(10));
93+
folly::dynamic expected = folly::dynamic::object("width", 10);
94+
EXPECT_EQ(finalValues.at(10).props, expected);
95+
}
96+
97+
TEST_F(AnimatedPropsRegistryTest, restoredFinalValuesYieldToNewerOnes) {
98+
updateRaw(folly::dynamic::object("opacity", 0.2)("width", 10), false, true);
99+
auto taken = registry.takeFinalValues(1);
100+
updateRaw(folly::dynamic::object("opacity", 0.3), false, true);
101+
102+
registry.restoreFinalValues(1, std::move(taken));
103+
104+
auto finalValues = registry.takeFinalValues(1);
105+
ASSERT_TRUE(finalValues.contains(10));
106+
folly::dynamic expected = folly::dynamic::object("opacity", 0.3)("width", 10);
107+
EXPECT_EQ(finalValues.at(10).props, expected);
108+
}
109+
110+
TEST_F(AnimatedPropsRegistryTest, unsyncedUpdatesEraseOlderRecordedValues) {
111+
updateRaw(folly::dynamic::object("opacity", 0.8)("width", 5));
112+
snapshot();
113+
updateRaw(folly::dynamic::object("opacity", 0.7));
114+
updateRaw(folly::dynamic::object("opacity", 0.2), false);
115+
expectRawProps(folly::dynamic::object("width", 5));
116+
}
117+
118+
TEST_F(AnimatedPropsRegistryTest, laterSyncedUpdatesAreRecorded) {
119+
updateRaw(folly::dynamic::object("opacity", 0.25));
120+
snapshot();
121+
updateRaw(folly::dynamic::object("opacity", 0.5), false);
122+
updateRaw(folly::dynamic::object("opacity", 0.75));
123+
expectRawProps(folly::dynamic::object("opacity", 0.75));
124+
}
125+
57126
TEST_F(AnimatedPropsRegistryTest, rawUpdatesDoNotAllocateTypedProps) {
58127
for (int frame = 0; frame < 10; frame++) {
59128
updateRaw(folly::dynamic::object("opacity", frame / 10.0));
@@ -68,16 +137,17 @@ TEST_F(AnimatedPropsRegistryTest, updatePreservesInputForSynchronousMount) {
68137
folly::dynamic rawProps = folly::dynamic::object("opacity", 0.25)(
69138
"transform",
70139
folly::dynamic::array(folly::dynamic::object("translateX", 10)));
71-
std::vector<AnimationMutations> batches(1);
72-
batches[0].batch.push_back(
140+
std::vector<FrontendMutations> batches(1);
141+
batches[0].mutations.batch.push_back(
73142
AnimationMutation{
74143
.tag = 10,
75144
.family = node->getFamilyShared(),
76145
.props = AnimatedProps{
77146
.props = {}, .rawProps = std::make_unique<RawProps>(rawProps)}});
78147
registry.update(batches);
79148
registry.update(batches);
80-
EXPECT_EQ(batches[0].batch[0].props.rawProps->toDynamic(), rawProps);
149+
EXPECT_EQ(
150+
batches[0].mutations.batch[0].props.rawProps->toDynamic(), rawProps);
81151
EXPECT_EQ(*snapshot().rawProps, rawProps);
82152
}
83153

0 commit comments

Comments
 (0)