import { chromium, test, expect, type BrowserContext, type Page } from "@playwright/test"; import { createServer, type Server } from "http"; import { readFile } from "fs/promises"; 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-chrome"); const FIXTURE_PATH = join(__dirname, "fixtures", "passkeys-smoke.html"); async function startFixtureServer(): Promise<{ server: Server; url: string }> { const html = await readFile(FIXTURE_PATH, "utf-8"); const server = createServer((_, res) => { res.writeHead(200, { "content-type": "text/html; charset=utf-8" }); res.end(html); }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); const address = server.address(); if (!address || typeof address === "string") { throw new Error("Unable to determine fixture server port"); } return { server, url: `http://localhost:${address.port}` }; } async function stopFixtureServer(server: Server): Promise { await new Promise((resolve, reject) => { server.close((error) => (error ? reject(error) : resolve())); }); } async function makeContext() { const userDataDir = join( os.tmpdir(), `peach-passkeys-${Date.now()}-${Math.random().toString(36).slice(2)}` ); await fs.mkdir(userDataDir, { recursive: true }); return chromium.launchPersistentContext(userDataDir, { headless: false, args: [ `--load-extension=${EXTENSION_PATH}`, `--disable-extensions-except=${EXTENSION_PATH}`, "--no-sandbox", "--disable-setuid-sandbox", ], }); } async function waitForExtensionId(ctx: BrowserContext, maxWait = 15_000): Promise { const start = Date.now(); let sw = ctx.serviceWorkers()[0]; while (!sw && Date.now() - start < maxWait) { await new Promise((resolve) => setTimeout(resolve, 250)); sw = ctx.serviceWorkers()[0]; } if (!sw) throw new Error(`No service worker after ${maxWait}ms`); return new URL(sw.url()).host; } async function openPopup(ctx: BrowserContext, extId: string): Promise { const popup = await ctx.newPage(); await popup.goto(`chrome-extension://${extId}/src/popup/index.html`); await popup.waitForTimeout(1500); return popup; } async function createVaultAndEnablePasskeys(popup: Page) { // Drive setup over messages in a single service-worker activation: create the // vault, UNLOCK (which persists the session key so the proxy sees an unlocked // vault on its next wake), enable the provider, and disable lock-on-close so // the approval popup's lifecycle can't lock the vault mid-ceremony. The old // UI-click setup was flaky because createVault alone does not persist a // session key, so a respawned worker handled the ceremony locked. const result = await popup.evaluate(async () => { const send = (m: object) => new Promise<{ ok?: boolean }>((res) => chrome.runtime.sendMessage(m, res)); const create = await send({ type: "CREATE_VAULT", payload: { password: "passkeysmoke123" }, }); const unlock = await send({ type: "UNLOCK", payload: { password: "passkeysmoke123" }, }); await send({ type: "UPDATE_SETTINGS", payload: { lockOnClose: false } }); const enable = await send({ type: "SET_PASSKEY_PROVIDER_ENABLED", payload: { enabled: true }, }); return { create, unlock, enable }; }); expect(result.create.ok).toBe(true); expect(result.unlock.ok).toBe(true); expect(result.enable.ok).toBe(true); } async function approvePopupRequest( ctx: BrowserContext, extId: string, buttonName: RegExp, expectedTitle: RegExp ) { // Open a fresh popup to catch the pending-request approval surface. The popup // is left open (closed at context teardown); lock-on-close is disabled during // setup so its eventual disconnect cannot lock the vault or deny the request. const popup = await openPopup(ctx, extId); await expect(popup.getByRole("heading", { name: expectedTitle })).toBeVisible({ timeout: 15_000, }); await popup.getByRole("button", { name: buttonName }).click(); } test.describe("Passkey Smoke", () => { let ctx: BrowserContext; let server: Server; let url: string; test.beforeEach(async () => { ({ server, url } = await startFixtureServer()); ctx = await makeContext(); }); test.afterEach(async () => { if (ctx) await ctx.close(); if (server) await stopFixtureServer(server); }); test("create() and get() resolve through the Peach extension provider", async () => { const extId = await waitForExtensionId(ctx); const popup = await openPopup(ctx, extId); await createVaultAndEnablePasskeys(popup); const page = await ctx.newPage(); await page.goto(url); await page.getByRole("button", { name: /create passkey/i }).click(); await approvePopupRequest(ctx, extId, /save passkey/i, /save a passkey\?/i); const createStatus = page.locator("#create-status"); await expect(createStatus).toHaveAttribute("data-state", "ok", { timeout: 15_000, }); await expect(createStatus).toContainText("Created"); await page.getByRole("button", { name: /authenticate/i }).click(); await approvePopupRequest(ctx, extId, /sign in/i, /sign in with passkey\?/i); const getStatus = page.locator("#get-status"); await expect(getStatus).toHaveAttribute("data-state", "ok", { timeout: 15_000, }); await expect(getStatus).toContainText("Authenticated"); const refreshedPopup = await openPopup(ctx, extId); await refreshedPopup.getByLabel("Search vault entries").click(); await expect(refreshedPopup.locator("text=Peach Passkey Smoke").first()).toBeVisible({ timeout: 10_000, }); await expect(refreshedPopup.locator("text=passkey").first()).toBeVisible(); }); });