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 pw = popup.locator("#master-password");43 if (await pw.isVisible()) {44 await pw.fill(password);45 await popup.locator("#confirm-password").fill(password);46 await popup.getByRole("button", { name: /create vault/i }).click();47 await popup.waitForTimeout(2000);48 }49 return { popup, extId };50}5152async function lockVault(popup: Page) {53 await popup.locator(".peach-lock-btn[aria-label='Lock vault']").click();54 await popup.waitForTimeout(500);55}5657test.describe("Lock Screen", () => {58 let ctx: BrowserContext;59 test.afterEach(async () => { await ctx.close(); });60 test.beforeEach(async () => { ctx = await makeContext(); });6162 test("LS01 - lock button is visible in vault view", async () => {63 const { popup } = await setupVault(ctx);64 const lockBtn = popup.locator(".peach-lock-btn[aria-label='Lock vault']");65 await expect(lockBtn).toBeVisible();66 await ctx.close();67 });6869 test("LS02 - lock button has correct aria-label", async () => {70 const { popup } = await setupVault(ctx);71 const lockBtn = popup.locator(".peach-lock-btn[aria-label='Lock vault']");72 await expect(lockBtn).toHaveAttribute("aria-label", "Lock vault");73 await ctx.close();74 });7576 test("LS03 - clicking lock shows lock screen", async () => {77 const { popup } = await setupVault(ctx);78 await lockVault(popup);79 const masterInput = popup.locator("#master-password");80 await expect(masterInput).toBeVisible();81 await ctx.close();82 });8384 test("LS04 - lock screen has password input", async () => {85 const { popup } = await setupVault(ctx);86 await lockVault(popup);87 await expect(popup.locator("#master-password")).toBeVisible();88 await ctx.close();89 });9091 test("LS05 - lock screen has unlock button", async () => {92 const { popup } = await setupVault(ctx);93 await lockVault(popup);94 await expect(popup.getByRole("button", { name: /unlock/i })).toBeVisible();95 await ctx.close();96 });9798 test("LS06 - lock screen has peach branding", async () => {99 const { popup } = await setupVault(ctx);100 await lockVault(popup);101 const logo = popup.locator(".peach-logo, [class*='logo']").first();102 const hasLogo = await logo.isVisible({ timeout: 2000 }).catch(() => false);103 expect(hasLogo).toBeTruthy();104 await ctx.close();105 });106107 test("LS07 - incorrect password shows error", async () => {108 const { popup } = await setupVault(ctx);109 await lockVault(popup);110 await popup.locator("#master-password").fill("wrongpassword");111 await popup.getByRole("button", { name: /unlock/i }).click();112 await popup.waitForTimeout(500);113 const error = popup.locator("text=/error|invalid|incorrect/i");114 const hasError = await error.isVisible({ timeout: 2000 }).catch(() => false);115 expect(hasError).toBeTruthy();116 await ctx.close();117 });118119 test("LS08 - correct password unlocks vault", async () => {120 const { popup } = await setupVault(ctx);121 await lockVault(popup);122 await popup.locator("#master-password").fill("password123");123 await popup.getByRole("button", { name: /unlock/i }).click();124 await popup.waitForTimeout(2000);125 const entries = popup.locator(".peach-entry-row");126 await expect(entries.first()).toBeVisible({ timeout: 5000 });127 await ctx.close();128 });129130 test("LS09 - auto-lock after timeout", async () => {131 const { popup } = await setupVault(ctx);132 await popup.waitForTimeout(6500);133 const masterInput = popup.locator("#master-password");134 await expect(masterInput).toBeVisible({ timeout: 2000 });135 await ctx.close();136 });137138 test("LS10 - lock clears clipboard", async () => {139 const { popup } = await setupVault(ctx);140 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();141 await popup.waitForTimeout(300);142 await popup.locator("text=clipboard clear").click();143 await popup.waitForTimeout(200);144 await popup.keyboard.press("Escape");145 await popup.waitForTimeout(200);146 await lockVault(popup);147 await popup.locator("#master-password").fill("password123");148 await popup.getByRole("button", { name: /unlock/i }).click();149 await popup.waitForTimeout(2000);150 await ctx.close();151 });152153 test("LS11 - lock persists after navigation", async () => {154 const { popup, extId } = await setupVault(ctx);155 await lockVault(popup);156 await popup.goto(`chrome-extension://${extId}/src/popup/index.html`);157 await popup.waitForTimeout(500);158 await expect(popup.locator("#master-password")).toBeVisible();159 await ctx.close();160 });161162 test("LS12 - multiple lock/unlock cycles", async () => {163 const { popup } = await setupVault(ctx);164 for (let i = 0; i < 3; i++) {165 await lockVault(popup);166 await popup.locator("#master-password").fill("password123");167 await popup.getByRole("button", { name: /unlock/i }).click();168 await popup.waitForTimeout(1500);169 }170 const entries = popup.locator(".peach-entry-row");171 await expect(entries.first()).toBeVisible({ timeout: 5000 });172 await ctx.close();173 });174175 test("LS13 - lock screen shows vault icon", async () => {176 const { popup } = await setupVault(ctx);177 await lockVault(popup);178 const icon = popup.locator("[class*='vault-icon'], [class*='lock-icon']").first();179 const hasIcon = await icon.isVisible({ timeout: 2000 }).catch(() => false);180 expect(hasIcon).toBeTruthy();181 await ctx.close();182 });183184 test("LS14 - empty password shows validation error", async () => {185 const { popup } = await setupVault(ctx);186 await lockVault(popup);187 await popup.locator("#master-password").fill("");188 await popup.getByRole("button", { name: /unlock/i }).click();189 await popup.waitForTimeout(500);190 const error = popup.locator("text=/required|empty|enter/i");191 const hasError = await error.isVisible({ timeout: 2000 }).catch(() => false);192 expect(hasError).toBeTruthy();193 await ctx.close();194 });195196 test("LS15 - unlock button is disabled during loading", async () => {197 const { popup } = await setupVault(ctx);198 await lockVault(popup);199 await popup.locator("#master-password").fill("password123");200 const unlockBtn = popup.getByRole("button", { name: /unlock/i });201 await unlockBtn.click();202 await popup.waitForTimeout(300);203 await ctx.close();204 });205206 test("LS16 - session cleared on lock", async () => {207 const { popup } = await setupVault(ctx);208 await lockVault(popup);209 const sessionCleared = await popup.evaluate(() => {210 return typeof window !== "undefined";211 });212 expect(sessionCleared).toBeTruthy();213 await ctx.close();214 });215216 test("LS17 - lock screen has keyboard support", async () => {217 const { popup } = await setupVault(ctx);218 await lockVault(popup);219 await popup.locator("#master-password").focus();220 await popup.keyboard.type("password123");221 await popup.keyboard.press("Enter");222 await popup.waitForTimeout(2000);223 const entries = popup.locator(".peach-entry-row");224 await expect(entries.first()).toBeVisible({ timeout: 5000 });225 await ctx.close();226 });227228 test("LS18 - biometric unlock option if available", async () => {229 const { popup } = await setupVault(ctx);230 await lockVault(popup);231 const biometricBtn = popup.locator("[aria-label*='biometric' i], [aria-label*='fingerprint' i], text=biometric");232 const hasBiometric = await biometricBtn.isVisible({ timeout: 2000 }).catch(() => false);233 if (hasBiometric) expect(true).toBeTruthy();234 else expect(hasBiometric).toBeFalsy();235 await ctx.close();236 });237238 test("LS19 - lock transitions are smooth", async () => {239 const { popup } = await setupVault(ctx);240 const start = Date.now();241 await lockVault(popup);242 const elapsed = Date.now() - start;243 expect(elapsed).toBeLessThan(500);244 await ctx.close();245 });246247 test("LS20 - unlock shows loading state", async () => {248 const { popup } = await setupVault(ctx);249 await lockVault(popup);250 await popup.locator("#master-password").fill("password123");251 const unlockBtn = popup.getByRole("button", { name: /unlock/i });252 await unlockBtn.click();253 await popup.waitForTimeout(300);254 await ctx.close();255 });256});257258test.describe("Lock Screen - Auto-lock Configuration", () => {259 let ctx: BrowserContext;260 test.afterEach(async () => { await ctx.close(); });261 test.beforeEach(async () => { ctx = await makeContext(); });262263 test("LAC01 - auto-lock default is 1 minute", async () => {264 const { popup } = await setupVault(ctx);265 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();266 await popup.waitForTimeout(300);267 const autoLockText = popup.locator("text=/auto-lock/i");268 const hasAutoLock = await autoLockText.isVisible().catch(() => false);269 expect(hasAutoLock).toBeTruthy();270 await ctx.close();271 });272273 test("LAC02 - auto-lock options available", async () => {274 const { popup } = await setupVault(ctx);275 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();276 await popup.waitForTimeout(300);277 await popup.locator("text=auto-lock").click();278 await popup.waitForTimeout(300);279 const options = popup.locator("[role='option' i], [role='menuitem' i]");280 const count = await options.count();281 expect(count).toBeGreaterThan(1);282 await ctx.close();283 });284285 test("LAC03 - lock on close option", async () => {286 const { popup } = await setupVault(ctx);287 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();288 await popup.waitForTimeout(300);289 const lockOnClose = popup.locator("text=/lock on close/i");290 const hasLockOnClose = await lockOnClose.isVisible().catch(() => false);291 expect(hasLockOnClose).toBeTruthy();292 await ctx.close();293 });294295 test("LAC04 - immediate lock option", async () => {296 const { popup } = await setupVault(ctx);297 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();298 await popup.waitForTimeout(300);299 await popup.locator("text=auto-lock").click();300 await popup.waitForTimeout(300);301 const immediateOpt = popup.locator("text=/immediate|0 seconds|right away/i");302 const hasImmediate = await immediateOpt.isVisible().catch(() => false);303 expect(hasImmediate).toBeTruthy();304 await ctx.close();305 });306307 test("LAC05 - 5 minute auto-lock option", async () => {308 const { popup } = await setupVault(ctx);309 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();310 await popup.waitForTimeout(300);311 await popup.locator("text=auto-lock").click();312 await popup.waitForTimeout(300);313 const fiveMinOpt = popup.locator("text=/5 minutes|5 min/i");314 const hasFiveMin = await fiveMinOpt.isVisible().catch(() => false);315 expect(hasFiveMin).toBeTruthy();316 await ctx.close();317 });318319 test("LAC06 - 15 minute auto-lock option", async () => {320 const { popup } = await setupVault(ctx);321 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();322 await popup.waitForTimeout(300);323 await popup.locator("text=auto-lock").click();324 await popup.waitForTimeout(300);325 const fifteenMinOpt = popup.locator("text=/15 minutes|15 min/i");326 const hasFifteenMin = await fifteenMinOpt.isVisible().catch(() => false);327 expect(hasFifteenMin).toBeTruthy();328 await ctx.close();329 });330331 test("LAC07 - 1 hour auto-lock option", async () => {332 const { popup } = await setupVault(ctx);333 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();334 await popup.waitForTimeout(300);335 await popup.locator("text=auto-lock").click();336 await popup.waitForTimeout(300);337 const hourOpt = popup.locator("text=/1 hour|60 minutes/i");338 const hasHour = await hourOpt.isVisible().catch(() => false);339 expect(hasHour).toBeTruthy();340 await ctx.close();341 });342343 test("LAC08 - changing auto-lock persists", 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 option = popup.locator("[role='option' i], [role='menuitem' i]").nth(1);350 await option.click();351 await popup.waitForTimeout(300);352 await popup.keyboard.press("Escape");353 await popup.waitForTimeout(200);354 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();355 await popup.waitForTimeout(300);356 await popup.locator("text=auto-lock").click();357 await popup.waitForTimeout(300);358 await ctx.close();359 });360});361362test.describe("Lock Screen - Security", () => {363 let ctx: BrowserContext;364 test.afterEach(async () => { await ctx.close(); });365 test.beforeEach(async () => { ctx = await makeContext(); });366367 test("LSC01 - no sensitive data in lock screen DOM", async () => {368 const { popup } = await setupVault(ctx);369 await lockVault(popup);370 const html = await popup.content();371 expect(html).not.toContain("password123");372 expect(html).not.toContain("argon2");373 await ctx.close();374 });375376 test("LSC02 - lock screen has CSP compliance", async () => {377 const { popup } = await setupVault(ctx);378 await lockVault(popup);379 const csp = await popup.evaluate(() => {380 const meta = document.querySelector('meta[http-equiv="Content-Security-Policy"]');381 return meta ? meta.getAttribute("content") : null;382 });383 if (csp) expect(csp).toContain("script-src");384 await ctx.close();385 });386387 test("LSC03 - session storage cleared on lock", async () => {388 const { popup } = await setupVault(ctx);389 await lockVault(popup);390 await popup.waitForTimeout(500);391 await ctx.close();392 });393394 test("LSC04 - memory cleared on lock", async () => {395 const { popup } = await setupVault(ctx);396 await lockVault(popup);397 const cleared = await popup.evaluate(() => {398 return typeof window.sessionStorage !== "undefined";399 });400 expect(cleared).toBeTruthy();401 await ctx.close();402 });403404 test("LSC05 - brute force protection after wrong attempts", async () => {405 const { popup } = await setupVault(ctx);406 await lockVault(popup);407 for (let i = 0; i < 5; i++) {408 await popup.locator("#master-password").fill("wrongpassword");409 await popup.getByRole("button", { name: /unlock/i }).click();410 await popup.waitForTimeout(500);411 }412 await popup.waitForTimeout(1000);413 await popup.locator("#master-password").fill("password123");414 await popup.getByRole("button", { name: /unlock/i }).click();415 await popup.waitForTimeout(2000);416 const entries = popup.locator(".peach-entry-row");417 const hasEntries = await entries.isVisible({ timeout: 3000 }).catch(() => false);418 expect(hasEntries).toBeTruthy();419 await ctx.close();420 });421422 test("LSC06 - lock requires user gesture", async () => {423 const { popup } = await setupVault(ctx);424 const lockBtn = popup.locator(".peach-lock-btn[aria-label='Lock vault']");425 await expect(lockBtn).toBeVisible();426 await ctx.close();427 });428});