Skip to content

Commit da1944d

Browse files
zeyapfacebook-github-bot
authored andcommitted
Add test for synchronous mount props override behavior (#56663)
Summary: ## Changelog: [Android] [Added] - Add test for synchronous mount props override behavior ___ overriding_review_checks_triggers_an_audit_and_retroactive_review Oncall Short Name: react_native Reviewed By: christophpurrer Differential Revision: D103237771
1 parent 52d87f9 commit da1944d

1 file changed

Lines changed: 178 additions & 0 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
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+
8+
@file:Suppress("DEPRECATION")
9+
10+
package com.facebook.react.fabric
11+
12+
import com.facebook.react.ReactRootView
13+
import com.facebook.react.bridge.JavaOnlyMap
14+
import com.facebook.react.bridge.ReactTestHelper
15+
import com.facebook.react.fabric.mounting.MountingManager
16+
import com.facebook.react.fabric.mounting.MountingManager.MountItemExecutor
17+
import com.facebook.react.fabric.mounting.SurfaceMountingManager
18+
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests
19+
import com.facebook.react.uimanager.ThemedReactContext
20+
import com.facebook.react.uimanager.ViewManager
21+
import com.facebook.react.uimanager.ViewManagerRegistry
22+
import com.facebook.react.views.view.ReactViewManager
23+
import com.facebook.testutils.shadows.ShadowNativeLoader
24+
import com.facebook.testutils.shadows.ShadowNativeMap
25+
import com.facebook.testutils.shadows.ShadowReadableNativeArray
26+
import com.facebook.testutils.shadows.ShadowReadableNativeMap
27+
import com.facebook.testutils.shadows.ShadowSoLoader
28+
import com.facebook.testutils.shadows.ShadowWritableNativeArray
29+
import com.facebook.testutils.shadows.ShadowWritableNativeMap
30+
import org.assertj.core.api.Assertions.assertThat
31+
import org.junit.Before
32+
import org.junit.Test
33+
import org.junit.runner.RunWith
34+
import org.robolectric.RobolectricTestRunner
35+
import org.robolectric.annotation.Config
36+
37+
/**
38+
* Tests for the synchronous mount props override behavior in [SurfaceMountingManager], controlled
39+
* by the `overrideBySynchronousMountPropsAtMountingAndroid` feature flag (default: true).
40+
*
41+
* This fixes a race condition where Native Animated applies props (e.g. opacity, transform)
42+
* synchronously on native view, but a regular Fabric mount update with stale props arrives later
43+
* and overwrites the fresh values, causing the view to visibly jump/flicker.
44+
*/
45+
@RunWith(RobolectricTestRunner::class)
46+
@Config(
47+
shadows =
48+
[
49+
ShadowSoLoader::class,
50+
ShadowNativeLoader::class,
51+
ShadowNativeMap::class,
52+
ShadowWritableNativeMap::class,
53+
ShadowReadableNativeMap::class,
54+
ShadowWritableNativeArray::class,
55+
ShadowReadableNativeArray::class,
56+
]
57+
)
58+
class SurfaceMountingManagerSynchronousMountPropsTest {
59+
private lateinit var mountingManager: MountingManager
60+
private lateinit var themedReactContext: ThemedReactContext
61+
private val surfaceId = 1
62+
63+
@Before
64+
fun setUp() {
65+
ReactNativeFeatureFlagsForTests.setUp()
66+
val reactContext = ReactTestHelper.createCatalystContextForTest()
67+
themedReactContext = ThemedReactContext(reactContext, reactContext, null, -1)
68+
mountingManager =
69+
MountingManager(
70+
ViewManagerRegistry(listOf<ViewManager<*, *>>(ReactViewManager())),
71+
MountItemExecutor {},
72+
)
73+
}
74+
75+
private fun startSurface(): SurfaceMountingManager {
76+
val rootView = ReactRootView(themedReactContext)
77+
mountingManager.startSurface(surfaceId, themedReactContext, rootView)
78+
return mountingManager.getSurfaceManagerEnforced(surfaceId, "test")
79+
}
80+
81+
private fun createAndAttachView(smm: SurfaceMountingManager, tag: Int) {
82+
smm.preallocateView("RCTView", tag, JavaOnlyMap.of(), null, true)
83+
smm.addViewAt(surfaceId, tag, 0)
84+
}
85+
86+
/** Stored synchronous opacity should override a stale Fabric mount update. */
87+
@Test
88+
fun storeSynchronousProps_overridesStaleOpacityInUpdateProps() {
89+
val smm = startSurface()
90+
val tag = 42
91+
createAndAttachView(smm, tag)
92+
93+
// Native Animated sets opacity=0.3 synchronously
94+
smm.storeSynchronousMountPropsOverride(tag, JavaOnlyMap.of("opacity", 0.3))
95+
96+
// Stale Fabric mount update arrives with opacity=1.0
97+
smm.updateProps(tag, JavaOnlyMap.of("opacity", 1.0))
98+
99+
// The synchronous value (0.3) should win
100+
assertThat(smm.getView(tag).alpha).isEqualTo(0.3f)
101+
}
102+
103+
/** Multiple storeSynchronousMountPropsOverride calls should merge — later values win. */
104+
@Test
105+
fun storeSynchronousProps_mergesMultipleCalls() {
106+
val smm = startSurface()
107+
val tag = 42
108+
createAndAttachView(smm, tag)
109+
110+
smm.storeSynchronousMountPropsOverride(tag, JavaOnlyMap.of("opacity", 0.3))
111+
smm.storeSynchronousMountPropsOverride(tag, JavaOnlyMap.of("opacity", 0.7))
112+
113+
smm.updateProps(tag, JavaOnlyMap.of("opacity", 1.0))
114+
115+
assertThat(smm.getView(tag).alpha).isEqualTo(0.7f)
116+
}
117+
118+
/**
119+
* Full race condition scenario: synchronous animated props survive a stale Fabric mount update.
120+
*/
121+
@Test
122+
fun raceCondition_synchronousPropsWinOverStaleMount() {
123+
val smm = startSurface()
124+
val tag = 42
125+
createAndAttachView(smm, tag)
126+
127+
// Native Animated applies fresh props synchronously
128+
val freshAnimatedProps = JavaOnlyMap.of("opacity", 0.2)
129+
smm.storeSynchronousMountPropsOverride(tag, freshAnimatedProps)
130+
smm.updatePropsSynchronously(tag, freshAnimatedProps)
131+
assertThat(smm.getView(tag).alpha).isEqualTo(0.2f)
132+
133+
// Stale Fabric mount update arrives
134+
smm.updateProps(tag, JavaOnlyMap.of("opacity", 1.0))
135+
136+
// Synchronous value preserved
137+
assertThat(smm.getView(tag).alpha).isEqualTo(0.2f)
138+
}
139+
140+
/**
141+
* When a view is deleted, stored synchronous props should be cleaned up. A recreated view with
142+
* the same tag should not be affected by the old stored props.
143+
*/
144+
@Test
145+
fun deleteView_cleansUpStoredSynchronousProps() {
146+
val smm = startSurface()
147+
val tag = 42
148+
createAndAttachView(smm, tag)
149+
150+
smm.storeSynchronousMountPropsOverride(tag, JavaOnlyMap.of("opacity", 0.3))
151+
smm.deleteView(tag)
152+
153+
// Recreate with same tag
154+
smm.createView("RCTView", tag, JavaOnlyMap.of(), null, null, true)
155+
smm.addViewAt(surfaceId, tag, 0)
156+
157+
smm.updateProps(tag, JavaOnlyMap.of("opacity", 0.9))
158+
assertThat(smm.getView(tag).alpha).isEqualTo(0.9f)
159+
}
160+
161+
/** Synchronous props stored for one tag should not affect a different tag. */
162+
@Test
163+
fun synchronousPropsAreIsolatedPerTag() {
164+
val smm = startSurface()
165+
createAndAttachView(smm, 42)
166+
createAndAttachView(smm, 43)
167+
168+
smm.storeSynchronousMountPropsOverride(42, JavaOnlyMap.of("opacity", 0.3))
169+
170+
smm.updateProps(42, JavaOnlyMap.of("opacity", 1.0))
171+
smm.updateProps(43, JavaOnlyMap.of("opacity", 1.0))
172+
173+
// Tag 42: synchronous override applies (0.3)
174+
assertThat(smm.getView(42).alpha).isEqualTo(0.3f)
175+
// Tag 43: no override, incoming props apply (1.0)
176+
assertThat(smm.getView(43).alpha).isEqualTo(1.0f)
177+
}
178+
}

0 commit comments

Comments
 (0)