From d448dce847d7de7ac2030e3c6b3f9eaec0f6f3a0 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Tue, 30 Jun 2026 15:44:45 +0100 Subject: [PATCH 1/3] Add managed-props regression tests for the native animated mounting override --- .../ManagedPropsMountingOverrideTests.cpp | 198 ++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 packages/react-native/ReactCommon/react/renderer/animated/tests/ManagedPropsMountingOverrideTests.cpp diff --git a/packages/react-native/ReactCommon/react/renderer/animated/tests/ManagedPropsMountingOverrideTests.cpp b/packages/react-native/ReactCommon/react/renderer/animated/tests/ManagedPropsMountingOverrideTests.cpp new file mode 100644 index 000000000000..1ba8ea2324cc --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/animated/tests/ManagedPropsMountingOverrideTests.cpp @@ -0,0 +1,198 @@ +/* + * 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 "AnimationTestsBase.h" + +#include +#include + +namespace facebook::react { + +// Pins the getManagedProps / hasManagedProps seam re-merged by +// AnimatedMountingOverrideDelegate to prevent the useNativeDriver mount flash. +class ManagedPropsMountingOverrideTests : public AnimationTestsBase { + protected: + struct OpacityGraph { + Tag opacityTag; + Tag styleTag; + Tag propsTag; + Tag viewTag; + }; + + // value -> style -> props graph for an animated opacity; view left unconnected. + OpacityGraph buildOpacityGraph(float initialOpacity) { + const auto opacityTag = ++rootTag_; + nodesManager_->createAnimatedNode( + opacityTag, + folly::dynamic::object("type", "value")("value", initialOpacity)( + "offset", 0)); + + const auto styleTag = ++rootTag_; + nodesManager_->createAnimatedNode( + styleTag, + folly::dynamic::object("type", "style")( + "style", folly::dynamic::object("opacity", opacityTag))); + nodesManager_->connectAnimatedNodes(opacityTag, styleTag); + + const auto propsTag = ++rootTag_; + nodesManager_->createAnimatedNode( + propsTag, + folly::dynamic::object("type", "props")( + "props", folly::dynamic::object("style", styleTag))); + nodesManager_->connectAnimatedNodes(styleTag, propsTag); + + const auto viewTag = ++rootTag_; + return OpacityGraph{opacityTag, styleTag, propsTag, viewTag}; + } + + Tag rootTag_{getNextRootViewTag()}; +}; + +TEST_F( + ManagedPropsMountingOverrideTests, + getManagedPropsNullForUnconnectedView) { + initNodesManager(); + + const auto unknownTag = ++rootTag_; + EXPECT_TRUE(nodesManager_->getManagedProps(unknownTag).isNull()); + EXPECT_FALSE(nodesManager_->hasManagedProps()); +} + +// Managed props mirror the live committed value every frame, never the frozen +// start value nor the 1.0 default. +TEST_F( + ManagedPropsMountingOverrideTests, + getManagedPropsReflectsLiveValueAcrossFrames) { + initNodesManager(); + + auto g = buildOpacityGraph(/*initialOpacity=*/0.0f); + nodesManager_->connectAnimatedNodeToView(g.propsTag, g.viewTag); + + const auto frames = folly::dynamic::array(0.0f, 0.25f, 0.5f, 0.75f, 1.0f); + nodesManager_->startAnimatingNode( + /*animationId=*/1, + g.opacityTag, + folly::dynamic::object("type", "frames")("frames", frames)( + "toValue", 1.0f), + std::nullopt); + + const double startTimeInTick = 1000; + for (int frame = 0; frame <= 4; ++frame) { + runAnimationFrame(startTimeInTick + frame * SingleFrameIntervalMs); + + auto props = nodesManager_->getManagedProps(g.viewTag); + ASSERT_FALSE(props.isNull()); + ASSERT_TRUE(props.isObject()); + ASSERT_EQ(props.count("opacity"), 1u); + + const double live = nodesManager_->getValue(g.opacityTag).value(); + EXPECT_DOUBLE_EQ(props["opacity"].asDouble(), live); + + if (frame == 0) { + EXPECT_DOUBLE_EQ(live, 0.0); + } + } + + EXPECT_DOUBLE_EQ(nodesManager_->getValue(g.opacityTag).value(), 1.0); + auto finalProps = nodesManager_->getManagedProps(g.viewTag); + ASSERT_EQ(finalProps.count("opacity"), 1u); + EXPECT_DOUBLE_EQ(finalProps["opacity"].asDouble(), 1.0); +} + +// Without the connect-time seed props_ is empty at connect, so the override has +// nothing to merge: the opacity assertion fails pre-hardening, passes post. +TEST_F( + ManagedPropsMountingOverrideTests, + getManagedPropsLiveImmediatelyOnConnect) { + initNodesManager(); + + auto g = buildOpacityGraph(/*initialOpacity=*/0.8f); + nodesManager_->connectAnimatedNodeToView(g.propsTag, g.viewTag); + // No runAnimationFrame: the value must be live the instant the view is managed. + + auto props = nodesManager_->getManagedProps(g.viewTag); + EXPECT_FALSE(props.isNull()); + ASSERT_TRUE(props.isObject()); + ASSERT_EQ(props.count("opacity"), 1u); + EXPECT_DOUBLE_EQ(props["opacity"].asDouble(), static_cast(0.8f)); +} + +// A brand-new view connected to an already mid-flight shared value, with no +// frame in between: fails pre-hardening, passes post. +TEST_F( + ManagedPropsMountingOverrideTests, + getManagedPropsLiveOnConnectWhileValueMidFlight) { + initNodesManager(); + + auto g = buildOpacityGraph(/*initialOpacity=*/0.0f); + nodesManager_->connectAnimatedNodeToView(g.propsTag, g.viewTag); + + const auto frames = folly::dynamic::array(0.0f, 0.25f, 0.5f, 0.75f, 1.0f); + nodesManager_->startAnimatingNode( + /*animationId=*/1, + g.opacityTag, + folly::dynamic::object("type", "frames")("frames", frames)( + "toValue", 1.0f), + std::nullopt); + runAnimationFrame(1000); + runAnimationFrame(1000 + 2 * SingleFrameIntervalMs); + + const double live = nodesManager_->getValue(g.opacityTag).value(); + ASSERT_GT(live, 0.0); + ASSERT_LT(live, 1.0); + + const auto styleTag2 = ++rootTag_; + nodesManager_->createAnimatedNode( + styleTag2, + folly::dynamic::object("type", "style")( + "style", folly::dynamic::object("opacity", g.opacityTag))); + nodesManager_->connectAnimatedNodes(g.opacityTag, styleTag2); + const auto propsTag2 = ++rootTag_; + nodesManager_->createAnimatedNode( + propsTag2, + folly::dynamic::object("type", "props")( + "props", folly::dynamic::object("style", styleTag2))); + nodesManager_->connectAnimatedNodes(styleTag2, propsTag2); + const auto viewTag2 = ++rootTag_; + nodesManager_->connectAnimatedNodeToView(propsTag2, viewTag2); + + auto props = nodesManager_->getManagedProps(viewTag2); + EXPECT_FALSE(props.isNull()); + ASSERT_EQ(props.count("opacity"), 1u); + EXPECT_DOUBLE_EQ(props["opacity"].asDouble(), live); +} + +TEST_F( + ManagedPropsMountingOverrideTests, + hasManagedPropsTracksConnectAndDisconnect) { + initNodesManager(); + + auto g = buildOpacityGraph(/*initialOpacity=*/0.5f); + + EXPECT_FALSE(nodesManager_->hasManagedProps()); + EXPECT_TRUE(nodesManager_->getManagedProps(g.viewTag).isNull()); + + nodesManager_->connectAnimatedNodeToView(g.propsTag, g.viewTag); + EXPECT_TRUE(nodesManager_->hasManagedProps()); + EXPECT_FALSE(nodesManager_->getManagedProps(g.viewTag).isNull()); + + nodesManager_->disconnectAnimatedNodeFromView(g.propsTag, g.viewTag); + EXPECT_FALSE(nodesManager_->hasManagedProps()); + EXPECT_TRUE(nodesManager_->getManagedProps(g.viewTag).isNull()); +} + +TEST_F(ManagedPropsMountingOverrideTests, getManagedPropsIsolatedPerView) { + initNodesManager(); + + auto g = buildOpacityGraph(/*initialOpacity=*/0.3f); + nodesManager_->connectAnimatedNodeToView(g.propsTag, g.viewTag); + + EXPECT_FALSE(nodesManager_->getManagedProps(g.viewTag).isNull()); + EXPECT_TRUE(nodesManager_->getManagedProps(g.viewTag + 1000).isNull()); +} + +} // namespace facebook::react From 5fd6a79fb26081b7f6686041c51300ec8c49cc87 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Tue, 30 Jun 2026 15:45:40 +0100 Subject: [PATCH 2/3] Seed props on connectAnimatedNodeToView so managed props are live at mount --- .../react/renderer/animated/NativeAnimatedNodesManager.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp index fb09d96cd731..4dbd7ff864de 100644 --- a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp @@ -229,6 +229,8 @@ void NativeAnimatedNodesManager::connectAnimatedNodeToView( connectedAnimatedNodes_.insert({viewTag, propsNodeTag}); } updatedNodeTags_.insert(node->tag()); + // Seed props_ so getManagedProps() is live the instant the view is managed. + node->update(); } else { LOG(WARNING) << "Cannot ConnectAnimatedNodeToView, animated node has to be props type"; From 0f82949c70e6377b008aa7b2ccd9847e93e3c2f6 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Tue, 30 Jun 2026 20:47:53 +0100 Subject: [PATCH 3/3] Seed managed props at connect without staging a commit --- .../animated/NativeAnimatedNodesManager.cpp | 4 +-- .../animated/nodes/PropsAnimatedNode.cpp | 33 +++++++++++++------ .../animated/nodes/PropsAnimatedNode.h | 7 ++++ 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp index 4dbd7ff864de..2f6db9d4f2c9 100644 --- a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp @@ -229,8 +229,8 @@ void NativeAnimatedNodesManager::connectAnimatedNodeToView( connectedAnimatedNodes_.insert({viewTag, propsNodeTag}); } updatedNodeTags_.insert(node->tag()); - // Seed props_ so getManagedProps() is live the instant the view is managed. - node->update(); + // Seed props_ so getManagedProps() is live at mount, without staging a commit. + node->collectProps(); } else { LOG(WARNING) << "Cannot ConnectAnimatedNodeToView, animated node has to be props type"; diff --git a/packages/react-native/ReactCommon/react/renderer/animated/nodes/PropsAnimatedNode.cpp b/packages/react-native/ReactCommon/react/renderer/animated/nodes/PropsAnimatedNode.cpp index b47bbd26fe3b..7ea473234938 100644 --- a/packages/react-native/ReactCommon/react/renderer/animated/nodes/PropsAnimatedNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animated/nodes/PropsAnimatedNode.cpp @@ -97,18 +97,9 @@ void PropsAnimatedNode::restoreDefaultValues() { } } -void PropsAnimatedNode::update() { - return update(false); -} - -void PropsAnimatedNode::update(bool forceFabricCommit) { - if (connectedViewTag_ == animated::undefinedAnimatedNodeIdentifier) { - return; - } - +void PropsAnimatedNode::collectPropsLocked() { // TODO: T190192206 consolidate shared update logic between // Props/StyleAnimatedNode - std::lock_guard lock(propsMutex_); const auto& configProps = getConfig()["props"]; for (const auto& entry : configProps.items()) { auto propName = entry.first.asString(); @@ -164,6 +155,28 @@ void PropsAnimatedNode::update(bool forceFabricCommit) { } layoutStyleUpdated_ = isLayoutStyleUpdated(getConfig()["props"], *manager_); +} + +void PropsAnimatedNode::collectProps() { + if (connectedViewTag_ == animated::undefinedAnimatedNodeIdentifier) { + return; + } + + std::lock_guard lock(propsMutex_); + collectPropsLocked(); +} + +void PropsAnimatedNode::update() { + return update(false); +} + +void PropsAnimatedNode::update(bool forceFabricCommit) { + if (connectedViewTag_ == animated::undefinedAnimatedNodeIdentifier) { + return; + } + + std::lock_guard lock(propsMutex_); + collectPropsLocked(); if (manager_->useSharedAnimatedBackend()) { manager_->schedulePropsCommit( diff --git a/packages/react-native/ReactCommon/react/renderer/animated/nodes/PropsAnimatedNode.h b/packages/react-native/ReactCommon/react/renderer/animated/nodes/PropsAnimatedNode.h index 51b1ac766604..f3bf6cd6b4f4 100644 --- a/packages/react-native/ReactCommon/react/renderer/animated/nodes/PropsAnimatedNode.h +++ b/packages/react-native/ReactCommon/react/renderer/animated/nodes/PropsAnimatedNode.h @@ -46,11 +46,18 @@ class PropsAnimatedNode final : public AnimatedNode { return props_; } + // Recompute props_ from the connected nodes WITHOUT scheduling a view + // commit, so getManagedProps() is live at connect with no commit side effect. + void collectProps(); + void update() override; void update(bool forceFabricCommit); private: + // Caller must hold propsMutex_. + void collectPropsLocked(); + std::mutex propsMutex_; folly::dynamic props_; bool layoutStyleUpdated_{false};