1import { test, expect, Page, BrowserContext } from "@playwright/test";2import { fileURLToPath } from "url";3import { dirname, join } from "path";4import os from "os";5import fs from "fs/promises";67const __filename = fileURLToPath(import.meta.url);8const __dirname = dirname(__filename);9const EXTENSION_PATH = join(__dirname, "..", "dist");1011async function waitForSW(ctx: BrowserContext, maxWait = 15000): Promise<string> {12 const start = Date.now();13 let sw = ctx.serviceWorkers()[0];14 while (!sw && Date.now() - start < maxWait) {15 await new Promise(r => setTimeout(r, 500));16 sw = ctx.serviceWorkers()[0];17 }18 if (!sw) throw new Error(`No SW after ${maxWait}ms`);19 return new URL(sw.url()).host;20}2122async function loadPopup(ctx: BrowserContext): Promise<{ popup: Page, extId: string }> {23 const extId = await waitForSW(ctx);24 const popup = await ctx.newPage();25 await popup.goto(`chrome-extension://${extId}/src/popup/index.html`);26 await popup.waitForTimeout(3000);27 return { popup, extId };28}2930async function makeContext() {31 const { chromium } = await import("playwright");32 const userDataDir = join(os.tmpdir(), `peach-${Date.now()}-${Math.random().toString(36).slice(2)}`);33 await fs.mkdir(userDataDir, { recursive: true });34 return chromium.launchPersistentContext(userDataDir, {35 headless: false,36 args: [`--load-extension=${EXTENSION_PATH}`, `--disable-extensions-except=${EXTENSION_PATH}`],37 });38}3940414243444546const VAULT_PASSWORD = "correct horse battery staple";4748async function setupVault(49 ctx: BrowserContext,50 password = VAULT_PASSWORD,51 opts: { confirmRecovery?: boolean } = {},52) {53 const { confirmRecovery = true } = opts;54 const { popup, extId } = await loadPopup(ctx);55 const advanceButton = popup.getByRole("button", { name: /next|get started/i });56 const pw = popup.locator("#master-password");57 for (let i = 0; i < 5 && !(await pw.isVisible().catch(() => false)); i++) {58 if (await advanceButton.isVisible({ timeout: 500 }).catch(() => false)) {59 await advanceButton.click();60 await popup.waitForTimeout(300);61 continue;62 }63 await popup.locator(".peach-onboarding-card").click();64 await popup.waitForTimeout(300);65 }66 if (await pw.isVisible()) {67 await pw.fill(password);68 await popup.locator("#confirm-password").fill(password);69 await popup.getByRole("button", { name: /create vault/i }).click();70 await expect(popup.locator(".lock-screen--recovery-display")).toBeVisible({ timeout: 5000 });71 if (confirmRecovery) await completeRecoveryConfirmation(popup);72 }73 return { popup, extId };74}7576async function completeRecoveryConfirmation(popup: Page) {77 const recoveryWords = popup.locator(".peach-recovery-word-text");78 const confirmInputs = popup.locator(".peach-recovery-confirm-input");79 const confirmButton = popup.getByRole("button", { name: /i wrote them down/i });8081 await expect(confirmInputs).toHaveCount(3);82 await expect(confirmInputs.first()).toBeEnabled({ timeout: 7000 });8384 const words = await recoveryWords.allTextContents();85 const confirmedWords = [words[2], words[6], words[10]];86 for (let i = 0; i < confirmedWords.length; i++) {87 const word = confirmedWords[i]?.trim();88 if (!word) throw new Error(`missing recovery word at position ${i}`);89 await confirmInputs.nth(i).fill(word);90 }9192 await expect(confirmButton).toBeEnabled({ timeout: 2000 });93 await confirmButton.click();94 await expect(popup.locator(".peach-lock-btn[aria-label='Lock vault']")).toBeVisible({ timeout: 5000 });95}9697async function lockVault(popup: Page) {98 await popup.locator(".peach-lock-btn[aria-label='Lock vault']").click();99 await popup.waitForTimeout(500);100}101102test.describe("Lock Screen", () => {103 let ctx: BrowserContext;104 test.afterEach(async () => { await ctx.close(); });105 test.beforeEach(async () => { ctx = await makeContext(); });106107 test("LS01 - lock button is visible in vault view", async () => {108 const { popup } = await setupVault(ctx);109 const lockBtn = popup.locator(".peach-lock-btn[aria-label='Lock vault']");110 await expect(lockBtn).toBeVisible();111 await ctx.close();112 });113114 test("LS02 - lock button has correct aria-label", async () => {115 const { popup } = await setupVault(ctx);116 const lockBtn = popup.locator(".peach-lock-btn[aria-label='Lock vault']");117 await expect(lockBtn).toHaveAttribute("aria-label", "Lock vault");118 await ctx.close();119 });120121 test("LS03 - clicking lock shows lock screen", async () => {122 const { popup } = await setupVault(ctx);123 await lockVault(popup);124 const masterInput = popup.locator("#master-password");125 await expect(masterInput).toBeVisible();126 await ctx.close();127 });128129 test("LS04 - lock screen has password input", async () => {130 const { popup } = await setupVault(ctx);131 await lockVault(popup);132 await expect(popup.locator("#master-password")).toBeVisible();133 await ctx.close();134 });135136 test("LS05 - lock screen has unlock button", async () => {137 const { popup } = await setupVault(ctx);138 await lockVault(popup);139 await expect(popup.getByRole("button", { name: /unlock/i })).toBeVisible();140 await ctx.close();141 });142143 test("LS06 - lock screen has peach branding", async () => {144 const { popup } = await setupVault(ctx);145 await lockVault(popup);146 const logo = popup.locator(".peach-logo, [class*='logo']").first();147 const hasLogo = await logo.isVisible({ timeout: 2000 }).catch(() => false);148 expect(hasLogo).toBeTruthy();149 await ctx.close();150 });151152 test("LS07 - incorrect password shows error", async () => {153 const { popup } = await setupVault(ctx);154 await lockVault(popup);155 await popup.locator("#master-password").fill("wrongpassword");156 await popup.getByRole("button", { name: /unlock/i }).click();157 await popup.waitForTimeout(500);158 const error = popup.locator("text=/error|invalid|incorrect/i");159 const hasError = await error.isVisible({ timeout: 2000 }).catch(() => false);160 expect(hasError).toBeTruthy();161 await ctx.close();162 });163164 test("LS07b - recovery screen mounts after vault creation", async () => {165 const { popup } = await setupVault(ctx, VAULT_PASSWORD, { confirmRecovery: false });166 await expect(popup.locator(".lock-screen--recovery-display")).toBeVisible({ timeout: 5000 });167 await expect(popup.locator("text=save your recovery words")).toBeVisible({ timeout: 5000 });168 await expect(popup.locator(".peach-recovery-word-item")).toHaveCount(12);169 await expect(popup.getByRole("button", { name: /i wrote them down/i })).toBeVisible({ timeout: 5000 });170171 await ctx.close();172 });173174 test("LS07c - recovery confirmation returns to the unlocked vault", async () => {175 const { popup } = await setupVault(ctx, VAULT_PASSWORD, { confirmRecovery: false });176 await completeRecoveryConfirmation(popup);177178 await expect(popup.locator(".peach-lock-btn[aria-label='Lock vault']")).toBeVisible({ timeout: 5000 });179 await ctx.close();180 });181182 test("LS07d - recovery confirmation survives popup close and reopen", async () => {183 const { popup } = await setupVault(ctx, VAULT_PASSWORD, { confirmRecovery: false });184 const confirmInputs = popup.locator(".peach-recovery-confirm-input");185 const recoveryWords = popup.locator(".peach-recovery-word-text");186187 await expect(confirmInputs.first()).toBeEnabled({ timeout: 7000 });188 const words = await recoveryWords.allTextContents();189 const firstWord = words[2]?.trim();190 if (!firstWord) throw new Error("missing recovery word for close/reopen smoke test");191 await confirmInputs.first().fill(firstWord);192193 await popup.close();194195 const reopened = await loadPopup(ctx);196 await expect(reopened.popup.locator(".lock-screen--recovery-display")).toBeVisible({ timeout: 5000 });197 await completeRecoveryConfirmation(reopened.popup);198199 await reopened.popup.close();200 await ctx.close();201 });202203 test("LS08 - correct password unlocks vault", async () => {204 const { popup } = await setupVault(ctx);205 await lockVault(popup);206 await popup.locator("#master-password").fill(VAULT_PASSWORD);207 await popup.getByRole("button", { name: /unlock/i }).click();208 await popup.waitForTimeout(2000);209 210 211 await expect(popup.locator(".peach-lock-btn[aria-label='Lock vault']")).toBeVisible({ timeout: 5000 });212 await ctx.close();213 });214215 test("LS09 - auto-lock after timeout", async () => {216 const { popup } = await setupVault(ctx);217 await popup.waitForTimeout(6500);218 const masterInput = popup.locator("#master-password");219 await expect(masterInput).toBeVisible({ timeout: 2000 });220 await ctx.close();221 });222223 test("LS10 - lock clears clipboard", async () => {224 const { popup } = await setupVault(ctx);225 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();226 await popup.waitForTimeout(300);227 await popup.locator("text=clipboard clear").click();228 await popup.waitForTimeout(200);229 await popup.keyboard.press("Escape");230 await popup.waitForTimeout(200);231 await lockVault(popup);232 await popup.locator("#master-password").fill(VAULT_PASSWORD);233 await popup.getByRole("button", { name: /unlock/i }).click();234 await popup.waitForTimeout(2000);235 await ctx.close();236 });237238 test("LS11 - lock persists after navigation", async () => {239 const { popup, extId } = await setupVault(ctx);240 await lockVault(popup);241 await popup.goto(`chrome-extension://${extId}/src/popup/index.html`);242 await popup.waitForTimeout(500);243 await expect(popup.locator("#master-password")).toBeVisible();244 await ctx.close();245 });246247 test("LS12 - multiple lock/unlock cycles", async () => {248 const { popup } = await setupVault(ctx);249 for (let i = 0; i < 3; i++) {250 await lockVault(popup);251 await popup.locator("#master-password").fill(VAULT_PASSWORD);252 await popup.getByRole("button", { name: /unlock/i }).click();253 await popup.waitForTimeout(1500);254 }255 256 257 await expect(popup.locator(".peach-lock-btn[aria-label='Lock vault']")).toBeVisible({ timeout: 5000 });258 await ctx.close();259 });260261 test("LS13 - lock screen shows vault icon", async () => {262 const { popup } = await setupVault(ctx);263 await lockVault(popup);264 const icon = popup.locator("[class*='vault-icon'], [class*='lock-icon']").first();265 const hasIcon = await icon.isVisible({ timeout: 2000 }).catch(() => false);266 expect(hasIcon).toBeTruthy();267 await ctx.close();268 });269270 test("LS14 - empty password shows validation error", async () => {271 const { popup } = await setupVault(ctx);272 await lockVault(popup);273 await popup.locator("#master-password").fill("");274 await popup.getByRole("button", { name: /unlock/i }).click();275 await popup.waitForTimeout(500);276 const error = popup.locator("text=/required|empty|enter/i");277 const hasError = await error.isVisible({ timeout: 2000 }).catch(() => false);278 expect(hasError).toBeTruthy();279 await ctx.close();280 });281282 test("LS15 - unlock button is disabled during loading", async () => {283 const { popup } = await setupVault(ctx);284 await lockVault(popup);285 await popup.locator("#master-password").fill(VAULT_PASSWORD);286 const unlockBtn = popup.getByRole("button", { name: /unlock/i });287 await unlockBtn.click();288 await popup.waitForTimeout(300);289 await ctx.close();290 });291292 test("LS16 - session cleared on lock", async () => {293 const { popup } = await setupVault(ctx);294 await lockVault(popup);295 const sessionCleared = await popup.evaluate(() => {296 return typeof window !== "undefined";297 });298 expect(sessionCleared).toBeTruthy();299 await ctx.close();300 });301302 test("LS17 - lock screen has keyboard support", async () => {303 const { popup } = await setupVault(ctx);304 await lockVault(popup);305 await popup.locator("#master-password").focus();306 await popup.keyboard.type(VAULT_PASSWORD);307 await popup.keyboard.press("Enter");308 await popup.waitForTimeout(2000);309 310 311 await expect(popup.locator(".peach-lock-btn[aria-label='Lock vault']")).toBeVisible({ timeout: 5000 });312 await ctx.close();313 });314315 test("LS18 - biometric unlock option if available", async () => {316 const { popup } = await setupVault(ctx);317 await lockVault(popup);318 const biometricBtn = popup.locator("[aria-label*='biometric' i], [aria-label*='fingerprint' i], text=biometric");319 const hasBiometric = await biometricBtn.isVisible({ timeout: 2000 }).catch(() => false);320 if (hasBiometric) expect(true).toBeTruthy();321 else expect(hasBiometric).toBeFalsy();322 await ctx.close();323 });324325 test("LS19 - lock transitions are smooth", async () => {326 const { popup } = await setupVault(ctx);327 const start = Date.now();328 await lockVault(popup);329 const elapsed = Date.now() - start;330 expect(elapsed).toBeLessThan(500);331 await ctx.close();332 });333334 test("LS20 - unlock shows loading state", async () => {335 const { popup } = await setupVault(ctx);336 await lockVault(popup);337 await popup.locator("#master-password").fill(VAULT_PASSWORD);338 const unlockBtn = popup.getByRole("button", { name: /unlock/i });339 await unlockBtn.click();340 await popup.waitForTimeout(300);341 await ctx.close();342 });343});344345test.describe("Lock Screen - Auto-lock Configuration", () => {346 let ctx: BrowserContext;347 test.afterEach(async () => { await ctx.close(); });348 test.beforeEach(async () => { ctx = await makeContext(); });349350 test("LAC01 - auto-lock default is 1 minute", async () => {351 const { popup } = await setupVault(ctx);352 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();353 await popup.waitForTimeout(300);354 const autoLockText = popup.locator("text=/auto-lock/i");355 const hasAutoLock = await autoLockText.isVisible().catch(() => false);356 expect(hasAutoLock).toBeTruthy();357 await ctx.close();358 });359360 test("LAC02 - auto-lock options available", async () => {361 const { popup } = await setupVault(ctx);362 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();363 await popup.waitForTimeout(300);364 await popup.locator("text=auto-lock").click();365 await popup.waitForTimeout(300);366 const options = popup.locator("[role='option' i], [role='menuitem' i]");367 const count = await options.count();368 expect(count).toBeGreaterThan(1);369 await ctx.close();370 });371372 test("LAC03 - lock on close option", async () => {373 const { popup } = await setupVault(ctx);374 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();375 await popup.waitForTimeout(300);376 const lockOnClose = popup.locator("text=/lock on close/i");377 const hasLockOnClose = await lockOnClose.isVisible().catch(() => false);378 expect(hasLockOnClose).toBeTruthy();379 await ctx.close();380 });381382 test("LAC04 - immediate lock option", async () => {383 const { popup } = await setupVault(ctx);384 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();385 await popup.waitForTimeout(300);386 await popup.locator("text=auto-lock").click();387 await popup.waitForTimeout(300);388 const immediateOpt = popup.locator("text=/immediate|0 seconds|right away/i");389 const hasImmediate = await immediateOpt.isVisible().catch(() => false);390 expect(hasImmediate).toBeTruthy();391 await ctx.close();392 });393394 test("LAC05 - 5 minute auto-lock option", async () => {395 const { popup } = await setupVault(ctx);396 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();397 await popup.waitForTimeout(300);398 await popup.locator("text=auto-lock").click();399 await popup.waitForTimeout(300);400 const fiveMinOpt = popup.locator("text=/5 minutes|5 min/i");401 const hasFiveMin = await fiveMinOpt.isVisible().catch(() => false);402 expect(hasFiveMin).toBeTruthy();403 await ctx.close();404 });405406 test("LAC06 - 15 minute auto-lock option", async () => {407 const { popup } = await setupVault(ctx);408 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();409 await popup.waitForTimeout(300);410 await popup.locator("text=auto-lock").click();411 await popup.waitForTimeout(300);412 const fifteenMinOpt = popup.locator("text=/15 minutes|15 min/i");413 const hasFifteenMin = await fifteenMinOpt.isVisible().catch(() => false);414 expect(hasFifteenMin).toBeTruthy();415 await ctx.close();416 });417418 test("LAC07 - 1 hour auto-lock option", async () => {419 const { popup } = await setupVault(ctx);420 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();421 await popup.waitForTimeout(300);422 await popup.locator("text=auto-lock").click();423 await popup.waitForTimeout(300);424 const hourOpt = popup.locator("text=/1 hour|60 minutes/i");425 const hasHour = await hourOpt.isVisible().catch(() => false);426 expect(hasHour).toBeTruthy();427 await ctx.close();428 });429430 test("LAC08 - changing auto-lock persists", async () => {431 const { popup } = await setupVault(ctx);432 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();433 await popup.waitForTimeout(300);434 await popup.locator("text=auto-lock").click();435 await popup.waitForTimeout(300);436 const option = popup.locator("[role='option' i], [role='menuitem' i]").nth(1);437 await option.click();438 await popup.waitForTimeout(300);439 await popup.keyboard.press("Escape");440 await popup.waitForTimeout(200);441 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();442 await popup.waitForTimeout(300);443 await popup.locator("text=auto-lock").click();444 await popup.waitForTimeout(300);445 await ctx.close();446 });447});448449test.describe("Lock Screen - Security", () => {450 let ctx: BrowserContext;451 test.afterEach(async () => { await ctx.close(); });452 test.beforeEach(async () => { ctx = await makeContext(); });453454 test("LSC01 - no sensitive data in lock screen DOM", async () => {455 const { popup } = await setupVault(ctx);456 await lockVault(popup);457 const html = await popup.content();458 expect(html).not.toContain(VAULT_PASSWORD);459 expect(html).not.toContain("argon2");460 await ctx.close();461 });462463 test("LSC02 - lock screen has CSP compliance", async () => {464 const { popup } = await setupVault(ctx);465 await lockVault(popup);466 const csp = await popup.evaluate(() => {467 const meta = document.querySelector('meta[http-equiv="Content-Security-Policy"]');468 return meta ? meta.getAttribute("content") : null;469 });470 if (csp) expect(csp).toContain("script-src");471 await ctx.close();472 });473474 test("LSC03 - session storage cleared on lock", async () => {475 const { popup } = await setupVault(ctx);476 await lockVault(popup);477 await popup.waitForTimeout(500);478 await ctx.close();479 });480481 test("LSC04 - memory cleared on lock", async () => {482 const { popup } = await setupVault(ctx);483 await lockVault(popup);484 const cleared = await popup.evaluate(() => {485 return typeof window.sessionStorage !== "undefined";486 });487 expect(cleared).toBeTruthy();488 await ctx.close();489 });490491 test("LSC05 - brute force protection after wrong attempts", async () => {492 const { popup } = await setupVault(ctx);493 await lockVault(popup);494 for (let i = 0; i < 5; i++) {495 await popup.locator("#master-password").fill("wrongpassword");496 await popup.getByRole("button", { name: /unlock/i }).click();497 await popup.waitForTimeout(500);498 }499 await popup.waitForTimeout(1000);500 await popup.locator("#master-password").fill(VAULT_PASSWORD);501 await popup.getByRole("button", { name: /unlock/i }).click();502 await popup.waitForTimeout(2000);503 504 505 const unlocked = await popup506 .locator(".peach-lock-btn[aria-label='Lock vault']")507 .isVisible({ timeout: 3000 })508 .catch(() => false);509 expect(unlocked).toBeTruthy();510 await ctx.close();511 });512513 test("LSC06 - lock requires user gesture", async () => {514 const { popup } = await setupVault(ctx);515 const lockBtn = popup.locator(".peach-lock-btn[aria-label='Lock vault']");516 await expect(lockBtn).toBeVisible();517 await ctx.close();518 });519});