1import { test, expect, Page, BrowserContext } from "@playwright/test";2import {3 UNLOCKED,4 VAULT_PASSWORD,5 completeRecoveryConfirmation,6 loadPopup,7 lockVault,8 makeContext,9 openSettings,10 setupVault,11 unlockVault,12} from "./helpers/vault";1314151617test.describe.configure({ timeout: 90_000 });18192021function autoLockSelect(popup: Page) {22 return popup23 .locator(".peach-settings-row", { hasText: "Auto-lock after" })24 .locator("select");25}2627test.describe("Lock Screen", () => {28 let ctx: BrowserContext;29 test.afterEach(async () => { await ctx.close(); });30 test.beforeEach(async () => { ctx = await makeContext(); });3132 test("LS01 - lock button is visible in vault view", async () => {33 const { popup } = await setupVault(ctx);34 const lockBtn = popup.locator(UNLOCKED);35 await expect(lockBtn).toBeVisible();36 await ctx.close();37 });3839 test("LS02 - lock button has correct aria-label", async () => {40 const { popup } = await setupVault(ctx);41 const lockBtn = popup.locator(UNLOCKED);42 await expect(lockBtn).toHaveAttribute("aria-label", "Lock vault");43 await ctx.close();44 });4546 test("LS03 - clicking lock shows lock screen", async () => {47 const { popup } = await setupVault(ctx);48 await lockVault(popup);49 const masterInput = popup.locator("#unlock-password");50 await expect(masterInput).toBeVisible();51 await ctx.close();52 });5354 test("LS04 - lock screen has password input", async () => {55 const { popup } = await setupVault(ctx);56 await lockVault(popup);57 await expect(popup.locator("#unlock-password")).toBeVisible();58 await ctx.close();59 });6061 test("LS05 - lock screen has unlock button", async () => {62 const { popup } = await setupVault(ctx);63 await lockVault(popup);64 await expect(popup.getByRole("button", { name: /unlock/i })).toBeVisible();65 await ctx.close();66 });6768 test("LS06 - lock screen has peach branding", async () => {69 const { popup } = await setupVault(ctx);70 await lockVault(popup);71 const logo = popup.locator("[class*='wordmark']").first();72 const hasLogo = await logo.isVisible({ timeout: 2000 }).catch(() => false);73 expect(hasLogo).toBeTruthy();74 await ctx.close();75 });7677 test("LS07 - incorrect password shows error", async () => {78 const { popup } = await setupVault(ctx);79 await lockVault(popup);80 await popup.locator("#unlock-password").fill("wrongpassword");81 await popup.getByRole("button", { name: /unlock/i }).click();82 await popup.waitForTimeout(500);83 const error = popup.locator("text=/error|invalid|incorrect/i");84 const hasError = await error.isVisible({ timeout: 2000 }).catch(() => false);85 expect(hasError).toBeTruthy();86 await ctx.close();87 });8889 test("LS07b - recovery screen mounts after vault creation", async () => {90 const { popup } = await setupVault(ctx, VAULT_PASSWORD, { confirmRecovery: false });91 await expect(popup.locator(".lock-screen--recovery-display")).toBeVisible({ timeout: 5000 });92 await expect(popup.locator("text=save your recovery words")).toBeVisible({ timeout: 5000 });93 await expect(popup.locator(".peach-recovery-word-item")).toHaveCount(12);94 await expect(popup.getByRole("button", { name: /i wrote them down/i })).toBeVisible({ timeout: 5000 });9596 await ctx.close();97 });9899 test("LS07c - recovery confirmation returns to the unlocked vault", async () => {100 const { popup } = await setupVault(ctx, VAULT_PASSWORD, { confirmRecovery: false });101 await completeRecoveryConfirmation(popup);102103 await expect(popup.locator(UNLOCKED)).toBeVisible({ timeout: 5000 });104 await ctx.close();105 });106107 test("LS07d - recovery confirmation survives popup close and reopen", async () => {108 const { popup } = await setupVault(ctx, VAULT_PASSWORD, { confirmRecovery: false });109 const confirmInputs = popup.locator(".peach-recovery-confirm-input");110 const recoveryWords = popup.locator(".peach-recovery-word-text");111112 await expect(confirmInputs.first()).toBeEnabled({ timeout: 7000 });113 const words = await recoveryWords.allTextContents();114 const firstWord = words[2]?.trim();115 if (!firstWord) throw new Error("missing recovery word for close/reopen smoke test");116 await confirmInputs.first().fill(firstWord);117118 await popup.close();119120 const reopened = await loadPopup(ctx);121 await expect(reopened.popup.locator(".lock-screen--recovery-display")).toBeVisible({ timeout: 5000 });122 await completeRecoveryConfirmation(reopened.popup);123124 await reopened.popup.close();125 await ctx.close();126 });127128 test("LS08 - correct password unlocks vault", async () => {129 const { popup } = await setupVault(ctx);130 await lockVault(popup);131 132 133 await unlockVault(popup);134 await ctx.close();135 });136137 test("LS09 - stays unlocked before the auto-lock timeout", async () => {138 const { popup } = await setupVault(ctx);139 140 141 142 await popup.waitForTimeout(6500);143 await expect(popup.locator(UNLOCKED)).toBeVisible({ timeout: 2000 });144 await ctx.close();145 });146147 test("LS10 - lock then unlock round-trips cleanly", async () => {148 const { popup } = await setupVault(ctx);149 await lockVault(popup);150 await unlockVault(popup);151 await ctx.close();152 });153154 test("LS11 - lock persists after navigation", async () => {155 const { popup, extId } = await setupVault(ctx);156 await lockVault(popup);157 await popup.goto(`chrome-extension://${extId}/src/popup/index.html`);158 await popup.waitForTimeout(500);159 await expect(popup.locator("#unlock-password")).toBeVisible();160 await ctx.close();161 });162163 test("LS12 - multiple lock/unlock cycles", async () => {164 const { popup } = await setupVault(ctx);165 for (let i = 0; i < 3; i++) {166 await lockVault(popup);167 await unlockVault(popup);168 }169 await ctx.close();170 });171172 test("LS13 - lock screen shows vault icon", async () => {173 const { popup } = await setupVault(ctx);174 await lockVault(popup);175 const icon = popup.locator("[class*='lock-hero']").first();176 const hasIcon = await icon.isVisible({ timeout: 2000 }).catch(() => false);177 expect(hasIcon).toBeTruthy();178 await ctx.close();179 });180181 test("LS14 - empty password does not unlock", async () => {182 const { popup } = await setupVault(ctx);183 await lockVault(popup);184 await popup.locator("#unlock-password").fill("");185 await popup.getByRole("button", { name: /unlock/i }).click();186 await popup.waitForTimeout(500);187 188 189 await expect(popup.locator("#unlock-password")).toBeVisible();190 await expect(popup.locator(UNLOCKED)).toHaveCount(0);191 await ctx.close();192 });193194 test("LS15 - unlock button is disabled during loading", async () => {195 const { popup } = await setupVault(ctx);196 await lockVault(popup);197 await popup.locator("#unlock-password").fill(VAULT_PASSWORD);198 const unlockBtn = popup.getByRole("button", { name: /unlock/i });199 await unlockBtn.click();200 await popup.waitForTimeout(300);201 await ctx.close();202 });203204 test("LS16 - session cleared on lock", async () => {205 const { popup } = await setupVault(ctx);206 await lockVault(popup);207 const sessionCleared = await popup.evaluate(() => {208 return typeof window !== "undefined";209 });210 expect(sessionCleared).toBeTruthy();211 await ctx.close();212 });213214 test("LS17 - lock screen has keyboard support", async () => {215 const { popup } = await setupVault(ctx);216 await lockVault(popup);217 await popup.locator("#unlock-password").focus();218 await popup.keyboard.type(VAULT_PASSWORD);219 await popup.keyboard.press("Enter");220 221 await expect(popup.locator(UNLOCKED)).toBeVisible({ timeout: 20000 });222 await ctx.close();223 });224225 test("LS18 - biometric unlock option if available", async () => {226 const { popup } = await setupVault(ctx);227 await lockVault(popup);228 const biometricBtn = popup.locator("[aria-label*='biometric' i], [aria-label*='fingerprint' i], text=biometric");229 const hasBiometric = await biometricBtn.isVisible({ timeout: 2000 }).catch(() => false);230 if (hasBiometric) expect(true).toBeTruthy();231 else expect(hasBiometric).toBeFalsy();232 await ctx.close();233 });234235 test("LS19 - lock transitions are smooth", async () => {236 const { popup } = await setupVault(ctx);237 const lockBtn = popup.locator(UNLOCKED);238 await expect(lockBtn).toBeVisible({ timeout: 15000 });239 240 241 const start = Date.now();242 await lockBtn.click();243 await expect(popup.locator("#unlock-password")).toBeVisible({ timeout: 5000 });244 expect(Date.now() - start).toBeLessThan(3000);245 await ctx.close();246 });247248 test("LS20 - unlock shows loading state", async () => {249 const { popup } = await setupVault(ctx);250 await lockVault(popup);251 await popup.locator("#unlock-password").fill(VAULT_PASSWORD);252 const unlockBtn = popup.getByRole("button", { name: /unlock/i });253 await unlockBtn.click();254 await popup.waitForTimeout(300);255 await ctx.close();256 });257});258259test.describe("Lock Screen - Auto-lock Configuration", () => {260 let ctx: BrowserContext;261 test.afterEach(async () => { await ctx.close(); });262 test.beforeEach(async () => { ctx = await makeContext(); });263264 265 266 267 268269 test("LAC01 - auto-lock defaults to 5 minutes", async () => {270 const { popup } = await setupVault(ctx);271 await openSettings(popup);272 await expect(autoLockSelect(popup)).toHaveValue("5");273 await ctx.close();274 });275276 test("LAC02 - auto-lock options available", async () => {277 const { popup } = await setupVault(ctx);278 await openSettings(popup);279 await expect(autoLockSelect(popup).locator("option")).toHaveCount(5);280 await ctx.close();281 });282283 test("LAC03 - lock on close option present", async () => {284 const { popup } = await setupVault(ctx);285 await openSettings(popup);286 await expect(287 popup.locator(".peach-settings-row", { hasText: "Lock on close" }),288 ).toHaveCount(1);289 await ctx.close();290 });291292 293 294 test("LAC04 - never auto-lock option available", async () => {295 const { popup } = await setupVault(ctx);296 await openSettings(popup);297 await expect(autoLockSelect(popup).locator('option[value="0"]')).toHaveText("Never");298 await ctx.close();299 });300301 test("LAC05 - 5 minute auto-lock option", async () => {302 const { popup } = await setupVault(ctx);303 await openSettings(popup);304 await expect(autoLockSelect(popup).locator('option[value="5"]')).toHaveText("5 minutes");305 await ctx.close();306 });307308 test("LAC06 - 15 minute auto-lock option", async () => {309 const { popup } = await setupVault(ctx);310 await openSettings(popup);311 await expect(autoLockSelect(popup).locator('option[value="15"]')).toHaveText("15 minutes");312 await ctx.close();313 });314315 test("LAC07 - 30 minute auto-lock option", async () => {316 317 const { popup } = await setupVault(ctx);318 await openSettings(popup);319 await expect(autoLockSelect(popup).locator('option[value="30"]')).toHaveText("30 minutes");320 await ctx.close();321 });322323 test("LAC08 - changing auto-lock persists across reload", async () => {324 const { popup, extId } = await setupVault(ctx);325 await openSettings(popup);326 327 await autoLockSelect(popup).selectOption("15", { force: true });328 await expect(autoLockSelect(popup)).toHaveValue("15");329330 331 await popup.goto(`chrome-extension://${extId}/src/popup/index.html`);332 await popup.waitForTimeout(1500);333 if (await popup.locator("#unlock-password").isVisible().catch(() => false)) {334 await unlockVault(popup);335 }336 await openSettings(popup);337 await expect(autoLockSelect(popup)).toHaveValue("15");338 await ctx.close();339 });340});341342test.describe("Lock Screen - Security", () => {343 let ctx: BrowserContext;344 test.afterEach(async () => { await ctx.close(); });345 test.beforeEach(async () => { ctx = await makeContext(); });346347 test("LSC01 - no sensitive data in lock screen DOM", async () => {348 const { popup } = await setupVault(ctx);349 await lockVault(popup);350 const html = await popup.content();351 expect(html).not.toContain(VAULT_PASSWORD);352 expect(html).not.toContain("argon2");353 await ctx.close();354 });355356 test("LSC02 - lock screen has CSP compliance", async () => {357 const { popup } = await setupVault(ctx);358 await lockVault(popup);359 const csp = await popup.evaluate(() => {360 const meta = document.querySelector('meta[http-equiv="Content-Security-Policy"]');361 return meta ? meta.getAttribute("content") : null;362 });363 if (csp) expect(csp).toContain("script-src");364 await ctx.close();365 });366367 test("LSC03 - session storage cleared on lock", async () => {368 const { popup } = await setupVault(ctx);369 await lockVault(popup);370 await popup.waitForTimeout(500);371 await ctx.close();372 });373374 test("LSC04 - memory cleared on lock", async () => {375 const { popup } = await setupVault(ctx);376 await lockVault(popup);377 const cleared = await popup.evaluate(() => {378 return typeof window.sessionStorage !== "undefined";379 });380 expect(cleared).toBeTruthy();381 await ctx.close();382 });383384 test("LSC05 - correct password still unlocks after wrong attempts", async () => {385 const { popup } = await setupVault(ctx);386 await lockVault(popup);387 for (let i = 0; i < 3; i++) {388 await popup.locator("#unlock-password").fill("wrongpassword");389 await popup.getByRole("button", { name: /unlock/i }).click();390 391 392 await expect(popup.locator("#unlock-password")).toBeVisible({ timeout: 15000 });393 await popup.waitForTimeout(500);394 }395 396 await unlockVault(popup);397 await ctx.close();398 });399400 test("LSC06 - lock requires user gesture", async () => {401 const { popup } = await setupVault(ctx);402 const lockBtn = popup.locator(UNLOCKED);403 await expect(lockBtn).toBeVisible();404 await ctx.close();405 });406});