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
↓ peach-browser-649e7841.tar.gzatlas at this save
files changed2 files
tests/content.real-page.e2e.spec.ts+189 −0tests/fixtures/login-form.html+32 −0tests/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+});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>