// T-011 through T-014: index.ts autofill orchestration // // Why these tests exist: src/content/index.ts (1,263 lines) is the central // autofill decision layer. It implements: // - A 3-layer dismissal model that prevents the selector reopening after // dismiss or fill (lastDismissedField, DISMISS_LOCK_MS, wasAlreadyFilled) // - handleFieldInteraction: ML predict → locked-vault branch → signup/fill // - initialScan: aggression levels 0/1/2 for different page contexts // - Message routing: SUMMON_AUTOFILL, VAULT_UNLOCKED, passkey branches // // The analysis (§5) flags this as "the most regression-prone code in the // project" (v1.4.14 had multiple regressions in this layer) with zero unit // test coverage. The e2e suite uses synthetic HTML and doesn't drive the // orchestration state machine. // // --------------------------------------------------------------------------- // Integration Contract — REFACTORING REQUIRED // --------------------------------------------------------------------------- // // index.ts exports NOTHING. All functions (handleFieldInteraction, initialScan, // wasAlreadyFilled, startDismissLock) and state (lastDismissedField, dismissLock, // passkeySummonActive, pageFilledAt) are module-private. To make the tests below // runnable, the following exports are needed: // // export { handleFieldInteraction } // for T-012 // export { initialScan } // for T-013 // export { wasAlreadyFilled } // for T-011 (wasAlreadyFilled check) // export const _testSeam: { // for T-011 (per-field lock state) // getLastDismissedField(): HTMLInputElement | null; // setLastDismissedField(f: HTMLInputElement | null): void; // isDismissLocked(): boolean; // DISMISS_LOCK_MS: number; // } // // Additionally, the test environment must be switched to jsdom: // // in vitest.config.ts: // environmentMatchGlobs: [['src/drafts/TEST-EXPANSION/content.orchestration.test.ts', 'jsdom']] // // And the content script's chrome API calls must be mocked: // - chrome.runtime.sendMessage (for vault queries) // - chrome.runtime.onMessage.addListener (for VAULT_UNLOCKED/SUMMON_AUTOFILL) // // --------------------------------------------------------------------------- import { describe, it } from "vitest"; // All tests below are stubs pending the refactoring described above. // They document the intended behavior and the specific bug each test catches. describe("T-011: Dismissal model — 3-layer prevention of selector reopen", () => { // T-011a: per-field lock (lastDismissedField) // After dismissing the selector on field A, focusing field A again must NOT // reopen the selector. Focusing a different field B must reopen normally. // // Bug this catches: removing the `field === lastDismissedField` guard in // handleFieldInteraction causes the selector to reopen immediately after // any Escape-dismiss (regression from v1.4.14). it.todo( "T-011a: focus field A → Escape dismiss → re-focus field A → selector does NOT reopen (requires _testSeam export)", ); // T-011b: DISMISS_LOCK_MS global cooldown // After any dismiss, a 800ms global lock prevents the selector reopening // on any field (cross-field rapid tabbing). // // Bug this catches: removing startDismissLock() from the onDismiss callback // allows the selector to reopen immediately when the user tabs to the next // field after dismissing. it.todo( "T-011b: dismiss → immediately focus another field → selector does NOT open within 800ms lock (requires _testSeam export)", ); // T-011c: wasAlreadyFilled form-level grace (PAGE_FILL_GRACE_MS = 3000ms) // After a successful fill, the form and page enter a 3-second grace period. // Re-focusing the same field during the grace period must not reopen the selector. // // Bug this catches: clearing pageFilledAt or removing the PAGE_FILL_GRACE_MS // check causes the selector to immediately reappear after autofill, annoying // users and breaking the "fill once" guarantee. it.todo( "T-011c: fill field → immediate re-focus → selector does NOT reopen (wasAlreadyFilled grace, requires jsdom + real fills)", ); }); describe("T-012: handleFieldInteraction core paths", () => { // T-012a: ML timeout race (150ms) // If contentMLDetector.predictBatch takes > 150ms, handleFieldInteraction // should use the fallback (no ML prediction) rather than waiting indefinitely. // // Bug this catches: removing the Promise.race timeout causes autofill to // hang on slow pages where ML prediction is delayed. it.todo( "T-012a: ML-predict timeout — mock predictBatch to take 200ms → timeout path taken (requires handleFieldInteraction export)", ); // T-012b: locked vault path // When the vault is locked, handleFieldInteraction must show the lock prompt // (not the entry selector) and NOT call showAutofillSelector. it.todo( "T-012b: locked vault → lock prompt shown, showAutofillSelector NOT called (requires chrome.runtime mock)", ); // T-012c: signup mode (no domain entries) // When vault has no entries for the current domain, the identity surface is // shown (for username pre-fill) rather than the entry selector. it.todo( "T-012c: vault has no domain entries → identity surface shown (requires chrome.runtime.sendMessage mock)", ); // T-012d: email-first form (no password field on step 1) // Google/Microsoft login pages show email, then password on the next page. // handleFieldInteraction must detect this and show only the email-fill surface. it.todo( "T-012d: email-first form (no password field) → email-only path taken (requires jsdom form)", ); }); describe("T-013: initialScan aggression levels", () => { // T-013a: level 0 — page with no credential form fields → no icon injected. // Bug this catches: a false-positive in isAnyCredentialForm causes the icon // to appear on forms that have no username/password fields. it.todo( "T-013a: form with no matching fields + level 0 → initialScan skips injection (requires jsdom)", ); // T-013b: level 1 — standard login form → icon injected on password field. it.todo( "T-013b: standard login form + level 1 → icon injected (requires jsdom + injectFieldIcon spy)", ); // T-013c: level 2 final-pass — bare password field (no username) → icon injected. // This covers the scenario where a SPA reveals the password field after the // username step. The final-pass injection catches fields missed by level 0/1. it.todo( "T-013c: bare password field + level 2 finalPass → icon injected (requires jsdom + injectFieldIcon spy)", ); }); describe("T-014: Message routing — SUMMON_AUTOFILL and VAULT_UNLOCKED", () => { // T-014a: SUMMON_AUTOFILL from keyboard shortcut opens selector on focused field. // Bug this catches: removing the SUMMON_AUTOFILL branch in the message listener // breaks the Ctrl+Shift+F shortcut for manual autofill. it.todo( "T-014a: SUMMON_AUTOFILL message → selector opens on active field (requires chrome.runtime.onMessage mock)", ); // T-014b: VAULT_UNLOCKED re-triggers autofill on pending field. // When the user unlocks the vault in response to a lock-prompt, the content // script should automatically retry the fill for the field that triggered it. it.todo( "T-014b: VAULT_UNLOCKED → re-trigger handleFieldInteraction on pending field (requires chrome.runtime.onMessage mock)", ); // T-014c: VAULT_UNLOCKED does not double-trigger when passkeySummonActive. // If a passkey toast is already showing (passkeySummonActive = true), the // VAULT_UNLOCKED handler must skip autofill to prevent a race condition. it.todo( "T-014c: VAULT_UNLOCKED with passkeySummonActive=true → no double-trigger (requires _testSeam export)", ); }); // --------------------------------------------------------------------------- // What CAN be tested without refactoring // --------------------------------------------------------------------------- // The pure helper functions in src/content/detect.ts, fill.ts, interaction.ts, // policy.ts, and identity-helper.ts are already well covered. The autofill // gap is specifically in index.ts's stateful orchestration layer. // // The only testable behavior from index.ts without exports is importing the // module and verifying it doesn't crash on load (smoke test): describe("T-011/smoke: index.ts module loads without throwing", () => { it.todo( "importing index.ts in a jsdom environment with chrome mock does not throw (requires jsdom environment switch)", ); });