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}3940async function setupVault(ctx: BrowserContext, password = "password123") {41 const { popup, extId } = await loadPopup(ctx);42 const advanceButton = popup.getByRole("button", { name: /next|get started/i });43 const pw = popup.locator("#master-password");44 for (let i = 0; i < 5 && !(await pw.isVisible().catch(() => false)); i++) {45 if (await advanceButton.isVisible({ timeout: 500 }).catch(() => false)) {46 await advanceButton.click();47 await popup.waitForTimeout(300);48 continue;49 }50 await popup.locator(".peach-onboarding-card").click();51 await popup.waitForTimeout(300);52 }53 if (await pw.isVisible()) {54 await pw.fill(password);55 await popup.locator("#confirm-password").fill(password);56 await popup.getByRole("button", { name: /create vault/i }).click();57 await expect(popup.locator(".lock-screen--recovery-display")).toBeVisible({ timeout: 5000 });58 }59 return { popup, extId };60}6162async function completeRecoveryConfirmation(popup: Page) {63 const recoveryWords = popup.locator(".peach-recovery-word-text");64 const confirmInputs = popup.locator(".peach-recovery-confirm-input");65 const confirmButton = popup.getByRole("button", { name: /i wrote them down/i });6667 await expect(confirmInputs).toHaveCount(3);68 await expect(confirmInputs.first()).toBeEnabled({ timeout: 7000 });6970 const words = await recoveryWords.allTextContents();71 const confirmedWords = [words[2], words[6], words[10]];72 for (let i = 0; i < confirmedWords.length; i++) {73 const word = confirmedWords[i]?.trim();74 if (!word) throw new Error(`missing recovery word at position ${i}`);75 await confirmInputs.nth(i).fill(word);76 }7778 await expect(confirmButton).toBeEnabled({ timeout: 2000 });79 await confirmButton.click();80 await expect(popup.locator(".peach-lock-btn[aria-label='Lock vault']")).toBeVisible({ timeout: 5000 });81}8283async function lockVault(popup: Page) {84 await popup.locator(".peach-lock-btn[aria-label='Lock vault']").click();85 await popup.waitForTimeout(500);86}8788test.describe("Lock Screen", () => {89 let ctx: BrowserContext;90 test.afterEach(async () => { await ctx.close(); });91 test.beforeEach(async () => { ctx = await makeContext(); });9293 test("LS01 - lock button is visible in vault view", async () => {94 const { popup } = await setupVault(ctx);95 const lockBtn = popup.locator(".peach-lock-btn[aria-label='Lock vault']");96 await expect(lockBtn).toBeVisible();97 await ctx.close();98 });99100 test("LS02 - lock button has correct aria-label", async () => {101 const { popup } = await setupVault(ctx);102 const lockBtn = popup.locator(".peach-lock-btn[aria-label='Lock vault']");103 await expect(lockBtn).toHaveAttribute("aria-label", "Lock vault");104 await ctx.close();105 });106107 test("LS03 - clicking lock shows lock screen", async () => {108 const { popup } = await setupVault(ctx);109 await lockVault(popup);110 const masterInput = popup.locator("#master-password");111 await expect(masterInput).toBeVisible();112 await ctx.close();113 });114115 test("LS04 - lock screen has password input", async () => {116 const { popup } = await setupVault(ctx);117 await lockVault(popup);118 await expect(popup.locator("#master-password")).toBeVisible();119 await ctx.close();120 });121122 test("LS05 - lock screen has unlock button", async () => {123 const { popup } = await setupVault(ctx);124 await lockVault(popup);125 await expect(popup.getByRole("button", { name: /unlock/i })).toBeVisible();126 await ctx.close();127 });128129 test("LS06 - lock screen has peach branding", async () => {130 const { popup } = await setupVault(ctx);131 await lockVault(popup);132 const logo = popup.locator(".peach-logo, [class*='logo']").first();133 const hasLogo = await logo.isVisible({ timeout: 2000 }).catch(() => false);134 expect(hasLogo).toBeTruthy();135 await ctx.close();136 });137138 test("LS07 - incorrect password shows error", async () => {139 const { popup } = await setupVault(ctx);140 await lockVault(popup);141 await popup.locator("#master-password").fill("wrongpassword");142 await popup.getByRole("button", { name: /unlock/i }).click();143 await popup.waitForTimeout(500);144 const error = popup.locator("text=/error|invalid|incorrect/i");145 const hasError = await error.isVisible({ timeout: 2000 }).catch(() => false);146 expect(hasError).toBeTruthy();147 await ctx.close();148 });149150 test("LS07b - recovery screen mounts after vault creation", async () => {151 const { popup } = await setupVault(ctx, "correct horse battery staple");152 await expect(popup.locator(".lock-screen--recovery-display")).toBeVisible({ timeout: 5000 });153 await expect(popup.locator("text=save your recovery words")).toBeVisible({ timeout: 5000 });154 await expect(popup.locator(".peach-recovery-word-item")).toHaveCount(12);155 await expect(popup.getByRole("button", { name: /i wrote them down/i })).toBeVisible({ timeout: 5000 });156157 await ctx.close();158 });159160 test("LS07c - recovery confirmation returns to the unlocked vault", async () => {161 const { popup } = await setupVault(ctx, "correct horse battery staple");162 await completeRecoveryConfirmation(popup);163164 await expect(popup.locator(".peach-lock-btn[aria-label='Lock vault']")).toBeVisible({ timeout: 5000 });165 await ctx.close();166 });167168 test("LS07d - recovery confirmation survives popup close and reopen", async () => {169 const { popup } = await setupVault(ctx, "correct horse battery staple");170 const confirmInputs = popup.locator(".peach-recovery-confirm-input");171 const recoveryWords = popup.locator(".peach-recovery-word-text");172173 await expect(confirmInputs.first()).toBeEnabled({ timeout: 7000 });174 const words = await recoveryWords.allTextContents();175 const firstWord = words[2]?.trim();176 if (!firstWord) throw new Error("missing recovery word for close/reopen smoke test");177 await confirmInputs.first().fill(firstWord);178179 await popup.close();180181 const reopened = await loadPopup(ctx);182 await expect(reopened.popup.locator(".lock-screen--recovery-display")).toBeVisible({ timeout: 5000 });183 await completeRecoveryConfirmation(reopened.popup);184185 await reopened.popup.close();186 await ctx.close();187 });188189 test("LS08 - correct password unlocks vault", async () => {190 const { popup } = await setupVault(ctx);191 await lockVault(popup);192 await popup.locator("#master-password").fill("password123");193 await popup.getByRole("button", { name: /unlock/i }).click();194 await popup.waitForTimeout(2000);195 const entries = popup.locator(".peach-entry-row");196 await expect(entries.first()).toBeVisible({ timeout: 5000 });197 await ctx.close();198 });199200 test("LS09 - auto-lock after timeout", async () => {201 const { popup } = await setupVault(ctx);202 await popup.waitForTimeout(6500);203 const masterInput = popup.locator("#master-password");204 await expect(masterInput).toBeVisible({ timeout: 2000 });205 await ctx.close();206 });207208 test("LS10 - lock clears clipboard", async () => {209 const { popup } = await setupVault(ctx);210 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();211 await popup.waitForTimeout(300);212 await popup.locator("text=clipboard clear").click();213 await popup.waitForTimeout(200);214 await popup.keyboard.press("Escape");215 await popup.waitForTimeout(200);216 await lockVault(popup);217 await popup.locator("#master-password").fill("password123");218 await popup.getByRole("button", { name: /unlock/i }).click();219 await popup.waitForTimeout(2000);220 await ctx.close();221 });222223 test("LS11 - lock persists after navigation", async () => {224 const { popup, extId } = await setupVault(ctx);225 await lockVault(popup);226 await popup.goto(`chrome-extension://${extId}/src/popup/index.html`);227 await popup.waitForTimeout(500);228 await expect(popup.locator("#master-password")).toBeVisible();229 await ctx.close();230 });231232 test("LS12 - multiple lock/unlock cycles", async () => {233 const { popup } = await setupVault(ctx);234 for (let i = 0; i < 3; i++) {235 await lockVault(popup);236 await popup.locator("#master-password").fill("password123");237 await popup.getByRole("button", { name: /unlock/i }).click();238 await popup.waitForTimeout(1500);239 }240 const entries = popup.locator(".peach-entry-row");241 await expect(entries.first()).toBeVisible({ timeout: 5000 });242 await ctx.close();243 });244245 test("LS13 - lock screen shows vault icon", async () => {246 const { popup } = await setupVault(ctx);247 await lockVault(popup);248 const icon = popup.locator("[class*='vault-icon'], [class*='lock-icon']").first();249 const hasIcon = await icon.isVisible({ timeout: 2000 }).catch(() => false);250 expect(hasIcon).toBeTruthy();251 await ctx.close();252 });253254 test("LS14 - empty password shows validation error", async () => {255 const { popup } = await setupVault(ctx);256 await lockVault(popup);257 await popup.locator("#master-password").fill("");258 await popup.getByRole("button", { name: /unlock/i }).click();259 await popup.waitForTimeout(500);260 const error = popup.locator("text=/required|empty|enter/i");261 const hasError = await error.isVisible({ timeout: 2000 }).catch(() => false);262 expect(hasError).toBeTruthy();263 await ctx.close();264 });265266 test("LS15 - unlock button is disabled during loading", async () => {267 const { popup } = await setupVault(ctx);268 await lockVault(popup);269 await popup.locator("#master-password").fill("password123");270 const unlockBtn = popup.getByRole("button", { name: /unlock/i });271 await unlockBtn.click();272 await popup.waitForTimeout(300);273 await ctx.close();274 });275276 test("LS16 - session cleared on lock", async () => {277 const { popup } = await setupVault(ctx);278 await lockVault(popup);279 const sessionCleared = await popup.evaluate(() => {280 return typeof window !== "undefined";281 });282 expect(sessionCleared).toBeTruthy();283 await ctx.close();284 });285286 test("LS17 - lock screen has keyboard support", async () => {287 const { popup } = await setupVault(ctx);288 await lockVault(popup);289 await popup.locator("#master-password").focus();290 await popup.keyboard.type("password123");291 await popup.keyboard.press("Enter");292 await popup.waitForTimeout(2000);293 const entries = popup.locator(".peach-entry-row");294 await expect(entries.first()).toBeVisible({ timeout: 5000 });295 await ctx.close();296 });297298 test("LS18 - biometric unlock option if available", async () => {299 const { popup } = await setupVault(ctx);300 await lockVault(popup);301 const biometricBtn = popup.locator("[aria-label*='biometric' i], [aria-label*='fingerprint' i], text=biometric");302 const hasBiometric = await biometricBtn.isVisible({ timeout: 2000 }).catch(() => false);303 if (hasBiometric) expect(true).toBeTruthy();304 else expect(hasBiometric).toBeFalsy();305 await ctx.close();306 });307308 test("LS19 - lock transitions are smooth", async () => {309 const { popup } = await setupVault(ctx);310 const start = Date.now();311 await lockVault(popup);312 const elapsed = Date.now() - start;313 expect(elapsed).toBeLessThan(500);314 await ctx.close();315 });316317 test("LS20 - unlock shows loading state", async () => {318 const { popup } = await setupVault(ctx);319 await lockVault(popup);320 await popup.locator("#master-password").fill("password123");321 const unlockBtn = popup.getByRole("button", { name: /unlock/i });322 await unlockBtn.click();323 await popup.waitForTimeout(300);324 await ctx.close();325 });326});327328test.describe("Lock Screen - Auto-lock Configuration", () => {329 let ctx: BrowserContext;330 test.afterEach(async () => { await ctx.close(); });331 test.beforeEach(async () => { ctx = await makeContext(); });332333 test("LAC01 - auto-lock default is 1 minute", async () => {334 const { popup } = await setupVault(ctx);335 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();336 await popup.waitForTimeout(300);337 const autoLockText = popup.locator("text=/auto-lock/i");338 const hasAutoLock = await autoLockText.isVisible().catch(() => false);339 expect(hasAutoLock).toBeTruthy();340 await ctx.close();341 });342343 test("LAC02 - auto-lock options available", async () => {344 const { popup } = await setupVault(ctx);345 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();346 await popup.waitForTimeout(300);347 await popup.locator("text=auto-lock").click();348 await popup.waitForTimeout(300);349 const options = popup.locator("[role='option' i], [role='menuitem' i]");350 const count = await options.count();351 expect(count).toBeGreaterThan(1);352 await ctx.close();353 });354355 test("LAC03 - lock on close option", async () => {356 const { popup } = await setupVault(ctx);357 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();358 await popup.waitForTimeout(300);359 const lockOnClose = popup.locator("text=/lock on close/i");360 const hasLockOnClose = await lockOnClose.isVisible().catch(() => false);361 expect(hasLockOnClose).toBeTruthy();362 await ctx.close();363 });364365 test("LAC04 - immediate lock option", async () => {366 const { popup } = await setupVault(ctx);367 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();368 await popup.waitForTimeout(300);369 await popup.locator("text=auto-lock").click();370 await popup.waitForTimeout(300);371 const immediateOpt = popup.locator("text=/immediate|0 seconds|right away/i");372 const hasImmediate = await immediateOpt.isVisible().catch(() => false);373 expect(hasImmediate).toBeTruthy();374 await ctx.close();375 });376377 test("LAC05 - 5 minute auto-lock option", async () => {378 const { popup } = await setupVault(ctx);379 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();380 await popup.waitForTimeout(300);381 await popup.locator("text=auto-lock").click();382 await popup.waitForTimeout(300);383 const fiveMinOpt = popup.locator("text=/5 minutes|5 min/i");384 const hasFiveMin = await fiveMinOpt.isVisible().catch(() => false);385 expect(hasFiveMin).toBeTruthy();386 await ctx.close();387 });388389 test("LAC06 - 15 minute auto-lock option", async () => {390 const { popup } = await setupVault(ctx);391 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();392 await popup.waitForTimeout(300);393 await popup.locator("text=auto-lock").click();394 await popup.waitForTimeout(300);395 const fifteenMinOpt = popup.locator("text=/15 minutes|15 min/i");396 const hasFifteenMin = await fifteenMinOpt.isVisible().catch(() => false);397 expect(hasFifteenMin).toBeTruthy();398 await ctx.close();399 });400401 test("LAC07 - 1 hour auto-lock option", async () => {402 const { popup } = await setupVault(ctx);403 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();404 await popup.waitForTimeout(300);405 await popup.locator("text=auto-lock").click();406 await popup.waitForTimeout(300);407 const hourOpt = popup.locator("text=/1 hour|60 minutes/i");408 const hasHour = await hourOpt.isVisible().catch(() => false);409 expect(hasHour).toBeTruthy();410 await ctx.close();411 });412413 test("LAC08 - changing auto-lock persists", async () => {414 const { popup } = await setupVault(ctx);415 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();416 await popup.waitForTimeout(300);417 await popup.locator("text=auto-lock").click();418 await popup.waitForTimeout(300);419 const option = popup.locator("[role='option' i], [role='menuitem' i]").nth(1);420 await option.click();421 await popup.waitForTimeout(300);422 await popup.keyboard.press("Escape");423 await popup.waitForTimeout(200);424 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();425 await popup.waitForTimeout(300);426 await popup.locator("text=auto-lock").click();427 await popup.waitForTimeout(300);428 await ctx.close();429 });430});431432test.describe("Lock Screen - Security", () => {433 let ctx: BrowserContext;434 test.afterEach(async () => { await ctx.close(); });435 test.beforeEach(async () => { ctx = await makeContext(); });436437 test("LSC01 - no sensitive data in lock screen DOM", async () => {438 const { popup } = await setupVault(ctx);439 await lockVault(popup);440 const html = await popup.content();441 expect(html).not.toContain("password123");442 expect(html).not.toContain("argon2");443 await ctx.close();444 });445446 test("LSC02 - lock screen has CSP compliance", async () => {447 const { popup } = await setupVault(ctx);448 await lockVault(popup);449 const csp = await popup.evaluate(() => {450 const meta = document.querySelector('meta[http-equiv="Content-Security-Policy"]');451 return meta ? meta.getAttribute("content") : null;452 });453 if (csp) expect(csp).toContain("script-src");454 await ctx.close();455 });456457 test("LSC03 - session storage cleared on lock", async () => {458 const { popup } = await setupVault(ctx);459 await lockVault(popup);460 await popup.waitForTimeout(500);461 await ctx.close();462 });463464 test("LSC04 - memory cleared on lock", async () => {465 const { popup } = await setupVault(ctx);466 await lockVault(popup);467 const cleared = await popup.evaluate(() => {468 return typeof window.sessionStorage !== "undefined";469 });470 expect(cleared).toBeTruthy();471 await ctx.close();472 });473474 test("LSC05 - brute force protection after wrong attempts", async () => {475 const { popup } = await setupVault(ctx);476 await lockVault(popup);477 for (let i = 0; i < 5; i++) {478 await popup.locator("#master-password").fill("wrongpassword");479 await popup.getByRole("button", { name: /unlock/i }).click();480 await popup.waitForTimeout(500);481 }482 await popup.waitForTimeout(1000);483 await popup.locator("#master-password").fill("password123");484 await popup.getByRole("button", { name: /unlock/i }).click();485 await popup.waitForTimeout(2000);486 const entries = popup.locator(".peach-entry-row");487 const hasEntries = await entries.isVisible({ timeout: 3000 }).catch(() => false);488 expect(hasEntries).toBeTruthy();489 await ctx.close();490 });491492 test("LSC06 - lock requires user gesture", async () => {493 const { popup } = await setupVault(ctx);494 const lockBtn = popup.locator(".peach-lock-btn[aria-label='Lock vault']");495 await expect(lockBtn).toBeVisible();496 await ctx.close();497 });498});