diff --git a/packages/react-native/Package.swift b/packages/react-native/Package.swift index fd34d2dfb0f0..4a0b2f88e73b 100644 --- a/packages/react-native/Package.swift +++ b/packages/react-native/Package.swift @@ -432,6 +432,7 @@ let reactFabric = RNTarget( path: "ReactCommon/react/renderer", excludedPaths: [ "animated/tests", + "animationbackend/tests", "animations/tests", "attributedstring/tests", "core/tests", diff --git a/packages/react-native/ReactCommon/React-Fabric.podspec b/packages/react-native/ReactCommon/React-Fabric.podspec index bc7f02073d01..c97a89e77f30 100644 --- a/packages/react-native/ReactCommon/React-Fabric.podspec +++ b/packages/react-native/ReactCommon/React-Fabric.podspec @@ -71,6 +71,7 @@ Pod::Spec.new do |s| s.subspec "animationbackend" do |ss| ss.source_files = podspec_sources("react/renderer/animationbackend/**/*.{m,mm,cpp,h}", "react/renderer/animationbackend/**/*.{h}") + ss.exclude_files = "react/renderer/animationbackend/tests" ss.header_dir = "react/renderer/animationbackend" end diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/tests/AnimatedPropsRegistryTest.cpp b/packages/react-native/ReactCommon/react/renderer/animationbackend/tests/AnimatedPropsRegistryTest.cpp new file mode 100644 index 000000000000..88576d07172c --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/tests/AnimatedPropsRegistryTest.cpp @@ -0,0 +1,443 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include +#include +#include + +#include +#include + +using namespace facebook::react; + +class AnimatedPropsRegistryTest : public ::testing::Test { + protected: + void SetUp() override { + registry_ = std::make_unique(); + } + + void TearDown() override { + registry_.reset(); + } + + std::unordered_map createSurfaceUpdates( + SurfaceId surfaceId, + Tag tag, + std::vector> props) { + std::unordered_map surfaceUpdates; + SurfaceUpdates updates; + + AnimatedProps animatedProps; + animatedProps.props = std::move(props); + updates.propsMap[tag] = std::move(animatedProps); + + surfaceUpdates[surfaceId] = std::move(updates); + return surfaceUpdates; + } + + std::unique_ptr registry_; + SurfaceId surfaceId_{1}; + Tag testTag_{100}; +}; + +// ============================================================================ +// Update Flow - Pending Accumulation +// ============================================================================ + +TEST_F(AnimatedPropsRegistryTest, accumulatesUpdatesInPendingMap) { + std::vector> props1; + props1.push_back(std::make_unique>(OPACITY, 0.5f)); + auto updates1 = createSurfaceUpdates(surfaceId_, testTag_, std::move(props1)); + registry_->update(updates1); + + std::vector> props2; + props2.push_back(std::make_unique>(SHADOW_OPACITY, 0.3f)); + auto updates2 = createSurfaceUpdates(surfaceId_, testTag_, std::move(props2)); + registry_->update(updates2); + + auto [families, map] = registry_->getMap(surfaceId_); + + ASSERT_TRUE(map.contains(testTag_)); + auto& snapshot = map.at(testTag_); + + EXPECT_TRUE(snapshot->propNames.contains(OPACITY)); + EXPECT_TRUE(snapshot->propNames.contains(SHADOW_OPACITY)); + EXPECT_FLOAT_EQ(snapshot->props.opacity, 0.5f); + EXPECT_FLOAT_EQ(snapshot->props.shadowOpacity, 0.3f); +} + +TEST_F(AnimatedPropsRegistryTest, multipleTagsAccumulateIndependently) { + Tag tag1 = 101; + Tag tag2 = 102; + + std::vector> props1; + props1.push_back(std::make_unique>(OPACITY, 0.5f)); + auto updates1 = createSurfaceUpdates(surfaceId_, tag1, std::move(props1)); + registry_->update(updates1); + + std::vector> props2; + props2.push_back(std::make_unique>(OPACITY, 0.8f)); + auto updates2 = createSurfaceUpdates(surfaceId_, tag2, std::move(props2)); + registry_->update(updates2); + + auto [families, map] = registry_->getMap(surfaceId_); + + ASSERT_TRUE(map.contains(tag1)); + ASSERT_TRUE(map.contains(tag2)); + EXPECT_FLOAT_EQ(map.at(tag1)->props.opacity, 0.5f); + EXPECT_FLOAT_EQ(map.at(tag2)->props.opacity, 0.8f); +} + +// ============================================================================ +// GetMap Flow - Pending to Merged +// ============================================================================ + +TEST_F(AnimatedPropsRegistryTest, mergesPendingIntoMapOnGetMap) { + std::vector> props1; + props1.push_back(std::make_unique>(OPACITY, 0.5f)); + auto updates1 = createSurfaceUpdates(surfaceId_, testTag_, std::move(props1)); + registry_->update(updates1); + + auto [families1, map1] = registry_->getMap(surfaceId_); + ASSERT_TRUE(map1.contains(testTag_)); + EXPECT_FLOAT_EQ(map1.at(testTag_)->props.opacity, 0.5f); + + std::vector> props2; + props2.push_back(std::make_unique>(SHADOW_RADIUS, 10.0f)); + auto updates2 = createSurfaceUpdates(surfaceId_, testTag_, std::move(props2)); + registry_->update(updates2); + + auto [families2, map2] = registry_->getMap(surfaceId_); + ASSERT_TRUE(map2.contains(testTag_)); + EXPECT_FLOAT_EQ(map2.at(testTag_)->props.opacity, 0.5f); + EXPECT_FLOAT_EQ(map2.at(testTag_)->props.shadowRadius, 10.0f); +} + +TEST_F(AnimatedPropsRegistryTest, preservesAccumulatedMapAcrossGetMapCalls) { + std::vector> props; + props.push_back(std::make_unique>(OPACITY, 0.7f)); + auto updates = createSurfaceUpdates(surfaceId_, testTag_, std::move(props)); + registry_->update(updates); + + auto [families1, map1] = registry_->getMap(surfaceId_); + EXPECT_FLOAT_EQ(map1.at(testTag_)->props.opacity, 0.7f); + + auto [families2, map2] = registry_->getMap(surfaceId_); + EXPECT_FLOAT_EQ(map2.at(testTag_)->props.opacity, 0.7f); + + auto [families3, map3] = registry_->getMap(surfaceId_); + EXPECT_FLOAT_EQ(map3.at(testTag_)->props.opacity, 0.7f); +} + +TEST_F( + AnimatedPropsRegistryTest, + getMapWithEmptyRegistryReturnsEmptyCollections) { + auto [families, map] = registry_->getMap(surfaceId_); + EXPECT_TRUE(families.empty()); + EXPECT_TRUE(map.empty()); +} + +// ============================================================================ +// RawProps Merge Behavior +// ============================================================================ + +TEST_F(AnimatedPropsRegistryTest, mergesRawPropsWithPatchSemantics) { + folly::dynamic rawDynamic1 = + folly::dynamic::object("opacity", 0.5)("nativeID", "node1"); + auto rawProps1 = std::make_unique(rawDynamic1); + + std::unordered_map surfaceUpdates1; + SurfaceUpdates updates1; + AnimatedProps animatedProps1; + animatedProps1.rawProps = std::move(rawProps1); + updates1.propsMap[testTag_] = std::move(animatedProps1); + surfaceUpdates1[surfaceId_] = std::move(updates1); + registry_->update(surfaceUpdates1); + + folly::dynamic rawDynamic2 = folly::dynamic::object("shadowRadius", 3.0); + auto rawProps2 = std::make_unique(rawDynamic2); + + std::unordered_map surfaceUpdates2; + SurfaceUpdates updates2; + AnimatedProps animatedProps2; + animatedProps2.rawProps = std::move(rawProps2); + updates2.propsMap[testTag_] = std::move(animatedProps2); + surfaceUpdates2[surfaceId_] = std::move(updates2); + registry_->update(surfaceUpdates2); + + auto [families, map] = registry_->getMap(surfaceId_); + ASSERT_TRUE(map.contains(testTag_)); + ASSERT_NE(map.at(testTag_)->rawProps, nullptr); + + auto& mergedRawProps = *map.at(testTag_)->rawProps; + EXPECT_DOUBLE_EQ(mergedRawProps["opacity"].getDouble(), 0.5); + EXPECT_EQ(mergedRawProps["nativeID"].getString(), "node1"); + EXPECT_DOUBLE_EQ(mergedRawProps["shadowRadius"].getDouble(), 3.0); +} + +TEST_F(AnimatedPropsRegistryTest, rawPropsOverwriteOnConflict) { + folly::dynamic rawDynamic1 = folly::dynamic::object("opacity", 0.5); + auto rawProps1 = std::make_unique(rawDynamic1); + + std::unordered_map surfaceUpdates1; + SurfaceUpdates updates1; + AnimatedProps animatedProps1; + animatedProps1.rawProps = std::move(rawProps1); + updates1.propsMap[testTag_] = std::move(animatedProps1); + surfaceUpdates1[surfaceId_] = std::move(updates1); + registry_->update(surfaceUpdates1); + + folly::dynamic rawDynamic2 = folly::dynamic::object("opacity", 0.8); + auto rawProps2 = std::make_unique(rawDynamic2); + + std::unordered_map surfaceUpdates2; + SurfaceUpdates updates2; + AnimatedProps animatedProps2; + animatedProps2.rawProps = std::move(rawProps2); + updates2.propsMap[testTag_] = std::move(animatedProps2); + surfaceUpdates2[surfaceId_] = std::move(updates2); + registry_->update(surfaceUpdates2); + + auto [families, map] = registry_->getMap(surfaceId_); + ASSERT_TRUE(map.contains(testTag_)); + ASSERT_NE(map.at(testTag_)->rawProps, nullptr); + + auto& mergedRawProps = *map.at(testTag_)->rawProps; + EXPECT_DOUBLE_EQ(mergedRawProps["opacity"].getDouble(), 0.8); +} + +TEST_F(AnimatedPropsRegistryTest, rawPropsHandlesNestedObjects) { + folly::dynamic rawDynamic = folly::dynamic::object( + "shadowOffset", folly::dynamic::object("width", 5.0)("height", 10.0)); + auto rawProps = std::make_unique(rawDynamic); + + std::unordered_map surfaceUpdates; + SurfaceUpdates updates; + AnimatedProps animatedProps; + animatedProps.rawProps = std::move(rawProps); + updates.propsMap[testTag_] = std::move(animatedProps); + surfaceUpdates[surfaceId_] = std::move(updates); + registry_->update(surfaceUpdates); + + auto [families, map] = registry_->getMap(surfaceId_); + ASSERT_TRUE(map.contains(testTag_)); + ASSERT_NE(map.at(testTag_)->rawProps, nullptr); + + auto& mergedRawProps = *map.at(testTag_)->rawProps; + EXPECT_DOUBLE_EQ(mergedRawProps["shadowOffset"]["width"].getDouble(), 5.0); + EXPECT_DOUBLE_EQ(mergedRawProps["shadowOffset"]["height"].getDouble(), 10.0); +} + +// ============================================================================ +// Typed Props Merge Behavior +// ============================================================================ + +TEST_F(AnimatedPropsRegistryTest, typedPropsOverwriteOnSameProp) { + std::vector> props1; + props1.push_back(std::make_unique>(OPACITY, 0.5f)); + auto updates1 = createSurfaceUpdates(surfaceId_, testTag_, std::move(props1)); + registry_->update(updates1); + + std::vector> props2; + props2.push_back(std::make_unique>(OPACITY, 0.8f)); + auto updates2 = createSurfaceUpdates(surfaceId_, testTag_, std::move(props2)); + registry_->update(updates2); + + auto [families, map] = registry_->getMap(surfaceId_); + ASSERT_TRUE(map.contains(testTag_)); + + EXPECT_FLOAT_EQ(map.at(testTag_)->props.opacity, 0.8f); +} + +TEST_F(AnimatedPropsRegistryTest, mixedTypedAndRawPropsAccumulate) { + std::vector> props; + props.push_back(std::make_unique>(OPACITY, 0.6f)); + auto updates1 = createSurfaceUpdates(surfaceId_, testTag_, std::move(props)); + registry_->update(updates1); + + folly::dynamic rawDynamic = folly::dynamic::object("nativeID", "test-node"); + auto rawProps = std::make_unique(rawDynamic); + + std::unordered_map surfaceUpdates2; + SurfaceUpdates updates2; + AnimatedProps animatedProps2; + animatedProps2.rawProps = std::move(rawProps); + updates2.propsMap[testTag_] = std::move(animatedProps2); + surfaceUpdates2[surfaceId_] = std::move(updates2); + registry_->update(surfaceUpdates2); + + auto [families, map] = registry_->getMap(surfaceId_); + ASSERT_TRUE(map.contains(testTag_)); + + EXPECT_FLOAT_EQ(map.at(testTag_)->props.opacity, 0.6f); + ASSERT_NE(map.at(testTag_)->rawProps, nullptr); + EXPECT_EQ((*map.at(testTag_)->rawProps)["nativeID"].getString(), "test-node"); +} + +// ============================================================================ +// Clear Behavior +// ============================================================================ + +TEST_F(AnimatedPropsRegistryTest, clearRemovesSurfaceData) { + std::vector> props; + props.push_back(std::make_unique>(OPACITY, 0.5f)); + auto updates = createSurfaceUpdates(surfaceId_, testTag_, std::move(props)); + registry_->update(updates); + + auto [families1, map1] = registry_->getMap(surfaceId_); + ASSERT_TRUE(map1.contains(testTag_)); + + registry_->clear(surfaceId_); + + auto [families2, map2] = registry_->getMap(surfaceId_); + EXPECT_TRUE(families2.empty()); + EXPECT_TRUE(map2.empty()); +} + +TEST_F(AnimatedPropsRegistryTest, clearDoesNotAffectOtherSurfaces) { + SurfaceId surface1 = 1; + SurfaceId surface2 = 2; + + std::vector> props1; + props1.push_back(std::make_unique>(OPACITY, 0.5f)); + auto updates1 = createSurfaceUpdates(surface1, testTag_, std::move(props1)); + registry_->update(updates1); + + std::vector> props2; + props2.push_back(std::make_unique>(OPACITY, 0.8f)); + auto updates2 = createSurfaceUpdates(surface2, testTag_, std::move(props2)); + registry_->update(updates2); + + registry_->getMap(surface1); + registry_->getMap(surface2); + + registry_->clear(surface1); + + auto [families1, map1] = registry_->getMap(surface1); + EXPECT_TRUE(map1.empty()); + + auto [families2, map2] = registry_->getMap(surface2); + ASSERT_TRUE(map2.contains(testTag_)); + EXPECT_FLOAT_EQ(map2.at(testTag_)->props.opacity, 0.8f); +} + +TEST_F(AnimatedPropsRegistryTest, updateAfterClearWorks) { + std::vector> props1; + props1.push_back(std::make_unique>(OPACITY, 0.5f)); + auto updates1 = createSurfaceUpdates(surfaceId_, testTag_, std::move(props1)); + registry_->update(updates1); + + registry_->getMap(surfaceId_); + + registry_->clear(surfaceId_); + + std::vector> props2; + props2.push_back(std::make_unique>(OPACITY, 0.9f)); + auto updates2 = createSurfaceUpdates(surfaceId_, testTag_, std::move(props2)); + registry_->update(updates2); + + auto [families, map] = registry_->getMap(surfaceId_); + ASSERT_TRUE(map.contains(testTag_)); + EXPECT_FLOAT_EQ(map.at(testTag_)->props.opacity, 0.9f); +} + +TEST_F(AnimatedPropsRegistryTest, clearDoesNotAffectPendingData) { + std::vector> props; + props.push_back(std::make_unique>(OPACITY, 0.5f)); + auto updates = createSurfaceUpdates(surfaceId_, testTag_, std::move(props)); + registry_->update(updates); + + registry_->clear(surfaceId_); + + auto [families, map] = registry_->getMap(surfaceId_); + ASSERT_TRUE(map.contains(testTag_)) + << "Pending data should survive clear() since clear() only clears the merged map"; + EXPECT_FLOAT_EQ(map.at(testTag_)->props.opacity, 0.5f); +} + +// ============================================================================ +// Thread Safety Tests +// ============================================================================ + +TEST_F(AnimatedPropsRegistryTest, concurrentUpdateAndGetMapOperations) { + constexpr int numIterations = 100; + std::atomic successfulUpdates{0}; + std::atomic successfulReads{0}; + + std::thread updateThread([this, &successfulUpdates]() { + for (int i = 0; i < numIterations; i++) { + std::vector> props; + props.push_back( + std::make_unique>(OPACITY, 0.01f * i)); + auto updates = + createSurfaceUpdates(surfaceId_, testTag_, std::move(props)); + registry_->update(updates); + successfulUpdates++; + } + }); + + std::thread readThread([this, &successfulReads]() { + for (int i = 0; i < numIterations; i++) { + [[maybe_unused]] auto [families, map] = registry_->getMap(surfaceId_); + successfulReads++; + } + }); + + updateThread.join(); + readThread.join(); + + EXPECT_EQ(successfulUpdates.load(), numIterations); + EXPECT_EQ(successfulReads.load(), numIterations); +} + +// ============================================================================ +// Other Tests +// ============================================================================ + +TEST_F(AnimatedPropsRegistryTest, handlesEmptyPropsVector) { + std::vector> props; + auto updates = createSurfaceUpdates(surfaceId_, testTag_, std::move(props)); + registry_->update(updates); + + auto [families, map] = registry_->getMap(surfaceId_); + + ASSERT_TRUE(map.contains(testTag_)); + EXPECT_TRUE(map.at(testTag_)->propNames.empty()); +} + +TEST_F(AnimatedPropsRegistryTest, handlesMultiplePropsInSingleUpdate) { + std::vector> props; + props.push_back(std::make_unique>(OPACITY, 0.5f)); + props.push_back(std::make_unique>(SHADOW_OPACITY, 0.3f)); + props.push_back(std::make_unique>(SHADOW_RADIUS, 5.0f)); + props.push_back( + std::make_unique>>(Z_INDEX, 10)); + + auto updates = createSurfaceUpdates(surfaceId_, testTag_, std::move(props)); + registry_->update(updates); + + auto [families, map] = registry_->getMap(surfaceId_); + + ASSERT_TRUE(map.contains(testTag_)); + EXPECT_FLOAT_EQ(map.at(testTag_)->props.opacity, 0.5f); + EXPECT_FLOAT_EQ(map.at(testTag_)->props.shadowOpacity, 0.3f); + EXPECT_FLOAT_EQ(map.at(testTag_)->props.shadowRadius, 5.0f); + EXPECT_TRUE(map.at(testTag_)->props.zIndex.has_value()); + EXPECT_EQ(map.at(testTag_)->props.zIndex.value(), 10); +} + +TEST_F(AnimatedPropsRegistryTest, handlesNullRawProps) { + std::vector> props; + props.push_back(std::make_unique>(OPACITY, 0.5f)); + auto updates = createSurfaceUpdates(surfaceId_, testTag_, std::move(props)); + registry_->update(updates); + + auto [families, map] = registry_->getMap(surfaceId_); + + ASSERT_TRUE(map.contains(testTag_)); + EXPECT_EQ(map.at(testTag_)->rawProps, nullptr); +} diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/tests/AnimatedPropsTest.cpp b/packages/react-native/ReactCommon/react/renderer/animationbackend/tests/AnimatedPropsTest.cpp new file mode 100644 index 000000000000..ac925072ad34 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/tests/AnimatedPropsTest.cpp @@ -0,0 +1,174 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include +#include +#include +#include + +using namespace facebook::react; + +// ============================================================================ +// cloneProp Tests - Simple Float Props +// ============================================================================ + +TEST(AnimatedPropsTest, clonePropAppliesOpacity) { + BaseViewProps viewProps; + AnimatedProp prop{OPACITY, 0.5f}; + cloneProp(viewProps, prop); + EXPECT_FLOAT_EQ(viewProps.opacity, 0.5f); +} + +TEST(AnimatedPropsTest, clonePropAppliesShadowOpacity) { + BaseViewProps viewProps; + AnimatedProp prop{SHADOW_OPACITY, 0.8f}; + cloneProp(viewProps, prop); + EXPECT_FLOAT_EQ(viewProps.shadowOpacity, 0.8f); +} + +TEST(AnimatedPropsTest, clonePropAppliesShadowRadius) { + BaseViewProps viewProps; + AnimatedProp prop{SHADOW_RADIUS, 5.0f}; + cloneProp(viewProps, prop); + EXPECT_FLOAT_EQ(viewProps.shadowRadius, 5.0f); +} + +TEST(AnimatedPropsTest, clonePropAppliesOutlineWidth) { + BaseViewProps viewProps; + AnimatedProp prop{OUTLINE_WIDTH, 2.0f}; + cloneProp(viewProps, prop); + EXPECT_FLOAT_EQ(viewProps.outlineWidth, 2.0f); +} + +TEST(AnimatedPropsTest, clonePropAppliesOutlineOffset) { + BaseViewProps viewProps; + AnimatedProp prop{OUTLINE_OFFSET, 3.0f}; + cloneProp(viewProps, prop); + EXPECT_FLOAT_EQ(viewProps.outlineOffset, 3.0f); +} + +// ============================================================================ +// cloneProp Tests - Transform Props +// ============================================================================ + +TEST(AnimatedPropsTest, clonePropAppliesTransform) { + BaseViewProps viewProps; + Transform transform = Transform::Identity(); + transform = transform * Transform::Translate(10.0f, 20.0f, 0.0f); + AnimatedProp prop{TRANSFORM, transform}; + cloneProp(viewProps, prop); + EXPECT_EQ(viewProps.transform, transform); +} + +TEST(AnimatedPropsTest, clonePropAppliesTransformOrigin) { + BaseViewProps viewProps; + TransformOrigin origin{ + .xy = + {ValueUnit(50.0f, UnitType::Percent), + ValueUnit(25.0f, UnitType::Percent)}, + .z = 10.0f}; + AnimatedProp prop{TRANSFORM_ORIGIN, origin}; + cloneProp(viewProps, prop); + EXPECT_EQ(viewProps.transformOrigin, origin); +} + +// ============================================================================ +// cloneProp Tests - Color Props +// ============================================================================ + +TEST(AnimatedPropsTest, clonePropAppliesBackgroundColor) { + BaseViewProps viewProps; + auto color = + colorFromComponents({.red = 255, .green = 0, .blue = 0, .alpha = 255}); + AnimatedProp prop{BACKGROUND_COLOR, color}; + cloneProp(viewProps, prop); + EXPECT_EQ(viewProps.backgroundColor, color); +} + +TEST(AnimatedPropsTest, clonePropAppliesShadowColor) { + BaseViewProps viewProps; + auto color = + colorFromComponents({.red = 0, .green = 0, .blue = 0, .alpha = 128}); + AnimatedProp prop{SHADOW_COLOR, color}; + cloneProp(viewProps, prop); + EXPECT_EQ(viewProps.shadowColor, color); +} + +TEST(AnimatedPropsTest, clonePropAppliesOutlineColor) { + BaseViewProps viewProps; + auto color = + colorFromComponents({.red = 0, .green = 255, .blue = 0, .alpha = 255}); + AnimatedProp prop{OUTLINE_COLOR, color}; + cloneProp(viewProps, prop); + EXPECT_EQ(viewProps.outlineColor, color); +} + +// ============================================================================ +// cloneProp Tests - Z-Index and Optional Props +// ============================================================================ + +TEST(AnimatedPropsTest, clonePropAppliesZIndex) { + BaseViewProps viewProps; + AnimatedProp> prop{Z_INDEX, 5}; + cloneProp(viewProps, prop); + EXPECT_TRUE(viewProps.zIndex.has_value()); + EXPECT_EQ(viewProps.zIndex.value(), 5); +} + +TEST(AnimatedPropsTest, clonePropAppliesNulloptZIndex) { + BaseViewProps viewProps; + viewProps.zIndex = 10; + AnimatedProp> prop{Z_INDEX, std::nullopt}; + cloneProp(viewProps, prop); + EXPECT_FALSE(viewProps.zIndex.has_value()); +} + +// ============================================================================ +// cloneProp Tests - Shadow Props +// ============================================================================ + +TEST(AnimatedPropsTest, clonePropAppliesShadowOffset) { + BaseViewProps viewProps; + facebook::react::Size offset{.width = 5.0f, .height = 10.0f}; + AnimatedProp prop{SHADOW_OFFSET, offset}; + cloneProp(viewProps, prop); + EXPECT_FLOAT_EQ(viewProps.shadowOffset.width, 5.0f); + EXPECT_FLOAT_EQ(viewProps.shadowOffset.height, 10.0f); +} +// ============================================================================ +// Multiple cloneProp Applications +// ============================================================================ + +TEST(AnimatedPropsTest, multipleClonePropCallsAccumulate) { + BaseViewProps viewProps; + + AnimatedProp opacityProp{OPACITY, 0.5f}; + cloneProp(viewProps, opacityProp); + + AnimatedProp shadowProp{SHADOW_OPACITY, 0.8f}; + cloneProp(viewProps, shadowProp); + + AnimatedProp> zIndexProp{Z_INDEX, 99}; + cloneProp(viewProps, zIndexProp); + + EXPECT_FLOAT_EQ(viewProps.opacity, 0.5f); + EXPECT_FLOAT_EQ(viewProps.shadowOpacity, 0.8f); + EXPECT_TRUE(viewProps.zIndex.has_value()); + EXPECT_EQ(viewProps.zIndex.value(), 99); +} + +TEST(AnimatedPropsTest, clonePropOverwritesPreviousValue) { + BaseViewProps viewProps; + + AnimatedProp prop1{OPACITY, 0.5f}; + cloneProp(viewProps, prop1); + EXPECT_FLOAT_EQ(viewProps.opacity, 0.5f); + + AnimatedProp prop2{OPACITY, 0.9f}; + cloneProp(viewProps, prop2); + EXPECT_FLOAT_EQ(viewProps.opacity, 0.9f); +} diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/tests/AnimationBackendCommitHookTest.cpp b/packages/react-native/ReactCommon/react/renderer/animationbackend/tests/AnimationBackendCommitHookTest.cpp new file mode 100644 index 000000000000..48c66043d530 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/tests/AnimationBackendCommitHookTest.cpp @@ -0,0 +1,655 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace facebook::react; + +class AnimationBackendCommitHookTest : public ::testing::Test { + protected: + void SetUp() override { + // Tests run in JS thread context where commit hook executes + // RSNRU is enabled on JS thread in production + ShadowNode::setUseRuntimeShadowNodeReferenceUpdateOnThread(true); + + /* + * The tree has the following structure: + * + * + * + * + * + * + * + * + */ + + contextContainer_ = std::make_shared(); + + ComponentDescriptorProviderRegistry componentDescriptorProviderRegistry{}; + eventDispatcher_ = std::shared_ptr(); + + componentDescriptorRegistry_ = + componentDescriptorProviderRegistry.createComponentDescriptorRegistry( + ComponentDescriptorParameters{ + .eventDispatcher = eventDispatcher_, + .contextContainer = contextContainer_, + .flavor = nullptr}); + + componentDescriptorProviderRegistry.add( + concreteComponentDescriptorProvider()); + componentDescriptorProviderRegistry.add( + concreteComponentDescriptorProvider()); + + builder_ = std::make_unique(componentDescriptorRegistry_); + + RuntimeExecutor runtimeExecutor = + [](std::function&& + /*callback*/) {}; + uiManager_ = + std::make_unique(runtimeExecutor, contextContainer_); + uiManager_->setComponentDescriptorRegistry(componentDescriptorRegistry_); + + registry_ = std::make_shared(); + + auto layoutConstraints = LayoutConstraints{}; + auto layoutContext = LayoutContext{}; + + initialRootNode_ = std::static_pointer_cast(builder_->build( + Element().children( + {Element().tag(nodeAATag_), + Element().tag(nodeABTag_), + Element() + .tag(nodeACTag_) + .children( + {Element().tag(nodeACATag_), + Element().tag(nodeACBTag_)})}))); + + shadowTree_ = std::make_unique( + surfaceId_, + layoutConstraints, + layoutContext, + *uiManager_, + *contextContainer_); + + shadowTree_->commit( + [this](const RootShadowNode& /*oldRootShadowNode*/) { + return initialRootNode_; + }, + {true}); + + uiManager_->startSurface( + std::move(shadowTree_), + "test", + folly::dynamic::object, + DisplayMode::Visible); + + commitHook_ = + std::make_unique(*uiManager_, registry_); + + nodeAA_ = std::static_pointer_cast( + initialRootNode_->getChildren()[0]); + nodeAB_ = std::static_pointer_cast( + initialRootNode_->getChildren()[1]); + nodeAC_ = std::static_pointer_cast( + initialRootNode_->getChildren()[2]); + nodeACA_ = std::static_pointer_cast( + nodeAC_->getChildren()[0]); + nodeACB_ = std::static_pointer_cast( + nodeAC_->getChildren()[1]); + } + + void TearDown() override { + ShadowNode::setUseRuntimeShadowNodeReferenceUpdateOnThread(false); + ReactNativeFeatureFlags::dangerouslyReset(); + + if (commitHook_) { + commitHook_.reset(); + uiManager_->stopSurface(surfaceId_); + } + } + + // Helper to set up animation props via registry update() method + void setupAnimationProps( + Tag tag, + const ShadowNode& node, + std::vector> props, + std::unique_ptr rawProps = nullptr) { + std::unordered_map surfaceUpdates; + SurfaceUpdates updates; + updates.families.insert(node.getFamilyShared()); + AnimatedProps animatedProps; + animatedProps.props = std::move(props); + animatedProps.rawProps = std::move(rawProps); + updates.propsMap[tag] = std::move(animatedProps); + surfaceUpdates[surfaceId_] = std::move(updates); + registry_->update(surfaceUpdates); + } + + std::shared_ptr commitReactUpdate() { + uiManager_->getShadowTreeRegistry().visit( + surfaceId_, [&](const ShadowTree& shadowTree) { + shadowTree.commit( + [&](const RootShadowNode& oldRootShadowNode) { + return std::static_pointer_cast( + oldRootShadowNode.ShadowNode::clone({})); + }, + {.source = ShadowTreeCommitSource::React}); + }); + + std::shared_ptr newRootNode; + uiManager_->getShadowTreeRegistry().visit( + surfaceId_, [&](const ShadowTree& shadowTree) { + newRootNode = shadowTree.getCurrentRevision().rootShadowNode; + }); + + return newRootNode; + } + + std::shared_ptr commitReactUpdateOverridingProps( + const ShadowNode& targetNode, + folly::dynamic reactProps) { + uiManager_->getShadowTreeRegistry().visit( + surfaceId_, [&](const ShadowTree& shadowTree) { + shadowTree.commit( + [&](const RootShadowNode& oldRootShadowNode) { + return std::static_pointer_cast( + oldRootShadowNode.cloneTree( + targetNode.getFamily(), + [&](const ShadowNode& oldShadowNode) { + auto& componentDescriptor = + oldShadowNode.getComponentDescriptor(); + PropsParserContext propsParserContext{ + surfaceId_, *contextContainer_}; + auto props = componentDescriptor.cloneProps( + propsParserContext, + oldShadowNode.getProps(), + RawProps(reactProps)); + return oldShadowNode.clone( + ShadowNodeFragment{.props = props}); + })); + }, + {.source = ShadowTreeCommitSource::React}); + }); + + std::shared_ptr newRootNode; + uiManager_->getShadowTreeRegistry().visit( + surfaceId_, [&](const ShadowTree& shadowTree) { + newRootNode = shadowTree.getCurrentRevision().rootShadowNode; + }); + + return newRootNode; + } + + // Helper to extract node props from root + std::shared_ptr getNodeABProps( + const std::shared_ptr& rootNode) { + auto nodeAB = std::static_pointer_cast( + rootNode->getChildren()[1]); + return std::static_pointer_cast(nodeAB->getProps()); + } + + std::shared_ptr getNodeACAProps( + const std::shared_ptr& rootNode) { + auto nodeAC = std::static_pointer_cast( + rootNode->getChildren()[2]); + auto nodeACA = std::static_pointer_cast( + nodeAC->getChildren()[0]); + return std::static_pointer_cast(nodeACA->getProps()); + } + + SurfaceId surfaceId_{11}; + std::shared_ptr contextContainer_; + std::shared_ptr eventDispatcher_; + ComponentDescriptorRegistry::Shared componentDescriptorRegistry_; + std::unique_ptr builder_; + std::unique_ptr uiManager_; + std::shared_ptr registry_; + std::shared_ptr initialRootNode_; + std::unique_ptr shadowTree_; + std::unique_ptr commitHook_; + Tag nodeAATag_{101}; + Tag nodeABTag_{102}; + Tag nodeACTag_{103}; + Tag nodeACATag_{104}; + Tag nodeACBTag_{105}; + std::shared_ptr nodeAA_; + std::shared_ptr nodeAB_; + std::shared_ptr nodeAC_; + std::shared_ptr nodeACA_; + std::shared_ptr nodeACB_; +}; + +// ============================================================================ +// Basic Animation State Preservation +// ============================================================================ + +TEST_F( + AnimationBackendCommitHookTest, + commitHookOnlyClonesAffectedNodesInComplexTree) { + // Set up animation props using the new update() API + std::vector> props; + props.push_back(std::make_unique>(OPACITY, 0.5f)); + setupAnimationProps(nodeABTag_, *nodeAB_, std::move(props)); + + auto newRootNode = commitReactUpdate(); + + ASSERT_NE(newRootNode, nullptr); + + auto newNodeAA = std::static_pointer_cast( + newRootNode->getChildren()[0]); + auto newNodeAB = std::static_pointer_cast( + newRootNode->getChildren()[1]); + auto newNodeAC = std::static_pointer_cast( + newRootNode->getChildren()[2]); + auto newNodeACA = std::static_pointer_cast( + newNodeAC->getChildren()[0]); + auto newNodeACB = std::static_pointer_cast( + newNodeAC->getChildren()[1]); + + EXPECT_NE(newRootNode.get(), initialRootNode_.get()) + << "Root node SHOULD be cloned"; + + // Yoga's ownership model forces sibling cloning: when the new root adopts + // children whose yoga nodes are still owned by the old root, adoptYogaChild() + // clones them to maintain the single-owner invariant. + EXPECT_NE(nodeAA_.get(), newNodeAA.get()) + << "nodeAA SHOULD be cloned (Yoga ownership)"; + + EXPECT_NE(nodeAB_.get(), newNodeAB.get()) + << "nodeAB SHOULD be cloned (animated)"; + + EXPECT_NE(nodeAC_.get(), newNodeAC.get()) + << "nodeAC SHOULD be cloned (Yoga ownership)"; + + EXPECT_EQ(nodeACA_.get(), newNodeACA.get()) << "nodeACA should not be cloned"; + + EXPECT_EQ(nodeACB_.get(), newNodeACB.get()) << "nodeACB should NOT be cloned"; + + auto newNodeABProps = + std::static_pointer_cast(newNodeAB->getProps()); + EXPECT_FLOAT_EQ(newNodeABProps->opacity, 0.5f) + << "nodeAB opacity should be updated"; +} + +TEST_F( + AnimationBackendCommitHookTest, + preservesAnimationStateAfterReactCommit) { + std::vector> props; + props.push_back(std::make_unique>(OPACITY, 0.3f)); + setupAnimationProps(nodeABTag_, *nodeAB_, std::move(props)); + + auto rootAfterFirstCommit = commitReactUpdateOverridingProps( + *nodeAB_, folly::dynamic::object("opacity", 1.0)); + ASSERT_NE(rootAfterFirstCommit, nullptr); + + auto propsAfterFirst = getNodeABProps(rootAfterFirstCommit); + EXPECT_FLOAT_EQ(propsAfterFirst->opacity, 0.3f) + << "Opacity should be 0.3 after first commit"; + + auto rootAfterSecondCommit = commitReactUpdateOverridingProps( + *nodeAB_, folly::dynamic::object("opacity", 1.0)); + ASSERT_NE(rootAfterSecondCommit, nullptr); + + auto propsAfterSecond = getNodeABProps(rootAfterSecondCommit); + EXPECT_FLOAT_EQ(propsAfterSecond->opacity, 0.3f) + << "Animation state (opacity 0.3) should be preserved after second React commit"; + + auto rootAfterThirdCommit = commitReactUpdateOverridingProps( + *nodeAB_, folly::dynamic::object("width", 100.0)); + ASSERT_NE(rootAfterThirdCommit, nullptr); + + auto propsAfterThird = getNodeABProps(rootAfterThirdCommit); + EXPECT_FLOAT_EQ(propsAfterThird->opacity, 0.3f) + << "Animation state (opacity 0.3) should be preserved after third React commit"; +} + +// ============================================================================ +// Multiple Animated Nodes +// ============================================================================ + +TEST_F(AnimationBackendCommitHookTest, preservesMultipleAnimatedNodes) { + // Animate nodeAB with opacity + std::vector> propsAB; + propsAB.push_back(std::make_unique>(OPACITY, 0.5f)); + setupAnimationProps(nodeABTag_, *nodeAB_, std::move(propsAB)); + + // Animate nodeACA with different opacity + std::vector> propsACA; + propsACA.push_back(std::make_unique>(OPACITY, 0.8f)); + setupAnimationProps(nodeACATag_, *nodeACA_, std::move(propsACA)); + + auto newRootNode = commitReactUpdateOverridingProps( + *nodeAB_, folly::dynamic::object("opacity", 1.0)); + ASSERT_NE(newRootNode, nullptr); + + auto nodeABProps = getNodeABProps(newRootNode); + EXPECT_FLOAT_EQ(nodeABProps->opacity, 0.5f) << "nodeAB opacity should be 0.5"; + + auto nodeACAProps = getNodeACAProps(newRootNode); + EXPECT_FLOAT_EQ(nodeACAProps->opacity, 0.8f) + << "nodeACA opacity should be 0.8"; +} + +TEST_F( + AnimationBackendCommitHookTest, + multipleNodesWithDifferentPropsAreAnimated) { + // Animate nodeAB with opacity + std::vector> propsAB; + propsAB.push_back(std::make_unique>(OPACITY, 0.5f)); + setupAnimationProps(nodeABTag_, *nodeAB_, std::move(propsAB)); + + // Animate nodeACA with zIndex + std::vector> propsACA; + propsACA.push_back( + std::make_unique>>(Z_INDEX, 10)); + setupAnimationProps(nodeACATag_, *nodeACA_, std::move(propsACA)); + + auto newRootNode = commitReactUpdateOverridingProps( + *nodeAB_, folly::dynamic::object("opacity", 1.0)); + ASSERT_NE(newRootNode, nullptr); + + auto nodeABProps = getNodeABProps(newRootNode); + EXPECT_FLOAT_EQ(nodeABProps->opacity, 0.5f); + + auto nodeACAProps = getNodeACAProps(newRootNode); + EXPECT_TRUE(nodeACAProps->zIndex.has_value()); + EXPECT_EQ(nodeACAProps->zIndex.value(), 10); +} + +// ============================================================================ +// Layout vs Non-Layout Props +// ============================================================================ + +TEST_F(AnimationBackendCommitHookTest, appliesLayoutPropsCorrectly) { + // Full layout prop testing is in AnimatedPropsTest + std::vector> props; + props.push_back(std::make_unique>(SHADOW_RADIUS, 5.0f)); + setupAnimationProps(nodeABTag_, *nodeAB_, std::move(props)); + + auto newRootNode = commitReactUpdateOverridingProps( + *nodeAB_, folly::dynamic::object("shadowRadius", 0.0)); + ASSERT_NE(newRootNode, nullptr); + + auto nodeABProps = getNodeABProps(newRootNode); + EXPECT_FLOAT_EQ(nodeABProps->shadowRadius, 5.0f); +} + +TEST_F(AnimationBackendCommitHookTest, appliesNonLayoutPropsCorrectly) { + // Test OPACITY + std::vector> props; + props.push_back(std::make_unique>(OPACITY, 0.75f)); + setupAnimationProps(nodeABTag_, *nodeAB_, std::move(props)); + + auto newRootNode = commitReactUpdateOverridingProps( + *nodeAB_, folly::dynamic::object("opacity", 1.0)); + ASSERT_NE(newRootNode, nullptr); + + auto nodeABProps = getNodeABProps(newRootNode); + EXPECT_FLOAT_EQ(nodeABProps->opacity, 0.75f); +} + +TEST_F(AnimationBackendCommitHookTest, appliesTransformPropCorrectly) { + std::vector> props; + Transform transform = Transform::Identity(); + transform = transform * Transform::Translate(10.0f, 20.0f, 0.0f); + props.push_back( + std::make_unique>(TRANSFORM, transform)); + setupAnimationProps(nodeABTag_, *nodeAB_, std::move(props)); + + auto newRootNode = commitReactUpdate(); + ASSERT_NE(newRootNode, nullptr); + + auto nodeABProps = getNodeABProps(newRootNode); + EXPECT_EQ(nodeABProps->transform, transform); +} + +// ============================================================================ +// RawProps Path Testing +// ============================================================================ + +TEST_F(AnimationBackendCommitHookTest, appliesRawPropsCorrectly) { + folly::dynamic rawDynamic = + folly::dynamic::object("opacity", 0.42)("nativeID", "animated-node"); + auto rawProps = std::make_unique(rawDynamic); + + std::vector> props; + setupAnimationProps( + nodeABTag_, *nodeAB_, std::move(props), std::move(rawProps)); + + auto newRootNode = commitReactUpdate(); + ASSERT_NE(newRootNode, nullptr); + + auto nodeABProps = getNodeABProps(newRootNode); + EXPECT_FLOAT_EQ(nodeABProps->opacity, 0.42f); + EXPECT_EQ(nodeABProps->nativeId, "animated-node"); +} + +TEST_F(AnimationBackendCommitHookTest, mergesRawPropsWithTypedProps) { + std::vector> props; + props.push_back(std::make_unique>(OPACITY, 0.6f)); + + folly::dynamic rawDynamic = folly::dynamic::object("nativeID", "test-node"); + auto rawProps = std::make_unique(rawDynamic); + + setupAnimationProps( + nodeABTag_, *nodeAB_, std::move(props), std::move(rawProps)); + + auto newRootNode = commitReactUpdate(); + ASSERT_NE(newRootNode, nullptr); + + auto nodeABProps = getNodeABProps(newRootNode); + EXPECT_FLOAT_EQ(nodeABProps->opacity, 0.6f); + EXPECT_EQ(nodeABProps->nativeId, "test-node"); +} + +// ============================================================================ +// Commit Source Filtering +// ============================================================================ + +TEST_F(AnimationBackendCommitHookTest, ignoresNonReactCommitSources) { + std::vector> props; + props.push_back(std::make_unique>(OPACITY, 0.4f)); + setupAnimationProps(nodeABTag_, *nodeAB_, std::move(props)); + + // Commit with Unknown source - should NOT apply animation props + // we only assume that React will override the props + uiManager_->getShadowTreeRegistry().visit( + surfaceId_, [&](const ShadowTree& shadowTree) { + shadowTree.commit( + [&](const RootShadowNode& oldRootShadowNode) { + return std::static_pointer_cast( + oldRootShadowNode.ShadowNode::clone({})); + }, + {.source = ShadowTreeCommitSource::Unknown}); + }); + + std::shared_ptr newRootNode; + uiManager_->getShadowTreeRegistry().visit( + surfaceId_, [&](const ShadowTree& shadowTree) { + newRootNode = shadowTree.getCurrentRevision().rootShadowNode; + }); + + ASSERT_NE(newRootNode, nullptr); + + auto nodeABProps = getNodeABProps(newRootNode); + EXPECT_FLOAT_EQ(nodeABProps->opacity, 1.0f) + << "Opacity should remain default for non-React commit source"; +} + +TEST_F(AnimationBackendCommitHookTest, processesAnimationEndSyncCommits) { + std::vector> props; + props.push_back(std::make_unique>(OPACITY, 0.4f)); + setupAnimationProps(nodeABTag_, *nodeAB_, std::move(props)); + + // Commit with AnimationEndSync source - SHOULD apply animation props + uiManager_->getShadowTreeRegistry().visit( + surfaceId_, [&](const ShadowTree& shadowTree) { + shadowTree.commit( + [&](const RootShadowNode& oldRootShadowNode) { + return std::static_pointer_cast( + oldRootShadowNode.ShadowNode::clone({})); + }, + {.source = ShadowTreeCommitSource::AnimationEndSync}); + }); + + std::shared_ptr newRootNode; + uiManager_->getShadowTreeRegistry().visit( + surfaceId_, [&](const ShadowTree& shadowTree) { + newRootNode = shadowTree.getCurrentRevision().rootShadowNode; + }); + + ASSERT_NE(newRootNode, nullptr); + + auto nodeABProps = getNodeABProps(newRootNode); + EXPECT_FLOAT_EQ(nodeABProps->opacity, 0.4f) + << "Animation props SHOULD be applied for AnimationEndSync commit source"; +} + +// ============================================================================ +// Empty Registry Handling +// ============================================================================ + +TEST_F(AnimationBackendCommitHookTest, returnsOriginalTreeWhenNoAnimations) { + auto newRootNode = commitReactUpdate(); + ASSERT_NE(newRootNode, nullptr); + + auto nodeABProps = getNodeABProps(newRootNode); + EXPECT_FLOAT_EQ(nodeABProps->opacity, 1.0f) + << "Default opacity should be preserved when no animations"; + + newRootNode = commitReactUpdateOverridingProps( + *nodeAB_, folly::dynamic::object("opacity", 0.5)); + + nodeABProps = getNodeABProps(newRootNode); + EXPECT_FLOAT_EQ(nodeABProps->opacity, 0.5f) + << "Opacity should be applied properly when no animations"; +} + +// ============================================================================ +// Additional Prop Type Tests +// ============================================================================ + +TEST_F(AnimationBackendCommitHookTest, appliesShadowOpacityCorrectly) { + std::vector> props; + props.push_back(std::make_unique>(SHADOW_OPACITY, 0.8f)); + setupAnimationProps(nodeABTag_, *nodeAB_, std::move(props)); + + auto newRootNode = commitReactUpdateOverridingProps( + *nodeAB_, folly::dynamic::object("shadowOpacity", 0.0)); + ASSERT_NE(newRootNode, nullptr); + + auto nodeABProps = getNodeABProps(newRootNode); + EXPECT_FLOAT_EQ(nodeABProps->shadowOpacity, 0.8f); +} + +TEST_F(AnimationBackendCommitHookTest, appliesZIndexCorrectly) { + std::vector> props; + props.push_back( + std::make_unique>>(Z_INDEX, 5)); + setupAnimationProps(nodeABTag_, *nodeAB_, std::move(props)); + + auto newRootNode = commitReactUpdateOverridingProps( + *nodeAB_, folly::dynamic::object("zIndex", 0)); + ASSERT_NE(newRootNode, nullptr); + + auto nodeABProps = getNodeABProps(newRootNode); + EXPECT_TRUE(nodeABProps->zIndex.has_value()); + EXPECT_EQ(nodeABProps->zIndex.value(), 5); +} + +TEST_F(AnimationBackendCommitHookTest, appliesOutlineWidthCorrectly) { + std::vector> props; + props.push_back(std::make_unique>(OUTLINE_WIDTH, 2.0f)); + setupAnimationProps(nodeABTag_, *nodeAB_, std::move(props)); + + auto newRootNode = commitReactUpdateOverridingProps( + *nodeAB_, folly::dynamic::object("outlineWidth", 0.0)); + ASSERT_NE(newRootNode, nullptr); + + auto nodeABProps = getNodeABProps(newRootNode); + EXPECT_FLOAT_EQ(nodeABProps->outlineWidth, 2.0f); +} + +TEST_F(AnimationBackendCommitHookTest, appliesOutlineOffsetCorrectly) { + std::vector> props; + props.push_back(std::make_unique>(OUTLINE_OFFSET, 3.0f)); + setupAnimationProps(nodeABTag_, *nodeAB_, std::move(props)); + + auto newRootNode = commitReactUpdateOverridingProps( + *nodeAB_, folly::dynamic::object("outlineOffset", 0.0)); + ASSERT_NE(newRootNode, nullptr); + + auto nodeABProps = getNodeABProps(newRootNode); + EXPECT_FLOAT_EQ(nodeABProps->outlineOffset, 3.0f); +} + +// ============================================================================ +// Multiple Props on Same Node +// ============================================================================ + +TEST_F(AnimationBackendCommitHookTest, appliesMultiplePropsToSameNode) { + std::vector> props; + props.push_back(std::make_unique>(OPACITY, 0.7f)); + props.push_back( + std::make_unique>>(Z_INDEX, 99)); + props.push_back(std::make_unique>(SHADOW_OPACITY, 0.5f)); + setupAnimationProps(nodeABTag_, *nodeAB_, std::move(props)); + + auto newRootNode = commitReactUpdateOverridingProps( + *nodeAB_, + folly::dynamic::object("opacity", 1.0)("zIndex", 0)( + "shadowOpacity", 0.0)); + ASSERT_NE(newRootNode, nullptr); + + auto nodeABProps = getNodeABProps(newRootNode); + EXPECT_FLOAT_EQ(nodeABProps->opacity, 0.7f); + EXPECT_TRUE(nodeABProps->zIndex.has_value()); + EXPECT_EQ(nodeABProps->zIndex.value(), 99); + EXPECT_FLOAT_EQ(nodeABProps->shadowOpacity, 0.5f); +} + +// ============================================================================ +// Animation Update During Commit Cycle +// ============================================================================ + +TEST_F( + AnimationBackendCommitHookTest, + animationUpdatesBetweenCommitsAreApplied) { + // First animation update + std::vector> props1; + props1.push_back(std::make_unique>(OPACITY, 0.3f)); + setupAnimationProps(nodeABTag_, *nodeAB_, std::move(props1)); + + auto rootAfterFirst = commitReactUpdateOverridingProps( + *nodeAB_, folly::dynamic::object("opacity", 1.0)); + auto propsAfterFirst = getNodeABProps(rootAfterFirst); + EXPECT_FLOAT_EQ(propsAfterFirst->opacity, 0.3f); + + // Second animation update with different value + std::vector> props2; + props2.push_back(std::make_unique>(OPACITY, 0.9f)); + setupAnimationProps(nodeABTag_, *nodeAB_, std::move(props2)); + + auto rootAfterSecond = commitReactUpdateOverridingProps( + *nodeAB_, folly::dynamic::object("opacity", 1.0)); + auto propsAfterSecond = getNodeABProps(rootAfterSecond); + EXPECT_FLOAT_EQ(propsAfterSecond->opacity, 0.9f) + << "Second animation update should be applied"; +}