content.real-page.e2e.spec.ts

8.9 KB · raw

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.2425import { test, expect } from "@playwright/test";26import type { BrowserContext, Page } from "@playwright/test";27import { fileURLToPath } from "url";28import { dirname, join } from "path";29import os from "os";30import fs from "fs/promises";3132const __filename = fileURLToPath(import.meta.url);33const __dirname = dirname(__filename);34const EXTENSION_PATH = join(__dirname, "..", "dist");35const FIXTURE = join(__dirname, "fixtures", "login-form.html");36const ICON = 'button[aria-label="Peach autofill"]';3738async 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}4748// 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.51async 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}5556// 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.59async 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}6465test.describe("Real-page autofill e2e (T-E01–T-E10)", () => {66  let ctx: BrowserContext;6768  test.beforeAll(async () => {69    ctx = await makeContext();70  });71  test.afterAll(async () => {72    await ctx?.close();73  });7475  // 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  });8182  // 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  });9192  // 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  });106107  // 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  });117118  // 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  });129130  // 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  });141142  // 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  });155156  // 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  });164165  // 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  });173174  // 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});