Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<d2b14345bf627e35562530912b3aae1f>>
* @generated SignedSource<<a9a8ce443fa160a7494fc1c9e7baa02f>>
*/

/**
Expand Down Expand Up @@ -153,7 +153,7 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi

override fun hideOffscreenVirtualViewsOnIOS(): Boolean = false

override fun overrideBySynchronousMountPropsAtMountingAndroid(): Boolean = false
override fun overrideBySynchronousMountPropsAtMountingAndroid(): Boolean = true

override fun perfIssuesEnabled(): Boolean = false

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
/*
* 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.
*/

@file:Suppress("DEPRECATION")

package com.facebook.react.fabric

import com.facebook.react.ReactRootView
import com.facebook.react.bridge.JavaOnlyMap
import com.facebook.react.bridge.ReactTestHelper
import com.facebook.react.fabric.mounting.MountingManager
import com.facebook.react.fabric.mounting.MountingManager.MountItemExecutor
import com.facebook.react.fabric.mounting.SurfaceMountingManager
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.ViewManager
import com.facebook.react.uimanager.ViewManagerRegistry
import com.facebook.react.views.view.ReactViewManager
import com.facebook.testutils.shadows.ShadowNativeLoader
import com.facebook.testutils.shadows.ShadowNativeMap
import com.facebook.testutils.shadows.ShadowReadableNativeArray
import com.facebook.testutils.shadows.ShadowReadableNativeMap
import com.facebook.testutils.shadows.ShadowSoLoader
import com.facebook.testutils.shadows.ShadowWritableNativeArray
import com.facebook.testutils.shadows.ShadowWritableNativeMap
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config

/**
* Tests for the synchronous mount props override behavior in [SurfaceMountingManager], controlled
* by the `overrideBySynchronousMountPropsAtMountingAndroid` feature flag (default: true).
*
* This fixes a race condition where Native Animated applies props (e.g. opacity, transform)
* synchronously on native view, but a regular Fabric mount update with stale props arrives later
* and overwrites the fresh values, causing the view to visibly jump/flicker.
*/
@RunWith(RobolectricTestRunner::class)
@Config(
shadows =
[
ShadowSoLoader::class,
ShadowNativeLoader::class,
ShadowNativeMap::class,
ShadowWritableNativeMap::class,
ShadowReadableNativeMap::class,
ShadowWritableNativeArray::class,
ShadowReadableNativeArray::class,
]
)
class SurfaceMountingManagerSynchronousMountPropsTest {
private lateinit var mountingManager: MountingManager
private lateinit var themedReactContext: ThemedReactContext
private val surfaceId = 1

@Before
fun setUp() {
ReactNativeFeatureFlagsForTests.setUp()
val reactContext = ReactTestHelper.createCatalystContextForTest()
themedReactContext = ThemedReactContext(reactContext, reactContext, null, -1)
mountingManager =
MountingManager(
ViewManagerRegistry(listOf<ViewManager<*, *>>(ReactViewManager())),
MountItemExecutor {},
)
}

private fun startSurface(): SurfaceMountingManager {
val rootView = ReactRootView(themedReactContext)
mountingManager.startSurface(surfaceId, themedReactContext, rootView)
return mountingManager.getSurfaceManagerEnforced(surfaceId, "test")
}

private fun createAndAttachView(smm: SurfaceMountingManager, tag: Int) {
smm.preallocateView("RCTView", tag, JavaOnlyMap.of(), null, true)
smm.addViewAt(surfaceId, tag, 0)
}

/** Stored synchronous opacity should override a stale Fabric mount update. */
@Test
fun storeSynchronousProps_overridesStaleOpacityInUpdateProps() {
val smm = startSurface()
val tag = 42
createAndAttachView(smm, tag)

// Native Animated sets opacity=0.3 synchronously
smm.storeSynchronousMountPropsOverride(tag, JavaOnlyMap.of("opacity", 0.3))

// Stale Fabric mount update arrives with opacity=1.0
smm.updateProps(tag, JavaOnlyMap.of("opacity", 1.0))

// The synchronous value (0.3) should win
assertThat(smm.getView(tag).alpha).isEqualTo(0.3f)
}

/** Multiple storeSynchronousMountPropsOverride calls should merge — later values win. */
@Test
fun storeSynchronousProps_mergesMultipleCalls() {
val smm = startSurface()
val tag = 42
createAndAttachView(smm, tag)

smm.storeSynchronousMountPropsOverride(tag, JavaOnlyMap.of("opacity", 0.3))
smm.storeSynchronousMountPropsOverride(tag, JavaOnlyMap.of("opacity", 0.7))

smm.updateProps(tag, JavaOnlyMap.of("opacity", 1.0))

assertThat(smm.getView(tag).alpha).isEqualTo(0.7f)
}

/**
* Full race condition scenario: synchronous animated props survive a stale Fabric mount update.
*/
@Test
fun raceCondition_synchronousPropsWinOverStaleMount() {
val smm = startSurface()
val tag = 42
createAndAttachView(smm, tag)

// Native Animated applies fresh props synchronously
val freshAnimatedProps = JavaOnlyMap.of("opacity", 0.2)
smm.storeSynchronousMountPropsOverride(tag, freshAnimatedProps)
smm.updatePropsSynchronously(tag, freshAnimatedProps)
assertThat(smm.getView(tag).alpha).isEqualTo(0.2f)

// Stale Fabric mount update arrives
smm.updateProps(tag, JavaOnlyMap.of("opacity", 1.0))

// Synchronous value preserved
assertThat(smm.getView(tag).alpha).isEqualTo(0.2f)
}

/**
* When a view is deleted, stored synchronous props should be cleaned up. A recreated view with
* the same tag should not be affected by the old stored props.
*/
@Test
fun deleteView_cleansUpStoredSynchronousProps() {
val smm = startSurface()
val tag = 42
createAndAttachView(smm, tag)

smm.storeSynchronousMountPropsOverride(tag, JavaOnlyMap.of("opacity", 0.3))
smm.deleteView(tag)

// Recreate with same tag
smm.createView("RCTView", tag, JavaOnlyMap.of(), null, null, true)
smm.addViewAt(surfaceId, tag, 0)

smm.updateProps(tag, JavaOnlyMap.of("opacity", 0.9))
assertThat(smm.getView(tag).alpha).isEqualTo(0.9f)
}

/** Synchronous props stored for one tag should not affect a different tag. */
@Test
fun synchronousPropsAreIsolatedPerTag() {
val smm = startSurface()
createAndAttachView(smm, 42)
createAndAttachView(smm, 43)

smm.storeSynchronousMountPropsOverride(42, JavaOnlyMap.of("opacity", 0.3))

smm.updateProps(42, JavaOnlyMap.of("opacity", 1.0))
smm.updateProps(43, JavaOnlyMap.of("opacity", 1.0))

// Tag 42: synchronous override applies (0.3)
assertThat(smm.getView(42).alpha).isEqualTo(0.3f)
// Tag 43: no override, incoming props apply (1.0)
assertThat(smm.getView(43).alpha).isEqualTo(1.0f)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<4a2fd61cbcdb28042f09ccb03c970674>>
* @generated SignedSource<<d987528598996fc7b1bf3c872f51e2ed>>
*/

/**
Expand Down Expand Up @@ -288,7 +288,7 @@ class ReactNativeFeatureFlagsDefaults : public ReactNativeFeatureFlagsProvider {
}

bool overrideBySynchronousMountPropsAtMountingAndroid() override {
return false;
return true;
}

bool perfIssuesEnabled() override {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -743,7 +743,7 @@ const definitions: FeatureFlagDefinitions = {
ossReleaseStage: 'none',
},
overrideBySynchronousMountPropsAtMountingAndroid: {
defaultValue: false,
defaultValue: true,
metadata: {
dateAdded: '2025-09-04',
description:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<aa202d346e68c3dd641d557306964ebe>>
* @generated SignedSource<<1dd51a152bb30c0e2073a14566c8368d>>
* @flow strict
* @noformat
*/
Expand Down Expand Up @@ -468,7 +468,7 @@ export const hideOffscreenVirtualViewsOnIOS: Getter<boolean> = createNativeFlagG
/**
* Override props at mounting with synchronously mounted (i.e. direct manipulation) props from Native Animated.
*/
export const overrideBySynchronousMountPropsAtMountingAndroid: Getter<boolean> = createNativeFlagGetter('overrideBySynchronousMountPropsAtMountingAndroid', false);
export const overrideBySynchronousMountPropsAtMountingAndroid: Getter<boolean> = createNativeFlagGetter('overrideBySynchronousMountPropsAtMountingAndroid', true);
/**
* Enable reporting Performance Issues (`detail.devtools.performanceIssue`). Displayed in the V2 Performance Monitor and the "Performance Issues" sub-panel in DevTools.
*/
Expand Down
Loading