INTEGRATION-CONTRACT.md
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
environmentMatchGlobschange required. Every file uses the per-file// @vitest-environment jsdompragma (the convention already used bydetect.test.ts,fill.test.ts, etc.). The stale glob comment in the committedcontent.orchestration.test.tsdoes not apply. - To run these in the real suite, either (a) move the
*.test.tsfiles intosrc/content/, or (b) drop/relax theexclude: ["src/drafts/**"]line invitest.config.ts. Until then they only run viavitest.drafts.config.ts. - chrome mock: there is no
__mocks__/chrome.tsin the repo (the brief assumed one)._setup.tsprovides the mock the content scripts need:runtime.sendMessage(promise + callback forms),runtime.onMessage(capturable),runtime.id,runtime.getURL,storage.session/local, plusnavigator.clipboardand aResizeObserverpolyfill. 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 capturedonDismiss/onFillFieldscallbacks the selector is opened with — no new exports required.
Behavioral findings the tests pin (read before "fixing" a failure)
querySelectorAllDeepancestor duplication. It re-queries every ancestor element, so a field underhtml>body>formappears 3× indetectLoginFields(). That inflates the password count, soisLoginForm(detectLoginFields())isfalsefor a normal single-password login — the page is recognised as fillable via theisSignupFormbranch ofisAnyCredentialForminstead. Detect tests therefore assert detection by membership and the page gate viaisAnyCredentialForm, and test the login-vs-signup classifier on clean, hand-built arrays (matching the committeddetect.test.ts). It also misses shadow-DOM inputs that are direct children of a shadow root (only those nested under an element are collected).- Closed shadow roots.
showAutofillSelector,showTOTPSelector,showCardSelector, andsave.ts's banner all mountattachShadow({ 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 inimport.meta.env.MODE==='test'" seam if richer unit coverage of the selector interior is wanted. wasAlreadyFilledpage-grace is NOT form-scoped (brief T-D10 assumed it was). ForPAGE_FILL_GRACE_MS(3 s) it suppresses every field; after the window, only the filled form stays locked (filledFormsis permanent). Tests pin the actual behavior.fillFieldsreturnsvoid(brief T-FL12 assumed it returns the filled set). Fill-success tracking is the caller's job (recordFillSuccess).detectPasswordPolicyignores aminlengthattribute (brief T-PO01); it readsmaxLength, thepattern, and nearby text. It inspects only the first password field, so "most restrictive across fields" (T-PO06) is not implemented.- No
DISMISS_AUTOFILL/FILL_AND_SUBMITmessages in index.ts's listener (brief T-M06/T-M07). The search-palette path isSUMMON_AUTOFILLwith no form (T-M08 ≡ T-M02 here). - 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). - Benign teardown log. Importing the real
index.tsregisters a url-watcherMutationObserver; its callback can loglocation is not definedonce 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)
# 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