1import { chromium, test, expect, type BrowserContext, type Page } from "@playwright/test";2import { createServer, type Server } from "http";3import { readFile } from "fs/promises";4import { fileURLToPath } from "url";5import { dirname, join } from "path";6import os from "os";7import fs from "fs/promises";89const __filename = fileURLToPath(import.meta.url);10const __dirname = dirname(__filename);11const EXTENSION_PATH = join(__dirname, "..", "dist-chrome");12const FIXTURE_PATH = join(__dirname, "fixtures", "passkeys-smoke.html");1314async function startFixtureServer(): Promise<{ server: Server; url: string }> {15 const html = await readFile(FIXTURE_PATH, "utf-8");16 const server = createServer((_, res) => {17 res.writeHead(200, { "content-type": "text/html; charset=utf-8" });18 res.end(html);19 });2021 await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));22 const address = server.address();23 if (!address || typeof address === "string") {24 throw new Error("Unable to determine fixture server port");25 }2627 return { server, url: `http://localhost:${address.port}` };28}2930async function stopFixtureServer(server: Server): Promise<void> {31 await new Promise<void>((resolve, reject) => {32 server.close((error) => (error ? reject(error) : resolve()));33 });34}3536async function makeContext() {37 const userDataDir = join(38 os.tmpdir(),39 `peach-passkeys-${Date.now()}-${Math.random().toString(36).slice(2)}`40 );41 await fs.mkdir(userDataDir, { recursive: true });42 return chromium.launchPersistentContext(userDataDir, {43 headless: false,44 args: [45 `--load-extension=${EXTENSION_PATH}`,46 `--disable-extensions-except=${EXTENSION_PATH}`,47 "--no-sandbox",48 "--disable-setuid-sandbox",49 ],50 });51}5253async function waitForExtensionId(ctx: BrowserContext, maxWait = 15_000): Promise<string> {54 const start = Date.now();55 let sw = ctx.serviceWorkers()[0];56 while (!sw && Date.now() - start < maxWait) {57 await new Promise((resolve) => setTimeout(resolve, 250));58 sw = ctx.serviceWorkers()[0];59 }60 if (!sw) throw new Error(`No service worker after ${maxWait}ms`);61 return new URL(sw.url()).host;62}6364async function openPopup(ctx: BrowserContext, extId: string): Promise<Page> {65 const popup = await ctx.newPage();66 await popup.goto(`chrome-extension://${extId}/src/popup/index.html`);67 await popup.waitForTimeout(1500);68 return popup;69}7071async function createVaultAndEnablePasskeys(popup: Page) {72 73 74 75 76 77 78 const result = await popup.evaluate(async () => {79 const send = (m: object) =>80 new Promise<{ ok?: boolean }>((res) => chrome.runtime.sendMessage(m, res));81 const create = await send({82 type: "CREATE_VAULT",83 payload: { password: "passkeysmoke123" },84 });85 const unlock = await send({86 type: "UNLOCK",87 payload: { password: "passkeysmoke123" },88 });89 await send({ type: "UPDATE_SETTINGS", payload: { lockOnClose: false } });90 const enable = await send({91 type: "SET_PASSKEY_PROVIDER_ENABLED",92 payload: { enabled: true },93 });94 return { create, unlock, enable };95 });9697 expect(result.create.ok).toBe(true);98 expect(result.unlock.ok).toBe(true);99 expect(result.enable.ok).toBe(true);100}101102async function approvePopupRequest(103 ctx: BrowserContext,104 extId: string,105 buttonName: RegExp,106 expectedTitle: RegExp107) {108 109 110 111 const popup = await openPopup(ctx, extId);112 await expect(popup.getByRole("heading", { name: expectedTitle })).toBeVisible({113 timeout: 15_000,114 });115 await popup.getByRole("button", { name: buttonName }).click();116}117118test.describe("Passkey Smoke", () => {119 let ctx: BrowserContext;120 let server: Server;121 let url: string;122123 test.beforeEach(async () => {124 ({ server, url } = await startFixtureServer());125 ctx = await makeContext();126 });127128 test.afterEach(async () => {129 if (ctx) await ctx.close();130 if (server) await stopFixtureServer(server);131 });132133 test("create() and get() resolve through the Peach extension provider", async () => {134 const extId = await waitForExtensionId(ctx);135 const popup = await openPopup(ctx, extId);136 await createVaultAndEnablePasskeys(popup);137138 const page = await ctx.newPage();139 await page.goto(url);140141 await page.getByRole("button", { name: /create passkey/i }).click();142 await approvePopupRequest(ctx, extId, /save passkey/i, /save a passkey\?/i);143144 const createStatus = page.locator("#create-status");145 await expect(createStatus).toHaveAttribute("data-state", "ok", {146 timeout: 15_000,147 });148 await expect(createStatus).toContainText("Created");149150 await page.getByRole("button", { name: /authenticate/i }).click();151 await approvePopupRequest(ctx, extId, /sign in/i, /sign in with passkey\?/i);152153 const getStatus = page.locator("#get-status");154 await expect(getStatus).toHaveAttribute("data-state", "ok", {155 timeout: 15_000,156 });157 await expect(getStatus).toContainText("Authenticated");158159 const refreshedPopup = await openPopup(ctx, extId);160 await refreshedPopup.getByLabel("Search vault entries").click();161 await expect(refreshedPopup.locator("text=Peach Passkey Smoke").first()).toBeVisible({162 timeout: 10_000,163 });164 await expect(refreshedPopup.locator("text=passkey").first()).toBeVisible();165 });166});