mirror of
https://github.com/bitwarden/android.git
synced 2025-12-10 09:56:45 -06:00
Refactor Autofill Hint Logic and Add Card Autofill Support (#5640)
This commit is contained in:
parent
6ee7f9b80f
commit
db03c7d703
@ -116,15 +116,13 @@ class FilledDataBuilderImpl(
|
||||
): FilledPartition {
|
||||
val filledItems = autofillViews
|
||||
.mapNotNull { autofillView ->
|
||||
val value = when (autofillView) {
|
||||
is AutofillView.Card.ExpirationMonth -> autofillCipher.expirationMonth
|
||||
is AutofillView.Card.ExpirationYear -> autofillCipher.expirationYear
|
||||
is AutofillView.Card.Number -> autofillCipher.number
|
||||
is AutofillView.Card.SecurityCode -> autofillCipher.code
|
||||
}
|
||||
autofillView.buildFilledItemOrNull(
|
||||
value = value,
|
||||
)
|
||||
autofillCipher
|
||||
.getAutofillValueOrNull(autofillView)
|
||||
?.let { value ->
|
||||
autofillView.buildFilledItemOrNull(
|
||||
value = value,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return FilledPartition(
|
||||
@ -162,6 +160,44 @@ class FilledDataBuilderImpl(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the autofill value for the given [autofillView], or null if no value is available.
|
||||
*/
|
||||
private fun AutofillCipher.Card.getAutofillValueOrNull(autofillView: AutofillView.Card): String? =
|
||||
when (autofillView) {
|
||||
is AutofillView.Card.CardholderName -> {
|
||||
cardholderName.takeIf { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
is AutofillView.Card.ExpirationMonth -> {
|
||||
expirationMonth.takeIf { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
is AutofillView.Card.ExpirationYear -> {
|
||||
expirationYear.takeIf { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
is AutofillView.Card.Number -> {
|
||||
number
|
||||
.filter { it.isDigit() }
|
||||
.takeIf { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
is AutofillView.Card.SecurityCode -> {
|
||||
code
|
||||
.filter { it.isDigit() }
|
||||
.takeIf { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
is AutofillView.Card.ExpirationDate -> {
|
||||
if (expirationMonth.isNotBlank() && expirationYear.isNotBlank()) {
|
||||
expirationMonth.padStart(2, '0') + expirationYear.takeLast(2)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the item at the [index]. If that fails, return the last item in the list. If that also fails,
|
||||
* return null.
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
package com.x8bit.bitwarden.data.autofill.model
|
||||
|
||||
/**
|
||||
* Autofill hints used to determine what data an input field is associated with.
|
||||
*/
|
||||
enum class AutofillHint {
|
||||
CARD_CARDHOLDER,
|
||||
CARD_EXPIRATION_DATE,
|
||||
CARD_EXPIRATION_MONTH,
|
||||
CARD_EXPIRATION_YEAR,
|
||||
CARD_NUMBER,
|
||||
CARD_SECURITY_CODE,
|
||||
PASSWORD,
|
||||
USERNAME,
|
||||
}
|
||||
@ -54,6 +54,20 @@ sealed class AutofillView {
|
||||
override val data: Data,
|
||||
) : Card()
|
||||
|
||||
/**
|
||||
* The expiration date [AutofillView] for the [Card] data partition.
|
||||
*/
|
||||
data class ExpirationDate(
|
||||
override val data: Data,
|
||||
) : Card()
|
||||
|
||||
/**
|
||||
* The cardholder name [AutofillView] for the [Card] data partition.
|
||||
*/
|
||||
data class CardholderName(
|
||||
override val data: Data,
|
||||
) : Card()
|
||||
|
||||
/**
|
||||
* The number [AutofillView] for the [Card] data partition.
|
||||
*/
|
||||
|
||||
@ -1,53 +1,111 @@
|
||||
@file:Suppress("TooManyFunctions")
|
||||
|
||||
package com.x8bit.bitwarden.data.autofill.util
|
||||
|
||||
import android.view.ViewStructure.HtmlInfo
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
|
||||
/**
|
||||
* Whether this [HtmlInfo] represents a password field.
|
||||
*
|
||||
* This function is untestable as [HtmlInfo] contains [android.util.Pair] which requires
|
||||
* instrumentation testing.
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
fun HtmlInfo?.isPasswordField(): Boolean =
|
||||
this
|
||||
?.let { htmlInfo ->
|
||||
if (htmlInfo.isInputField) {
|
||||
htmlInfo
|
||||
.attributes
|
||||
?.any {
|
||||
it.first == "type" && it.second == "password"
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
?: false
|
||||
fun HtmlInfo?.isPasswordField(): Boolean = isInputField &&
|
||||
hints().containsAnyTerms(SUPPORTED_RAW_PASSWORD_HINTS)
|
||||
|
||||
/**
|
||||
* Whether this [HtmlInfo] represents a username field.
|
||||
*/
|
||||
fun HtmlInfo?.isUsernameField(): Boolean = isInputField &&
|
||||
hints().containsAnyTerms(SUPPORTED_RAW_USERNAME_HINTS)
|
||||
|
||||
/**
|
||||
* Whether this [HtmlInfo] represents a cardholder name field.
|
||||
*/
|
||||
fun HtmlInfo?.isCardholderNameField(): Boolean = isInputField &&
|
||||
hints().containsAnyPatterns(SUPPORTED_RAW_CARDHOLDER_NAME_HINT_PATTERNS)
|
||||
|
||||
/**
|
||||
* Whether this [HtmlInfo] represents a card number field.
|
||||
*/
|
||||
fun HtmlInfo?.isCardNumberField(): Boolean = isInputField &&
|
||||
hints().containsAnyPatterns(SUPPORTED_RAW_CARD_NUMBER_HINT_PATTERNS)
|
||||
|
||||
/**
|
||||
* Whether this [HtmlInfo] represents a card expiration month field.
|
||||
*/
|
||||
fun HtmlInfo?.isCardExpirationMonthField(): Boolean = isInputField &&
|
||||
hints().containsAnyPatterns(SUPPORTED_RAW_CARD_EXP_MONTH_HINT_PATTERNS)
|
||||
|
||||
/**
|
||||
* Whether this [HtmlInfo] represents a card expiration year field.
|
||||
*/
|
||||
fun HtmlInfo?.isCardExpirationYearField(): Boolean = isInputField &&
|
||||
hints().containsAnyPatterns(SUPPORTED_RAW_CARD_EXP_YEAR_HINT_PATTERNS)
|
||||
|
||||
/**
|
||||
* Whether this [HtmlInfo] represents a card expiration date field.
|
||||
*/
|
||||
fun HtmlInfo?.isCardExpirationDateField(): Boolean = isInputField &&
|
||||
hints().containsAnyPatterns(SUPPORTED_RAW_CARD_EXP_DATE_HINT_PATTERNS)
|
||||
|
||||
/**
|
||||
* Whether this [HtmlInfo] represents a card security code field.
|
||||
*/
|
||||
fun HtmlInfo?.isCardSecurityCodeField(): Boolean = isInputField &&
|
||||
hints().containsAnyPatterns(SUPPORTED_RAW_CARD_SECURITY_CODE_HINT_PATTERNS)
|
||||
|
||||
/**
|
||||
* Attributes that can be used as hints to determine the type of data the associated node expects.
|
||||
*
|
||||
* This function is untestable as [HtmlInfo] contains [android.util.Pair] which requires
|
||||
* instrumentation testing.
|
||||
*
|
||||
* @see IGNORED_RAW_HINTS
|
||||
* @see SUPPORTED_HTML_ATTRIBUTE_HINTS
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
fun HtmlInfo?.isUsernameField(): Boolean =
|
||||
this
|
||||
?.let { htmlInfo ->
|
||||
if (htmlInfo.isInputField) {
|
||||
htmlInfo
|
||||
.attributes
|
||||
?.any {
|
||||
it.first == "type" && it.second == "email"
|
||||
}
|
||||
} else {
|
||||
false
|
||||
fun HtmlInfo?.hints(): List<String> = this
|
||||
?.let { htmlInfo ->
|
||||
htmlInfo
|
||||
.attributes
|
||||
// Filter out attributes with null values or values that match ignored raw hints
|
||||
?.filter { attribute ->
|
||||
attribute.second != null &&
|
||||
!attribute.second.containsAnyTerms(IGNORED_RAW_HINTS)
|
||||
}
|
||||
}
|
||||
?: false
|
||||
// Filter attributes that match supported HTML attribute hints
|
||||
?.filter { attribute ->
|
||||
attribute.first.containsAnyTerms(
|
||||
terms = SUPPORTED_HTML_ATTRIBUTE_HINTS,
|
||||
ignoreCase = true,
|
||||
)
|
||||
}
|
||||
.orEmpty()
|
||||
.mapNotNull { it.second }
|
||||
}
|
||||
.orEmpty()
|
||||
|
||||
/**
|
||||
* Whether this [HtmlInfo] represents an input field.
|
||||
*/
|
||||
val HtmlInfo?.isInputField: Boolean get() = this?.tag == "input"
|
||||
|
||||
/**
|
||||
* Checks if the list of strings contains any of the specified patterns.
|
||||
*/
|
||||
private fun List<String>.containsAnyPatterns(patterns: List<Regex>): Boolean = this
|
||||
.any { string -> patterns.any { pattern -> string.matches(pattern) } }
|
||||
|
||||
/**
|
||||
* Checks if the list of strings contains any of the specified terms.
|
||||
*/
|
||||
private fun List<String>.containsAnyTerms(terms: List<String>): Boolean =
|
||||
this.any { string -> string.containsAnyTerms(terms) }
|
||||
|
||||
/**
|
||||
* The supported attribute keys whose value can represent an autofill hint.
|
||||
*/
|
||||
private val SUPPORTED_HTML_ATTRIBUTE_HINTS: List<String> = listOf(
|
||||
"name",
|
||||
"label",
|
||||
"type",
|
||||
"hint",
|
||||
"autofill",
|
||||
)
|
||||
|
||||
@ -16,3 +16,13 @@ fun String.containsAnyTerms(
|
||||
ignoreCase = ignoreCase,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether this string matches any of these [expressions].
|
||||
*/
|
||||
fun String.matchesAnyExpressions(
|
||||
expressions: List<Regex>,
|
||||
): Boolean =
|
||||
expressions.any {
|
||||
this.matches(regex = it)
|
||||
}
|
||||
|
||||
@ -3,7 +3,9 @@ package com.x8bit.bitwarden.data.autofill.util
|
||||
import android.app.assist.AssistStructure
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import com.bitwarden.ui.platform.base.util.orNullIfBlank
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillHint
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillView
|
||||
|
||||
/**
|
||||
@ -11,39 +13,13 @@ import com.x8bit.bitwarden.data.autofill.model.AutofillView
|
||||
*/
|
||||
private const val DEFAULT_SCHEME: String = "https"
|
||||
|
||||
/**
|
||||
* The set of raw autofill hints that should be ignored.
|
||||
*/
|
||||
private val IGNORED_RAW_HINTS: List<String> = listOf(
|
||||
"search",
|
||||
"find",
|
||||
"recipient",
|
||||
"edit",
|
||||
)
|
||||
|
||||
/**
|
||||
* The supported password autofill hints.
|
||||
*/
|
||||
private val SUPPORTED_RAW_PASSWORD_HINTS: List<String> = listOf(
|
||||
"password",
|
||||
"pswd",
|
||||
)
|
||||
|
||||
/**
|
||||
* The supported raw autofill hints.
|
||||
*/
|
||||
private val SUPPORTED_RAW_USERNAME_HINTS: List<String> = listOf(
|
||||
"email",
|
||||
"phone",
|
||||
"username",
|
||||
)
|
||||
|
||||
/**
|
||||
* The supported autofill Android View hints.
|
||||
*/
|
||||
private val SUPPORTED_VIEW_HINTS: List<String> = listOf(
|
||||
View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH,
|
||||
View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR,
|
||||
View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE,
|
||||
View.AUTOFILL_HINT_CREDIT_CARD_NUMBER,
|
||||
View.AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE,
|
||||
View.AUTOFILL_HINT_EMAIL_ADDRESS,
|
||||
@ -60,7 +36,7 @@ private val AssistStructure.ViewNode.isInputField: Boolean
|
||||
?.let {
|
||||
try {
|
||||
Class.forName(it)
|
||||
} catch (e: ClassNotFoundException) {
|
||||
} catch (_: ClassNotFoundException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
@ -78,11 +54,7 @@ fun AssistStructure.ViewNode.toAutofillView(): AutofillView? =
|
||||
.autofillId
|
||||
// We only care about nodes with a valid `AutofillId`.
|
||||
?.let { nonNullAutofillId ->
|
||||
val supportedHint = this
|
||||
.autofillHints
|
||||
?.firstOrNull { SUPPORTED_VIEW_HINTS.contains(it) }
|
||||
|
||||
if (supportedHint != null || this.isInputField) {
|
||||
if (supportedAutofillHint != null || this.isInputField) {
|
||||
val autofillOptions = this
|
||||
.autofillOptions
|
||||
.orEmpty()
|
||||
@ -99,22 +71,63 @@ fun AssistStructure.ViewNode.toAutofillView(): AutofillView? =
|
||||
buildAutofillView(
|
||||
autofillOptions = autofillOptions,
|
||||
autofillViewData = autofillViewData,
|
||||
supportedHint = supportedHint,
|
||||
autofillHint = supportedAutofillHint,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The first supported autofill hint for this view node, or null if none are found.
|
||||
*/
|
||||
private val AssistStructure.ViewNode.supportedAutofillHint: AutofillHint?
|
||||
get() = firstSupportedAutofillHintOrNull()
|
||||
?: when {
|
||||
this.isUsernameField -> AutofillHint.USERNAME
|
||||
this.isPasswordField -> AutofillHint.PASSWORD
|
||||
this.isCardExpirationMonthField -> AutofillHint.CARD_EXPIRATION_MONTH
|
||||
this.isCardExpirationYearField -> AutofillHint.CARD_EXPIRATION_YEAR
|
||||
this.isCardExpirationDateField -> AutofillHint.CARD_EXPIRATION_DATE
|
||||
this.isCardNumberField -> AutofillHint.CARD_NUMBER
|
||||
this.isCardSecurityCodeField -> AutofillHint.CARD_SECURITY_CODE
|
||||
this.isCardholderNameField -> AutofillHint.CARD_CARDHOLDER
|
||||
else -> null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first supported autofill hint from the view node's autofillHints, or null if none are
|
||||
* found.
|
||||
*/
|
||||
private fun AssistStructure.ViewNode.firstSupportedAutofillHintOrNull(): AutofillHint? =
|
||||
autofillHints
|
||||
?.firstOrNull { SUPPORTED_VIEW_HINTS.contains(it) }
|
||||
?.toBitwardenAutofillHintOrNull()
|
||||
|
||||
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_PASSWORD -> AutofillHint.PASSWORD
|
||||
View.AUTOFILL_HINT_EMAIL_ADDRESS,
|
||||
View.AUTOFILL_HINT_USERNAME,
|
||||
-> AutofillHint.USERNAME
|
||||
|
||||
else -> null
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to convert this [AssistStructure.ViewNode] and [autofillViewData] into an [AutofillView].
|
||||
*/
|
||||
private fun AssistStructure.ViewNode.buildAutofillView(
|
||||
autofillOptions: List<String>,
|
||||
autofillViewData: AutofillView.Data,
|
||||
supportedHint: String?,
|
||||
): AutofillView = when {
|
||||
supportedHint == View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH -> {
|
||||
autofillHint: AutofillHint?,
|
||||
): AutofillView = when (autofillHint) {
|
||||
AutofillHint.CARD_EXPIRATION_MONTH -> {
|
||||
val monthValue = this
|
||||
.autofillValue
|
||||
?.extractMonthValue(
|
||||
@ -127,31 +140,43 @@ private fun AssistStructure.ViewNode.buildAutofillView(
|
||||
)
|
||||
}
|
||||
|
||||
supportedHint == View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR -> {
|
||||
AutofillHint.CARD_EXPIRATION_YEAR -> {
|
||||
AutofillView.Card.ExpirationYear(
|
||||
data = autofillViewData,
|
||||
)
|
||||
}
|
||||
|
||||
supportedHint == View.AUTOFILL_HINT_CREDIT_CARD_NUMBER -> {
|
||||
AutofillHint.CARD_EXPIRATION_DATE -> {
|
||||
AutofillView.Card.ExpirationDate(
|
||||
data = autofillViewData,
|
||||
)
|
||||
}
|
||||
|
||||
AutofillHint.CARD_NUMBER -> {
|
||||
AutofillView.Card.Number(
|
||||
data = autofillViewData,
|
||||
)
|
||||
}
|
||||
|
||||
supportedHint == View.AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE -> {
|
||||
AutofillHint.CARD_SECURITY_CODE -> {
|
||||
AutofillView.Card.SecurityCode(
|
||||
data = autofillViewData,
|
||||
)
|
||||
}
|
||||
|
||||
this.isPasswordField(supportedHint) -> {
|
||||
AutofillHint.CARD_CARDHOLDER -> {
|
||||
AutofillView.Card.CardholderName(
|
||||
data = autofillViewData,
|
||||
)
|
||||
}
|
||||
|
||||
AutofillHint.PASSWORD -> {
|
||||
AutofillView.Login.Password(
|
||||
data = autofillViewData,
|
||||
)
|
||||
}
|
||||
|
||||
this.isUsernameField(supportedHint) -> {
|
||||
AutofillHint.USERNAME -> {
|
||||
AutofillView.Login.Username(
|
||||
data = autofillViewData,
|
||||
)
|
||||
@ -167,41 +192,97 @@ private fun AssistStructure.ViewNode.buildAutofillView(
|
||||
/**
|
||||
* Check whether this [AssistStructure.ViewNode] represents a password field.
|
||||
*/
|
||||
fun AssistStructure.ViewNode.isPasswordField(
|
||||
supportedHint: String?,
|
||||
): Boolean {
|
||||
if (supportedHint == View.AUTOFILL_HINT_PASSWORD) return true
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
internal val AssistStructure.ViewNode.isPasswordField: Boolean
|
||||
get() {
|
||||
val isUsernameField = this.isUsernameField
|
||||
if (
|
||||
this.inputType.isPasswordInputType &&
|
||||
!this.containsIgnoredHintTerms() &&
|
||||
!isUsernameField
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
val isInvalidField = this.idEntry?.containsAnyTerms(IGNORED_RAW_HINTS) == true ||
|
||||
this.hint?.containsAnyTerms(IGNORED_RAW_HINTS) == true
|
||||
val isUsernameField = this.isUsernameField(supportedHint)
|
||||
if (this.inputType.isPasswordInputType && !isInvalidField && !isUsernameField) return true
|
||||
|
||||
return this
|
||||
.htmlInfo
|
||||
.isPasswordField()
|
||||
}
|
||||
return hint?.containsAnyTerms(SUPPORTED_RAW_PASSWORD_HINTS) == true ||
|
||||
htmlInfo.isPasswordField()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether this [AssistStructure.ViewNode] includes any password specific terms.
|
||||
*/
|
||||
fun AssistStructure.ViewNode.hasPasswordTerms(): Boolean =
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
internal fun AssistStructure.ViewNode.hasPasswordTerms(): Boolean =
|
||||
this.idEntry?.containsAnyTerms(SUPPORTED_RAW_PASSWORD_HINTS) == true ||
|
||||
this.hint?.containsAnyTerms(SUPPORTED_RAW_PASSWORD_HINTS) == true
|
||||
this.hint?.containsAnyTerms(SUPPORTED_RAW_PASSWORD_HINTS) == true ||
|
||||
this.htmlInfo.hints().any { it.containsAnyTerms(SUPPORTED_RAW_PASSWORD_HINTS) }
|
||||
|
||||
/**
|
||||
* Check whether this [AssistStructure.ViewNode] represents a username field.
|
||||
*/
|
||||
fun AssistStructure.ViewNode.isUsernameField(
|
||||
supportedHint: String?,
|
||||
): Boolean =
|
||||
supportedHint == View.AUTOFILL_HINT_USERNAME ||
|
||||
supportedHint == View.AUTOFILL_HINT_EMAIL_ADDRESS ||
|
||||
inputType.isUsernameInputType ||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
internal val AssistStructure.ViewNode.isUsernameField: Boolean
|
||||
get() = inputType.isUsernameInputType ||
|
||||
idEntry?.containsAnyTerms(SUPPORTED_RAW_USERNAME_HINTS) == true ||
|
||||
hint?.containsAnyTerms(SUPPORTED_RAW_USERNAME_HINTS) == true ||
|
||||
htmlInfo.isUsernameField()
|
||||
|
||||
/**
|
||||
* Check whether this [AssistStructure.ViewNode] represents a card expiration month field.
|
||||
*/
|
||||
private val AssistStructure.ViewNode.isCardExpirationMonthField: Boolean
|
||||
get() = idEntry?.matchesAnyExpressions(SUPPORTED_RAW_CARD_EXP_MONTH_HINT_PATTERNS) == true ||
|
||||
hint?.matchesAnyExpressions(SUPPORTED_RAW_CARD_EXP_MONTH_HINT_PATTERNS) == true ||
|
||||
htmlInfo.isCardExpirationMonthField()
|
||||
|
||||
/**
|
||||
* Check whether this [AssistStructure.ViewNode] represents a card expiration year field.
|
||||
*/
|
||||
private val AssistStructure.ViewNode.isCardExpirationYearField: Boolean
|
||||
get() = idEntry?.matchesAnyExpressions(SUPPORTED_RAW_CARD_EXP_YEAR_HINT_PATTERNS) == true ||
|
||||
hint?.matchesAnyExpressions(SUPPORTED_RAW_CARD_EXP_YEAR_HINT_PATTERNS) == true ||
|
||||
htmlInfo.isCardExpirationYearField()
|
||||
|
||||
/**
|
||||
* Check whether this [AssistStructure.ViewNode] represents a card expiration date field.
|
||||
*/
|
||||
private val AssistStructure.ViewNode.isCardExpirationDateField: Boolean
|
||||
get() = idEntry?.matchesAnyExpressions(SUPPORTED_RAW_CARD_EXP_DATE_HINT_PATTERNS) == true ||
|
||||
hint?.matchesAnyExpressions(SUPPORTED_RAW_CARD_EXP_DATE_HINT_PATTERNS) == true ||
|
||||
htmlInfo.isCardExpirationDateField()
|
||||
|
||||
/**
|
||||
* Check whether this [AssistStructure.ViewNode] represents a card number field based.
|
||||
*/
|
||||
private val AssistStructure.ViewNode.isCardNumberField: Boolean
|
||||
get() = idEntry?.matchesAnyExpressions(SUPPORTED_RAW_CARD_NUMBER_HINT_PATTERNS) == true ||
|
||||
hint?.matchesAnyExpressions(SUPPORTED_RAW_CARD_NUMBER_HINT_PATTERNS) == true ||
|
||||
htmlInfo.isCardNumberField()
|
||||
|
||||
/**
|
||||
* Check whether this [AssistStructure.ViewNode] represents a card security code field based.
|
||||
*/
|
||||
private val AssistStructure.ViewNode.isCardSecurityCodeField: Boolean
|
||||
get() =
|
||||
idEntry?.matchesAnyExpressions(SUPPORTED_RAW_CARD_SECURITY_CODE_HINT_PATTERNS) == true ||
|
||||
hint?.matchesAnyExpressions(SUPPORTED_RAW_CARD_SECURITY_CODE_HINT_PATTERNS) == true ||
|
||||
htmlInfo.isCardSecurityCodeField()
|
||||
|
||||
/**
|
||||
* Check whether this [AssistStructure.ViewNode] represents a cardholder name field based.
|
||||
*/
|
||||
private val AssistStructure.ViewNode.isCardholderNameField: Boolean
|
||||
get() = idEntry?.matchesAnyExpressions(SUPPORTED_RAW_CARDHOLDER_NAME_HINT_PATTERNS) == true ||
|
||||
hint?.matchesAnyExpressions(SUPPORTED_RAW_CARDHOLDER_NAME_HINT_PATTERNS) == true ||
|
||||
htmlInfo.isCardholderNameField()
|
||||
|
||||
/**
|
||||
* Check whether this [AssistStructure.ViewNode] contains any ignored hint terms.
|
||||
*/
|
||||
private fun AssistStructure.ViewNode.containsIgnoredHintTerms(): Boolean =
|
||||
this.idEntry?.containsAnyTerms(IGNORED_RAW_HINTS) == true ||
|
||||
this.hint?.containsAnyTerms(IGNORED_RAW_HINTS) == true ||
|
||||
this.htmlInfo.hints().any { it.containsAnyTerms(IGNORED_RAW_HINTS) }
|
||||
/**
|
||||
* The website that this [AssistStructure.ViewNode] is a part of representing.
|
||||
*/
|
||||
|
||||
@ -0,0 +1,137 @@
|
||||
package com.x8bit.bitwarden.data.autofill.util
|
||||
|
||||
/**
|
||||
* The set of raw autofill hints that should be ignored.
|
||||
*/
|
||||
val IGNORED_RAW_HINTS: List<String> = listOf(
|
||||
"search",
|
||||
"find",
|
||||
"recipient",
|
||||
"edit",
|
||||
)
|
||||
|
||||
/**
|
||||
* The supported password autofill hints.
|
||||
*/
|
||||
val SUPPORTED_RAW_PASSWORD_HINTS: List<String> = listOf(
|
||||
"password",
|
||||
"pswd",
|
||||
)
|
||||
|
||||
/**
|
||||
* The supported raw autofill hints.
|
||||
*/
|
||||
val SUPPORTED_RAW_USERNAME_HINTS: List<String> = listOf(
|
||||
"email",
|
||||
"phone",
|
||||
"username",
|
||||
)
|
||||
|
||||
/**
|
||||
* Matches common patterns for cardholder name hints.
|
||||
* - `\b(?i)`: Case-insensitive word boundary to ensure we match whole words.
|
||||
* - `(?:credit[\\s_-])?`: Optionally matches "credit" followed by a space, underscore, or hyphen.
|
||||
* - `(?:cc|card)[\\s_-](?:name|cardholder).*`: Matches "cc" or "card" followed by a space,
|
||||
* underscore, or hyphen, then "name" or "cardholder", and finally any characters. This covers
|
||||
* variations like "cc name", "card_cardholder", "credit-card-name something else".
|
||||
* - `|`: OR operator, allowing for an alternative pattern.
|
||||
* - `name[\\s_-]on[\\s_-]card`: Matches "name" followed by a space, underscore, or hyphen, then
|
||||
* "on", another space, underscore, or hyphen, and finally "card". This covers phrases like "name on
|
||||
* card" or "name_on_card".
|
||||
* - `\b`: Word boundary to ensure we match whole words.
|
||||
*/
|
||||
val SUPPORTED_RAW_CARDHOLDER_NAME_HINT_PATTERNS: List<Regex> = listOf(
|
||||
"\\b(?i)(?:credit[\\s_-])?(?:cc|card)[\\s_-](?:name|cardholder).*\\b".toRegex(),
|
||||
"\\b(?i)name[\\s_-]on[\\s_-]card\\b".toRegex(),
|
||||
)
|
||||
|
||||
/**
|
||||
* Matches common patterns for card number hints.
|
||||
* - `\b(?i)`: Case-insensitive word boundary to ensure we match whole words.
|
||||
* - `(?:credit[\\s_-])?`: Optionally matches "credit" followed by a space, underscore, or hyphen.
|
||||
* - `(?:cc|card)`: Matches "cc" or "card".
|
||||
* - `[\\s_-]number`: Matches "number" preceded by a space, underscore, or hyphen.
|
||||
* - `\b`: Word boundary to ensure we match whole words.
|
||||
*/
|
||||
val SUPPORTED_RAW_CARD_NUMBER_HINT_PATTERNS: List<Regex> = listOf(
|
||||
"\\b(?i)(?:credit[\\s_-])?(?:cc|card)[\\s_-]number\\b".toRegex(),
|
||||
)
|
||||
|
||||
/**
|
||||
* Matches common patterns for card expiration month hints.
|
||||
* - `\b(?i)`: Case-insensitive word boundary to ensure we match whole words.
|
||||
* - `(?:credit[\\s_-])?`: Optionally matches "credit" followed by a space, underscore, or hyphen.
|
||||
* - `(?:cc|card)`: Matches "cc" or "card".
|
||||
* - `[\\s_-]exp[\\s_-]month`: Matches "exp" followed by a space, underscore, or hyphen, then
|
||||
* "month".
|
||||
* - `\b`: Word boundary to ensure we match whole words.
|
||||
*
|
||||
* Examples:
|
||||
* - "credit card exp month"
|
||||
* - "cc_exp_month"
|
||||
* - "card-exp-month"
|
||||
*/
|
||||
val SUPPORTED_RAW_CARD_EXP_MONTH_HINT_PATTERNS: List<Regex> = listOf(
|
||||
"\\b(?i)(?:credit[\\s_-])?(?:(cc|card)[\\s_-])?(?:exp|expiration|expiry)[\\s_-]month\\b"
|
||||
.toRegex(),
|
||||
)
|
||||
|
||||
/**
|
||||
* Matches common patterns for card expiration year hints.
|
||||
* - `\b(?i)`: Case-insensitive word boundary to ensure we match whole words.
|
||||
* - `(?:credit[\\s_-])?`: Optionally matches "credit" followed by a space, underscore, or hyphen.
|
||||
* - `(?:cc|card)`: Matches "cc" or "card".
|
||||
* - `[\\s_-]exp[\\s_-]year`: Matches "exp" followed by a space, underscore, or hyphen, then "year".
|
||||
* - `\b`: Word boundary to ensure we match whole words.
|
||||
*
|
||||
* Similar to [SUPPORTED_RAW_CARD_EXP_MONTH_HINT_PATTERNS], but for "year" instead of "month".
|
||||
* @see SUPPORTED_RAW_CARD_EXP_MONTH_HINT_PATTERNS
|
||||
*/
|
||||
val SUPPORTED_RAW_CARD_EXP_YEAR_HINT_PATTERNS: List<Regex> = listOf(
|
||||
"\\b(?i)(?:credit[\\s_-])?(?:(cc|card)[\\s_-])?(?:exp|expiration|expiry)[\\s_-]year\\b"
|
||||
.toRegex(),
|
||||
)
|
||||
|
||||
/**
|
||||
* Matches common patterns for card expiration date hints.
|
||||
* - `\b(?i)`: Case-insensitive word boundary to ensure we match whole words.
|
||||
* - `(?:credit[\\s_-])?`: Optionally matches "credit" followed by a space, underscore, or hyphen.
|
||||
* - `(?:cc|card)`: Matches "cc" or "card".
|
||||
* - `[\\s_-]exp[\\s_-]date`: Matches "exp" followed by a space, underscore, or hyphen, then "date".
|
||||
* - `.*`: Matches any characters following "date" (e.g., "MM/YY", "month/year").
|
||||
* - `\b`: Word boundary to ensure we match whole words.
|
||||
*
|
||||
* Examples:
|
||||
* - "credit card exp date"
|
||||
* - "cc_exp_date_mm_yy"
|
||||
* - "card-exp-date month/year"
|
||||
*/
|
||||
val SUPPORTED_RAW_CARD_EXP_DATE_HINT_PATTERNS: List<Regex> = listOf(
|
||||
"\\b(?i)(?:credit[\\s_-])?(?:(cc|card)[\\s_-])?(?:exp|expiration|expiry)[\\s_-]date\\b"
|
||||
.toRegex(),
|
||||
)
|
||||
|
||||
/**
|
||||
* Matches common patterns for card security code hints.
|
||||
* - `\b(?i)`: Case-insensitive word boundary to ensure we match whole words.
|
||||
* - `(?:credit[\\s_-])?`: Optionally matches "credit" followed by a space, underscore, or hyphen.
|
||||
* - The first pattern `(?:cc|card[\\s_-])(cvc|cvv)\b`:
|
||||
* - `(?:cc|card[\\s_-])`: Matches "cc" or "card" followed by a space, underscore, or hyphen.
|
||||
* - `(cvc|cvv)\b`: Matches "cvc" or "cvv" followed by a word boundary.
|
||||
* - The second pattern `(?:cc|card)(?:[\\s_-]verification)?([\\s_-]code)\b`:
|
||||
* - `(?:cc|card)`: Matches "cc" or "card".
|
||||
* - `(?:[\\s_-]verification)?`: Optionally matches "verification" preceded by a space,
|
||||
* underscore, or hyphen.
|
||||
* - `([\\s_-]code)\b`: Matches "code" preceded by a space, underscore, or hyphen, and
|
||||
* followed by a word boundary.
|
||||
*
|
||||
* Examples:
|
||||
* - "credit card cvc"
|
||||
* - "cc_verification_code"
|
||||
* - "card-code"
|
||||
*/
|
||||
val SUPPORTED_RAW_CARD_SECURITY_CODE_HINT_PATTERNS: List<Regex> = listOf(
|
||||
"\\b(?i)(?:credit[\\s_-])?(?:cc|card[\\s_-])?(cvc|cvv)2?\\b".toRegex(),
|
||||
"\\b(?i)(?:credit[\\s_-])?(?:cc|card)(?:[\\s_-](?:verification|security))?([\\s_-]code)\\b"
|
||||
.toRegex(),
|
||||
)
|
||||
@ -26,6 +26,7 @@ import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.bitwarden.ui.util.concat
|
||||
import com.bitwarden.vault.CipherListViewType
|
||||
import com.bitwarden.vault.CipherRepromptType
|
||||
import com.bitwarden.vault.CipherType
|
||||
import com.bitwarden.vault.CipherView
|
||||
@ -2476,19 +2477,35 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
* Takes the given vault data and filters it for autofill if necessary.
|
||||
*/
|
||||
private suspend fun DataState<VaultData>.filterForAutofillIfNecessary(): DataState<VaultData> {
|
||||
val matchUri = state
|
||||
.autofillSelectionData
|
||||
?.uri
|
||||
?: return this
|
||||
return this.map { vaultData ->
|
||||
vaultData.copy(
|
||||
decryptCipherListResult = vaultData.decryptCipherListResult.copy(
|
||||
successes = cipherMatchingManager.filterCiphersForMatches(
|
||||
cipherListViews = vaultData.decryptCipherListResult.successes,
|
||||
matchUri = matchUri,
|
||||
),
|
||||
),
|
||||
)
|
||||
val autofillSelectionData = state.autofillSelectionData ?: return this
|
||||
return when (autofillSelectionData.type) {
|
||||
AutofillSelectionData.Type.CARD -> {
|
||||
this.map { vaultData ->
|
||||
vaultData.copy(
|
||||
decryptCipherListResult = vaultData.decryptCipherListResult.copy(
|
||||
successes = vaultData.decryptCipherListResult.successes
|
||||
.filter { it.type is CipherListViewType.Card },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AutofillSelectionData.Type.LOGIN -> {
|
||||
val matchUri = state
|
||||
.autofillSelectionData
|
||||
?.uri
|
||||
?: return this
|
||||
this.map { vaultData ->
|
||||
vaultData.copy(
|
||||
decryptCipherListResult = vaultData.decryptCipherListResult.copy(
|
||||
successes = cipherMatchingManager.filterCiphersForMatches(
|
||||
cipherListViews = vaultData.decryptCipherListResult.successes,
|
||||
matchUri = matchUri,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2626,9 +2643,21 @@ data class VaultItemListingState(
|
||||
*/
|
||||
val appBarTitle: Text
|
||||
get() = autofillSelectionData
|
||||
?.uri
|
||||
?.toHostOrPathOrNull()
|
||||
?.let { BitwardenString.items_for_uri.asText(it) }
|
||||
?.let { data ->
|
||||
data.uri
|
||||
?.toHostOrPathOrNull()
|
||||
?.let {
|
||||
when (data.type) {
|
||||
AutofillSelectionData.Type.CARD -> {
|
||||
BitwardenString.select_a_card_for_x.asText(it)
|
||||
}
|
||||
|
||||
AutofillSelectionData.Type.LOGIN -> {
|
||||
BitwardenString.items_for_uri.asText(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
?: createCredentialRequest
|
||||
?.relyingPartyIdOrNull
|
||||
?.let { BitwardenString.items_for_uri.asText(it) }
|
||||
|
||||
@ -177,11 +177,13 @@ class FilledDataBuilderTest {
|
||||
runTest {
|
||||
// Setup
|
||||
val code = "123"
|
||||
val expirationMonth = "January"
|
||||
val expirationMonth = "01"
|
||||
val expirationYear = "1999"
|
||||
val number = "1234567890"
|
||||
val expirationDate = "0199"
|
||||
val cardholderName = "John"
|
||||
val autofillCipher = AutofillCipher.Card(
|
||||
cardholderName = "John",
|
||||
cardholderName = cardholderName,
|
||||
cipherId = null,
|
||||
code = code,
|
||||
expirationMonth = expirationMonth,
|
||||
@ -194,6 +196,8 @@ class FilledDataBuilderTest {
|
||||
val filledItemExpirationMonth: FilledItem = mockk()
|
||||
val filledItemExpirationYear: FilledItem = mockk()
|
||||
val filledItemNumber: FilledItem = mockk()
|
||||
val filledItemCardholderName: FilledItem = mockk()
|
||||
val filledItemExpirationDate: FilledItem = mockk()
|
||||
val autofillViewCode: AutofillView.Card.SecurityCode = mockk {
|
||||
every { buildFilledItemOrNull(code) } returns filledItemCode
|
||||
}
|
||||
@ -209,6 +213,12 @@ class FilledDataBuilderTest {
|
||||
val autofillViewNumberTwo: AutofillView.Card.Number = mockk {
|
||||
every { buildFilledItemOrNull(number) } returns null
|
||||
}
|
||||
val autofillViewCardholderName: AutofillView.Card.CardholderName = mockk {
|
||||
every { buildFilledItemOrNull(cardholderName) } returns filledItemCardholderName
|
||||
}
|
||||
val autofillViewExpirationDate: AutofillView.Card.ExpirationDate = mockk {
|
||||
every { buildFilledItemOrNull(expirationDate) } returns filledItemExpirationDate
|
||||
}
|
||||
val autofillPartition = AutofillPartition.Card(
|
||||
views = listOf(
|
||||
autofillViewCode,
|
||||
@ -216,6 +226,8 @@ class FilledDataBuilderTest {
|
||||
autofillViewExpirationYear,
|
||||
autofillViewNumberOne,
|
||||
autofillViewNumberTwo,
|
||||
autofillViewCardholderName,
|
||||
autofillViewExpirationDate,
|
||||
),
|
||||
)
|
||||
val ignoreAutofillIds: List<AutofillId> = mockk()
|
||||
@ -234,6 +246,8 @@ class FilledDataBuilderTest {
|
||||
filledItemExpirationMonth,
|
||||
filledItemExpirationYear,
|
||||
filledItemNumber,
|
||||
filledItemCardholderName,
|
||||
filledItemExpirationDate,
|
||||
),
|
||||
inlinePresentationSpec = null,
|
||||
)
|
||||
@ -265,6 +279,96 @@ class FilledDataBuilderTest {
|
||||
autofillViewExpirationYear.buildFilledItemOrNull(expirationYear)
|
||||
autofillViewNumberOne.buildFilledItemOrNull(number)
|
||||
autofillViewNumberTwo.buildFilledItemOrNull(number)
|
||||
autofillViewCardholderName.buildFilledItemOrNull(cardholderName)
|
||||
autofillViewExpirationDate.buildFilledItemOrNull(expirationDate)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `build should skip empty AutofillValues and return empty data when Card data is blank`() =
|
||||
runTest {
|
||||
// Setup
|
||||
val code = ""
|
||||
val expirationMonth = ""
|
||||
val expirationYear = ""
|
||||
val number = ""
|
||||
val autofillCipher = AutofillCipher.Card(
|
||||
cardholderName = "",
|
||||
cipherId = null,
|
||||
code = code,
|
||||
expirationMonth = expirationMonth,
|
||||
expirationYear = expirationYear,
|
||||
name = "",
|
||||
number = number,
|
||||
subtitle = "",
|
||||
)
|
||||
val filledItemCode: FilledItem = mockk()
|
||||
val filledItemExpirationMonth: FilledItem = mockk()
|
||||
val filledItemExpirationYear: FilledItem = mockk()
|
||||
val filledItemNumber: FilledItem = mockk()
|
||||
val autofillViewCode: AutofillView.Card.SecurityCode = mockk()
|
||||
val autofillViewExpirationMonth: AutofillView.Card.ExpirationMonth = mockk()
|
||||
val autofillViewExpirationYear: AutofillView.Card.ExpirationYear = mockk()
|
||||
val autofillViewNumberOne: AutofillView.Card.Number = mockk()
|
||||
val autofillViewNumberTwo: AutofillView.Card.Number = mockk()
|
||||
val autofillViewCardholderName: AutofillView.Card.CardholderName = mockk()
|
||||
val autofillViewExpirationDate: AutofillView.Card.ExpirationDate = mockk()
|
||||
val autofillPartition = AutofillPartition.Card(
|
||||
views = listOf(
|
||||
autofillViewCode,
|
||||
autofillViewExpirationMonth,
|
||||
autofillViewExpirationYear,
|
||||
autofillViewNumberOne,
|
||||
autofillViewNumberTwo,
|
||||
autofillViewCardholderName,
|
||||
autofillViewExpirationDate,
|
||||
),
|
||||
)
|
||||
val ignoreAutofillIds: List<AutofillId> = mockk()
|
||||
val autofillRequest = AutofillRequest.Fillable(
|
||||
ignoreAutofillIds = ignoreAutofillIds,
|
||||
inlinePresentationSpecs = emptyList(),
|
||||
maxInlineSuggestionsCount = 0,
|
||||
packageName = null,
|
||||
partition = autofillPartition,
|
||||
uri = URI,
|
||||
)
|
||||
val filledPartition = FilledPartition(
|
||||
autofillCipher = autofillCipher,
|
||||
filledItems = emptyList(),
|
||||
inlinePresentationSpec = null,
|
||||
)
|
||||
val expected = FilledData(
|
||||
filledPartitions = listOf(
|
||||
filledPartition,
|
||||
),
|
||||
ignoreAutofillIds = ignoreAutofillIds,
|
||||
originalPartition = autofillPartition,
|
||||
uri = URI,
|
||||
vaultItemInlinePresentationSpec = null,
|
||||
isVaultLocked = false,
|
||||
)
|
||||
coEvery {
|
||||
autofillCipherProvider.getCardAutofillCiphers()
|
||||
} returns listOf(autofillCipher)
|
||||
|
||||
// Test
|
||||
val actual = filledDataBuilder.build(
|
||||
autofillRequest = autofillRequest,
|
||||
)
|
||||
|
||||
// Verify
|
||||
assertEquals(expected, actual)
|
||||
coVerify(exactly = 1) {
|
||||
autofillCipherProvider.getCardAutofillCiphers()
|
||||
}
|
||||
coVerify(exactly = 0) {
|
||||
autofillViewCode.buildFilledItemOrNull(code)
|
||||
autofillViewExpirationMonth.buildFilledItemOrNull(expirationMonth)
|
||||
autofillViewExpirationYear.buildFilledItemOrNull(expirationYear)
|
||||
autofillViewNumberOne.buildFilledItemOrNull(number)
|
||||
autofillViewNumberTwo.buildFilledItemOrNull(number)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -10,29 +10,23 @@ import org.junit.jupiter.api.Test
|
||||
class HtmlInfoExtensionsTest {
|
||||
@Test
|
||||
fun `isInputField should return true when tag is 'input'`() {
|
||||
// Setup
|
||||
val htmlInfo: HtmlInfo = mockk {
|
||||
every { tag } returns "input"
|
||||
}
|
||||
|
||||
// Test
|
||||
val actual = htmlInfo.isInputField
|
||||
|
||||
// Verify
|
||||
assertTrue(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isInputField should return false when tag is not 'input'`() {
|
||||
// Setup
|
||||
val htmlInfo: HtmlInfo = mockk {
|
||||
every { tag } returns "not input"
|
||||
}
|
||||
|
||||
// Test
|
||||
val actual = htmlInfo.isInputField
|
||||
|
||||
// Verify
|
||||
assertFalse(actual)
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,17 +30,21 @@ class ViewNodeExtensionsTest {
|
||||
hasPasswordTerms = false,
|
||||
)
|
||||
private val testAutofillValue: AutofillValue = mockk()
|
||||
private val mockHtmlInfo: HtmlInfo = mockk {
|
||||
every { attributes } returns emptyList()
|
||||
}
|
||||
|
||||
private val viewNode: AssistStructure.ViewNode = mockk {
|
||||
every { this@mockk.autofillId } returns expectedAutofillId
|
||||
every { this@mockk.idEntry } returns null
|
||||
every { this@mockk.hint } returns null
|
||||
every { this@mockk.autofillOptions } returns AUTOFILL_OPTIONS_ARRAY
|
||||
every { this@mockk.autofillType } returns AUTOFILL_TYPE
|
||||
every { this@mockk.autofillValue } returns testAutofillValue
|
||||
every { this@mockk.childCount } returns 0
|
||||
every { this@mockk.inputType } returns 1
|
||||
every { this@mockk.isFocused } returns expectedIsFocused
|
||||
every { autofillId } returns expectedAutofillId
|
||||
every { idEntry } returns null
|
||||
every { hint } returns null
|
||||
every { autofillOptions } returns AUTOFILL_OPTIONS_ARRAY
|
||||
every { autofillType } returns AUTOFILL_TYPE
|
||||
every { autofillValue } returns testAutofillValue
|
||||
every { childCount } returns 0
|
||||
every { inputType } returns 1
|
||||
every { isFocused } returns expectedIsFocused
|
||||
every { htmlInfo } returns mockHtmlInfo
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
@ -69,8 +73,9 @@ class ViewNodeExtensionsTest {
|
||||
unmockkStatic(AutofillValue::extractTextValue)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `toAutofillView should return AutofillView Card ExpirationMonth when hint matches`() {
|
||||
fun `toAutofillView should return AutofillView Card ExpirationMonth when autofillHints match`() {
|
||||
// Setup
|
||||
val autofillHint = View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH
|
||||
val expected = AutofillView.Card.ExpirationMonth(
|
||||
@ -88,7 +93,7 @@ class ViewNodeExtensionsTest {
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `toAutofillView should return AutofillView Card ExpirationMonth with empty options when hint matches and options are null`() {
|
||||
fun `toAutofillView should return AutofillView Card ExpirationMonth with empty options when autofillHints match and options are null`() {
|
||||
// Setup
|
||||
val autofillViewData = autofillViewData.copy(
|
||||
autofillOptions = emptyList(),
|
||||
@ -114,24 +119,139 @@ class ViewNodeExtensionsTest {
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `toAutofillView should return AutofillView Card ExpirationYear when hint matches`() {
|
||||
// Setup
|
||||
fun `toAutofillView should return AutofillView Card ExpirationMonth when html info isCardExpirationMonthField`() {
|
||||
setupUnsupportedInputFieldViewNode()
|
||||
val expected = AutofillView.Card.ExpirationMonth(
|
||||
data = autofillViewData,
|
||||
monthValue = MONTH_VALUE,
|
||||
)
|
||||
every { viewNode.htmlInfo.hints() } returns SUPPORTED_RAW_CARD_EXP_MONTH_HINTS
|
||||
|
||||
val actual = viewNode.toAutofillView()
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toAutofillView should return AutofillView Card ExpirationMonth when idEntry matches`() {
|
||||
setupUnsupportedInputFieldViewNode()
|
||||
val expected = AutofillView.Card.ExpirationMonth(
|
||||
data = autofillViewData,
|
||||
monthValue = MONTH_VALUE,
|
||||
)
|
||||
SUPPORTED_RAW_CARD_EXP_MONTH_HINTS.forEach { idEntry ->
|
||||
every { viewNode.idEntry } returns idEntry
|
||||
|
||||
val actual = viewNode.toAutofillView()
|
||||
|
||||
assertEquals(expected, actual, "Failed for idEntry: $idEntry")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toAutofillView should return AutofillView Card ExpirationMonth when hint matches`() {
|
||||
setupUnsupportedInputFieldViewNode()
|
||||
val expected = AutofillView.Card.ExpirationMonth(
|
||||
data = autofillViewData,
|
||||
monthValue = MONTH_VALUE,
|
||||
)
|
||||
SUPPORTED_RAW_CARD_EXP_MONTH_HINTS.forEach { hint ->
|
||||
every { viewNode.hint } returns hint
|
||||
val actual = viewNode.toAutofillView()
|
||||
assertEquals(expected, actual, "Failed for hint: $hint")
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `toAutofillView should return AutofillView Card ExpirationYear when autofillHints match`() {
|
||||
val autofillHint = View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR
|
||||
val expected = AutofillView.Card.ExpirationYear(
|
||||
data = autofillViewData,
|
||||
)
|
||||
every { viewNode.autofillHints } returns arrayOf(autofillHint)
|
||||
|
||||
// Test
|
||||
val actual = viewNode.toAutofillView()
|
||||
|
||||
// Verify
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toAutofillView should return AutofillView Card Number when hint matches`() {
|
||||
fun `toAutofillView should return AutofillView Card ExpirationYear when hint matches`() {
|
||||
setupUnsupportedInputFieldViewNode()
|
||||
val expected = AutofillView.Card.ExpirationYear(
|
||||
data = autofillViewData,
|
||||
)
|
||||
SUPPORTED_RAW_CARD_EXP_YEAR_HINTS.forEach { hint ->
|
||||
every { viewNode.hint } returns hint
|
||||
|
||||
val actual = viewNode.toAutofillView()
|
||||
|
||||
assertEquals(expected, actual, "Failed for hint: $hint")
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `toAutofillView should return AutofillView Card ExpirationYear when html info isCardExpirationYearField`() {
|
||||
setupUnsupportedInputFieldViewNode()
|
||||
val expected = AutofillView.Card.ExpirationYear(
|
||||
data = autofillViewData,
|
||||
)
|
||||
every { viewNode.htmlInfo.hints() } returns SUPPORTED_RAW_CARD_EXP_YEAR_HINTS
|
||||
|
||||
val actual = viewNode.toAutofillView()
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toAutofillView should return AutofillView Card ExpirationDate when autofillHints match`() {
|
||||
val autofillHint = View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE
|
||||
val expected = AutofillView.Card.ExpirationDate(
|
||||
data = autofillViewData,
|
||||
)
|
||||
every { viewNode.autofillHints } returns arrayOf(autofillHint)
|
||||
every { mockHtmlInfo.isInputField } returns true
|
||||
|
||||
val actual = viewNode.toAutofillView()
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toAutofillView should return AutofillView Card ExpirationDate when hint matches`() {
|
||||
setupUnsupportedInputFieldViewNode()
|
||||
val expected = AutofillView.Card.ExpirationDate(
|
||||
data = autofillViewData,
|
||||
)
|
||||
SUPPORTED_RAW_CARD_EXP_DATE_HINTS.forEach { hint ->
|
||||
every { viewNode.hint } returns hint
|
||||
|
||||
val actual = viewNode.toAutofillView()
|
||||
|
||||
assertEquals(expected, actual, "Failed for hint: $hint")
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `toAutofillView should return AutofillView Card ExpirationDate when html info isCardExpirationDateField`() {
|
||||
setupUnsupportedInputFieldViewNode()
|
||||
val expected = AutofillView.Card.ExpirationDate(
|
||||
data = autofillViewData,
|
||||
)
|
||||
every { viewNode.htmlInfo.hints() } returns SUPPORTED_RAW_CARD_EXP_DATE_HINTS
|
||||
|
||||
val actual = viewNode.toAutofillView()
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toAutofillView should return AutofillView Card Number when autofillHints match`() {
|
||||
// Setup
|
||||
val autofillHint = View.AUTOFILL_HINT_CREDIT_CARD_NUMBER
|
||||
val expected = AutofillView.Card.Number(
|
||||
@ -147,7 +267,35 @@ class ViewNodeExtensionsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toAutofillView should return AutofillView Card SecurityCode when hint matches`() {
|
||||
fun `toAutofillView should return AutofillView Card Number when hint matches`() {
|
||||
setupUnsupportedInputFieldViewNode()
|
||||
val expected = AutofillView.Card.Number(
|
||||
data = autofillViewData,
|
||||
)
|
||||
SUPPORTED_RAW_CARD_NUMBER_HINTS.forEach { hint ->
|
||||
every { viewNode.hint } returns hint
|
||||
|
||||
val actual = viewNode.toAutofillView()
|
||||
|
||||
assertEquals(expected, actual, "Failed for hint: $hint")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toAutofillView should return AutofillView Card Number when html info isCardNumberField`() {
|
||||
setupUnsupportedInputFieldViewNode()
|
||||
val expected = AutofillView.Card.Number(
|
||||
data = autofillViewData,
|
||||
)
|
||||
every { viewNode.htmlInfo.hints() } returns SUPPORTED_RAW_CARD_NUMBER_HINTS
|
||||
|
||||
val actual = viewNode.toAutofillView()
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toAutofillView should return AutofillView Card SecurityCode when autofillHints match`() {
|
||||
// Setup
|
||||
val autofillHint = View.AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE
|
||||
val expected = AutofillView.Card.SecurityCode(
|
||||
@ -163,18 +311,114 @@ class ViewNodeExtensionsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toAutofillView should return AutofillView Login Password when isPasswordField`() {
|
||||
// Setup
|
||||
fun `toAutofillView should return AutofillView Card SecurityCode when hint matches`() {
|
||||
setupUnsupportedInputFieldViewNode()
|
||||
val expected = AutofillView.Card.SecurityCode(
|
||||
data = autofillViewData,
|
||||
)
|
||||
SUPPORTED_RAW_CARD_SECURITY_CODE_HINTS.forEach { hint ->
|
||||
every { viewNode.hint } returns hint
|
||||
|
||||
val actual = viewNode.toAutofillView()
|
||||
|
||||
assertEquals(expected, actual, "Failed for hint: $hint")
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `toAutofillView should return AutofillView Card SecurityCode when html info isCardSecurityCodeField`() {
|
||||
setupUnsupportedInputFieldViewNode()
|
||||
val expected = AutofillView.Card.SecurityCode(
|
||||
data = autofillViewData,
|
||||
)
|
||||
every { viewNode.htmlInfo.hints() } returns SUPPORTED_RAW_CARD_SECURITY_CODE_HINTS
|
||||
val actual = viewNode.toAutofillView()
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toAutofillView should return AutofillView Card CardholderName when idEntry matches`() {
|
||||
setupUnsupportedInputFieldViewNode()
|
||||
val expected = AutofillView.Card.CardholderName(
|
||||
data = autofillViewData,
|
||||
)
|
||||
SUPPORTED_RAW_CARDHOLDER_NAME_HINTS.forEach { idEntry ->
|
||||
every { viewNode.idEntry } returns idEntry
|
||||
|
||||
val actual = viewNode.toAutofillView()
|
||||
|
||||
assertEquals(expected, actual, "Failed for idEntry: $idEntry")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toAutofillView should return AutofillView Card CardholderName when hint matches`() {
|
||||
setupUnsupportedInputFieldViewNode()
|
||||
val expected = AutofillView.Card.CardholderName(
|
||||
data = autofillViewData,
|
||||
)
|
||||
SUPPORTED_RAW_CARDHOLDER_NAME_HINTS.forEach { hint ->
|
||||
every { viewNode.hint } returns hint
|
||||
|
||||
val actual = viewNode.toAutofillView()
|
||||
|
||||
assertEquals(expected, actual, "Failed for hint: $hint")
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `toAutofillView should return AutofillView Card CardholderName when html info isCardholderNameField`() {
|
||||
setupUnsupportedInputFieldViewNode()
|
||||
val expected = AutofillView.Card.CardholderName(
|
||||
data = autofillViewData,
|
||||
)
|
||||
every { viewNode.htmlInfo.hints() } returns SUPPORTED_RAW_CARDHOLDER_NAME_HINTS
|
||||
val actual = viewNode.toAutofillView()
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `toAutofillView should return AutofillView Login Password when autofillHints match`() {
|
||||
val autofillHint = View.AUTOFILL_HINT_PASSWORD
|
||||
val expected = AutofillView.Login.Password(
|
||||
data = autofillViewData,
|
||||
)
|
||||
every { viewNode.autofillHints } returns arrayOf(autofillHint)
|
||||
|
||||
// Test
|
||||
val actual = viewNode.toAutofillView()
|
||||
|
||||
// Verify
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toAutofillView should return AutofillView Login Password when hint matches`() {
|
||||
setupUnsupportedInputFieldViewNode()
|
||||
val expected = AutofillView.Login.Password(
|
||||
data = autofillViewData.copy(hasPasswordTerms = true),
|
||||
)
|
||||
SUPPORTED_RAW_PASSWORD_HINTS.forEach { hint ->
|
||||
every { viewNode.hint } returns hint
|
||||
|
||||
val actual = viewNode.toAutofillView()
|
||||
|
||||
assertEquals(expected, actual, "Failed for hint: $hint")
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `toAutofillView should return AutofillView Login Password when html info isPasswordField`() {
|
||||
setupUnsupportedInputFieldViewNode()
|
||||
val expected = AutofillView.Login.Password(
|
||||
data = autofillViewData,
|
||||
)
|
||||
every { viewNode.htmlInfo.isPasswordField() } returns true
|
||||
|
||||
val actual = viewNode.toAutofillView()
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@ -234,16 +478,13 @@ class ViewNodeExtensionsTest {
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `toAutofillView should return only unused field when hint is not supported, is an inputField, and isn't a username or password`() {
|
||||
// Setup
|
||||
setupUnsupportedInputFieldViewNode()
|
||||
val expected = AutofillView.Unused(
|
||||
data = autofillViewData,
|
||||
)
|
||||
|
||||
// Test
|
||||
val actual = viewNode.toAutofillView()
|
||||
|
||||
// Verify
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@ -265,14 +506,13 @@ class ViewNodeExtensionsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isPasswordField returns true when supportedHint is AUTOFILL_HINT_PASSWORD`() {
|
||||
fun `isPasswordField returns true when html hints contains a supported password hint`() {
|
||||
// Setup
|
||||
val supportedHint = View.AUTOFILL_HINT_PASSWORD
|
||||
every { mockHtmlInfo.isInputField } returns true
|
||||
every { mockHtmlInfo.hints() } returns listOf("password")
|
||||
|
||||
// Test
|
||||
val actual = viewNode.isPasswordField(
|
||||
supportedHint = supportedHint,
|
||||
)
|
||||
val actual = viewNode.isPasswordField
|
||||
|
||||
// Verify
|
||||
assertTrue(actual)
|
||||
@ -286,9 +526,7 @@ class ViewNodeExtensionsTest {
|
||||
every { any<Int>().isPasswordInputType } returns true
|
||||
|
||||
// Test
|
||||
val actual = viewNode.isPasswordField(
|
||||
supportedHint = null,
|
||||
)
|
||||
val actual = viewNode.isPasswordField
|
||||
|
||||
// Verify
|
||||
assertTrue(actual)
|
||||
@ -302,9 +540,7 @@ class ViewNodeExtensionsTest {
|
||||
every { viewNode.htmlInfo.isPasswordField() } returns true
|
||||
|
||||
// Test
|
||||
val actual = viewNode.isPasswordField(
|
||||
supportedHint = null,
|
||||
)
|
||||
val actual = viewNode.isPasswordField
|
||||
|
||||
// Verify
|
||||
assertTrue(actual)
|
||||
@ -322,9 +558,7 @@ class ViewNodeExtensionsTest {
|
||||
every { viewNode.hint } returns hint
|
||||
|
||||
// Test
|
||||
val actual = viewNode.isPasswordField(
|
||||
supportedHint = null,
|
||||
)
|
||||
val actual = viewNode.isPasswordField
|
||||
|
||||
// Verify
|
||||
assertFalse(actual)
|
||||
@ -337,9 +571,7 @@ class ViewNodeExtensionsTest {
|
||||
every { viewNode.idEntry } returns hint
|
||||
|
||||
// Test
|
||||
val actual = viewNode.isPasswordField(
|
||||
supportedHint = null,
|
||||
)
|
||||
val actual = viewNode.isPasswordField
|
||||
|
||||
// Verify
|
||||
assertFalse(actual)
|
||||
@ -355,37 +587,20 @@ class ViewNodeExtensionsTest {
|
||||
every { viewNode.hint } returns SUPPORTED_RAW_USERNAME_HINTS.first()
|
||||
|
||||
// Test
|
||||
val actual = viewNode.isPasswordField(
|
||||
supportedHint = null,
|
||||
)
|
||||
val actual = viewNode.isPasswordField
|
||||
|
||||
// Verify
|
||||
assertFalse(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isUsernameField returns true when supportedHint is AUTOFILL_HINT_USERNAME`() {
|
||||
fun `isUsernameField returns true when html hints contains a supported username hint`() {
|
||||
// Setup
|
||||
val supportedHint = View.AUTOFILL_HINT_USERNAME
|
||||
every { mockHtmlInfo.isInputField } returns true
|
||||
every { mockHtmlInfo.hints() } returns SUPPORTED_RAW_USERNAME_HINTS
|
||||
|
||||
// Test
|
||||
val actual = viewNode.isUsernameField(
|
||||
supportedHint = supportedHint,
|
||||
)
|
||||
|
||||
// Verify
|
||||
assertTrue(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isUsernameField returns true when supportedHint is AUTOFILL_HINT_EMAIL_ADDRESS`() {
|
||||
// Setup
|
||||
val supportedHint = View.AUTOFILL_HINT_EMAIL_ADDRESS
|
||||
|
||||
// Test
|
||||
val actual = viewNode.isUsernameField(
|
||||
supportedHint = supportedHint,
|
||||
)
|
||||
val actual = viewNode.isUsernameField
|
||||
|
||||
// Verify
|
||||
assertTrue(actual)
|
||||
@ -400,9 +615,7 @@ class ViewNodeExtensionsTest {
|
||||
every { viewNode.hint } returns hint
|
||||
|
||||
// Test
|
||||
val actual = viewNode.isUsernameField(
|
||||
supportedHint = null,
|
||||
)
|
||||
val actual = viewNode.isUsernameField
|
||||
|
||||
// Verify
|
||||
assertTrue(actual)
|
||||
@ -415,9 +628,7 @@ class ViewNodeExtensionsTest {
|
||||
every { viewNode.idEntry } returns hint
|
||||
|
||||
// Test
|
||||
val actual = viewNode.isUsernameField(
|
||||
supportedHint = null,
|
||||
)
|
||||
val actual = viewNode.isUsernameField
|
||||
|
||||
// Verify
|
||||
assertTrue(actual)
|
||||
@ -431,9 +642,7 @@ class ViewNodeExtensionsTest {
|
||||
every { viewNode.htmlInfo.isUsernameField() } returns true
|
||||
|
||||
// Test
|
||||
val actual = viewNode.isUsernameField(
|
||||
supportedHint = null,
|
||||
)
|
||||
val actual = viewNode.isUsernameField
|
||||
|
||||
// Verify
|
||||
assertTrue(actual)
|
||||
@ -570,6 +779,7 @@ class ViewNodeExtensionsTest {
|
||||
every { viewNode.className } returns null
|
||||
every { any<Int>().isPasswordInputType } returns false
|
||||
every { any<Int>().isUsernameInputType } returns false
|
||||
every { viewNode.htmlInfo.hints() } returns emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
@ -599,5 +809,50 @@ private val SUPPORTED_RAW_USERNAME_HINTS: List<String> = listOf(
|
||||
"phone",
|
||||
"username",
|
||||
)
|
||||
private val SUPPORTED_RAW_CARD_EXP_MONTH_HINTS: List<String> = listOf(
|
||||
"exp_month",
|
||||
"expiration_month",
|
||||
"cc_exp_month",
|
||||
"card_exp_month",
|
||||
)
|
||||
private val SUPPORTED_RAW_CARD_EXP_YEAR_HINTS: List<String> = listOf(
|
||||
"exp_year",
|
||||
"expiration_year",
|
||||
"cc_exp_year",
|
||||
"card_exp_year",
|
||||
)
|
||||
private val SUPPORTED_RAW_CARD_NUMBER_HINTS: List<String> = listOf(
|
||||
"cc_number",
|
||||
"card_number",
|
||||
"credit_card_number",
|
||||
)
|
||||
private val SUPPORTED_RAW_CARD_SECURITY_CODE_HINTS: List<String> = listOf(
|
||||
"cc_security_code",
|
||||
"card_security_code",
|
||||
"credit_card_security_code",
|
||||
"cc_verification_code",
|
||||
"card_verification_code",
|
||||
"credit_card_verification_code",
|
||||
"cvv",
|
||||
"cvc",
|
||||
"cvv2",
|
||||
"cvc2",
|
||||
)
|
||||
private val SUPPORTED_RAW_CARD_EXP_DATE_HINTS: List<String> = listOf(
|
||||
"exp_date",
|
||||
"expiration_date",
|
||||
"expiry_date",
|
||||
"cc_exp_date",
|
||||
"card_exp_date",
|
||||
)
|
||||
private val SUPPORTED_RAW_CARDHOLDER_NAME_HINTS: List<String> = listOf(
|
||||
"cc_name",
|
||||
"cc_cardholder",
|
||||
"card_name",
|
||||
"card_cardholder",
|
||||
"credit_card_name",
|
||||
"credit_card_cardholder",
|
||||
"name_on_card",
|
||||
)
|
||||
private const val MONTH_VALUE: String = "MONTH_VALUE"
|
||||
private const val TEXT_VALUE: String = "TEXT_VALUE"
|
||||
|
||||
@ -1069,4 +1069,5 @@ Do you want to switch to this account?</string>
|
||||
<string name="local_items_are_collapsed_click_to_expand">Local items are collapsed, click to expand.</string>
|
||||
<string name="items_expanded_click_to_collapse">Items are expanded, click to collapse.</string>
|
||||
<string name="items_are_collapsed_click_to_expand">Items are collapsed, click to expand.</string>
|
||||
<string name="select_a_card_for_x">Select a card for %s</string>
|
||||
</resources>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user