mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 15:15:34 +03:00
Perform deeper login data parsing (#758)
This commit is contained in:
parent
54802db0b3
commit
96513d74c3
8 changed files with 639 additions and 56 deletions
|
@ -0,0 +1,31 @@
|
|||
package com.x8bit.bitwarden.data.autofill.util
|
||||
|
||||
import android.view.ViewStructure.HtmlInfo
|
||||
import com.x8bit.bitwarden.data.platform.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
|
||||
|
||||
/**
|
||||
* Whether this [HtmlInfo] represents an input field.
|
||||
*/
|
||||
val HtmlInfo?.isInputField: Boolean get() = this?.tag == "input"
|
|
@ -0,0 +1,31 @@
|
|||
package com.x8bit.bitwarden.data.autofill.util
|
||||
|
||||
import android.text.InputType
|
||||
|
||||
/**
|
||||
* Whether this [Int] is a password [InputType].
|
||||
*/
|
||||
val Int.isPasswordInputType: Boolean
|
||||
get() {
|
||||
// The legacy xamarin app mentions that multi-line input types are coming through with
|
||||
// TYPE_TEXT_VARIATION_PASSWORD flags. We have no other context to this.
|
||||
val isMultiline = this.hasFlag(InputType.TYPE_TEXT_VARIATION_PASSWORD) &&
|
||||
this.hasFlag(InputType.TYPE_TEXT_FLAG_MULTI_LINE)
|
||||
|
||||
val isPasswordInputType = this.hasFlag(InputType.TYPE_TEXT_VARIATION_PASSWORD) ||
|
||||
this.hasFlag(InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) ||
|
||||
this.hasFlag(InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD)
|
||||
|
||||
return !isMultiline && isPasswordInputType
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this [Int] is a username [InputType].
|
||||
*/
|
||||
val Int.isUsernameInputType: Boolean
|
||||
get() = this.hasFlag(InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS)
|
||||
|
||||
/**
|
||||
* Whether this [Int] contains [flag].
|
||||
*/
|
||||
private fun Int.hasFlag(flag: Int): Boolean = (this and flag) == flag
|
|
@ -0,0 +1,18 @@
|
|||
package com.x8bit.bitwarden.data.autofill.util
|
||||
|
||||
/**
|
||||
* Check whether this string contains any of these [terms].
|
||||
*
|
||||
* @param terms The terms that should be searched for in this [String].
|
||||
* @param ignoreCase Whether the comparison should be sensitive to casing.
|
||||
*/
|
||||
fun String.containsAnyTerms(
|
||||
terms: List<String>,
|
||||
ignoreCase: Boolean = true,
|
||||
): Boolean =
|
||||
terms.any {
|
||||
this.contains(
|
||||
other = it,
|
||||
ignoreCase = ignoreCase,
|
||||
)
|
||||
}
|
|
@ -4,17 +4,72 @@ import android.app.assist.AssistStructure
|
|||
import android.view.View
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillView
|
||||
|
||||
/**
|
||||
* The class name of the android edit text field.
|
||||
*/
|
||||
private const val ANDROID_EDIT_TEXT_CLASS_NAME: String = "android.widget.EditText"
|
||||
|
||||
/**
|
||||
* 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_NUMBER,
|
||||
View.AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE,
|
||||
View.AUTOFILL_HINT_EMAIL_ADDRESS,
|
||||
View.AUTOFILL_HINT_PASSWORD,
|
||||
View.AUTOFILL_HINT_USERNAME,
|
||||
)
|
||||
|
||||
/**
|
||||
* Whether this [AssistStructure.ViewNode] represents an input field.
|
||||
*/
|
||||
private val AssistStructure.ViewNode.isInputField: Boolean
|
||||
get() = className == ANDROID_EDIT_TEXT_CLASS_NAME || htmlInfo.isInputField
|
||||
|
||||
/**
|
||||
* Attempt to convert this [AssistStructure.ViewNode] into an [AutofillView]. If the view node
|
||||
* doesn't contain a valid autofillId, it isn't an a view setup for autofill, so we return null. If
|
||||
* it is has an autofillHint that we do not support, we also return null.
|
||||
* it doesn't have a supported hint and isn't an input field, we also return null.
|
||||
*/
|
||||
fun AssistStructure.ViewNode.toAutofillView(): AutofillView? = autofillId
|
||||
// We only care about nodes with a valid `AutofillId`.
|
||||
?.let { nonNullAutofillId ->
|
||||
autofillHints
|
||||
?.firstOrNull { SUPPORTED_HINTS.contains(it) }
|
||||
?.let { supportedHint ->
|
||||
fun AssistStructure.ViewNode.toAutofillView(): AutofillView? =
|
||||
this
|
||||
.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) {
|
||||
val autofillViewData = AutofillView.Data(
|
||||
autofillId = nonNullAutofillId,
|
||||
idPackage = idPackage,
|
||||
|
@ -24,51 +79,51 @@ fun AssistStructure.ViewNode.toAutofillView(): AutofillView? = autofillId
|
|||
)
|
||||
buildAutofillView(
|
||||
autofillViewData = autofillViewData,
|
||||
hint = supportedHint,
|
||||
supportedHint = supportedHint,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert [autofillViewData] into an [AutofillView] if the [hint] is supported.
|
||||
* Attempt to convert this [AssistStructure.ViewNode] and [autofillViewData] into an [AutofillView].
|
||||
*/
|
||||
private fun buildAutofillView(
|
||||
private fun AssistStructure.ViewNode.buildAutofillView(
|
||||
autofillViewData: AutofillView.Data,
|
||||
hint: String,
|
||||
): AutofillView? = when (hint) {
|
||||
View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH -> {
|
||||
supportedHint: String?,
|
||||
): AutofillView? = when {
|
||||
supportedHint == View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH -> {
|
||||
AutofillView.Card.ExpirationMonth(
|
||||
data = autofillViewData,
|
||||
)
|
||||
}
|
||||
|
||||
View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR -> {
|
||||
supportedHint == View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR -> {
|
||||
AutofillView.Card.ExpirationYear(
|
||||
data = autofillViewData,
|
||||
)
|
||||
}
|
||||
|
||||
View.AUTOFILL_HINT_CREDIT_CARD_NUMBER -> {
|
||||
supportedHint == View.AUTOFILL_HINT_CREDIT_CARD_NUMBER -> {
|
||||
AutofillView.Card.Number(
|
||||
data = autofillViewData,
|
||||
)
|
||||
}
|
||||
|
||||
View.AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE -> {
|
||||
supportedHint == View.AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE -> {
|
||||
AutofillView.Card.SecurityCode(
|
||||
data = autofillViewData,
|
||||
)
|
||||
}
|
||||
|
||||
View.AUTOFILL_HINT_PASSWORD -> {
|
||||
this.isPasswordField(supportedHint) -> {
|
||||
AutofillView.Login.Password(
|
||||
data = autofillViewData,
|
||||
)
|
||||
}
|
||||
|
||||
View.AUTOFILL_HINT_EMAIL_ADDRESS,
|
||||
View.AUTOFILL_HINT_USERNAME,
|
||||
-> {
|
||||
this.isUsernameField(supportedHint) -> {
|
||||
AutofillView.Login.Username(
|
||||
data = autofillViewData,
|
||||
)
|
||||
|
@ -78,14 +133,34 @@ private fun buildAutofillView(
|
|||
}
|
||||
|
||||
/**
|
||||
* All of the supported autofill hints for the app.
|
||||
* Check whether this [AssistStructure.ViewNode] represents a password field.
|
||||
*/
|
||||
private val SUPPORTED_HINTS: List<String> = listOf(
|
||||
View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH,
|
||||
View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR,
|
||||
View.AUTOFILL_HINT_CREDIT_CARD_NUMBER,
|
||||
View.AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE,
|
||||
View.AUTOFILL_HINT_EMAIL_ADDRESS,
|
||||
View.AUTOFILL_HINT_PASSWORD,
|
||||
View.AUTOFILL_HINT_USERNAME,
|
||||
)
|
||||
@Suppress("ReturnCount")
|
||||
fun AssistStructure.ViewNode.isPasswordField(
|
||||
supportedHint: String?,
|
||||
): Boolean {
|
||||
if (supportedHint == View.AUTOFILL_HINT_PASSWORD) return true
|
||||
|
||||
if (this.hint?.containsAnyTerms(SUPPORTED_RAW_PASSWORD_HINTS) == true) 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()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 ||
|
||||
idEntry?.containsAnyTerms(SUPPORTED_RAW_USERNAME_HINTS) == true ||
|
||||
hint?.containsAnyTerms(SUPPORTED_RAW_USERNAME_HINTS) == true
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
package com.x8bit.bitwarden.data.autofill.util
|
||||
|
||||
import android.view.ViewStructure.HtmlInfo
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
package com.x8bit.bitwarden.data.autofill.util
|
||||
|
||||
import android.text.InputType
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class IntExtensionsTest {
|
||||
@Test
|
||||
fun `isPasswordInputType returns false when is multiline`() {
|
||||
// Setup
|
||||
val int = InputType.TYPE_TEXT_VARIATION_PASSWORD or InputType.TYPE_TEXT_FLAG_MULTI_LINE
|
||||
|
||||
// Test
|
||||
val actual = int.isPasswordInputType
|
||||
|
||||
// Verify
|
||||
assertFalse(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isPasswordInputType returns false when not multiline and not password`() {
|
||||
// Setup
|
||||
val int = InputType.TYPE_CLASS_PHONE
|
||||
|
||||
// Test
|
||||
val actual = int.isPasswordInputType
|
||||
|
||||
// Verify
|
||||
assertFalse(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isPasswordInputType returns true when not multiline and TYPE_TEXT_VARIATION_PASSWORD`() {
|
||||
// Setup
|
||||
val int = InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
|
||||
// Test
|
||||
val actual = int.isPasswordInputType
|
||||
|
||||
// Verify
|
||||
assertTrue(actual)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `isPasswordInputType returns true when not multiline and TYPE_TEXT_VARIATION_VISIBLE_PASSWORD`() {
|
||||
// Setup
|
||||
val int = InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
|
||||
|
||||
// Test
|
||||
val actual = int.isPasswordInputType
|
||||
|
||||
// Verify
|
||||
assertTrue(actual)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `isPasswordInputType returns true when not multiline and TYPE_TEXT_VARIATION_WEB_PASSWORD`() {
|
||||
// Setup
|
||||
val int = InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD
|
||||
|
||||
// Test
|
||||
val actual = int.isPasswordInputType
|
||||
|
||||
// Verify
|
||||
assertTrue(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isUsernameInputType returns false when not TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS`() {
|
||||
// Setup
|
||||
val int = InputType.TYPE_CLASS_PHONE
|
||||
|
||||
// Test
|
||||
val actual = int.isUsernameInputType
|
||||
|
||||
// Verify
|
||||
assertFalse(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isUsernameInputType returns true when TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS`() {
|
||||
// Setup
|
||||
val int = InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS
|
||||
|
||||
// Test
|
||||
val actual = int.isUsernameInputType
|
||||
|
||||
// Verify
|
||||
assertTrue(actual)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
package com.x8bit.bitwarden.data.autofill.util
|
||||
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class StringExtensionsTest {
|
||||
@Test
|
||||
fun `containsAnyTerms returns true when string contains a term`() {
|
||||
// Setup
|
||||
val terms = listOf(
|
||||
"bike",
|
||||
"bicycle",
|
||||
)
|
||||
val string = "I want to ride my bicycle"
|
||||
|
||||
// Test
|
||||
val actual = string.containsAnyTerms(
|
||||
terms = terms,
|
||||
ignoreCase = false,
|
||||
)
|
||||
|
||||
// Verify
|
||||
assertTrue(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `containsAnyTerms returns false when string doesn't contain a term`() {
|
||||
// Setup
|
||||
val terms = listOf(
|
||||
"bike",
|
||||
"bicycle",
|
||||
)
|
||||
val string = "I want to ride my tricycle"
|
||||
|
||||
// Test
|
||||
val actual = string.containsAnyTerms(
|
||||
terms = terms,
|
||||
ignoreCase = false,
|
||||
)
|
||||
|
||||
// Verify
|
||||
assertFalse(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `containsAnyTerms returns true when string contains a term while ignoring case`() {
|
||||
// Setup
|
||||
val terms = listOf(
|
||||
"bike",
|
||||
"bicycle",
|
||||
)
|
||||
val string = "I want to ride my BICYCLE"
|
||||
|
||||
// Test
|
||||
val actual = string.containsAnyTerms(
|
||||
terms = terms,
|
||||
ignoreCase = true,
|
||||
)
|
||||
|
||||
// Verify
|
||||
assertTrue(actual)
|
||||
}
|
||||
}
|
|
@ -2,12 +2,19 @@ package com.x8bit.bitwarden.data.autofill.util
|
|||
|
||||
import android.app.assist.AssistStructure
|
||||
import android.view.View
|
||||
import android.view.ViewStructure.HtmlInfo
|
||||
import android.view.autofill.AutofillId
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillView
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkStatic
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class ViewNodeExtensionsTest {
|
||||
|
@ -20,15 +27,33 @@ class ViewNodeExtensionsTest {
|
|||
webDomain = WEB_DOMAIN,
|
||||
webScheme = WEB_SCHEME,
|
||||
)
|
||||
|
||||
private val viewNode: AssistStructure.ViewNode = mockk {
|
||||
every { this@mockk.autofillId } returns expectedAutofillId
|
||||
every { this@mockk.childCount } returns 0
|
||||
every { this@mockk.inputType } returns 1
|
||||
every { this@mockk.idPackage } returns ID_PACKAGE
|
||||
every { this@mockk.isFocused } returns expectedIsFocused
|
||||
every { this@mockk.webDomain } returns WEB_DOMAIN
|
||||
every { this@mockk.webScheme } returns WEB_SCHEME
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
mockkStatic(HtmlInfo::isInputField)
|
||||
mockkStatic(HtmlInfo::isPasswordField)
|
||||
mockkStatic(Int::isPasswordInputType)
|
||||
mockkStatic(Int::isUsernameInputType)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun teardown() {
|
||||
unmockkStatic(HtmlInfo::isInputField)
|
||||
unmockkStatic(HtmlInfo::isPasswordField)
|
||||
unmockkStatic(Int::isPasswordInputType)
|
||||
unmockkStatic(Int::isUsernameInputType)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toAutofillView should return AutofillView Card ExpirationMonth when hint matches`() {
|
||||
// Setup
|
||||
|
@ -94,23 +119,7 @@ class ViewNodeExtensionsTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `toAutofillView should return AutofillView Login Username when hint is EMAIL`() {
|
||||
// Setup
|
||||
val autofillHint = View.AUTOFILL_HINT_EMAIL_ADDRESS
|
||||
val expected = AutofillView.Login.Username(
|
||||
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`() {
|
||||
fun `toAutofillView should return AutofillView Login Password when isPasswordField`() {
|
||||
// Setup
|
||||
val autofillHint = View.AUTOFILL_HINT_PASSWORD
|
||||
val expected = AutofillView.Login.Password(
|
||||
|
@ -125,14 +134,17 @@ class ViewNodeExtensionsTest {
|
|||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `toAutofillView should return AutofillView Login Username when hint is USERNAME`() {
|
||||
fun `toAutofillView should return AutofillView Login Username when is android text field and is isUsernameField`() {
|
||||
// Setup
|
||||
val autofillHint = View.AUTOFILL_HINT_USERNAME
|
||||
val expected = AutofillView.Login.Username(
|
||||
data = autofillViewData,
|
||||
)
|
||||
every { viewNode.autofillHints } returns arrayOf(autofillHint)
|
||||
setupUnsupportedInputFieldViewNode()
|
||||
every { viewNode.className } returns ANDROID_EDIT_TEXT_CLASS_NAME
|
||||
every { any<Int>().isPasswordInputType } returns false
|
||||
every { any<Int>().isUsernameInputType } returns true
|
||||
|
||||
// Test
|
||||
val actual = viewNode.toAutofillView()
|
||||
|
@ -142,10 +154,25 @@ class ViewNodeExtensionsTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `toAutofillView should return null when hint is not supported`() {
|
||||
fun `toAutofillView should return null when hint is not supported and isn't an inputField`() {
|
||||
// Setup
|
||||
val autofillHint = "Shenanigans"
|
||||
every { viewNode.autofillHints } returns arrayOf(autofillHint)
|
||||
every { viewNode.className } returns null
|
||||
every { viewNode.htmlInfo.isInputField } returns false
|
||||
|
||||
// Test
|
||||
val actual = viewNode.toAutofillView()
|
||||
|
||||
// Verify
|
||||
assertNull(actual)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `toAutofillView should return null when hint is not supported, is an inputField, and isn't a username or password`() {
|
||||
// Setup
|
||||
setupUnsupportedInputFieldViewNode()
|
||||
|
||||
// Test
|
||||
val actual = viewNode.toAutofillView()
|
||||
|
@ -171,9 +198,214 @@ class ViewNodeExtensionsTest {
|
|||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ID_PACKAGE: String = "ID_PACKAGE"
|
||||
private const val WEB_DOMAIN: String = "WEB_DOMAIN"
|
||||
private const val WEB_SCHEME: String = "WEB_SCHEME"
|
||||
@Test
|
||||
fun `isPasswordField returns true when supportedHint is AUTOFILL_HINT_PASSWORD`() {
|
||||
// Setup
|
||||
val supportedHint = View.AUTOFILL_HINT_PASSWORD
|
||||
|
||||
// Test
|
||||
val actual = viewNode.isPasswordField(
|
||||
supportedHint = supportedHint,
|
||||
)
|
||||
|
||||
// Verify
|
||||
assertTrue(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isPasswordField returns true when supportedHint is null and hint is supported`() {
|
||||
SUPPORTED_RAW_PASSWORD_HINTS
|
||||
.forEach { hint ->
|
||||
// Setup
|
||||
every { viewNode.hint } returns hint
|
||||
|
||||
// Test
|
||||
val actual = viewNode.isPasswordField(
|
||||
supportedHint = null,
|
||||
)
|
||||
|
||||
// Verify
|
||||
assertTrue(actual)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `isPasswordField returns true when hints aren't supported, isPasswordInputType, isValidField, and isn't username`() {
|
||||
// Setup
|
||||
setupUnsupportedInputFieldViewNode()
|
||||
every { any<Int>().isPasswordInputType } returns true
|
||||
|
||||
// Test
|
||||
val actual = viewNode.isPasswordField(
|
||||
supportedHint = null,
|
||||
)
|
||||
|
||||
// Verify
|
||||
assertTrue(actual)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `isPasswordField returns true when hints aren't supported, not isPasswordInputType, and htmlInfo isPasswordField is true`() {
|
||||
// Setup
|
||||
setupUnsupportedInputFieldViewNode()
|
||||
every { viewNode.htmlInfo.isPasswordField() } returns true
|
||||
|
||||
// Test
|
||||
val actual = viewNode.isPasswordField(
|
||||
supportedHint = null,
|
||||
)
|
||||
|
||||
// Verify
|
||||
assertTrue(actual)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `isPasswordField returns false when hints aren't supported, isPasswordInputType, not validInputField, and htmlInfo isPasswordField is false`() {
|
||||
// Setup testing the hint
|
||||
setupUnsupportedInputFieldViewNode()
|
||||
every { any<Int>().isPasswordInputType } returns true
|
||||
|
||||
IGNORED_RAW_HINTS.forEach { hint ->
|
||||
// Setup
|
||||
every { viewNode.hint } returns hint
|
||||
|
||||
// Test
|
||||
val actual = viewNode.isPasswordField(
|
||||
supportedHint = null,
|
||||
)
|
||||
|
||||
// Verify
|
||||
assertFalse(actual)
|
||||
}
|
||||
|
||||
// Setup testing the idEntry
|
||||
every { viewNode.hint } returns null
|
||||
IGNORED_RAW_HINTS.forEach { hint ->
|
||||
// Setup
|
||||
every { viewNode.idEntry } returns hint
|
||||
|
||||
// Test
|
||||
val actual = viewNode.isPasswordField(
|
||||
supportedHint = null,
|
||||
)
|
||||
|
||||
// Verify
|
||||
assertFalse(actual)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `isPasswordField returns false when hints aren't supported, isPasswordInputType, validInputField, isUsernameField, and htmlInfo isPasswordField is false`() {
|
||||
// Setup
|
||||
setupUnsupportedInputFieldViewNode()
|
||||
every { any<Int>().isPasswordInputType } returns true
|
||||
every { viewNode.hint } returns SUPPORTED_RAW_USERNAME_HINTS.first()
|
||||
|
||||
// Test
|
||||
val actual = viewNode.isPasswordField(
|
||||
supportedHint = null,
|
||||
)
|
||||
|
||||
// Verify
|
||||
assertFalse(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isUsernameField returns true whe supportedHint is AUTOFILL_HINT_USERNAME`() {
|
||||
// Setup
|
||||
val supportedHint = View.AUTOFILL_HINT_USERNAME
|
||||
|
||||
// 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,
|
||||
)
|
||||
|
||||
// Verify
|
||||
assertTrue(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isUsernameField returns true when supportedHint is null and raw hint is supported`() {
|
||||
// Setup testing the hints
|
||||
every { viewNode.idEntry } returns null
|
||||
SUPPORTED_RAW_USERNAME_HINTS.forEach { hint ->
|
||||
// Setup
|
||||
every { viewNode.hint } returns hint
|
||||
|
||||
// Test
|
||||
val actual = viewNode.isUsernameField(
|
||||
supportedHint = null,
|
||||
)
|
||||
|
||||
// Verify
|
||||
assertTrue(actual)
|
||||
}
|
||||
|
||||
// Setup testing the idEntries
|
||||
every { viewNode.hint } returns null
|
||||
SUPPORTED_RAW_USERNAME_HINTS.forEach { hint ->
|
||||
// Setup
|
||||
every { viewNode.idEntry } returns hint
|
||||
|
||||
// Test
|
||||
val actual = viewNode.isUsernameField(
|
||||
supportedHint = null,
|
||||
)
|
||||
|
||||
// Verify
|
||||
assertTrue(actual)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up [viewNode] to be an input field but not supported.
|
||||
*/
|
||||
private fun setupUnsupportedInputFieldViewNode() {
|
||||
every { viewNode.hint } returns null
|
||||
every { viewNode.htmlInfo.isPasswordField() } returns false
|
||||
every { viewNode.htmlInfo.isInputField } returns true
|
||||
every { viewNode.idEntry } returns null
|
||||
every { viewNode.autofillHints } returns emptyArray()
|
||||
every { viewNode.className } returns null
|
||||
every { any<Int>().isPasswordInputType } returns false
|
||||
every { any<Int>().isUsernameInputType } returns false
|
||||
}
|
||||
}
|
||||
|
||||
private const val ANDROID_EDIT_TEXT_CLASS_NAME: String = "android.widget.EditText"
|
||||
private const val ID_PACKAGE: String = "ID_PACKAGE"
|
||||
private const val WEB_DOMAIN: String = "WEB_DOMAIN"
|
||||
private const val WEB_SCHEME: String = "WEB_SCHEME"
|
||||
private val IGNORED_RAW_HINTS: List<String> = listOf(
|
||||
"search",
|
||||
"find",
|
||||
"recipient",
|
||||
"edit",
|
||||
)
|
||||
private val SUPPORTED_RAW_PASSWORD_HINTS: List<String> = listOf(
|
||||
"password",
|
||||
"pswd",
|
||||
)
|
||||
private val SUPPORTED_RAW_USERNAME_HINTS: List<String> = listOf(
|
||||
"email",
|
||||
"phone",
|
||||
"username",
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue