Skip to content
Draft
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 @@ -19,6 +19,14 @@ private const val DEFAULT_SCHEME: String = "https"
* The supported autofill Android View hints that predate identity autofill.
*/
private val SUPPORTED_VIEW_HINTS: List<String> = listOf(
// Chromium can pass HTML autocomplete tokens directly as Android autofill hints.
"cc-exp-month",
"cc-exp-year",
"cc-exp",
"cc-number",
"cc-csc",
"cc-name",
"cc-type",
View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH,
View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR,
View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE,
Expand Down Expand Up @@ -209,11 +217,28 @@ private fun AssistStructure.ViewNode.firstSupportedAutofillHintOrNull(

private fun String.toBitwardenAutofillHintOrNull(): AutofillHint? =
when (this) {
View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH -> AutofillHint.Card.EXPIRATION_MONTH
View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR -> AutofillHint.Card.EXPIRATION_YEAR
View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE -> AutofillHint.Card.EXPIRATION_DATE
View.AUTOFILL_HINT_CREDIT_CARD_NUMBER -> AutofillHint.Card.NUMBER
View.AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE -> AutofillHint.Card.SECURITY_CODE
View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH,
"cc-exp-month",
-> AutofillHint.Card.EXPIRATION_MONTH

View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR,
"cc-exp-year",
-> AutofillHint.Card.EXPIRATION_YEAR

View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE,
"cc-exp",
-> AutofillHint.Card.EXPIRATION_DATE

View.AUTOFILL_HINT_CREDIT_CARD_NUMBER,
"cc-number",
-> AutofillHint.Card.NUMBER

View.AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE,
"cc-csc",
-> AutofillHint.Card.SECURITY_CODE

"cc-name" -> AutofillHint.Card.CARDHOLDER
"cc-type" -> AutofillHint.Card.BRAND
View.AUTOFILL_HINT_PASSWORD -> AutofillHint.Login.PASSWORD
View.AUTOFILL_HINT_EMAIL_ADDRESS,
View.AUTOFILL_HINT_USERNAME,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ class ViewNodeExtensionsTest {
mockkStatic(Int::isUsernameInputType)
mockkStatic(AutofillValue::extractMonthValue)
mockkStatic(AutofillValue::extractYearValue)
mockkStatic(AutofillValue::extractCardBrandValue)
mockkStatic(AutofillValue::extractTextValue)
every {
testAutofillValue.extractMonthValue(
Expand All @@ -85,9 +86,95 @@ class ViewNodeExtensionsTest {
unmockkStatic(Int::isUsernameInputType)
unmockkStatic(AutofillValue::extractMonthValue)
unmockkStatic(AutofillValue::extractYearValue)
unmockkStatic(AutofillValue::extractCardBrandValue)
unmockkStatic(AutofillValue::extractTextValue)
}

@Test
fun `HTML card autofill hints classify localized fields without heuristic hints`() {
setupUnsupportedInputFieldViewNode()
every { viewNode.hint } returns "رقم البطاقة"
every {
testAutofillValue.extractCardBrandValue(autofillOptions = AUTOFILL_OPTIONS_LIST)
} returns null
val cases = listOf(
"cc-number" to AutofillView.Card.Number(data = autofillViewData),
"cc-exp" to AutofillView.Card.ExpirationDate(data = autofillViewData),
"cc-exp-month" to AutofillView.Card.ExpirationMonth(
data = autofillViewData,
monthValue = MONTH_VALUE,
),
"cc-exp-year" to AutofillView.Card.ExpirationYear(
data = autofillViewData,
yearValue = YEAR_VALUE,
),
"cc-csc" to AutofillView.Card.SecurityCode(data = autofillViewData),
"cc-name" to AutofillView.Card.CardholderName(data = autofillViewData),
"cc-type" to AutofillView.Card.Brand(data = autofillViewData, brandValue = null),
)

cases.forEach { (hint, expected) ->
every { viewNode.autofillHints } returns arrayOf(hint)

listOf(false, true).forEach { identityAutofillEnabled ->
val actual = viewNode.toAutofillView(
parentWebsite = null,
isIdentityAutofillEnabled = identityAutofillEnabled,
)

assertEquals(expected, actual, "Failed for HTML hint: $hint")
}
}
}

@Test
fun `HTML security code hint takes precedence over password field heuristics`() {
setupUnsupportedInputFieldViewNode()
every { viewNode.autofillHints } returns arrayOf("cc-csc")
every { viewNode.htmlInfo.isPasswordField() } returns true
every { viewNode.htmlInfo.hints() } returns listOf("password")
val expected = AutofillView.Card.SecurityCode(
data = autofillViewData.copy(hasPasswordTerms = true),
)

val actual = viewNode.toAutofillView(
parentWebsite = null,
isIdentityAutofillEnabled = false,
)

assertEquals(expected, actual)
}

@Test
fun `unknown HTML card hint does not classify an otherwise unsupported field`() {
setupUnsupportedInputFieldViewNode()
every { viewNode.autofillHints } returns arrayOf("cc-unknown")

val actual = viewNode.toAutofillView(
parentWebsite = null,
isIdentityAutofillEnabled = false,
)

assertEquals(AutofillView.Unused(data = autofillViewData), actual)
}

@Test
fun `unsupported hints are skipped while supported hint order is preserved`() {
setupUnsupportedInputFieldViewNode()
every { viewNode.autofillHints } returns arrayOf(
"cc-unknown",
View.AUTOFILL_HINT_PASSWORD,
"cc-number",
)

val actual = viewNode.toAutofillView(
parentWebsite = null,
isIdentityAutofillEnabled = false,
)

assertEquals(AutofillView.Login.Password(data = autofillViewData), actual)
}

@Suppress("MaxLineLength")
@Test
fun `toAutofillView should return AutofillView Card ExpirationMonth when autofillHints match`() {
Expand Down