BIT-150: Add more comprehensive list of settings rows. (#155)

Co-authored-by: David Perez <david@livefront.com>
This commit is contained in:
Brian Yencho 2023-10-25 09:59:49 -05:00 committed by Álison Fernandes
parent 207bed42ed
commit 8bdda9bffd
5 changed files with 287 additions and 34 deletions

View file

@ -3,26 +3,40 @@ package com.x8bit.bitwarden.ui.platform.feature.settings
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.components.BitwardenMediumTopAppBar
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
/**
* Displays the settings screen.
@ -38,21 +52,35 @@ fun SettingsScreen(
SettingsEvent.NavigateAccountSecurity -> onNavigateToAccountSecurity.invoke()
}
}
Column(
Modifier
.fillMaxSize()
.background(color = MaterialTheme.colorScheme.surface),
) {
BitwardenMediumTopAppBar(
title = stringResource(id = R.string.settings),
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(),
)
SettingsRow(
text = R.string.account.asText(),
onClick = remember(viewModel) {
{ viewModel.trySendAction(SettingsAction.AccountSecurityClick) }
},
)
val scrollBehavior =
TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())
Scaffold(
topBar = {
BitwardenMediumTopAppBar(
title = stringResource(id = R.string.settings),
scrollBehavior = scrollBehavior,
)
},
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.fillMaxSize()
.background(color = MaterialTheme.colorScheme.surface)
.verticalScroll(state = rememberScrollState()),
) {
Settings.values().forEach {
SettingsRow(
text = it.text,
onClick = remember(viewModel) {
{ viewModel.trySendAction(SettingsAction.SettingsClick(it)) }
},
)
}
}
}
}
@ -61,17 +89,61 @@ private fun SettingsRow(
text: Text,
onClick: () -> Unit,
) {
Text(
Box(
contentAlignment = Alignment.BottomCenter,
modifier = Modifier
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(color = MaterialTheme.colorScheme.primary),
onClick = onClick,
),
) {
Row(
modifier = Modifier
.defaultMinSize(minHeight = 56.dp)
.padding(start = 16.dp, end = 24.dp, top = 8.dp, bottom = 8.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier
.padding(end = 16.dp)
.weight(1f),
text = text(),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
)
.padding(horizontal = 16.dp, vertical = 16.dp)
.fillMaxWidth(),
text = text(),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
)
Icon(
painter = painterResource(id = R.drawable.ic_navigate_next),
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurface,
)
}
HorizontalDivider(
modifier = Modifier.padding(start = 16.dp),
thickness = 1.dp,
color = MaterialTheme.colorScheme.outlineVariant,
)
}
}
@Preview
@Composable
private fun SettingsRows_preview() {
BitwardenTheme {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxSize()
.background(color = MaterialTheme.colorScheme.surface),
) {
Settings.values().forEach {
SettingsRow(
text = it.text,
onClick = { },
)
}
}
}
}

View file

@ -1,6 +1,10 @@
package com.x8bit.bitwarden.ui.platform.feature.settings
import androidx.compose.material3.Text
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@ -12,11 +16,35 @@ class SettingsViewModel @Inject constructor() : BaseViewModel<Unit, SettingsEven
initialState = Unit,
) {
override fun handleAction(action: SettingsAction): Unit = when (action) {
SettingsAction.AccountSecurityClick -> handleAccountSecurityClick()
is SettingsAction.SettingsClick -> handleAccountSecurityClick(action)
}
private fun handleAccountSecurityClick() {
sendEvent(SettingsEvent.NavigateAccountSecurity)
private fun handleAccountSecurityClick(action: SettingsAction.SettingsClick) {
when (action.settings) {
Settings.ACCOUNT_SECURITY -> {
sendEvent(SettingsEvent.NavigateAccountSecurity)
}
Settings.AUTO_FILL -> {
// TODO: BIT-927 Launch auto-fill UI
}
Settings.VAULT -> {
// TODO: BIT-928 Launch vault UI
}
Settings.APPEARANCE -> {
// TODO: BIT-929 Launch appearance UI
}
Settings.OTHER -> {
// TODO: BIT-930 Launch other UI
}
Settings.ABOUT -> {
// TODO: BIT-931 Launch about UI
}
}
}
}
@ -35,7 +63,24 @@ sealed class SettingsEvent {
*/
sealed class SettingsAction {
/**
* User clicked account security.
* User clicked a settings row.
*/
data object AccountSecurityClick : SettingsAction()
data class SettingsClick(
val settings: Settings,
) : SettingsAction()
}
/**
* Enum representing the settings rows, such as "account security" or "vault".
*
* @property text The [Text] of the string that represents the label of each setting.
*/
// TODO: BIT-944 Missing correct resources for "Account Security", "Vault", and "Appearance".
enum class Settings(val text: Text) {
ACCOUNT_SECURITY(R.string.security.asText()),
AUTO_FILL(R.string.autofill.asText()),
VAULT(R.string.vaults.asText()),
APPEARANCE(R.string.language.asText()),
OTHER(R.string.other.asText()),
ABOUT(R.string.about.asText()),
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportHeight="20"
android:viewportWidth="20">
<path
android:fillColor="#1B1B1F"
android:pathData="M14.697,10.431C15.082,10.052 15.014,9.888 14.634,9.519C14.634,9.519 7.04,1.619 6.524,1.062C6.008,0.504 6.764,-0.522 7.536,0.317C8.309,1.157 15.759,8.887 15.759,8.887C16.301,9.553 16.301,10.461 15.759,11.126L7.644,19.589C6.792,20.527 5.843,19.68 6.628,18.859C9.647,15.7 14.697,10.431 14.697,10.431Z" />
</vector>

View file

@ -13,10 +13,10 @@ import org.junit.Test
class SettingsScreenTest : BaseComposeTest() {
@Test
fun `on account row click should emit AccountSecurityClick`() {
fun `on about row click should emit SettingsClick`() {
val viewModel = mockk<SettingsViewModel> {
every { eventFlow } returns emptyFlow()
every { trySendAction(SettingsAction.AccountSecurityClick) } returns Unit
every { trySendAction(SettingsAction.SettingsClick(Settings.ABOUT)) } returns Unit
}
composeTestRule.setContent {
SettingsScreen(
@ -24,8 +24,90 @@ class SettingsScreenTest : BaseComposeTest() {
onNavigateToAccountSecurity = { },
)
}
composeTestRule.onNodeWithText("Account").performClick()
verify { viewModel.trySendAction(SettingsAction.AccountSecurityClick) }
composeTestRule.onNodeWithText("About").performClick()
verify { viewModel.trySendAction(SettingsAction.SettingsClick(Settings.ABOUT)) }
}
@Test
fun `on account security row click should emit SettingsClick`() {
val viewModel = mockk<SettingsViewModel> {
every { eventFlow } returns emptyFlow()
every {
trySendAction(SettingsAction.SettingsClick(Settings.ACCOUNT_SECURITY))
} returns Unit
}
composeTestRule.setContent {
SettingsScreen(
viewModel = viewModel,
onNavigateToAccountSecurity = { },
)
}
composeTestRule.onNodeWithText("Security").performClick()
verify { viewModel.trySendAction(SettingsAction.SettingsClick(Settings.ACCOUNT_SECURITY)) }
}
@Test
fun `on appearance row click should emit SettingsClick`() {
val viewModel = mockk<SettingsViewModel> {
every { eventFlow } returns emptyFlow()
every { trySendAction(SettingsAction.SettingsClick(Settings.APPEARANCE)) } returns Unit
}
composeTestRule.setContent {
SettingsScreen(
viewModel = viewModel,
onNavigateToAccountSecurity = { },
)
}
composeTestRule.onNodeWithText("Language").performClick()
verify { viewModel.trySendAction(SettingsAction.SettingsClick(Settings.APPEARANCE)) }
}
@Test
fun `on auto-fill row click should emit SettingsClick`() {
val viewModel = mockk<SettingsViewModel> {
every { eventFlow } returns emptyFlow()
every { trySendAction(SettingsAction.SettingsClick(Settings.AUTO_FILL)) } returns Unit
}
composeTestRule.setContent {
SettingsScreen(
viewModel = viewModel,
onNavigateToAccountSecurity = { },
)
}
composeTestRule.onNodeWithText("Auto-fill").performClick()
verify { viewModel.trySendAction(SettingsAction.SettingsClick(Settings.AUTO_FILL)) }
}
@Test
fun `on other row click should emit SettingsClick`() {
val viewModel = mockk<SettingsViewModel> {
every { eventFlow } returns emptyFlow()
every { trySendAction(SettingsAction.SettingsClick(Settings.OTHER)) } returns Unit
}
composeTestRule.setContent {
SettingsScreen(
viewModel = viewModel,
onNavigateToAccountSecurity = { },
)
}
composeTestRule.onNodeWithText("Other").performClick()
verify { viewModel.trySendAction(SettingsAction.SettingsClick(Settings.OTHER)) }
}
@Test
fun `on vault row click should emit SettingsClick`() {
val viewModel = mockk<SettingsViewModel> {
every { eventFlow } returns emptyFlow()
every { trySendAction(SettingsAction.SettingsClick(Settings.VAULT)) } returns Unit
}
composeTestRule.setContent {
SettingsScreen(
viewModel = viewModel,
onNavigateToAccountSecurity = { },
)
}
composeTestRule.onNodeWithText("Vaults").performClick()
verify { viewModel.trySendAction(SettingsAction.SettingsClick(Settings.VAULT)) }
}
@Test

View file

@ -9,11 +9,56 @@ import org.junit.jupiter.api.Test
class SettingsViewModelTest : BaseViewModelTest() {
@Test
fun `on AccountSecurityClick should emit NavigateAccountSecurity`() = runTest {
fun `on SettingsClick with ABOUT should emit nothing`() = runTest {
val viewModel = SettingsViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(SettingsAction.AccountSecurityClick)
viewModel.trySendAction(SettingsAction.SettingsClick(Settings.ABOUT))
expectNoEvents()
}
}
@Test
fun `on SettingsClick with ACCOUNT_SECURITY should emit NavigateAccountSecurity`() = runTest {
val viewModel = SettingsViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(SettingsAction.SettingsClick(Settings.ACCOUNT_SECURITY))
assertEquals(SettingsEvent.NavigateAccountSecurity, awaitItem())
}
}
@Test
fun `on SettingsClick with APPEARANCE should emit nothing`() = runTest {
val viewModel = SettingsViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(SettingsAction.SettingsClick(Settings.APPEARANCE))
expectNoEvents()
}
}
@Test
fun `on SettingsClick with AUTO_FILL should emit nothing`() = runTest {
val viewModel = SettingsViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(SettingsAction.SettingsClick(Settings.AUTO_FILL))
expectNoEvents()
}
}
@Test
fun `on SettingsClick with OTHER should emit nothing`() = runTest {
val viewModel = SettingsViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(SettingsAction.SettingsClick(Settings.OTHER))
expectNoEvents()
}
}
@Test
fun `on SettingsClick with VAULT should emit nothing`() = runTest {
val viewModel = SettingsViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(SettingsAction.SettingsClick(Settings.VAULT))
expectNoEvents()
}
}
}