649e78412625

test: autofill real-page e2e + login-form fixture — content.real-page.e2e.spec.ts→tests/, login-form.fixture.html→tests/fixtures/login-form.html; fix EXTENSION_PATH (3-up→1-up) + FIXTURE path; Playwright collects 10 tests (T-E01-10), vault-seed/served-origin still integration TODO per contract (v3.9.9)

time
2026-06-12 13:51 UTC · 4 months ago
machine
koh2
agent
claude-opus-4-8
session
autofill tests integration
parent
ce63d2b47335
2 files tests/content.real-page.e2e.spec.ts+189 −0tests/fixtures/login-form.html+32 −0
tests/content.real-page.e2e.spec.ts+189 −0
@@ -0,0 +1,189 @@1+// Playwright e2e (T-028). Named *.spec.ts (Playwright's testMatch). Requires the2+// built extension in dist/ (npm run build:e2e) and a served origin — it is not3+// part of the vitest unit suite. The SETUP ASSUMPTIONS below are still open4+// integration TODOs (vault seeding + a real served origin); until they are wired5+// the cases are skeletons that document the intended end-to-end coverage.6+//7+// WHY THIS LAYER EXISTS: the inline selector mounts a CLOSED shadow root, so its8+// interior (entry list, arrow/Enter/Escape nav, search filter, TOTP chip) cannot9+// be asserted by reading the DOM from any unit test. The e2e verifies these10+// through EFFECTS instead — the light-DOM icon button appears and is clickable,11+// fields actually receive values, the host appears/disappears, and the12+// dismissal/grace behavior is observed end to end. This is the home for the13+// it.todo cases left in content.selector-ui.test.ts (T-I02–T-I16) and the14+// gesture-dependent dismissal cases (T-D14/T-D15) and save acceptance flows15+// (T-SV06–T-SV09).16+//17+// SETUP ASSUMPTIONS for the integrator:18+//   - Serve the fixture from a real origin so domain matching works. Either a19+//     tiny static server (http://localhost:<port>/login-form.html) or20+//     page.route() interception mapping an https origin to the fixture file.21+//     file:// will NOT exercise domain-scoped vault matching correctly.22+//   - Seed the vault with one entry whose url matches that origin and whose23+//     username/password are known, via the popup or an IMPORT message.24+25+import { test, expect } from "@playwright/test";26+import type { BrowserContext, Page } from "@playwright/test";27+import { fileURLToPath } from "url";28+import { dirname, join } from "path";29+import os from "os";30+import fs from "fs/promises";31+32+const __filename = fileURLToPath(import.meta.url);33+const __dirname = dirname(__filename);34+const EXTENSION_PATH = join(__dirname, "..", "dist");35+const FIXTURE = join(__dirname, "fixtures", "login-form.html");36+const ICON = 'button[aria-label="Peach autofill"]';37+38+async function makeContext(): Promise<BrowserContext> {39+  const { chromium } = await import("playwright");40+  const userDataDir = join(os.tmpdir(), `peach-e2e-${Date.now()}`);41+  await fs.mkdir(userDataDir, { recursive: true });42+  return chromium.launchPersistentContext(userDataDir, {43+    headless: false,44+    args: [`--load-extension=${EXTENSION_PATH}`, `--disable-extensions-except=${EXTENSION_PATH}`],45+  });46+}47+48+// Unlock/create the vault and seed one entry for the fixture origin. Left as a49+// helper skeleton — wire to the project's popup setup (see autofill.spec.ts50+// setupVault) and an IMPORT/ADD_ENTRY for the seeded credential.51+async function setupVaultWithEntry(_ctx: BrowserContext, _origin: string): Promise<void> {52+  // TODO(integration): reuse setupVault() from autofill.spec.ts, then add an53+  // entry { url: origin, username: "alice", password: "s3cret" }.54+}55+56+// Serve the fixture from a real origin. TODO(integration): replace with a static57+// server or page.route mapping; returning the file path is a placeholder so the58+// spec reads end-to-end.59+async function openFixture(ctx: BrowserContext): Promise<{ page: Page; origin: string }> {60+  const page = await ctx.newPage();61+  await page.goto(`file://${FIXTURE}`); // placeholder — see SETUP ASSUMPTIONS62+  return { page, origin: new URL(page.url()).origin };63+}64+65+test.describe("Real-page autofill e2e (T-E01–T-E10)", () => {66+  let ctx: BrowserContext;67+68+  test.beforeAll(async () => {69+    ctx = await makeContext();70+  });71+  test.afterAll(async () => {72+    await ctx?.close();73+  });74+75+  // T-E01: the field icon appears in the password field of the standard form.76+  test("T-E01: field icon appears in the password field", async () => {77+    const { page } = await openFixture(ctx);78+    await expect(page.locator(ICON)).toBeVisible();79+    await page.close();80+  });81+82+  // T-E02: clicking the icon opens the selector (host mounts; closed shadow, so83+  // assert on the host's presence and the absence of a page-level error).84+  test("T-E02: clicking the icon opens the selector for domain-matched entries", async () => {85+    const { page, origin } = await openFixture(ctx);86+    await setupVaultWithEntry(ctx, origin);87+    await page.locator(ICON).click();88+    await expect(page.locator('div[style*="2147483647"]')).toBeVisible();89+    await page.close();90+  });91+92+  // T-E03: selecting the entry fills username and password correctly (the effect93+  // that proves the closed-shadow selection worked).94+  test("T-E03: selecting an entry fills username and password", async () => {95+    const { page, origin } = await openFixture(ctx);96+    await setupVaultWithEntry(ctx, origin);97+    await page.locator(ICON).click();98+    // TODO(integration): activate the first entry. The row is in a closed shadow,99+    // so drive it via keyboard on the focused host (ArrowDown + Enter) and assert100+    // the fill effect rather than locating the row.101+    await page.keyboard.press("Enter");102+    await expect(page.locator("#username")).toHaveValue("alice");103+    await expect(page.locator("#password")).toHaveValue("s3cret");104+    await page.close();105+  });106+107+  // T-E04: after fill, the icon remains but the selector host is gone.108+  test("T-E04: after fill the selector closes, the icon remains", async () => {109+    const { page, origin } = await openFixture(ctx);110+    await setupVaultWithEntry(ctx, origin);111+    await page.locator(ICON).click();112+    await page.keyboard.press("Enter");113+    await expect(page.locator('div[style*="2147483647"]')).toHaveCount(0);114+    await expect(page.locator(ICON)).toBeVisible();115+    await page.close();116+  });117+118+  // T-E05: clicking the same field immediately after fill does NOT reopen119+  // (fill-success grace — the Layer 3 behavior, observed end to end).120+  test("T-E05: clicking the filled field within the grace window does not reopen", async () => {121+    const { page, origin } = await openFixture(ctx);122+    await setupVaultWithEntry(ctx, origin);123+    await page.locator(ICON).click();124+    await page.keyboard.press("Enter");125+    await page.locator("#password").click();126+    await expect(page.locator('div[style*="2147483647"]')).toHaveCount(0);127+    await page.close();128+  });129+130+  // T-E06: after the grace window (>3s), clicking the field opens again.131+  test("T-E06: after the grace window the selector opens again", async () => {132+    const { page, origin } = await openFixture(ctx);133+    await setupVaultWithEntry(ctx, origin);134+    await page.locator(ICON).click();135+    await page.keyboard.press("Enter");136+    await page.waitForTimeout(3200); // past PAGE_FILL_GRACE_MS137+    await page.locator("#password").click();138+    await expect(page.locator('div[style*="2147483647"]')).toBeVisible();139+    await page.close();140+  });141+142+  // T-E07: submitting the form after a fill keeps post-submit suppression active143+  // (no save banner for the just-filled, unchanged credential).144+  test("T-E07: form submit after fill does not re-prompt to save unchanged creds", async () => {145+    const { page, origin } = await openFixture(ctx);146+    await setupVaultWithEntry(ctx, origin);147+    await page.locator(ICON).click();148+    await page.keyboard.press("Enter");149+    await page.evaluate(() => (document.getElementById("login-form") as HTMLFormElement).requestSubmit());150+    // No save/update banner for an unchanged saved credential.151+    await page.waitForTimeout(500);152+    await expect(page.locator('div[style*="2147483647"]')).toHaveCount(0);153+    await page.close();154+  });155+156+  // T-E08: the email-first form (shown) gets an icon on the email field and157+  // opens an email surface.158+  test("T-E08: email-first form gets an email icon and opens", async () => {159+    const { page } = await openFixture(ctx);160+    await page.evaluate(() => ((document.getElementById("email-first-form") as HTMLElement).style.display = "block"));161+    await expect(page.locator(ICON)).toHaveCount(1); // login form's icon; email form adds another after rescan162+    await page.close();163+  });164+165+  // T-E09: revealing the signup form → signup detection (identity surface).166+  test("T-E09: signup form detection shows the identity surface", async () => {167+    const { page } = await openFixture(ctx);168+    await page.evaluate(() => ((document.getElementById("signup-form") as HTMLElement).style.display = "block"));169+    await page.locator("#new-password").click();170+    await expect(page.locator('div[style*="2147483647"]')).toBeVisible();171+    await page.close();172+  });173+174+  // T-E10: a cross-origin iframe on the page must not crash the content script;175+  // the main form still works.176+  test("T-E10: cross-origin iframe present → no crash, main form still fills", async () => {177+    const { page, origin } = await openFixture(ctx);178+    await setupVaultWithEntry(ctx, origin);179+    await page.evaluate(() => {180+      const f = document.createElement("iframe");181+      f.src = "https://example.com/";182+      document.body.appendChild(f);183+    });184+    await page.locator(ICON).first().click();185+    await page.keyboard.press("Enter");186+    await expect(page.locator("#password")).toHaveValue("s3cret");187+    await page.close();188+  });189+});

view file

tests/fixtures/login-form.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>

view file