Skip to content

Commit 2cfe7ee

Browse files
add tests for follow-up dispatch in an ongoing dispatch
1 parent f58bfe2 commit 2cfe7ee

2 files changed

Lines changed: 184 additions & 0 deletions

File tree

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
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+
package com.facebook.react.fabric.mounting
9+
10+
import com.facebook.react.fabric.mounting.MountItemDispatcher.ItemDispatchListener
11+
import com.facebook.react.fabric.mounting.mountitems.MountItem
12+
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests
13+
import com.facebook.react.internal.tracing.PerformanceTracer
14+
import com.facebook.react.uimanager.ViewManagerRegistry
15+
import com.facebook.testutils.shadows.ShadowSoLoader
16+
import org.assertj.core.api.Assertions.assertThat
17+
import org.junit.Before
18+
import org.junit.Test
19+
import org.junit.runner.RunWith
20+
import org.robolectric.RobolectricTestRunner
21+
import org.robolectric.annotation.Config
22+
import org.robolectric.annotation.Implementation
23+
import org.robolectric.annotation.Implements
24+
25+
@RunWith(RobolectricTestRunner::class)
26+
@Config(shadows = [ShadowSoLoader::class, MountItemDispatcherTest.ShadowPerformanceTracer::class])
27+
class MountItemDispatcherTest {
28+
29+
private lateinit var dispatcher: MountItemDispatcher
30+
private lateinit var dispatchListener: RecordingItemDispatchListener
31+
32+
@Before
33+
fun setUp() {
34+
ReactNativeFeatureFlagsForTests.setUp()
35+
val mountingManager =
36+
MountingManager(ViewManagerRegistry(emptyList()), MountingManager.MountItemExecutor {})
37+
dispatchListener = RecordingItemDispatchListener()
38+
dispatcher = MountItemDispatcher(mountingManager, dispatchListener)
39+
}
40+
41+
@Test
42+
fun tryDispatchMountItems_executesQueuedMountItem() {
43+
val item = RecordingMountItem()
44+
45+
dispatcher.addMountItem(item)
46+
dispatcher.tryDispatchMountItems()
47+
48+
assertThat(item.executed).isTrue()
49+
assertThat(dispatchListener.didDispatchCount).isEqualTo(1)
50+
}
51+
52+
/**
53+
* Regression test for synchronous (`unstable_Immediate`) state updates on Android. When a mount
54+
* item's execution synchronously updates shadow node state, it can enqueue new mount items and
55+
* re-enter [MountItemDispatcher.tryDispatchMountItems] while a dispatch is already in progress.
56+
*
57+
* The re-entrant call must not drop those items: they should be flushed in the same dispatch pass,
58+
* rather than being deferred to the next frame.
59+
*/
60+
@Test
61+
fun tryDispatchMountItems_reentrantDispatch_executesFollowUpItemInSamePass() {
62+
val followUpItem = RecordingMountItem()
63+
// Simulates a synchronous state update triggered while the first item is being mounted: it
64+
// enqueues another mount item and re-enters the dispatcher.
65+
val initialItem =
66+
RecordingMountItem(
67+
onExecute = {
68+
dispatcher.addMountItem(followUpItem)
69+
dispatcher.tryDispatchMountItems()
70+
})
71+
72+
dispatcher.addMountItem(initialItem)
73+
dispatcher.tryDispatchMountItems()
74+
75+
assertThat(initialItem.executed).isTrue()
76+
assertThat(followUpItem.executed).isTrue()
77+
}
78+
79+
@Test
80+
fun tryDispatchMountItems_reentrantDispatch_preservesExecutionOrder() {
81+
val executionOrder = mutableListOf<String>()
82+
val followUpItem = RecordingMountItem(onExecute = { executionOrder.add("followUp") })
83+
val initialItem =
84+
RecordingMountItem(
85+
onExecute = {
86+
executionOrder.add("initial")
87+
dispatcher.addMountItem(followUpItem)
88+
dispatcher.tryDispatchMountItems()
89+
})
90+
91+
dispatcher.addMountItem(initialItem)
92+
dispatcher.tryDispatchMountItems()
93+
94+
assertThat(executionOrder).containsExactly("initial", "followUp")
95+
}
96+
97+
@Test
98+
fun tryDispatchMountItems_reentrantDispatch_invokesDidDispatchOnceForOuterCall() {
99+
val initialItem =
100+
RecordingMountItem(
101+
onExecute = {
102+
dispatcher.addMountItem(RecordingMountItem())
103+
dispatcher.tryDispatchMountItems()
104+
})
105+
106+
dispatcher.addMountItem(initialItem)
107+
dispatcher.tryDispatchMountItems()
108+
109+
// The re-entrant call returns early and must not notify the listener; only the outer call does,
110+
// once, after the follow-up loop has drained everything.
111+
assertThat(dispatchListener.didDispatchCount).isEqualTo(1)
112+
}
113+
114+
private class RecordingMountItem(
115+
private val surfaceId: Int = 1,
116+
private val onExecute: () -> Unit = {},
117+
) : MountItem {
118+
var executed: Boolean = false
119+
private set
120+
121+
override fun execute(mountingManager: MountingManager) {
122+
executed = true
123+
onExecute()
124+
}
125+
126+
override fun getSurfaceId(): Int = surfaceId
127+
}
128+
129+
private class RecordingItemDispatchListener : ItemDispatchListener {
130+
var didDispatchCount: Int = 0
131+
private set
132+
133+
override fun willMountItems(mountItems: List<MountItem>?) = Unit
134+
135+
override fun didMountItems(mountItems: List<MountItem>?) = Unit
136+
137+
override fun didDispatchMountItems() {
138+
didDispatchCount++
139+
}
140+
}
141+
142+
// isTracing() is a native method; its JNI library isn't loaded in JVM tests, so return false and
143+
// let trace() just run its block. ShadowSoLoader covers the SoLoader.loadLibrary init.
144+
@Implements(PerformanceTracer::class)
145+
class ShadowPerformanceTracer {
146+
companion object {
147+
@JvmStatic @Implementation fun isTracing(): Boolean = false
148+
}
149+
}
150+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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+
package com.facebook.react.uimanager
9+
10+
import com.facebook.react.uimanager.StateWrapper.UpdateMode
11+
import org.assertj.core.api.Assertions.assertThat
12+
import org.junit.Test
13+
import org.junit.runner.RunWith
14+
import org.robolectric.RobolectricTestRunner
15+
16+
/**
17+
* UpdateMode.value crosses JNI and is cast straight to C++ EventQueue::UpdateMode, so the values
18+
* must match its order (Asynchronous = 0, unstable_Immediate = 1). A mismatch silently flips
19+
* async/sync state updates.
20+
*/
21+
@RunWith(RobolectricTestRunner::class)
22+
class StateWrapperUpdateModeTest {
23+
24+
@Test
25+
fun updateModeValues_matchCxxEventQueueUpdateMode() {
26+
assertThat(UpdateMode.Asynchronous.value).isEqualTo(0)
27+
assertThat(UpdateMode.unstable_Immediate.value).isEqualTo(1)
28+
}
29+
30+
@Test
31+
fun updateModeValues_areDistinct() {
32+
assertThat(UpdateMode.entries.map { it.value }).doesNotHaveDuplicates()
33+
}
34+
}

0 commit comments

Comments
 (0)