From d2201eb838d65394fc533c899c5fdec3e20b860d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Kwas=CC=81niewski?= Date: Fri, 25 Sep 2026 17:55:59 +0200 Subject: [PATCH] fix(android): name the placeholder in a fill verification failure A fill mismatch reported the raw dump text as actual, which on modern Android is the field's hint when the field is empty. The observed node now carries the helper's hint-showing fact and the hint itself, so a placeholder read as the value is visible in the failure details. Refs #2903, #2634. --- .../src/__tests__/fill-diagnostics.test.ts | 24 +++++++++++++++++++ .../src/__tests__/text-input-fill.test.ts | 14 +++++++++++ .../platform-android/src/fill-diagnostics.ts | 7 ++++++ .../platform-android/src/fill-verification.ts | 3 +-- 4 files changed, 46 insertions(+), 2 deletions(-) diff --git a/packages/platform-android/src/__tests__/fill-diagnostics.test.ts b/packages/platform-android/src/__tests__/fill-diagnostics.test.ts index 6e8680911a..5b95c7bab0 100644 --- a/packages/platform-android/src/__tests__/fill-diagnostics.test.ts +++ b/packages/platform-android/src/__tests__/fill-diagnostics.test.ts @@ -13,6 +13,8 @@ function node(overrides: Partial): AndroidFillVerif password: false, inputMethodOwned: false, area: 0, + hintShowing: false, + placeholder: null, ...overrides, }; } @@ -85,3 +87,25 @@ test('buildFillFailureDetails redacts common masked field glyphs', () => { assert.doesNotMatch(JSON.stringify(details), /Secret123|\*|•|●/); } }); + +// A field whose value the app rewrites, or an empty field dumping its hint as `text`, fails with the +// raw dump text as `actual`. The node facts say whether that text was the field's placeholder. +test('buildFillFailureDetails carries the hint-showing fact and the placeholder on the observed node', () => { + const details = buildFillFailureDetails('Acme Ltd', { + ok: false, + actual: 'e.g. Merchant', + reason: 'text_mismatch', + targetInput: node({ text: 'e.g. Merchant', hintShowing: true, placeholder: 'e.g. Merchant' }), + actualInput: node({ + text: 'e.g. Merchant', + focused: true, + hintShowing: true, + placeholder: 'e.g. Merchant', + }), + }); + + assert.equal(details.expected, 'Acme Ltd'); + assert.equal(details.actual, 'e.g. Merchant'); + assert.equal(details.actualInput?.hintShowing, true); + assert.equal(details.actualInput?.placeholder, 'e.g. Merchant'); +}); diff --git a/packages/platform-android/src/__tests__/text-input-fill.test.ts b/packages/platform-android/src/__tests__/text-input-fill.test.ts index f5f171ca5c..4a20d6ee77 100644 --- a/packages/platform-android/src/__tests__/text-input-fill.test.ts +++ b/packages/platform-android/src/__tests__/text-input-fill.test.ts @@ -430,6 +430,20 @@ test('verifyAndroidFilledTextInHierarchy treats hint-only text as an empty value ); assert.equal(verification.ok, false); assert.equal(verification.actual, 'Search settings'); + // The failure names the placeholder the dump text was, so "actual" is not read as a typed value. + assert.equal(verification.actualInput?.hintShowing, true); +}); + +test('verifyAndroidFilledTextInHierarchy carries the helper hint onto the observed node', () => { + const verification = verifyAndroidFilledTextInHierarchy( + '', + 10, + 10, + 'Acme Ltd', + ); + assert.equal(verification.ok, false); + assert.equal(verification.actualInput?.placeholder, 'e.g. Merchant'); + assert.equal(verification.actualInput?.hintShowing, false); }); test('verifyAndroidFilledTextInHierarchy still refuses a non-empty field for an empty expectation', () => { diff --git a/packages/platform-android/src/fill-diagnostics.ts b/packages/platform-android/src/fill-diagnostics.ts index 962bbaa0fd..7cc9b5bda1 100644 --- a/packages/platform-android/src/fill-diagnostics.ts +++ b/packages/platform-android/src/fill-diagnostics.ts @@ -10,6 +10,13 @@ export type AndroidFillVerificationNode = { password: boolean; inputMethodOwned: boolean; area: number; + /** + * Helper-only fact: the node's dump `text` is its HINT, so the field itself is empty. Reported on + * a mismatch so a placeholder read as the value is visible in the failure, not only in the log. + */ + hintShowing: boolean; + /** The field's hint text when the helper supplied it, whether or not it is showing. */ + placeholder: string | null; }; export type FillFailureReason = 'ime_capture' | 'masked_unverified' | 'text_mismatch'; diff --git a/packages/platform-android/src/fill-verification.ts b/packages/platform-android/src/fill-verification.ts index 085b18b4e8..bc55aa7c56 100644 --- a/packages/platform-android/src/fill-verification.ts +++ b/packages/platform-android/src/fill-verification.ts @@ -24,8 +24,6 @@ export type { AndroidFillVerification } from './fill-diagnostics.ts'; type AndroidFillVerificationCandidate = AndroidFillVerificationNode & { editText: boolean; - // Helper-only fact: the node's dump text is its HINT, so the field itself is empty. - hintShowing: boolean; }; type AndroidTextAtPointInspection = { @@ -410,6 +408,7 @@ function androidFillCandidateFromNode( area, editText: isEditTextClass(node.className ?? ''), hintShowing: node.hintShowing === true, + placeholder: node.hint ?? null, }; }