# AUTOFILL-TESTS — Integration Contract Draft test coverage for the autofill layer (`src/content/index.ts`, `inject.ts`, `detect.ts`, `fill.ts`, `save.ts`, `interaction.ts`, `policy.ts`). Authored against **v3.9.9**, dev lane. All draft files live in `src/drafts/AUTOFILL-TESTS/` and were verified green with a throwaway runner config — **no source file was modified**. ## Status at a glance | Metric | Count | | --- | --- | | Runnable unit/integration tests (vitest) | 118 | | `expect()` assertions | 212 | | `it.todo` (routed to e2e / proposed features) | 23 | | `describe.skipIf` blocks (proposed-feature specs) | 2 (6 tests) | | Playwright e2e tests (draft) | 10 | | Result | **13 files, 114 passed / 6 skipped / 23 todo** | ## Files ### Shared (do not delete) | File | Role | Destination | | --- | --- | --- | | `_setup.ts` | chrome mock (side-effect on import) + DOM factories + jsdom `getBoundingClientRect`/`ResizeObserver` shims | `src/content/__support__/` or inline into each test | | `_smoke.test.ts` | proves `index.ts`/`inject.ts` import under the harness | keep as a guard | | `vitest.drafts.config.ts` *(repo root)* | throwaway runner (includes drafts, drops the `src/drafts/**` exclude) | **delete after integration** | ### vitest (unit/integration) → keep in `src/content/` (or wherever the suite lives) | File | Brief mapping | Notes | | --- | --- | --- | | `content.detect.test.ts` | Test File 7, T-DT01–T-DT21 + Addendum 1 | classifiers tested on hand-built arrays (see "querySelectorAllDeep" below) | | `content.fill.test.ts` | Test File 8, T-FL01–T-FL12 | real DOM; React prototype-setter bypass | | `content.interaction.test.ts` | Test File 12, T-IT01–T-IT06 | pure function | | `content.policy.test.ts` | Test File 11, T-PO01–T-PO06 | real scraping; see deviations | | `content.dismissal.test.ts` | Test File 1, T-D01–T-D15 | 3-layer model via seams + fake timers | | `content.field-interaction.test.ts` | Test File 2, T-F01–T-F12 | mocks `./inject` + `./content-ml-detector` | | `content.initial-scan.test.ts` | Test File 3, T-S01–T-S10 | mocks `./inject` (icon spy) | | `content.message-routing.test.ts` | Test File 4, T-M01–T-M10 | drives captured `onMessage` listeners | | `content.field-icon.test.ts` | Test File 6, T-IC01–T-IC08 | real light-DOM button | | `content.selector-ui.test.ts` | Test File 5, T-I01–T-I16 | **closed shadow** — mount/dismiss only | | `content.totp-toast.addendum.test.ts` | Addendum 2, T-M11–T-M13 | `skipIf` until `showTotpCopyToast` exists | ### Playwright (e2e) → move to `tests/` | File | Brief mapping | Destination | | --- | --- | --- | | `content.real-page.e2e.spec.ts` | Test File 10 / T-028, T-E01–T-E10 | `tests/content.real-page.e2e.spec.ts` | | `login-form.fixture.html` | required fixture | `tests/fixtures/login-form.html` (update the `FIXTURE` path + `page.route`/static server) | ## Environment / config changes needed - **No `environmentMatchGlobs` change required.** Every file uses the per-file `// @vitest-environment jsdom` pragma (the convention already used by `detect.test.ts`, `fill.test.ts`, etc.). The stale glob comment in the committed `content.orchestration.test.ts` does not apply. - To run these in the real suite, either (a) move the `*.test.ts` files into `src/content/`, or (b) drop/relax the `exclude: ["src/drafts/**"]` line in `vitest.config.ts`. Until then they only run via `vitest.drafts.config.ts`. - **chrome mock:** there is **no** `__mocks__/chrome.ts` in the repo (the brief assumed one). `_setup.ts` provides the mock the content scripts need: `runtime.sendMessage` (promise + callback forms), `runtime.onMessage` (capturable), `runtime.id`, `runtime.getURL`, `storage.session/local`, plus `navigator.clipboard` and a `ResizeObserver` polyfill. Fold this into the project's shared test support during integration. ## `_testSeam` usage Tests that need module-private state use the existing `_testSeam` export (`getLastDismissedField`/`setLastDismissedField`/`isDismissLocked`/ `DISMISS_LOCK_MS`): - `content.dismissal.test.ts` — Layers 1 & 2. The other index.ts files drive behavior through the public seams (`handleFieldInteraction`, `initialScan`, `wasAlreadyFilled`) and the captured `onDismiss`/`onFillFields` callbacks the selector is opened with — **no new exports required.** ## Behavioral findings the tests pin (read before "fixing" a failure) 1. **`querySelectorAllDeep` ancestor duplication.** It re-queries every ancestor element, so a field under `html>body>form` appears 3× in `detectLoginFields()`. That inflates the password count, so `isLoginForm(detectLoginFields())` is `false` for a normal single-password login — the page is recognised as fillable via the `isSignupForm` branch of `isAnyCredentialForm` instead. Detect tests therefore assert detection by *membership* and the page gate via `isAnyCredentialForm`, and test the login-vs-signup classifier on clean, hand-built arrays (matching the committed `detect.test.ts`). It also misses shadow-DOM inputs that are *direct* children of a shadow root (only those nested under an element are collected). 2. **Closed shadow roots.** `showAutofillSelector`, `showTOTPSelector`, `showCardSelector`, and `save.ts`'s banner all mount `attachShadow({ mode: "closed" })`. Their interiors (entry rows, search box, keyboard nav, TOTP chip, Save/Update buttons, homograph warning) are unreachable from any external script. Unit tests assert only mount/dismiss + outbound messages; the interiors are covered by the e2e through effects. **Recommendation:** add a test-only "open shadow in `import.meta.env.MODE==='test'`" seam if richer unit coverage of the selector interior is wanted. 3. **`wasAlreadyFilled` page-grace is NOT form-scoped** (brief T-D10 assumed it was). For `PAGE_FILL_GRACE_MS` (3 s) it suppresses *every* field; after the window, only the filled form stays locked (`filledForms` is permanent). Tests pin the actual behavior. 4. **`fillFields` returns `void`** (brief T-FL12 assumed it returns the filled set). Fill-success tracking is the caller's job (`recordFillSuccess`). 5. **`detectPasswordPolicy` ignores a `minlength` attribute** (brief T-PO01); it reads `maxLength`, the `pattern`, and nearby text. It inspects only the first password field, so "most restrictive across fields" (T-PO06) is not implemented. 6. **No `DISMISS_AUTOFILL` / `FILL_AND_SUBMIT` messages** in index.ts's listener (brief T-M06/T-M07). The search-palette path is `SUMMON_AUTOFILL` with no form (T-M08 ≡ T-M02 here). 7. **PCH-01-025 CSS.escape:** jsdom's selector engine *throws* on `CSS.escape`'s output, so the label-resolution benefit is only observable in the e2e. The unit tests pin the resilience invariant (a hostile id never aborts detection; the embedded-quote case fails if the try/catch is removed). 8. **Benign teardown log.** Importing the real `index.ts` registers a url-watcher `MutationObserver`; its callback can log `location is not defined` once during jsdom teardown. Exit code is 0 and no test fails. Tests flush it per-test where practical. ## Refactoring still needed beyond Phase 1 (to satisfy the brief fully) | Want | Needs | | --- | --- | | Addendum 1 split-OTP `isSplitOtpGroup` (T-DT18–21) | export `isSplitOtpGroup(field)` from `detect.ts` (3+ adjacent numeric single-char inputs; numeric guard). The `skipIf` spec activates automatically. Existing `findOTPDigitBoxGroup` (4–10 boxes) is already covered. | | Addendum 2 `showTotpCopyToast` (T-M11–13) | export `showTotpCopyToast(secondsUntilClear)` from `inject.ts` and call it after the auto-copy clipboard write. `skipIf` spec activates automatically. | | Selector interior unit coverage (T-I02–16, T-SV06–09, T-D14/15) | a test-only open-shadow seam, **or** rely on the e2e (current plan). | | T-IC07 "suppress icon on `autocomplete=current-password`" | no such policy exists in `injectFieldIcon`; add it to the detect/index gate if desired. | ## How to run (during integration) ```sh # drafts only, via the throwaway config (delete the config after moving files): npx vitest run --config vitest.drafts.config.ts # after moving *.test.ts into src/content/ (or relaxing the exclude): npx vitest run ```