Skip to content
Merged
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 @@ -2,19 +2,15 @@ package io.newm.sharedfeatures.devmenu

import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import com.slack.circuit.runtime.CircuitContext
import com.slack.circuit.runtime.Navigator
import com.slack.circuit.runtime.presenter.Presenter
import com.slack.circuit.runtime.screen.Screen
import io.newm.sharedfeatures.screens.DevMenuItem
import io.newm.sharedfeatures.screens.DevMenuMainScreen
import io.newm.sharedfeatures.screens.DevMenuMainScreen.UiEvent
import io.newm.sharedfeatures.screens.FeatureFlagsListScreen
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import me.tatarka.inject.annotations.Assisted
import me.tatarka.inject.annotations.Inject

Expand All @@ -23,55 +19,26 @@ class DevMenuPresenter
constructor(
@Assisted private val navigator: Navigator,
) : Presenter<DevMenuMainScreen.UiState> {
@Composable
override fun present(): DevMenuMainScreen.UiState {
var navigationInProgress by remember { mutableStateOf(false) }

val menuItems =
produceState(initialValue = emptyList()) {
value =
listOf(
DevMenuItem(
title = "Feature Flags",
screen = FeatureFlagsListScreen,
description = "Manage and test feature flags",
),
// Add more items as needed
)
}.value
val menuItems =
listOf(
DevMenuItem(
title = "Feature Flags",
screen = FeatureFlagsListScreen,
description = "Manage and test feature flags",
),
)

return DevMenuMainScreen.UiState.Content(
@Composable
override fun present(): DevMenuMainScreen.UiState =
DevMenuMainScreen.UiState.Content(
menuItems = menuItems,
onEvent = { event ->
when (event) {
is DevMenuMainScreen.UiEvent.OnItemClick -> {
// Fix navigation issue with debouncing
if (!navigationInProgress) {
navigationInProgress = true
try {
navigator.goTo(event.screen)
} catch (e: Exception) {
println("Navigation error: ${e.message}")
}
// Reset navigation state after delay
kotlinx.coroutines
.CoroutineScope(kotlinx.coroutines.Dispatchers.Main)
.launch {
delay(1000)
navigationInProgress = false
}
}
}

DevMenuMainScreen.UiEvent.OnBack -> {
if (!navigationInProgress) {
navigator.pop()
}
}
is UiEvent.OnItemClick -> navigator.goTo(event.screen)
UiEvent.OnBack -> navigator.pop()
}
},
)
}
}

class DevMenuPresenterFactory
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package io.newm.sharedfeatures.devmenu

import com.slack.circuit.test.test
import com.varabyte.truthish.assertThat
import io.newm.sharedfeatures.fakes.FakeNavigator
import io.newm.sharedfeatures.screens.DevMenuMainScreen.UiEvent
import io.newm.sharedfeatures.screens.DevMenuMainScreen.UiState
import io.newm.sharedfeatures.screens.FeatureFlagsListScreen
import kotlinx.coroutines.test.runTest
import kotlin.test.BeforeTest
import kotlin.test.Test

class DevMenuPresenterTest {
private lateinit var navigator: FakeNavigator

@BeforeTest
fun setup() {
navigator = FakeNavigator()
}

@Test
fun `initial state has correct menu items`() =
runTest {
val presenter = DevMenuPresenter(navigator)

presenter.test {
val state = awaitItem() as UiState.Content

assertThat(state.menuItems.single().screen).isEqualTo(FeatureFlagsListScreen)
}
}

@Test
fun `clicking feature flags navigates to feature flags screen`() =
runTest {
val presenter = DevMenuPresenter(navigator)

presenter.test {
val state = awaitItem() as UiState.Content

state.onEvent(UiEvent.OnItemClick(FeatureFlagsListScreen))

assertThat(navigator.goToHistory.single()).isEqualTo(FeatureFlagsListScreen)
}
}

@Test
fun `back event navigates back`() =
runTest {
val presenter = DevMenuPresenter(navigator)

presenter.test {
val state = awaitItem() as UiState.Content

state.onEvent(UiEvent.OnBack)

assertThat(navigator.popHistory).hasSize(1)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package io.newm.sharedfeatures.devmenu

import com.slack.circuit.test.test
import com.varabyte.truthish.assertThat
import io.newm.shared.commonPublic.featureflags.FeatureFlags
import io.newm.sharedfeatures.fakes.FakeFeatureFlagService
import io.newm.sharedfeatures.fakes.FakeNavigator
import io.newm.sharedfeatures.fakes.FakeNewmSharedBuildConfig
import io.newm.sharedfeatures.screens.FeatureFlagsListScreen.UiEvent
import io.newm.sharedfeatures.screens.FeatureFlagsListScreen.UiState
import kotlinx.coroutines.test.runTest
import kotlin.test.BeforeTest
import kotlin.test.Test

class FeatureFlagsListPresenterTest {
private lateinit var navigator: FakeNavigator
private lateinit var featureFlagService: FakeFeatureFlagService
private lateinit var buildConfig: FakeNewmSharedBuildConfig

@BeforeTest
fun setup() {
navigator = FakeNavigator()
featureFlagService = FakeFeatureFlagService()
buildConfig = FakeNewmSharedBuildConfig()

// Setup default flags
featureFlagService.setAvailableFlags(FeatureFlags.ALL_FLAGS)
FeatureFlags.ALL_FLAGS.forEach { featureFlagService.setFlag(it.key, it.defaultValue) }
}

@Test
fun `initial state loads flags`() =
runTest {
val presenter = FeatureFlagsListPresenter(navigator, featureFlagService, buildConfig)

presenter.test {
assertThat(awaitItem()).isInstanceOf<UiState.Loading>()

val initialState = awaitItem()
assertThat(initialState).isInstanceOf<UiState.Content>()

val contentState = initialState as UiState.Content
assertThat(contentState.flags).isNotEmpty()
assertThat(contentState.environmentInfo.environment)
.isEqualTo("Production") // Default in fake is !isStagingMode
}
}

@Test
fun `toggling flag updates local override`() =
runTest {
val presenter = FeatureFlagsListPresenter(navigator, featureFlagService, buildConfig)

presenter.test {
awaitItem() // Loading
val initialState = awaitItem() as UiState.Content
val firstFlag = initialState.flags.first()

initialState.onEvent(
UiEvent.OnFlagToggled(firstFlag.featureFlag.key, !firstFlag.effectiveValue),
)

// Verify override was set in service
val override = featureFlagService.getLocalOverride(firstFlag.featureFlag.key)
assertThat(override).isEqualTo(!firstFlag.effectiveValue)
}
}

@Test
fun `resetting flag removes local override`() =
runTest {
val presenter = FeatureFlagsListPresenter(navigator, featureFlagService, buildConfig)

val flag = FeatureFlags.ALL_FLAGS.first()
featureFlagService.setLocalOverride(flag.key, !flag.defaultValue)

presenter.test {
awaitItem() // Loading
val initialState = awaitItem() as UiState.Content
val flagItem = initialState.flags.first { it.featureFlag.key == flag.key }
assertThat(flagItem.isOverridden).isTrue()

initialState.onEvent(UiEvent.OnResetFlag(flag.key))

assertThat(featureFlagService.getLocalOverride(flag.key)).isNull()
}
}

@Test
fun `resetting all flags removes all overrides`() =
runTest {
val presenter = FeatureFlagsListPresenter(navigator, featureFlagService, buildConfig)

featureFlagService.setLocalOverride(FeatureFlags.ALL_FLAGS[0].key, true)
featureFlagService.setLocalOverride(FeatureFlags.ALL_FLAGS[1].key, false)

presenter.test {
awaitItem() // Loading
val initialState = awaitItem() as UiState.Content
assertThat(initialState.flags.any { it.isOverridden }).isTrue()

initialState.onEvent(UiEvent.OnResetAllFlags)

assertThat(featureFlagService.localOverrides.value).isEmpty()
}
}

@Test
fun `refreshing reloads flags`() =
runTest {
val presenter = FeatureFlagsListPresenter(navigator, featureFlagService, buildConfig)

presenter.test {
awaitItem() // Loading
val initialState = awaitItem() as UiState.Content

// Change a value in the service to verify refresh picks it up
val flag = FeatureFlags.ALL_FLAGS.first()
featureFlagService.setFlag(flag.key, !flag.defaultValue)

initialState.onEvent(UiEvent.OnRefresh)

// Expect the state to settle
val finalState = expectMostRecentItem() as UiState.Content
assertThat(finalState.isRefreshing).isFalse()

val updatedFlag = finalState.flags.first { it.featureFlag.key == flag.key }
assertThat(updatedFlag.remoteValue).isEqualTo(!flag.defaultValue)
}
}

@Test
fun `back navigates back`() =
runTest {
val presenter = FeatureFlagsListPresenter(navigator, featureFlagService, buildConfig)

presenter.test {
awaitItem() // Loading
val initialState = awaitItem() as UiState.Content

initialState.onEvent(UiEvent.OnBack)

assertThat(navigator.popHistory).isNotEmpty()
}
}
}
Loading