content.real-page.e2e.spec.ts

9.0 KB · raw

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.2526import { test, expect } from "@playwright/test";27import type { BrowserContext, Page } from "@playwright/test";28import { fileURLToPath } from "url";29import { dirname, join } from "path";30import os from "os";31import fs from "fs/promises";3233const __filename = fileURLToPath(import.meta.url);34const __dirname = dirname(__filename);35const EXTENSION_PATH = join(__dirname, "..", "..", "..", "dist");36// After the move to tests/, this becomes join(__dirname, "fixtures", "login-form.html").37const FIXTURE = join(__dirname, "login-form.fixture.html");38const ICON = 'button[aria-label="Peach autofill"]';3940async 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}4950// 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.53async 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}5758// 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.61async 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}6667test.describe("Real-page autofill e2e (T-E01–T-E10)", () => {68  let ctx: BrowserContext;6970  test.beforeAll(async () => {71    ctx = await makeContext();72  });73  test.afterAll(async () => {74    await ctx?.close();75  });7677  // 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  });8384  // 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  });9394  // 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  });108109  // 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  });119120  // 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  });131132  // 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  });143144  // 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  });157158  // 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  });166167  // 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  });175176  // 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});