content.orchestration.test.ts

8.5 KB · raw

1// T-011 through T-014: index.ts autofill orchestration2//3// Why these tests exist: src/content/index.ts (1,263 lines) is the central4// autofill decision layer. It implements:5//   - A 3-layer dismissal model that prevents the selector reopening after6//     dismiss or fill (lastDismissedField, DISMISS_LOCK_MS, wasAlreadyFilled)7//   - handleFieldInteraction: ML predict → locked-vault branch → signup/fill8//   - initialScan: aggression levels 0/1/2 for different page contexts9//   - Message routing: SUMMON_AUTOFILL, VAULT_UNLOCKED, passkey branches10//11// The analysis (§5) flags this as "the most regression-prone code in the12// project" (v1.4.14 had multiple regressions in this layer) with zero unit13// test coverage. The e2e suite uses synthetic HTML and doesn't drive the14// orchestration state machine.15//16// ---------------------------------------------------------------------------17// Integration Contract — REFACTORING REQUIRED18// ---------------------------------------------------------------------------19//20// index.ts exports NOTHING. All functions (handleFieldInteraction, initialScan,21// wasAlreadyFilled, startDismissLock) and state (lastDismissedField, dismissLock,22// passkeySummonActive, pageFilledAt) are module-private. To make the tests below23// runnable, the following exports are needed:24//25//   export { handleFieldInteraction }       // for T-01226//   export { initialScan }                  // for T-01327//   export { wasAlreadyFilled }             // for T-011 (wasAlreadyFilled check)28//   export const _testSeam: {              // for T-011 (per-field lock state)29//     getLastDismissedField(): HTMLInputElement | null;30//     setLastDismissedField(f: HTMLInputElement | null): void;31//     isDismissLocked(): boolean;32//     DISMISS_LOCK_MS: number;33//   }34//35// Additionally, the test environment must be switched to jsdom:36//   // in vitest.config.ts:37//   environmentMatchGlobs: [['src/drafts/TEST-EXPANSION/content.orchestration.test.ts', 'jsdom']]38//39// And the content script's chrome API calls must be mocked:40//   - chrome.runtime.sendMessage (for vault queries)41//   - chrome.runtime.onMessage.addListener (for VAULT_UNLOCKED/SUMMON_AUTOFILL)42//43// ---------------------------------------------------------------------------4445import { describe, it } from "vitest";4647// All tests below are stubs pending the refactoring described above.48// They document the intended behavior and the specific bug each test catches.4950describe("T-011: Dismissal model — 3-layer prevention of selector reopen", () => {51  // T-011a: per-field lock (lastDismissedField)52  // After dismissing the selector on field A, focusing field A again must NOT53  // reopen the selector. Focusing a different field B must reopen normally.54  //55  // Bug this catches: removing the `field === lastDismissedField` guard in56  // handleFieldInteraction causes the selector to reopen immediately after57  // any Escape-dismiss (regression from v1.4.14).58  it.todo(59    "T-011a: focus field A → Escape dismiss → re-focus field A → selector does NOT reopen (requires _testSeam export)",60  );6162  // T-011b: DISMISS_LOCK_MS global cooldown63  // After any dismiss, a 800ms global lock prevents the selector reopening64  // on any field (cross-field rapid tabbing).65  //66  // Bug this catches: removing startDismissLock() from the onDismiss callback67  // allows the selector to reopen immediately when the user tabs to the next68  // field after dismissing.69  it.todo(70    "T-011b: dismiss → immediately focus another field → selector does NOT open within 800ms lock (requires _testSeam export)",71  );7273  // T-011c: wasAlreadyFilled form-level grace (PAGE_FILL_GRACE_MS = 3000ms)74  // After a successful fill, the form and page enter a 3-second grace period.75  // Re-focusing the same field during the grace period must not reopen the selector.76  //77  // Bug this catches: clearing pageFilledAt or removing the PAGE_FILL_GRACE_MS78  // check causes the selector to immediately reappear after autofill, annoying79  // users and breaking the "fill once" guarantee.80  it.todo(81    "T-011c: fill field → immediate re-focus → selector does NOT reopen (wasAlreadyFilled grace, requires jsdom + real fills)",82  );83});8485describe("T-012: handleFieldInteraction core paths", () => {86  // T-012a: ML timeout race (150ms)87  // If contentMLDetector.predictBatch takes > 150ms, handleFieldInteraction88  // should use the fallback (no ML prediction) rather than waiting indefinitely.89  //90  // Bug this catches: removing the Promise.race timeout causes autofill to91  // hang on slow pages where ML prediction is delayed.92  it.todo(93    "T-012a: ML-predict timeout — mock predictBatch to take 200ms → timeout path taken (requires handleFieldInteraction export)",94  );9596  // T-012b: locked vault path97  // When the vault is locked, handleFieldInteraction must show the lock prompt98  // (not the entry selector) and NOT call showAutofillSelector.99  it.todo(100    "T-012b: locked vault → lock prompt shown, showAutofillSelector NOT called (requires chrome.runtime mock)",101  );102103  // T-012c: signup mode (no domain entries)104  // When vault has no entries for the current domain, the identity surface is105  // shown (for username pre-fill) rather than the entry selector.106  it.todo(107    "T-012c: vault has no domain entries → identity surface shown (requires chrome.runtime.sendMessage mock)",108  );109110  // T-012d: email-first form (no password field on step 1)111  // Google/Microsoft login pages show email, then password on the next page.112  // handleFieldInteraction must detect this and show only the email-fill surface.113  it.todo(114    "T-012d: email-first form (no password field) → email-only path taken (requires jsdom form)",115  );116});117118describe("T-013: initialScan aggression levels", () => {119  // T-013a: level 0 — page with no credential form fields → no icon injected.120  // Bug this catches: a false-positive in isAnyCredentialForm causes the icon121  // to appear on forms that have no username/password fields.122  it.todo(123    "T-013a: form with no matching fields + level 0 → initialScan skips injection (requires jsdom)",124  );125126  // T-013b: level 1 — standard login form → icon injected on password field.127  it.todo(128    "T-013b: standard login form + level 1 → icon injected (requires jsdom + injectFieldIcon spy)",129  );130131  // T-013c: level 2 final-pass — bare password field (no username) → icon injected.132  // This covers the scenario where a SPA reveals the password field after the133  // username step. The final-pass injection catches fields missed by level 0/1.134  it.todo(135    "T-013c: bare password field + level 2 finalPass → icon injected (requires jsdom + injectFieldIcon spy)",136  );137});138139describe("T-014: Message routing — SUMMON_AUTOFILL and VAULT_UNLOCKED", () => {140  // T-014a: SUMMON_AUTOFILL from keyboard shortcut opens selector on focused field.141  // Bug this catches: removing the SUMMON_AUTOFILL branch in the message listener142  // breaks the Ctrl+Shift+F shortcut for manual autofill.143  it.todo(144    "T-014a: SUMMON_AUTOFILL message → selector opens on active field (requires chrome.runtime.onMessage mock)",145  );146147  // T-014b: VAULT_UNLOCKED re-triggers autofill on pending field.148  // When the user unlocks the vault in response to a lock-prompt, the content149  // script should automatically retry the fill for the field that triggered it.150  it.todo(151    "T-014b: VAULT_UNLOCKED → re-trigger handleFieldInteraction on pending field (requires chrome.runtime.onMessage mock)",152  );153154  // T-014c: VAULT_UNLOCKED does not double-trigger when passkeySummonActive.155  // If a passkey toast is already showing (passkeySummonActive = true), the156  // VAULT_UNLOCKED handler must skip autofill to prevent a race condition.157  it.todo(158    "T-014c: VAULT_UNLOCKED with passkeySummonActive=true → no double-trigger (requires _testSeam export)",159  );160});161162// ---------------------------------------------------------------------------163// What CAN be tested without refactoring164// ---------------------------------------------------------------------------165// The pure helper functions in src/content/detect.ts, fill.ts, interaction.ts,166// policy.ts, and identity-helper.ts are already well covered. The autofill167// gap is specifically in index.ts's stateful orchestration layer.168//169// The only testable behavior from index.ts without exports is importing the170// module and verifying it doesn't crash on load (smoke test):171describe("T-011/smoke: index.ts module loads without throwing", () => {172  it.todo(173    "importing index.ts in a jsdom environment with chrome mock does not throw (requires jsdom environment switch)",174  );175});