PM-17838 - Add help button for authenticator key (#4697)

This commit is contained in:
Phil Cappelli
2025-02-10 13:58:26 -05:00
committed by GitHub
parent 30e882d7d1
commit eb3bc838d6
11 changed files with 239 additions and 123 deletions

View File

@@ -1,16 +1,20 @@
package com.x8bit.bitwarden.ui.platform.components.field
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
@@ -26,14 +30,19 @@ import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.focus.onFocusEvent
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalTextToolbar
import androidx.compose.ui.platform.TextToolbar
import androidx.compose.ui.semantics.CustomAccessibilityAction
import androidx.compose.ui.semantics.customActions
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.TextFieldValue
@@ -41,11 +50,13 @@ import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.PopupProperties
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.cardStyle
import com.x8bit.bitwarden.ui.platform.base.util.nullableTestTag
import com.x8bit.bitwarden.ui.platform.base.util.toPx
import com.x8bit.bitwarden.ui.platform.base.util.withLineBreaksAtWidth
import com.x8bit.bitwarden.ui.platform.components.appbar.color.bitwardenMenuItemColors
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider
import com.x8bit.bitwarden.ui.platform.components.field.color.bitwardenTextFieldColors
import com.x8bit.bitwarden.ui.platform.components.field.toolbar.BitwardenCutCopyTextToolbar
@@ -53,6 +64,7 @@ import com.x8bit.bitwarden.ui.platform.components.field.toolbar.BitwardenEmptyTe
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.model.IconResource
import com.x8bit.bitwarden.ui.platform.components.model.TextToolbarType
import com.x8bit.bitwarden.ui.platform.components.model.TooltipData
import com.x8bit.bitwarden.ui.platform.components.row.BitwardenRowOfActions
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import kotlinx.collections.immutable.ImmutableList
@@ -192,6 +204,7 @@ fun BitwardenTextField(
supportingContent: (@Composable ColumnScope.() -> Unit)?,
cardStyle: CardStyle,
modifier: Modifier = Modifier,
tooltip: TooltipData? = null,
supportingContentPadding: PaddingValues = PaddingValues(vertical = 12.dp, horizontal = 16.dp),
placeholder: String? = null,
leadingIconResource: IconResource? = null,
@@ -255,12 +268,52 @@ fun BitwardenTextField(
paddingTop = 6.dp,
paddingBottom = 0.dp,
)
.fillMaxWidth(),
.fillMaxWidth()
.semantics {
customActions = listOfNotNull(
tooltip?.let {
CustomAccessibilityAction(
label = it.contentDescription,
action = {
it.onClick()
true
},
)
},
)
},
) {
var focused by remember { mutableStateOf(false) }
TextField(
colors = bitwardenTextFieldColors(),
enabled = enabled,
label = label?.let { { Text(text = it) } },
label = label?.let {
{
Row(verticalAlignment = Alignment.CenterVertically) {
Text(text = it)
tooltip?.let {
val targetSize = if (textFieldValue.text.isEmpty() || focused) {
16.dp
} else {
12.dp
}
val size by animateDpAsState(
targetValue = targetSize,
label = "${it.contentDescription}_animation",
)
Spacer(modifier = Modifier.width(16.dp))
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_question_circle_small,
contentDescription = it.contentDescription,
onClick = it.onClick,
contentColor = BitwardenTheme.colorScheme.icon.secondary,
modifier = Modifier.size(size),
)
}
}
}
},
value = textFieldValue,
leadingIcon = leadingIconResource?.let { iconResource ->
{
@@ -298,7 +351,10 @@ fun BitwardenTextField(
visualTransformation = visualTransformation,
modifier = Modifier
.nullableTestTag(tag = textFieldTestTag)
.fillMaxWidth(),
.fillMaxWidth()
.onFocusChanged { focusState ->
focused = focusState.isFocused
},
)
supportingContent
?.let { content ->

View File

@@ -64,7 +64,11 @@ fun BitwardenClickableText(
Icon(
painter = leadingIcon,
contentDescription = null,
tint = color,
tint = if (isEnabled) {
color
} else {
BitwardenTheme.colorScheme.filledButton.foregroundDisabled
},
modifier = Modifier.size(size = 16.dp),
)
Spacer(modifier = Modifier.width(width = 8.dp))

View File

@@ -1,6 +1,5 @@
package com.x8bit.bitwarden.ui.vault.feature.addedit
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
@@ -14,6 +13,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@@ -23,7 +23,6 @@ import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.base.util.cardStyle
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
import com.x8bit.bitwarden.ui.platform.components.coachmark.CoachMarkActionText
import com.x8bit.bitwarden.ui.platform.components.coachmark.CoachMarkScope
@@ -34,8 +33,8 @@ import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.model.TooltipData
import com.x8bit.bitwarden.ui.platform.components.text.BitwardenClickableText
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditLoginTypeHandlers
@@ -281,7 +280,6 @@ private fun CoachMarkScope<AddEditItemCoachMark>.PasswordRow(
label = stringResource(id = R.string.check_password_for_data_breaches),
style = BitwardenTheme.typography.labelMedium,
onClick = loginItemTypeHandlers.onPasswordCheckerClick,
leadingIcon = painterResource(id = R.drawable.ic_camera_small),
innerPadding = PaddingValues(all = 16.dp),
cornerSize = 0.dp,
modifier = Modifier
@@ -364,69 +362,51 @@ private fun TotpRow(
onTotpSetupClick: () -> Unit,
modifier: Modifier = Modifier,
) {
if (totpKey != null) {
if (canViewTotp) {
BitwardenTextField(
label = stringResource(id = R.string.authenticator_key),
value = totpKey,
onValueChange = {},
readOnly = true,
singleLine = true,
actions = {
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_clear,
contentDescription = stringResource(id = R.string.delete),
onClick = loginItemTypeHandlers.onClearTotpKeyClick,
)
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_copy,
contentDescription = stringResource(id = R.string.copy_totp),
onClick = { loginItemTypeHandlers.onCopyTotpKeyClick(totpKey) },
)
},
supportingContentPadding = PaddingValues(),
supportingContent = {
BitwardenClickableText(
label = stringResource(id = R.string.set_up_authenticator_key),
onClick = onTotpSetupClick,
leadingIcon = painterResource(id = R.drawable.ic_plus_small),
style = BitwardenTheme.typography.labelMedium,
innerPadding = PaddingValues(all = 16.dp),
cornerSize = 0.dp,
modifier = Modifier.fillMaxWidth(),
)
},
textFieldTestTag = "LoginTotpEntry",
cardStyle = CardStyle.Full,
modifier = modifier.fillMaxWidth(),
)
} else {
BitwardenTextField(
label = stringResource(id = R.string.authenticator_key),
value = totpKey,
cardStyle = CardStyle.Full,
textFieldTestTag = "LoginTotpEntry",
onValueChange = {},
readOnly = true,
enabled = false,
singleLine = true,
modifier = modifier.fillMaxWidth(),
)
}
} else {
Column(modifier = modifier) {
Spacer(modifier = Modifier.height(8.dp))
BitwardenOutlinedButton(
label = stringResource(id = R.string.setup_totp),
icon = rememberVectorPainter(id = R.drawable.ic_light_bulb),
BitwardenTextField(
label = stringResource(id = R.string.authenticator_key),
value = totpKey.orEmpty(),
onValueChange = {},
readOnly = true,
singleLine = true,
actions = {
totpKey?.let {
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_clear,
contentDescription = stringResource(id = R.string.delete),
onClick = loginItemTypeHandlers.onClearTotpKeyClick,
)
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_copy,
contentDescription = stringResource(id = R.string.copy_totp),
onClick = { loginItemTypeHandlers.onCopyTotpKeyClick(totpKey) },
)
}
},
tooltip = TooltipData(
onClick = loginItemTypeHandlers.onAuthenticatorHelpToolTipClick,
contentDescription = stringResource(id = R.string.authenticator_key_help),
),
supportingContentPadding = PaddingValues(),
supportingContent = {
BitwardenClickableText(
label = stringResource(id = R.string.set_up_authenticator_key),
onClick = onTotpSetupClick,
leadingIcon = painterResource(id = R.drawable.ic_camera_small),
style = BitwardenTheme.typography.labelMedium,
innerPadding = PaddingValues(all = 16.dp),
isEnabled = canViewTotp,
cornerSize = 0.dp,
modifier = Modifier
.testTag("SetupTotpButton")
.fillMaxWidth(),
.fillMaxWidth()
.testTag("SetupTotpButton"),
)
}
}
},
textFieldTestTag = "LoginTotpEntry",
cardStyle = CardStyle.Full,
modifier = modifier
.fillMaxWidth()
.focusProperties { canFocus = false },
)
}
@Composable

View File

@@ -130,6 +130,12 @@ fun VaultAddEditScreen(
)
}
is VaultAddEditEvent.NavigateToAuthenticatorKeyTooltipUri -> {
intentManager.launchUri(
"https://bitwarden.com/help/integrated-authenticator".toUri(),
)
}
is VaultAddEditEvent.CompleteFido2Registration -> {
fido2CompletionManager.completeFido2Registration(event.result)
}

View File

@@ -991,6 +991,10 @@ class VaultAddEditViewModel @Inject constructor(
VaultAddEditAction.ItemType.LoginType.StartLearnAboutLogins -> {
handleStartLearnAboutLogins()
}
VaultAddEditAction.ItemType.LoginType.AuthenticatorHelpToolTipClick -> {
handleAuthenticatorHelpToolTipClick()
}
}
}
@@ -1823,6 +1827,10 @@ class VaultAddEditViewModel @Inject constructor(
getRequestAndRegisterCredential()
}
private fun handleAuthenticatorHelpToolTipClick() {
sendEvent(VaultAddEditEvent.NavigateToAuthenticatorKeyTooltipUri)
}
//endregion Internal Type Handlers
//region Utility Functions
@@ -2649,6 +2657,11 @@ sealed class VaultAddEditEvent {
* Start the coach mark guided tour of the add login content.
*/
data object StartAddLoginItemCoachMarkTour : VaultAddEditEvent()
/**
* Navigate the user to the tooltip URI for Authenticator key help.
*/
data object NavigateToAuthenticatorKeyTooltipUri : VaultAddEditEvent()
}
/**
@@ -2969,6 +2982,11 @@ sealed class VaultAddEditAction {
* User has dismissed the learn about logins card.
*/
data object LearnAboutLoginsDismissed : LoginType()
/**
* User has clicked the call to action on the authenticator help tooltip.
*/
data object AuthenticatorHelpToolTipClick : LoginType()
}
/**

View File

@@ -26,6 +26,8 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriItem
* clicked.
* @property onClearFido2CredentialClick Handles the action when the clear Fido2 credential button
* is clicked.
* @property onAuthenticatorHelpToolTipClick Handles the action when the authenticator help tooltip
* is clicked.
*/
@Suppress("LongParameterList")
data class VaultAddEditLoginTypeHandlers(
@@ -44,6 +46,7 @@ data class VaultAddEditLoginTypeHandlers(
val onClearFido2CredentialClick: () -> Unit,
val onStartLoginCoachMarkTour: () -> Unit,
val onDismissLearnAboutLoginsCard: () -> Unit,
val onAuthenticatorHelpToolTipClick: () -> Unit,
) {
@Suppress("UndocumentedPublicClass")
companion object {
@@ -103,6 +106,11 @@ data class VaultAddEditLoginTypeHandlers(
onAddNewUriClick = {
viewModel.trySendAction(VaultAddEditAction.ItemType.LoginType.AddNewUriClick)
},
onAuthenticatorHelpToolTipClick = {
viewModel.trySendAction(
VaultAddEditAction.ItemType.LoginType.AuthenticatorHelpToolTipClick,
)
},
onCopyTotpKeyClick = { totpKey ->
viewModel.trySendAction(
VaultAddEditAction.ItemType.LoginType.CopyTotpKeyClick(

View File

@@ -13,9 +13,6 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@@ -44,8 +41,10 @@ import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.CustomAccessibilityAction
import androidx.compose.ui.semantics.customActions
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@@ -55,9 +54,9 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.LocalLifecycleOwner
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.toAnnotatedString
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.text.BitwardenClickableText
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import com.x8bit.bitwarden.ui.platform.theme.LocalBitwardenColorScheme
@@ -87,7 +86,7 @@ fun QrCodeScanScreen(
val context = LocalContext.current
val onEnterCodeManuallyClick = remember(viewModel) {
val onEnterKeyManuallyClick = remember(viewModel) {
{ viewModel.trySendAction(QrCodeScanAction.ManualEntryTextClick) }
}
@@ -135,11 +134,11 @@ fun QrCodeScanScreen(
if (LocalConfiguration.current.isPortrait) {
PortraitQRCodeContent(
onEnterCodeManuallyClick = onEnterCodeManuallyClick,
onEnterKeyManuallyClick = onEnterKeyManuallyClick,
)
} else {
LandscapeQRCodeContent(
onEnterCodeManuallyClick = onEnterCodeManuallyClick,
onEnterKeyManuallyClick = onEnterKeyManuallyClick,
)
}
}
@@ -148,7 +147,7 @@ fun QrCodeScanScreen(
@Composable
private fun PortraitQRCodeContent(
onEnterCodeManuallyClick: () -> Unit,
onEnterKeyManuallyClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
@@ -178,8 +177,8 @@ private fun PortraitQRCodeContent(
modifier = Modifier.padding(horizontal = 16.dp),
)
BottomClickableText(
onEnterCodeManuallyClick = onEnterCodeManuallyClick,
EnterKeyManuallyText(
onEnterKeyManuallyClick = onEnterKeyManuallyClick,
)
Spacer(modifier = Modifier.navigationBarsPadding())
}
@@ -188,7 +187,7 @@ private fun PortraitQRCodeContent(
@Composable
private fun LandscapeQRCodeContent(
onEnterCodeManuallyClick: () -> Unit,
onEnterKeyManuallyClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Row(
@@ -218,8 +217,8 @@ private fun LandscapeQRCodeContent(
style = BitwardenTheme.typography.bodySmall,
)
BottomClickableText(
onEnterCodeManuallyClick = onEnterCodeManuallyClick,
EnterKeyManuallyText(
onEnterKeyManuallyClick = onEnterKeyManuallyClick,
)
}
}
@@ -403,28 +402,33 @@ private fun QrCodeSquare(
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun BottomClickableText(
onEnterCodeManuallyClick: () -> Unit,
private fun EnterKeyManuallyText(
onEnterKeyManuallyClick: () -> Unit,
modifier: Modifier = Modifier,
) {
FlowRow(
modifier = modifier,
verticalArrangement = Arrangement.SpaceEvenly,
) {
Text(
modifier = Modifier.padding(vertical = 4.dp),
text = stringResource(id = R.string.cannot_scan_qr_code),
color = Color.White,
style = BitwardenTheme.typography.bodyMedium,
)
BitwardenClickableText(
label = stringResource(id = R.string.enter_key_manually),
style = BitwardenTheme.typography.labelLarge,
innerPadding = PaddingValues(vertical = 4.dp, horizontal = 12.dp),
onClick = onEnterCodeManuallyClick,
modifier = Modifier.testTag("EnterKeyManuallyButton"),
)
val enterKeyManuallyString = stringResource(R.string.enter_key_manually)
val annotatedLinkString = R.string.cannot_scan_qr_code_enter_key_manually.toAnnotatedString {
when (it) {
"enterKeyManually" -> onEnterKeyManuallyClick()
}
}
Text(
text = annotatedLinkString,
style = BitwardenTheme.typography.bodySmall,
color = Color.White,
textAlign = TextAlign.Center,
modifier = modifier
.semantics {
customActions = listOf(
CustomAccessibilityAction(
label = enterKeyManuallyString,
action = {
onEnterKeyManuallyClick()
true
},
),
)
},
)
}

View File

@@ -709,8 +709,9 @@ Scanning will happen automatically.</string>
<string name="cannot_add_authenticator_key">Cannot add authenticator key?</string>
<string name="cannot_add_authenticator_key_scan_qr_code">Cannot add authenticator key? <annotation link="scanQrCode">Scan QR Code</annotation></string>
<string name="scan_qr_code">Scan QR Code</string>
<string name="cannot_scan_qr_code">Cannot scan QR Code?</string>
<string name="cannot_scan_qr_code_enter_key_manually">Cannot scan QR code? <annotation link="enterKeyManually">Enter key manually</annotation></string>
<string name="authenticator_key_scanner">Authenticator key</string>
<string name="authenticator_key_help">Authenticator key help</string>
<string name="enter_key_manually">Enter key manually</string>
<string name="add_totp">Add TOTP</string>
<string name="setup_totp">Set up TOTP</string>

View File

@@ -154,6 +154,16 @@ class VaultAddEditScreenTest : BaseComposeTest() {
}
}
@Test
fun `on NavigateToAuthenticatorKeyTooltipUri Event should invoke IntentManager`() {
mutableEventFlow.tryEmit(VaultAddEditEvent.NavigateToAuthenticatorKeyTooltipUri)
verify {
intentManager.launchUri(
"https://bitwarden.com/help/integrated-authenticator".toUri(),
)
}
}
@Test
fun `on NavigateToQrCodeScan event should invoke NavigateToQrCodeScan`() {
mutableEventFlow.tryEmit(VaultAddEditEvent.NavigateToQrCodeScan)
@@ -1020,10 +1030,6 @@ class VaultAddEditScreenTest : BaseComposeTest() {
mutableStateFlow.update { currentState ->
updateLoginType(currentState) { copy(totp = null) }
}
composeTestRule
.onNodeWithText("Authenticator key")
.assertDoesNotExist()
}
@Suppress("MaxLineLength")
@@ -1090,13 +1096,13 @@ class VaultAddEditScreenTest : BaseComposeTest() {
}
@Test
fun `in ItemType_Login state SetupTOTP button should be present based on state`() {
fun `in ItemType_Login state Set up authenticator key button should always be present`() {
mutableStateFlow.update { currentState ->
updateLoginType(currentState) { copy(totp = null) }
}
composeTestRule
.onNodeWithTextAfterScroll(text = "Set up TOTP")
.onNodeWithTextAfterScroll(text = "Set up authenticator key")
.assertIsDisplayed()
mutableStateFlow.update { currentState ->
@@ -1104,8 +1110,8 @@ class VaultAddEditScreenTest : BaseComposeTest() {
}
composeTestRule
.onNodeWithText(text = "Set up TOTP")
.assertIsNotDisplayed()
.onNodeWithText(text = "Set up authenticator key")
.assertIsDisplayed()
}
@Suppress("MaxLineLength")
@@ -1114,7 +1120,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
fakePermissionManager.checkPermissionResult = true
composeTestRule
.onNodeWithTextAfterScroll(text = "Set up TOTP")
.onNodeWithTextAfterScroll(text = "Set up authenticator key")
.performClick()
verify {
@@ -1131,7 +1137,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
fakePermissionManager.getPermissionsResult = true
composeTestRule
.onNodeWithTextAfterScroll(text = "Set up TOTP")
.onNodeWithTextAfterScroll(text = "Set up authenticator key")
.performClick()
verify {
@@ -1145,12 +1151,12 @@ class VaultAddEditScreenTest : BaseComposeTest() {
@Suppress("MaxLineLength")
@Test
fun `in ItemType_Login state clicking Set up TOTP button with a negative result should send false`() {
fun `in ItemType_Login state clicking Set up authenticator key button with a negative result should send false`() {
fakePermissionManager.checkPermissionResult = false
fakePermissionManager.getPermissionsResult = false
composeTestRule
.onNodeWithTextAfterScroll(text = "Set up TOTP")
.onNodeWithTextAfterScroll(text = "Set up authenticator key")
.performClick()
verify {
@@ -1164,7 +1170,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
@Suppress("MaxLineLength")
@Test
fun `in ItemType_Login state the SetupTOTP button should be visible but disabled based on state`() {
fun `in ItemType_Login state the SetupAuthenticatorKey button should be visible but disabled based on state`() {
mutableStateFlow.update { currentState ->
updateLoginType(currentState) {
copy(
@@ -1175,7 +1181,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
}
composeTestRule
.onNodeWithTextAfterScroll(text = "Set up TOTP")
.onNodeWithTextAfterScroll(text = "Set up authenticator key")
.assertIsDisplayed()
.assertIsNotEnabled()
}
@@ -1193,7 +1199,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
}
composeTestRule
.onNodeWithTextAfterScroll(text = "Authenticator key")
.onNodeWithTextAfterScroll(text = "Set up authenticator key")
.assertIsDisplayed()
.assertIsNotEnabled()
@@ -1202,6 +1208,19 @@ class VaultAddEditScreenTest : BaseComposeTest() {
composeTestRule.assertScrollableNodeDoesNotExist("Camera")
}
@Test
fun `Clicking the Authenticator key tooltip sends AuthenticatorHelpToolTipClick action`() {
composeTestRule
.onNodeWithContentDescriptionAfterScroll(label = "Authenticator key help")
.performClick()
verify {
viewModel.trySendAction(
VaultAddEditAction.ItemType.LoginType.AuthenticatorHelpToolTipClick,
)
}
}
@Test
fun `in ItemType_Login state changing URI text field should trigger UriValueChange`() {
mutableStateFlow.update { currentState ->

View File

@@ -2421,6 +2421,19 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
}
}
@Test
fun `Authenticator TooltipCLick emits NavigateToAuthenticatorKeyTooltipUri`() = runTest {
viewModel.eventFlow.test {
viewModel.trySendAction(
VaultAddEditAction.ItemType.LoginType.AuthenticatorHelpToolTipClick,
)
assertEquals(
VaultAddEditEvent.NavigateToAuthenticatorKeyTooltipUri,
awaitItem(),
)
}
}
@Test
fun `ClearTotpKeyClick call should clear the totp code`() {
val viewModel = createAddVaultItemViewModel(

View File

@@ -4,6 +4,7 @@ import androidx.camera.core.ImageProxy
import androidx.compose.ui.test.onNodeWithText
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import com.x8bit.bitwarden.ui.util.performCustomAccessibilityAction
import com.x8bit.bitwarden.ui.vault.feature.qrcodescan.util.FakeQrCodeAnalyzer
import io.mockk.every
import io.mockk.mockk
@@ -89,17 +90,23 @@ class QrCodeScanScreenTest : BaseComposeTest() {
@Config(qualifiers = "land")
@Test
fun `clicking on manual text should send ManualEntryTextClick in landscape mode`() = runTest {
// TODO Update the tests once clickable text issue is resolved (BIT-1357)
composeTestRule
.onNodeWithText("Enter key manually", substring = true)
.assertExists()
.onNodeWithText(text = "Cannot scan QR code? Enter key manually")
.performCustomAccessibilityAction(label = "Enter key manually")
verify {
viewModel.trySendAction(QrCodeScanAction.ManualEntryTextClick)
}
}
@Test
fun `clicking on manual text should send ManualEntryTextClick`() = runTest {
// TODO Update the tests once clickable text issue is resolved (BIT-1357)
composeTestRule
.onNodeWithText("Enter key manually", substring = true)
.assertExists()
.onNodeWithText(text = "Cannot scan QR code? Enter key manually")
.performCustomAccessibilityAction(label = "Enter key manually")
verify {
viewModel.trySendAction(QrCodeScanAction.ManualEntryTextClick)
}
}
}