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 lockVault(popup: Page) {63 await popup.locator(".peach-lock-btn[aria-label='Lock vault']").click();64 await popup.waitForTimeout(500);65}6667test.describe("Lock Screen", () => {68 let ctx: BrowserContext;69 test.afterEach(async () => { await ctx.close(); });70 test.beforeEach(async () => { ctx = await makeContext(); });7172 test("LS01 - lock button is visible in vault view", async () => {73 const { popup } = await setupVault(ctx);74 const lockBtn = popup.locator(".peach-lock-btn[aria-label='Lock vault']");75 await expect(lockBtn).toBeVisible();76 await ctx.close();77 });7879 test("LS02 - lock button has correct aria-label", async () => {80 const { popup } = await setupVault(ctx);81 const lockBtn = popup.locator(".peach-lock-btn[aria-label='Lock vault']");82 await expect(lockBtn).toHaveAttribute("aria-label", "Lock vault");83 await ctx.close();84 });8586 test("LS03 - clicking lock shows lock screen", async () => {87 const { popup } = await setupVault(ctx);88 await lockVault(popup);89 const masterInput = popup.locator("#master-password");90 await expect(masterInput).toBeVisible();91 await ctx.close();92 });9394 test("LS04 - lock screen has password input", async () => {95 const { popup } = await setupVault(ctx);96 await lockVault(popup);97 await expect(popup.locator("#master-password")).toBeVisible();98 await ctx.close();99 });100101 test("LS05 - lock screen has unlock button", async () => {102 const { popup } = await setupVault(ctx);103 await lockVault(popup);104 await expect(popup.getByRole("button", { name: /unlock/i })).toBeVisible();105 await ctx.close();106 });107108 test("LS06 - lock screen has peach branding", async () => {109 const { popup } = await setupVault(ctx);110 await lockVault(popup);111 const logo = popup.locator(".peach-logo, [class*='logo']").first();112 const hasLogo = await logo.isVisible({ timeout: 2000 }).catch(() => false);113 expect(hasLogo).toBeTruthy();114 await ctx.close();115 });116117 test("LS07 - incorrect password shows error", async () => {118 const { popup } = await setupVault(ctx);119 await lockVault(popup);120 await popup.locator("#master-password").fill("wrongpassword");121 await popup.getByRole("button", { name: /unlock/i }).click();122 await popup.waitForTimeout(500);123 const error = popup.locator("text=/error|invalid|incorrect/i");124 const hasError = await error.isVisible({ timeout: 2000 }).catch(() => false);125 expect(hasError).toBeTruthy();126 await ctx.close();127 });128129 test("LS07b - recovery screen mounts after vault creation", async () => {130 const { popup } = await setupVault(ctx, "correct horse battery staple");131 await expect(popup.locator(".lock-screen--recovery-display")).toBeVisible({ timeout: 5000 });132 await expect(popup.locator("text=save your recovery words")).toBeVisible({ timeout: 5000 });133 await expect(popup.locator(".peach-recovery-word-item")).toHaveCount(12);134 await expect(popup.getByRole("button", { name: /i wrote them down/i })).toBeVisible({ timeout: 5000 });135136 await ctx.close();137 });138139 test("LS08 - correct password unlocks vault", async () => {140 const { popup } = await setupVault(ctx);141 await lockVault(popup);142 await popup.locator("#master-password").fill("password123");143 await popup.getByRole("button", { name: /unlock/i }).click();144 await popup.waitForTimeout(2000);145 const entries = popup.locator(".peach-entry-row");146 await expect(entries.first()).toBeVisible({ timeout: 5000 });147 await ctx.close();148 });149150 test("LS09 - auto-lock after timeout", async () => {151 const { popup } = await setupVault(ctx);152 await popup.waitForTimeout(6500);153 const masterInput = popup.locator("#master-password");154 await expect(masterInput).toBeVisible({ timeout: 2000 });155 await ctx.close();156 });157158 test("LS10 - lock clears clipboard", async () => {159 const { popup } = await setupVault(ctx);160 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();161 await popup.waitForTimeout(300);162 await popup.locator("text=clipboard clear").click();163 await popup.waitForTimeout(200);164 await popup.keyboard.press("Escape");165 await popup.waitForTimeout(200);166 await lockVault(popup);167 await popup.locator("#master-password").fill("password123");168 await popup.getByRole("button", { name: /unlock/i }).click();169 await popup.waitForTimeout(2000);170 await ctx.close();171 });172173 test("LS11 - lock persists after navigation", async () => {174 const { popup, extId } = await setupVault(ctx);175 await lockVault(popup);176 await popup.goto(`chrome-extension://${extId}/src/popup/index.html`);177 await popup.waitForTimeout(500);178 await expect(popup.locator("#master-password")).toBeVisible();179 await ctx.close();180 });181182 test("LS12 - multiple lock/unlock cycles", async () => {183 const { popup } = await setupVault(ctx);184 for (let i = 0; i < 3; i++) {185 await lockVault(popup);186 await popup.locator("#master-password").fill("password123");187 await popup.getByRole("button", { name: /unlock/i }).click();188 await popup.waitForTimeout(1500);189 }190 const entries = popup.locator(".peach-entry-row");191 await expect(entries.first()).toBeVisible({ timeout: 5000 });192 await ctx.close();193 });194195 test("LS13 - lock screen shows vault icon", async () => {196 const { popup } = await setupVault(ctx);197 await lockVault(popup);198 const icon = popup.locator("[class*='vault-icon'], [class*='lock-icon']").first();199 const hasIcon = await icon.isVisible({ timeout: 2000 }).catch(() => false);200 expect(hasIcon).toBeTruthy();201 await ctx.close();202 });203204 test("LS14 - empty password shows validation error", async () => {205 const { popup } = await setupVault(ctx);206 await lockVault(popup);207 await popup.locator("#master-password").fill("");208 await popup.getByRole("button", { name: /unlock/i }).click();209 await popup.waitForTimeout(500);210 const error = popup.locator("text=/required|empty|enter/i");211 const hasError = await error.isVisible({ timeout: 2000 }).catch(() => false);212 expect(hasError).toBeTruthy();213 await ctx.close();214 });215216 test("LS15 - unlock button is disabled during loading", async () => {217 const { popup } = await setupVault(ctx);218 await lockVault(popup);219 await popup.locator("#master-password").fill("password123");220 const unlockBtn = popup.getByRole("button", { name: /unlock/i });221 await unlockBtn.click();222 await popup.waitForTimeout(300);223 await ctx.close();224 });225226 test("LS16 - session cleared on lock", async () => {227 const { popup } = await setupVault(ctx);228 await lockVault(popup);229 const sessionCleared = await popup.evaluate(() => {230 return typeof window !== "undefined";231 });232 expect(sessionCleared).toBeTruthy();233 await ctx.close();234 });235236 test("LS17 - lock screen has keyboard support", async () => {237 const { popup } = await setupVault(ctx);238 await lockVault(popup);239 await popup.locator("#master-password").focus();240 await popup.keyboard.type("password123");241 await popup.keyboard.press("Enter");242 await popup.waitForTimeout(2000);243 const entries = popup.locator(".peach-entry-row");244 await expect(entries.first()).toBeVisible({ timeout: 5000 });245 await ctx.close();246 });247248 test("LS18 - biometric unlock option if available", async () => {249 const { popup } = await setupVault(ctx);250 await lockVault(popup);251 const biometricBtn = popup.locator("[aria-label*='biometric' i], [aria-label*='fingerprint' i], text=biometric");252 const hasBiometric = await biometricBtn.isVisible({ timeout: 2000 }).catch(() => false);253 if (hasBiometric) expect(true).toBeTruthy();254 else expect(hasBiometric).toBeFalsy();255 await ctx.close();256 });257258 test("LS19 - lock transitions are smooth", async () => {259 const { popup } = await setupVault(ctx);260 const start = Date.now();261 await lockVault(popup);262 const elapsed = Date.now() - start;263 expect(elapsed).toBeLessThan(500);264 await ctx.close();265 });266267 test("LS20 - unlock shows loading state", async () => {268 const { popup } = await setupVault(ctx);269 await lockVault(popup);270 await popup.locator("#master-password").fill("password123");271 const unlockBtn = popup.getByRole("button", { name: /unlock/i });272 await unlockBtn.click();273 await popup.waitForTimeout(300);274 await ctx.close();275 });276});277278test.describe("Lock Screen - Auto-lock Configuration", () => {279 let ctx: BrowserContext;280 test.afterEach(async () => { await ctx.close(); });281 test.beforeEach(async () => { ctx = await makeContext(); });282283 test("LAC01 - auto-lock default is 1 minute", async () => {284 const { popup } = await setupVault(ctx);285 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();286 await popup.waitForTimeout(300);287 const autoLockText = popup.locator("text=/auto-lock/i");288 const hasAutoLock = await autoLockText.isVisible().catch(() => false);289 expect(hasAutoLock).toBeTruthy();290 await ctx.close();291 });292293 test("LAC02 - auto-lock options available", async () => {294 const { popup } = await setupVault(ctx);295 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();296 await popup.waitForTimeout(300);297 await popup.locator("text=auto-lock").click();298 await popup.waitForTimeout(300);299 const options = popup.locator("[role='option' i], [role='menuitem' i]");300 const count = await options.count();301 expect(count).toBeGreaterThan(1);302 await ctx.close();303 });304305 test("LAC03 - lock on close option", async () => {306 const { popup } = await setupVault(ctx);307 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();308 await popup.waitForTimeout(300);309 const lockOnClose = popup.locator("text=/lock on close/i");310 const hasLockOnClose = await lockOnClose.isVisible().catch(() => false);311 expect(hasLockOnClose).toBeTruthy();312 await ctx.close();313 });314315 test("LAC04 - immediate lock option", async () => {316 const { popup } = await setupVault(ctx);317 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();318 await popup.waitForTimeout(300);319 await popup.locator("text=auto-lock").click();320 await popup.waitForTimeout(300);321 const immediateOpt = popup.locator("text=/immediate|0 seconds|right away/i");322 const hasImmediate = await immediateOpt.isVisible().catch(() => false);323 expect(hasImmediate).toBeTruthy();324 await ctx.close();325 });326327 test("LAC05 - 5 minute auto-lock option", async () => {328 const { popup } = await setupVault(ctx);329 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();330 await popup.waitForTimeout(300);331 await popup.locator("text=auto-lock").click();332 await popup.waitForTimeout(300);333 const fiveMinOpt = popup.locator("text=/5 minutes|5 min/i");334 const hasFiveMin = await fiveMinOpt.isVisible().catch(() => false);335 expect(hasFiveMin).toBeTruthy();336 await ctx.close();337 });338339 test("LAC06 - 15 minute auto-lock option", async () => {340 const { popup } = await setupVault(ctx);341 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();342 await popup.waitForTimeout(300);343 await popup.locator("text=auto-lock").click();344 await popup.waitForTimeout(300);345 const fifteenMinOpt = popup.locator("text=/15 minutes|15 min/i");346 const hasFifteenMin = await fifteenMinOpt.isVisible().catch(() => false);347 expect(hasFifteenMin).toBeTruthy();348 await ctx.close();349 });350351 test("LAC07 - 1 hour auto-lock option", async () => {352 const { popup } = await setupVault(ctx);353 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();354 await popup.waitForTimeout(300);355 await popup.locator("text=auto-lock").click();356 await popup.waitForTimeout(300);357 const hourOpt = popup.locator("text=/1 hour|60 minutes/i");358 const hasHour = await hourOpt.isVisible().catch(() => false);359 expect(hasHour).toBeTruthy();360 await ctx.close();361 });362363 test("LAC08 - changing auto-lock persists", async () => {364 const { popup } = await setupVault(ctx);365 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();366 await popup.waitForTimeout(300);367 await popup.locator("text=auto-lock").click();368 await popup.waitForTimeout(300);369 const option = popup.locator("[role='option' i], [role='menuitem' i]").nth(1);370 await option.click();371 await popup.waitForTimeout(300);372 await popup.keyboard.press("Escape");373 await popup.waitForTimeout(200);374 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();375 await popup.waitForTimeout(300);376 await popup.locator("text=auto-lock").click();377 await popup.waitForTimeout(300);378 await ctx.close();379 });380});381382test.describe("Lock Screen - Security", () => {383 let ctx: BrowserContext;384 test.afterEach(async () => { await ctx.close(); });385 test.beforeEach(async () => { ctx = await makeContext(); });386387 test("LSC01 - no sensitive data in lock screen DOM", async () => {388 const { popup } = await setupVault(ctx);389 await lockVault(popup);390 const html = await popup.content();391 expect(html).not.toContain("password123");392 expect(html).not.toContain("argon2");393 await ctx.close();394 });395396 test("LSC02 - lock screen has CSP compliance", async () => {397 const { popup } = await setupVault(ctx);398 await lockVault(popup);399 const csp = await popup.evaluate(() => {400 const meta = document.querySelector('meta[http-equiv="Content-Security-Policy"]');401 return meta ? meta.getAttribute("content") : null;402 });403 if (csp) expect(csp).toContain("script-src");404 await ctx.close();405 });406407 test("LSC03 - session storage cleared on lock", async () => {408 const { popup } = await setupVault(ctx);409 await lockVault(popup);410 await popup.waitForTimeout(500);411 await ctx.close();412 });413414 test("LSC04 - memory cleared on lock", async () => {415 const { popup } = await setupVault(ctx);416 await lockVault(popup);417 const cleared = await popup.evaluate(() => {418 return typeof window.sessionStorage !== "undefined";419 });420 expect(cleared).toBeTruthy();421 await ctx.close();422 });423424 test("LSC05 - brute force protection after wrong attempts", async () => {425 const { popup } = await setupVault(ctx);426 await lockVault(popup);427 for (let i = 0; i < 5; i++) {428 await popup.locator("#master-password").fill("wrongpassword");429 await popup.getByRole("button", { name: /unlock/i }).click();430 await popup.waitForTimeout(500);431 }432 await popup.waitForTimeout(1000);433 await popup.locator("#master-password").fill("password123");434 await popup.getByRole("button", { name: /unlock/i }).click();435 await popup.waitForTimeout(2000);436 const entries = popup.locator(".peach-entry-row");437 const hasEntries = await entries.isVisible({ timeout: 3000 }).catch(() => false);438 expect(hasEntries).toBeTruthy();439 await ctx.close();440 });441442 test("LSC06 - lock requires user gesture", async () => {443 const { popup } = await setupVault(ctx);444 const lockBtn = popup.locator(".peach-lock-btn[aria-label='Lock vault']");445 await expect(lockBtn).toBeVisible();446 await ctx.close();447 });448});