Skip to content
Open
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 @@ -248,6 +248,7 @@ private fun PasswordField(
showPassword = passwordData.isVisible,
onValueChange = { },
readOnly = true,
useFixedLengthMask = true,
singleLine = false,
actions = {
BitwardenStandardIconButton(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ fun CustomField(
showPassword = customField.isVisible,
onValueChange = { },
readOnly = true,
useFixedLengthMask = true,
singleLine = false,
showPasswordTestTag = "CustomFieldShowPasswordButton",
passwordFieldTestTag = "CustomFieldValue",
Expand All @@ -74,6 +75,7 @@ fun CustomField(
showPassword = customField.isVisible,
onValueChange = { },
readOnly = true,
useFixedLengthMask = true,
singleLine = false,
cardStyle = cardStyle,
modifier = modifier,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -972,6 +972,10 @@ class VaultItemScreenTest : BitwardenComposeTest() {
)
}

composeTestRule
.onNodeWithTextAfterScroll(textField.name)
.assertTextEquals(textField.name, "••••••••")

composeTestRule
.onNodeWithTextAfterScroll(textField.name)
.onChildren()
Expand Down Expand Up @@ -1152,7 +1156,7 @@ class VaultItemScreenTest : BitwardenComposeTest() {
viewState = EMPTY_LOGIN_VIEW_STATE.copy(
type = EMPTY_LOGIN_TYPE.copy(
passwordData = VaultItemState.ViewState.Content.ItemType.Login.PasswordData(
password = "p@ssw0rd",
password = "correct horse battery staple",
isVisible = false,
canViewPassword = true,
),
Expand Down Expand Up @@ -1180,7 +1184,7 @@ class VaultItemScreenTest : BitwardenComposeTest() {
viewState = EMPTY_LOGIN_VIEW_STATE.copy(
type = EMPTY_LOGIN_TYPE.copy(
passwordData = VaultItemState.ViewState.Content.ItemType.Login.PasswordData(
password = "p@ssw0rd",
password = "correct horse battery staple",
isVisible = true,
canViewPassword = true,
),
Expand All @@ -1191,7 +1195,7 @@ class VaultItemScreenTest : BitwardenComposeTest() {

composeTestRule
.onNodeWithText("Password")
.assertTextEquals("Password", "p@ssw0rd")
.assertTextEquals("Password", "correct horse battery staple")
.assertIsEnabled()
composeTestRule
.onNodeWithTextAfterScroll("Check password for data breaches")
Expand All @@ -1208,7 +1212,7 @@ class VaultItemScreenTest : BitwardenComposeTest() {
viewState = EMPTY_LOGIN_VIEW_STATE.copy(
type = EMPTY_LOGIN_TYPE.copy(
passwordData = VaultItemState.ViewState.Content.ItemType.Login.PasswordData(
password = "p@ssw0rd",
password = "correct horse battery staple",
isVisible = true,
canViewPassword = false,
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ fun BitwardenHiddenPasswordField(
.nullableTestTag(tag = passwordFieldTestTag),
textStyle = BitwardenTheme.typography.sensitiveInfoSmall,
label = label?.let { { Text(text = it) } },
value = value,
value = if (value.isNotEmpty()) MASKED_PASSWORD_VALUE else value,
onValueChange = { },
visualTransformation = PasswordVisualTransformation(),
singleLine = true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.platform.theme.BitwardenTheme

internal const val MASKED_PASSWORD_VALUE = "********"

/**
* Represents a Bitwarden-styled password field that hoists show/hide password state to the caller.
*
Expand All @@ -77,6 +79,8 @@ import com.bitwarden.ui.platform.theme.BitwardenTheme
* @param modifier Modifier for the composable.
* @param helpData An optional help button to be displayed in the label.
* @param readOnly `true` if the input should be read-only and not accept user interactions.
* @param useFixedLengthMask `true` to hide the underlying value length when the field is read-only
* and the password is hidden.
* @param singleLine when `true`, this text field becomes a single line that horizontally scrolls
* instead of wrapping onto multiple lines.
* @param showPasswordTestTag The test tag to be used on the show password button (testing tool).
Expand Down Expand Up @@ -107,6 +111,7 @@ fun BitwardenPasswordField(
modifier: Modifier = Modifier,
helpData: BitwardenHelpButtonData? = null,
readOnly: Boolean = false,
useFixedLengthMask: Boolean = false,
singleLine: Boolean = true,
showPasswordTestTag: String? = null,
supportingContentPadding: PaddingValues = PaddingValues(vertical = 12.dp, horizontal = 16.dp),
Expand All @@ -120,8 +125,14 @@ fun BitwardenPasswordField(
actions: @Composable (RowScope.() -> Unit)? = null,
) {
val focusRequester = remember { FocusRequester() }
var textFieldValueState by remember { mutableStateOf(TextFieldValue(text = value)) }
val textFieldValue = textFieldValueState.copy(text = value)
val shouldUseFixedLengthMask = readOnly && useFixedLengthMask && !showPassword
val displayedValue = if (shouldUseFixedLengthMask && value.isNotEmpty()) {
MASKED_PASSWORD_VALUE
} else {
value
}
var textFieldValueState by remember { mutableStateOf(TextFieldValue(text = displayedValue)) }
val textFieldValue = textFieldValueState.copy(text = displayedValue)
SideEffect {
if (textFieldValue.selection != textFieldValueState.selection ||
textFieldValue.composition != textFieldValueState.composition
Expand All @@ -140,7 +151,9 @@ fun BitwardenPasswordField(

TextToolbarType.NONE -> BitwardenEmptyTextToolbar
}
var lastTextValue by remember(value) { mutableStateOf(value = value) }
var lastTextValue by remember(displayedValue) {
mutableStateOf(value = displayedValue)
}
CompositionLocalProvider(value = LocalTextToolbar provides textToolbar) {
Column(
modifier = modifier
Expand Down Expand Up @@ -265,6 +278,8 @@ fun BitwardenPasswordField(
* @param modifier Modifier for the composable.
* @param helpData An optional help button to be displayed in the label.
* @param readOnly `true` if the input should be read-only and not accept user interactions.
* @param useFixedLengthMask `true` to hide the underlying value length when the field is read-only
* and the password is hidden.
* @param singleLine when `true`, this text field becomes a single line that horizontally scrolls
* instead of wrapping onto multiple lines.
* @param supportingText An optional supporting text that will appear below the text input.
Expand Down Expand Up @@ -294,6 +309,7 @@ fun BitwardenPasswordField(
modifier: Modifier = Modifier,
helpData: BitwardenHelpButtonData? = null,
readOnly: Boolean = false,
useFixedLengthMask: Boolean = false,
singleLine: Boolean = true,
supportingText: String? = null,
showPasswordTestTag: String? = null,
Expand All @@ -316,6 +332,7 @@ fun BitwardenPasswordField(
modifier = modifier,
helpData = helpData,
readOnly = readOnly,
useFixedLengthMask = useFixedLengthMask,
singleLine = singleLine,
supportingContent = supportingText?.let {
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package com.bitwarden.ui.platform.components.field

import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import com.bitwarden.ui.platform.base.BaseComposeTest
import com.bitwarden.ui.platform.components.model.CardStyle
import com.bitwarden.ui.platform.theme.BitwardenTheme
import org.junit.Assert.assertEquals
import org.junit.Test

class BitwardenPasswordFieldTest : BaseComposeTest() {

@Test
fun `read only hidden password uses fixed length mask`() {
val password = "correct horse battery staple"

setTestContent {
BitwardenTheme {
BitwardenPasswordField(
label = "Password",
value = password,
showPassword = false,
showPasswordChange = { },
onValueChange = { },
readOnly = true,
useFixedLengthMask = true,
cardStyle = CardStyle.Full,
)
}
}

composeTestRule
.onNodeWithText("Password")
.assertTextEquals("Password", "••••••••")
}

@Test
fun `read only visible password shows actual value`() {
val password = "correct horse battery staple"

setTestContent {
BitwardenTheme {
BitwardenPasswordField(
label = "Password",
value = password,
showPassword = true,
showPasswordChange = { },
onValueChange = { },
readOnly = true,
useFixedLengthMask = true,
cardStyle = CardStyle.Full,
)
}
}

composeTestRule
.onNodeWithText("Password")
.assertTextEquals("Password", password)
}

@Test
fun `read only hidden password preserves value length by default`() {
setTestContent {
BitwardenTheme {
BitwardenPasswordField(
label = "Password",
value = "12345",
showPassword = false,
showPasswordChange = { },
onValueChange = { },
readOnly = true,
cardStyle = CardStyle.Full,
)
}
}

composeTestRule
.onNodeWithText("Password")
.assertTextEquals("Password", "•••••")
}

@Test
fun `tapping a masked read only field does not emit the mask`() {
val emitted = mutableListOf<String>()

setTestContent {
BitwardenTheme {
BitwardenPasswordField(
label = "Password",
value = "correct horse battery staple",
showPassword = false,
showPasswordChange = { },
onValueChange = { emitted += it },
readOnly = true,
useFixedLengthMask = true,
cardStyle = CardStyle.Full,
)
}
}

composeTestRule.onNodeWithText("Password").performClick()
composeTestRule.waitForIdle()

assertEquals(emptyList<String>(), emitted)
}

@Test
fun `non interactable hidden password uses fixed length mask`() {
setTestContent {
BitwardenTheme {
BitwardenHiddenPasswordField(
label = "Password",
value = "correct horse battery staple",
cardStyle = CardStyle.Full,
)
}
}

composeTestRule
.onNodeWithText("Password")
.assertTextEquals("Password", "••••••••")
}
}