Skip to content

Commit 93ae838

Browse files
camilopiresclaude
andcommitted
Test the Android contrast listener and events with Robolectric
Robolectric doesn't call contrast listeners, so on API 34 the test serves a mock UiModeManager and calls the listener the module registers. It checks that a change sends contrastLevelDidChange once (including a negative level), resuming twice registers one listener, pausing and invalidating remove it, and a change made while paused is sent on resume. On API 33 it uses Robolectric's UiModeManager and checks the level stays 0 with no event. Removing the listener registration makes five of the seven tests fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 4c86af4 commit 93ae838

1 file changed

Lines changed: 164 additions & 0 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
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.modules.accessibilityinfo
9+
10+
import android.app.UiModeManager
11+
import android.content.Context
12+
import android.content.ContextWrapper
13+
import android.os.Build
14+
import com.facebook.react.bridge.BridgeReactContext
15+
import com.facebook.react.bridge.Callback
16+
import com.facebook.react.bridge.ReactTestHelper
17+
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests
18+
import com.facebook.testutils.shadows.ShadowNativeLoader
19+
import com.facebook.testutils.shadows.ShadowSoLoader
20+
import org.assertj.core.api.Assertions.assertThat
21+
import org.junit.Before
22+
import org.junit.Test
23+
import org.junit.runner.RunWith
24+
import org.mockito.ArgumentMatchers
25+
import org.mockito.kotlin.any
26+
import org.mockito.kotlin.argumentCaptor
27+
import org.mockito.kotlin.mock
28+
import org.mockito.kotlin.never
29+
import org.mockito.kotlin.spy
30+
import org.mockito.kotlin.times
31+
import org.mockito.kotlin.verify
32+
import org.mockito.kotlin.whenever
33+
import org.robolectric.RobolectricTestRunner
34+
import org.robolectric.RuntimeEnvironment
35+
import org.robolectric.annotation.Config
36+
37+
@RunWith(RobolectricTestRunner::class)
38+
@Config(sdk = [34], shadows = [ShadowSoLoader::class, ShadowNativeLoader::class])
39+
class AccessibilityInfoModuleTest {
40+
41+
private lateinit var uiModeManager: UiModeManager
42+
private lateinit var reactContext: BridgeReactContext
43+
private lateinit var module: AccessibilityInfoModule
44+
45+
@Before
46+
fun setUp() {
47+
ReactNativeFeatureFlagsForTests.setUp()
48+
val application = RuntimeEnvironment.getApplication()
49+
val context =
50+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
51+
uiModeManager = mock()
52+
whenever(uiModeManager.contrast).thenReturn(0f)
53+
UiModeContext(application, uiModeManager)
54+
} else {
55+
// Mockito can't mock the API 34 UiModeManager on older versions, so use Robolectric's.
56+
application
57+
}
58+
reactContext = spy(BridgeReactContext(context))
59+
reactContext.initializeWithInstance(ReactTestHelper.createMockCatalystInstance())
60+
module = AccessibilityInfoModule(reactContext)
61+
}
62+
63+
@Test
64+
fun aContrastChangeSendsContrastLevelDidChangeOnce() {
65+
module.onHostResume()
66+
val listener = registeredListener()
67+
68+
whenever(uiModeManager.contrast).thenReturn(0.5f)
69+
listener.onContrastChanged(0.5f)
70+
listener.onContrastChanged(0.5f)
71+
72+
verify(reactContext, times(1)).emitDeviceEvent(CONTRAST_LEVEL_EVENT, 0.5)
73+
assertThat(currentContrastLevel()).isEqualTo(0.5)
74+
}
75+
76+
@Test
77+
fun aNegativeContrastIsSent() {
78+
module.onHostResume()
79+
80+
whenever(uiModeManager.contrast).thenReturn(-1f)
81+
registeredListener().onContrastChanged(-1f)
82+
83+
verify(reactContext, times(1)).emitDeviceEvent(CONTRAST_LEVEL_EVENT, -1.0)
84+
}
85+
86+
@Test
87+
fun resumingTwiceRegistersOneListener() {
88+
module.onHostResume()
89+
module.onHostResume()
90+
91+
verify(uiModeManager, times(1)).addContrastChangeListener(any(), any())
92+
}
93+
94+
@Test
95+
fun pausingRemovesTheListener() {
96+
module.onHostResume()
97+
val listener = registeredListener()
98+
99+
module.onHostPause()
100+
101+
verify(uiModeManager).removeContrastChangeListener(listener)
102+
}
103+
104+
@Test
105+
fun invalidatingRemovesTheListener() {
106+
module.onHostResume()
107+
val listener = registeredListener()
108+
109+
module.invalidate()
110+
111+
verify(uiModeManager).removeContrastChangeListener(listener)
112+
}
113+
114+
@Test
115+
fun aChangeWhilePausedIsSentOnResume() {
116+
module.onHostResume()
117+
module.onHostPause()
118+
119+
whenever(uiModeManager.contrast).thenReturn(1f)
120+
module.onHostResume()
121+
122+
verify(reactContext, times(1)).emitDeviceEvent(CONTRAST_LEVEL_EVENT, 1.0)
123+
}
124+
125+
@Test
126+
@Config(sdk = [33])
127+
fun theContrastLevelIsAlwaysZeroBeforeAndroid14() {
128+
module.onHostResume()
129+
module.onHostPause()
130+
module.invalidate()
131+
132+
assertThat(currentContrastLevel()).isEqualTo(0.0)
133+
verify(reactContext, never())
134+
.emitDeviceEvent(
135+
ArgumentMatchers.eq(CONTRAST_LEVEL_EVENT),
136+
ArgumentMatchers.any(),
137+
)
138+
}
139+
140+
private fun registeredListener(): UiModeManager.ContrastChangeListener {
141+
val captor = argumentCaptor<UiModeManager.ContrastChangeListener>()
142+
verify(uiModeManager).addContrastChangeListener(any(), captor.capture())
143+
return captor.firstValue
144+
}
145+
146+
private fun currentContrastLevel(): Double {
147+
var result: Double? = null
148+
module.getContrastLevel(Callback { args -> result = args[0] as Double })
149+
return checkNotNull(result)
150+
}
151+
152+
/** Serves a mock [UiModeManager], since Robolectric doesn't call contrast listeners. */
153+
private class UiModeContext(base: Context, private val uiModeManager: UiModeManager) :
154+
ContextWrapper(base) {
155+
override fun getApplicationContext(): Context = this
156+
157+
override fun getSystemService(name: String): Any? =
158+
if (name == Context.UI_MODE_SERVICE) uiModeManager else super.getSystemService(name)
159+
}
160+
161+
private companion object {
162+
const val CONTRAST_LEVEL_EVENT = "contrastLevelDidChange"
163+
}
164+
}

0 commit comments

Comments
 (0)