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 @@ -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 @@ -8,14 +8,14 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalTextToolbar
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.bitwarden.ui.platform.base.util.cardStyle
import com.bitwarden.ui.platform.base.util.nullableTestTag
import com.bitwarden.ui.platform.components.field.color.bitwardenTextFieldColors
import com.bitwarden.ui.platform.components.field.toolbar.BitwardenEmptyTextToolbar
import com.bitwarden.ui.platform.components.model.CardStyle
import com.bitwarden.ui.platform.components.util.passwordVisualTransformation
import com.bitwarden.ui.platform.theme.BitwardenTheme

/**
Expand Down Expand Up @@ -44,7 +44,7 @@ fun BitwardenHiddenPasswordField(
label = label?.let { { Text(text = it) } },
value = value,
onValueChange = { },
visualTransformation = PasswordVisualTransformation(),
visualTransformation = passwordVisualTransformation(staticCharacterCount = 8),
singleLine = true,
enabled = false,
readOnly = true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ import androidx.compose.ui.platform.TextToolbar
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
Expand All @@ -58,6 +57,7 @@ import com.bitwarden.ui.platform.components.model.CardStyle
import com.bitwarden.ui.platform.components.row.BitwardenRowOfActions
import com.bitwarden.ui.platform.components.support.BitwardenSupportingContent
import com.bitwarden.ui.platform.components.util.nonLetterColorVisualTransformation
import com.bitwarden.ui.platform.components.util.passwordVisualTransformation
import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.platform.theme.BitwardenTheme
Expand Down Expand Up @@ -190,7 +190,9 @@ fun BitwardenPasswordField(
}
},
visualTransformation = when {
!showPassword -> PasswordVisualTransformation()
!showPassword -> passwordVisualTransformation(
staticCharacterCount = 8.takeIf { readOnly },
)
readOnly -> nonLetterColorVisualTransformation()
else -> VisualTransformation.None
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.bitwarden.ui.platform.components.util

import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.input.OffsetMapping
import androidx.compose.ui.text.input.TransformedText
import androidx.compose.ui.text.input.VisualTransformation

/**
* Returns a [VisualTransformation] that masks text with [mask]. When [staticCharacterCount] is
* non-null, the transformed text always contains that number of mask characters.
*/
@Composable
fun passwordVisualTransformation(
mask: Char = '\u2022',
staticCharacterCount: Int? = null,
): VisualTransformation =
remember(mask, staticCharacterCount) {
BitwardenPasswordVisualTransformation(
mask = mask,
staticCharacterCount = staticCharacterCount,
)
}

private class BitwardenPasswordVisualTransformation(
private val mask: Char,
private val staticCharacterCount: Int?,
) : VisualTransformation {
override fun filter(
text: AnnotatedString,
): TransformedText = TransformedText(
AnnotatedString(
mask.toString().repeat(n = staticCharacterCount ?: text.text.length),
),
object : OffsetMapping {
override fun originalToTransformed(
offset: Int,
): Int = staticCharacterCount?.let { offset.coerceAtMost(it) } ?: offset

override fun transformedToOriginal(
offset: Int,
): Int = offset.coerceAtMost(text.length)
},
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
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,
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,
cardStyle = CardStyle.Full,
)
}
}

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

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

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

@Test
fun `editable hidden password preserves value length`() {
setTestContent {
BitwardenTheme {
BitwardenPasswordField(
label = "Password",
value = "12345",
showPassword = false,
showPasswordChange = { },
onValueChange = { },
readOnly = false,
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,
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", "••••••••")
}
}