mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-25 00:42:57 -04:00
Add formdetector autocomplete tag detection (#1614)
This commit is contained in:
committed by
Leendert de Borst
parent
9d02ddaf9b
commit
e1d2e7d8fb
@@ -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., <span slot="label">Email or username</span>)
|
||||
* Look for slot elements within the input's parent hierarchy
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -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('<!DOCTYPE html>');
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>France Tax Login</title>
|
||||
</head>
|
||||
<body>
|
||||
<form id="loginForm">
|
||||
<div class="fr-fieldset__element">
|
||||
<div class="fr-input-group" id="usernameGroup">
|
||||
<label class="fr-label" for="spi_tmp">
|
||||
Numéro fiscal <span class="fr-hint-text">Format attendu : 13 chiffres</span>
|
||||
</label>
|
||||
<input class="fr-input form-control" autocomplete="username" aria-required="true" autocorrect="off" aria-describedby="usernameMessage" name="spi_tmp" id="spi_tmp" type="text" maxlength="13" inputmode="numeric" pattern="[0-9]*" data-original-title="13 chiffres" data-placement="top" data-mask="int" data-max="13" data-feedbackok="1/">
|
||||
<div class="fr-messages-group" id="usernameMessage"></div>
|
||||
</div>
|
||||
<p class="fr-mt-n3v" id="lostFiscalNumberLink">
|
||||
<button type="button" class="fr-btn fr-btn--tertiary-no-outline" data-fr-opened="false" aria-controls="numFiscal" style="text-decoration: underline; text-underline-offset: 0.25rem; text-decoration-thickness: 1px; background-color: transparent !important; transition: text-decoration-thickness 0.1s ease; margin: -17px;" onmouseover="this.style.textDecorationThickness='2px'; this.style.backgroundColor='transparent'" onmouseout="this.style.textDecorationThickness='1px'; this.style.backgroundColor='transparent'" data-fr-js-modal-button="true">
|
||||
Numéro fiscal oublié ?
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user