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 115 const firstPos = parseInt(116 (await popup.locator(".peach-recovery-confirm-num").first().innerText()).replace(/\D/g, ""),117 10,118 );119 const firstWord = words[firstPos - 1]?.trim();120 if (!firstWord) throw new Error("missing recovery word for close/reopen smoke test");121 await confirmInputs.first().fill(firstWord);122123 await popup.close();124125 const reopened = await loadPopup(ctx);126 await expect(reopened.popup.locator(".lock-screen--recovery-display")).toBeVisible({ timeout: 5000 });127 await completeRecoveryConfirmation(reopened.popup);128129 await reopened.popup.close();130 await ctx.close();131 });132133 test("LS08 - correct password unlocks vault", async () => {134 const { popup } = await setupVault(ctx);135 await lockVault(popup);136 137 138 await unlockVault(popup);139 await ctx.close();140 });141142 test("LS09 - stays unlocked before the auto-lock timeout", async () => {143 const { popup } = await setupVault(ctx);144 145 146 147 await popup.waitForTimeout(6500);148 await expect(popup.locator(UNLOCKED)).toBeVisible({ timeout: 2000 });149 await ctx.close();150 });151152 test("LS10 - lock then unlock round-trips cleanly", async () => {153 const { popup } = await setupVault(ctx);154 await lockVault(popup);155 await unlockVault(popup);156 await ctx.close();157 });158159 test("LS11 - lock persists after navigation", async () => {160 const { popup, extId } = await setupVault(ctx);161 await lockVault(popup);162 await popup.goto(`chrome-extension://${extId}/src/popup/index.html`);163 await popup.waitForTimeout(500);164 await expect(popup.locator("#unlock-password")).toBeVisible();165 await ctx.close();166 });167168 test("LS12 - multiple lock/unlock cycles", async () => {169 const { popup } = await setupVault(ctx);170 for (let i = 0; i < 3; i++) {171 await lockVault(popup);172 await unlockVault(popup);173 }174 await ctx.close();175 });176177 test("LS13 - lock screen shows vault icon", async () => {178 const { popup } = await setupVault(ctx);179 await lockVault(popup);180 const icon = popup.locator("[class*='lock-hero']").first();181 const hasIcon = await icon.isVisible({ timeout: 2000 }).catch(() => false);182 expect(hasIcon).toBeTruthy();183 await ctx.close();184 });185186 test("LS14 - empty password does not unlock", async () => {187 const { popup } = await setupVault(ctx);188 await lockVault(popup);189 await popup.locator("#unlock-password").fill("");190 await popup.getByRole("button", { name: /unlock/i }).click();191 await popup.waitForTimeout(500);192 193 194 await expect(popup.locator("#unlock-password")).toBeVisible();195 await expect(popup.locator(UNLOCKED)).toHaveCount(0);196 await ctx.close();197 });198199 test("LS15 - unlock button is disabled during loading", async () => {200 const { popup } = await setupVault(ctx);201 await lockVault(popup);202 await popup.locator("#unlock-password").fill(VAULT_PASSWORD);203 const unlockBtn = popup.getByRole("button", { name: /unlock/i });204 await unlockBtn.click();205 await popup.waitForTimeout(300);206 await ctx.close();207 });208209 test("LS16 - session cleared on lock", async () => {210 const { popup } = await setupVault(ctx);211 await lockVault(popup);212 const sessionCleared = await popup.evaluate(() => {213 return typeof window !== "undefined";214 });215 expect(sessionCleared).toBeTruthy();216 await ctx.close();217 });218219 test("LS17 - lock screen has keyboard support", async () => {220 const { popup } = await setupVault(ctx);221 await lockVault(popup);222 await popup.locator("#unlock-password").focus();223 await popup.keyboard.type(VAULT_PASSWORD);224 await popup.keyboard.press("Enter");225 226 await expect(popup.locator(UNLOCKED)).toBeVisible({ timeout: 20000 });227 await ctx.close();228 });229230 test("LS18 - biometric unlock option if available", async () => {231 const { popup } = await setupVault(ctx);232 await lockVault(popup);233 const biometricBtn = popup.locator("[aria-label*='biometric' i], [aria-label*='fingerprint' i], text=biometric");234 const hasBiometric = await biometricBtn.isVisible({ timeout: 2000 }).catch(() => false);235 if (hasBiometric) expect(true).toBeTruthy();236 else expect(hasBiometric).toBeFalsy();237 await ctx.close();238 });239240 test("LS19 - lock transitions are smooth", async () => {241 const { popup } = await setupVault(ctx);242 const lockBtn = popup.locator(UNLOCKED);243 await expect(lockBtn).toBeVisible({ timeout: 15000 });244 245 246 const start = Date.now();247 await lockBtn.click();248 await expect(popup.locator("#unlock-password")).toBeVisible({ timeout: 5000 });249 expect(Date.now() - start).toBeLessThan(3000);250 await ctx.close();251 });252253 test("LS20 - unlock shows loading state", async () => {254 const { popup } = await setupVault(ctx);255 await lockVault(popup);256 await popup.locator("#unlock-password").fill(VAULT_PASSWORD);257 const unlockBtn = popup.getByRole("button", { name: /unlock/i });258 await unlockBtn.click();259 await popup.waitForTimeout(300);260 await ctx.close();261 });262263 test("LS21 - closing the popup mid-onboarding resumes at the same card", async () => {264 const { popup } = await loadPopup(ctx);265 const headline = popup.locator(".peach-onboarding-card-headline");266 await expect(headline).toContainText("Welcome to");267 await popup.locator(".peach-onboarding-next").click();268 await expect(headline).toContainText("Two keys.", { timeout: 3000 });269 270 await popup.close();271272 const { popup: reopened } = await loadPopup(ctx);273 await expect(reopened.locator(".peach-onboarding-card-headline")).toContainText("Two keys.", {274 timeout: 5000,275 });276 await ctx.close();277 });278279 test("LS22 - closing the popup from the create-vault screen resumes the create-vault screen", async () => {280 281 282 const { popup } = await loadPopup(ctx);283 const next = popup.locator(".peach-onboarding-next");284 for (let i = 0; i < 3; i++) {285 await next.click();286 await popup.waitForTimeout(350);287 }288 await expect(popup.locator("#master-password")).toBeVisible({ timeout: 5000 });289 await popup.close();290291 const { popup: reopened } = await loadPopup(ctx);292 293 await expect(reopened.locator("#master-password")).toBeVisible({ timeout: 5000 });294 await expect(reopened.locator(".peach-onboarding-card")).toHaveCount(0);295 await ctx.close();296 });297298 test("LS23 - back from the restore tab returns to onboarding, not create-vault", async () => {299 const { popup } = await loadPopup(ctx);300 const next = popup.locator(".peach-onboarding-next");301 await next.click();302 await popup.waitForTimeout(350);303 await next.click();304 await popup.waitForTimeout(350);305 await popup.locator(".peach-onboarding-choice:not(.peach-onboarding-choice--adopt)").click();306 await expect(popup.locator(".peach-restore-sources")).toBeVisible({ timeout: 5000 });307308 await popup.locator(".peach-restore-back").click();309310 await expect(popup.locator(".peach-onboarding-card")).toBeVisible({ timeout: 5000 });311 await expect(popup.locator(".peach-onboarding-card")).toContainText("Already using Peach?");312 await expect(popup.locator("#master-password")).toHaveCount(0);313 await ctx.close();314 });315316 test("LS25 - closing the popup on a restore source resumes that source, not the picker", async () => {317 const { popup } = await loadPopup(ctx);318 const next = popup.locator(".peach-onboarding-next");319 await next.click();320 await popup.waitForTimeout(350);321 await next.click();322 await popup.waitForTimeout(350);323 await popup.locator(".peach-onboarding-choice:not(.peach-onboarding-choice--adopt)").click();324 await expect(popup.locator(".peach-restore-sources")).toBeVisible({ timeout: 5000 });325 await popup.locator(".peach-restore-source", { hasText: "PeachScript" }).click();326 await expect(popup.locator(".peach-restore-script")).toBeVisible({ timeout: 5000 });327328 329 330 await popup.close();331 const { popup: reopened } = await loadPopup(ctx);332 await expect(reopened.locator(".peach-restore-script")).toBeVisible({ timeout: 5000 });333 await expect(reopened.locator(".peach-restore-sources")).toHaveCount(0);334 await ctx.close();335 });336337 test("LS24 - locked out before confirming recovery words: unlock routes back to the confirmation", async () => {338 const { popup } = await setupVault(ctx, VAULT_PASSWORD, { confirmRecovery: false });339 await popup.waitForSelector(".lock-screen--recovery-display", { timeout: 15000 });340 341 await popup.evaluate(() => chrome.runtime.sendMessage({ type: "LOCK" }));342 await popup.waitForTimeout(300);343 await popup.close();344345 const { popup: reopened } = await loadPopup(ctx);346 await reopened.waitForSelector("#unlock-password", { timeout: 15000 });347 await reopened.locator("#unlock-password").fill(VAULT_PASSWORD);348 await reopened.locator("#unlock-password").press("Enter");349350 351 await expect(reopened.locator(".lock-screen--recovery-display")).toBeVisible({ timeout: 15000 });352 await expect(reopened.locator("#unlock-password")).toHaveCount(0);353354 await reopened.waitForSelector(".peach-recovery-confirm-input:not([disabled])", { timeout: 20000 });355 const words = await reopened.locator(".peach-recovery-word-text").allTextContents();356 const fields = reopened.locator(".peach-recovery-confirm-field");357 const count = await fields.count();358 for (let i = 0; i < count; i++) {359 const field = fields.nth(i);360 const pos = parseInt((await field.locator(".peach-recovery-confirm-num").innerText()).replace(/\D/g, ""), 10);361 await field.locator(".peach-recovery-confirm-input").fill(words[pos - 1]!.trim());362 }363 await reopened.getByRole("button", { name: /i wrote them down/i }).click();364 await expect(reopened.locator(UNLOCKED)).toBeVisible({ timeout: 15000 });365 await ctx.close();366 });367});368369test.describe("Lock Screen - Auto-lock Configuration", () => {370 let ctx: BrowserContext;371 test.afterEach(async () => { await ctx.close(); });372 test.beforeEach(async () => { ctx = await makeContext(); });373374 375 376 377 378379 test("LAC01 - auto-lock defaults to 5 minutes", async () => {380 const { popup } = await setupVault(ctx);381 await openSettings(popup);382 await expect(autoLockSelect(popup)).toHaveValue("5");383 await ctx.close();384 });385386 test("LAC02 - auto-lock options available", async () => {387 const { popup } = await setupVault(ctx);388 await openSettings(popup);389 await expect(autoLockSelect(popup).locator("option")).toHaveCount(5);390 await ctx.close();391 });392393 test("LAC03 - lock on close option present", async () => {394 const { popup } = await setupVault(ctx);395 await openSettings(popup);396 await expect(397 popup.locator(".peach-settings-row", { hasText: "Lock on close" }),398 ).toHaveCount(1);399 await ctx.close();400 });401402 403 404 test("LAC04 - never auto-lock option available", async () => {405 const { popup } = await setupVault(ctx);406 await openSettings(popup);407 await expect(autoLockSelect(popup).locator('option[value="0"]')).toHaveText("Never");408 await ctx.close();409 });410411 test("LAC05 - 5 minute auto-lock option", async () => {412 const { popup } = await setupVault(ctx);413 await openSettings(popup);414 await expect(autoLockSelect(popup).locator('option[value="5"]')).toHaveText("5 minutes");415 await ctx.close();416 });417418 test("LAC06 - 15 minute auto-lock option", async () => {419 const { popup } = await setupVault(ctx);420 await openSettings(popup);421 await expect(autoLockSelect(popup).locator('option[value="15"]')).toHaveText("15 minutes");422 await ctx.close();423 });424425 test("LAC07 - 30 minute auto-lock option", async () => {426 427 const { popup } = await setupVault(ctx);428 await openSettings(popup);429 await expect(autoLockSelect(popup).locator('option[value="30"]')).toHaveText("30 minutes");430 await ctx.close();431 });432433 test("LAC08 - changing auto-lock persists across reload", async () => {434 const { popup, extId } = await setupVault(ctx);435 await openSettings(popup);436 437 await autoLockSelect(popup).selectOption("15", { force: true });438 await expect(autoLockSelect(popup)).toHaveValue("15");439440 441 await popup.goto(`chrome-extension://${extId}/src/popup/index.html`);442 await popup.waitForTimeout(1500);443 if (await popup.locator("#unlock-password").isVisible().catch(() => false)) {444 await unlockVault(popup);445 }446 await openSettings(popup);447 await expect(autoLockSelect(popup)).toHaveValue("15");448 await ctx.close();449 });450});451452test.describe("Lock Screen - Security", () => {453 let ctx: BrowserContext;454 test.afterEach(async () => { await ctx.close(); });455 test.beforeEach(async () => { ctx = await makeContext(); });456457 test("LSC01 - no sensitive data in lock screen DOM", async () => {458 const { popup } = await setupVault(ctx);459 await lockVault(popup);460 const html = await popup.content();461 expect(html).not.toContain(VAULT_PASSWORD);462 expect(html).not.toContain("argon2");463 await ctx.close();464 });465466 test("LSC02 - lock screen has CSP compliance", async () => {467 const { popup } = await setupVault(ctx);468 await lockVault(popup);469 const csp = await popup.evaluate(() => {470 const meta = document.querySelector('meta[http-equiv="Content-Security-Policy"]');471 return meta ? meta.getAttribute("content") : null;472 });473 if (csp) expect(csp).toContain("script-src");474 await ctx.close();475 });476477 test("LSC03 - session storage cleared on lock", async () => {478 const { popup } = await setupVault(ctx);479 await lockVault(popup);480 await popup.waitForTimeout(500);481 await ctx.close();482 });483484 test("LSC04 - memory cleared on lock", async () => {485 const { popup } = await setupVault(ctx);486 await lockVault(popup);487 const cleared = await popup.evaluate(() => {488 return typeof window.sessionStorage !== "undefined";489 });490 expect(cleared).toBeTruthy();491 await ctx.close();492 });493494 test("LSC05 - correct password still unlocks after wrong attempts", async () => {495 const { popup } = await setupVault(ctx);496 await lockVault(popup);497 for (let i = 0; i < 3; i++) {498 await popup.locator("#unlock-password").fill("wrongpassword");499 await popup.getByRole("button", { name: /unlock/i }).click();500 501 502 await expect(popup.locator("#unlock-password")).toBeVisible({ timeout: 15000 });503 await popup.waitForTimeout(500);504 }505 506 await unlockVault(popup);507 await ctx.close();508 });509510 test("LSC06 - lock requires user gesture", async () => {511 const { popup } = await setupVault(ctx);512 const lockBtn = popup.locator(UNLOCKED);513 await expect(lockBtn).toBeVisible();514 await ctx.close();515 });516});