1import { test, expect, type Page, type BrowserContext } from "@playwright/test";2import { fileURLToPath } from "node:url";34const PEACH_FILE = fileURLToPath(new URL("./fixtures/synthetic-backup.peach", import.meta.url));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("LS19 - lock transitions are smooth", async () => {234 const { popup } = await setupVault(ctx);235 const lockBtn = popup.locator(UNLOCKED);236 await expect(lockBtn).toBeVisible({ timeout: 15000 });237 238 239 const start = Date.now();240 await lockBtn.click();241 await expect(popup.locator("#unlock-password")).toBeVisible({ timeout: 5000 });242 expect(Date.now() - start).toBeLessThan(3000);243 await ctx.close();244 });245246 test("LS20 - unlock shows loading state", async () => {247 const { popup } = await setupVault(ctx);248 await lockVault(popup);249 await popup.locator("#unlock-password").fill(VAULT_PASSWORD);250 const unlockBtn = popup.getByRole("button", { name: /unlock/i });251 await unlockBtn.click();252 await popup.waitForTimeout(300);253 await ctx.close();254 });255256 test("LS21 - closing the popup mid-onboarding resumes at the same card", async () => {257 const { popup } = await loadPopup(ctx);258 const headline = popup.locator(".peach-onboarding-card-headline");259 await expect(headline).toContainText("Welcome to");260 await popup.locator(".peach-onboarding-next").click();261 await expect(headline).toContainText("Two keys.", { timeout: 3000 });262 263 await popup.close();264265 const { popup: reopened } = await loadPopup(ctx);266 await expect(reopened.locator(".peach-onboarding-card-headline")).toContainText("Two keys.", {267 timeout: 5000,268 });269 await ctx.close();270 });271272 test("LS22 - closing the popup from the create-vault screen resumes the create-vault screen", async () => {273 274 275 const { popup } = await loadPopup(ctx);276 const next = popup.locator(".peach-onboarding-next");277 for (let i = 0; i < 3; i++) {278 await next.click();279 await popup.waitForTimeout(350);280 }281 await expect(popup.locator("#master-password")).toBeVisible({ timeout: 5000 });282 await popup.close();283284 const { popup: reopened } = await loadPopup(ctx);285 286 await expect(reopened.locator("#master-password")).toBeVisible({ timeout: 5000 });287 await expect(reopened.locator(".peach-onboarding-card")).toHaveCount(0);288 await ctx.close();289 });290291 test("LS23 - back from the restore tab returns to onboarding, not create-vault", async () => {292 const { popup } = await loadPopup(ctx);293 const next = popup.locator(".peach-onboarding-next");294 await next.click();295 await popup.waitForTimeout(350);296 await next.click();297 await popup.waitForTimeout(350);298 await popup.locator(".peach-onboarding-choice:not(.peach-onboarding-choice--adopt)").click();299 await expect(popup.locator(".peach-restore-sources")).toBeVisible({ timeout: 5000 });300301 await popup.locator(".peach-restore-back").click();302303 await expect(popup.locator(".peach-onboarding-card")).toBeVisible({ timeout: 5000 });304 await expect(popup.locator(".peach-onboarding-card")).toContainText("Already using Peach?");305 await expect(popup.locator("#master-password")).toHaveCount(0);306 await ctx.close();307 });308309 test("LS25 - closing the popup on a restore source resumes that source, not the picker", async () => {310 const { popup } = await loadPopup(ctx);311 const next = popup.locator(".peach-onboarding-next");312 await next.click();313 await popup.waitForTimeout(350);314 await next.click();315 await popup.waitForTimeout(350);316 await popup.locator(".peach-onboarding-choice:not(.peach-onboarding-choice--adopt)").click();317 await expect(popup.locator(".peach-restore-sources")).toBeVisible({ timeout: 5000 });318 await popup.locator(".peach-restore-source", { hasText: "PeachScript" }).click();319 await expect(popup.locator(".peach-restore-script")).toBeVisible({ timeout: 5000 });320321 322 323 await popup.close();324 const { popup: reopened } = await loadPopup(ctx);325 await expect(reopened.locator(".peach-restore-script")).toBeVisible({ timeout: 5000 });326 await expect(reopened.locator(".peach-restore-sources")).toHaveCount(0);327 await ctx.close();328 });329330 test("LS26 - the .peach picker detaches to a window that receives the file", async () => {331 332 333 const { popup } = await loadPopup(ctx);334 const next = popup.locator(".peach-onboarding-next");335 await next.click();336 await popup.waitForTimeout(350);337 await next.click();338 await popup.waitForTimeout(350);339 await popup.locator(".peach-onboarding-choice:not(.peach-onboarding-choice--adopt)").click();340 await expect(popup.locator(".peach-restore-sources")).toBeVisible({ timeout: 5000 });341 await popup.locator(".peach-restore-source", { hasText: "PeachScript" }).click();342 await expect(popup.locator(".peach-restore-script")).toBeVisible({ timeout: 5000 });343344 const detachedPromise = ctx.waitForEvent("page");345 await popup.locator(".peach-restore-file-btn").click();346 const detached = await detachedPromise;347 348 349 350 let served = false;351 detached.on("filechooser", (chooser) => {352 if (!served) { served = true; void chooser.setFiles(PEACH_FILE); }353 else void chooser.setFiles([]);354 });355 await detached.waitForSelector(".peach-restore-script", { timeout: 15000 });356 expect(detached.url()).toContain("restore=peachscript");357 358 359 try {360 await expect(detached.locator(".peach-rune-canvas")).toBeVisible({ timeout: 3000 });361 } catch {362 try { await detached.locator(".peach-restore-file-btn").click({ timeout: 5000 }); } catch { }363 }364 await expect(detached.locator(".peach-rune-canvas")).toBeVisible({ timeout: 15000 });365 await detached.locator("#restore-backup-pass").fill("backup-passphrase-probe");366 await expect(detached.locator(".peach-restore-submit")).toBeEnabled({ timeout: 15000 });367 await ctx.close();368 });369370 test("LS24 - locked out before confirming recovery words: unlock routes back to the confirmation", async () => {371 const { popup } = await setupVault(ctx, VAULT_PASSWORD, { confirmRecovery: false });372 await popup.waitForSelector(".lock-screen--recovery-display", { timeout: 15000 });373 374 await popup.evaluate(() => chrome.runtime.sendMessage({ type: "LOCK" }));375 await popup.waitForTimeout(300);376 await popup.close();377378 const { popup: reopened } = await loadPopup(ctx);379 await reopened.waitForSelector("#unlock-password", { timeout: 15000 });380 await reopened.locator("#unlock-password").fill(VAULT_PASSWORD);381 await reopened.locator("#unlock-password").press("Enter");382383 384 await expect(reopened.locator(".lock-screen--recovery-display")).toBeVisible({ timeout: 15000 });385 await expect(reopened.locator("#unlock-password")).toHaveCount(0);386387 await reopened.waitForSelector(".peach-recovery-confirm-input:not([disabled])", { timeout: 20000 });388 const words = await reopened.locator(".peach-recovery-word-text").allTextContents();389 const fields = reopened.locator(".peach-recovery-confirm-field");390 const count = await fields.count();391 for (let i = 0; i < count; i++) {392 const field = fields.nth(i);393 const pos = parseInt((await field.locator(".peach-recovery-confirm-num").innerText()).replace(/\D/g, ""), 10);394 await field.locator(".peach-recovery-confirm-input").fill(words[pos - 1]!.trim());395 }396 await reopened.getByRole("button", { name: /i wrote them down/i }).click();397 await expect(reopened.locator(UNLOCKED)).toBeVisible({ timeout: 15000 });398 await ctx.close();399 });400});401402test.describe("Lock Screen - Auto-lock Configuration", () => {403 let ctx: BrowserContext;404 test.afterEach(async () => { await ctx.close(); });405 test.beforeEach(async () => { ctx = await makeContext(); });406407 408 409 410 411412 test("LAC01 - auto-lock defaults to 5 minutes", async () => {413 const { popup } = await setupVault(ctx);414 await openSettings(popup);415 await expect(autoLockSelect(popup)).toHaveValue("5");416 await ctx.close();417 });418419 test("LAC02 - auto-lock options available", async () => {420 const { popup } = await setupVault(ctx);421 await openSettings(popup);422 await expect(autoLockSelect(popup).locator("option")).toHaveCount(5);423 await ctx.close();424 });425426 test("LAC03 - lock on close option present", async () => {427 const { popup } = await setupVault(ctx);428 await openSettings(popup);429 await expect(430 popup.locator(".peach-settings-row", { hasText: "Lock on close" }),431 ).toHaveCount(1);432 await ctx.close();433 });434435 436 437 test("LAC04 - never auto-lock option available", async () => {438 const { popup } = await setupVault(ctx);439 await openSettings(popup);440 await expect(autoLockSelect(popup).locator('option[value="0"]')).toHaveText("Never");441 await ctx.close();442 });443444 test("LAC05 - 5 minute auto-lock option", async () => {445 const { popup } = await setupVault(ctx);446 await openSettings(popup);447 await expect(autoLockSelect(popup).locator('option[value="5"]')).toHaveText("5 minutes");448 await ctx.close();449 });450451 test("LAC06 - 15 minute auto-lock option", async () => {452 const { popup } = await setupVault(ctx);453 await openSettings(popup);454 await expect(autoLockSelect(popup).locator('option[value="15"]')).toHaveText("15 minutes");455 await ctx.close();456 });457458 test("LAC07 - 30 minute auto-lock option", async () => {459 460 const { popup } = await setupVault(ctx);461 await openSettings(popup);462 await expect(autoLockSelect(popup).locator('option[value="30"]')).toHaveText("30 minutes");463 await ctx.close();464 });465466 test("LAC08 - changing auto-lock persists across reload", async () => {467 const { popup, extId } = await setupVault(ctx);468 await openSettings(popup);469 470 await autoLockSelect(popup).selectOption("15", { force: true });471 await expect(autoLockSelect(popup)).toHaveValue("15");472473 474 await popup.goto(`chrome-extension://${extId}/src/popup/index.html`);475 await popup.waitForTimeout(1500);476 if (await popup.locator("#unlock-password").isVisible().catch(() => false)) {477 await unlockVault(popup);478 }479 await openSettings(popup);480 await expect(autoLockSelect(popup)).toHaveValue("15");481 await ctx.close();482 });483});484485test.describe("Lock Screen - Security", () => {486 let ctx: BrowserContext;487 test.afterEach(async () => { await ctx.close(); });488 test.beforeEach(async () => { ctx = await makeContext(); });489490 test("LSC01 - no sensitive data in lock screen DOM", async () => {491 const { popup } = await setupVault(ctx);492 await lockVault(popup);493 const html = await popup.content();494 expect(html).not.toContain(VAULT_PASSWORD);495 expect(html).not.toContain("argon2");496 await ctx.close();497 });498499 test("LSC02 - lock screen has CSP compliance", async () => {500 const { popup } = await setupVault(ctx);501 await lockVault(popup);502 const csp = await popup.evaluate(() => {503 const meta = document.querySelector('meta[http-equiv="Content-Security-Policy"]');504 return meta ? meta.getAttribute("content") : null;505 });506 if (csp) expect(csp).toContain("script-src");507 await ctx.close();508 });509510 test("LSC03 - session storage cleared on lock", async () => {511 const { popup } = await setupVault(ctx);512 await lockVault(popup);513 await popup.waitForTimeout(500);514 await ctx.close();515 });516517 test("LSC04 - memory cleared on lock", async () => {518 const { popup } = await setupVault(ctx);519 await lockVault(popup);520 const cleared = await popup.evaluate(() => {521 return typeof window.sessionStorage !== "undefined";522 });523 expect(cleared).toBeTruthy();524 await ctx.close();525 });526527 test("LSC05 - correct password still unlocks after wrong attempts", async () => {528 const { popup } = await setupVault(ctx);529 await lockVault(popup);530 for (let i = 0; i < 3; i++) {531 await popup.locator("#unlock-password").fill("wrongpassword");532 await popup.getByRole("button", { name: /unlock/i }).click();533 534 535 await expect(popup.locator("#unlock-password")).toBeVisible({ timeout: 15000 });536 await popup.waitForTimeout(500);537 }538 539 await unlockVault(popup);540 await ctx.close();541 });542543 test("LSC06 - lock requires user gesture", async () => {544 const { popup } = await setupVault(ctx);545 const lockBtn = popup.locator(UNLOCKED);546 await expect(lockBtn).toBeVisible();547 await ctx.close();548 });549});