diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreen.kt index 88fc33f0bf..680dacbdef 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreen.kt @@ -43,6 +43,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -499,7 +500,9 @@ private fun CounterItem( Row( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .semantics(mergeDescendants = true) {} + .fillMaxWidth(), ) { Text(label) Row( @@ -510,7 +513,8 @@ private fun CounterItem( ) { Icon( Icons.Default.ArrowBack, - contentDescription = null, + // Unicode for "minus" + contentDescription = "\u2212", tint = MaterialTheme.colorScheme.primary, ) } @@ -522,7 +526,7 @@ private fun CounterItem( ) { Icon( Icons.Default.ArrowForward, - contentDescription = null, + contentDescription = "+", tint = MaterialTheme.colorScheme.primary, ) } @@ -553,6 +557,7 @@ private fun TextInputItem( CommonPadding { Column( modifier = Modifier + .semantics(mergeDescendants = true) {} .fillMaxHeight() .padding(top = 4.dp, bottom = 4.dp), verticalArrangement = Arrangement.Center, @@ -596,7 +601,9 @@ private fun SwitchItem( ) { CommonPadding { Row( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .semantics(mergeDescendants = true) {} + .fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreenTest.kt new file mode 100644 index 0000000000..d8b9cfd1c6 --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreenTest.kt @@ -0,0 +1,224 @@ +@file:Suppress("MaxLineLength") + +package com.x8bit.bitwarden.ui.tools.feature.generator + +import androidx.compose.ui.semantics.Role.Companion.Switch +import androidx.compose.ui.semantics.SemanticsProperties.Role +import androidx.compose.ui.test.SemanticsMatcher.Companion.expectValue +import androidx.compose.ui.test.filterToOne +import androidx.compose.ui.test.hasContentDescription +import androidx.compose.ui.test.hasSetTextAction +import androidx.compose.ui.test.onAllNodesWithText +import androidx.compose.ui.test.onChildren +import androidx.compose.ui.test.onFirst +import androidx.compose.ui.test.onLast +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollTo +import androidx.compose.ui.test.performTextInput +import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.emptyFlow +import org.junit.Test + +class GeneratorScreenTest : BaseComposeTest() { + private val mutableStateFlow = MutableStateFlow( + GeneratorState( + generatedText = "Placeholder", + selectedType = GeneratorState.MainType.Passcode(GeneratorState.MainType.Passcode.PasscodeType.Password()), + ), + ) + + private val viewModel = mockk(relaxed = true) { + every { eventFlow } returns emptyFlow() + every { stateFlow } returns mutableStateFlow + } + + @Test + fun `clicking a MainStateOption should send MainTypeOptionSelect action`() { + composeTestRule.setContent { + GeneratorScreen(viewModel = viewModel) + } + + // Opens the menu + composeTestRule.onAllNodesWithText(text = "Password").onFirst().performClick() + + // Choose the option from the menu + composeTestRule.onAllNodesWithText(text = "Password").onLast().performClick() + + verify { + viewModel.trySendAction(GeneratorAction.MainTypeOptionSelect(GeneratorState.MainTypeOption.PASSWORD)) + } + } + + @Test + fun `clicking a PasscodeOption should send PasscodeTypeOption action`() { + composeTestRule.setContent { + GeneratorScreen(viewModel = viewModel) + } + + // Opens the menu + composeTestRule.onAllNodesWithText(text = "Password").onLast().performClick() + + // Choose the option from the menu + composeTestRule.onAllNodesWithText(text = "Passphrase").onLast().performClick() + + verify { + viewModel.trySendAction( + GeneratorAction.MainType.Passcode.PasscodeTypeOptionSelect( + GeneratorState.MainType.Passcode.PasscodeTypeOption.PASSPHRASE, + ), + ) + } + } + + @Test + fun `in Passcode_Passphrase state, decrementing number of words should send NumWordsCounterChange action with decremented value`() { + val initialNumWords = 3 + updateState( + GeneratorState( + generatedText = "Placeholder", + selectedType = GeneratorState.MainType.Passcode(GeneratorState.MainType.Passcode.PasscodeType.Passphrase()), + ), + ) + + composeTestRule.setContent { + GeneratorScreen(viewModel = viewModel) + } + + // Unicode for "minus" used for content description + composeTestRule + .onNodeWithText("Number of words") + .onChildren() + .filterToOne(hasContentDescription("\u2212")) + .performClick() + + verify { + viewModel.trySendAction( + GeneratorAction.MainType.Passcode.PasscodeType.Passphrase.NumWordsCounterChange( + numWords = initialNumWords - 1, + ), + ) + } + } + + @Test + fun `in Passcode_Passphrase state, incrementing number of words should send NumWordsCounterChange action with incremented value`() { + val initialNumWords = 3 + updateState( + GeneratorState( + generatedText = "Placeholder", + selectedType = GeneratorState.MainType.Passcode(GeneratorState.MainType.Passcode.PasscodeType.Passphrase()), + ), + ) + + composeTestRule.setContent { + GeneratorScreen(viewModel = viewModel) + } + + composeTestRule + .onNodeWithText("Number of words") + .onChildren() + .filterToOne(hasContentDescription("+")) + .performClick() + + verify { + viewModel.trySendAction( + GeneratorAction.MainType.Passcode.PasscodeType.Passphrase.NumWordsCounterChange( + numWords = initialNumWords + 1, + ), + ) + } + } + + @Test + fun `in Passcode_Passphrase state, toggling capitalize should send ToggleCapitalizeChange action`() { + updateState( + GeneratorState( + generatedText = "Placeholder", + selectedType = GeneratorState.MainType.Passcode(GeneratorState.MainType.Passcode.PasscodeType.Passphrase()), + ), + ) + + composeTestRule.setContent { + GeneratorScreen(viewModel = viewModel) + } + + composeTestRule + .onNodeWithText("Capitalize") + .onChildren() + .filterToOne(expectValue(Role, Switch)) + .performScrollTo() + .performClick() + + verify { + viewModel.trySendAction( + GeneratorAction.MainType.Passcode.PasscodeType.Passphrase.ToggleCapitalizeChange( + capitalize = true, + ), + ) + } + } + + @Test + fun `in Passcode_Passphrase state, toggling the include number toggle should send ToggleIncludeNumberChange action`() { + updateState( + GeneratorState( + generatedText = "Placeholder", + selectedType = GeneratorState.MainType.Passcode(GeneratorState.MainType.Passcode.PasscodeType.Passphrase()), + ), + ) + + composeTestRule.setContent { + GeneratorScreen(viewModel = viewModel) + } + + composeTestRule.onNodeWithText("Include number") + .onChildren() + .filterToOne(expectValue(Role, Switch)) + .performScrollTo() + .performClick() + + verify { + viewModel.trySendAction( + GeneratorAction.MainType.Passcode.PasscodeType.Passphrase.ToggleIncludeNumberChange( + includeNumber = true, + ), + ) + } + } + + @Test + fun `in Passcode_Passphrase state, updating text in word separator should send WordSeparatorTextChange action`() { + updateState( + GeneratorState( + generatedText = "Placeholder", + selectedType = GeneratorState.MainType.Passcode(GeneratorState.MainType.Passcode.PasscodeType.Passphrase()), + ), + ) + + composeTestRule.setContent { + GeneratorScreen(viewModel = viewModel) + } + + composeTestRule.onNodeWithText("Word separator") + .onChildren() + .filterToOne(hasSetTextAction()) + .performTextInput("a") + + verify { + viewModel.trySendAction( + GeneratorAction.MainType.Passcode.PasscodeType.Passphrase.WordSeparatorTextChange( + wordSeparator = 'a', + ), + ) + } + } + + private fun updateState(state: GeneratorState) { + mutableStateFlow.value = state + } +}