Repository navigation
Expand file tree
/
Copy pathRootViewTest.kt
More file actions
196 lines (170 loc) · 7.6 KB
/
Copy pathRootViewTest.kt
File metadata and controls
196 lines (170 loc) · 7.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
/*
* 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") // Suppressing as we want to test RCTEventEmitter here
package com.facebook.react
import android.annotation.SuppressLint
import android.app.Activity
import android.graphics.Insets
import android.graphics.Rect
import android.view.ViewGroup
import android.view.WindowInsets
import android.view.WindowManager
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactTestHelper.createTestReactApplicationContext
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests
import com.facebook.react.uimanager.DisplayMetricsHolder
import com.facebook.react.uimanager.UIManagerModule
import com.facebook.testutils.shadows.ShadowArguments
import com.facebook.testutils.shadows.ShadowSoLoader
import org.assertj.core.api.Assertions.*
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.doNothing
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.spy
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config
@Config(shadows = [ShadowArguments::class, ShadowSoLoader::class])
@RunWith(RobolectricTestRunner::class)
class RootViewTest {
private lateinit var reactContext: ReactApplicationContext
@Before
fun setUp() {
ReactNativeFeatureFlagsForTests.setUp()
reactContext = spy(createTestReactApplicationContext(RuntimeEnvironment.getApplication()))
doNothing().whenever(reactContext).emitDeviceEvent(any(), any())
DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(reactContext)
val uiManagerModuleMock: UIManagerModule = mock()
doReturn(uiManagerModuleMock)
.whenever(reactContext)
.getNativeModule(UIManagerModule::class.java)
}
@Test
fun testRemountApplication() {
val instanceManager: ReactInstanceManager = mock()
val rootView = ReactRootView(reactContext)
rootView.startReactApplication(instanceManager, "")
rootView.unmountReactApplication()
rootView.startReactApplication(instanceManager, "")
}
@Test
fun testCheckForKeyboardEvents() {
val instanceManager: ReactInstanceManager = mock()
val activity = Robolectric.buildActivity(Activity::class.java).create().get()
whenever(instanceManager.currentReactContext).thenReturn(reactContext)
val rootView: ReactRootView =
object : ReactRootView(activity) {
override fun getWindowVisibleDisplayFrame(outRect: Rect) {
if (outRect.bottom == 0) {
outRect.bottom += 100
outRect.right += 370
} else {
outRect.bottom += 370
}
}
override fun getRootWindowInsets() =
WindowInsets.Builder()
.setInsets(WindowInsets.Type.ime(), Insets.of(0, 0, 0, 370))
.setVisible(WindowInsets.Type.ime(), true)
.build()
}
val rootViewSpy = spy(rootView)
whenever(rootViewSpy.layoutParams).thenReturn(WindowManager.LayoutParams())
rootViewSpy.startReactApplication(instanceManager, "")
rootViewSpy.simulateCheckForKeyboardForTesting()
val params = Arguments.createMap()
val endCoordinates = Arguments.createMap()
val screenHeight = 470.0
val keyboardHeight = 100.0
params.putDouble("duration", 0.0)
endCoordinates.putDouble("width", screenHeight - keyboardHeight)
endCoordinates.putDouble("screenX", 0.0)
endCoordinates.putDouble("height", screenHeight - keyboardHeight)
endCoordinates.putDouble("screenY", keyboardHeight)
params.putMap("endCoordinates", endCoordinates)
params.putString("easing", "keyboard")
verify(reactContext, times(1)).emitDeviceEvent("keyboardDidShow", params)
}
// Regression test for the keyboard re-emit behavior. Without the
// height-change re-emit in `checkForKeyboardEvents`, JS consumers that
// cache `endCoordinates` (KeyboardAvoidingView, ScrollView, Keyboard.metrics)
// observe stale geometry when the IME height changes (e.g., emoji panel
// toggle) without a visibility transition.
@SuppressLint("NewApi", "DeprecatedClass")
@Test
fun testCheckForKeyboardEventsReEmitsOnHeightChange() {
val instanceManager: ReactInstanceManager = mock()
val activity = Robolectric.buildActivity(Activity::class.java).create().get()
whenever(instanceManager.currentReactContext).thenReturn(reactContext)
val imeBottom = intArrayOf(370)
val imeVisible = booleanArrayOf(true)
val rootView: ReactRootView =
object : ReactRootView(activity) {
override fun getWindowVisibleDisplayFrame(outRect: Rect) {
outRect.set(0, 0, 370, 100)
}
override fun getRootWindowInsets(): WindowInsets =
WindowInsets.Builder()
.setInsets(WindowInsets.Type.ime(), Insets.of(0, 0, 0, imeBottom[0]))
.setVisible(WindowInsets.Type.ime(), imeVisible[0])
.build()
override fun getLayoutParams(): ViewGroup.LayoutParams = WindowManager.LayoutParams()
}
rootView.startReactApplication(instanceManager, "")
// 1) Initial show — keyboardDidShow fires once with height=370.
rootView.simulateCheckForKeyboardForTesting()
verify(reactContext, times(1)).emitDeviceEvent("keyboardDidShow", showParams(370.0))
// 2) Idempotent layout pass with same height — must NOT re-emit.
rootView.simulateCheckForKeyboardForTesting()
verify(reactContext, times(1)).emitDeviceEvent("keyboardDidShow", showParams(370.0))
// 3) IME height grows (e.g., emoji panel) — must re-emit with new height.
// This is the case the regression silently dropped.
imeBottom[0] = 420
rootView.simulateCheckForKeyboardForTesting()
verify(reactContext, times(1)).emitDeviceEvent("keyboardDidShow", showParams(420.0))
// 4) Hide — keyboardDidHide fires once.
imeVisible[0] = false
rootView.simulateCheckForKeyboardForTesting()
verify(reactContext, times(1)).emitDeviceEvent("keyboardDidHide", hideParams())
// 5) Idempotent layout pass with keyboard still hidden — must NOT re-emit.
rootView.simulateCheckForKeyboardForTesting()
verify(reactContext, times(1)).emitDeviceEvent("keyboardDidHide", hideParams())
}
private fun showParams(keyboardHeight: Double): com.facebook.react.bridge.WritableMap {
val params = Arguments.createMap()
val endCoordinates = Arguments.createMap()
params.putDouble("duration", 0.0)
endCoordinates.putDouble("width", 370.0)
endCoordinates.putDouble("screenX", 0.0)
endCoordinates.putDouble("height", keyboardHeight)
endCoordinates.putDouble("screenY", 100.0)
params.putMap("endCoordinates", endCoordinates)
params.putString("easing", "keyboard")
return params
}
private fun hideParams(): com.facebook.react.bridge.WritableMap {
val params = Arguments.createMap()
val endCoordinates = Arguments.createMap()
params.putDouble("duration", 0.0)
endCoordinates.putDouble("width", 370.0)
endCoordinates.putDouble("screenX", 0.0)
endCoordinates.putDouble("height", 0.0)
endCoordinates.putDouble("screenY", 100.0)
params.putMap("endCoordinates", endCoordinates)
params.putString("easing", "keyboard")
return params
}
}