Skip to content

Commit 97c04e7

Browse files
committed
Add tests to shared features presenters
1 parent 2d8edcd commit 97c04e7

File tree

5 files changed

+337
-47
lines changed

5 files changed

+337
-47
lines changed

sharedfeatures/src/commonMain/kotlin/io/newm/sharedfeatures/devmenu/DevMenuPresenter.kt

Lines changed: 14 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,15 @@ package io.newm.sharedfeatures.devmenu
22

33
import androidx.compose.runtime.Composable
44
import androidx.compose.runtime.getValue
5-
import androidx.compose.runtime.mutableStateOf
6-
import androidx.compose.runtime.produceState
7-
import androidx.compose.runtime.remember
85
import androidx.compose.runtime.setValue
96
import com.slack.circuit.runtime.CircuitContext
107
import com.slack.circuit.runtime.Navigator
118
import com.slack.circuit.runtime.presenter.Presenter
129
import com.slack.circuit.runtime.screen.Screen
1310
import io.newm.sharedfeatures.screens.DevMenuItem
1411
import io.newm.sharedfeatures.screens.DevMenuMainScreen
12+
import io.newm.sharedfeatures.screens.DevMenuMainScreen.UiEvent
1513
import io.newm.sharedfeatures.screens.FeatureFlagsListScreen
16-
import kotlinx.coroutines.delay
17-
import kotlinx.coroutines.launch
1814
import me.tatarka.inject.annotations.Assisted
1915
import me.tatarka.inject.annotations.Inject
2016

@@ -23,55 +19,26 @@ class DevMenuPresenter
2319
constructor(
2420
@Assisted private val navigator: Navigator,
2521
) : Presenter<DevMenuMainScreen.UiState> {
26-
@Composable
27-
override fun present(): DevMenuMainScreen.UiState {
28-
var navigationInProgress by remember { mutableStateOf(false) }
29-
30-
val menuItems =
31-
produceState(initialValue = emptyList()) {
32-
value =
33-
listOf(
34-
DevMenuItem(
35-
title = "Feature Flags",
36-
screen = FeatureFlagsListScreen,
37-
description = "Manage and test feature flags",
38-
),
39-
// Add more items as needed
40-
)
41-
}.value
22+
val menuItems =
23+
listOf(
24+
DevMenuItem(
25+
title = "Feature Flags",
26+
screen = FeatureFlagsListScreen,
27+
description = "Manage and test feature flags",
28+
),
29+
)
4230

43-
return DevMenuMainScreen.UiState.Content(
31+
@Composable
32+
override fun present(): DevMenuMainScreen.UiState =
33+
DevMenuMainScreen.UiState.Content(
4434
menuItems = menuItems,
4535
onEvent = { event ->
4636
when (event) {
47-
is DevMenuMainScreen.UiEvent.OnItemClick -> {
48-
// Fix navigation issue with debouncing
49-
if (!navigationInProgress) {
50-
navigationInProgress = true
51-
try {
52-
navigator.goTo(event.screen)
53-
} catch (e: Exception) {
54-
println("Navigation error: ${e.message}")
55-
}
56-
// Reset navigation state after delay
57-
kotlinx.coroutines
58-
.CoroutineScope(kotlinx.coroutines.Dispatchers.Main)
59-
.launch {
60-
delay(1000)
61-
navigationInProgress = false
62-
}
63-
}
64-
}
65-
66-
DevMenuMainScreen.UiEvent.OnBack -> {
67-
if (!navigationInProgress) {
68-
navigator.pop()
69-
}
70-
}
37+
is UiEvent.OnItemClick -> navigator.goTo(event.screen)
38+
UiEvent.OnBack -> navigator.pop()
7139
}
7240
},
7341
)
74-
}
7542
}
7643

7744
class DevMenuPresenterFactory
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package io.newm.sharedfeatures.devmenu
2+
3+
import com.slack.circuit.test.test
4+
import com.varabyte.truthish.assertThat
5+
import io.newm.sharedfeatures.fakes.FakeNavigator
6+
import io.newm.sharedfeatures.screens.DevMenuMainScreen.UiEvent
7+
import io.newm.sharedfeatures.screens.DevMenuMainScreen.UiState
8+
import io.newm.sharedfeatures.screens.FeatureFlagsListScreen
9+
import kotlinx.coroutines.test.runTest
10+
import kotlin.test.BeforeTest
11+
import kotlin.test.Test
12+
13+
class DevMenuPresenterTest {
14+
private lateinit var navigator: FakeNavigator
15+
16+
@BeforeTest
17+
fun setup() {
18+
navigator = FakeNavigator()
19+
}
20+
21+
@Test
22+
fun `initial state has correct menu items`() =
23+
runTest {
24+
val presenter = DevMenuPresenter(navigator)
25+
26+
presenter.test {
27+
val state = awaitItem() as UiState.Content
28+
29+
assertThat(state.menuItems.single().screen).isEqualTo(FeatureFlagsListScreen)
30+
}
31+
}
32+
33+
@Test
34+
fun `clicking feature flags navigates to feature flags screen`() =
35+
runTest {
36+
val presenter = DevMenuPresenter(navigator)
37+
38+
presenter.test {
39+
val state = awaitItem() as UiState.Content
40+
41+
state.onEvent(UiEvent.OnItemClick(FeatureFlagsListScreen))
42+
43+
assertThat(navigator.goToHistory.single()).isEqualTo(FeatureFlagsListScreen)
44+
}
45+
}
46+
47+
@Test
48+
fun `back event navigates back`() =
49+
runTest {
50+
val presenter = DevMenuPresenter(navigator)
51+
52+
presenter.test {
53+
val state = awaitItem() as UiState.Content
54+
55+
state.onEvent(UiEvent.OnBack)
56+
57+
assertThat(navigator.popHistory).hasSize(1)
58+
}
59+
}
60+
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package io.newm.sharedfeatures.devmenu
2+
3+
import com.slack.circuit.test.test
4+
import com.varabyte.truthish.assertThat
5+
import io.newm.shared.commonPublic.featureflags.FeatureFlags
6+
import io.newm.sharedfeatures.fakes.FakeFeatureFlagService
7+
import io.newm.sharedfeatures.fakes.FakeNavigator
8+
import io.newm.sharedfeatures.fakes.FakeNewmSharedBuildConfig
9+
import io.newm.sharedfeatures.screens.FeatureFlagsListScreen.UiEvent
10+
import io.newm.sharedfeatures.screens.FeatureFlagsListScreen.UiState
11+
import kotlinx.coroutines.test.runTest
12+
import kotlin.test.BeforeTest
13+
import kotlin.test.Test
14+
15+
class FeatureFlagsListPresenterTest {
16+
private lateinit var navigator: FakeNavigator
17+
private lateinit var featureFlagService: FakeFeatureFlagService
18+
private lateinit var buildConfig: FakeNewmSharedBuildConfig
19+
20+
@BeforeTest
21+
fun setup() {
22+
navigator = FakeNavigator()
23+
featureFlagService = FakeFeatureFlagService()
24+
buildConfig = FakeNewmSharedBuildConfig()
25+
26+
// Setup default flags
27+
featureFlagService.setAvailableFlags(FeatureFlags.ALL_FLAGS)
28+
FeatureFlags.ALL_FLAGS.forEach { featureFlagService.setFlag(it.key, it.defaultValue) }
29+
}
30+
31+
@Test
32+
fun `initial state loads flags`() =
33+
runTest {
34+
val presenter = FeatureFlagsListPresenter(navigator, featureFlagService, buildConfig)
35+
36+
presenter.test {
37+
assertThat(awaitItem()).isInstanceOf<UiState.Loading>()
38+
39+
val initialState = awaitItem()
40+
assertThat(initialState).isInstanceOf<UiState.Content>()
41+
42+
val contentState = initialState as UiState.Content
43+
assertThat(contentState.flags).isNotEmpty()
44+
assertThat(contentState.environmentInfo.environment)
45+
.isEqualTo("Production") // Default in fake is !isStagingMode
46+
}
47+
}
48+
49+
@Test
50+
fun `toggling flag updates local override`() =
51+
runTest {
52+
val presenter = FeatureFlagsListPresenter(navigator, featureFlagService, buildConfig)
53+
54+
presenter.test {
55+
awaitItem() // Loading
56+
val initialState = awaitItem() as UiState.Content
57+
val firstFlag = initialState.flags.first()
58+
59+
initialState.onEvent(
60+
UiEvent.OnFlagToggled(firstFlag.featureFlag.key, !firstFlag.effectiveValue),
61+
)
62+
63+
// Verify override was set in service
64+
val override = featureFlagService.getLocalOverride(firstFlag.featureFlag.key)
65+
assertThat(override).isEqualTo(!firstFlag.effectiveValue)
66+
}
67+
}
68+
69+
@Test
70+
fun `resetting flag removes local override`() =
71+
runTest {
72+
val presenter = FeatureFlagsListPresenter(navigator, featureFlagService, buildConfig)
73+
74+
val flag = FeatureFlags.ALL_FLAGS.first()
75+
featureFlagService.setLocalOverride(flag.key, !flag.defaultValue)
76+
77+
presenter.test {
78+
awaitItem() // Loading
79+
val initialState = awaitItem() as UiState.Content
80+
val flagItem = initialState.flags.first { it.featureFlag.key == flag.key }
81+
assertThat(flagItem.isOverridden).isTrue()
82+
83+
initialState.onEvent(UiEvent.OnResetFlag(flag.key))
84+
85+
assertThat(featureFlagService.getLocalOverride(flag.key)).isNull()
86+
}
87+
}
88+
89+
@Test
90+
fun `resetting all flags removes all overrides`() =
91+
runTest {
92+
val presenter = FeatureFlagsListPresenter(navigator, featureFlagService, buildConfig)
93+
94+
featureFlagService.setLocalOverride(FeatureFlags.ALL_FLAGS[0].key, true)
95+
featureFlagService.setLocalOverride(FeatureFlags.ALL_FLAGS[1].key, false)
96+
97+
presenter.test {
98+
awaitItem() // Loading
99+
val initialState = awaitItem() as UiState.Content
100+
assertThat(initialState.flags.any { it.isOverridden }).isTrue()
101+
102+
initialState.onEvent(UiEvent.OnResetAllFlags)
103+
104+
assertThat(featureFlagService.localOverrides.value).isEmpty()
105+
}
106+
}
107+
108+
@Test
109+
fun `refreshing reloads flags`() =
110+
runTest {
111+
val presenter = FeatureFlagsListPresenter(navigator, featureFlagService, buildConfig)
112+
113+
presenter.test {
114+
awaitItem() // Loading
115+
val initialState = awaitItem() as UiState.Content
116+
117+
// Change a value in the service to verify refresh picks it up
118+
val flag = FeatureFlags.ALL_FLAGS.first()
119+
featureFlagService.setFlag(flag.key, !flag.defaultValue)
120+
121+
initialState.onEvent(UiEvent.OnRefresh)
122+
123+
// Expect the state to settle
124+
val finalState = expectMostRecentItem() as UiState.Content
125+
assertThat(finalState.isRefreshing).isFalse()
126+
127+
val updatedFlag = finalState.flags.first { it.featureFlag.key == flag.key }
128+
assertThat(updatedFlag.remoteValue).isEqualTo(!flag.defaultValue)
129+
}
130+
}
131+
132+
@Test
133+
fun `back navigates back`() =
134+
runTest {
135+
val presenter = FeatureFlagsListPresenter(navigator, featureFlagService, buildConfig)
136+
137+
presenter.test {
138+
awaitItem() // Loading
139+
val initialState = awaitItem() as UiState.Content
140+
141+
initialState.onEvent(UiEvent.OnBack)
142+
143+
assertThat(navigator.popHistory).isNotEmpty()
144+
}
145+
}
146+
}

0 commit comments

Comments
 (0)