f29f759b9d38
add save.ts save-detection tests (T-SV01-10, banner-mount + AUTOFILL_QUERY/GET_ENTRY_FOR_FILL lookup; accept flows are closed-shadow e2e), Playwright e2e draft (T-E01-10) + login-form fixture, INTEGRATION-CONTRACT.md (file map, deviations, remaining refactors, 212 assertions) and CONTINUITY session record — full real suite recertified 104 passed/1 skipped, drafts excluded and unaffected
- time
- 2026-06-12 13:02 UTC · 4 months ago
- machine
- koh2
- agent
- claude-opus-4-8
- session
- autofill-tests draft suite
- parent
- cbcc3e0006bd
↓ peach-browser-f29f759b.tar.gzatlas at this save
files changed4 files
src/drafts/AUTOFILL-TESTS/INTEGRATION-CONTRACT.md+136 −0src/drafts/AUTOFILL-TESTS/content.real-page.e2e.spec.ts+191 −0src/drafts/AUTOFILL-TESTS/content.save.test.ts+136 −0src/drafts/AUTOFILL-TESTS/login-form.fixture.html+32 −0src/drafts/AUTOFILL-TESTS/INTEGRATION-CONTRACT.md+136 −0
@@ -0,0 +1,136 @@1+# AUTOFILL-TESTS — Integration Contract2+3+Draft test coverage for the autofill layer (`src/content/index.ts`,4+`inject.ts`, `detect.ts`, `fill.ts`, `save.ts`, `interaction.ts`, `policy.ts`).5+Authored against **v3.9.9**, dev lane. All draft files live in6+`src/drafts/AUTOFILL-TESTS/` and were verified green with a throwaway runner7+config — **no source file was modified**.8+9+## Status at a glance10+11+| Metric | Count |12+| --- | --- |13+| Runnable unit/integration tests (vitest) | 118 |14+| `expect()` assertions | 212 |15+| `it.todo` (routed to e2e / proposed features) | 23 |16+| `describe.skipIf` blocks (proposed-feature specs) | 2 (6 tests) |17+| Playwright e2e tests (draft) | 10 |18+| Result | **13 files, 114 passed / 6 skipped / 23 todo** |19+20+## Files21+22+### Shared (do not delete)23+| File | Role | Destination |24+| --- | --- | --- |25+| `_setup.ts` | chrome mock (side-effect on import) + DOM factories + jsdom `getBoundingClientRect`/`ResizeObserver` shims | `src/content/__support__/` or inline into each test |26+| `_smoke.test.ts` | proves `index.ts`/`inject.ts` import under the harness | keep as a guard |27+| `vitest.drafts.config.ts` *(repo root)* | throwaway runner (includes drafts, drops the `src/drafts/**` exclude) | **delete after integration** |28+29+### vitest (unit/integration) → keep in `src/content/` (or wherever the suite lives)30+| File | Brief mapping | Notes |31+| --- | --- | --- |32+| `content.detect.test.ts` | Test File 7, T-DT01–T-DT21 + Addendum 1 | classifiers tested on hand-built arrays (see "querySelectorAllDeep" below) |33+| `content.fill.test.ts` | Test File 8, T-FL01–T-FL12 | real DOM; React prototype-setter bypass |34+| `content.interaction.test.ts` | Test File 12, T-IT01–T-IT06 | pure function |35+| `content.policy.test.ts` | Test File 11, T-PO01–T-PO06 | real scraping; see deviations |36+| `content.dismissal.test.ts` | Test File 1, T-D01–T-D15 | 3-layer model via seams + fake timers |37+| `content.field-interaction.test.ts` | Test File 2, T-F01–T-F12 | mocks `./inject` + `./content-ml-detector` |38+| `content.initial-scan.test.ts` | Test File 3, T-S01–T-S10 | mocks `./inject` (icon spy) |39+| `content.message-routing.test.ts` | Test File 4, T-M01–T-M10 | drives captured `onMessage` listeners |40+| `content.field-icon.test.ts` | Test File 6, T-IC01–T-IC08 | real light-DOM button |41+| `content.selector-ui.test.ts` | Test File 5, T-I01–T-I16 | **closed shadow** — mount/dismiss only |42+| `content.totp-toast.addendum.test.ts` | Addendum 2, T-M11–T-M13 | `skipIf` until `showTotpCopyToast` exists |43+44+### Playwright (e2e) → move to `tests/`45+| File | Brief mapping | Destination |46+| --- | --- | --- |47+| `content.real-page.e2e.spec.ts` | Test File 10 / T-028, T-E01–T-E10 | `tests/content.real-page.e2e.spec.ts` |48+| `login-form.fixture.html` | required fixture | `tests/fixtures/login-form.html` (update the `FIXTURE` path + `page.route`/static server) |49+50+## Environment / config changes needed51+52+- **No `environmentMatchGlobs` change required.** Every file uses the per-file53+ `// @vitest-environment jsdom` pragma (the convention already used by54+ `detect.test.ts`, `fill.test.ts`, etc.). The stale glob comment in the55+ committed `content.orchestration.test.ts` does not apply.56+- To run these in the real suite, either (a) move the `*.test.ts` files into57+ `src/content/`, or (b) drop/relax the `exclude: ["src/drafts/**"]` line in58+ `vitest.config.ts`. Until then they only run via `vitest.drafts.config.ts`.59+- **chrome mock:** there is **no** `__mocks__/chrome.ts` in the repo (the brief60+ assumed one). `_setup.ts` provides the mock the content scripts need:61+ `runtime.sendMessage` (promise + callback forms), `runtime.onMessage`62+ (capturable), `runtime.id`, `runtime.getURL`, `storage.session/local`, plus63+ `navigator.clipboard` and a `ResizeObserver` polyfill. Fold this into the64+ project's shared test support during integration.65+66+## `_testSeam` usage67+68+Tests that need module-private state use the existing `_testSeam` export69+(`getLastDismissedField`/`setLastDismissedField`/`isDismissLocked`/70+`DISMISS_LOCK_MS`):71+- `content.dismissal.test.ts` — Layers 1 & 2.72+The other index.ts files drive behavior through the public seams73+(`handleFieldInteraction`, `initialScan`, `wasAlreadyFilled`) and the captured74+`onDismiss`/`onFillFields` callbacks the selector is opened with — **no new75+exports required.**76+77+## Behavioral findings the tests pin (read before "fixing" a failure)78+79+1. **`querySelectorAllDeep` ancestor duplication.** It re-queries every ancestor80+ element, so a field under `html>body>form` appears 3× in81+ `detectLoginFields()`. That inflates the password count, so82+ `isLoginForm(detectLoginFields())` is `false` for a normal single-password83+ login — the page is recognised as fillable via the `isSignupForm` branch of84+ `isAnyCredentialForm` instead. Detect tests therefore assert detection by85+ *membership* and the page gate via `isAnyCredentialForm`, and test the86+ login-vs-signup classifier on clean, hand-built arrays (matching the committed87+ `detect.test.ts`). It also misses shadow-DOM inputs that are *direct* children88+ of a shadow root (only those nested under an element are collected).89+2. **Closed shadow roots.** `showAutofillSelector`, `showTOTPSelector`,90+ `showCardSelector`, and `save.ts`'s banner all mount `attachShadow({ mode:91+ "closed" })`. Their interiors (entry rows, search box, keyboard nav, TOTP92+ chip, Save/Update buttons, homograph warning) are unreachable from any93+ external script. Unit tests assert only mount/dismiss + outbound messages;94+ the interiors are covered by the e2e through effects. **Recommendation:** add95+ a test-only "open shadow in `import.meta.env.MODE==='test'`" seam if richer96+ unit coverage of the selector interior is wanted.97+3. **`wasAlreadyFilled` page-grace is NOT form-scoped** (brief T-D10 assumed it98+ was). For `PAGE_FILL_GRACE_MS` (3 s) it suppresses *every* field; after the99+ window, only the filled form stays locked (`filledForms` is permanent). Tests100+ pin the actual behavior.101+4. **`fillFields` returns `void`** (brief T-FL12 assumed it returns the filled102+ set). Fill-success tracking is the caller's job (`recordFillSuccess`).103+5. **`detectPasswordPolicy` ignores a `minlength` attribute** (brief T-PO01); it104+ reads `maxLength`, the `pattern`, and nearby text. It inspects only the first105+ password field, so "most restrictive across fields" (T-PO06) is not106+ implemented.107+6. **No `DISMISS_AUTOFILL` / `FILL_AND_SUBMIT` messages** in index.ts's listener108+ (brief T-M06/T-M07). The search-palette path is `SUMMON_AUTOFILL` with no109+ form (T-M08 ≡ T-M02 here).110+7. **PCH-01-025 CSS.escape:** jsdom's selector engine *throws* on `CSS.escape`'s111+ output, so the label-resolution benefit is only observable in the e2e. The112+ unit tests pin the resilience invariant (a hostile id never aborts detection;113+ the embedded-quote case fails if the try/catch is removed).114+8. **Benign teardown log.** Importing the real `index.ts` registers a url-watcher115+ `MutationObserver`; its callback can log `location is not defined` once during116+ jsdom teardown. Exit code is 0 and no test fails. Tests flush it per-test117+ where practical.118+119+## Refactoring still needed beyond Phase 1 (to satisfy the brief fully)120+121+| Want | Needs |122+| --- | --- |123+| 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. |124+| 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. |125+| 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). |126+| T-IC07 "suppress icon on `autocomplete=current-password`" | no such policy exists in `injectFieldIcon`; add it to the detect/index gate if desired. |127+128+## How to run (during integration)129+130+```sh131+# drafts only, via the throwaway config (delete the config after moving files):132+npx vitest run --config vitest.drafts.config.ts133+134+# after moving *.test.ts into src/content/ (or relaxing the exclude):135+npx vitest run136+```src/drafts/AUTOFILL-TESTS/content.real-page.e2e.spec.ts+191 −0
@@ -0,0 +1,191 @@1+// DRAFT — Playwright e2e (T-028). Moves to tests/content.real-page.e2e.spec.ts2+// during integration; the fixture login-form.fixture.html moves to3+// tests/fixtures/login-form.html. Named *.spec.ts (Playwright's testMatch) and4+// kept in src/drafts/ so neither the vitest suite nor the drafts runner picks it5+// up. It is NOT run as part of this draft pass — it needs the built extension in6+// dist/ and a served origin.7+//8+// WHY THIS LAYER EXISTS: the inline selector mounts a CLOSED shadow root, so its9+// interior (entry list, arrow/Enter/Escape nav, search filter, TOTP chip) cannot10+// be asserted by reading the DOM from any unit test. The e2e verifies these11+// through EFFECTS instead — the light-DOM icon button appears and is clickable,12+// fields actually receive values, the host appears/disappears, and the13+// dismissal/grace behavior is observed end to end. This is the home for the14+// it.todo cases left in content.selector-ui.test.ts (T-I02–T-I16) and the15+// gesture-dependent dismissal cases (T-D14/T-D15) and save acceptance flows16+// (T-SV06–T-SV09).17+//18+// SETUP ASSUMPTIONS for the integrator:19+// - Serve the fixture from a real origin so domain matching works. Either a20+// tiny static server (http://localhost:<port>/login-form.html) or21+// page.route() interception mapping an https origin to the fixture file.22+// file:// will NOT exercise domain-scoped vault matching correctly.23+// - Seed the vault with one entry whose url matches that origin and whose24+// username/password are known, via the popup or an IMPORT message.25+26+import { test, expect } from "@playwright/test";27+import type { BrowserContext, Page } from "@playwright/test";28+import { fileURLToPath } from "url";29+import { dirname, join } from "path";30+import os from "os";31+import fs from "fs/promises";32+33+const __filename = fileURLToPath(import.meta.url);34+const __dirname = dirname(__filename);35+const EXTENSION_PATH = join(__dirname, "..", "..", "..", "dist");36+// After the move to tests/, this becomes join(__dirname, "fixtures", "login-form.html").37+const FIXTURE = join(__dirname, "login-form.fixture.html");38+const ICON = 'button[aria-label="Peach autofill"]';39+40+async function makeContext(): Promise<BrowserContext> {41+ const { chromium } = await import("playwright");42+ const userDataDir = join(os.tmpdir(), `peach-e2e-${Date.now()}`);43+ await fs.mkdir(userDataDir, { recursive: true });44+ return chromium.launchPersistentContext(userDataDir, {45+ headless: false,46+ args: [`--load-extension=${EXTENSION_PATH}`, `--disable-extensions-except=${EXTENSION_PATH}`],47+ });48+}49+50+// Unlock/create the vault and seed one entry for the fixture origin. Left as a51+// helper skeleton — wire to the project's popup setup (see autofill.spec.ts52+// setupVault) and an IMPORT/ADD_ENTRY for the seeded credential.53+async function setupVaultWithEntry(_ctx: BrowserContext, _origin: string): Promise<void> {54+ // TODO(integration): reuse setupVault() from autofill.spec.ts, then add an55+ // entry { url: origin, username: "alice", password: "s3cret" }.56+}57+58+// Serve the fixture from a real origin. TODO(integration): replace with a static59+// server or page.route mapping; returning the file path is a placeholder so the60+// spec reads end-to-end.61+async function openFixture(ctx: BrowserContext): Promise<{ page: Page; origin: string }> {62+ const page = await ctx.newPage();63+ await page.goto(`file://${FIXTURE}`); // placeholder — see SETUP ASSUMPTIONS64+ return { page, origin: new URL(page.url()).origin };65+}66+67+test.describe("Real-page autofill e2e (T-E01–T-E10)", () => {68+ let ctx: BrowserContext;69+70+ test.beforeAll(async () => {71+ ctx = await makeContext();72+ });73+ test.afterAll(async () => {74+ await ctx?.close();75+ });76+77+ // T-E01: the field icon appears in the password field of the standard form.78+ test("T-E01: field icon appears in the password field", async () => {79+ const { page } = await openFixture(ctx);80+ await expect(page.locator(ICON)).toBeVisible();81+ await page.close();82+ });83+84+ // T-E02: clicking the icon opens the selector (host mounts; closed shadow, so85+ // assert on the host's presence and the absence of a page-level error).86+ test("T-E02: clicking the icon opens the selector for domain-matched entries", async () => {87+ const { page, origin } = await openFixture(ctx);88+ await setupVaultWithEntry(ctx, origin);89+ await page.locator(ICON).click();90+ await expect(page.locator('div[style*="2147483647"]')).toBeVisible();91+ await page.close();92+ });93+94+ // T-E03: selecting the entry fills username and password correctly (the effect95+ // that proves the closed-shadow selection worked).96+ test("T-E03: selecting an entry fills username and password", async () => {97+ const { page, origin } = await openFixture(ctx);98+ await setupVaultWithEntry(ctx, origin);99+ await page.locator(ICON).click();100+ // TODO(integration): activate the first entry. The row is in a closed shadow,101+ // so drive it via keyboard on the focused host (ArrowDown + Enter) and assert102+ // the fill effect rather than locating the row.103+ await page.keyboard.press("Enter");104+ await expect(page.locator("#username")).toHaveValue("alice");105+ await expect(page.locator("#password")).toHaveValue("s3cret");106+ await page.close();107+ });108+109+ // T-E04: after fill, the icon remains but the selector host is gone.110+ test("T-E04: after fill the selector closes, the icon remains", async () => {111+ const { page, origin } = await openFixture(ctx);112+ await setupVaultWithEntry(ctx, origin);113+ await page.locator(ICON).click();114+ await page.keyboard.press("Enter");115+ await expect(page.locator('div[style*="2147483647"]')).toHaveCount(0);116+ await expect(page.locator(ICON)).toBeVisible();117+ await page.close();118+ });119+120+ // T-E05: clicking the same field immediately after fill does NOT reopen121+ // (fill-success grace — the Layer 3 behavior, observed end to end).122+ test("T-E05: clicking the filled field within the grace window does not reopen", async () => {123+ const { page, origin } = await openFixture(ctx);124+ await setupVaultWithEntry(ctx, origin);125+ await page.locator(ICON).click();126+ await page.keyboard.press("Enter");127+ await page.locator("#password").click();128+ await expect(page.locator('div[style*="2147483647"]')).toHaveCount(0);129+ await page.close();130+ });131+132+ // T-E06: after the grace window (>3s), clicking the field opens again.133+ test("T-E06: after the grace window the selector opens again", async () => {134+ const { page, origin } = await openFixture(ctx);135+ await setupVaultWithEntry(ctx, origin);136+ await page.locator(ICON).click();137+ await page.keyboard.press("Enter");138+ await page.waitForTimeout(3200); // past PAGE_FILL_GRACE_MS139+ await page.locator("#password").click();140+ await expect(page.locator('div[style*="2147483647"]')).toBeVisible();141+ await page.close();142+ });143+144+ // T-E07: submitting the form after a fill keeps post-submit suppression active145+ // (no save banner for the just-filled, unchanged credential).146+ test("T-E07: form submit after fill does not re-prompt to save unchanged creds", async () => {147+ const { page, origin } = await openFixture(ctx);148+ await setupVaultWithEntry(ctx, origin);149+ await page.locator(ICON).click();150+ await page.keyboard.press("Enter");151+ await page.evaluate(() => (document.getElementById("login-form") as HTMLFormElement).requestSubmit());152+ // No save/update banner for an unchanged saved credential.153+ await page.waitForTimeout(500);154+ await expect(page.locator('div[style*="2147483647"]')).toHaveCount(0);155+ await page.close();156+ });157+158+ // T-E08: the email-first form (shown) gets an icon on the email field and159+ // opens an email surface.160+ test("T-E08: email-first form gets an email icon and opens", async () => {161+ const { page } = await openFixture(ctx);162+ await page.evaluate(() => ((document.getElementById("email-first-form") as HTMLElement).style.display = "block"));163+ await expect(page.locator(ICON)).toHaveCount(1); // login form's icon; email form adds another after rescan164+ await page.close();165+ });166+167+ // T-E09: revealing the signup form → signup detection (identity surface).168+ test("T-E09: signup form detection shows the identity surface", async () => {169+ const { page } = await openFixture(ctx);170+ await page.evaluate(() => ((document.getElementById("signup-form") as HTMLElement).style.display = "block"));171+ await page.locator("#new-password").click();172+ await expect(page.locator('div[style*="2147483647"]')).toBeVisible();173+ await page.close();174+ });175+176+ // T-E10: a cross-origin iframe on the page must not crash the content script;177+ // the main form still works.178+ test("T-E10: cross-origin iframe present → no crash, main form still fills", async () => {179+ const { page, origin } = await openFixture(ctx);180+ await setupVaultWithEntry(ctx, origin);181+ await page.evaluate(() => {182+ const f = document.createElement("iframe");183+ f.src = "https://example.com/";184+ document.body.appendChild(f);185+ });186+ await page.locator(ICON).first().click();187+ await page.keyboard.press("Enter");188+ await expect(page.locator("#password")).toHaveValue("s3cret");189+ await page.close();190+ });191+});src/drafts/AUTOFILL-TESTS/content.save.test.ts+136 −0
@@ -0,0 +1,136 @@1+// @vitest-environment jsdom2+//3+// Test File 9 — save.ts save-on-submit detection (T-SV01–T-SV10).4+//5+// setupSaveDetection installs a document submit listener that scrapes the6+// submitted username/password, looks up a matching vault entry7+// (AUTOFILL_QUERY → GET_ENTRY_FOR_FILL), and shows a save/update banner unless8+// the credentials already match. The banner itself is a CLOSED shadow root, and9+// its Save/Update buttons (which send ADD_ENTRY/UPDATE_ENTRY) are unreachable10+// from outside — so those acceptance flows are routed to the e2e. What we pin11+// here is the decision: does the banner mount, and is the lookup correct?12+13+import "./_setup";14+import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";15+import { setupSaveDetection } from "../../content/save";16+import { resetChromeMock, setMessageResponder, sentMessages, makeInput } from "./_setup";17+18+// Mounted save/update banner host (top-right, max z-index, closed shadow).19+function bannerHost(): HTMLElement | null {20+ return document.body.querySelector<HTMLElement>('div[style*="2147483647"]');21+}22+23+// Build and submit a form with the given fields; returns after the async24+// save-detection chain settles.25+async function submitForm(fields: { user?: string; pass?: string; type?: string }): Promise<void> {26+ const form = document.createElement("form");27+ document.body.appendChild(form);28+ if (fields.user !== undefined) {29+ const u = makeInput({ type: "text", name: "username", autocomplete: "username" }, form);30+ u.value = fields.user;31+ }32+ if (fields.pass !== undefined) {33+ const p = makeInput({ type: fields.type ?? "password", name: "password" }, form);34+ p.value = fields.pass;35+ }36+ form.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true }));37+ await new Promise((r) => setTimeout(r, 30)); // let the AUTOFILL_QUERY/GET_ENTRY roundtrip settle38+}39+40+beforeAll(() => {41+ setupSaveDetection(); // one document submit listener for the whole file42+});43+44+beforeEach(() => {45+ document.body.innerHTML = "";46+ resetChromeMock();47+});48+afterEach(() => {49+ document.body.innerHTML = "";50+});51+52+describe("T-SV01, T-SV03: banner mounts for new and changed credentials", () => {53+ // T-SV01: submitting credentials with no matching vault entry → save banner.54+ it("T-SV01: new credentials → save banner mounts", async () => {55+ setMessageResponder((msg) => (msg.type === "AUTOFILL_QUERY" ? { ok: true, data: [] } : { ok: true, data: null }));56+ await submitForm({ user: "alice", pass: "s3cret" });57+ expect(bannerHost()).not.toBeNull();58+ });59+60+ // T-SV03: submitting a changed password for an existing entry → update banner.61+ it("T-SV03: changed password for an existing entry → banner mounts (update)", async () => {62+ setMessageResponder((msg) => {63+ if (msg.type === "AUTOFILL_QUERY") return { ok: true, data: [{ id: "e1" }] };64+ if (msg.type === "GET_ENTRY_FOR_FILL") return { ok: true, data: { id: "e1", title: "Acme", password: "old-password" } };65+ return { ok: true, data: null };66+ });67+ await submitForm({ user: "alice", pass: "new-password" });68+ expect(bannerHost()).not.toBeNull();69+ });70+});71+72+describe("T-SV02, T-SV04, T-SV05: no banner cases", () => {73+ // T-SV02: the submitted password already matches the saved entry → no banner.74+ it("T-SV02: credentials already saved (password matches) → no banner", async () => {75+ setMessageResponder((msg) => {76+ if (msg.type === "AUTOFILL_QUERY") return { ok: true, data: [{ id: "e1" }] };77+ if (msg.type === "GET_ENTRY_FOR_FILL") return { ok: true, data: { id: "e1", title: "Acme", password: "s3cret" } };78+ return { ok: true, data: null };79+ });80+ await submitForm({ user: "alice", pass: "s3cret" });81+ expect(bannerHost()).toBeNull();82+ });83+84+ // T-SV04: a search form has no password field — save detection ignores it and85+ // never even runs the vault lookup.86+ it("T-SV04: search form (no password) → no banner, no vault lookup", async () => {87+ setMessageResponder(() => ({ ok: true, data: [] }));88+ const form = document.createElement("form");89+ document.body.appendChild(form);90+ const q = makeInput({ type: "search", name: "q" }, form);91+ q.value = "kittens";92+ form.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true }));93+ await new Promise((r) => setTimeout(r, 30));94+ expect(bannerHost()).toBeNull();95+ expect(sentMessages.some((m) => m.type === "AUTOFILL_QUERY")).toBe(false);96+ });97+98+ // T-SV05: an empty password short-circuits before any lookup.99+ it("T-SV05: empty password → no banner, no vault lookup", async () => {100+ setMessageResponder(() => ({ ok: true, data: [] }));101+ await submitForm({ user: "alice", pass: "" });102+ expect(bannerHost()).toBeNull();103+ expect(sentMessages.some((m) => m.type === "AUTOFILL_QUERY")).toBe(false);104+ });105+});106+107+describe("T-SV10: domain-matched lookup (MV-1 surface)", () => {108+ // T-SV10: the lookup queries AUTOFILL_QUERY with the page domain, then fetches109+ // the full entry by the returned id via GET_ENTRY_FOR_FILL.110+ it("T-SV10: sends AUTOFILL_QUERY{domain} then GET_ENTRY_FOR_FILL{id}", async () => {111+ setMessageResponder((msg) => {112+ if (msg.type === "AUTOFILL_QUERY") return { ok: true, data: [{ id: "e1" }] };113+ if (msg.type === "GET_ENTRY_FOR_FILL") return { ok: true, data: { id: "e1", title: "Acme", password: "old" } };114+ return { ok: true, data: null };115+ });116+ await submitForm({ user: "alice", pass: "new" });117+118+ const query = sentMessages.find((m) => m.type === "AUTOFILL_QUERY");119+ expect(query?.payload?.domain).toBeTruthy();120+ const fetch = sentMessages.find((m) => m.type === "GET_ENTRY_FOR_FILL");121+ expect(fetch?.payload?.id).toBe("e1");122+ });123+});124+125+// ── Closed-shadow / lifecycle boundaries (T-SV06–T-SV09) ─────────────────────126+describe("T-SV06–T-SV09: documented boundaries", () => {127+ // The Save/Dismiss buttons and the homograph warning render inside the128+ // banner's CLOSED shadow root; the accept/dismiss flows (which send129+ // ADD_ENTRY/UPDATE_ENTRY), the popup-lifecycle suppression, and the homograph130+ // warning copy are all verified through the Playwright e2e. See131+ // INTEGRATION-CONTRACT.md.132+ it.todo("T-SV06: user accepts the banner → ADD_ENTRY sent (closed shadow → e2e)");133+ it.todo("T-SV07: user dismisses the banner → nothing saved (closed shadow → e2e)");134+ it.todo("T-SV08: banner does not reappear after popup lifecycle break (e2e)");135+ it.todo("T-SV09: homograph warning shown for non-ASCII domain (closed shadow → e2e)");136+});src/drafts/AUTOFILL-TESTS/login-form.fixture.html+32 −0
@@ -0,0 +1,32 @@1+<!DOCTYPE html>2+<!--3+ DRAFT FIXTURE — moves to tests/fixtures/login-form.html during integration.4+ Referenced by content.real-page.e2e.spec.ts (T-E01–T-E10).5+-->6+<html>7+<head><title>Login Fixture</title></head>8+<body>9+ <!-- Standard login form -->10+ <form id="login-form" action="/submit" method="post">11+ <label for="username">Username</label>12+ <input type="text" id="username" name="username" autocomplete="username" />13+ <label for="password">Password</label>14+ <input type="password" id="password" name="password" autocomplete="current-password" />15+ <button type="submit" id="submit-btn">Log in</button>16+ </form>17+18+ <!-- Email-first form (separate) -->19+ <form id="email-first-form" style="display:none">20+ <input type="email" id="email" name="email" autocomplete="email" />21+ <button type="submit">Next</button>22+ </form>23+24+ <!-- Signup form (separate) -->25+ <form id="signup-form" style="display:none">26+ <input type="text" id="new-username" name="username" />27+ <input type="password" id="new-password" name="password" autocomplete="new-password" />28+ <input type="password" id="confirm-password" name="confirm" autocomplete="new-password" />29+ <button type="submit">Create account</button>30+ </form>31+</body>32+</html>