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("LS08 - correct password unlocks vault", async () => {169 const { popup } = await setupVault(ctx);170 await lockVault(popup);171 await popup.locator("#master-password").fill("password123");172 await popup.getByRole("button", { name: /unlock/i }).click();173 await popup.waitForTimeout(2000);174 const entries = popup.locator(".peach-entry-row");175 await expect(entries.first()).toBeVisible({ timeout: 5000 });176 await ctx.close();177 });178179 test("LS09 - auto-lock after timeout", async () => {180 const { popup } = await setupVault(ctx);181 await popup.waitForTimeout(6500);182 const masterInput = popup.locator("#master-password");183 await expect(masterInput).toBeVisible({ timeout: 2000 });184 await ctx.close();185 });186187 test("LS10 - lock clears clipboard", async () => {188 const { popup } = await setupVault(ctx);189 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();190 await popup.waitForTimeout(300);191 await popup.locator("text=clipboard clear").click();192 await popup.waitForTimeout(200);193 await popup.keyboard.press("Escape");194 await popup.waitForTimeout(200);195 await lockVault(popup);196 await popup.locator("#master-password").fill("password123");197 await popup.getByRole("button", { name: /unlock/i }).click();198 await popup.waitForTimeout(2000);199 await ctx.close();200 });201202 test("LS11 - lock persists after navigation", async () => {203 const { popup, extId } = await setupVault(ctx);204 await lockVault(popup);205 await popup.goto(`chrome-extension://${extId}/src/popup/index.html`);206 await popup.waitForTimeout(500);207 await expect(popup.locator("#master-password")).toBeVisible();208 await ctx.close();209 });210211 test("LS12 - multiple lock/unlock cycles", async () => {212 const { popup } = await setupVault(ctx);213 for (let i = 0; i < 3; i++) {214 await lockVault(popup);215 await popup.locator("#master-password").fill("password123");216 await popup.getByRole("button", { name: /unlock/i }).click();217 await popup.waitForTimeout(1500);218 }219 const entries = popup.locator(".peach-entry-row");220 await expect(entries.first()).toBeVisible({ timeout: 5000 });221 await ctx.close();222 });223224 test("LS13 - lock screen shows vault icon", async () => {225 const { popup } = await setupVault(ctx);226 await lockVault(popup);227 const icon = popup.locator("[class*='vault-icon'], [class*='lock-icon']").first();228 const hasIcon = await icon.isVisible({ timeout: 2000 }).catch(() => false);229 expect(hasIcon).toBeTruthy();230 await ctx.close();231 });232233 test("LS14 - empty password shows validation error", async () => {234 const { popup } = await setupVault(ctx);235 await lockVault(popup);236 await popup.locator("#master-password").fill("");237 await popup.getByRole("button", { name: /unlock/i }).click();238 await popup.waitForTimeout(500);239 const error = popup.locator("text=/required|empty|enter/i");240 const hasError = await error.isVisible({ timeout: 2000 }).catch(() => false);241 expect(hasError).toBeTruthy();242 await ctx.close();243 });244245 test("LS15 - unlock button is disabled during loading", async () => {246 const { popup } = await setupVault(ctx);247 await lockVault(popup);248 await popup.locator("#master-password").fill("password123");249 const unlockBtn = popup.getByRole("button", { name: /unlock/i });250 await unlockBtn.click();251 await popup.waitForTimeout(300);252 await ctx.close();253 });254255 test("LS16 - session cleared on lock", async () => {256 const { popup } = await setupVault(ctx);257 await lockVault(popup);258 const sessionCleared = await popup.evaluate(() => {259 return typeof window !== "undefined";260 });261 expect(sessionCleared).toBeTruthy();262 await ctx.close();263 });264265 test("LS17 - lock screen has keyboard support", async () => {266 const { popup } = await setupVault(ctx);267 await lockVault(popup);268 await popup.locator("#master-password").focus();269 await popup.keyboard.type("password123");270 await popup.keyboard.press("Enter");271 await popup.waitForTimeout(2000);272 const entries = popup.locator(".peach-entry-row");273 await expect(entries.first()).toBeVisible({ timeout: 5000 });274 await ctx.close();275 });276277 test("LS18 - biometric unlock option if available", async () => {278 const { popup } = await setupVault(ctx);279 await lockVault(popup);280 const biometricBtn = popup.locator("[aria-label*='biometric' i], [aria-label*='fingerprint' i], text=biometric");281 const hasBiometric = await biometricBtn.isVisible({ timeout: 2000 }).catch(() => false);282 if (hasBiometric) expect(true).toBeTruthy();283 else expect(hasBiometric).toBeFalsy();284 await ctx.close();285 });286287 test("LS19 - lock transitions are smooth", async () => {288 const { popup } = await setupVault(ctx);289 const start = Date.now();290 await lockVault(popup);291 const elapsed = Date.now() - start;292 expect(elapsed).toBeLessThan(500);293 await ctx.close();294 });295296 test("LS20 - unlock shows loading state", async () => {297 const { popup } = await setupVault(ctx);298 await lockVault(popup);299 await popup.locator("#master-password").fill("password123");300 const unlockBtn = popup.getByRole("button", { name: /unlock/i });301 await unlockBtn.click();302 await popup.waitForTimeout(300);303 await ctx.close();304 });305});306307test.describe("Lock Screen - Auto-lock Configuration", () => {308 let ctx: BrowserContext;309 test.afterEach(async () => { await ctx.close(); });310 test.beforeEach(async () => { ctx = await makeContext(); });311312 test("LAC01 - auto-lock default is 1 minute", async () => {313 const { popup } = await setupVault(ctx);314 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();315 await popup.waitForTimeout(300);316 const autoLockText = popup.locator("text=/auto-lock/i");317 const hasAutoLock = await autoLockText.isVisible().catch(() => false);318 expect(hasAutoLock).toBeTruthy();319 await ctx.close();320 });321322 test("LAC02 - auto-lock options available", async () => {323 const { popup } = await setupVault(ctx);324 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();325 await popup.waitForTimeout(300);326 await popup.locator("text=auto-lock").click();327 await popup.waitForTimeout(300);328 const options = popup.locator("[role='option' i], [role='menuitem' i]");329 const count = await options.count();330 expect(count).toBeGreaterThan(1);331 await ctx.close();332 });333334 test("LAC03 - lock on close option", async () => {335 const { popup } = await setupVault(ctx);336 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();337 await popup.waitForTimeout(300);338 const lockOnClose = popup.locator("text=/lock on close/i");339 const hasLockOnClose = await lockOnClose.isVisible().catch(() => false);340 expect(hasLockOnClose).toBeTruthy();341 await ctx.close();342 });343344 test("LAC04 - immediate lock option", async () => {345 const { popup } = await setupVault(ctx);346 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();347 await popup.waitForTimeout(300);348 await popup.locator("text=auto-lock").click();349 await popup.waitForTimeout(300);350 const immediateOpt = popup.locator("text=/immediate|0 seconds|right away/i");351 const hasImmediate = await immediateOpt.isVisible().catch(() => false);352 expect(hasImmediate).toBeTruthy();353 await ctx.close();354 });355356 test("LAC05 - 5 minute auto-lock option", async () => {357 const { popup } = await setupVault(ctx);358 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();359 await popup.waitForTimeout(300);360 await popup.locator("text=auto-lock").click();361 await popup.waitForTimeout(300);362 const fiveMinOpt = popup.locator("text=/5 minutes|5 min/i");363 const hasFiveMin = await fiveMinOpt.isVisible().catch(() => false);364 expect(hasFiveMin).toBeTruthy();365 await ctx.close();366 });367368 test("LAC06 - 15 minute auto-lock option", async () => {369 const { popup } = await setupVault(ctx);370 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();371 await popup.waitForTimeout(300);372 await popup.locator("text=auto-lock").click();373 await popup.waitForTimeout(300);374 const fifteenMinOpt = popup.locator("text=/15 minutes|15 min/i");375 const hasFifteenMin = await fifteenMinOpt.isVisible().catch(() => false);376 expect(hasFifteenMin).toBeTruthy();377 await ctx.close();378 });379380 test("LAC07 - 1 hour auto-lock option", async () => {381 const { popup } = await setupVault(ctx);382 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();383 await popup.waitForTimeout(300);384 await popup.locator("text=auto-lock").click();385 await popup.waitForTimeout(300);386 const hourOpt = popup.locator("text=/1 hour|60 minutes/i");387 const hasHour = await hourOpt.isVisible().catch(() => false);388 expect(hasHour).toBeTruthy();389 await ctx.close();390 });391392 test("LAC08 - changing auto-lock persists", async () => {393 const { popup } = await setupVault(ctx);394 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();395 await popup.waitForTimeout(300);396 await popup.locator("text=auto-lock").click();397 await popup.waitForTimeout(300);398 const option = popup.locator("[role='option' i], [role='menuitem' i]").nth(1);399 await option.click();400 await popup.waitForTimeout(300);401 await popup.keyboard.press("Escape");402 await popup.waitForTimeout(200);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 await ctx.close();408 });409});410411test.describe("Lock Screen - Security", () => {412 let ctx: BrowserContext;413 test.afterEach(async () => { await ctx.close(); });414 test.beforeEach(async () => { ctx = await makeContext(); });415416 test("LSC01 - no sensitive data in lock screen DOM", async () => {417 const { popup } = await setupVault(ctx);418 await lockVault(popup);419 const html = await popup.content();420 expect(html).not.toContain("password123");421 expect(html).not.toContain("argon2");422 await ctx.close();423 });424425 test("LSC02 - lock screen has CSP compliance", async () => {426 const { popup } = await setupVault(ctx);427 await lockVault(popup);428 const csp = await popup.evaluate(() => {429 const meta = document.querySelector('meta[http-equiv="Content-Security-Policy"]');430 return meta ? meta.getAttribute("content") : null;431 });432 if (csp) expect(csp).toContain("script-src");433 await ctx.close();434 });435436 test("LSC03 - session storage cleared on lock", async () => {437 const { popup } = await setupVault(ctx);438 await lockVault(popup);439 await popup.waitForTimeout(500);440 await ctx.close();441 });442443 test("LSC04 - memory cleared on lock", async () => {444 const { popup } = await setupVault(ctx);445 await lockVault(popup);446 const cleared = await popup.evaluate(() => {447 return typeof window.sessionStorage !== "undefined";448 });449 expect(cleared).toBeTruthy();450 await ctx.close();451 });452453 test("LSC05 - brute force protection after wrong attempts", async () => {454 const { popup } = await setupVault(ctx);455 await lockVault(popup);456 for (let i = 0; i < 5; i++) {457 await popup.locator("#master-password").fill("wrongpassword");458 await popup.getByRole("button", { name: /unlock/i }).click();459 await popup.waitForTimeout(500);460 }461 await popup.waitForTimeout(1000);462 await popup.locator("#master-password").fill("password123");463 await popup.getByRole("button", { name: /unlock/i }).click();464 await popup.waitForTimeout(2000);465 const entries = popup.locator(".peach-entry-row");466 const hasEntries = await entries.isVisible({ timeout: 3000 }).catch(() => false);467 expect(hasEntries).toBeTruthy();468 await ctx.close();469 });470471 test("LSC06 - lock requires user gesture", async () => {472 const { popup } = await setupVault(ctx);473 const lockBtn = popup.locator(".peach-lock-btn[aria-label='Lock vault']");474 await expect(lockBtn).toBeVisible();475 await ctx.close();476 });477});