BIT-929: Add UI for Appearance screen (#478)

This commit is contained in:
Caleb Derosier
2024-01-04 13:15:47 -07:00
committed by GitHub
parent 1379c91bfe
commit 5efe7864f8
4 changed files with 379 additions and 25 deletions

View File

@@ -1,46 +1,110 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.appearance
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.filterToOne
import androidx.compose.ui.test.hasAnyAncestor
import androidx.compose.ui.test.isDialog
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
class AppearanceScreenTest : BaseComposeTest() {
@Test
fun `on back click should send BackClick`() {
val viewModel: AppearanceViewModel = mockk {
every { eventFlow } returns emptyFlow()
every { trySendAction(AppearanceAction.BackClick) } returns Unit
}
private var haveCalledNavigateBack = false
private val mutableEventFlow = bufferedMutableSharedFlow<AppearanceEvent>()
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
private val viewModel = mockk<AppearanceViewModel>(relaxed = true) {
every { eventFlow } returns mutableEventFlow
every { stateFlow } returns mutableStateFlow
}
@Before
fun setup() {
composeTestRule.setContent {
AppearanceScreen(
onNavigateBack = { haveCalledNavigateBack = true },
viewModel = viewModel,
onNavigateBack = { },
)
}
}
@Test
fun `on back click should send BackClick`() {
composeTestRule.onNodeWithContentDescription("Back").performClick()
verify { viewModel.trySendAction(AppearanceAction.BackClick) }
}
@Test
fun `on NavigateAbout should call onNavigateToAbout`() {
var haveCalledNavigateBack = false
val viewModel = mockk<AppearanceViewModel> {
every { eventFlow } returns flowOf(AppearanceEvent.NavigateBack)
}
composeTestRule.setContent {
AppearanceScreen(
viewModel = viewModel,
onNavigateBack = { haveCalledNavigateBack = true },
fun `on language row click should display language selection dialog`() {
composeTestRule.onNodeWithText("Language").performClick()
composeTestRule
.onAllNodesWithText("Language")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
}
@Test
fun `on language selection dialog item click should send LanguageChange`() {
composeTestRule.onNodeWithText("Language").performClick()
composeTestRule.onNodeWithText("English").performClick()
verify {
viewModel.trySendAction(
AppearanceAction.LanguageChange(
newLanguage = AppearanceState.Language.ENGLISH,
),
)
}
}
@Test
fun `on theme row click should display theme selection dialog`() {
composeTestRule.onNodeWithText("Theme").performClick()
composeTestRule
.onAllNodesWithText("Theme")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
}
@Test
fun `on theme selection dialog item click should send ThemeChange`() {
composeTestRule.onNodeWithText("Theme").performClick()
composeTestRule.onNodeWithText("Dark").performClick()
verify {
viewModel.trySendAction(
AppearanceAction.ThemeChange(
newTheme = AppearanceState.Theme.DARK,
),
)
}
}
@Test
fun `on show website icons row click should send ShowWebsiteIconsToggled`() {
composeTestRule.onNodeWithText("Show website icons").performClick()
verify { viewModel.trySendAction(AppearanceAction.ShowWebsiteIconsToggle(true)) }
}
@Test
fun `on NavigateBack should call onNavigateBack`() {
mutableEventFlow.tryEmit(AppearanceEvent.NavigateBack)
assertTrue(haveCalledNavigateBack)
}
}
private val DEFAULT_STATE = AppearanceState(
language = AppearanceState.Language.DEFAULT,
showWebsiteIcons = false,
theme = AppearanceState.Theme.DEFAULT,
)

View File

@@ -1,5 +1,6 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.appearance
import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import kotlinx.coroutines.test.runTest
@@ -7,13 +8,91 @@ import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class AppearanceViewModelTest : BaseViewModelTest() {
@Test
fun `initial state should be correct when not set`() {
val viewModel = createViewModel(state = null)
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
}
@Test
fun `initial state should be correct when set`() {
val state = DEFAULT_STATE.copy(theme = AppearanceState.Theme.DARK)
val viewModel = createViewModel(state = state)
assertEquals(state, viewModel.stateFlow.value)
}
@Test
fun `on BackClick should emit NavigateBack`() = runTest {
val viewModel = AppearanceViewModel()
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(AppearanceAction.BackClick)
assertEquals(AppearanceEvent.NavigateBack, awaitItem())
}
}
@Test
fun `on LanguageChange should update state`() = runTest {
val viewModel = createViewModel()
viewModel.stateFlow.test {
assertEquals(
DEFAULT_STATE,
awaitItem(),
)
viewModel.trySendAction(
AppearanceAction.LanguageChange(AppearanceState.Language.ENGLISH),
)
assertEquals(
DEFAULT_STATE.copy(language = AppearanceState.Language.ENGLISH),
awaitItem(),
)
}
}
@Test
fun `on ShowWebsiteIconsToggle should update value in state`() = runTest {
val viewModel = createViewModel()
viewModel.stateFlow.test {
assertEquals(
DEFAULT_STATE,
awaitItem(),
)
viewModel.trySendAction(AppearanceAction.ShowWebsiteIconsToggle(true))
assertEquals(
DEFAULT_STATE.copy(showWebsiteIcons = true),
awaitItem(),
)
}
}
@Test
fun `on ThemeChange should update state`() = runTest {
val viewModel = createViewModel()
viewModel.stateFlow.test {
assertEquals(
DEFAULT_STATE,
awaitItem(),
)
viewModel.trySendAction(AppearanceAction.ThemeChange(AppearanceState.Theme.DARK))
assertEquals(
DEFAULT_STATE.copy(theme = AppearanceState.Theme.DARK),
awaitItem(),
)
}
}
private fun createViewModel(
state: AppearanceState? = null,
) = AppearanceViewModel(
savedStateHandle = SavedStateHandle().apply {
set("state", state)
},
)
companion object {
private val DEFAULT_STATE = AppearanceState(
language = AppearanceState.Language.DEFAULT,
showWebsiteIcons = false,
theme = AppearanceState.Theme.DEFAULT,
)
}
}