diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/builder/FillResponseBuilderImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/builder/FillResponseBuilderImpl.kt index f7ebd91c89..45289a15ec 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/builder/FillResponseBuilderImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/builder/FillResponseBuilderImpl.kt @@ -32,8 +32,7 @@ class FillResponseBuilderImpl : FillResponseBuilder { filledData .filledPartitions .forEach { filledPartition -> - // It won't be empty but we really don't want to make an empty dataset, - // it causes a crash. + // We really don't want to make an empty dataset as it causes a crash. if (filledPartition.filledItems.isNotEmpty()) { // We build a dataset for each filled partition. A filled partition is a // copy of all the views that we are going to fill, loaded with the data diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/builder/FilledDataBuilderImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/builder/FilledDataBuilderImpl.kt index dcb8fdf7b4..e26d813da0 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/builder/FilledDataBuilderImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/builder/FilledDataBuilderImpl.kt @@ -95,7 +95,7 @@ class FilledDataBuilderImpl( inlinePresentationSpec: InlinePresentationSpec?, ): FilledPartition { val filledItems = autofillViews - .map { autofillView -> + .mapNotNull { autofillView -> val value = when (autofillView) { is AutofillView.Card.ExpirationMonth -> autofillCipher.expirationMonth is AutofillView.Card.ExpirationYear -> autofillCipher.expirationYear @@ -124,7 +124,7 @@ class FilledDataBuilderImpl( inlinePresentationSpec: InlinePresentationSpec?, ): FilledPartition { val filledItems = autofillViews - .map { autofillView -> + .mapNotNull { autofillView -> val value = when (autofillView) { is AutofillView.Login.Username -> autofillCipher.username is AutofillView.Login.Password -> autofillCipher.password diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/model/AutofillView.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/model/AutofillView.kt index 2d112bbdd3..d6be853153 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/model/AutofillView.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/model/AutofillView.kt @@ -11,11 +11,15 @@ sealed class AutofillView { * The data important to a given [AutofillView]. * * @param autofillId The [AutofillId] associated with this view. + * @param autofillOptions A list of autofill options that can be used to fill this view. + * @param autofillType The autofill field type. (ex: View.AUTOFILL_TYPE_TEXT) * @param isFocused Whether the view is currently focused. * @param textValue A text value that represents the input present in the field. */ data class Data( val autofillId: AutofillId, + val autofillOptions: List, + val autofillType: Int, val isFocused: Boolean, val textValue: String?, ) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/util/AutofillValueExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/util/AutofillValueExtensions.kt index 1723e6b0c6..0bb5d6591a 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/util/AutofillValueExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/util/AutofillValueExtensions.kt @@ -7,14 +7,14 @@ import android.view.autofill.AutofillValue */ @Suppress("MagicNumber") fun AutofillValue.extractMonthValue( - autofillOptions: List?, + autofillOptions: List, ): String? = when { - this.isList && autofillOptions?.size == 13 -> { + this.isList && autofillOptions.size == 13 -> { this.listValue.toString() } - this.isList && autofillOptions?.size == 12 -> { + this.isList && autofillOptions.size == 12 -> { (this.listValue + 1).toString() } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/util/AutofillViewExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/util/AutofillViewExtensions.kt index c9bedd7bb0..f74f227174 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/util/AutofillViewExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/util/AutofillViewExtensions.kt @@ -1,17 +1,63 @@ package com.x8bit.bitwarden.data.autofill.util +import android.view.View import android.view.autofill.AutofillValue import com.x8bit.bitwarden.data.autofill.model.AutofillView import com.x8bit.bitwarden.data.autofill.model.FilledItem /** - * Convert this [AutofillView] into a [FilledItem]. + * Convert this [AutofillView] into a [FilledItem]. Return null if not possible. */ fun AutofillView.buildFilledItemOrNull( value: String, -): FilledItem = - // TODO: handle other autofill types (BIT-1457) - FilledItem( - autofillId = data.autofillId, - value = AutofillValue.forText(value), - ) +): FilledItem? = + when (this.data.autofillType) { + View.AUTOFILL_TYPE_DATE -> { + value + .toLongOrNull() + ?.let { AutofillValue.forDate(it) } + } + + View.AUTOFILL_TYPE_LIST -> this.buildListAutofillValueOrNull(value = value) + View.AUTOFILL_TYPE_TEXT -> AutofillValue.forText(value) + View.AUTOFILL_TYPE_TOGGLE -> { + value + .toBooleanStrictOrNull() + ?.let { AutofillValue.forToggle(it) } + } + + else -> null + } + ?.let { autofillValue -> + FilledItem( + autofillId = this.data.autofillId, + value = autofillValue, + ) + } + +/** + * Build a list [AutofillValue] out of [value] or return null if not possible. + */ +@Suppress("MagicNumber") +private fun AutofillView.buildListAutofillValueOrNull( + value: String, +): AutofillValue? = + if (this is AutofillView.Card.ExpirationMonth) { + val autofillOptionsSize = this.data.autofillOptions.size + // The idea here is that `value` is a numerical representation of a month. + val monthIndex = value.toIntOrNull() + when { + monthIndex == null -> null + // We expect there is some placeholder or empty space at the beginning of the list. + autofillOptionsSize == 13 -> AutofillValue.forList(monthIndex) + autofillOptionsSize >= monthIndex -> AutofillValue.forList(monthIndex - 1) + else -> null + } + } else { + this + .data + .autofillOptions + .indexOfFirst { it == value } + .takeIf { it != -1 } + ?.let { AutofillValue.forList(it) } + } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/util/ViewNodeExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/util/ViewNodeExtensions.kt index 1ea8e97cec..f5c71e2f1c 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/util/ViewNodeExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/util/ViewNodeExtensions.kt @@ -76,12 +76,20 @@ fun AssistStructure.ViewNode.toAutofillView(): AutofillView? = ?.firstOrNull { SUPPORTED_VIEW_HINTS.contains(it) } if (supportedHint != null || this.isInputField) { + val autofillOptions = this + .autofillOptions + .orEmpty() + .map { it.toString() } + val autofillViewData = AutofillView.Data( autofillId = nonNullAutofillId, - isFocused = isFocused, + autofillOptions = autofillOptions, + autofillType = this.autofillType, + isFocused = this.isFocused, textValue = this.autofillValue?.extractTextValue(), ) buildAutofillView( + autofillOptions = autofillOptions, autofillViewData = autofillViewData, supportedHint = supportedHint, ) @@ -94,13 +102,11 @@ fun AssistStructure.ViewNode.toAutofillView(): AutofillView? = * Attempt to convert this [AssistStructure.ViewNode] and [autofillViewData] into an [AutofillView]. */ private fun AssistStructure.ViewNode.buildAutofillView( + autofillOptions: List, autofillViewData: AutofillView.Data, supportedHint: String?, ): AutofillView? = when { supportedHint == View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH -> { - val autofillOptions = this - .autofillOptions - ?.map { it.toString() } val monthValue = this .autofillValue ?.extractMonthValue( diff --git a/app/src/test/java/com/x8bit/bitwarden/data/autofill/builder/FillResponseBuilderTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/autofill/builder/FillResponseBuilderTest.kt index d942fd05bf..ef1ddbd0de 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/autofill/builder/FillResponseBuilderTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/autofill/builder/FillResponseBuilderTest.kt @@ -5,6 +5,7 @@ import android.content.IntentSender import android.service.autofill.Dataset import android.service.autofill.FillResponse import android.service.autofill.SaveInfo +import android.view.View import android.view.autofill.AutofillId import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo import com.x8bit.bitwarden.data.autofill.model.AutofillCipher @@ -150,6 +151,8 @@ class FillResponseBuilderTest { AutofillView.Login.Username( data = AutofillView.Data( autofillId = mockk(), + autofillOptions = emptyList(), + autofillType = AUTOFILL_TYPE, isFocused = true, textValue = null, ), @@ -259,6 +262,8 @@ class FillResponseBuilderTest { AutofillView.Login.Username( data = AutofillView.Data( autofillId = mockk(), + autofillOptions = emptyList(), + autofillType = AUTOFILL_TYPE, isFocused = true, textValue = null, ), @@ -341,6 +346,7 @@ class FillResponseBuilderTest { } companion object { + private const val AUTOFILL_TYPE: Int = View.AUTOFILL_TYPE_TEXT private const val CIPHER_ID: String = "1234567890" private const val PACKAGE_NAME: String = "com.x8bit.bitwarden" } diff --git a/app/src/test/java/com/x8bit/bitwarden/data/autofill/builder/FilledDataBuilderTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/autofill/builder/FilledDataBuilderTest.kt index 501f22ecdc..f0af2d6078 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/autofill/builder/FilledDataBuilderTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/autofill/builder/FilledDataBuilderTest.kt @@ -47,8 +47,9 @@ class FilledDataBuilderTest { unmockkStatic(AutofillView::buildFilledItemOrNull) } + @Suppress("MaxLineLength") @Test - fun `build should return filled data and ignored AutofillIds when Login`() = runTest { + fun `build should skip null AutofillValues and return filled data and ignored AutofillIds when Login`() = runTest { // Setup val password = "Password" val username = "johnDoe" @@ -65,13 +66,17 @@ class FilledDataBuilderTest { val autofillViewPassword: AutofillView.Login.Password = mockk { every { buildFilledItemOrNull(password) } returns filledItemPassword } - val autofillViewUsername: AutofillView.Login.Username = mockk { + val autofillViewUsernameOne: AutofillView.Login.Username = mockk { every { buildFilledItemOrNull(username) } returns filledItemUsername } + val autofillViewUsernameTwo: AutofillView.Login.Username = mockk { + every { buildFilledItemOrNull(username) } returns null + } val autofillPartition = AutofillPartition.Login( views = listOf( autofillViewPassword, - autofillViewUsername, + autofillViewUsernameOne, + autofillViewUsernameTwo, ), ) val ignoreAutofillIds: List = mockk() @@ -121,7 +126,8 @@ class FilledDataBuilderTest { } verify(exactly = 1) { autofillViewPassword.buildFilledItemOrNull(password) - autofillViewUsername.buildFilledItemOrNull(username) + autofillViewUsernameOne.buildFilledItemOrNull(username) + autofillViewUsernameTwo.buildFilledItemOrNull(username) } } @@ -164,8 +170,9 @@ class FilledDataBuilderTest { assertEquals(expected, actual) } + @Suppress("MaxLineLength") @Test - fun `build should return filled data and ignored AutofillIds when Card`() = runTest { + fun `build should skip null AutofillValues and return filled data and ignored AutofillIds when Card`() = runTest { // Setup val code = "123" val expirationMonth = "January" @@ -194,15 +201,19 @@ class FilledDataBuilderTest { val autofillViewExpirationYear: AutofillView.Card.ExpirationYear = mockk { every { buildFilledItemOrNull(expirationYear) } returns filledItemExpirationYear } - val autofillViewNumber: AutofillView.Card.Number = mockk { + val autofillViewNumberOne: AutofillView.Card.Number = mockk { every { buildFilledItemOrNull(number) } returns filledItemNumber } + val autofillViewNumberTwo: AutofillView.Card.Number = mockk { + every { buildFilledItemOrNull(number) } returns null + } val autofillPartition = AutofillPartition.Card( views = listOf( autofillViewCode, autofillViewExpirationMonth, autofillViewExpirationYear, - autofillViewNumber, + autofillViewNumberOne, + autofillViewNumberTwo, ), ) val ignoreAutofillIds: List = mockk() @@ -248,7 +259,8 @@ class FilledDataBuilderTest { autofillViewCode.buildFilledItemOrNull(code) autofillViewExpirationMonth.buildFilledItemOrNull(expirationMonth) autofillViewExpirationYear.buildFilledItemOrNull(expirationYear) - autofillViewNumber.buildFilledItemOrNull(number) + autofillViewNumberOne.buildFilledItemOrNull(number) + autofillViewNumberTwo.buildFilledItemOrNull(number) } } diff --git a/app/src/test/java/com/x8bit/bitwarden/data/autofill/builder/SaveInfoBuilderTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/autofill/builder/SaveInfoBuilderTest.kt index 0bf3066a3c..c75dd576cf 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/autofill/builder/SaveInfoBuilderTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/autofill/builder/SaveInfoBuilderTest.kt @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.autofill.builder import android.os.Build import android.service.autofill.FillRequest import android.service.autofill.SaveInfo +import android.view.View import android.view.autofill.AutofillId import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo import com.x8bit.bitwarden.data.autofill.model.AutofillPartition @@ -30,12 +31,16 @@ class SaveInfoBuilderTest { private val autofillIdOptional: AutofillId = mockk() private val autofillViewDataOptional = AutofillView.Data( autofillId = autofillIdOptional, + autofillOptions = emptyList(), + autofillType = View.AUTOFILL_TYPE_TEXT, isFocused = true, textValue = null, ) private val autofillIdValid: AutofillId = mockk() private val autofillViewDataValid = AutofillView.Data( autofillId = autofillIdValid, + autofillOptions = emptyList(), + autofillType = View.AUTOFILL_TYPE_TEXT, isFocused = true, textValue = null, ) diff --git a/app/src/test/java/com/x8bit/bitwarden/data/autofill/parser/AutofillParserTests.kt b/app/src/test/java/com/x8bit/bitwarden/data/autofill/parser/AutofillParserTests.kt index eb80e515f5..8f8e8174de 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/autofill/parser/AutofillParserTests.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/autofill/parser/AutofillParserTests.kt @@ -187,6 +187,8 @@ class AutofillParserTests { val parentAutofillView: AutofillView.Card = AutofillView.Card.ExpirationMonth( data = AutofillView.Data( autofillId = parentAutofillId, + autofillOptions = emptyList(), + autofillType = AUTOFILL_TYPE, isFocused = true, textValue = null, ), @@ -247,6 +249,8 @@ class AutofillParserTests { val cardAutofillView: AutofillView.Card = AutofillView.Card.ExpirationMonth( data = AutofillView.Data( autofillId = cardAutofillId, + autofillOptions = emptyList(), + autofillType = AUTOFILL_TYPE, isFocused = true, textValue = null, ), @@ -255,6 +259,8 @@ class AutofillParserTests { val loginAutofillView: AutofillView.Login = AutofillView.Login.Username( data = AutofillView.Data( autofillId = loginAutofillId, + autofillOptions = emptyList(), + autofillType = AUTOFILL_TYPE, isFocused = false, textValue = null, ), @@ -302,6 +308,8 @@ class AutofillParserTests { val cardAutofillView: AutofillView.Card = AutofillView.Card.ExpirationMonth( data = AutofillView.Data( autofillId = cardAutofillId, + autofillOptions = emptyList(), + autofillType = AUTOFILL_TYPE, isFocused = false, textValue = null, ), @@ -310,6 +318,8 @@ class AutofillParserTests { val loginAutofillView: AutofillView.Login = AutofillView.Login.Username( data = AutofillView.Data( autofillId = loginAutofillId, + autofillOptions = emptyList(), + autofillType = AUTOFILL_TYPE, isFocused = true, textValue = null, ), @@ -357,6 +367,8 @@ class AutofillParserTests { val cardAutofillView: AutofillView.Card = AutofillView.Card.ExpirationMonth( data = AutofillView.Data( autofillId = cardAutofillId, + autofillOptions = emptyList(), + autofillType = AUTOFILL_TYPE, isFocused = true, textValue = null, ), @@ -365,6 +377,8 @@ class AutofillParserTests { val loginAutofillView: AutofillView.Login = AutofillView.Login.Username( data = AutofillView.Data( autofillId = loginAutofillId, + autofillOptions = emptyList(), + autofillType = AUTOFILL_TYPE, isFocused = true, textValue = null, ), @@ -413,6 +427,8 @@ class AutofillParserTests { val cardAutofillView: AutofillView.Card = AutofillView.Card.ExpirationMonth( data = AutofillView.Data( autofillId = cardAutofillId, + autofillOptions = emptyList(), + autofillType = AUTOFILL_TYPE, isFocused = true, textValue = null, ), @@ -421,6 +437,8 @@ class AutofillParserTests { val loginAutofillView: AutofillView.Login = AutofillView.Login.Username( data = AutofillView.Data( autofillId = loginAutofillId, + autofillOptions = emptyList(), + autofillType = AUTOFILL_TYPE, isFocused = true, textValue = null, ), @@ -469,6 +487,8 @@ class AutofillParserTests { val cardAutofillView: AutofillView.Card = AutofillView.Card.ExpirationMonth( data = AutofillView.Data( autofillId = cardAutofillId, + autofillOptions = emptyList(), + autofillType = AUTOFILL_TYPE, isFocused = true, textValue = null, ), @@ -477,6 +497,8 @@ class AutofillParserTests { val loginAutofillView: AutofillView.Login = AutofillView.Login.Username( data = AutofillView.Data( autofillId = loginAutofillId, + autofillOptions = emptyList(), + autofillType = AUTOFILL_TYPE, isFocused = true, textValue = null, ), @@ -524,6 +546,8 @@ class AutofillParserTests { val cardAutofillView: AutofillView.Card = AutofillView.Card.ExpirationMonth( data = AutofillView.Data( autofillId = cardAutofillId, + autofillOptions = emptyList(), + autofillType = AUTOFILL_TYPE, isFocused = true, textValue = null, ), @@ -532,6 +556,8 @@ class AutofillParserTests { val loginAutofillView: AutofillView.Login = AutofillView.Login.Username( data = AutofillView.Data( autofillId = loginAutofillId, + autofillOptions = emptyList(), + autofillType = AUTOFILL_TYPE, isFocused = true, textValue = null, ), @@ -589,6 +615,7 @@ private val BLOCK_LISTED_URIS: List = listOf( "androidapp://com.x8bit.bitwarden", "androidapp://com.oneplus.applocker", ) +private const val AUTOFILL_TYPE: Int = View.AUTOFILL_TYPE_TEXT private const val ID_PACKAGE: String = "com.x8bit.bitwarden" private const val MAX_INLINE_SUGGESTION_COUNT: Int = 42 private const val PACKAGE_NAME: String = "com.google" diff --git a/app/src/test/java/com/x8bit/bitwarden/data/autofill/util/AutofillPartitionExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/autofill/util/AutofillPartitionExtensionsTest.kt index c81e6072ca..181d392763 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/autofill/util/AutofillPartitionExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/autofill/util/AutofillPartitionExtensionsTest.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.data.autofill.util +import android.view.View import com.x8bit.bitwarden.data.autofill.model.AutofillPartition import com.x8bit.bitwarden.data.autofill.model.AutofillView import io.mockk.mockk @@ -10,11 +11,15 @@ import org.junit.jupiter.api.Test class AutofillPartitionExtensionsTest { private val autofillDataEmptyText: AutofillView.Data = AutofillView.Data( autofillId = mockk(), + autofillOptions = emptyList(), + autofillType = View.AUTOFILL_TYPE_TEXT, isFocused = false, textValue = null, ) private val autofillDataValidText: AutofillView.Data = AutofillView.Data( autofillId = mockk(), + autofillOptions = emptyList(), + autofillType = View.AUTOFILL_TYPE_TEXT, isFocused = false, textValue = TEXT_VALUE, ) diff --git a/app/src/test/java/com/x8bit/bitwarden/data/autofill/util/AutofillViewExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/autofill/util/AutofillViewExtensionsTest.kt index e28ea0c768..cfb2cad709 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/autofill/util/AutofillViewExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/autofill/util/AutofillViewExtensionsTest.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.data.autofill.util +import android.view.View import android.view.autofill.AutofillId import android.view.autofill.AutofillValue import com.x8bit.bitwarden.data.autofill.model.AutofillView @@ -11,6 +12,7 @@ import io.mockk.unmockkStatic import io.mockk.verify import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -19,6 +21,8 @@ class AutofillViewExtensionsTest { private val autofillValue: AutofillValue = mockk() private val autofillViewData = AutofillView.Data( autofillId = autofillId, + autofillOptions = emptyList(), + autofillType = View.AUTOFILL_TYPE_TEXT, isFocused = false, textValue = null, ) @@ -34,10 +38,202 @@ class AutofillViewExtensionsTest { } @Test - fun `buildFilledItem returns AutofillValue`() { + fun `buildFilledItemOrNull should return date value when date type`() { // Setup - val value = "2002421451023587L" - val autofillView = AutofillView.Card.Number( + val rawValue = 2002421451023587L + val value = rawValue.toString() + val autofillViewData = autofillViewData.copy( + autofillType = View.AUTOFILL_TYPE_DATE, + ) + val autofillView = AutofillView.Card.ExpirationYear( + data = autofillViewData, + ) + val expected = FilledItem( + autofillId = autofillId, + value = autofillValue, + ) + every { AutofillValue.forDate(rawValue) } returns autofillValue + + // Test + val actual = autofillView.buildFilledItemOrNull( + value = value, + ) + + // Verify + assertEquals(expected, actual) + verify(exactly = 1) { + AutofillValue.forDate(rawValue) + } + } + + @Test + fun `buildFilledItemOrNull should return null when date type but non-numerical value`() { + // Setup + val value = "January 1, 2024" + val autofillViewData = autofillViewData.copy( + autofillType = View.AUTOFILL_TYPE_DATE, + ) + val autofillView = AutofillView.Card.ExpirationYear( + data = autofillViewData, + ) + + // Test + val actual = autofillView.buildFilledItemOrNull( + value = value, + ) + + // Verify + assertNull(actual) + } + + @Suppress("MaxLineLength") + @Test + fun `buildFilledItemOrNull should return null when is expiration month value, list type, and non-numerical value`() { + // Setup + val value = "January" + val autofillViewData = autofillViewData.copy( + autofillType = View.AUTOFILL_TYPE_LIST, + ) + val autofillView = AutofillView.Card.ExpirationMonth( + data = autofillViewData, + monthValue = null, + ) + + // Test + val actual = autofillView.buildFilledItemOrNull( + value = value, + ) + + // Verify + assertNull(actual) + } + + @Suppress("MaxLineLength") + @Test + fun `buildFilledItemOrNull should return list value when is expiration list value, list type, and 13 options`() { + // Setup + val rawValue = 1 + val value = rawValue.toString() + val autofillViewData = autofillViewData.copy( + autofillType = View.AUTOFILL_TYPE_LIST, + autofillOptions = List(13) { it.toString() }, + ) + val autofillView = AutofillView.Card.ExpirationMonth( + data = autofillViewData, + monthValue = null, + ) + val expected = FilledItem( + autofillId = autofillId, + value = autofillValue, + ) + every { AutofillValue.forList(rawValue) } returns autofillValue + + // Test + val actual = autofillView.buildFilledItemOrNull( + value = value, + ) + + // Verify + assertEquals(expected, actual) + verify(exactly = 1) { + AutofillValue.forList(rawValue) + } + } + + @Suppress("MaxLineLength") + @Test + fun `buildFilledItemOrNull should return list value minus one when is expiration list value, list type, and options size is greater than month value`() { + // Setup + val rawValue = 1 + val value = (rawValue).toString() + val expectedListValue = rawValue - 1 + val autofillViewData = autofillViewData.copy( + autofillType = View.AUTOFILL_TYPE_LIST, + autofillOptions = List(12) { it.toString() }, + ) + val autofillView = AutofillView.Card.ExpirationMonth( + data = autofillViewData, + monthValue = null, + ) + val expected = FilledItem( + autofillId = autofillId, + value = autofillValue, + ) + every { AutofillValue.forList(expectedListValue) } returns autofillValue + + // Test + val actual = autofillView.buildFilledItemOrNull( + value = value, + ) + + // Verify + assertEquals(expected, actual) + verify(exactly = 1) { + AutofillValue.forList(expectedListValue) + } + } + + @Suppress("MaxLineLength") + @Test + fun `buildFilledItemOrNull should return index list value when list type, and value is found in options`() { + // Setup + val expectedValue = 1 + val value = "2025" + val autofillViewData = autofillViewData.copy( + autofillType = View.AUTOFILL_TYPE_LIST, + autofillOptions = listOf("2024", value, "2026"), + ) + val autofillView = AutofillView.Card.ExpirationYear( + data = autofillViewData, + ) + val expected = FilledItem( + autofillId = autofillId, + value = autofillValue, + ) + every { AutofillValue.forList(expectedValue) } returns autofillValue + + // Test + val actual = autofillView.buildFilledItemOrNull( + value = value, + ) + + // Verify + assertEquals(expected, actual) + verify(exactly = 1) { + AutofillValue.forList(expectedValue) + } + } + + @Suppress("MaxLineLength") + @Test + fun `buildFilledItemOrNull should return null when list type, and value is not found in options`() { + // Setup + val value = "2027" + val autofillViewData = autofillViewData.copy( + autofillType = View.AUTOFILL_TYPE_LIST, + autofillOptions = listOf("2024", "2025", "2026"), + ) + val autofillView = AutofillView.Card.ExpirationYear( + data = autofillViewData, + ) + + // Test + val actual = autofillView.buildFilledItemOrNull( + value = value, + ) + + // Verify + assertNull(actual) + } + + @Test + fun `buildFilledItemOrNull should return text value when text type`() { + // Setup + val value = "Jimmy" + val autofillViewData = autofillViewData.copy( + autofillType = View.AUTOFILL_TYPE_TEXT, + ) + val autofillView = AutofillView.Login.Username( data = autofillViewData, ) val expected = FilledItem( @@ -57,4 +253,73 @@ class AutofillViewExtensionsTest { AutofillValue.forText(value) } } + + @Test + fun `buildFilledItemOrNull should return toggle value when toggle type and value is boolean`() { + // Setup + val value = "false" + val expectedValue = value.toBooleanStrict() + val autofillViewData = autofillViewData.copy( + autofillType = View.AUTOFILL_TYPE_TOGGLE, + ) + val autofillView = AutofillView.Login.Username( + data = autofillViewData, + ) + val expected = FilledItem( + autofillId = autofillId, + value = autofillValue, + ) + every { AutofillValue.forToggle(expectedValue) } returns autofillValue + + // Test + val actual = autofillView.buildFilledItemOrNull( + value = value, + ) + + // Verify + assertEquals(expected, actual) + verify(exactly = 1) { + AutofillValue.forToggle(expectedValue) + } + } + + @Test + fun `buildFilledItemOrNull should return null when toggle type and value not boolean`() { + // Setup + val value = "Jimmy" + val autofillViewData = autofillViewData.copy( + autofillType = View.AUTOFILL_TYPE_TOGGLE, + ) + val autofillView = AutofillView.Login.Username( + data = autofillViewData, + ) + + // Test + val actual = autofillView.buildFilledItemOrNull( + value = value, + ) + + // Verify + assertNull(actual) + } + + @Test + fun `buildFilledItemOrNull should return null when not recognized type`() { + // Setup + val value = "Jimmy" + val autofillViewData = autofillViewData.copy( + autofillType = 100, + ) + val autofillView = AutofillView.Login.Username( + data = autofillViewData, + ) + + // Test + val actual = autofillView.buildFilledItemOrNull( + value = value, + ) + + // Verify + assertNull(actual) + } } diff --git a/app/src/test/java/com/x8bit/bitwarden/data/autofill/util/FilledDataExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/autofill/util/FilledDataExtensionsTest.kt index 214129a5c8..7d8ad04ca6 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/autofill/util/FilledDataExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/autofill/util/FilledDataExtensionsTest.kt @@ -7,6 +7,7 @@ import android.content.res.Resources import android.service.autofill.Dataset import android.service.autofill.InlinePresentation import android.service.autofill.Presentations +import android.view.View import android.view.autofill.AutofillId import android.view.autofill.AutofillValue import android.widget.RemoteViews @@ -69,6 +70,8 @@ class FilledDataExtensionsTest { AutofillView.Login.Username( data = AutofillView.Data( autofillId = autofillId, + autofillOptions = emptyList(), + autofillType = View.AUTOFILL_TYPE_TEXT, isFocused = true, textValue = null, ), diff --git a/app/src/test/java/com/x8bit/bitwarden/data/autofill/util/ViewNodeExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/autofill/util/ViewNodeExtensionsTest.kt index 9afde68e20..45b97b10a2 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/autofill/util/ViewNodeExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/autofill/util/ViewNodeExtensionsTest.kt @@ -23,18 +23,17 @@ class ViewNodeExtensionsTest { private val expectedIsFocused = true private val autofillViewData = AutofillView.Data( autofillId = expectedAutofillId, + autofillOptions = AUTOFILL_OPTIONS_LIST, + autofillType = AUTOFILL_TYPE, isFocused = expectedIsFocused, textValue = TEXT_VALUE, ) - private val testAutofillOptions = arrayOf( - "option one", - "option two", - ) private val testAutofillValue: AutofillValue = mockk() private val viewNode: AssistStructure.ViewNode = mockk { every { this@mockk.autofillId } returns expectedAutofillId - every { this@mockk.autofillOptions } returns testAutofillOptions + 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 @@ -51,7 +50,7 @@ class ViewNodeExtensionsTest { mockkStatic(AutofillValue::extractTextValue) every { testAutofillValue.extractMonthValue( - autofillOptions = testAutofillOptions.toList(), + autofillOptions = AUTOFILL_OPTIONS_LIST, ) } returns MONTH_VALUE every { testAutofillValue.extractTextValue() } returns TEXT_VALUE @@ -84,6 +83,34 @@ class ViewNodeExtensionsTest { assertEquals(expected, actual) } + @Suppress("MaxLineLength") + @Test + fun `toAutofillView should return AutofillView Card ExpirationMonth with empty options when hint matches and options are null`() { + // Setup + val autofillViewData = autofillViewData.copy( + autofillOptions = emptyList(), + ) + val autofillHint = View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH + val monthValue = "11" + val expected = AutofillView.Card.ExpirationMonth( + data = autofillViewData, + monthValue = monthValue, + ) + every { viewNode.autofillHints } returns arrayOf(autofillHint) + every { viewNode.autofillOptions } returns null + every { + testAutofillValue.extractMonthValue( + autofillOptions = emptyList(), + ) + } returns monthValue + + // Test + val actual = viewNode.toAutofillView() + + // Verify + assertEquals(expected, actual) + } + @Test fun `toAutofillView should return AutofillView Card ExpirationYear when hint matches`() { // Setup @@ -478,6 +505,17 @@ class ViewNodeExtensionsTest { } } +private const val AUTOFILL_OPTION_ONE: String = "AUTOFILL_OPTION_ONE" +private const val AUTOFILL_OPTION_TWO: String = "AUTOFILL_OPTION_TWO" +private val AUTOFILL_OPTIONS_ARRAY: Array = arrayOf( + AUTOFILL_OPTION_ONE, + AUTOFILL_OPTION_TWO, +) +private val AUTOFILL_OPTIONS_LIST: List = listOf( + AUTOFILL_OPTION_ONE, + AUTOFILL_OPTION_TWO, +) +private const val AUTOFILL_TYPE: Int = View.AUTOFILL_TYPE_LIST private const val ANDROID_EDIT_TEXT_CLASS_NAME: String = "android.widget.EditText" private val IGNORED_RAW_HINTS: List = listOf( "search", diff --git a/app/src/test/java/com/x8bit/bitwarden/data/autofill/util/ViewNodeTraversalDataExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/autofill/util/ViewNodeTraversalDataExtensionsTest.kt index 23d4c8a972..34446a6e91 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/autofill/util/ViewNodeTraversalDataExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/autofill/util/ViewNodeTraversalDataExtensionsTest.kt @@ -1,7 +1,6 @@ package com.x8bit.bitwarden.data.autofill.util import android.app.assist.AssistStructure -import com.x8bit.bitwarden.data.autofill.model.AutofillView import com.x8bit.bitwarden.data.autofill.model.ViewNodeTraversalData import io.mockk.every import io.mockk.mockk @@ -15,11 +14,6 @@ class ViewNodeTraversalDataExtensionsTest { every { this@mockk.windowNodeCount } returns 1 every { this@mockk.getWindowNodeAt(0) } returns windowNode } - private val autofillViewData = AutofillView.Data( - autofillId = mockk(), - isFocused = false, - textValue = null, - ) @Test fun `buildUriOrNull should return website URI when present`() {