// DRAFT — Playwright e2e (T-028). Moves to tests/content.real-page.e2e.spec.ts // during integration; the fixture login-form.fixture.html moves to // tests/fixtures/login-form.html. Named *.spec.ts (Playwright's testMatch) and // kept in src/drafts/ so neither the vitest suite nor the drafts runner picks it // up. It is NOT run as part of this draft pass — it needs the built extension in // dist/ and a served origin. // // WHY THIS LAYER EXISTS: the inline selector mounts a CLOSED shadow root, so its // interior (entry list, arrow/Enter/Escape nav, search filter, TOTP chip) cannot // be asserted by reading the DOM from any unit test. The e2e verifies these // through EFFECTS instead — the light-DOM icon button appears and is clickable, // fields actually receive values, the host appears/disappears, and the // dismissal/grace behavior is observed end to end. This is the home for the // it.todo cases left in content.selector-ui.test.ts (T-I02–T-I16) and the // gesture-dependent dismissal cases (T-D14/T-D15) and save acceptance flows // (T-SV06–T-SV09). // // SETUP ASSUMPTIONS for the integrator: // - Serve the fixture from a real origin so domain matching works. Either a // tiny static server (http://localhost:/login-form.html) or // page.route() interception mapping an https origin to the fixture file. // file:// will NOT exercise domain-scoped vault matching correctly. // - Seed the vault with one entry whose url matches that origin and whose // username/password are known, via the popup or an IMPORT message. import { test, expect } from "@playwright/test"; import type { BrowserContext, Page } from "@playwright/test"; import { fileURLToPath } from "url"; import { dirname, join } from "path"; import os from "os"; import fs from "fs/promises"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const EXTENSION_PATH = join(__dirname, "..", "..", "..", "dist"); // After the move to tests/, this becomes join(__dirname, "fixtures", "login-form.html"). const FIXTURE = join(__dirname, "login-form.fixture.html"); const ICON = 'button[aria-label="Peach autofill"]'; async function makeContext(): Promise { const { chromium } = await import("playwright"); const userDataDir = join(os.tmpdir(), `peach-e2e-${Date.now()}`); await fs.mkdir(userDataDir, { recursive: true }); return chromium.launchPersistentContext(userDataDir, { headless: false, args: [`--load-extension=${EXTENSION_PATH}`, `--disable-extensions-except=${EXTENSION_PATH}`], }); } // Unlock/create the vault and seed one entry for the fixture origin. Left as a // helper skeleton — wire to the project's popup setup (see autofill.spec.ts // setupVault) and an IMPORT/ADD_ENTRY for the seeded credential. async function setupVaultWithEntry(_ctx: BrowserContext, _origin: string): Promise { // TODO(integration): reuse setupVault() from autofill.spec.ts, then add an // entry { url: origin, username: "alice", password: "s3cret" }. } // Serve the fixture from a real origin. TODO(integration): replace with a static // server or page.route mapping; returning the file path is a placeholder so the // spec reads end-to-end. async function openFixture(ctx: BrowserContext): Promise<{ page: Page; origin: string }> { const page = await ctx.newPage(); await page.goto(`file://${FIXTURE}`); // placeholder — see SETUP ASSUMPTIONS return { page, origin: new URL(page.url()).origin }; } test.describe("Real-page autofill e2e (T-E01–T-E10)", () => { let ctx: BrowserContext; test.beforeAll(async () => { ctx = await makeContext(); }); test.afterAll(async () => { await ctx?.close(); }); // T-E01: the field icon appears in the password field of the standard form. test("T-E01: field icon appears in the password field", async () => { const { page } = await openFixture(ctx); await expect(page.locator(ICON)).toBeVisible(); await page.close(); }); // T-E02: clicking the icon opens the selector (host mounts; closed shadow, so // assert on the host's presence and the absence of a page-level error). test("T-E02: clicking the icon opens the selector for domain-matched entries", async () => { const { page, origin } = await openFixture(ctx); await setupVaultWithEntry(ctx, origin); await page.locator(ICON).click(); await expect(page.locator('div[style*="2147483647"]')).toBeVisible(); await page.close(); }); // T-E03: selecting the entry fills username and password correctly (the effect // that proves the closed-shadow selection worked). test("T-E03: selecting an entry fills username and password", async () => { const { page, origin } = await openFixture(ctx); await setupVaultWithEntry(ctx, origin); await page.locator(ICON).click(); // TODO(integration): activate the first entry. The row is in a closed shadow, // so drive it via keyboard on the focused host (ArrowDown + Enter) and assert // the fill effect rather than locating the row. await page.keyboard.press("Enter"); await expect(page.locator("#username")).toHaveValue("alice"); await expect(page.locator("#password")).toHaveValue("s3cret"); await page.close(); }); // T-E04: after fill, the icon remains but the selector host is gone. test("T-E04: after fill the selector closes, the icon remains", async () => { const { page, origin } = await openFixture(ctx); await setupVaultWithEntry(ctx, origin); await page.locator(ICON).click(); await page.keyboard.press("Enter"); await expect(page.locator('div[style*="2147483647"]')).toHaveCount(0); await expect(page.locator(ICON)).toBeVisible(); await page.close(); }); // T-E05: clicking the same field immediately after fill does NOT reopen // (fill-success grace — the Layer 3 behavior, observed end to end). test("T-E05: clicking the filled field within the grace window does not reopen", async () => { const { page, origin } = await openFixture(ctx); await setupVaultWithEntry(ctx, origin); await page.locator(ICON).click(); await page.keyboard.press("Enter"); await page.locator("#password").click(); await expect(page.locator('div[style*="2147483647"]')).toHaveCount(0); await page.close(); }); // T-E06: after the grace window (>3s), clicking the field opens again. test("T-E06: after the grace window the selector opens again", async () => { const { page, origin } = await openFixture(ctx); await setupVaultWithEntry(ctx, origin); await page.locator(ICON).click(); await page.keyboard.press("Enter"); await page.waitForTimeout(3200); // past PAGE_FILL_GRACE_MS await page.locator("#password").click(); await expect(page.locator('div[style*="2147483647"]')).toBeVisible(); await page.close(); }); // T-E07: submitting the form after a fill keeps post-submit suppression active // (no save banner for the just-filled, unchanged credential). test("T-E07: form submit after fill does not re-prompt to save unchanged creds", async () => { const { page, origin } = await openFixture(ctx); await setupVaultWithEntry(ctx, origin); await page.locator(ICON).click(); await page.keyboard.press("Enter"); await page.evaluate(() => (document.getElementById("login-form") as HTMLFormElement).requestSubmit()); // No save/update banner for an unchanged saved credential. await page.waitForTimeout(500); await expect(page.locator('div[style*="2147483647"]')).toHaveCount(0); await page.close(); }); // T-E08: the email-first form (shown) gets an icon on the email field and // opens an email surface. test("T-E08: email-first form gets an email icon and opens", async () => { const { page } = await openFixture(ctx); await page.evaluate(() => ((document.getElementById("email-first-form") as HTMLElement).style.display = "block")); await expect(page.locator(ICON)).toHaveCount(1); // login form's icon; email form adds another after rescan await page.close(); }); // T-E09: revealing the signup form → signup detection (identity surface). test("T-E09: signup form detection shows the identity surface", async () => { const { page } = await openFixture(ctx); await page.evaluate(() => ((document.getElementById("signup-form") as HTMLElement).style.display = "block")); await page.locator("#new-password").click(); await expect(page.locator('div[style*="2147483647"]')).toBeVisible(); await page.close(); }); // T-E10: a cross-origin iframe on the page must not crash the content script; // the main form still works. test("T-E10: cross-origin iframe present → no crash, main form still fills", async () => { const { page, origin } = await openFixture(ctx); await setupVaultWithEntry(ctx, origin); await page.evaluate(() => { const f = document.createElement("iframe"); f.src = "https://example.com/"; document.body.appendChild(f); }); await page.locator(ICON).first().click(); await page.keyboard.press("Enter"); await expect(page.locator("#password")).toHaveValue("s3cret"); await page.close(); }); });