Add Fantom test for layout props & fix an issue in c++ animated (#52110)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/52110

## Changelog:

[General] [Internal] - Add Fantom test for layout props

With this test it turns out `layoutStyleUpdated_` on PropsAnimatedNode actually can change after animation update, because its connected StyleAnimatedNodes might be changing. This bug was introduced since D74602321

Reviewed By: rshest

Differential Revision: D76753864

fbshipit-source-id: 5bebb11340086390df20c89adf80abaa63cadc90
This commit is contained in:
Zeya Peng
2025-06-20 16:04:48 -07:00
committed by Facebook GitHub Bot
parent cc442eb8c8
commit 710e08cd54
5 changed files with 96 additions and 14 deletions
@@ -17,6 +17,7 @@ import ensureInstance from '../../../src/private/__tests__/utilities/ensureInsta
import * as Fantom from '@react-native/fantom';
import {createRef} from 'react';
import {Animated, View, useAnimatedValue} from 'react-native';
import {allowStyleProp} from 'react-native/Libraries/Animated/NativeAnimatedAllowlist';
import * as ReactNativeFeatureFlags from 'react-native/src/private/featureflags/ReactNativeFeatureFlags';
import ReactNativeElement from 'react-native/src/private/webapis/dom/nodes/ReactNativeElement';
@@ -152,6 +153,7 @@ test('animation driven by onScroll event', () => {
test('animated opacity', () => {
let _opacity;
let _opacityAnimation;
const viewRef = createRef<HostInstance>();
function MyApp() {
@@ -182,7 +184,7 @@ test('animated opacity', () => {
expect(viewElement.getBoundingClientRect().x).toBe(0);
Fantom.runTask(() => {
Animated.timing(_opacity, {
_opacityAnimation = Animated.timing(_opacity, {
toValue: 0,
duration: 30,
useNativeDriver: true,
@@ -194,8 +196,10 @@ test('animated opacity', () => {
0,
);
// TODO(T223344928): this shouldn't be neccessary with cxxNativeAnimatedRemoveJsSync:true
Fantom.runWorkLoop();
// TODO: this shouldn't be neccessary since animation should be stopped after duration
Fantom.runTask(() => {
_opacityAnimation?.stop();
});
expect(root.getRenderedOutput({props: ['opacity']}).toJSX()).toEqual(
<rn-view opacity="0" />,
@@ -475,3 +479,61 @@ describe('Value.extractOffset', () => {
Fantom.runWorkLoop();
});
});
test('animate layout props', () => {
const viewRef = createRef<HostInstance>();
allowStyleProp('height');
let _animatedHeight;
let _heightAnimation;
function MyApp() {
const animatedHeight = useAnimatedValue(0);
_animatedHeight = animatedHeight;
return (
<Animated.View
ref={viewRef}
style={[
{
width: 100,
height: animatedHeight,
},
]}
/>
);
}
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(<MyApp />);
});
const viewElement = ensureInstance(viewRef.current, ReactNativeElement);
Fantom.runTask(() => {
_heightAnimation = Animated.timing(_animatedHeight, {
toValue: 100,
duration: 10,
useNativeDriver: true,
}).start();
});
Fantom.unstable_produceFramesForDuration(10);
// TODO: this shouldn't be neccessary since animation should be stopped after duration
Fantom.runTask(() => {
_heightAnimation?.stop();
});
// $FlowFixMe[incompatible-use]
expect(Fantom.unstable_getDirectManipulationProps(viewElement).height).toBe(
100,
);
expect(Fantom.unstable_getFabricUpdateProps(viewElement).height).toBe(100);
expect(root.getRenderedOutput({props: ['height']}).toJSX()).toEqual(
<rn-view height="100.000000" />,
);
});
@@ -12,7 +12,6 @@
#include "PropsAnimatedNode.h"
#include <react/debug/react_native_assert.h>
#include <react/renderer/animated/NativeAnimatedAllowlist.h>
#include <react/renderer/animated/NativeAnimatedNodesManager.h>
#include <react/renderer/animated/nodes/ColorAnimatedNode.h>
#include <react/renderer/animated/nodes/StyleAnimatedNode.h>
@@ -31,12 +30,8 @@ bool isLayoutStyleUpdated(
if (node->type() == AnimatedNodeType::Style) {
if (const auto& styleNode =
manager.getAnimatedNode<StyleAnimatedNode>(nodeTag)) {
auto& styleNodeProps = styleNode->getProps();
for (const auto& styleNodeProp : styleNodeProps.items()) {
if (getDirectManipulationAllowlist().count(
styleNodeProp.first.asString()) == 0u) {
return true;
}
if (styleNode->isLayoutStyleUpdated()) {
return true;
}
}
}
@@ -53,9 +48,7 @@ PropsAnimatedNode::PropsAnimatedNode(
const folly::dynamic& config,
NativeAnimatedNodesManager& manager)
: AnimatedNode(tag, config, manager, AnimatedNodeType::Props),
props_(folly::dynamic::object()),
layoutStyleUpdated_(isLayoutStyleUpdated(getConfig()["props"], manager)) {
}
props_(folly::dynamic::object()) {}
void PropsAnimatedNode::connectToView(Tag viewTag) {
react_native_assert(
@@ -145,6 +138,8 @@ void PropsAnimatedNode::update(bool forceFabricCommit) {
}
}
layoutStyleUpdated_ = isLayoutStyleUpdated(getConfig()["props"], *manager_);
manager_->schedulePropsCommit(
connectedViewTag_, props_, layoutStyleUpdated_, forceFabricCommit);
}
@@ -43,7 +43,7 @@ class PropsAnimatedNode final : public AnimatedNode {
private:
std::mutex propsMutex_;
folly::dynamic props_;
const bool layoutStyleUpdated_;
bool layoutStyleUpdated_{false};
Tag connectedViewTag_{animated::undefinedAnimatedNodeIdentifier};
};
@@ -11,12 +11,29 @@
#include "StyleAnimatedNode.h"
#include <react/renderer/animated/NativeAnimatedAllowlist.h>
#include <react/renderer/animated/NativeAnimatedNodesManager.h>
#include <react/renderer/animated/nodes/ColorAnimatedNode.h>
#include <react/renderer/animated/nodes/TransformAnimatedNode.h>
#include <react/renderer/animated/nodes/ValueAnimatedNode.h>
namespace facebook::react {
namespace {
bool isLayoutPropsUpdated(const folly::dynamic& props) {
for (const auto& styleNodeProp : props.items()) {
if (getDirectManipulationAllowlist().count(
styleNodeProp.first.asString()) == 0u) {
return true;
}
}
return false;
}
} // namespace
StyleAnimatedNode::StyleAnimatedNode(
Tag tag,
const folly::dynamic& config,
@@ -77,5 +94,8 @@ void StyleAnimatedNode::update() {
}
}
}
layoutStyleUpdated_ = isLayoutPropsUpdated(props_);
}
} // namespace facebook::react
@@ -28,7 +28,12 @@ class StyleAnimatedNode final : public AnimatedNode {
return props_;
}
bool isLayoutStyleUpdated() const noexcept {
return layoutStyleUpdated_;
}
private:
folly::dynamic props_;
bool layoutStyleUpdated_;
};
} // namespace facebook::react