From e1d2e7d8fba34e4347bc0b73989e8d9dd8c11da3 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Tue, 3 Feb 2026 23:49:37 +0100 Subject: [PATCH] Add formdetector autocomplete tag detection (#1614) --- .../src/utils/formDetector/FormDetector.ts | 59 ++++++++++++++++++- .../__tests__/FormDetector.en.test.ts | 6 ++ .../__tests__/FormFiller.en.test.ts | 31 +++++++++- .../__tests__/test-forms/fr-login-form1.html | 25 ++++++++ 4 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 apps/browser-extension/src/utils/formDetector/__tests__/test-forms/fr-login-form1.html diff --git a/apps/browser-extension/src/utils/formDetector/FormDetector.ts b/apps/browser-extension/src/utils/formDetector/FormDetector.ts index 8f4022aa9..01a972ac1 100644 --- a/apps/browser-extension/src/utils/formDetector/FormDetector.ts +++ b/apps/browser-extension/src/utils/formDetector/FormDetector.ts @@ -360,11 +360,49 @@ export class FormDetector { continue; } + // Check autocomplete attribute for direct field type matching + const autocomplete = input.getAttribute('autocomplete')?.toLowerCase() ?? ''; + + // Direct autocomplete matches take highest priority (score -2, higher than type=email at -1) + if (autocomplete) { + // Match autocomplete="username" for username patterns + if (patterns === CombinedFieldPatterns.username && autocomplete === 'username') { + matches.push({ input: input as HTMLInputElement, score: -2 }); + continue; + } + // Match autocomplete="email" for email patterns + if (patterns === CombinedFieldPatterns.email && autocomplete === 'email') { + matches.push({ input: input as HTMLInputElement, score: -2 }); + continue; + } + // Match autocomplete="current-password" or "new-password" for password patterns + if (patterns === CombinedFieldPatterns.password && + (autocomplete === 'current-password' || autocomplete === 'new-password')) { + matches.push({ input: input as HTMLInputElement, score: -2 }); + continue; + } + } + + /** + * Check aria-describedby ID for direct field type matching (e.g., aria-describedby="usernameMessage") + * Only match if it's a clear username indicator (not usernameConfirm, etc.) + */ + const ariaDescribedById = input.getAttribute('aria-describedby')?.toLowerCase() ?? ''; + if (ariaDescribedById) { + // Match aria-describedby containing "username" for username patterns + if (patterns === CombinedFieldPatterns.username && + ariaDescribedById.includes('username')) { + matches.push({ input: input as HTMLInputElement, score: -2 }); + continue; + } + } + // Collect all text attributes to check const attributesToCheck = [ input.id, input.getAttribute('name'), - input.getAttribute('placeholder') + input.getAttribute('placeholder'), + autocomplete ] .map(a => a?.toLowerCase() ?? ''); @@ -376,6 +414,25 @@ export class FormDetector { } } + // Check aria-describedby for additional field hints + const ariaDescribedBy = input.getAttribute('aria-describedby'); + if (ariaDescribedBy) { + // aria-describedby can contain multiple space-separated IDs + const describedByIds = ariaDescribedBy.split(/\s+/); + for (const descId of describedByIds) { + const describedByElement = this.document.getElementById(descId); + if (describedByElement) { + attributesToCheck.push(describedByElement.textContent?.toLowerCase() ?? ''); + } + } + } + + // Check aria-label attribute + const ariaLabel = input.getAttribute('aria-label'); + if (ariaLabel) { + attributesToCheck.push(ariaLabel.toLowerCase()); + } + /** * Check for slot-based labels (e.g., Email or username) * Look for slot elements within the input's parent hierarchy diff --git a/apps/browser-extension/src/utils/formDetector/__tests__/FormDetector.en.test.ts b/apps/browser-extension/src/utils/formDetector/__tests__/FormDetector.en.test.ts index 80603f5ff..2eeace919 100644 --- a/apps/browser-extension/src/utils/formDetector/__tests__/FormDetector.en.test.ts +++ b/apps/browser-extension/src/utils/formDetector/__tests__/FormDetector.en.test.ts @@ -92,4 +92,10 @@ describe('FormDetector English tests', () => { testField(FormField.BirthYear, 'YearDropdown', htmlFile); }); + describe('French login form 1 detection (France Tax Authority)', () => { + const htmlFile = 'fr-login-form1.html'; + + testField(FormField.Username, 'spi_tmp', htmlFile); + }); + }); diff --git a/apps/browser-extension/src/utils/formDetector/__tests__/FormFiller.en.test.ts b/apps/browser-extension/src/utils/formDetector/__tests__/FormFiller.en.test.ts index 3612612aa..52bc527a0 100644 --- a/apps/browser-extension/src/utils/formDetector/__tests__/FormFiller.en.test.ts +++ b/apps/browser-extension/src/utils/formDetector/__tests__/FormFiller.en.test.ts @@ -3,10 +3,11 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; import type { Credential } from '@/utils/dist/core/models/vault'; +import { FormDetector } from '../FormDetector'; import { FormFiller } from '../FormFiller'; import { FormFields } from '../types/FormFields'; -import { setupTestDOM, createMockFormFields, createMockCredential, wasTriggerCalledFor, createDateSelects } from './TestUtils'; +import { setupTestDOM, createMockFormFields, createMockCredential, wasTriggerCalledFor, createDateSelects, createTestDom } from './TestUtils'; const { window } = new JSDOM(''); global.HTMLSelectElement = window.HTMLSelectElement; @@ -62,4 +63,32 @@ describe('FormFiller English', () => { expect(wasTriggerCalledFor(mockTriggerInputEvents, yearSelect)).toBe(true); }); }); + + describe('French login form 1 (France Tax Authority)', () => { + it('should detect and fill username field with autocomplete="username" and name="spi_tmp"', async () => { + const dom = createTestDom('fr-login-form1.html'); + const doc = dom.window.document; + + // Get the input field + const usernameInput = doc.getElementById('spi_tmp') as HTMLInputElement; + expect(usernameInput).not.toBeNull(); + + // Create form detector to find the form fields + const formDetector = new FormDetector(doc, usernameInput); + const detectedFields = formDetector.getForm(); + + // The username field should be detected due to autocomplete="username" + expect(detectedFields?.usernameField).toBe(usernameInput); + + // If the field is detected, test filling it + if (detectedFields) { + const triggerMock = vi.fn(); + const filler = new FormFiller(detectedFields, triggerMock); + await filler.fillFields(mockCredential); + + expect(usernameInput.value).toBe('testuser'); + expect(wasTriggerCalledFor(triggerMock, usernameInput)).toBe(true); + } + }); + }); }); \ No newline at end of file diff --git a/apps/browser-extension/src/utils/formDetector/__tests__/test-forms/fr-login-form1.html b/apps/browser-extension/src/utils/formDetector/__tests__/test-forms/fr-login-form1.html new file mode 100644 index 000000000..7f217647e --- /dev/null +++ b/apps/browser-extension/src/utils/formDetector/__tests__/test-forms/fr-login-form1.html @@ -0,0 +1,25 @@ + + + + + France Tax Login + + +
+
+
+ + +
+
+ +
+
+ +