Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -177,12 +177,15 @@ class AutofillParserImpl(
uri = uri,
focusedView = focusedView,
urlBarWebsite = urlBarWebsite,
isIdentityAutofillEnabled = isIdentityAutofillEnabled,
)
} else {
autofillViews
}

val effectiveFocusedView = effectiveViews.firstFocusedOrNull()
val effectiveFocusedView = effectiveViews
.filterNot { it is AutofillView.Identity }
.firstFocusedOrNull()
?: return AutofillRequest.Unfillable

// Choose the first focused partition of data for fulfillment.
Expand Down Expand Up @@ -248,6 +251,7 @@ class AutofillParserImpl(
uri: String?,
focusedView: AutofillView,
urlBarWebsite: String?,
isIdentityAutofillEnabled: Boolean,
): List<AutofillView> {
val hostRules = uri
?.takeUnless { it.startsWith("androidapp://") }
Expand Down Expand Up @@ -275,6 +279,7 @@ class AutofillParserImpl(
val fillAssistViews = assistStructure.buildFillAssistViews(
hostRules = hostRules,
urlBarWebsite = urlBarWebsite,
isIdentityAutofillEnabled = isIdentityAutofillEnabled,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ IMPORTANT: With the flag on, a focused Identity view from fill-assist can force the whole request to Unfillable, unlike the heuristic path.

Details and fix

toEffectiveViews returns fillAssistViews unfiltered, and buildFillAssistViews is given all of the host's rules (including identity-category ones), so an identity-keyed node is now classified even when coversCurrentPartition matched on a login/card rule. parseInternal then does:

val effectiveFocusedView = effectiveViews.firstFocusedOrNull() ?: return AutofillRequest.Unfillable
val partition = when (effectiveFocusedView) {
    ...
    is AutofillView.Identity -> return AutofillRequest.Unfillable

Traceable path (flag on, fill-assist on, host has account-login rules plus identity field keys):

  1. User focuses a "Full name" field. Heuristics classify it Identity (or Unused), so it is not in autofillViews; focusedView falls back to autofillViews.firstOrNull() β€” an unfocused Login.Username.
  2. coversCurrentPartition is true (Login rule present), so fill-assist runs.
  3. fillAssistViews now contains Identity.PersonNameFull(isFocused = true) alongside Login.Username/Login.Password.
  4. firstFocusedOrNull() returns the Identity view β†’ Unfillable.

Before this change the identity key mapped to null, the node was omitted, and firstFocusedOrNull() fell back to the first Login view β†’ a fillable Login partition. So turning the flag on removes login suggestions on these hosts.

This is the exact case the heuristic path guards against at line 141-144 (it !is AutofillView.Unused && it !is AutofillView.Identity), covered by parse should choose AutofillPartition Login when an Identity view is focused but a Login view is fillable elsewhere. The fill-assist path has no equivalent.

Mirroring that filter until Phase D would keep the two paths aligned, e.g. in parseInternal:

val effectiveFocusedView = effectiveViews
    .filterNot { it is AutofillView.Identity }
    .firstFocusedOrNull()
    ?: return AutofillRequest.Unfillable

A parser-level test with a focused identity-keyed fill-assist node would lock this in.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@claude fixed

@claude claude Bot Sep 2, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude finished @aj-rosado's task in 1m 18s β€”β€” View job


Fix verified βœ…

  • Read the new commit since my review (e75acee "Filter out autofill identity")
  • Check the focused-Identity β†’ Unfillable path is closed
  • Check test coverage locks the behavior in

The fix is correct and complete. AutofillParserImpl.kt:186-189 now filters Identity out before resolving the effective focused view:

val effectiveFocusedView = effectiveViews
    .filterNot { it is AutofillView.Identity }
    .firstFocusedOrNull()
    ?: return AutofillRequest.Unfillable

Tracing the path I reported: with the flag on, the focused identity-keyed node is now skipped, firstFocusedOrNull()'s ?: firstOrNull() fallback (AutofillParserImpl.kt:337-338) lands on the fill-assist Login.Username, and the request stays a fillable Login partition. The two paths are aligned β€” this mirrors the heuristic filter at AutofillParserImpl.kt:141-144.

No regressions I can see from the filter itself:

  • Fill-assist off: autofillViews already excludes Identity, so filterNot is a no-op.
  • Only identity views present: filtered list is empty β†’ Unfillable, same as before.

The new test parse should choose AutofillPartition Login when fill-assist resolves the focused view to Identity but a Login view is fillable elsewhere (AutofillParserTests.kt:1789+) exercises exactly the scenario β€” focused Unused node reclassified to Identity.PersonNameFull by a rule that also carries a username key β€” and asserts the Login partition rather than Unfillable. It would fail without the filterNot. Fixtures all check out against the existing test setup.


One nit (non-blocking)

AutofillParserImpl.kt:205-208 β€” the is AutofillView.Identity -> return AutofillRequest.Unfillable branch is now unreachable, since Identity can no longer reach effectiveFocusedView. The branch still has to exist for when exhaustiveness, but the comment now reads as if it's a live path. Consider matching the tone of the adjacent Unused branch:

is AutofillView.Identity -> {
    // Unreachable: Identity views are filtered out of the focused-view lookup above.
    // Identity partition construction lands in Phase D.
    return AutofillRequest.Unfillable
}

Fix this β†’


⚠️ I was not able to run ./gradlew :app:testStandardDebugUnitTest in this job β€” the Gradle command was blocked by the tool permission gate β€” so my verification is by code reading only. CI test results are authoritative.
β€’ PM-41293/identity-autofill-fill-assist-field-mapping

)
// Fill-assist is authoritative for a partition its rules cover (guarded by
// coversCurrentPartition above), so its views are used even when empty: for Login/Card
Expand Down Expand Up @@ -523,44 +528,20 @@ private fun AutofillView.updateWebsiteIfNecessary(website: String?): AutofillVie
is AutofillView.Login.Email -> this.copy(data = this.data.copy(website = site))
is AutofillView.Login.Password -> this.copy(data = this.data.copy(website = site))
is AutofillView.Login.Username -> this.copy(data = this.data.copy(website = site))
is AutofillView.Identity.AddressCountry -> {
this.copy(data = this.data.copy(website = site))
}

is AutofillView.Identity.AddressLocality -> {
this.copy(data = this.data.copy(website = site))
}

is AutofillView.Identity.AddressRegion -> {
this.copy(data = this.data.copy(website = site))
}

is AutofillView.Identity.AddressStreet -> {
this.copy(data = this.data.copy(website = site))
}

is AutofillView.Identity.AddressCountry -> this.copy(data = this.data.copy(website = site))
is AutofillView.Identity.AddressLocality -> this.copy(data = this.data.copy(website = site))
is AutofillView.Identity.AddressRegion -> this.copy(data = this.data.copy(website = site))
is AutofillView.Identity.AddressStreet -> this.copy(data = this.data.copy(website = site))
is AutofillView.Identity.Company -> this.copy(data = this.data.copy(website = site))
is AutofillView.Identity.Email -> this.copy(data = this.data.copy(website = site))
is AutofillView.Identity.LicenseNumber -> {
this.copy(data = this.data.copy(website = site))
}

is AutofillView.Identity.PassportNumber -> {
this.copy(data = this.data.copy(website = site))
}

is AutofillView.Identity.LicenseNumber -> this.copy(data = this.data.copy(website = site))
is AutofillView.Identity.PassportNumber -> this.copy(data = this.data.copy(website = site))
is AutofillView.Identity.PersonNameFamily -> {
this.copy(data = this.data.copy(website = site))
}

is AutofillView.Identity.PersonNameFull -> {
this.copy(data = this.data.copy(website = site))
}

is AutofillView.Identity.PersonNameGiven -> {
this.copy(data = this.data.copy(website = site))
}

is AutofillView.Identity.PersonNameFull -> this.copy(data = this.data.copy(website = site))
is AutofillView.Identity.PersonNameGiven -> this.copy(data = this.data.copy(website = site))
is AutofillView.Identity.PersonNameMiddle -> {
this.copy(data = this.data.copy(website = site))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,49 @@ private const val FIELD_KEY_CARD_EXPIRATION_MONTH = "cardExpirationMonth"
private const val FIELD_KEY_CARD_EXPIRATION_YEAR = "cardExpirationYear"
private const val FIELD_KEY_CARD_CVV = "cardCvv"
private const val FIELD_KEY_CARD_TYPE = "cardType"
private const val FIELD_KEY_PERSON_NAME_FULL = "fullName"
private const val FIELD_KEY_PERSON_NAME_PREFIX = "honorificPrefix"
private const val FIELD_KEY_PERSON_NAME_GIVEN = "firstName"
private const val FIELD_KEY_PERSON_NAME_MIDDLE = "middleName"
private const val FIELD_KEY_PERSON_NAME_FAMILY = "lastName"
private const val FIELD_KEY_ADDRESS_STREET = "addressLine1"
private const val FIELD_KEY_ADDRESS_LOCALITY = "addressLevel2"
private const val FIELD_KEY_ADDRESS_REGION = "addressLevel1"
private const val FIELD_KEY_ADDRESS_COUNTRY = "country"
private const val FIELD_KEY_POSTAL_CODE = "postalCode"
private const val FIELD_KEY_COMPANY = "organization"
private const val FIELD_KEY_SSN = "ssn"
private const val FIELD_KEY_PASSPORT_NUMBER = "passportNumber"
private const val FIELD_KEY_LICENSE_NUMBER = "licenseNumber"

/**
* Traverses the [AssistStructure] and returns a list of [AutofillView]s classified by the
* provided [hostRules]. Only view nodes whose [android.view.ViewStructure.HtmlInfo] attributes
* match a [FillAssistRules.SelectorClause] are included; unmatched nodes are omitted (no
* heuristic fallback).
* heuristic fallback). All identity classification is gated behind [isIdentityAutofillEnabled].
*/
internal fun AssistStructure.buildFillAssistViews(
hostRules: List<FillAssistRules.HostRule>,
urlBarWebsite: String?,
isIdentityAutofillEnabled: Boolean,
): List<AutofillView> =
(0 until windowNodeCount)
.mapNotNull { getWindowNodeAt(it).rootViewNode }
.flatMap { it.traverseForFillAssist(hostRules = hostRules, parentWebsite = urlBarWebsite) }
.flatMap {
it.traverseForFillAssist(
hostRules = hostRules,
parentWebsite = urlBarWebsite,
isIdentityAutofillEnabled = isIdentityAutofillEnabled,
)
}

private fun AssistStructure.ViewNode.traverseForFillAssist(
hostRules: List<FillAssistRules.HostRule>,
parentWebsite: String?,
isIdentityAutofillEnabled: Boolean,
): List<AutofillView> {
val website = this.website ?: parentWebsite
val ownView = autofillId?.let { id ->
val ownViews = autofillId?.let { id ->
hostRules
.flatMap { it.fields.entries }
.filter { (_, alternatives) ->
Expand All @@ -48,45 +70,108 @@ private fun AssistStructure.ViewNode.traverseForFillAssist(
?.let { matchingEntries ->
val data = toAutofillViewData(autofillId = id, website = website)
val candidateViews = matchingEntries.mapNotNull { (key, _) ->
key.toAutofillViewForFieldKey(data = data)
key.toAutofillViewForFieldKey(
data = data,
isIdentityAutofillEnabled = isIdentityAutofillEnabled,
)?.let { key to it }
}
// Prefer Username: it has no format gate, while Login.Email rejects non-email
// values via isValidEmail(). Prefer any non-Identity view next, since Identity
// partitions are unbuilt and Login/Card must remain authoritative while the
// flag is on.
val view = candidateViews
.firstOrNull { (_, view) -> view is AutofillView.Login.Username }
?.second
?: candidateViews.firstOrNull { (_, view) -> view !is AutofillView.Identity }
?.second
?: candidateViews.firstOrNull()?.second
?: return@let null
Comment on lines +82 to +88

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ IMPORTANT: The tie-break can now let an Identity view outrank a Login/Card view for the same node, dropping it from the partition when the flag is on.

Details and fix

candidateViews holds one entry per matched field key, in hostRules.flatMap { it.fields.entries } order. The fallback is candidateViews.firstOrNull(), which has no notion of Identity being the weakest classification.

Traceable path (flag on):

  1. A node matches both an identity key and a non-username/phone login or card key β€” e.g. personNameFull and email β€” because two of the host's clauses select it (a loose clause such as input[type=text] is enough).
  2. No Login.Username candidate exists, so candidateViews.firstOrNull() wins.
  3. If the identity key is declared first, view is Identity.PersonNameFull.
  4. parseInternal builds the partition with effectiveViews.filterIsInstance<AutofillView.Login>(), so that node is silently absent β€” the email field gets no suggestion.

With the flag off, toIdentityViewForFieldKey returns null, the identity key is dropped from candidateViews, and the same node classifies as Login.Email. So flipping the flag removes a login/card field from the partition, the same class of flag-on regression as the focused-Identity case fixed in AutofillParserImpl.

Login.Username is already preferred for a reason; extending that ordering to deprioritize Identity keeps Login/Card authoritative while Identity partitions are unbuilt:

val view = candidateViews
    .firstOrNull { (_, view) -> view is AutofillView.Login.Username }
    ?.second
    ?: candidateViews.firstOrNull { (_, view) -> view !is AutofillView.Identity }?.second
    ?: candidateViews.firstOrNull()?.second
    ?: return@let null

A test with a node matched under both an identity key (listed first) and email would lock this in.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@claude fixed

@claude claude Bot Sep 2, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude finished @aj-rosado's task in 1m 6s β€”β€” View job


Fix verified βœ…

  • Read commit d1c1bb4 "Prioritize login/card classifications over identity"
  • Verify the Identity-outranks-Login/Card path is closed
  • Check test coverage locks it in

The tie-break in FillAssistViewNodeExtensions.kt:84-90 now demotes Identity below every other classification:

val view = candidateViews
    .firstOrNull { (_, view) -> view is AutofillView.Login.Username }
    ?.second
    ?: candidateViews.firstOrNull { (_, view) -> view !is AutofillView.Identity }
        ?.second
    ?: candidateViews.firstOrNull()?.second
    ?: return@let null

Tracing the path I reported β€” node matched under personNameFull (declared first) and email: no Login.Username candidate, so the new middle clause picks Login.Email instead of Identity.PersonNameFull. isLoginIdentifierView is then true, matchedKeys contains email, so Identity.Email is appended β€” the node lands in the Login partition and keeps its identity candidate. That's the behavior the new test asserts.

No regressions I can find:

  • Flag off β€” toIdentityViewForFieldKey is skipped, so candidateViews holds no Identity entries and the new filterNot-style clause is a no-op.
  • Only identity keys match β€” both preferential clauses miss, candidateViews.firstOrNull() still returns the Identity view, so plain identity-only nodes classify exactly as before.
  • Relative order among non-Identity candidates β€” firstOrNull on the filtered view preserves declaration order, so existing Login/Card tie-breaks are untouched.
  • Card keys β€” a node matched under an identity key plus cardNumber now resolves to Card.Number, which is the intended direction.

FillAssistViewNodeExtensionsTest.kt:458+ (buildFillAssistViews should prefer Login Email over Identity PersonNameFull when identity key is listed first and matches the same node) uses linkedMapOf so the identity key genuinely comes first, and createHtmlInfo() matches any clause so both keys hit the same node. It would fail on the pre-fix code (candidateViews.firstOrNull() β†’ Identity.PersonNameFull). Fixture helpers all line up with the existing suite.


One nit (non-blocking, Phase D)

An identity classification can now be silently dropped when the same node also matches a login/card key that isn't in the dual-classify set. E.g. a node matched under phoneFull + email resolves to Login.Email + Identity.Email, and the Identity.PhoneFull candidate disappears β€” the dual-classify block only checks the "email" and "phone" keys (FillAssistViewNodeExtensions.kt:100-105), not "phoneFull". No user-visible effect today since identity partitions aren't built, but worth revisiting when Phase D starts consuming these views.


⚠️ I could not run ./gradlew :app:testStandardDebugUnitTest in this job β€” the Gradle command was blocked by the tool permission gate β€” so this verification is by code reading only. CI results are authoritative.
β€’ PM-41293/identity-autofill-fill-assist-field-mapping


// Dual-classify off the full matched-key set, not just the winning key, so a
// field matched under both "email" and "phone" gets both Identity views.
val isLoginIdentifierView = view is AutofillView.Login.Username ||
view is AutofillView.Login.Email
buildList<AutofillView> {
add(view)
if (isIdentityAutofillEnabled && isLoginIdentifierView) {
val matchedKeys = candidateViews.mapTo(mutableSetOf()) { it.first }
if (FIELD_KEY_EMAIL in matchedKeys) {
add(AutofillView.Identity.Email(data = view.data))
}
if (FIELD_KEY_PHONE in matchedKeys) {
add(AutofillView.Identity.PhoneFull(data = view.data))
}
}
}
// A single field can legitimately match both the "email" and "phone"/"username"
// keys (e.g. a combined phone-or-email login field). Login.Username has no format
// gate and fills any stored value, while Login.Email rejects non-email values via
// isValidEmail(). Preferring Username when both match avoids rejecting a phone
// number credential on a field that would have accepted it.
candidateViews.firstOrNull { it is AutofillView.Login.Username }
?: candidateViews.firstOrNull()
}
}
}.orEmpty()
val childViews = (0 until childCount)
.flatMap { index ->
getChildAt(index).traverseForFillAssist(
hostRules = hostRules,
parentWebsite = website,
isIdentityAutofillEnabled = isIdentityAutofillEnabled,
)
}
return listOfNotNull(ownView) + childViews
return ownViews + childViews
}

private fun String.toAutofillViewForFieldKey(data: AutofillView.Data): AutofillView? = when (this) {
FIELD_KEY_USERNAME, FIELD_KEY_PHONE -> AutofillView.Login.Username(data = data)
FIELD_KEY_EMAIL -> AutofillView.Login.Email(data = data)
FIELD_KEY_PASSWORD, FIELD_KEY_NEW_PASSWORD -> AutofillView.Login.Password(data = data)
FIELD_KEY_CARD_NUMBER -> AutofillView.Card.Number(data = data)
FIELD_KEY_CARDHOLDER_NAME -> AutofillView.Card.CardholderName(data = data)
FIELD_KEY_CARD_EXPIRATION_DATE -> AutofillView.Card.ExpirationDate(data = data)
FIELD_KEY_CARD_EXPIRATION_MONTH -> AutofillView.Card.ExpirationMonth(
data = data,
monthValue = null,
)
/**
* Maps this field key to the [AutofillView] it represents, or null if this key is unrecognized.
* Delegates to a type-specific mapper ([toLoginViewForFieldKey], [toCardViewForFieldKey],
* [toIdentityViewForFieldKey]) grouped by the category the field key belongs to.
*/
private fun String.toAutofillViewForFieldKey(
data: AutofillView.Data,
isIdentityAutofillEnabled: Boolean,
): AutofillView? =
toLoginViewForFieldKey(data = data)
?: toCardViewForFieldKey(data = data)
?: if (isIdentityAutofillEnabled) toIdentityViewForFieldKey(data = data) else null

FIELD_KEY_CARD_EXPIRATION_YEAR -> AutofillView.Card.ExpirationYear(
data = data,
yearValue = null,
)
private fun String.toLoginViewForFieldKey(data: AutofillView.Data): AutofillView.Login? =
when (this) {
FIELD_KEY_USERNAME, FIELD_KEY_PHONE -> AutofillView.Login.Username(data = data)
FIELD_KEY_EMAIL -> AutofillView.Login.Email(data = data)
FIELD_KEY_PASSWORD, FIELD_KEY_NEW_PASSWORD -> AutofillView.Login.Password(data = data)
else -> null
}

FIELD_KEY_CARD_CVV -> AutofillView.Card.SecurityCode(data = data)
FIELD_KEY_CARD_TYPE -> AutofillView.Card.Brand(data = data, brandValue = null)
else -> null
}
private fun String.toCardViewForFieldKey(data: AutofillView.Data): AutofillView.Card? =
when (this) {
FIELD_KEY_CARD_NUMBER -> AutofillView.Card.Number(data = data)
FIELD_KEY_CARDHOLDER_NAME -> AutofillView.Card.CardholderName(data = data)
FIELD_KEY_CARD_EXPIRATION_DATE -> AutofillView.Card.ExpirationDate(data = data)
FIELD_KEY_CARD_EXPIRATION_MONTH -> AutofillView.Card.ExpirationMonth(
data = data,
monthValue = null,
)

FIELD_KEY_CARD_EXPIRATION_YEAR -> AutofillView.Card.ExpirationYear(
data = data,
yearValue = null,
)

FIELD_KEY_CARD_CVV -> AutofillView.Card.SecurityCode(data = data)
FIELD_KEY_CARD_TYPE -> AutofillView.Card.Brand(data = data, brandValue = null)
else -> null
}

private fun String.toIdentityViewForFieldKey(data: AutofillView.Data): AutofillView.Identity? =
when (this) {
FIELD_KEY_PERSON_NAME_FULL -> AutofillView.Identity.PersonNameFull(data = data)
FIELD_KEY_PERSON_NAME_PREFIX -> AutofillView.Identity.PersonNamePrefix(data = data)
FIELD_KEY_PERSON_NAME_GIVEN -> AutofillView.Identity.PersonNameGiven(data = data)
FIELD_KEY_PERSON_NAME_MIDDLE -> AutofillView.Identity.PersonNameMiddle(data = data)
FIELD_KEY_PERSON_NAME_FAMILY -> AutofillView.Identity.PersonNameFamily(data = data)
FIELD_KEY_ADDRESS_STREET -> AutofillView.Identity.AddressStreet(data = data)
FIELD_KEY_ADDRESS_LOCALITY -> AutofillView.Identity.AddressLocality(data = data)
FIELD_KEY_ADDRESS_REGION -> AutofillView.Identity.AddressRegion(data = data)
FIELD_KEY_ADDRESS_COUNTRY -> AutofillView.Identity.AddressCountry(data = data)
FIELD_KEY_POSTAL_CODE -> AutofillView.Identity.PostalCode(data = data)
FIELD_KEY_COMPANY -> AutofillView.Identity.Company(data = data)
FIELD_KEY_SSN -> AutofillView.Identity.Ssn(data = data)
FIELD_KEY_PASSPORT_NUMBER -> AutofillView.Identity.PassportNumber(data = data)
FIELD_KEY_LICENSE_NUMBER -> AutofillView.Identity.LicenseNumber(data = data)
else -> null
}
Original file line number Diff line number Diff line change
Expand Up @@ -270,5 +270,4 @@ private val SUPPORTED_HTML_ATTRIBUTE_HINTS: List<String> = listOf(
"type",
"hint",
"autofill",
"autocomplete",
)
Original file line number Diff line number Diff line change
Expand Up @@ -1786,6 +1786,114 @@ class AutofillParserTests {
assertEquals(expected, actual)
}

@Suppress("MaxLineLength")
@Test
fun `parse should choose AutofillPartition Login when fill-assist resolves the focused view to Identity but a Login view is fillable elsewhere`() {
// A host rule pools an identity field with a login field under one category, so
// fill-assist reclassifies the focused Unused node as Identity while a fillable Login
// view exists elsewhere -- this must not force Unfillable.
mutableFillAssistFlagFlow.value = true
mutableIdentityAutofillFlagFlow.value = true
mockIsFillAssistEnabled = true
every { any<AutofillView>().buildUriOrNull(PACKAGE_NAME) } returns FILL_ASSIST_URI

val usernameSelectorClause = FillAssistRules.SelectorClause(
tag = "input", id = "user", name = null, type = null, role = null,
)
val nameSelectorClause = FillAssistRules.SelectorClause(
tag = "input", id = "fname", name = null, type = null, role = null,
)
every { fillAssistManager.getFillAssistRules() } returns FillAssistRules(
hostRules = mapOf(
FILL_ASSIST_URI to listOf(
FillAssistRules.HostRule(
category = "account-creation",
fields = mapOf(
"username" to listOf(usernameSelectorClause),
"fullName" to listOf(nameSelectorClause),
),
),
),
),
)

val identityAutofillId: AutofillId = mockk()
val identityHtmlInfo: HtmlInfo = mockk(relaxed = true)
val identityViewNode: AssistStructure.ViewNode = mockk {
every { this@mockk.autofillHints } returns emptyArray()
every { this@mockk.autofillId } returns identityAutofillId
every { this@mockk.childCount } returns 0
every { this@mockk.htmlInfo } returns identityHtmlInfo
every { this@mockk.idPackage } returns ID_PACKAGE
every { this@mockk.website } returns null
}
val identityWindowNode: AssistStructure.WindowNode = mockk {
every { this@mockk.rootViewNode } returns identityViewNode
}
every { identityHtmlInfo.matchesSelectorClause(nameSelectorClause) } returns true
every { loginViewNode.htmlInfo!!.matchesSelectorClause(usernameSelectorClause) } returns true

val unusedIdentityView = AutofillView.Unused(
data = AutofillView.Data(
autofillId = identityAutofillId,
autofillOptions = emptyList(),
autofillType = AUTOFILL_TYPE,
isFocused = true,
textValue = null,
hasPasswordTerms = false,
website = null,
),
)
val unusedLoginView = AutofillView.Unused(
data = AutofillView.Data(
autofillId = loginAutofillId,
autofillOptions = emptyList(),
autofillType = AUTOFILL_TYPE,
isFocused = false,
textValue = null,
hasPasswordTerms = false,
website = null,
),
)
every { assistStructure.windowNodeCount } returns 2
every { assistStructure.getWindowNodeAt(0) } returns identityWindowNode
every { assistStructure.getWindowNodeAt(1) } returns loginWindowNode
every {
identityViewNode.toAutofillView(parentWebsite = any(), isIdentityAutofillEnabled = any())
} returns unusedIdentityView
every {
loginViewNode.toAutofillView(parentWebsite = any(), isIdentityAutofillEnabled = any())
} returns unusedLoginView

// Test
val actual = parser.parse(autofillAppInfo = autofillAppInfo, fillRequest = fillRequest)

// Verify: falls through to the fillable Login view instead of becoming Unfillable.
val expected = AutofillRequest.Fillable(
ignoreAutofillIds = emptyList(),
inlinePresentationSpecs = inlinePresentationSpecs,
maxInlineSuggestionsCount = MAX_INLINE_SUGGESTION_COUNT,
packageName = PACKAGE_NAME,
partition = AutofillPartition.Login(
views = listOf(
AutofillView.Login.Username(
data = AutofillView.Data(
autofillId = loginAutofillId,
autofillOptions = emptyList(),
autofillType = AUTOFILL_TYPE,
isFocused = false,
textValue = null,
hasPasswordTerms = false,
website = WEBSITE,
),
),
),
),
uri = FILL_ASSIST_URI,
)
assertEquals(expected, actual)
}

@Suppress("MaxLineLength")
@Test
fun `parse should promote a phone-hinted field to Login Username via updateForMissingUsernameFields when IdentityAutofill is disabled`() {
Expand Down
Loading
Loading