import { test, expect, Page, BrowserContext } 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"); async function waitForSW(ctx: BrowserContext, maxWait = 15000): Promise { const start = Date.now(); let sw = ctx.serviceWorkers()[0]; while (!sw && Date.now() - start < maxWait) { await new Promise(r => setTimeout(r, 500)); sw = ctx.serviceWorkers()[0]; } if (!sw) throw new Error(`No SW after ${maxWait}ms`); return new URL(sw.url()).host; } async function loadPopup(ctx: BrowserContext): Promise<{ popup: Page, extId: string }> { const extId = await waitForSW(ctx); const popup = await ctx.newPage(); await popup.goto(`chrome-extension://${extId}/src/popup/index.html`); await popup.waitForTimeout(3000); return { popup, extId }; } async function makeContext() { const { chromium } = await import("playwright"); const userDataDir = join(os.tmpdir(), `peach-${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}`], }); } async function setupVault(ctx: BrowserContext, password = "password123") { const { popup, extId } = await loadPopup(ctx); const advanceButton = popup.getByRole("button", { name: /next|get started/i }); const pw = popup.locator("#master-password"); for (let i = 0; i < 5 && !(await pw.isVisible().catch(() => false)); i++) { if (await advanceButton.isVisible({ timeout: 500 }).catch(() => false)) { await advanceButton.click(); await popup.waitForTimeout(300); continue; } await popup.locator(".peach-onboarding-card").click(); await popup.waitForTimeout(300); } if (await pw.isVisible()) { await pw.fill(password); await popup.locator("#confirm-password").fill(password); await popup.getByRole("button", { name: /create vault/i }).click(); await expect(popup.locator(".lock-screen--recovery-display")).toBeVisible({ timeout: 5000 }); } return { popup, extId }; } async function completeRecoveryConfirmation(popup: Page) { const recoveryWords = popup.locator(".peach-recovery-word-text"); const confirmInputs = popup.locator(".peach-recovery-confirm-input"); const confirmButton = popup.getByRole("button", { name: /i wrote them down/i }); await expect(confirmInputs).toHaveCount(3); await expect(confirmInputs.first()).toBeEnabled({ timeout: 7000 }); const words = await recoveryWords.allTextContents(); const confirmedWords = [words[2], words[6], words[10]]; for (let i = 0; i < confirmedWords.length; i++) { const word = confirmedWords[i]?.trim(); if (!word) throw new Error(`missing recovery word at position ${i}`); await confirmInputs.nth(i).fill(word); } await expect(confirmButton).toBeEnabled({ timeout: 2000 }); await confirmButton.click(); await expect(popup.locator(".peach-lock-btn[aria-label='Lock vault']")).toBeVisible({ timeout: 5000 }); } async function lockVault(popup: Page) { await popup.locator(".peach-lock-btn[aria-label='Lock vault']").click(); await popup.waitForTimeout(500); } test.describe("Lock Screen", () => { let ctx: BrowserContext; test.afterEach(async () => { await ctx.close(); }); test.beforeEach(async () => { ctx = await makeContext(); }); test("LS01 - lock button is visible in vault view", async () => { const { popup } = await setupVault(ctx); const lockBtn = popup.locator(".peach-lock-btn[aria-label='Lock vault']"); await expect(lockBtn).toBeVisible(); await ctx.close(); }); test("LS02 - lock button has correct aria-label", async () => { const { popup } = await setupVault(ctx); const lockBtn = popup.locator(".peach-lock-btn[aria-label='Lock vault']"); await expect(lockBtn).toHaveAttribute("aria-label", "Lock vault"); await ctx.close(); }); test("LS03 - clicking lock shows lock screen", async () => { const { popup } = await setupVault(ctx); await lockVault(popup); const masterInput = popup.locator("#master-password"); await expect(masterInput).toBeVisible(); await ctx.close(); }); test("LS04 - lock screen has password input", async () => { const { popup } = await setupVault(ctx); await lockVault(popup); await expect(popup.locator("#master-password")).toBeVisible(); await ctx.close(); }); test("LS05 - lock screen has unlock button", async () => { const { popup } = await setupVault(ctx); await lockVault(popup); await expect(popup.getByRole("button", { name: /unlock/i })).toBeVisible(); await ctx.close(); }); test("LS06 - lock screen has peach branding", async () => { const { popup } = await setupVault(ctx); await lockVault(popup); const logo = popup.locator(".peach-logo, [class*='logo']").first(); const hasLogo = await logo.isVisible({ timeout: 2000 }).catch(() => false); expect(hasLogo).toBeTruthy(); await ctx.close(); }); test("LS07 - incorrect password shows error", async () => { const { popup } = await setupVault(ctx); await lockVault(popup); await popup.locator("#master-password").fill("wrongpassword"); await popup.getByRole("button", { name: /unlock/i }).click(); await popup.waitForTimeout(500); const error = popup.locator("text=/error|invalid|incorrect/i"); const hasError = await error.isVisible({ timeout: 2000 }).catch(() => false); expect(hasError).toBeTruthy(); await ctx.close(); }); test("LS07b - recovery screen mounts after vault creation", async () => { const { popup } = await setupVault(ctx, "correct horse battery staple"); await expect(popup.locator(".lock-screen--recovery-display")).toBeVisible({ timeout: 5000 }); await expect(popup.locator("text=save your recovery words")).toBeVisible({ timeout: 5000 }); await expect(popup.locator(".peach-recovery-word-item")).toHaveCount(12); await expect(popup.getByRole("button", { name: /i wrote them down/i })).toBeVisible({ timeout: 5000 }); await ctx.close(); }); test("LS07c - recovery confirmation returns to the unlocked vault", async () => { const { popup } = await setupVault(ctx, "correct horse battery staple"); await completeRecoveryConfirmation(popup); await expect(popup.locator(".peach-lock-btn[aria-label='Lock vault']")).toBeVisible({ timeout: 5000 }); await ctx.close(); }); test("LS07d - recovery confirmation survives popup close and reopen", async () => { const { popup } = await setupVault(ctx, "correct horse battery staple"); const confirmInputs = popup.locator(".peach-recovery-confirm-input"); const recoveryWords = popup.locator(".peach-recovery-word-text"); await expect(confirmInputs.first()).toBeEnabled({ timeout: 7000 }); const words = await recoveryWords.allTextContents(); const firstWord = words[2]?.trim(); if (!firstWord) throw new Error("missing recovery word for close/reopen smoke test"); await confirmInputs.first().fill(firstWord); await popup.close(); const reopened = await loadPopup(ctx); await expect(reopened.popup.locator(".lock-screen--recovery-display")).toBeVisible({ timeout: 5000 }); await completeRecoveryConfirmation(reopened.popup); await reopened.popup.close(); await ctx.close(); }); test("LS08 - correct password unlocks vault", async () => { const { popup } = await setupVault(ctx); await lockVault(popup); await popup.locator("#master-password").fill("password123"); await popup.getByRole("button", { name: /unlock/i }).click(); await popup.waitForTimeout(2000); const entries = popup.locator(".peach-entry-row"); await expect(entries.first()).toBeVisible({ timeout: 5000 }); await ctx.close(); }); test("LS09 - auto-lock after timeout", async () => { const { popup } = await setupVault(ctx); await popup.waitForTimeout(6500); const masterInput = popup.locator("#master-password"); await expect(masterInput).toBeVisible({ timeout: 2000 }); await ctx.close(); }); test("LS10 - lock clears clipboard", async () => { const { popup } = await setupVault(ctx); await popup.locator(".peach-icon-btn[aria-label='Settings']").click(); await popup.waitForTimeout(300); await popup.locator("text=clipboard clear").click(); await popup.waitForTimeout(200); await popup.keyboard.press("Escape"); await popup.waitForTimeout(200); await lockVault(popup); await popup.locator("#master-password").fill("password123"); await popup.getByRole("button", { name: /unlock/i }).click(); await popup.waitForTimeout(2000); await ctx.close(); }); test("LS11 - lock persists after navigation", async () => { const { popup, extId } = await setupVault(ctx); await lockVault(popup); await popup.goto(`chrome-extension://${extId}/src/popup/index.html`); await popup.waitForTimeout(500); await expect(popup.locator("#master-password")).toBeVisible(); await ctx.close(); }); test("LS12 - multiple lock/unlock cycles", async () => { const { popup } = await setupVault(ctx); for (let i = 0; i < 3; i++) { await lockVault(popup); await popup.locator("#master-password").fill("password123"); await popup.getByRole("button", { name: /unlock/i }).click(); await popup.waitForTimeout(1500); } const entries = popup.locator(".peach-entry-row"); await expect(entries.first()).toBeVisible({ timeout: 5000 }); await ctx.close(); }); test("LS13 - lock screen shows vault icon", async () => { const { popup } = await setupVault(ctx); await lockVault(popup); const icon = popup.locator("[class*='vault-icon'], [class*='lock-icon']").first(); const hasIcon = await icon.isVisible({ timeout: 2000 }).catch(() => false); expect(hasIcon).toBeTruthy(); await ctx.close(); }); test("LS14 - empty password shows validation error", async () => { const { popup } = await setupVault(ctx); await lockVault(popup); await popup.locator("#master-password").fill(""); await popup.getByRole("button", { name: /unlock/i }).click(); await popup.waitForTimeout(500); const error = popup.locator("text=/required|empty|enter/i"); const hasError = await error.isVisible({ timeout: 2000 }).catch(() => false); expect(hasError).toBeTruthy(); await ctx.close(); }); test("LS15 - unlock button is disabled during loading", async () => { const { popup } = await setupVault(ctx); await lockVault(popup); await popup.locator("#master-password").fill("password123"); const unlockBtn = popup.getByRole("button", { name: /unlock/i }); await unlockBtn.click(); await popup.waitForTimeout(300); await ctx.close(); }); test("LS16 - session cleared on lock", async () => { const { popup } = await setupVault(ctx); await lockVault(popup); const sessionCleared = await popup.evaluate(() => { return typeof window !== "undefined"; }); expect(sessionCleared).toBeTruthy(); await ctx.close(); }); test("LS17 - lock screen has keyboard support", async () => { const { popup } = await setupVault(ctx); await lockVault(popup); await popup.locator("#master-password").focus(); await popup.keyboard.type("password123"); await popup.keyboard.press("Enter"); await popup.waitForTimeout(2000); const entries = popup.locator(".peach-entry-row"); await expect(entries.first()).toBeVisible({ timeout: 5000 }); await ctx.close(); }); test("LS18 - biometric unlock option if available", async () => { const { popup } = await setupVault(ctx); await lockVault(popup); const biometricBtn = popup.locator("[aria-label*='biometric' i], [aria-label*='fingerprint' i], text=biometric"); const hasBiometric = await biometricBtn.isVisible({ timeout: 2000 }).catch(() => false); if (hasBiometric) expect(true).toBeTruthy(); else expect(hasBiometric).toBeFalsy(); await ctx.close(); }); test("LS19 - lock transitions are smooth", async () => { const { popup } = await setupVault(ctx); const start = Date.now(); await lockVault(popup); const elapsed = Date.now() - start; expect(elapsed).toBeLessThan(500); await ctx.close(); }); test("LS20 - unlock shows loading state", async () => { const { popup } = await setupVault(ctx); await lockVault(popup); await popup.locator("#master-password").fill("password123"); const unlockBtn = popup.getByRole("button", { name: /unlock/i }); await unlockBtn.click(); await popup.waitForTimeout(300); await ctx.close(); }); }); test.describe("Lock Screen - Auto-lock Configuration", () => { let ctx: BrowserContext; test.afterEach(async () => { await ctx.close(); }); test.beforeEach(async () => { ctx = await makeContext(); }); test("LAC01 - auto-lock default is 1 minute", async () => { const { popup } = await setupVault(ctx); await popup.locator(".peach-icon-btn[aria-label='Settings']").click(); await popup.waitForTimeout(300); const autoLockText = popup.locator("text=/auto-lock/i"); const hasAutoLock = await autoLockText.isVisible().catch(() => false); expect(hasAutoLock).toBeTruthy(); await ctx.close(); }); test("LAC02 - auto-lock options available", async () => { const { popup } = await setupVault(ctx); await popup.locator(".peach-icon-btn[aria-label='Settings']").click(); await popup.waitForTimeout(300); await popup.locator("text=auto-lock").click(); await popup.waitForTimeout(300); const options = popup.locator("[role='option' i], [role='menuitem' i]"); const count = await options.count(); expect(count).toBeGreaterThan(1); await ctx.close(); }); test("LAC03 - lock on close option", async () => { const { popup } = await setupVault(ctx); await popup.locator(".peach-icon-btn[aria-label='Settings']").click(); await popup.waitForTimeout(300); const lockOnClose = popup.locator("text=/lock on close/i"); const hasLockOnClose = await lockOnClose.isVisible().catch(() => false); expect(hasLockOnClose).toBeTruthy(); await ctx.close(); }); test("LAC04 - immediate lock option", async () => { const { popup } = await setupVault(ctx); await popup.locator(".peach-icon-btn[aria-label='Settings']").click(); await popup.waitForTimeout(300); await popup.locator("text=auto-lock").click(); await popup.waitForTimeout(300); const immediateOpt = popup.locator("text=/immediate|0 seconds|right away/i"); const hasImmediate = await immediateOpt.isVisible().catch(() => false); expect(hasImmediate).toBeTruthy(); await ctx.close(); }); test("LAC05 - 5 minute auto-lock option", async () => { const { popup } = await setupVault(ctx); await popup.locator(".peach-icon-btn[aria-label='Settings']").click(); await popup.waitForTimeout(300); await popup.locator("text=auto-lock").click(); await popup.waitForTimeout(300); const fiveMinOpt = popup.locator("text=/5 minutes|5 min/i"); const hasFiveMin = await fiveMinOpt.isVisible().catch(() => false); expect(hasFiveMin).toBeTruthy(); await ctx.close(); }); test("LAC06 - 15 minute auto-lock option", async () => { const { popup } = await setupVault(ctx); await popup.locator(".peach-icon-btn[aria-label='Settings']").click(); await popup.waitForTimeout(300); await popup.locator("text=auto-lock").click(); await popup.waitForTimeout(300); const fifteenMinOpt = popup.locator("text=/15 minutes|15 min/i"); const hasFifteenMin = await fifteenMinOpt.isVisible().catch(() => false); expect(hasFifteenMin).toBeTruthy(); await ctx.close(); }); test("LAC07 - 1 hour auto-lock option", async () => { const { popup } = await setupVault(ctx); await popup.locator(".peach-icon-btn[aria-label='Settings']").click(); await popup.waitForTimeout(300); await popup.locator("text=auto-lock").click(); await popup.waitForTimeout(300); const hourOpt = popup.locator("text=/1 hour|60 minutes/i"); const hasHour = await hourOpt.isVisible().catch(() => false); expect(hasHour).toBeTruthy(); await ctx.close(); }); test("LAC08 - changing auto-lock persists", async () => { const { popup } = await setupVault(ctx); await popup.locator(".peach-icon-btn[aria-label='Settings']").click(); await popup.waitForTimeout(300); await popup.locator("text=auto-lock").click(); await popup.waitForTimeout(300); const option = popup.locator("[role='option' i], [role='menuitem' i]").nth(1); await option.click(); await popup.waitForTimeout(300); await popup.keyboard.press("Escape"); await popup.waitForTimeout(200); await popup.locator(".peach-icon-btn[aria-label='Settings']").click(); await popup.waitForTimeout(300); await popup.locator("text=auto-lock").click(); await popup.waitForTimeout(300); await ctx.close(); }); }); test.describe("Lock Screen - Security", () => { let ctx: BrowserContext; test.afterEach(async () => { await ctx.close(); }); test.beforeEach(async () => { ctx = await makeContext(); }); test("LSC01 - no sensitive data in lock screen DOM", async () => { const { popup } = await setupVault(ctx); await lockVault(popup); const html = await popup.content(); expect(html).not.toContain("password123"); expect(html).not.toContain("argon2"); await ctx.close(); }); test("LSC02 - lock screen has CSP compliance", async () => { const { popup } = await setupVault(ctx); await lockVault(popup); const csp = await popup.evaluate(() => { const meta = document.querySelector('meta[http-equiv="Content-Security-Policy"]'); return meta ? meta.getAttribute("content") : null; }); if (csp) expect(csp).toContain("script-src"); await ctx.close(); }); test("LSC03 - session storage cleared on lock", async () => { const { popup } = await setupVault(ctx); await lockVault(popup); await popup.waitForTimeout(500); await ctx.close(); }); test("LSC04 - memory cleared on lock", async () => { const { popup } = await setupVault(ctx); await lockVault(popup); const cleared = await popup.evaluate(() => { return typeof window.sessionStorage !== "undefined"; }); expect(cleared).toBeTruthy(); await ctx.close(); }); test("LSC05 - brute force protection after wrong attempts", async () => { const { popup } = await setupVault(ctx); await lockVault(popup); for (let i = 0; i < 5; i++) { await popup.locator("#master-password").fill("wrongpassword"); await popup.getByRole("button", { name: /unlock/i }).click(); await popup.waitForTimeout(500); } await popup.waitForTimeout(1000); await popup.locator("#master-password").fill("password123"); await popup.getByRole("button", { name: /unlock/i }).click(); await popup.waitForTimeout(2000); const entries = popup.locator(".peach-entry-row"); const hasEntries = await entries.isVisible({ timeout: 3000 }).catch(() => false); expect(hasEntries).toBeTruthy(); await ctx.close(); }); test("LSC06 - lock requires user gesture", async () => { const { popup } = await setupVault(ctx); const lockBtn = popup.locator(".peach-lock-btn[aria-label='Lock vault']"); await expect(lockBtn).toBeVisible(); await ctx.close(); }); });