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 371 372 try {373 await expect(detached.locator(".peach-rune-canvas")).toBeVisible({ timeout: 3000 });374 } catch {375 try { await detached.locator(".peach-restore-file-btn").click({ timeout: 5000 }); } catch { }376 }377 await expect(detached.locator(".peach-rune-canvas")).toBeVisible({ timeout: 15000 });378 await detached.locator("#restore-backup-pass").fill("backup-passphrase-probe");379 await expect(detached.locator(".peach-restore-submit")).toBeEnabled({ timeout: 15000 });380 await ctx.close();381 });382383 test("LS24 - locked out before confirming recovery words: unlock routes back to the confirmation", async () => {384 const { popup } = await setupVault(ctx, VAULT_PASSWORD, { confirmRecovery: false });385 await popup.waitForSelector(".lock-screen--recovery-display", { timeout: 15000 });386 387 await popup.evaluate(() => chrome.runtime.sendMessage({ type: "LOCK" }));388 await popup.waitForTimeout(300);389 await popup.close();390391 const { popup: reopened } = await loadPopup(ctx);392 await reopened.waitForSelector("#unlock-password", { timeout: 15000 });393 await reopened.locator("#unlock-password").fill(VAULT_PASSWORD);394 await reopened.locator("#unlock-password").press("Enter");395396 397 await expect(reopened.locator(".lock-screen--recovery-display")).toBeVisible({ timeout: 15000 });398 await expect(reopened.locator("#unlock-password")).toHaveCount(0);399400 await reopened.waitForSelector(".peach-recovery-confirm-input:not([disabled])", { timeout: 20000 });401 const words = await reopened.locator(".peach-recovery-word-text").allTextContents();402 const fields = reopened.locator(".peach-recovery-confirm-field");403 const count = await fields.count();404 for (let i = 0; i < count; i++) {405 const field = fields.nth(i);406 const pos = parseInt((await field.locator(".peach-recovery-confirm-num").innerText()).replace(/\D/g, ""), 10);407 await field.locator(".peach-recovery-confirm-input").fill(words[pos - 1]!.trim());408 }409 await reopened.getByRole("button", { name: /i wrote them down/i }).click();410 await expect(reopened.locator(UNLOCKED)).toBeVisible({ timeout: 15000 });411 await ctx.close();412 });413});414415test.describe("Lock Screen - Auto-lock Configuration", () => {416 let ctx: BrowserContext;417 test.afterEach(async () => { await ctx.close(); });418 test.beforeEach(async () => { ctx = await makeContext(); });419420 421 422 423 424425 test("LAC01 - auto-lock defaults to 5 minutes", async () => {426 const { popup } = await setupVault(ctx);427 await openSettings(popup);428 await expect(autoLockSelect(popup)).toHaveValue("5");429 await ctx.close();430 });431432 test("LAC02 - auto-lock options available", async () => {433 const { popup } = await setupVault(ctx);434 await openSettings(popup);435 await expect(autoLockSelect(popup).locator("option")).toHaveCount(5);436 await ctx.close();437 });438439 test("LAC03 - lock on close option present", async () => {440 const { popup } = await setupVault(ctx);441 await openSettings(popup);442 await expect(443 popup.locator(".peach-settings-row", { hasText: "Lock on close" }),444 ).toHaveCount(1);445 await ctx.close();446 });447448 449 450 test("LAC04 - never auto-lock option available", async () => {451 const { popup } = await setupVault(ctx);452 await openSettings(popup);453 await expect(autoLockSelect(popup).locator('option[value="0"]')).toHaveText("Never");454 await ctx.close();455 });456457 test("LAC05 - 5 minute auto-lock option", async () => {458 const { popup } = await setupVault(ctx);459 await openSettings(popup);460 await expect(autoLockSelect(popup).locator('option[value="5"]')).toHaveText("5 minutes");461 await ctx.close();462 });463464 test("LAC06 - 15 minute auto-lock option", async () => {465 const { popup } = await setupVault(ctx);466 await openSettings(popup);467 await expect(autoLockSelect(popup).locator('option[value="15"]')).toHaveText("15 minutes");468 await ctx.close();469 });470471 test("LAC07 - 30 minute auto-lock option", async () => {472 473 const { popup } = await setupVault(ctx);474 await openSettings(popup);475 await expect(autoLockSelect(popup).locator('option[value="30"]')).toHaveText("30 minutes");476 await ctx.close();477 });478479 test("LAC08 - changing auto-lock persists across reload", async () => {480 const { popup, extId } = await setupVault(ctx);481 await openSettings(popup);482 483 await autoLockSelect(popup).selectOption("15", { force: true });484 await expect(autoLockSelect(popup)).toHaveValue("15");485486 487 await popup.goto(`chrome-extension://${extId}/src/popup/index.html`);488 await popup.waitForTimeout(1500);489 if (await popup.locator("#unlock-password").isVisible().catch(() => false)) {490 await unlockVault(popup);491 }492 await openSettings(popup);493 await expect(autoLockSelect(popup)).toHaveValue("15");494 await ctx.close();495 });496});497498test.describe("Lock Screen - Security", () => {499 let ctx: BrowserContext;500 test.afterEach(async () => { await ctx.close(); });501 test.beforeEach(async () => { ctx = await makeContext(); });502503 test("LSC01 - no sensitive data in lock screen DOM", async () => {504 const { popup } = await setupVault(ctx);505 await lockVault(popup);506 const html = await popup.content();507 expect(html).not.toContain(VAULT_PASSWORD);508 expect(html).not.toContain("argon2");509 await ctx.close();510 });511512 test("LSC02 - lock screen has CSP compliance", async () => {513 const { popup } = await setupVault(ctx);514 await lockVault(popup);515 const csp = await popup.evaluate(() => {516 const meta = document.querySelector('meta[http-equiv="Content-Security-Policy"]');517 return meta ? meta.getAttribute("content") : null;518 });519 if (csp) expect(csp).toContain("script-src");520 await ctx.close();521 });522523 test("LSC03 - session storage cleared on lock", async () => {524 const { popup } = await setupVault(ctx);525 await lockVault(popup);526 await popup.waitForTimeout(500);527 await ctx.close();528 });529530 test("LSC04 - memory cleared on lock", async () => {531 const { popup } = await setupVault(ctx);532 await lockVault(popup);533 const cleared = await popup.evaluate(() => {534 return typeof window.sessionStorage !== "undefined";535 });536 expect(cleared).toBeTruthy();537 await ctx.close();538 });539540 test("LSC05 - correct password still unlocks after wrong attempts", async () => {541 const { popup } = await setupVault(ctx);542 await lockVault(popup);543 for (let i = 0; i < 3; i++) {544 await popup.locator("#unlock-password").fill("wrongpassword");545 await popup.getByRole("button", { name: /unlock/i }).click();546 547 548 await expect(popup.locator("#unlock-password")).toBeVisible({ timeout: 15000 });549 await popup.waitForTimeout(500);550 }551 552 await unlockVault(popup);553 await ctx.close();554 });555556 test("LSC06 - lock requires user gesture", async () => {557 const { popup } = await setupVault(ctx);558 const lockBtn = popup.locator(UNLOCKED);559 await expect(lockBtn).toBeVisible();560 await ctx.close();561 });562});