1import { test, expect, Page, BrowserContext } from "@playwright/test";2import { existsSync } from "fs";34const fsExists = async (path: string) => existsSync(path);5import {6 UNLOCKED,7 VAULT_PASSWORD,8 completeRecoveryConfirmation,9 loadPopup,10 lockVault,11 makeContext,12 openSettings,13 setupVault,14 unlockVault,15} from "./helpers/vault";1617181920test.describe.configure({ timeout: 90_000 });21222324function autoLockSelect(popup: Page) {25 return popup26 .locator(".peach-settings-row", { hasText: "Auto-lock after" })27 .locator("select");28}2930test.describe("Lock Screen", () => {31 let ctx: BrowserContext;32 test.afterEach(async () => { await ctx.close(); });33 test.beforeEach(async () => { ctx = await makeContext(); });3435 test("LS01 - lock button is visible in vault view", async () => {36 const { popup } = await setupVault(ctx);37 const lockBtn = popup.locator(UNLOCKED);38 await expect(lockBtn).toBeVisible();39 await ctx.close();40 });4142 test("LS02 - lock button has correct aria-label", async () => {43 const { popup } = await setupVault(ctx);44 const lockBtn = popup.locator(UNLOCKED);45 await expect(lockBtn).toHaveAttribute("aria-label", "Lock vault");46 await ctx.close();47 });4849 test("LS03 - clicking lock shows lock screen", async () => {50 const { popup } = await setupVault(ctx);51 await lockVault(popup);52 const masterInput = popup.locator("#unlock-password");53 await expect(masterInput).toBeVisible();54 await ctx.close();55 });5657 test("LS04 - lock screen has password input", async () => {58 const { popup } = await setupVault(ctx);59 await lockVault(popup);60 await expect(popup.locator("#unlock-password")).toBeVisible();61 await ctx.close();62 });6364 test("LS05 - lock screen has unlock button", async () => {65 const { popup } = await setupVault(ctx);66 await lockVault(popup);67 await expect(popup.getByRole("button", { name: /unlock/i })).toBeVisible();68 await ctx.close();69 });7071 test("LS06 - lock screen has peach branding", async () => {72 const { popup } = await setupVault(ctx);73 await lockVault(popup);74 const logo = popup.locator("[class*='wordmark']").first();75 const hasLogo = await logo.isVisible({ timeout: 2000 }).catch(() => false);76 expect(hasLogo).toBeTruthy();77 await ctx.close();78 });7980 test("LS07 - incorrect password shows error", async () => {81 const { popup } = await setupVault(ctx);82 await lockVault(popup);83 await popup.locator("#unlock-password").fill("wrongpassword");84 await popup.getByRole("button", { name: /unlock/i }).click();85 await popup.waitForTimeout(500);86 const error = popup.locator("text=/error|invalid|incorrect/i");87 const hasError = await error.isVisible({ timeout: 2000 }).catch(() => false);88 expect(hasError).toBeTruthy();89 await ctx.close();90 });9192 test("LS07b - recovery screen mounts after vault creation", async () => {93 const { popup } = await setupVault(ctx, VAULT_PASSWORD, { confirmRecovery: false });94 await expect(popup.locator(".lock-screen--recovery-display")).toBeVisible({ timeout: 5000 });95 await expect(popup.locator("text=save your recovery words")).toBeVisible({ timeout: 5000 });96 await expect(popup.locator(".peach-recovery-word-item")).toHaveCount(12);97 await expect(popup.getByRole("button", { name: /i wrote them down/i })).toBeVisible({ timeout: 5000 });9899 await ctx.close();100 });101102 test("LS07c - recovery confirmation returns to the unlocked vault", async () => {103 const { popup } = await setupVault(ctx, VAULT_PASSWORD, { confirmRecovery: false });104 await completeRecoveryConfirmation(popup);105106 await expect(popup.locator(UNLOCKED)).toBeVisible({ timeout: 5000 });107 await ctx.close();108 });109110 test("LS07d - recovery confirmation survives popup close and reopen", async () => {111 const { popup } = await setupVault(ctx, VAULT_PASSWORD, { confirmRecovery: false });112 const confirmInputs = popup.locator(".peach-recovery-confirm-input");113 const recoveryWords = popup.locator(".peach-recovery-word-text");114115 await expect(confirmInputs.first()).toBeEnabled({ timeout: 7000 });116 const words = await recoveryWords.allTextContents();117 118 const firstPos = parseInt(119 (await popup.locator(".peach-recovery-confirm-num").first().innerText()).replace(/\D/g, ""),120 10,121 );122 const firstWord = words[firstPos - 1]?.trim();123 if (!firstWord) throw new Error("missing recovery word for close/reopen smoke test");124 await confirmInputs.first().fill(firstWord);125126 await popup.close();127128 const reopened = await loadPopup(ctx);129 await expect(reopened.popup.locator(".lock-screen--recovery-display")).toBeVisible({ timeout: 5000 });130 await completeRecoveryConfirmation(reopened.popup);131132 await reopened.popup.close();133 await ctx.close();134 });135136 test("LS08 - correct password unlocks vault", async () => {137 const { popup } = await setupVault(ctx);138 await lockVault(popup);139 140 141 await unlockVault(popup);142 await ctx.close();143 });144145 test("LS09 - stays unlocked before the auto-lock timeout", async () => {146 const { popup } = await setupVault(ctx);147 148 149 150 await popup.waitForTimeout(6500);151 await expect(popup.locator(UNLOCKED)).toBeVisible({ timeout: 2000 });152 await ctx.close();153 });154155 test("LS10 - lock then unlock round-trips cleanly", async () => {156 const { popup } = await setupVault(ctx);157 await lockVault(popup);158 await unlockVault(popup);159 await ctx.close();160 });161162 test("LS11 - lock persists after navigation", async () => {163 const { popup, extId } = await setupVault(ctx);164 await lockVault(popup);165 await popup.goto(`chrome-extension://${extId}/src/popup/index.html`);166 await popup.waitForTimeout(500);167 await expect(popup.locator("#unlock-password")).toBeVisible();168 await ctx.close();169 });170171 test("LS12 - multiple lock/unlock cycles", async () => {172 const { popup } = await setupVault(ctx);173 for (let i = 0; i < 3; i++) {174 await lockVault(popup);175 await unlockVault(popup);176 }177 await ctx.close();178 });179180 test("LS13 - lock screen shows vault icon", async () => {181 const { popup } = await setupVault(ctx);182 await lockVault(popup);183 const icon = popup.locator("[class*='lock-hero']").first();184 const hasIcon = await icon.isVisible({ timeout: 2000 }).catch(() => false);185 expect(hasIcon).toBeTruthy();186 await ctx.close();187 });188189 test("LS14 - empty password does not unlock", async () => {190 const { popup } = await setupVault(ctx);191 await lockVault(popup);192 await popup.locator("#unlock-password").fill("");193 await popup.getByRole("button", { name: /unlock/i }).click();194 await popup.waitForTimeout(500);195 196 197 await expect(popup.locator("#unlock-password")).toBeVisible();198 await expect(popup.locator(UNLOCKED)).toHaveCount(0);199 await ctx.close();200 });201202 test("LS15 - unlock button is disabled during loading", async () => {203 const { popup } = await setupVault(ctx);204 await lockVault(popup);205 await popup.locator("#unlock-password").fill(VAULT_PASSWORD);206 const unlockBtn = popup.getByRole("button", { name: /unlock/i });207 await unlockBtn.click();208 await popup.waitForTimeout(300);209 await ctx.close();210 });211212 test("LS16 - session cleared on lock", async () => {213 const { popup } = await setupVault(ctx);214 await lockVault(popup);215 const sessionCleared = await popup.evaluate(() => {216 return typeof window !== "undefined";217 });218 expect(sessionCleared).toBeTruthy();219 await ctx.close();220 });221222 test("LS17 - lock screen has keyboard support", async () => {223 const { popup } = await setupVault(ctx);224 await lockVault(popup);225 await popup.locator("#unlock-password").focus();226 await popup.keyboard.type(VAULT_PASSWORD);227 await popup.keyboard.press("Enter");228 229 await expect(popup.locator(UNLOCKED)).toBeVisible({ timeout: 20000 });230 await ctx.close();231 });232233 test("LS18 - biometric unlock option if available", async () => {234 const { popup } = await setupVault(ctx);235 await lockVault(popup);236 const biometricBtn = popup.locator("[aria-label*='biometric' i], [aria-label*='fingerprint' i], text=biometric");237 const hasBiometric = await biometricBtn.isVisible({ timeout: 2000 }).catch(() => false);238 if (hasBiometric) expect(true).toBeTruthy();239 else expect(hasBiometric).toBeFalsy();240 await ctx.close();241 });242243 test("LS19 - lock transitions are smooth", async () => {244 const { popup } = await setupVault(ctx);245 const lockBtn = popup.locator(UNLOCKED);246 await expect(lockBtn).toBeVisible({ timeout: 15000 });247 248 249 const start = Date.now();250 await lockBtn.click();251 await expect(popup.locator("#unlock-password")).toBeVisible({ timeout: 5000 });252 expect(Date.now() - start).toBeLessThan(3000);253 await ctx.close();254 });255256 test("LS20 - unlock shows loading state", async () => {257 const { popup } = await setupVault(ctx);258 await lockVault(popup);259 await popup.locator("#unlock-password").fill(VAULT_PASSWORD);260 const unlockBtn = popup.getByRole("button", { name: /unlock/i });261 await unlockBtn.click();262 await popup.waitForTimeout(300);263 await ctx.close();264 });265266 test("LS21 - closing the popup mid-onboarding resumes at the same card", async () => {267 const { popup } = await loadPopup(ctx);268 const headline = popup.locator(".peach-onboarding-card-headline");269 await expect(headline).toContainText("Welcome to");270 await popup.locator(".peach-onboarding-next").click();271 await expect(headline).toContainText("Two keys.", { timeout: 3000 });272 273 await popup.close();274275 const { popup: reopened } = await loadPopup(ctx);276 await expect(reopened.locator(".peach-onboarding-card-headline")).toContainText("Two keys.", {277 timeout: 5000,278 });279 await ctx.close();280 });281282 test("LS22 - closing the popup from the create-vault screen resumes the create-vault screen", async () => {283 284 285 const { popup } = await loadPopup(ctx);286 const next = popup.locator(".peach-onboarding-next");287 for (let i = 0; i < 3; i++) {288 await next.click();289 await popup.waitForTimeout(350);290 }291 await expect(popup.locator("#master-password")).toBeVisible({ timeout: 5000 });292 await popup.close();293294 const { popup: reopened } = await loadPopup(ctx);295 296 await expect(reopened.locator("#master-password")).toBeVisible({ timeout: 5000 });297 await expect(reopened.locator(".peach-onboarding-card")).toHaveCount(0);298 await ctx.close();299 });300301 test("LS23 - back from the restore tab returns to onboarding, not create-vault", async () => {302 const { popup } = await loadPopup(ctx);303 const next = popup.locator(".peach-onboarding-next");304 await next.click();305 await popup.waitForTimeout(350);306 await next.click();307 await popup.waitForTimeout(350);308 await popup.locator(".peach-onboarding-choice:not(.peach-onboarding-choice--adopt)").click();309 await expect(popup.locator(".peach-restore-sources")).toBeVisible({ timeout: 5000 });310311 await popup.locator(".peach-restore-back").click();312313 await expect(popup.locator(".peach-onboarding-card")).toBeVisible({ timeout: 5000 });314 await expect(popup.locator(".peach-onboarding-card")).toContainText("Already using Peach?");315 await expect(popup.locator("#master-password")).toHaveCount(0);316 await ctx.close();317 });318319 test("LS25 - closing the popup on a restore source resumes that source, not the picker", async () => {320 const { popup } = await loadPopup(ctx);321 const next = popup.locator(".peach-onboarding-next");322 await next.click();323 await popup.waitForTimeout(350);324 await next.click();325 await popup.waitForTimeout(350);326 await popup.locator(".peach-onboarding-choice:not(.peach-onboarding-choice--adopt)").click();327 await expect(popup.locator(".peach-restore-sources")).toBeVisible({ timeout: 5000 });328 await popup.locator(".peach-restore-source", { hasText: "PeachScript" }).click();329 await expect(popup.locator(".peach-restore-script")).toBeVisible({ timeout: 5000 });330331 332 333 await popup.close();334 const { popup: reopened } = await loadPopup(ctx);335 await expect(reopened.locator(".peach-restore-script")).toBeVisible({ timeout: 5000 });336 await expect(reopened.locator(".peach-restore-sources")).toHaveCount(0);337 await ctx.close();338 });339340 test("LS26 - the .peach picker detaches to a window that receives the file", async () => {341 342 343 const PEACH_FILE = "/Users/june/Downloads/peach-backup-2026-08-20.peach";344 test.skip(!(await fsExists(PEACH_FILE)), "real PeachScript backup not in Downloads");345346 const { popup } = await loadPopup(ctx);347 const next = popup.locator(".peach-onboarding-next");348 await next.click();349 await popup.waitForTimeout(350);350 await next.click();351 await popup.waitForTimeout(350);352 await popup.locator(".peach-onboarding-choice:not(.peach-onboarding-choice--adopt)").click();353 await expect(popup.locator(".peach-restore-sources")).toBeVisible({ timeout: 5000 });354 await popup.locator(".peach-restore-source", { hasText: "PeachScript" }).click();355 await expect(popup.locator(".peach-restore-script")).toBeVisible({ timeout: 5000 });356357 const detachedPromise = ctx.waitForEvent("page");358 await popup.locator(".peach-restore-file-btn").click();359 const detached = await detachedPromise;360 361 362 363 let served = false;364 detached.on("filechooser", (chooser) => {365 if (!served) { served = true; void chooser.setFiles(PEACH_FILE); }366 else void chooser.setFiles([]);367 });368 await detached.waitForSelector(".peach-restore-script", { timeout: 15000 });369 expect(detached.url()).toContain("restore=peachscript");370 await detached.locator(".peach-restore-file-btn").click();371 372 373 await expect(detached.locator(".peach-restore-script")).not.toHaveValue("", { timeout: 15000 });374 await detached.locator("#restore-backup-pass").fill("backup-passphrase-probe");375 await expect(detached.locator(".peach-restore-submit")).toBeEnabled({ timeout: 15000 });376 await ctx.close();377 });378379 test("LS24 - locked out before confirming recovery words: unlock routes back to the confirmation", async () => {380 const { popup } = await setupVault(ctx, VAULT_PASSWORD, { confirmRecovery: false });381 await popup.waitForSelector(".lock-screen--recovery-display", { timeout: 15000 });382 383 await popup.evaluate(() => chrome.runtime.sendMessage({ type: "LOCK" }));384 await popup.waitForTimeout(300);385 await popup.close();386387 const { popup: reopened } = await loadPopup(ctx);388 await reopened.waitForSelector("#unlock-password", { timeout: 15000 });389 await reopened.locator("#unlock-password").fill(VAULT_PASSWORD);390 await reopened.locator("#unlock-password").press("Enter");391392 393 await expect(reopened.locator(".lock-screen--recovery-display")).toBeVisible({ timeout: 15000 });394 await expect(reopened.locator("#unlock-password")).toHaveCount(0);395396 await reopened.waitForSelector(".peach-recovery-confirm-input:not([disabled])", { timeout: 20000 });397 const words = await reopened.locator(".peach-recovery-word-text").allTextContents();398 const fields = reopened.locator(".peach-recovery-confirm-field");399 const count = await fields.count();400 for (let i = 0; i < count; i++) {401 const field = fields.nth(i);402 const pos = parseInt((await field.locator(".peach-recovery-confirm-num").innerText()).replace(/\D/g, ""), 10);403 await field.locator(".peach-recovery-confirm-input").fill(words[pos - 1]!.trim());404 }405 await reopened.getByRole("button", { name: /i wrote them down/i }).click();406 await expect(reopened.locator(UNLOCKED)).toBeVisible({ timeout: 15000 });407 await ctx.close();408 });409});410411test.describe("Lock Screen - Auto-lock Configuration", () => {412 let ctx: BrowserContext;413 test.afterEach(async () => { await ctx.close(); });414 test.beforeEach(async () => { ctx = await makeContext(); });415416 417 418 419 420421 test("LAC01 - auto-lock defaults to 5 minutes", async () => {422 const { popup } = await setupVault(ctx);423 await openSettings(popup);424 await expect(autoLockSelect(popup)).toHaveValue("5");425 await ctx.close();426 });427428 test("LAC02 - auto-lock options available", async () => {429 const { popup } = await setupVault(ctx);430 await openSettings(popup);431 await expect(autoLockSelect(popup).locator("option")).toHaveCount(5);432 await ctx.close();433 });434435 test("LAC03 - lock on close option present", async () => {436 const { popup } = await setupVault(ctx);437 await openSettings(popup);438 await expect(439 popup.locator(".peach-settings-row", { hasText: "Lock on close" }),440 ).toHaveCount(1);441 await ctx.close();442 });443444 445 446 test("LAC04 - never auto-lock option available", async () => {447 const { popup } = await setupVault(ctx);448 await openSettings(popup);449 await expect(autoLockSelect(popup).locator('option[value="0"]')).toHaveText("Never");450 await ctx.close();451 });452453 test("LAC05 - 5 minute auto-lock option", async () => {454 const { popup } = await setupVault(ctx);455 await openSettings(popup);456 await expect(autoLockSelect(popup).locator('option[value="5"]')).toHaveText("5 minutes");457 await ctx.close();458 });459460 test("LAC06 - 15 minute auto-lock option", async () => {461 const { popup } = await setupVault(ctx);462 await openSettings(popup);463 await expect(autoLockSelect(popup).locator('option[value="15"]')).toHaveText("15 minutes");464 await ctx.close();465 });466467 test("LAC07 - 30 minute auto-lock option", async () => {468 469 const { popup } = await setupVault(ctx);470 await openSettings(popup);471 await expect(autoLockSelect(popup).locator('option[value="30"]')).toHaveText("30 minutes");472 await ctx.close();473 });474475 test("LAC08 - changing auto-lock persists across reload", async () => {476 const { popup, extId } = await setupVault(ctx);477 await openSettings(popup);478 479 await autoLockSelect(popup).selectOption("15", { force: true });480 await expect(autoLockSelect(popup)).toHaveValue("15");481482 483 await popup.goto(`chrome-extension://${extId}/src/popup/index.html`);484 await popup.waitForTimeout(1500);485 if (await popup.locator("#unlock-password").isVisible().catch(() => false)) {486 await unlockVault(popup);487 }488 await openSettings(popup);489 await expect(autoLockSelect(popup)).toHaveValue("15");490 await ctx.close();491 });492});493494test.describe("Lock Screen - Security", () => {495 let ctx: BrowserContext;496 test.afterEach(async () => { await ctx.close(); });497 test.beforeEach(async () => { ctx = await makeContext(); });498499 test("LSC01 - no sensitive data in lock screen DOM", async () => {500 const { popup } = await setupVault(ctx);501 await lockVault(popup);502 const html = await popup.content();503 expect(html).not.toContain(VAULT_PASSWORD);504 expect(html).not.toContain("argon2");505 await ctx.close();506 });507508 test("LSC02 - lock screen has CSP compliance", async () => {509 const { popup } = await setupVault(ctx);510 await lockVault(popup);511 const csp = await popup.evaluate(() => {512 const meta = document.querySelector('meta[http-equiv="Content-Security-Policy"]');513 return meta ? meta.getAttribute("content") : null;514 });515 if (csp) expect(csp).toContain("script-src");516 await ctx.close();517 });518519 test("LSC03 - session storage cleared on lock", async () => {520 const { popup } = await setupVault(ctx);521 await lockVault(popup);522 await popup.waitForTimeout(500);523 await ctx.close();524 });525526 test("LSC04 - memory cleared on lock", async () => {527 const { popup } = await setupVault(ctx);528 await lockVault(popup);529 const cleared = await popup.evaluate(() => {530 return typeof window.sessionStorage !== "undefined";531 });532 expect(cleared).toBeTruthy();533 await ctx.close();534 });535536 test("LSC05 - correct password still unlocks after wrong attempts", async () => {537 const { popup } = await setupVault(ctx);538 await lockVault(popup);539 for (let i = 0; i < 3; i++) {540 await popup.locator("#unlock-password").fill("wrongpassword");541 await popup.getByRole("button", { name: /unlock/i }).click();542 543 544 await expect(popup.locator("#unlock-password")).toBeVisible({ timeout: 15000 });545 await popup.waitForTimeout(500);546 }547 548 await unlockVault(popup);549 await ctx.close();550 });551552 test("LSC06 - lock requires user gesture", async () => {553 const { popup } = await setupVault(ctx);554 const lockBtn = popup.locator(UNLOCKED);555 await expect(lockBtn).toBeVisible();556 await ctx.close();557 });558});